From 3ba78dd893ab87f38ee40ec1d3e93974b1690a7c Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Sun, 24 Mar 2024 13:13:03 +0200 Subject: [PATCH 01/11] Random working stuff --- package.json | 2 + src/common/util/benevityCSVParser.ts | 121 ++++++ src/common/util/lazyWithPreload.ts | 21 + .../modals/BenevityImportDialog.tsx | 365 ++++++++++++++++++ .../modals/FileImportDialog.tsx | 12 + .../store/BenevityImportStore.ts | 124 ++++++ .../donations/dialogs/CreatePaymentDialog.tsx | 162 ++++++++ src/components/common/Carousel.tsx | 91 +++++ src/components/common/withLazyloadPreload.tsx | 23 ++ src/pages/index.tsx | 3 +- yarn.lock | 18 + 11 files changed, 941 insertions(+), 1 deletion(-) create mode 100644 src/common/util/benevityCSVParser.ts create mode 100644 src/common/util/lazyWithPreload.ts create mode 100644 src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx create mode 100644 src/components/admin/bank-transactions/modals/FileImportDialog.tsx create mode 100644 src/components/admin/bank-transactions/store/BenevityImportStore.ts create mode 100644 src/components/admin/donations/dialogs/CreatePaymentDialog.tsx create mode 100644 src/components/common/Carousel.tsx create mode 100644 src/components/common/withLazyloadPreload.tsx diff --git a/package.json b/package.json index 6cae1e8da..794084c80 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/common/util/benevityCSVParser.ts b/src/common/util/benevityCSVParser.ts new file mode 100644 index 000000000..be6fc542b --- /dev/null +++ b/src/common/util/benevityCSVParser.ts @@ -0,0 +1,121 @@ +import camelCase from 'lodash/camelCase' +import Papa from 'papaparse' + +/** + * Serialize Benevity CSV donation report to JavaScript Object + * @param csvString CSV string to serialize + * @returns + */ +export function BenevityCSVParser(csvString: string): TBenevityCSVParser { + const benevityObj = {} as TBenevityCSVParser + benevityObj.donations = [] + + const EXPECTED_CSV_SECTIONS = 4 + const SECTION_DELIMETER = '#-------------------------------------------,' + + //Some reports include redundant quotes which affects the end result of the parser + const csvWithoutQuotes = csvString.replace(/["']/gm, '') + + //Get index of Totals inside the string, and prepend SECTION_DELIMETER to split project donations the rest of data inside csv. + const END_OF_PROJECTS_SECTION = csvWithoutQuotes.lastIndexOf('Totals,') + const finalString = + csvWithoutQuotes.substring(0, END_OF_PROJECTS_SECTION) + + `${SECTION_DELIMETER}` + + csvWithoutQuotes.substring(END_OF_PROJECTS_SECTION) + + const csvArr = finalString.split(SECTION_DELIMETER).filter((line) => line !== '\n') + + if (csvArr.length !== EXPECTED_CSV_SECTIONS) { + throw new Error( + 'More or less sections found than expected.\nPlease check your file and try again or import benevity report manually ', + ) + } + + const donationSummary = csvArr[1].concat(csvArr[3]) + Papa.parse(donationSummary, { + skipEmptyLines: true, + step(row: Papa.ParseStepResult) { + const [key, value] = row.data as [string, string] + + if (!key || !value) return + const transformedKey = camelCase(key) as keyof TBenevityCSVParser + ;(benevityObj[transformedKey] as TBenevityCSVParser[typeof transformedKey]) = + value as TBenevityCSVParser[typeof transformedKey] + return benevityObj + }, + // complete(results: Papa.ParseResult) { + // console.log(results) + // return results + // }, + }) + + const projectsSummary = csvArr[2] + Papa.parse(projectsSummary, { + header: true, + skipEmptyLines: true, + dynamicTyping(field) { + return ( + field === 'merchantFee' || + field === 'causeSupportFee' || + field === 'matchAmount' || + field === 'totalDonationToBeAcknowledged' + ) + }, + transformHeader(header) { + return camelCase(header) + }, + complete: (result: Papa.ParseResult) => { + const test = result.data.map((result) => { + result['totalFee'] = result.merchantFee + result.causeSupportFee + result['totalAmount'] = result.totalDonationToBeAcknowledged + result.matchAmount + return result + }) + + benevityObj.donations = test + }, + }) + return benevityObj +} + +export type TBenevityCSVParser = { + charityName: string + charityId: string + periodEnding: string + currency: string + paymentMethod: string + disbursementId: string + totalDonationsGross: string + checkFee: string + netTotalPayment: number + transactionAmount: number + exchangeRate: number + donations: TBenevityDonation[] +} + +export type TBenevityDonation = { + company: string + project: string + donationDate: string + donorFirstName: string + donorLastName: string + email: string + address: string + city: string + stateProvince: string + postalCode: string + activity: string + comment: string + transactionId: string + donationFrequency: string + currency: string + projectRemoteId: string + source: string + reason: string + totalDonationToBeAcknowledged: number + matchAmount: number + causeSupportFee: number + merchantFee: number + feeComment: string + totalFee: number + totalAmount: number +} diff --git a/src/common/util/lazyWithPreload.ts b/src/common/util/lazyWithPreload.ts new file mode 100644 index 000000000..42a3d332a --- /dev/null +++ b/src/common/util/lazyWithPreload.ts @@ -0,0 +1,21 @@ +import React, { ComponentProps, ComponentType, LazyExoticComponent } from 'react' + +export type PreloadableComponent>> = + LazyExoticComponent & { + preload(): Promise + } + +function lazyWithPreload>>( + factory: () => Promise<{ default: T }>, +): PreloadableComponent { + const Component: Partial> = React.lazy(factory) + + Component.preload = async () => { + const LoadableComponent = await factory() + return LoadableComponent.default + } + + return Component as PreloadableComponent +} + +export default lazyWithPreload diff --git a/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx b/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx new file mode 100644 index 000000000..d32096459 --- /dev/null +++ b/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx @@ -0,0 +1,365 @@ +import { + Box, + Button, + Card, + CardContent, + Dialog, + Grid, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, +} from '@mui/material' +import React, { useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + CreatePaymentStore, + TImportType, + benevityDonationInitialValues, +} from '../store/BenevityImportStore' +import AddCircleIcon from '@mui/icons-material/AddCircle' + +import { observer } from 'mobx-react' +import { BenevityCSVParser, TBenevityCSVParser } from 'common/util/benevityCSVParser' + +import SubmitButton from 'components/common/form/SubmitButton' +import { Field, FieldArray, FieldProps, Form, Formik, useField, useFormikContext } from 'formik' +import { TranslatableField, translateError } from 'common/form/validation' +import EditIcon from '@mui/icons-material/Edit' + +type TModalSettings = { + onClose: () => void + onUpload: (data: TBenevityCSVParser) => void +} + +function BenevityImportDialog() { + const { t } = useTranslation() + const { setImportType } = CreatePaymentStore + + const handleImportTypeChange = (type: TImportType) => { + setImportType(type) + } + + return ( + <> + + Прикачване на CSV файл + + + {t('Създаване на дарения от Benevity, чрез CSV файл')} + + + + + + + ) +} + +export function FileImportDialog() { + const { isImportModalOpen, importType, setBenevityData } = CreatePaymentStore + const [isDragging, setIsDragging] = useState(false) + const inputFile = useRef(null) + + const onDragOver = (event: React.DragEvent) => { + event.preventDefault() + setIsDragging(true) + event.dataTransfer.dropEffect = 'copy' + } + const onDragLeave = (event: React.DragEvent) => { + event.preventDefault() + setIsDragging(false) + } + const onDrop = (event: React.DragEvent) => { + const fileReader = new FileReader() + event.preventDefault() + setIsDragging(false) + const file = event.dataTransfer.files[0] + fileReader.readAsText(file) + fileReader.onload = function () { + if (!fileReader.result) return + const csvToJSON = BenevityCSVParser(fileReader.result as string) + if (!csvToJSON) throw new Error('Something went wrong') + setBenevityData(csvToJSON) + } + } + const onClick = (event: React.MouseEvent) => { + inputFile.current?.click() + } + + const onChange = (event: React.ChangeEvent) => { + event.preventDefault() + setIsDragging(false) + + const fileReader = new FileReader() + const filelist = event.target.files + if (!filelist) return + const file = filelist[0] + if (file.type !== 'text/csv') + throw new Error('Unsupported file format. Only csv files are allowed') + fileReader.readAsText(file) + fileReader.onload = function () { + if (!fileReader.result) return + const csvToJSON = BenevityCSVParser(fileReader.result as string) + if (!csvToJSON) throw new Error('Something went wrong') + setBenevityData(csvToJSON) + } + } + + return ( + <> +
+
+ Провлачете файла в квадрата +
+
+ + + ) +} + +const BenevityInput = ({ name }: { name: string }) => { + 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(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, + ) => { + formikBlur(e) + toggleEdit() + } + return ( + 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: ( + <> + + + + + ), + }} + /> + ) +} + +const ExchangeRate = () => { + const { values } = useFormikContext() + + return Курс: {values.exchangeRate.toFixed(5)} +} + +const DonationsTable = () => { + const { values } = useFormikContext() + const [showAddButton, setShowAddButton] = useState(false) + const exchangeRate = values.transactionAmount / values.netTotalPayment || 0 + return ( + ( + <> + + setShowAddButton(true)} + onMouseLeave={() => setShowAddButton(false)}> + + + ID на транзакция + Кампания + Project Remote Id + Дарение(орг. валута) + Дарение(BGN) + Дарител име + Дарител фамилия + Дарител емайл + + + + {values.donations.map((donation, index) => { + return ( + + + + + + + + + + + + + + + ≈{(donation.totalAmount * exchangeRate).toFixed(2)} BGN + + + + + + + + + + + + ) + })} + {showAddButton && ( + + + + arrayHelper.push(benevityDonationInitialValues)}> + + Добави ново дарение + + + + + )} + +
+
+ + )} + /> + ) +} + +export function DonationImportSummary() { + const { benevityData, selectedRecord, hideImportModal } = CreatePaymentStore + benevityData['transactionAmount'] = Number(selectedRecord.id.substring(59, 66) ?? 0) + benevityData['exchangeRate'] = benevityData.transactionAmount / benevityData.netTotalPayment + + const onSubmit = (data: any) => { + console.log(data.donations) + } + + return ( + + +
+ + + + Получени средства(BGN): + + + + Превод валута: + + + + Изпратени средства(Нето): + + + + + + + Дарения по кампании: + + + + + + + + + + +
+
+
+ ) +} + +export default observer(BenevityImportDialog) diff --git a/src/components/admin/bank-transactions/modals/FileImportDialog.tsx b/src/components/admin/bank-transactions/modals/FileImportDialog.tsx new file mode 100644 index 000000000..281a00f57 --- /dev/null +++ b/src/components/admin/bank-transactions/modals/FileImportDialog.tsx @@ -0,0 +1,12 @@ +import { Dialog } from '@mui/material' +import React from 'react' +import { CreatePaymentStore } from '../store/BenevityImportStore' + +export default function FileImportDialog() { + const { isImportModalOpen, importType } = CreatePaymentStore + return ( + + FileImportDialog + + ) +} diff --git a/src/components/admin/bank-transactions/store/BenevityImportStore.ts b/src/components/admin/bank-transactions/store/BenevityImportStore.ts new file mode 100644 index 000000000..cab11ff3a --- /dev/null +++ b/src/components/admin/bank-transactions/store/BenevityImportStore.ts @@ -0,0 +1,124 @@ +import { action, makeObservable, observable } from 'mobx' +import { enableStaticRendering } from 'mobx-react' +import type { TBenevityCSVParser, TBenevityDonation } from 'common/util/benevityCSVParser' + +enableStaticRendering(typeof window === 'undefined') + +export type SelectedPaymentSource = 'none' | 'stripe' | 'benevity' +export type TImportType = 'none' | 'file' | 'manual' + +export const benevityInitialValues: TBenevityCSVParser = { + charityId: '', + charityName: '', + currency: '', + donations: [], + periodEnding: '', + paymentMethod: '', + disbursementId: '', + checkFee: '', + totalDonationsGross: '', + netTotalPayment: 0, + transactionAmount: 0, + exchangeRate: 0, +} + +export const benevityDonationInitialValues: TBenevityDonation = { + company: '', + project: '', + donationDate: '', + donorFirstName: '', + donorLastName: '', + email: '', + address: '', + city: '', + stateProvince: '', + postalCode: '', + activity: '', + comment: '', + transactionId: '', + donationFrequency: '', + currency: '', + projectRemoteId: '', + source: '', + reason: '', + totalDonationToBeAcknowledged: 0, + matchAmount: 0, + causeSupportFee: 0, + merchantFee: 0, + feeComment: '', + totalFee: 0, + totalAmount: 0, +} + +interface IBenevityStoreImpl { + isImportModalOpen: boolean + importType: TImportType + benevityData: TBenevityCSVParser + selectedRecord: Record<'id', string> + showImportModal: () => void + hideImportModal: () => void + setSelectedRecord: (record: Record<'id', string>) => void + setImportType: (type: TImportType) => void + setBenevityData: (data: TBenevityCSVParser) => void +} + +export class BenevityImportStoreImpl implements IBenevityStoreImpl { + isImportModalOpen = true + selectedRecord = { + id: '', + } + importType: TImportType = 'none' + paymentSource: SelectedPaymentSource = 'none' + benevityData: TBenevityCSVParser = benevityInitialValues + step = 0 + + constructor() { + makeObservable(this, { + isImportModalOpen: observable, + selectedRecord: observable, + paymentSource: observable, + importType: observable, + step: observable, + showImportModal: action, + setPaymentSource: action, + hideImportModal: action, + setImportType: action, + setBenevityData: action, + }) + } + + showImportModal = () => { + this.isImportModalOpen = true + } + + hideImportModal = () => { + this.isImportModalOpen = false + this.importType = 'none' + this.selectedRecord = { id: '' } + this.benevityData = benevityInitialValues + this.step = 0 + } + + setSelectedRecord = (record: typeof this.selectedRecord) => { + this.selectedRecord = record + } + + setPaymentSource = (source: SelectedPaymentSource) => { + this.paymentSource = source + } + + setStep = (step: number) => { + this.step = step + } + + setImportType = (type: TImportType) => { + this.importType = type + this.step = 1 + } + setBenevityData = (data: TBenevityCSVParser) => { + this.step = 2 + this.benevityData = data + } +} + +export const CreatePaymentStore = new BenevityImportStoreImpl() diff --git a/src/components/admin/donations/dialogs/CreatePaymentDialog.tsx b/src/components/admin/donations/dialogs/CreatePaymentDialog.tsx new file mode 100644 index 000000000..4f7b16dc3 --- /dev/null +++ b/src/components/admin/donations/dialogs/CreatePaymentDialog.tsx @@ -0,0 +1,162 @@ +import { Box, Button, Card, CardContent, Dialog, TextField, Typography } from '@mui/material' +import BenevityImportDialog, { + DonationImportSummary, +} from 'components/admin/bank-transactions/modals/BenevityImportDialog' +import { Form, Formik, useField, useFormikContext } from 'formik' +import React from 'react' +import * as yup from 'yup' +import { FileImportDialog } from 'components/admin/bank-transactions/modals/BenevityImportDialog' +import { + CreatePaymentStore, + SelectedPaymentSource, +} from 'components/admin/bank-transactions/store/BenevityImportStore' +import { useTranslation } from 'react-i18next' +import { observer } from 'mobx-react' +import { TranslatableField, translateError } from 'common/form/validation' + +const stripeInputValidation = yup.object({ + extPaymentIntentId: yup.string().required().matches(/^pi_/, 'Невалиден номер на Страйп'), +}) + +const benevityInputValidation = yup.object({ + transactionId: yup.string().required(), +}) + +function StripeInputDialog() { + const [field, meta] = useField('extPaymentIntentId') + const { handleSubmit } = useFormikContext() + const { t } = useTranslation() + const helperText = translateError(meta.error as TranslatableField, t) + + const handleSubmitS = (e: any) => { + handleSubmit() + } + return ( + <> + + + {t('Въведете номер на плащане от Страйп')} + + + + + Платещените номера от Страйп започват с: +
pi_ +
+ +
+ + ) +} + +function PaymentTypeSelectDialog() { + const { setPaymentSource } = CreatePaymentStore + const { t } = useTranslation('') + const handleSubmit = (source: SelectedPaymentSource) => { + setPaymentSource(source) + } + return ( + <> + + Ръчно добавяне на плащане + + + {t('')} + + + + + + + ) +} + +function BenevityManualImport() { + const { t } = useTranslation('') + const [field, meta] = useField('transactionId') + const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' + return ( + <> + + + {t('Въведете ID от Benevity или ID на банкова транзация')} + + + + + + + ) +} + +type Validation = yup.InferType +type Steps = { + [key in SelectedPaymentSource]: { + component: React.JSX.Element + validation?: yup.SchemaOf + }[] +} + +function CreatePaymentDialog() { + const { isImportModalOpen, hideImportModal, paymentSource, importType, step } = CreatePaymentStore + + const steps: Steps = { + none: [{ component: }], + stripe: [ + { + component: , + validation: stripeInputValidation, + }, + ], + benevity: [ + { + component: , + }, + { + component: importType === 'file' ? : , + validation: importType === 'file' ? undefined : benevityInputValidation, + }, + { + component: , + }, + ], + } + return ( + + + + { + console.log(values, helpers) + }} + initialValues={{}} + validationSchema={steps[paymentSource][step].validation}> +
{steps[paymentSource][step].component}
+
+
+
+
+ ) +} + +export default observer(CreatePaymentDialog) diff --git a/src/components/common/Carousel.tsx b/src/components/common/Carousel.tsx new file mode 100644 index 000000000..0f79733b1 --- /dev/null +++ b/src/components/common/Carousel.tsx @@ -0,0 +1,91 @@ +import 'slick-carousel/slick/slick-theme.css' +import 'slick-carousel/slick/slick.css' +import { useEffect, useRef } from 'react' +import Slider from 'react-slick' +import { styled } from '@mui/material/styles' + +import type { InnerSlider, Settings } from 'react-slick' + +type Props = Settings & { + children: React.JSX.Element[] | React.JSX.Element | undefined +} + +type TInnerSlider = InnerSlider & { + props: Settings +} + +interface InternalSlider extends Slider { + innerSlider: TInnerSlider +} + +const SliderStyled = styled(Slider)(({ theme }) => ({ + '.slick-list': { + paddingBottom: theme.spacing(3), + transition: 'visibility 2000ms', + }, + + '.slick-track': { + transition: 'visibility 3000ms', + }, + + '.slick-dots li button::before': { + fontSize: theme.typography.pxToRem(10), + color: '#D9D9D9', + opacity: 1, + }, + + '.slick-dots li.slick-active button::before': { + fontSize: theme.typography.pxToRem(10), + color: '#B0E5FF', + opacity: 1, + }, + + '.slick-slide[aria-hidden=false]': { + visibility: 'visible', + }, + + '.slick-slide[aria-hidden=true]': { + visibility: 'hidden', + animation: 'fadeIn 400ms', + }, + + '@keyframes fadeIn': { + from: { visibility: 'visible' }, + to: { visibility: 'hidden' }, + }, +})) + +export default function Carousel({ children, ...props }: Props) { + const sliderRef = useRef(null) + const timerRef = useRef(null) + + // Clear setTimeout instance if created + useEffect(() => { + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current) + } + } + }, []) + + const afterChange = (currentSlide: number) => { + const totalChilds = (sliderRef.current?.props?.children as []).length + const isLastSlide = + totalChilds - (sliderRef.current?.innerSlider.props.slidesToShow as number) === currentSlide + if ( + sliderRef.current?.props.infinite === false && + sliderRef.current?.props.autoplay === true && + isLastSlide + ) { + timerRef.current = setTimeout(() => { + sliderRef.current?.slickGoTo(0) + }, sliderRef.current?.innerSlider?.props.autoplaySpeed) + } + } + + return ( + + {children} + + ) +} diff --git a/src/components/common/withLazyloadPreload.tsx b/src/components/common/withLazyloadPreload.tsx new file mode 100644 index 000000000..af80955b4 --- /dev/null +++ b/src/components/common/withLazyloadPreload.tsx @@ -0,0 +1,23 @@ +import lazyWithPreload from 'common/util/lazyWithPreload' +import { Suspense, useState } from 'react' + +const RenderNotificationModal = lazyWithPreload( + () => import('components/client/notifications/GeneralSubscribeModal'), +) +export default function withLazyloadSubscribtionModal( + WrappedComponent: React.ComponentType, +) { + const [open, setOpen] = useState(false) + const onClick = () => { + setOpen((prev) => !prev) + } + const onMouseOver = () => { + RenderNotificationModal.preload() + } + return (props: T) => ( + <> + + {open && } + + ) +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 224f6550f..5b825245d 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -9,6 +9,7 @@ import { CampaignResponse } from 'gql/campaigns' import { endpoints } from 'service/apiEndpoints' import { authOptions } from './api/auth/[...nextauth]' +import CreatePaymentDialog from 'components/admin/donations/dialogs/CreatePaymentDialog' export const getServerSideProps: GetServerSideProps<{ session: Session | null @@ -37,4 +38,4 @@ export const getServerSideProps: GetServerSideProps<{ } } -export default IndexPage +export default CreatePaymentDialog diff --git a/yarn.lock b/yarn.lock index d9b4c5a6c..1f825d25c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4034,6 +4034,15 @@ __metadata: languageName: node linkType: hard +"@types/papaparse@npm:^5": + version: 5.3.14 + resolution: "@types/papaparse@npm:5.3.14" + dependencies: + "@types/node": "*" + checksum: fbf942ed92179eeb824d4e544cc701468157a4ce3f6f668f8b17692d9886fea92ccff5e56965615ff64f049efa01ff95ddb7d30c67e0186bc802a6cc8ef26e63 + languageName: node + linkType: hard + "@types/parse-json@npm:^4.0.0": version: 4.0.0 resolution: "@types/parse-json@npm:4.0.0" @@ -11538,6 +11547,13 @@ __metadata: languageName: node linkType: hard +"papaparse@npm:^5.4.1": + version: 5.4.1 + resolution: "papaparse@npm:5.4.1" + checksum: fc9e52f7158dca3517c229e3309065b1ab5da6c7194572fba4f31ff138bc43e3c91182cc40365cc828f97fe10d0aca416068fd731661058bea0f69ddb84a411a + languageName: node + linkType: hard + "parchment@npm:^1.1.2, parchment@npm:^1.1.4": version: 1.1.4 resolution: "parchment@npm:1.1.4" @@ -11750,6 +11766,7 @@ __metadata: "@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 @@ -11791,6 +11808,7 @@ __metadata: next-i18next: ^14.0.3 next-sitemap: ^3.1.52 nookies: ^2.5.2 + papaparse: ^5.4.1 prettier: 2.3.0 quill-blot-formatter: ^1.0.5 quill-html-edit-button: ^2.2.12 From d53f1399f7bcd828ab640e2f14e992bf55a29069 Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Wed, 27 Mar 2024 19:01:33 +0200 Subject: [PATCH 02/11] commit --- .../donations/dialogs/CreatePaymentDialog.tsx | 14 ++- .../stripe/StripeCreatePaymentDialog.tsx | 119 ++++++++++++++++++ src/pages/index.tsx | 1 + src/service/apiEndpoints.ts | 6 + 4 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx diff --git a/src/components/admin/donations/dialogs/CreatePaymentDialog.tsx b/src/components/admin/donations/dialogs/CreatePaymentDialog.tsx index 4f7b16dc3..a1eb75526 100644 --- a/src/components/admin/donations/dialogs/CreatePaymentDialog.tsx +++ b/src/components/admin/donations/dialogs/CreatePaymentDialog.tsx @@ -13,6 +13,8 @@ import { import { useTranslation } from 'react-i18next' import { observer } from 'mobx-react' import { TranslatableField, translateError } from 'common/form/validation' +import { UseMutationResult, useMutation } from '@tanstack/react-query' +import StripeCreatePaymentDialog from './stripe/StripeCreatePaymentDialog' const stripeInputValidation = yup.object({ extPaymentIntentId: yup.string().required().matches(/^pi_/, 'Невалиден номер на Страйп'), @@ -85,7 +87,7 @@ function PaymentTypeSelectDialog() { ) } - +// const stripeMutation = useMutation() function BenevityManualImport() { const { t } = useTranslation('') const [field, meta] = useField('transactionId') @@ -113,11 +115,13 @@ type Steps = { [key in SelectedPaymentSource]: { component: React.JSX.Element validation?: yup.SchemaOf + mutation?: UseMutationResult }[] } function CreatePaymentDialog() { - const { isImportModalOpen, hideImportModal, paymentSource, importType, step } = CreatePaymentStore + const { isImportModalOpen, hideImportModal, paymentSource, importType, step, setStep } = + CreatePaymentStore const steps: Steps = { none: [{ component: }], @@ -126,6 +130,10 @@ function CreatePaymentDialog() { component: , validation: stripeInputValidation, }, + { + component: , + validation: stripeInputValidation, + }, ], benevity: [ { @@ -147,7 +155,7 @@ function CreatePaymentDialog() { { - console.log(values, helpers) + setStep(step + 1) }} initialValues={{}} validationSchema={steps[paymentSource][step].validation}> diff --git a/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx b/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx new file mode 100644 index 000000000..6fd2b8940 --- /dev/null +++ b/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx @@ -0,0 +1,119 @@ +import { + Button, + Grid, + Tab, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from '@mui/material' +import { useMutation, useQuery } from '@tanstack/react-query' +import { AxiosResponse } from 'axios' +import { money } from 'common/util/money' +import CenteredSpinner from 'components/common/CenteredSpinner' +import { useField } from 'formik' +import { TPaymentResponse } from 'gql/donations' +import { PaymentStatus } from 'gql/donations.enums' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { apiClient } from 'service/apiClient' +import { endpoints } from 'service/apiEndpoints' + +type Reference = { + netAmount: number + status: PaymentStatus +} + +type GetStripeChargeResponse = { + stripe: Reference + internal: Reference +} + +function useGetStripeChargeFromPID(stripeId: string) { + return useQuery([ + endpoints.payments.referenceStripeWithInternal(stripeId).url, + ]) +} + +type SynchronizeStripeWithInternalProps = { + data: GetStripeChargeResponse + id: string +} + +type SynchronizeWithStripeInput = { + id: string + stripe: Reference + internal: Reference +} + +function synchronizeWithStrip(data: any) { + return apiClient.patch(endpoints.payments.synchronizeWithStripe(data.id).url, data) +} + +const useSynchronizeWithStripeMutation = () => { + return useMutation, unknown, SynchronizeWithStripeInput>( + [endpoints.payments.synchronizeWithStripe], + { mutationFn: synchronizeWithStrip }, + ) +} + +function SynchronizeStripeWithInternal({ data, id }: SynchronizeStripeWithInternalProps) { + const { t } = useTranslation() + const stripeMutation = useSynchronizeWithStripeMutation() + const handleSubmit = () => { + const mutateData: SynchronizeWithStripeInput = { + id: id, + ...data, + } + stripeMutation.mutate(mutateData) + } + return ( + + + + Намерено плащане + + + + + Плащане с номер{' '} + + {id} + {' '} + беше намерено. Желате ли да синхронизирате вътрешните данни със Страйп? + + + + + + + База данни на Страйп + Вътрещна база данни + + + Сума + {money(data.stripe.netAmount)} + {money(data.internal.netAmount)} + + + Статус + {t('profile:donations.status.' + data.stripe.status)} + {t('profile:donations.status.' + data.internal.status)} + + + + + + ) +} + +export default function StripeCreatePaymentDialog() { + const [field] = useField('extPaymentIntentId') + const data = useGetStripeChargeFromPID(field.value) + if (data.isLoading) return + if (data.isError) return + return +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 5b825245d..5fe96a9fa 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -31,6 +31,7 @@ export const getServerSideProps: GetServerSideProps<{ 'campaigns', 'validation', 'auth', + 'profile', ])), session, dehydratedState: dehydrate(client), diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index b4fad72e2..12f36650a 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -144,6 +144,12 @@ export const endpoints = { getPayment: (id: string) => { return { url: `/donation/payments/${id}`, method: 'GET' } }, + referenceStripeWithInternal: (id: string) => { + return { url: `/donation/stripe/${id}`, method: 'GET' } + }, + synchronizeWithStripe: (id: string) => { + return { url: `/donation/${id}/synchronize-with-stripe`, method: 'PATCH' } + }, }, donation: { prices: { url: '/donation/prices', method: 'GET' }, From 17715797cd8838c5cf1ffe57e2b3ec5e7583b54e Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Tue, 2 Apr 2024 12:18:27 +0300 Subject: [PATCH 03/11] commit2 --- package.json | 2 +- .../stripe/StripeCreatePaymentDialog.tsx | 46 ++++++++++++++----- src/gql/donations.d.ts | 1 + src/service/apiEndpoints.ts | 2 +- yarn.lock | 10 ++-- 5 files changed, 42 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 794084c80..10bfc22d2 100644 --- a/package.json +++ b/package.json @@ -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.0", diff --git a/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx b/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx index 6fd2b8940..d4159458b 100644 --- a/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx +++ b/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx @@ -11,14 +11,16 @@ import { import { useMutation, useQuery } from '@tanstack/react-query' import { AxiosResponse } from 'axios' import { money } from 'common/util/money' +import { stripeFeeCalculator } from 'components/client/one-time-donation/helpers/stripe-fee-calculator' import CenteredSpinner from 'components/common/CenteredSpinner' import { useField } from 'formik' import { TPaymentResponse } from 'gql/donations' -import { PaymentStatus } from 'gql/donations.enums' +import { CardRegion, PaymentStatus } from 'gql/donations.enums' import React from 'react' import { useTranslation } from 'react-i18next' import { apiClient } from 'service/apiClient' import { endpoints } from 'service/apiEndpoints' +import Stripe from 'stripe' type Reference = { netAmount: number @@ -26,8 +28,9 @@ type Reference = { } type GetStripeChargeResponse = { - stripe: Reference - internal: Reference + stripe: Stripe.Charge + internal?: TPaymentResponse + region: CardRegion } function useGetStripeChargeFromPID(stripeId: string) { @@ -41,10 +44,8 @@ type SynchronizeStripeWithInternalProps = { id: string } -type SynchronizeWithStripeInput = { +type SynchronizeWithStripeInput = GetStripeChargeResponse & { id: string - stripe: Reference - internal: Reference } function synchronizeWithStrip(data: any) { @@ -92,14 +93,35 @@ function SynchronizeStripeWithInternal({ data, id }: SynchronizeStripeWithIntern Вътрещна база данни - Сума - {money(data.stripe.netAmount)} - {money(data.internal.netAmount)} + Статус + + {t( + 'profile:donations.status.' + data.stripe.refunded ? 'refund' : data.stripe.status, + )} + + {t('profile:donations.status.' + data.internal?.status)} - Статус - {t('profile:donations.status.' + data.stripe.status)} - {t('profile:donations.status.' + data.internal.status)} + Сума(бруто) + {money(data.stripe.amount)} + {money(data.internal?.chargedAmount ?? 0)} + + + Сума(нето) + + {money(data.stripe.amount - stripeFeeCalculator(data.stripe.amount, data.region))} + + {money(data.internal?.amount ?? 0)} + + + Дарител(име) + {data.stripe.billing_details.name} + {data.internal?.billingName ?? 'N/A'} + + + Дарител(име) + {data.stripe.billing_details.email} + {data.internal?.billingEmail ?? 'N/A'} diff --git a/src/gql/donations.d.ts b/src/gql/donations.d.ts index 5a22ad562..c0b600fa2 100644 --- a/src/gql/donations.d.ts +++ b/src/gql/donations.d.ts @@ -40,6 +40,7 @@ export type TPaymentResponse = { provider: PaymentProvider extCustomerId: string amount: number + chargedAmount: number currency: Currency extPaymentIntentId: string extPaymentMethodId: string diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index 12f36650a..5da91da9b 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -148,7 +148,7 @@ export const endpoints = { return { url: `/donation/stripe/${id}`, method: 'GET' } }, synchronizeWithStripe: (id: string) => { - return { url: `/donation/${id}/synchronize-with-stripe`, method: 'PATCH' } + return { url: `/donation/${id}/synch-with-stripe`, method: 'PATCH' } }, }, donation: { diff --git a/yarn.lock b/yarn.lock index 1f825d25c..01441c50d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3660,10 +3660,10 @@ __metadata: languageName: node linkType: hard -"@stripe/stripe-js@npm:^1.46.0": - version: 1.54.1 - resolution: "@stripe/stripe-js@npm:1.54.1" - checksum: eb54054edea3d3f9a8c18e8442cc05b46f7afbe5bd85863121737f268875d30aab6de16ee84d467853ad9d983bf69f922f45daeee6f13134ca138f7e862c9eb4 +"@stripe/stripe-js@npm:^3.1.0": + version: 3.1.0 + resolution: "@stripe/stripe-js@npm:3.1.0" + checksum: a32e5d3d6cd7bd7130e0d2f15312bdae83f568125407e461c033d726c44503bd00aabe8d6d593a6329d4a10b8a325440ce947cec21b685f260ee3f01f4597ec8 languageName: node linkType: hard @@ -11756,7 +11756,7 @@ __metadata: "@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 "@testing-library/jest-dom": ^5.16.5 "@testing-library/react": ^13.4.0 From a70d9e845ec7bc07e314c7b4b7c0f139ccb01d61 Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Thu, 4 Apr 2024 16:02:28 +0300 Subject: [PATCH 04/11] new-commig --- src/common/util/benevityCSVParser.ts | 4 +- .../modals/BenevityImportDialog.tsx | 89 +++++++++---------- .../stripe/StripeCreatePaymentDialog.tsx | 1 - src/service/apiEndpoints.ts | 1 + 4 files changed, 45 insertions(+), 50 deletions(-) diff --git a/src/common/util/benevityCSVParser.ts b/src/common/util/benevityCSVParser.ts index be6fc542b..b8b507b46 100644 --- a/src/common/util/benevityCSVParser.ts +++ b/src/common/util/benevityCSVParser.ts @@ -65,13 +65,13 @@ export function BenevityCSVParser(csvString: string): TBenevityCSVParser { return camelCase(header) }, complete: (result: Papa.ParseResult) => { - const test = result.data.map((result) => { + const donationsWithCalculation = result.data.map((result) => { result['totalFee'] = result.merchantFee + result.causeSupportFee result['totalAmount'] = result.totalDonationToBeAcknowledged + result.matchAmount return result }) - benevityObj.donations = test + benevityObj.donations = donationsWithCalculation }, }) return benevityObj diff --git a/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx b/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx index d32096459..143acfe69 100644 --- a/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx +++ b/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx @@ -28,15 +28,10 @@ import { observer } from 'mobx-react' import { BenevityCSVParser, TBenevityCSVParser } from 'common/util/benevityCSVParser' import SubmitButton from 'components/common/form/SubmitButton' -import { Field, FieldArray, FieldProps, Form, Formik, useField, useFormikContext } from 'formik' +import { FieldArray, Form, Formik, useField, useFormikContext } from 'formik' import { TranslatableField, translateError } from 'common/form/validation' import EditIcon from '@mui/icons-material/Edit' -type TModalSettings = { - onClose: () => void - onUpload: (data: TBenevityCSVParser) => void -} - function BenevityImportDialog() { const { t } = useTranslation() const { setImportType } = CreatePaymentStore @@ -66,9 +61,10 @@ function BenevityImportDialog() { } export function FileImportDialog() { - const { isImportModalOpen, importType, setBenevityData } = CreatePaymentStore + const { setBenevityData } = CreatePaymentStore const [isDragging, setIsDragging] = useState(false) const inputFile = useRef(null) + const [field, meta, { setValue }] = useField('benevityData') const onDragOver = (event: React.DragEvent) => { event.preventDefault() @@ -90,6 +86,7 @@ export function FileImportDialog() { const csvToJSON = BenevityCSVParser(fileReader.result as string) if (!csvToJSON) throw new Error('Something went wrong') setBenevityData(csvToJSON) + setValue(csvToJSON.disbursementId) } } const onClick = (event: React.MouseEvent) => { @@ -158,7 +155,7 @@ export function FileImportDialog() { ) } -const BenevityInput = ({ name }: { name: string }) => { +const BenevityInput = ({ name, currency }: { name: string; currency?: string }) => { const { t } = useTranslation() const [editable, setEditable] = useState(false) const [field, meta] = useField(name) @@ -206,6 +203,7 @@ const BenevityInput = ({ name }: { name: string }) => { }, endAdornment: ( <> + {currency && {currency}} @@ -272,7 +270,10 @@ const DonationsTable = () => { - + ≈{(donation.totalAmount * exchangeRate).toFixed(2)} BGN @@ -318,47 +319,41 @@ export function DonationImportSummary() { benevityData['exchangeRate'] = benevityData.transactionAmount / benevityData.netTotalPayment const onSubmit = (data: any) => { - console.log(data.donations) + console.log(data) } return ( - - -
- - - - Получени средства(BGN): - - - - Превод валута: - - - - Изпратени средства(Нето): - - - - - - - Дарения по кампании: - - - - - - - - - - -
-
-
+ + + + Получени средства(BGN): + + + + Превод валута: + + + + Изпратени средства(Нето): + + + + + + + Дарения по кампании: + + + + + + + + + + ) } diff --git a/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx b/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx index d4159458b..e2e078d09 100644 --- a/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx +++ b/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx @@ -1,7 +1,6 @@ import { Button, Grid, - Tab, TableCell, TableContainer, TableHead, diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index 5da91da9b..8271f2a26 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -247,6 +247,7 @@ export const endpoints = { editPaymentRef: (id: string) => { url: `/bank-transaction/${id}/edit-ref`, method: 'PUT' }, rerunDates: { url: '/bank-transaction/rerun-dates', method: 'POST' }, + getTransaction: (id: string) => { url: `/bank-transaction/${id}`, method: 'GET' }, }, documents: { documentsList: { url: '/document', method: 'GET' }, From 76946c5ef0b006cf189a2f3fc93aaea9d7a16996 Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Tue, 9 Apr 2024 12:48:22 +0300 Subject: [PATCH 05/11] commit3 --- src/common/util/benevityCSVParser.ts | 36 +- .../modals/BenevityImportDialog.tsx | 324 ++++++++++++------ .../donations/dialogs/CreatePaymentDialog.tsx | 48 ++- src/service/apiEndpoints.ts | 8 +- 4 files changed, 284 insertions(+), 132 deletions(-) diff --git a/src/common/util/benevityCSVParser.ts b/src/common/util/benevityCSVParser.ts index b8b507b46..8a8e8c668 100644 --- a/src/common/util/benevityCSVParser.ts +++ b/src/common/util/benevityCSVParser.ts @@ -6,9 +6,10 @@ import Papa from 'papaparse' * @param csvString CSV string to serialize * @returns */ + export function BenevityCSVParser(csvString: string): TBenevityCSVParser { const benevityObj = {} as TBenevityCSVParser - benevityObj.donations = [] + benevityObj.donations = [] as TBenevityDonation[] const EXPECTED_CSV_SECTIONS = 4 const SECTION_DELIMETER = '#-------------------------------------------,' @@ -32,22 +33,21 @@ export function BenevityCSVParser(csvString: string): TBenevityCSVParser { } const donationSummary = csvArr[1].concat(csvArr[3]) - Papa.parse(donationSummary, { - skipEmptyLines: true, - step(row: Papa.ParseStepResult) { - const [key, value] = row.data as [string, string] + Papa.parse>( + donationSummary, + { + skipEmptyLines: true, + dynamicTyping: true, + step(row: Papa.ParseStepResult) { + const [key, value] = row.data as [string, string | number] - if (!key || !value) return - const transformedKey = camelCase(key) as keyof TBenevityCSVParser - ;(benevityObj[transformedKey] as TBenevityCSVParser[typeof transformedKey]) = - value as TBenevityCSVParser[typeof transformedKey] - return benevityObj + if (!key || !value) return + const transformedKey = camelCase(key) as keyof TBenevityCSVParser + + ;(benevityObj[transformedKey] as TBenevityCSVParser[typeof transformedKey]) = value + }, }, - // complete(results: Papa.ParseResult) { - // console.log(results) - // return results - // }, - }) + ) const projectsSummary = csvArr[2] Papa.parse(projectsSummary, { @@ -66,8 +66,9 @@ export function BenevityCSVParser(csvString: string): TBenevityCSVParser { }, complete: (result: Papa.ParseResult) => { const donationsWithCalculation = result.data.map((result) => { - result['totalFee'] = result.merchantFee + result.causeSupportFee - result['totalAmount'] = result.totalDonationToBeAcknowledged + result.matchAmount + const totalFee = result.merchantFee + result.causeSupportFee + result['totalFee'] = totalFee + result['totalAmount'] = result.totalDonationToBeAcknowledged + result.matchAmount - totalFee return result }) @@ -86,6 +87,7 @@ export type TBenevityCSVParser = { disbursementId: string totalDonationsGross: string checkFee: string + note: string netTotalPayment: number transactionAmount: number exchangeRate: number diff --git a/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx b/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx index 143acfe69..031d56192 100644 --- a/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx +++ b/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx @@ -3,6 +3,7 @@ import { Button, Card, CardContent, + CircularProgress, Dialog, Grid, IconButton, @@ -21,16 +22,32 @@ import { CreatePaymentStore, TImportType, benevityDonationInitialValues, + benevityInitialValues, } from '../store/BenevityImportStore' import AddCircleIcon from '@mui/icons-material/AddCircle' import { observer } from 'mobx-react' -import { BenevityCSVParser, TBenevityCSVParser } from 'common/util/benevityCSVParser' +import { + BenevityCSVParser, + TBenevityCSVParser, + TBenevityDonation, +} from 'common/util/benevityCSVParser' import SubmitButton from 'components/common/form/SubmitButton' import { FieldArray, Form, Formik, useField, useFormikContext } from 'formik' import { TranslatableField, translateError } from 'common/form/validation' import EditIcon from '@mui/icons-material/Edit' +import { useQuery } from '@tanstack/react-query' +import { endpoints } from 'service/apiEndpoints' +import { useSession } from 'next-auth/react' +import { authQueryFnFactory } from 'service/restRequests' +import { BankTransactionsInput } from 'gql/bank-transactions' +import * as yup from 'yup' +import { fromMoney, moneyPublic } from 'common/util/money' +import { useCampaignList } from 'common/hooks/campaigns' +import { v5 as uuidv5 } from 'uuid' +import { BenevityRequest } from 'components/admin/donations/dialogs/CreatePaymentDialog' +import { Delete } from '@mui/icons-material' function BenevityImportDialog() { const { t } = useTranslation() @@ -64,6 +81,7 @@ export function FileImportDialog() { const { setBenevityData } = CreatePaymentStore const [isDragging, setIsDragging] = useState(false) const inputFile = useRef(null) + const submitButtonRef = useRef(null) const [field, meta, { setValue }] = useField('benevityData') const onDragOver = (event: React.DragEvent) => { @@ -86,7 +104,7 @@ export function FileImportDialog() { const csvToJSON = BenevityCSVParser(fileReader.result as string) if (!csvToJSON) throw new Error('Something went wrong') setBenevityData(csvToJSON) - setValue(csvToJSON.disbursementId) + setValue(csvToJSON) } } const onClick = (event: React.MouseEvent) => { @@ -108,7 +126,8 @@ export function FileImportDialog() { if (!fileReader.result) return const csvToJSON = BenevityCSVParser(fileReader.result as string) if (!csvToJSON) throw new Error('Something went wrong') - setBenevityData(csvToJSON) + setValue(csvToJSON) + submitButtonRef.current?.click() } } @@ -151,16 +170,26 @@ export function FileImportDialog() { accept=".csv" onChange={onChange} /> + + ) } -type Validation = yup.InferType +export type BenevityRequest = { + amount: number + extPaymentIntentId: string + exchangeRate: number + benevityData: Pick +} + +type Validation = yup.InferType< + typeof stripeInputValidation | typeof benevityValidation | typeof benevityInputValidation +> + type Steps = { [key in SelectedPaymentSource]: { component: React.JSX.Element - validation?: yup.SchemaOf - mutation?: UseMutationResult + validation?: yup.SchemaOf | null + mutation?: UseMutationResult, unknown, BenevityRequest> }[] } @@ -123,6 +139,12 @@ function CreatePaymentDialog() { const { isImportModalOpen, hideImportModal, paymentSource, importType, step, setStep } = CreatePaymentStore + const benevityMutation = useMutation, unknown, BenevityRequest>({ + mutationFn: async (data) => { + return await apiClient.post(endpoints.payments.createFromBeneivty.url, data) + }, + }) + const steps: Steps = { none: [{ component: }], stripe: [ @@ -138,24 +160,32 @@ function CreatePaymentDialog() { benevity: [ { component: , + validation: null, }, { component: importType === 'file' ? : , - validation: importType === 'file' ? undefined : benevityInputValidation, + validation: importType === 'file' ? null : benevityInputValidation, }, { component: , + validation: benevityValidation, + mutation: benevityMutation, }, ], } return ( - - + + { - setStep(step + 1) + onSubmit={async (values, helpers) => { + if (step < steps[paymentSource].length - 1) { + setStep(step + 1) + } + if (step === steps[paymentSource].length - 1) { + steps[paymentSource][step].mutation?.mutate(values as BenevityRequest) + } }} initialValues={{}} validationSchema={steps[paymentSource][step].validation}> diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index 8271f2a26..c1aff9e0e 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -147,9 +147,11 @@ export const endpoints = { referenceStripeWithInternal: (id: string) => { return { url: `/donation/stripe/${id}`, method: 'GET' } }, - synchronizeWithStripe: (id: string) => { - return { url: `/donation/${id}/synch-with-stripe`, method: 'PATCH' } + synchronizeWithStripe: { + url: `/donation/create-update-stripe-payment`, + method: 'PUT', }, + createFromBeneivty: { url: `/donation/import/benevity`, method: 'POST' }, }, donation: { prices: { url: '/donation/prices', method: 'GET' }, @@ -247,7 +249,7 @@ export const endpoints = { editPaymentRef: (id: string) => { url: `/bank-transaction/${id}/edit-ref`, method: 'PUT' }, rerunDates: { url: '/bank-transaction/rerun-dates', method: 'POST' }, - getTransaction: (id: string) => { url: `/bank-transaction/${id}`, method: 'GET' }, + getTransactionById: (id: string) => { url: `/bank-transaction/${id}`, method: 'GET' }, }, documents: { documentsList: { url: '/document', method: 'GET' }, From f0e018033e0117b32341ee3888678c346564768a Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Tue, 25 Jun 2024 15:44:05 +0300 Subject: [PATCH 06/11] refactor: Restructure the create payment modal - Added button in admin's Payment section to create new payment from source, stripe or benevity - Restructured the modal file-structure --- src/common/hooks/bank-transactions.ts | 10 +- src/common/hooks/donation.ts | 11 +- .../modals/BenevityImportDialog.tsx | 478 ------------------ .../store/BenevityImportStore.ts | 124 ----- .../donations/dialogs/CreatePaymentDialog.tsx | 200 -------- .../admin/payments/PaymentsPage.tsx | 2 +- .../create-payment/CreatePaymentStepper.tsx | 87 ++++ .../create-payment/PaymentTypeSelector.tsx | 31 ++ .../benevity/BenevityEditableInput.tsx | 76 +++ .../benevity/BenevityImportTypeSelector.tsx | 36 ++ .../benevity/BenevityManualImport.tsx | 25 + .../CreatePaymentFromBenevityForm.tsx | 81 +++ .../CreatePaymentFromBenevityRecord.tsx | 21 + .../benevity/FileImportDialog.tsx | 91 ++++ .../benevity/helpers/benevity.types.ts | 63 +++ .../benevity/helpers/benevityCsvParser.ts} | 52 +- .../helpers/form/BenevityDonationTable.tsx | 132 +++++ .../helpers/form/BenevityTransactionData.tsx | 34 ++ .../benevity/helpers/form/ExchangeRate.tsx | 8 + .../benevity/helpers/readFile.ts | 21 + .../benevity/helpers/validation.ts | 30 ++ .../stripe/CreatePaymentFromStripeCharge.tsx | 12 + .../stripe/StripeChargeLookupForm.tsx | 38 ++ .../stripe/StripeCreatePaymentDialog.tsx | 89 +--- .../stripe/helpers/validations.ts | 4 + src/components/admin/payments/grid/Grid.tsx | 6 +- .../admin/payments/grid/GridAppbar.tsx | 37 +- .../payments/store/CreatePaymentContext.tsx | 21 + .../payments/store/CreatePaymentStore.ts | 80 +++ .../store/createPaymentStepReducer.ts | 64 +++ src/gql/donations.d.ts | 6 + src/pages/index.tsx | 2 +- src/service/donation.ts | 23 + src/stores/dashboard/ModalStore.ts | 10 + 34 files changed, 1076 insertions(+), 929 deletions(-) delete mode 100644 src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx delete mode 100644 src/components/admin/bank-transactions/store/BenevityImportStore.ts delete mode 100644 src/components/admin/donations/dialogs/CreatePaymentDialog.tsx create mode 100644 src/components/admin/payments/create-payment/CreatePaymentStepper.tsx create mode 100644 src/components/admin/payments/create-payment/PaymentTypeSelector.tsx create mode 100644 src/components/admin/payments/create-payment/benevity/BenevityEditableInput.tsx create mode 100644 src/components/admin/payments/create-payment/benevity/BenevityImportTypeSelector.tsx create mode 100644 src/components/admin/payments/create-payment/benevity/BenevityManualImport.tsx create mode 100644 src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx create mode 100644 src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityRecord.tsx create mode 100644 src/components/admin/payments/create-payment/benevity/FileImportDialog.tsx create mode 100644 src/components/admin/payments/create-payment/benevity/helpers/benevity.types.ts rename src/{common/util/benevityCSVParser.ts => components/admin/payments/create-payment/benevity/helpers/benevityCsvParser.ts} (68%) create mode 100644 src/components/admin/payments/create-payment/benevity/helpers/form/BenevityDonationTable.tsx create mode 100644 src/components/admin/payments/create-payment/benevity/helpers/form/BenevityTransactionData.tsx create mode 100644 src/components/admin/payments/create-payment/benevity/helpers/form/ExchangeRate.tsx create mode 100644 src/components/admin/payments/create-payment/benevity/helpers/readFile.ts create mode 100644 src/components/admin/payments/create-payment/benevity/helpers/validation.ts create mode 100644 src/components/admin/payments/create-payment/stripe/CreatePaymentFromStripeCharge.tsx create mode 100644 src/components/admin/payments/create-payment/stripe/StripeChargeLookupForm.tsx rename src/components/admin/{donations/dialogs => payments/create-payment}/stripe/StripeCreatePaymentDialog.tsx (53%) create mode 100644 src/components/admin/payments/create-payment/stripe/helpers/validations.ts create mode 100644 src/components/admin/payments/store/CreatePaymentContext.tsx create mode 100644 src/components/admin/payments/store/CreatePaymentStore.ts create mode 100644 src/components/admin/payments/store/createPaymentStepReducer.ts diff --git a/src/common/hooks/bank-transactions.ts b/src/common/hooks/bank-transactions.ts index 028acce03..c2c8a7cd0 100644 --- a/src/common/hooks/bank-transactions.ts +++ b/src/common/hooks/bank-transactions.ts @@ -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' @@ -21,3 +21,11 @@ export function useBankTransactionsList( }, ) } + +export function useFindBankTransaction(id: string) { + const { data: session } = useSession() + return useQuery( + [endpoints.bankTransactions.getTransactionById(id).url], + authQueryFnFactory(session?.accessToken), + ) +} diff --git a/src/common/hooks/donation.ts b/src/common/hooks/donation.ts index a23cf91c4..bd2ceb0c1 100644 --- a/src/common/hooks/donation.ts +++ b/src/common/hooks/donation.ts @@ -6,13 +6,14 @@ import { QueryClient, useMutation, useQuery } from '@tanstack/react-query' import { ApiErrors } from 'service/apiErrors' import { AlertStore } from 'stores/AlertStore' import { endpoints } from 'service/apiEndpoints' -import { authQueryFnFactory } from 'service/restRequests' +import { authConfig, authQueryFnFactory } from 'service/restRequests' import { CheckoutSessionInput, CheckoutSessionResponse, DonationResponse, DonorsCountResult, PaymentAdminResponse, + StripeChargeResponse, TPaymentResponse, TotalDonatedMoneyResponse, UserDonationResult, @@ -111,3 +112,11 @@ export function getTotalDonatedMoney() { export function useDonatedUsersCount() { return useQuery([endpoints.donation.getDonorsCount.url]) } + +export function useGetStripeChargeFromPID(stripeId: string) { + const { data: session } = useSession() + return useQuery( + [endpoints.payments.referenceStripeWithInternal(stripeId).url], + { queryFn: authQueryFnFactory(session?.accessToken) }, + ) +} diff --git a/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx b/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx deleted file mode 100644 index 031d56192..000000000 --- a/src/components/admin/bank-transactions/modals/BenevityImportDialog.tsx +++ /dev/null @@ -1,478 +0,0 @@ -import { - Box, - Button, - Card, - CardContent, - CircularProgress, - Dialog, - Grid, - IconButton, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Typography, -} from '@mui/material' -import React, { useEffect, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { - CreatePaymentStore, - TImportType, - benevityDonationInitialValues, - benevityInitialValues, -} from '../store/BenevityImportStore' -import AddCircleIcon from '@mui/icons-material/AddCircle' - -import { observer } from 'mobx-react' -import { - BenevityCSVParser, - TBenevityCSVParser, - TBenevityDonation, -} from 'common/util/benevityCSVParser' - -import SubmitButton from 'components/common/form/SubmitButton' -import { FieldArray, Form, Formik, useField, useFormikContext } from 'formik' -import { TranslatableField, translateError } from 'common/form/validation' -import EditIcon from '@mui/icons-material/Edit' -import { useQuery } from '@tanstack/react-query' -import { endpoints } from 'service/apiEndpoints' -import { useSession } from 'next-auth/react' -import { authQueryFnFactory } from 'service/restRequests' -import { BankTransactionsInput } from 'gql/bank-transactions' -import * as yup from 'yup' -import { fromMoney, moneyPublic } from 'common/util/money' -import { useCampaignList } from 'common/hooks/campaigns' -import { v5 as uuidv5 } from 'uuid' -import { BenevityRequest } from 'components/admin/donations/dialogs/CreatePaymentDialog' -import { Delete } from '@mui/icons-material' - -function BenevityImportDialog() { - const { t } = useTranslation() - const { setImportType } = CreatePaymentStore - - const handleImportTypeChange = (type: TImportType) => { - setImportType(type) - } - - return ( - <> - - Прикачване на CSV файл - - - {t('Създаване на дарения от Benevity, чрез CSV файл')} - - - - - - - ) -} - -export function FileImportDialog() { - const { setBenevityData } = CreatePaymentStore - const [isDragging, setIsDragging] = useState(false) - const inputFile = useRef(null) - const submitButtonRef = useRef(null) - const [field, meta, { setValue }] = useField('benevityData') - - const onDragOver = (event: React.DragEvent) => { - event.preventDefault() - setIsDragging(true) - event.dataTransfer.dropEffect = 'copy' - } - const onDragLeave = (event: React.DragEvent) => { - event.preventDefault() - setIsDragging(false) - } - const onDrop = (event: React.DragEvent) => { - const fileReader = new FileReader() - event.preventDefault() - setIsDragging(false) - const file = event.dataTransfer.files[0] - fileReader.readAsText(file) - fileReader.onload = function () { - if (!fileReader.result) return - const csvToJSON = BenevityCSVParser(fileReader.result as string) - if (!csvToJSON) throw new Error('Something went wrong') - setBenevityData(csvToJSON) - setValue(csvToJSON) - } - } - const onClick = (event: React.MouseEvent) => { - inputFile.current?.click() - } - - const onChange = (event: React.ChangeEvent) => { - event.preventDefault() - setIsDragging(false) - - const fileReader = new FileReader() - const filelist = event.target.files - if (!filelist) return - const file = filelist[0] - if (file.type !== 'text/csv') - throw new Error('Unsupported file format. Only csv files are allowed') - fileReader.readAsText(file) - fileReader.onload = function () { - if (!fileReader.result) return - const csvToJSON = BenevityCSVParser(fileReader.result as string) - if (!csvToJSON) throw new Error('Something went wrong') - setValue(csvToJSON) - submitButtonRef.current?.click() - } - } - - return ( - <> -
-
- Провлачете файла в квадрата -
-
- - - - - - ) -} - -export default observer(BenevityImportDialog) diff --git a/src/components/admin/bank-transactions/store/BenevityImportStore.ts b/src/components/admin/bank-transactions/store/BenevityImportStore.ts deleted file mode 100644 index cab11ff3a..000000000 --- a/src/components/admin/bank-transactions/store/BenevityImportStore.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { action, makeObservable, observable } from 'mobx' -import { enableStaticRendering } from 'mobx-react' -import type { TBenevityCSVParser, TBenevityDonation } from 'common/util/benevityCSVParser' - -enableStaticRendering(typeof window === 'undefined') - -export type SelectedPaymentSource = 'none' | 'stripe' | 'benevity' -export type TImportType = 'none' | 'file' | 'manual' - -export const benevityInitialValues: TBenevityCSVParser = { - charityId: '', - charityName: '', - currency: '', - donations: [], - periodEnding: '', - paymentMethod: '', - disbursementId: '', - checkFee: '', - totalDonationsGross: '', - netTotalPayment: 0, - transactionAmount: 0, - exchangeRate: 0, -} - -export const benevityDonationInitialValues: TBenevityDonation = { - company: '', - project: '', - donationDate: '', - donorFirstName: '', - donorLastName: '', - email: '', - address: '', - city: '', - stateProvince: '', - postalCode: '', - activity: '', - comment: '', - transactionId: '', - donationFrequency: '', - currency: '', - projectRemoteId: '', - source: '', - reason: '', - totalDonationToBeAcknowledged: 0, - matchAmount: 0, - causeSupportFee: 0, - merchantFee: 0, - feeComment: '', - totalFee: 0, - totalAmount: 0, -} - -interface IBenevityStoreImpl { - isImportModalOpen: boolean - importType: TImportType - benevityData: TBenevityCSVParser - selectedRecord: Record<'id', string> - showImportModal: () => void - hideImportModal: () => void - setSelectedRecord: (record: Record<'id', string>) => void - setImportType: (type: TImportType) => void - setBenevityData: (data: TBenevityCSVParser) => void -} - -export class BenevityImportStoreImpl implements IBenevityStoreImpl { - isImportModalOpen = true - selectedRecord = { - id: '', - } - importType: TImportType = 'none' - paymentSource: SelectedPaymentSource = 'none' - benevityData: TBenevityCSVParser = benevityInitialValues - step = 0 - - constructor() { - makeObservable(this, { - isImportModalOpen: observable, - selectedRecord: observable, - paymentSource: observable, - importType: observable, - step: observable, - showImportModal: action, - setPaymentSource: action, - hideImportModal: action, - setImportType: action, - setBenevityData: action, - }) - } - - showImportModal = () => { - this.isImportModalOpen = true - } - - hideImportModal = () => { - this.isImportModalOpen = false - this.importType = 'none' - this.selectedRecord = { id: '' } - this.benevityData = benevityInitialValues - this.step = 0 - } - - setSelectedRecord = (record: typeof this.selectedRecord) => { - this.selectedRecord = record - } - - setPaymentSource = (source: SelectedPaymentSource) => { - this.paymentSource = source - } - - setStep = (step: number) => { - this.step = step - } - - setImportType = (type: TImportType) => { - this.importType = type - this.step = 1 - } - setBenevityData = (data: TBenevityCSVParser) => { - this.step = 2 - this.benevityData = data - } -} - -export const CreatePaymentStore = new BenevityImportStoreImpl() diff --git a/src/components/admin/donations/dialogs/CreatePaymentDialog.tsx b/src/components/admin/donations/dialogs/CreatePaymentDialog.tsx deleted file mode 100644 index 84002336e..000000000 --- a/src/components/admin/donations/dialogs/CreatePaymentDialog.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { Box, Button, Card, CardContent, Dialog, TextField, Typography } from '@mui/material' -import BenevityImportDialog, { - DonationImportSummary, - benevityValidation, -} from 'components/admin/bank-transactions/modals/BenevityImportDialog' -import { Form, Formik, useField, useFormikContext } from 'formik' -import React from 'react' -import * as yup from 'yup' -import { FileImportDialog } from 'components/admin/bank-transactions/modals/BenevityImportDialog' -import { - CreatePaymentStore, - SelectedPaymentSource, -} from 'components/admin/bank-transactions/store/BenevityImportStore' -import { useTranslation } from 'react-i18next' -import { observer } from 'mobx-react' -import { TranslatableField, translateError } from 'common/form/validation' -import { UseMutationResult, useMutation } from '@tanstack/react-query' -import StripeCreatePaymentDialog from './stripe/StripeCreatePaymentDialog' -import { AxiosResponse } from 'axios' -import { TPaymentResponse } from 'gql/donations' -import { endpoints } from 'service/apiEndpoints' -import { apiClient } from 'service/apiClient' -import { TBenevityCSVParser } from 'common/util/benevityCSVParser' - -const stripeInputValidation = yup.object({ - extPaymentIntentId: yup.string().required().matches(/^pi_/, 'Невалиден номер на Страйп'), -}) - -const benevityInputValidation = yup.object({ - transactionId: yup.string().required(), -}) - -function StripeInputDialog() { - const [field, meta] = useField('extPaymentIntentId') - const { handleSubmit } = useFormikContext() - const { t } = useTranslation() - const helperText = translateError(meta.error as TranslatableField, t) - - const handleSubmitS = (e: any) => { - handleSubmit() - } - return ( - <> - - - {t('Въведете номер на плащане от Страйп')} - - - - - Платещените номера от Страйп започват с: -
pi_ -
- -
- - ) -} - -function PaymentTypeSelectDialog() { - const { setPaymentSource } = CreatePaymentStore - const { t } = useTranslation('') - const handleSubmit = (source: SelectedPaymentSource) => { - setPaymentSource(source) - } - return ( - <> - - Ръчно добавяне на плащане - - - {t('')} - - - - - - - ) -} -// const stripeMutation = useMutation() -function BenevityManualImport() { - const { t } = useTranslation('') - const [field, meta] = useField('transactionId') - const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' - return ( - <> - - - {t('Въведете ID от Benevity или ID на банкова транзация')} - - - - - - - ) -} - -export type BenevityRequest = { - amount: number - extPaymentIntentId: string - exchangeRate: number - benevityData: Pick -} - -type Validation = yup.InferType< - typeof stripeInputValidation | typeof benevityValidation | typeof benevityInputValidation -> - -type Steps = { - [key in SelectedPaymentSource]: { - component: React.JSX.Element - validation?: yup.SchemaOf | null - mutation?: UseMutationResult, unknown, BenevityRequest> - }[] -} - -function CreatePaymentDialog() { - const { isImportModalOpen, hideImportModal, paymentSource, importType, step, setStep } = - CreatePaymentStore - - const benevityMutation = useMutation, unknown, BenevityRequest>({ - mutationFn: async (data) => { - return await apiClient.post(endpoints.payments.createFromBeneivty.url, data) - }, - }) - - const steps: Steps = { - none: [{ component: }], - stripe: [ - { - component: , - validation: stripeInputValidation, - }, - { - component: , - validation: stripeInputValidation, - }, - ], - benevity: [ - { - component: , - validation: null, - }, - { - component: importType === 'file' ? : , - validation: importType === 'file' ? null : benevityInputValidation, - }, - { - component: , - validation: benevityValidation, - mutation: benevityMutation, - }, - ], - } - return ( - - - - { - if (step < steps[paymentSource].length - 1) { - setStep(step + 1) - } - if (step === steps[paymentSource].length - 1) { - steps[paymentSource][step].mutation?.mutate(values as BenevityRequest) - } - }} - initialValues={{}} - validationSchema={steps[paymentSource][step].validation}> -
{steps[paymentSource][step].component}
-
-
-
-
- ) -} - -export default observer(CreatePaymentDialog) diff --git a/src/components/admin/payments/PaymentsPage.tsx b/src/components/admin/payments/PaymentsPage.tsx index 50e2c6d21..8083c4892 100644 --- a/src/components/admin/payments/PaymentsPage.tsx +++ b/src/components/admin/payments/PaymentsPage.tsx @@ -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 ( diff --git a/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx b/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx new file mode 100644 index 000000000..6f8bbbcb2 --- /dev/null +++ b/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx @@ -0,0 +1,87 @@ +import { Card, CardContent, Dialog } from '@mui/material' + +import { Form, Formik, useField, useFormikContext } 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 '../store/CreatePaymentContext' +import PaymentTypeSelector from './PaymentTypeSelector' +import { BenevityManualImport } from './benevity/BenevityManualImport' +import { stripeInputValidation } from './stripe/helpers/validations' +import { StripeChargeLookupForm } from './stripe/StripeChargeLookupForm' +import { SelectedPaymentSource } from '../store/createPaymentStepReducer' +import { CreatePaymentFromStripeCharge } from './stripe/CreatePaymentFromStripeCharge' +import { ModalStore } from '../PaymentsPage' + +type Validation = yup.InferType< + typeof stripeInputValidation | typeof benevityValidation | typeof benevityInputValidation +> + +type Steps = { + [key in SelectedPaymentSource]: { + component: React.JSX.Element + validation?: yup.SchemaOf | null + }[] +} + +function CreatePaymentStepper() { + const paymentContext = useContext(PaymentContext) + const { isPaymentImportOpen, hideImport } = ModalStore + const { payment, dispatch } = paymentContext + + const steps: Steps = { + none: [{ component: }], + stripe: [ + { + component: , + validation: stripeInputValidation, + }, + { + component: , + }, + ], + benevity: [ + { + component: , + validation: null, + }, + { + component: + payment.benevityImportType === 'file' ? : , + validation: payment.benevityImportType === 'file' ? null : benevityInputValidation, + }, + { + component: , + validation: benevityValidation, + }, + ], + } + const onClose = () => { + dispatch({ type: 'RESET_MODAL' }) + hideImport() + } + return ( + + + + { + if (payment.step < steps[payment.paymentSource].length - 1) { + dispatch({ type: 'INCREMENT_STEP' }) + } + }} + initialValues={{}} + validationSchema={steps[payment.paymentSource][payment.step].validation}> +
{steps[payment.paymentSource][payment.step].component}
+
+
+
+
+ ) +} + +export default CreatePaymentStepper diff --git a/src/components/admin/payments/create-payment/PaymentTypeSelector.tsx b/src/components/admin/payments/create-payment/PaymentTypeSelector.tsx new file mode 100644 index 000000000..265a5364d --- /dev/null +++ b/src/components/admin/payments/create-payment/PaymentTypeSelector.tsx @@ -0,0 +1,31 @@ +import { Box, Button, Typography } from '@mui/material' +import { useTranslation } from 'next-i18next' +import React, { useContext } from 'react' +import { SelectedPaymentSource } from '../store/createPaymentStepReducer' +import { PaymentContext } from '../store/CreatePaymentContext' + +export default function PaymentTypeSelector() { + const paymentContext = useContext(PaymentContext) + const { t } = useTranslation('') + const handleSubmit = (source: SelectedPaymentSource) => { + paymentContext.dispatch({ type: 'UPDATE_PAYMENT_SOURCE', payload: source }) + } + return ( + <> + + Ръчно добавяне на плащане + + + {t('')} + + + + + + + ) +} diff --git a/src/components/admin/payments/create-payment/benevity/BenevityEditableInput.tsx b/src/components/admin/payments/create-payment/benevity/BenevityEditableInput.tsx new file mode 100644 index 000000000..a453744aa --- /dev/null +++ b/src/components/admin/payments/create-payment/benevity/BenevityEditableInput.tsx @@ -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, { setValue }] = useField(name) + const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' + + const ref = useRef(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, + ) => { + formikBlur(e) + toggleEdit() + } + + return ( + 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 && {suffix}} + {canEdit && ( + + + + )} + + ), + }} + /> + ) +} diff --git a/src/components/admin/payments/create-payment/benevity/BenevityImportTypeSelector.tsx b/src/components/admin/payments/create-payment/benevity/BenevityImportTypeSelector.tsx new file mode 100644 index 000000000..a2d12b1f5 --- /dev/null +++ b/src/components/admin/payments/create-payment/benevity/BenevityImportTypeSelector.tsx @@ -0,0 +1,36 @@ +import { Box, Button, Typography } from '@mui/material' +import { useTranslation } from 'next-i18next' +import { CreatePaymentStore, TImportType } from '../../store/CreatePaymentStore' +import { observer } from 'mobx-react' +import { useContext } from 'react' +import { PaymentContext } from '../../store/CreatePaymentContext' + +function BenevityImportFirstStep() { + const { t } = useTranslation() + const paymentContext = useContext(PaymentContext) + + const handleImportTypeChange = (importType: TImportType) => { + paymentContext.dispatch({ type: 'SET_BENEVITY_IMPORT_TYPE', payload: importType }) + } + + return ( + <> + + Прикачване на CSV файл + + + {t('Създаване на дарения от Benevity, чрез CSV файл')} + + + + + + + ) +} + +export default observer(BenevityImportFirstStep) diff --git a/src/components/admin/payments/create-payment/benevity/BenevityManualImport.tsx b/src/components/admin/payments/create-payment/benevity/BenevityManualImport.tsx new file mode 100644 index 000000000..adf61776e --- /dev/null +++ b/src/components/admin/payments/create-payment/benevity/BenevityManualImport.tsx @@ -0,0 +1,25 @@ +import { Box, Button, TextField, Typography } from '@mui/material' +import { TranslatableField, translateError } from 'common/form/validation' +import { useField } from 'formik' +import { useTranslation } from 'next-i18next' + +export function BenevityManualImport() { + const { t } = useTranslation('') + const [field, meta] = useField('transactionId') + const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' + return ( + <> + + {t('Въведете ID от Benevity или ID на банкова транзация')} + + + + + + + ) +} diff --git a/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx b/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx new file mode 100644 index 000000000..41e94b57d --- /dev/null +++ b/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx @@ -0,0 +1,81 @@ +import { Button, Grid, Typography } from '@mui/material' +import { useMutation } from '@tanstack/react-query' +import { useImportBenevityDonation } from 'service/donation' +import { CreatePaymentStore, benevityInitialValues } from '../../store/CreatePaymentStore' +import { useFormikContext } from 'formik' +import { AxiosResponse } from 'axios' +import { TPaymentResponse } from 'gql/donations' +import { BenevityRequest } from './helpers/benevity.types' +import { useEffect } from 'react' +import { fromMoney } from 'common/util/money' +import SubmitButton from 'components/common/form/SubmitButton' +import { BenevityImportInput } from './helpers/benevity.types' +import BenevityTransactionData from './helpers/form/BenevityTransactionData' +import { BenevityDonationsTable } from './helpers/form/BenevityDonationTable' +import { BankTransactionsInput } from 'gql/bank-transactions' + +type CreatePaymentFromBenevityForm = { + data: BankTransactionsInput +} +export function CreatePaymentFromBenevityForm({ data }: CreatePaymentFromBenevityForm) { + const { hideImportModal } = CreatePaymentStore + + const benevityMutation = useMutation, unknown, BenevityRequest>({ + mutationFn: useImportBenevityDonation(), + }) + const { values, setFieldValue } = useFormikContext() + + useEffect(() => { + if (!data) return + setFieldValue('amount', fromMoney(data.amount ?? 0)) + setFieldValue('transactionId', data.id) + setFieldValue('currency', values.benevityData?.currency) + if (!values.benevityData) { + setFieldValue('benevityData', benevityInitialValues) + } + }, []) + + useEffect(() => { + setFieldValue('exchangeRate', values.amount / values.benevityData?.netTotalPayment) + }, [values.amount, values.benevityData?.netTotalPayment]) + + const handleSubmit = () => { + const bodyReq: BenevityRequest = { + amount: values.amount, + exchangeRate: values.exchangeRate, + benevityData: values.benevityData, + extPaymentIntentId: values.transactionId, + } + benevityMutation.mutate(bodyReq) + } + return ( + + + + + + + Дарения по кампании: + + + + + + + + + + + ) +} diff --git a/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityRecord.tsx b/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityRecord.tsx new file mode 100644 index 000000000..06c4f5fe2 --- /dev/null +++ b/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityRecord.tsx @@ -0,0 +1,21 @@ +import { useFindBankTransaction } from 'common/hooks/bank-transactions' +import { useFormikContext } from 'formik' +import React from 'react' +import { BenevityImportInput } from './helpers/benevity.types' +import { CircularProgress } from '@mui/material' +import { CreatePaymentFromBenevityForm } from './CreatePaymentFromBenevityForm' + +export default function CreatePaymentFromBenevityRecord() { + const { values } = useFormikContext() + const { data, isLoading, isError } = useFindBankTransaction( + values.transactionId ?? values.benevityData?.disbursementId, + ) + if (isLoading) return + if (isError) return + if (!isLoading && !data) + return `Bank donation with ${ + values.transactionId ?? values.benevityData.disbursementId + } not found` + + return +} diff --git a/src/components/admin/payments/create-payment/benevity/FileImportDialog.tsx b/src/components/admin/payments/create-payment/benevity/FileImportDialog.tsx new file mode 100644 index 000000000..fd2980df7 --- /dev/null +++ b/src/components/admin/payments/create-payment/benevity/FileImportDialog.tsx @@ -0,0 +1,91 @@ +import { BenevityCSVParser } from './helpers/benevityCsvParser' +import { asyncFileReader } from './helpers/readFile' +import { useField } from 'formik' +import { useRef, useState } from 'react' + +export function FileImportDialog() { + const [isDragging, setIsDragging] = useState(false) + const inputFile = useRef(null) + const submitButtonRef = useRef(null) + const [field, meta, { setValue }] = useField('benevityData') + + const onDragOver = (event: React.DragEvent) => { + event.preventDefault() + setIsDragging(true) + event.dataTransfer.dropEffect = 'copy' + } + const onDragLeave = (event: React.DragEvent) => { + event.preventDefault() + setIsDragging(false) + } + const onDrop = async (event: React.DragEvent) => { + event.preventDefault() + setIsDragging(false) + const file = event.dataTransfer.files[0] + const fileAsText = await asyncFileReader(file) + const csvToJSON = BenevityCSVParser(fileAsText as string) + setValue(csvToJSON) + submitButtonRef.current?.click() + } + const onClick = () => { + inputFile.current?.click() + } + + const onChange = async (event: React.ChangeEvent) => { + event.preventDefault() + setIsDragging(false) + + const filelist = event.target.files + if (!filelist) return + const file = filelist[0] + if (file.type !== 'text/csv') + throw new Error('Unsupported file format. Only csv files are allowed') + const fileAsText = await asyncFileReader(file) + const csvToJSON = BenevityCSVParser(fileAsText as string) + setValue(csvToJSON) + submitButtonRef.current?.click() + } + + return ( + <> +
+
+ Провлачете файла в квадрата +
+
+ + + + + ) +} diff --git a/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx b/src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx similarity index 53% rename from src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx rename to src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx index e2e078d09..4b8b7eee1 100644 --- a/src/components/admin/donations/dialogs/stripe/StripeCreatePaymentDialog.tsx +++ b/src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx @@ -7,67 +7,40 @@ import { TableRow, Typography, } from '@mui/material' -import { useMutation, useQuery } from '@tanstack/react-query' +import { useMutation } from '@tanstack/react-query' import { AxiosResponse } from 'axios' import { money } from 'common/util/money' import { stripeFeeCalculator } from 'components/client/one-time-donation/helpers/stripe-fee-calculator' -import CenteredSpinner from 'components/common/CenteredSpinner' -import { useField } from 'formik' -import { TPaymentResponse } from 'gql/donations' -import { CardRegion, PaymentStatus } from 'gql/donations.enums' + +import { StripeChargeResponse, TPaymentResponse } from 'gql/donations' import React from 'react' -import { useTranslation } from 'react-i18next' -import { apiClient } from 'service/apiClient' +import { useTranslation } from 'next-i18next' import { endpoints } from 'service/apiEndpoints' +import { useCreatePaymentFromStripeMutation } from 'service/donation' import Stripe from 'stripe' -type Reference = { - netAmount: number - status: PaymentStatus -} - -type GetStripeChargeResponse = { - stripe: Stripe.Charge - internal?: TPaymentResponse - region: CardRegion -} - -function useGetStripeChargeFromPID(stripeId: string) { - return useQuery([ - endpoints.payments.referenceStripeWithInternal(stripeId).url, - ]) -} - -type SynchronizeStripeWithInternalProps = { - data: GetStripeChargeResponse - id: string -} - -type SynchronizeWithStripeInput = GetStripeChargeResponse & { +type CreateUpdatePaymentFromStripeChargeProps = { + data: StripeChargeResponse id: string } -function synchronizeWithStrip(data: any) { - return apiClient.patch(endpoints.payments.synchronizeWithStripe(data.id).url, data) -} +export function CreatePaymentFromStripeChargeTable({ + data, + id, +}: CreateUpdatePaymentFromStripeChargeProps) { + const { t } = useTranslation() -const useSynchronizeWithStripeMutation = () => { - return useMutation, unknown, SynchronizeWithStripeInput>( - [endpoints.payments.synchronizeWithStripe], - { mutationFn: synchronizeWithStrip }, - ) -} + const stripeMutation = useMutation, unknown, Stripe.Charge>({ + mutationFn: useCreatePaymentFromStripeMutation(), + }) -function SynchronizeStripeWithInternal({ data, id }: SynchronizeStripeWithInternalProps) { - const { t } = useTranslation() - const stripeMutation = useSynchronizeWithStripeMutation() const handleSubmit = () => { - const mutateData: SynchronizeWithStripeInput = { - id: id, - ...data, - } - stripeMutation.mutate(mutateData) + console.log(`HandleSubmit is called`) + stripeMutation.mutate(data.stripe) } + + const stripeStatus = data.stripe.refunded ? 'refund' : data.stripe.status + return ( @@ -89,16 +62,16 @@ function SynchronizeStripeWithInternal({ data, id }: SynchronizeStripeWithIntern База данни на Страйп - Вътрещна база данни + Вътрена база данни Статус + {t('profile:donations.status.' + stripeStatus)} - {t( - 'profile:donations.status.' + data.stripe.refunded ? 'refund' : data.stripe.status, - )} + {data.internal?.status + ? t('profile:donations.status.' + data.internal?.status) + : 'N/A'} - {t('profile:donations.status.' + data.internal?.status)} Сума(бруто) @@ -118,23 +91,15 @@ function SynchronizeStripeWithInternal({ data, id }: SynchronizeStripeWithIntern {data.internal?.billingName ?? 'N/A'} - Дарител(име) + Дарител(емайл) {data.stripe.billing_details.email} {data.internal?.billingEmail ?? 'N/A'} - ) } - -export default function StripeCreatePaymentDialog() { - const [field] = useField('extPaymentIntentId') - const data = useGetStripeChargeFromPID(field.value) - if (data.isLoading) return - if (data.isError) return - return -} diff --git a/src/components/admin/payments/create-payment/stripe/helpers/validations.ts b/src/components/admin/payments/create-payment/stripe/helpers/validations.ts new file mode 100644 index 000000000..e4fe60fd3 --- /dev/null +++ b/src/components/admin/payments/create-payment/stripe/helpers/validations.ts @@ -0,0 +1,4 @@ +import * as yup from 'yup' +export const stripeInputValidation = yup.object({ + extPaymentIntentId: yup.string().required().matches(/^ch_/, 'Невалиден номер на Страйп'), +}) diff --git a/src/components/admin/payments/grid/Grid.tsx b/src/components/admin/payments/grid/Grid.tsx index 35fd0e982..3eef8a884 100644 --- a/src/components/admin/payments/grid/Grid.tsx +++ b/src/components/admin/payments/grid/Grid.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { UseQueryResult } from '@tanstack/react-query' import { useTranslation } from 'next-i18next' -import { Box, IconButton, Tooltip } from '@mui/material' +import { Box, Button, IconButton, Tooltip } from '@mui/material' import { Edit } from '@mui/icons-material' import { DataGrid, @@ -30,6 +30,7 @@ import { PaymentStatus, PaymentProvider } from '../../../../gql/donations.enums' import { useSession } from 'next-auth/react' import { PaymentAdminResponse } from 'gql/donations' import Link from 'next/link' +import CreatePaymentDialog from '../store/CreatePaymentContext' interface RenderCellProps { params: GridRenderCellParams @@ -51,7 +52,7 @@ export default observer(function Grid() { const [focusedRowId, setFocusedRowId] = useState(null as string | null) const { t } = useTranslation() const router = useRouter() - const { isDetailsOpen } = ModalStore + const { isDetailsOpen, isPaymentImportOpen } = ModalStore const { isRefundOpen } = RefundStore const { isDeleteOpen, @@ -274,6 +275,7 @@ export default observer(function Grid() { {isDetailsOpen && } {isRefundOpen && } {isDeleteOpen && } + {isPaymentImportOpen && } ) }) diff --git a/src/components/admin/payments/grid/GridAppbar.tsx b/src/components/admin/payments/grid/GridAppbar.tsx index 7388944ec..a63a65e87 100644 --- a/src/components/admin/payments/grid/GridAppbar.tsx +++ b/src/components/admin/payments/grid/GridAppbar.tsx @@ -4,6 +4,7 @@ import { Box, Button, FormControl, + IconButton, InputLabel, MenuItem, Select, @@ -26,6 +27,9 @@ import theme from 'common/theme' import debounce from 'lodash/debounce' import { useCampaignList } from 'common/hooks/campaigns' +import AddIcon from '@mui/icons-material/Add' +import { ModalStore } from '../PaymentsPage' +import { observer } from 'mobx-react' const addIconStyles = { background: theme.palette.primary.light, @@ -35,7 +39,7 @@ const addIconStyles = { boxShadow: 3, } -export default function GridAppbar() { +function GridAppbar() { const router = useRouter() const { donationStore } = useStores() const { t } = useTranslation() @@ -50,7 +54,7 @@ export default function GridAppbar() { const [searchValue, setSearchValue] = useState('') const [selectedCampaign, setSelectedCampaign] = useState('') const { data: campaigns } = useCampaignList() - + const { showImport } = ModalStore const debounceSearch = useMemo( () => debounce( @@ -130,9 +134,23 @@ export default function GridAppbar() { - + + showImport()}> + + + + + + + exportToExcel.mutate()} + /> + + {/* button is disabled because we have a bug(conflicting bank donations) https://github.com/podkrepi-bg/frontend/issues/1649 */} - - - exportToExcel.mutate()} - /> - + ) } + +export default observer(GridAppbar) diff --git a/src/components/admin/payments/store/CreatePaymentContext.tsx b/src/components/admin/payments/store/CreatePaymentContext.tsx new file mode 100644 index 000000000..e5b57fdb2 --- /dev/null +++ b/src/components/admin/payments/store/CreatePaymentContext.tsx @@ -0,0 +1,21 @@ +import React, { createContext } from 'react' +import { Actions, CreatePayment, createPaymentStepReducer } from './createPaymentStepReducer' +import CreatePaymentStepper from '../create-payment/CreatePaymentStepper' +import { observer } from 'mobx-react' + +export type PaymentContext = { + payment: CreatePayment + dispatch: React.Dispatch +} +export const PaymentContext = createContext({} as PaymentContext) + +function CreatePaymentDialog() { + const [payment, dispatch] = createPaymentStepReducer() + return ( + + + + ) +} + +export default observer(CreatePaymentDialog) diff --git a/src/components/admin/payments/store/CreatePaymentStore.ts b/src/components/admin/payments/store/CreatePaymentStore.ts new file mode 100644 index 000000000..9d1768e50 --- /dev/null +++ b/src/components/admin/payments/store/CreatePaymentStore.ts @@ -0,0 +1,80 @@ +import { action, makeObservable, observable } from 'mobx' +import { enableStaticRendering } from 'mobx-react' +import type { + TBenevityCSVParser, + TBenevityDonation, +} from '../create-payment/benevity/helpers/benevity.types' + +enableStaticRendering(typeof window === 'undefined') + +export const benevityInitialValues: TBenevityCSVParser = { + charityId: '', + charityName: '', + currency: '', + donations: [], + periodEnding: '', + paymentMethod: '', + disbursementId: '', + checkFee: '', + totalDonationsGross: '', + netTotalPayment: 0, + transactionAmount: 0, + exchangeRate: 0, + note: '', +} + +export const benevityDonationInitialValues: TBenevityDonation = { + company: '', + project: '', + donationDate: '', + donorFirstName: '', + donorLastName: '', + email: '', + address: '', + city: '', + stateProvince: '', + postalCode: '', + activity: '', + comment: '', + transactionId: '', + donationFrequency: '', + currency: '', + projectRemoteId: '', + source: '', + reason: '', + totalDonationToBeAcknowledged: 0, + matchAmount: 0, + causeSupportFee: 0, + merchantFee: 0, + feeComment: '', + totalFee: 0, + totalAmount: 0, +} + +interface ICreatePaymentStoreImpl { + isImportModalOpen: boolean + showImportModal: () => void + hideImportModal: () => void +} + +export class CreatePaymentStoreImpl implements ICreatePaymentStoreImpl { + isImportModalOpen = true + + constructor() { + makeObservable(this, { + isImportModalOpen: observable, + showImportModal: action, + hideImportModal: action, + }) + } + + showImportModal = () => { + this.isImportModalOpen = true + } + + hideImportModal = () => { + this.isImportModalOpen = false + } +} + +export const CreatePaymentStore = new CreatePaymentStoreImpl() diff --git a/src/components/admin/payments/store/createPaymentStepReducer.ts b/src/components/admin/payments/store/createPaymentStepReducer.ts new file mode 100644 index 000000000..ab01a3071 --- /dev/null +++ b/src/components/admin/payments/store/createPaymentStepReducer.ts @@ -0,0 +1,64 @@ +import { useReducer } from 'react' + +export const ACTIONS = { + CREATE_PAYMENT: 'CREATE_PAYMENT', + INCREMENT_STEP: 'INCREMENT_STEP', + UPDATE_PAYMENT_SOURCE: 'UPDATE_PAYMENT_SOURCE', + RESET_MODAL: 'RESET_MODAL', + SET_BENEVITY_IMPORT_TYPE: 'SET_BENEVITY_IMPORT_TYPE', +} +export type SelectedPaymentSource = 'none' | 'stripe' | 'benevity' +export type BenevityImportType = 'none' | 'file' | 'manual' + +export type CreatePayment = { + paymentSource: SelectedPaymentSource + step: number + dialogOpen: boolean + benevityImportType?: BenevityImportType +} + +export type Actions = { + type: keyof typeof ACTIONS + payload?: unknown +} + +const initialValues: CreatePayment = { + paymentSource: 'none', + step: 0, + dialogOpen: true, +} + +function reducer(state: CreatePayment, actions: Actions): CreatePayment { + switch (actions.type) { + case 'CREATE_PAYMENT': + return { + ...state, + paymentSource: actions.payload as SelectedPaymentSource, + step: 0, + dialogOpen: true, + } + case 'UPDATE_PAYMENT_SOURCE': + return { + ...state, + paymentSource: actions.payload as SelectedPaymentSource, + } + case 'INCREMENT_STEP': + return { + ...state, + step: state.step + 1, + } + case 'RESET_MODAL': + return initialValues + case 'SET_BENEVITY_IMPORT_TYPE': + return { + ...state, + benevityImportType: actions.payload as BenevityImportType, + step: 1, + } + default: + return state + } +} + +export const createPaymentStepReducer = (initialState = initialValues) => + useReducer(reducer, initialState) diff --git a/src/gql/donations.d.ts b/src/gql/donations.d.ts index c0b600fa2..cf05e4732 100644 --- a/src/gql/donations.d.ts +++ b/src/gql/donations.d.ts @@ -248,3 +248,9 @@ export type PaymentAdminResponse = { items: PaymentAdminResponse[] total: number } + +export type StripeChargeResponse = { + stripe: Stripe.Charge + internal?: TPaymentResponse + region: CardRegion +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 5fe96a9fa..a34fe19b8 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -9,7 +9,7 @@ import { CampaignResponse } from 'gql/campaigns' import { endpoints } from 'service/apiEndpoints' import { authOptions } from './api/auth/[...nextauth]' -import CreatePaymentDialog from 'components/admin/donations/dialogs/CreatePaymentDialog' +import CreatePaymentDialog from 'components/admin/payments/store/CreatePaymentContext' export const getServerSideProps: GetServerSideProps<{ session: Session | null diff --git a/src/service/donation.ts b/src/service/donation.ts index f1dba2b4f..1d7d289f4 100644 --- a/src/service/donation.ts +++ b/src/service/donation.ts @@ -17,6 +17,7 @@ import { authConfig } from 'service/restRequests' import { UploadBankTransactionsFiles } from 'components/admin/bank-transactions-file/types' import { useMutation } from '@tanstack/react-query' import { FilterData } from 'gql/types' +import { BenevityRequest } from 'components/admin/payments/create-payment/benevity/helpers/benevity.types' export const createCheckoutSession = async (data: CheckoutSessionInput) => { return await apiClient.post>( @@ -125,3 +126,25 @@ export const useExportToExcel = (filterData?: FilterData, searchData?: string) = }) } } + +export const useImportBenevityDonation = () => { + const { data: session } = useSession() + return async (data: BenevityRequest) => { + return await apiClient.post( + endpoints.payments.createFromBeneivty.url, + data, + authConfig(session?.accessToken), + ) + } +} + +export function useCreatePaymentFromStripeMutation() { + const { data: session } = useSession() + return async (data: Stripe.Charge) => { + return await apiClient.put( + endpoints.payments.synchronizeWithStripe.url, + data, + authConfig(session?.accessToken), + ) + } +} diff --git a/src/stores/dashboard/ModalStore.ts b/src/stores/dashboard/ModalStore.ts index 898d46ee8..e3ae35e40 100644 --- a/src/stores/dashboard/ModalStore.ts +++ b/src/stores/dashboard/ModalStore.ts @@ -10,6 +10,7 @@ type Record = { export class ModalStoreImpl { isDetailsOpen = false isDeleteOpen = false + isPaymentImportOpen = false selectedRecord: Record = { id: '', name: '', @@ -20,8 +21,10 @@ export class ModalStoreImpl { isDetailsOpen: observable, isDeleteOpen: observable, selectedRecord: observable, + isPaymentImportOpen: observable, setSelectedRecord: action, showDetails: action, + showImport: action, hideDetails: action, showDelete: action, hideDelete: action, @@ -44,6 +47,13 @@ export class ModalStoreImpl { this.isDeleteOpen = false } + showImport = () => { + this.isPaymentImportOpen = true + } + + hideImport = () => { + this.isPaymentImportOpen = false + } setSelectedRecord = (record: Record) => { this.selectedRecord = record } From 827f11400c6c6c7c902ca9d6ae8ef616ae924ab5 Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Tue, 25 Jun 2024 15:45:47 +0300 Subject: [PATCH 07/11] chore: Set IndexPage component back to default / route --- src/pages/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index a34fe19b8..7b7fcf4b4 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -9,7 +9,6 @@ import { CampaignResponse } from 'gql/campaigns' import { endpoints } from 'service/apiEndpoints' import { authOptions } from './api/auth/[...nextauth]' -import CreatePaymentDialog from 'components/admin/payments/store/CreatePaymentContext' export const getServerSideProps: GetServerSideProps<{ session: Session | null @@ -39,4 +38,4 @@ export const getServerSideProps: GetServerSideProps<{ } } -export default CreatePaymentDialog +export default IndexPage From 14e99d148a03c860446b8b3d262cb53b58741cdb Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Tue, 25 Jun 2024 17:26:51 +0300 Subject: [PATCH 08/11] chore: Add Alerts if import operation is successfull or not --- .../CreatePaymentFromBenevityForm.tsx | 26 +++++++++++++++---- .../stripe/StripeCreatePaymentDialog.tsx | 21 ++++++++++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx b/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx index 41e94b57d..5450464ac 100644 --- a/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx +++ b/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx @@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query' import { useImportBenevityDonation } from 'service/donation' import { CreatePaymentStore, benevityInitialValues } from '../../store/CreatePaymentStore' import { useFormikContext } from 'formik' -import { AxiosResponse } from 'axios' +import { AxiosError, AxiosResponse } from 'axios' import { TPaymentResponse } from 'gql/donations' import { BenevityRequest } from './helpers/benevity.types' import { useEffect } from 'react' @@ -13,15 +13,31 @@ import { BenevityImportInput } from './helpers/benevity.types' import BenevityTransactionData from './helpers/form/BenevityTransactionData' import { BenevityDonationsTable } from './helpers/form/BenevityDonationTable' import { BankTransactionsInput } from 'gql/bank-transactions' +import { AlertStore } from 'stores/AlertStore' +import { ApiError } from 'service/apiErrors' +import { useTranslation } from 'next-i18next' +import { ModalStore } from '../../PaymentsPage' type CreatePaymentFromBenevityForm = { data: BankTransactionsInput } export function CreatePaymentFromBenevityForm({ data }: CreatePaymentFromBenevityForm) { - const { hideImportModal } = CreatePaymentStore + const { hideImport } = ModalStore + const { t } = useTranslation() - const benevityMutation = useMutation, unknown, BenevityRequest>({ + const benevityMutation = useMutation< + AxiosResponse, + AxiosError, + BenevityRequest + >({ mutationFn: useImportBenevityDonation(), + onSuccess: () => { + AlertStore.show(t('common:alerts.success'), 'success') + hideImport() + }, + onError: (error: AxiosError) => { + AlertStore.show(error.message, 'error') + }, }) const { values, setFieldValue } = useFormikContext() @@ -67,11 +83,11 @@ export function CreatePaymentFromBenevityForm({ data }: CreatePaymentFromBenevit - diff --git a/src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx b/src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx index 4b8b7eee1..3b4630a6d 100644 --- a/src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx +++ b/src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx @@ -8,7 +8,7 @@ import { Typography, } from '@mui/material' import { useMutation } from '@tanstack/react-query' -import { AxiosResponse } from 'axios' +import { AxiosError, AxiosResponse } from 'axios' import { money } from 'common/util/money' import { stripeFeeCalculator } from 'components/client/one-time-donation/helpers/stripe-fee-calculator' @@ -18,6 +18,9 @@ import { useTranslation } from 'next-i18next' import { endpoints } from 'service/apiEndpoints' import { useCreatePaymentFromStripeMutation } from 'service/donation' import Stripe from 'stripe' +import { AlertStore } from 'stores/AlertStore' +import { ApiError } from 'service/apiErrors' +import { ModalStore } from '../../PaymentsPage' type CreateUpdatePaymentFromStripeChargeProps = { data: StripeChargeResponse @@ -29,13 +32,23 @@ export function CreatePaymentFromStripeChargeTable({ id, }: CreateUpdatePaymentFromStripeChargeProps) { const { t } = useTranslation() - - const stripeMutation = useMutation, unknown, Stripe.Charge>({ + const { hideImport } = ModalStore + const stripeMutation = useMutation< + AxiosResponse, + AxiosError, + Stripe.Charge + >({ mutationFn: useCreatePaymentFromStripeMutation(), + onSuccess: () => { + AlertStore.show(t('common:alerts.success'), 'success') + hideImport() + }, + onError: (error: AxiosError) => { + AlertStore.show(error.message, 'error') + }, }) const handleSubmit = () => { - console.log(`HandleSubmit is called`) stripeMutation.mutate(data.stripe) } From c64dc85dacad660639d899587ce91541da739aa1 Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Tue, 25 Jun 2024 18:00:59 +0300 Subject: [PATCH 09/11] chore: Add Back controller to the Create Payment modal --- .../create-payment/CreatePaymentStepper.tsx | 52 ++++++++++++++----- .../store/createPaymentStepReducer.ts | 8 +++ src/pages/admin/payments/index.tsx | 2 +- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx b/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx index 6f8bbbcb2..b39cb9c99 100644 --- a/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx +++ b/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, Dialog } from '@mui/material' +import { Button, Card, CardContent, Dialog, Grid, IconButton } from '@mui/material' import { Form, Formik, useField, useFormikContext } from 'formik' import React, { useContext } from 'react' @@ -15,7 +15,8 @@ import { StripeChargeLookupForm } from './stripe/StripeChargeLookupForm' import { SelectedPaymentSource } from '../store/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 > @@ -63,21 +64,46 @@ function CreatePaymentStepper() { dispatch({ type: 'RESET_MODAL' }) hideImport() } + + const handleOnBackClick = () => { + dispatch({ type: 'DECREMENT_STEP' }) + } return ( - { - if (payment.step < steps[payment.paymentSource].length - 1) { - dispatch({ type: 'INCREMENT_STEP' }) - } - }} - initialValues={{}} - validationSchema={steps[payment.paymentSource][payment.step].validation}> -
{steps[payment.paymentSource][payment.step].component}
-
+ + + + + + + + + { + if (payment.step < steps[payment.paymentSource].length - 1) { + dispatch({ type: 'INCREMENT_STEP' }) + } + }} + initialValues={{}} + validationSchema={steps[payment.paymentSource][payment.step].validation}> +
{steps[payment.paymentSource][payment.step].component}
+
+
+
diff --git a/src/components/admin/payments/store/createPaymentStepReducer.ts b/src/components/admin/payments/store/createPaymentStepReducer.ts index ab01a3071..d78645c7c 100644 --- a/src/components/admin/payments/store/createPaymentStepReducer.ts +++ b/src/components/admin/payments/store/createPaymentStepReducer.ts @@ -3,6 +3,7 @@ import { useReducer } from 'react' export const ACTIONS = { CREATE_PAYMENT: 'CREATE_PAYMENT', INCREMENT_STEP: 'INCREMENT_STEP', + DECREMENT_STEP: 'DECREMENT_STEP', UPDATE_PAYMENT_SOURCE: 'UPDATE_PAYMENT_SOURCE', RESET_MODAL: 'RESET_MODAL', SET_BENEVITY_IMPORT_TYPE: 'SET_BENEVITY_IMPORT_TYPE', @@ -55,6 +56,13 @@ function reducer(state: CreatePayment, actions: Actions): CreatePayment { benevityImportType: actions.payload as BenevityImportType, step: 1, } + case 'DECREMENT_STEP': + return { + ...state, + paymentSource: + state.paymentSource !== 'none' && state.step === 0 ? 'none' : state.paymentSource, + step: state.step - 1 > 1 ? state.step-- : 0, + } default: return state } diff --git a/src/pages/admin/payments/index.tsx b/src/pages/admin/payments/index.tsx index 9ac7e137d..46ff81af4 100644 --- a/src/pages/admin/payments/index.tsx +++ b/src/pages/admin/payments/index.tsx @@ -3,7 +3,7 @@ import { securedAdminProps } from 'middleware/auth/securedProps' import { endpoints } from 'service/apiEndpoints' export const getServerSideProps = securedAdminProps( - ['common', 'auth', 'admin', 'donations', 'validation'], + ['common', 'auth', 'admin', 'donations', 'validation', 'profile'], () => endpoints.payments.list(undefined, undefined, { pageIndex: 0, pageSize: 20 }).url, ) From 9c34ce1782ad1579271e39d982c73ce98ffe5485 Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Tue, 25 Jun 2024 18:27:33 +0300 Subject: [PATCH 10/11] chore: Remove obsolete file --- .../bank-transactions/modals/FileImportDialog.tsx | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/components/admin/bank-transactions/modals/FileImportDialog.tsx diff --git a/src/components/admin/bank-transactions/modals/FileImportDialog.tsx b/src/components/admin/bank-transactions/modals/FileImportDialog.tsx deleted file mode 100644 index 281a00f57..000000000 --- a/src/components/admin/bank-transactions/modals/FileImportDialog.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Dialog } from '@mui/material' -import React from 'react' -import { CreatePaymentStore } from '../store/BenevityImportStore' - -export default function FileImportDialog() { - const { isImportModalOpen, importType } = CreatePaymentStore - return ( - - FileImportDialog - - ) -} From 991dda10af13ea392bdfd4cf8703eadbc2a07252 Mon Sep 17 00:00:00 2001 From: Alexander Petkov Date: Tue, 25 Jun 2024 19:07:10 +0300 Subject: [PATCH 11/11] refactor: Additional restructure and cleanups --- src/common/hooks/donation.ts | 2 +- .../CreatePaymentDialog.tsx} | 8 +- .../create-payment/CreatePaymentStepper.tsx | 6 +- .../create-payment/PaymentTypeSelector.tsx | 4 +- .../benevity/BenevityEditableInput.tsx | 2 +- .../benevity/BenevityImportTypeSelector.tsx | 6 +- .../CreatePaymentFromBenevityForm.tsx | 2 +- .../benevity/FileImportDialog.tsx | 4 +- .../helpers/form/BenevityDonationTable.tsx | 2 +- .../benevity/helpers/form/initialValues.ts | 45 +++++++++++ .../helpers}/createPaymentStepReducer.ts | 0 .../stripe/StripeCreatePaymentDialog.tsx | 3 +- src/components/admin/payments/grid/Grid.tsx | 4 +- .../admin/payments/grid/GridAppbar.tsx | 1 - .../payments/store/CreatePaymentStore.ts | 80 ------------------- 15 files changed, 68 insertions(+), 101 deletions(-) rename src/components/admin/payments/{store/CreatePaymentContext.tsx => create-payment/CreatePaymentDialog.tsx} (76%) create mode 100644 src/components/admin/payments/create-payment/benevity/helpers/form/initialValues.ts rename src/components/admin/payments/{store => create-payment/helpers}/createPaymentStepReducer.ts (100%) delete mode 100644 src/components/admin/payments/store/CreatePaymentStore.ts diff --git a/src/common/hooks/donation.ts b/src/common/hooks/donation.ts index 8cf984612..2232fbb07 100644 --- a/src/common/hooks/donation.ts +++ b/src/common/hooks/donation.ts @@ -6,7 +6,7 @@ import { QueryClient, useMutation, useQuery } from '@tanstack/react-query' import { ApiErrors } from 'service/apiErrors' import { AlertStore } from 'stores/AlertStore' import { endpoints } from 'service/apiEndpoints' -import { authConfig, authQueryFnFactory } from 'service/restRequests' +import { authQueryFnFactory } from 'service/restRequests' import { CheckoutSessionInput, CheckoutSessionResponse, diff --git a/src/components/admin/payments/store/CreatePaymentContext.tsx b/src/components/admin/payments/create-payment/CreatePaymentDialog.tsx similarity index 76% rename from src/components/admin/payments/store/CreatePaymentContext.tsx rename to src/components/admin/payments/create-payment/CreatePaymentDialog.tsx index e5b57fdb2..738685d13 100644 --- a/src/components/admin/payments/store/CreatePaymentContext.tsx +++ b/src/components/admin/payments/create-payment/CreatePaymentDialog.tsx @@ -1,6 +1,10 @@ import React, { createContext } from 'react' -import { Actions, CreatePayment, createPaymentStepReducer } from './createPaymentStepReducer' -import CreatePaymentStepper from '../create-payment/CreatePaymentStepper' +import { + Actions, + CreatePayment, + createPaymentStepReducer, +} from './helpers/createPaymentStepReducer' +import CreatePaymentStepper from './CreatePaymentStepper' import { observer } from 'mobx-react' export type PaymentContext = { diff --git a/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx b/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx index b39cb9c99..8f714568b 100644 --- a/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx +++ b/src/components/admin/payments/create-payment/CreatePaymentStepper.tsx @@ -1,18 +1,18 @@ import { Button, Card, CardContent, Dialog, Grid, IconButton } from '@mui/material' -import { Form, Formik, useField, useFormikContext } from 'formik' +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 '../store/CreatePaymentContext' +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 '../store/createPaymentStepReducer' +import { SelectedPaymentSource } from './helpers/createPaymentStepReducer' import { CreatePaymentFromStripeCharge } from './stripe/CreatePaymentFromStripeCharge' import { ModalStore } from '../PaymentsPage' import ArrowBackIcon from '@mui/icons-material/ArrowBack' diff --git a/src/components/admin/payments/create-payment/PaymentTypeSelector.tsx b/src/components/admin/payments/create-payment/PaymentTypeSelector.tsx index 265a5364d..50d84bfc2 100644 --- a/src/components/admin/payments/create-payment/PaymentTypeSelector.tsx +++ b/src/components/admin/payments/create-payment/PaymentTypeSelector.tsx @@ -1,8 +1,8 @@ import { Box, Button, Typography } from '@mui/material' import { useTranslation } from 'next-i18next' import React, { useContext } from 'react' -import { SelectedPaymentSource } from '../store/createPaymentStepReducer' -import { PaymentContext } from '../store/CreatePaymentContext' +import { SelectedPaymentSource } from './helpers/createPaymentStepReducer' +import { PaymentContext } from './CreatePaymentDialog' export default function PaymentTypeSelector() { const paymentContext = useContext(PaymentContext) diff --git a/src/components/admin/payments/create-payment/benevity/BenevityEditableInput.tsx b/src/components/admin/payments/create-payment/benevity/BenevityEditableInput.tsx index a453744aa..c5dc374a5 100644 --- a/src/components/admin/payments/create-payment/benevity/BenevityEditableInput.tsx +++ b/src/components/admin/payments/create-payment/benevity/BenevityEditableInput.tsx @@ -16,7 +16,7 @@ export const BenevityInput = ({ }) => { const { t } = useTranslation() const [editable, setEditable] = useState(false) - const [field, meta, { setValue }] = useField(name) + const [field, meta] = useField(name) const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' const ref = useRef(null) diff --git a/src/components/admin/payments/create-payment/benevity/BenevityImportTypeSelector.tsx b/src/components/admin/payments/create-payment/benevity/BenevityImportTypeSelector.tsx index a2d12b1f5..b0f3b1bfc 100644 --- a/src/components/admin/payments/create-payment/benevity/BenevityImportTypeSelector.tsx +++ b/src/components/admin/payments/create-payment/benevity/BenevityImportTypeSelector.tsx @@ -1,15 +1,15 @@ import { Box, Button, Typography } from '@mui/material' import { useTranslation } from 'next-i18next' -import { CreatePaymentStore, TImportType } from '../../store/CreatePaymentStore' import { observer } from 'mobx-react' import { useContext } from 'react' -import { PaymentContext } from '../../store/CreatePaymentContext' +import { PaymentContext } from '../CreatePaymentDialog' +import { BenevityImportType } from '../helpers/createPaymentStepReducer' function BenevityImportFirstStep() { const { t } = useTranslation() const paymentContext = useContext(PaymentContext) - const handleImportTypeChange = (importType: TImportType) => { + const handleImportTypeChange = (importType: BenevityImportType) => { paymentContext.dispatch({ type: 'SET_BENEVITY_IMPORT_TYPE', payload: importType }) } diff --git a/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx b/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx index 5450464ac..3d6cba0b0 100644 --- a/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx +++ b/src/components/admin/payments/create-payment/benevity/CreatePaymentFromBenevityForm.tsx @@ -1,7 +1,7 @@ import { Button, Grid, Typography } from '@mui/material' import { useMutation } from '@tanstack/react-query' import { useImportBenevityDonation } from 'service/donation' -import { CreatePaymentStore, benevityInitialValues } from '../../store/CreatePaymentStore' +import { benevityInitialValues } from './helpers/form/initialValues' import { useFormikContext } from 'formik' import { AxiosError, AxiosResponse } from 'axios' import { TPaymentResponse } from 'gql/donations' diff --git a/src/components/admin/payments/create-payment/benevity/FileImportDialog.tsx b/src/components/admin/payments/create-payment/benevity/FileImportDialog.tsx index fd2980df7..f98bd873b 100644 --- a/src/components/admin/payments/create-payment/benevity/FileImportDialog.tsx +++ b/src/components/admin/payments/create-payment/benevity/FileImportDialog.tsx @@ -4,10 +4,10 @@ import { useField } from 'formik' import { useRef, useState } from 'react' export function FileImportDialog() { - const [isDragging, setIsDragging] = useState(false) + const [, setIsDragging] = useState(false) const inputFile = useRef(null) const submitButtonRef = useRef(null) - const [field, meta, { setValue }] = useField('benevityData') + const [, , { setValue }] = useField('benevityData') const onDragOver = (event: React.DragEvent) => { event.preventDefault() diff --git a/src/components/admin/payments/create-payment/benevity/helpers/form/BenevityDonationTable.tsx b/src/components/admin/payments/create-payment/benevity/helpers/form/BenevityDonationTable.tsx index fd47287f2..28c9a8c90 100644 --- a/src/components/admin/payments/create-payment/benevity/helpers/form/BenevityDonationTable.tsx +++ b/src/components/admin/payments/create-payment/benevity/helpers/form/BenevityDonationTable.tsx @@ -14,7 +14,7 @@ import { FieldArray, useFormikContext } from 'formik' import { BenevityImportInput } from '../benevity.types' import { Delete } from '@mui/icons-material' -import { benevityDonationInitialValues } from '../../../../store/CreatePaymentStore' +import { benevityDonationInitialValues } from './initialValues' import { BenevityInput } from '../../BenevityEditableInput' import AddCircleIcon from '@mui/icons-material/AddCircle' diff --git a/src/components/admin/payments/create-payment/benevity/helpers/form/initialValues.ts b/src/components/admin/payments/create-payment/benevity/helpers/form/initialValues.ts new file mode 100644 index 000000000..59fc02f6f --- /dev/null +++ b/src/components/admin/payments/create-payment/benevity/helpers/form/initialValues.ts @@ -0,0 +1,45 @@ +import { TBenevityCSVParser, TBenevityDonation } from '../benevity.types' + +export const benevityInitialValues: TBenevityCSVParser = { + charityId: '', + charityName: '', + currency: '', + donations: [], + periodEnding: '', + paymentMethod: '', + disbursementId: '', + checkFee: '', + totalDonationsGross: '', + netTotalPayment: 0, + transactionAmount: 0, + exchangeRate: 0, + note: '', +} + +export const benevityDonationInitialValues: TBenevityDonation = { + company: '', + project: '', + donationDate: '', + donorFirstName: '', + donorLastName: '', + email: '', + address: '', + city: '', + stateProvince: '', + postalCode: '', + activity: '', + comment: '', + transactionId: '', + donationFrequency: '', + currency: '', + projectRemoteId: '', + source: '', + reason: '', + totalDonationToBeAcknowledged: 0, + matchAmount: 0, + causeSupportFee: 0, + merchantFee: 0, + feeComment: '', + totalFee: 0, + totalAmount: 0, +} diff --git a/src/components/admin/payments/store/createPaymentStepReducer.ts b/src/components/admin/payments/create-payment/helpers/createPaymentStepReducer.ts similarity index 100% rename from src/components/admin/payments/store/createPaymentStepReducer.ts rename to src/components/admin/payments/create-payment/helpers/createPaymentStepReducer.ts diff --git a/src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx b/src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx index 3b4630a6d..42a454af2 100644 --- a/src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx +++ b/src/components/admin/payments/create-payment/stripe/StripeCreatePaymentDialog.tsx @@ -15,7 +15,6 @@ import { stripeFeeCalculator } from 'components/client/one-time-donation/helpers import { StripeChargeResponse, TPaymentResponse } from 'gql/donations' import React from 'react' import { useTranslation } from 'next-i18next' -import { endpoints } from 'service/apiEndpoints' import { useCreatePaymentFromStripeMutation } from 'service/donation' import Stripe from 'stripe' import { AlertStore } from 'stores/AlertStore' @@ -73,7 +72,7 @@ export function CreatePaymentFromStripeChargeTable({ - + База данни на Страйп Вътрена база данни diff --git a/src/components/admin/payments/grid/Grid.tsx b/src/components/admin/payments/grid/Grid.tsx index 2d184aefb..790881774 100644 --- a/src/components/admin/payments/grid/Grid.tsx +++ b/src/components/admin/payments/grid/Grid.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { UseQueryResult } from '@tanstack/react-query' import { useTranslation } from 'next-i18next' -import { Box, Button, IconButton, Tooltip } from '@mui/material' +import { Box, IconButton, Tooltip } from '@mui/material' import { Edit } from '@mui/icons-material' import { DataGrid, @@ -30,7 +30,7 @@ import { PaymentStatus, PaymentProvider } from '../../../../gql/donations.enums' import { useSession } from 'next-auth/react' import { PaymentAdminResponse } from 'gql/donations' import Link from 'next/link' -import CreatePaymentDialog from '../store/CreatePaymentContext' +import CreatePaymentDialog from '../create-payment/CreatePaymentDialog' interface RenderCellProps { params: GridRenderCellParams diff --git a/src/components/admin/payments/grid/GridAppbar.tsx b/src/components/admin/payments/grid/GridAppbar.tsx index a63a65e87..6b1d80a93 100644 --- a/src/components/admin/payments/grid/GridAppbar.tsx +++ b/src/components/admin/payments/grid/GridAppbar.tsx @@ -2,7 +2,6 @@ import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' import { Box, - Button, FormControl, IconButton, InputLabel, diff --git a/src/components/admin/payments/store/CreatePaymentStore.ts b/src/components/admin/payments/store/CreatePaymentStore.ts deleted file mode 100644 index 9d1768e50..000000000 --- a/src/components/admin/payments/store/CreatePaymentStore.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { action, makeObservable, observable } from 'mobx' -import { enableStaticRendering } from 'mobx-react' -import type { - TBenevityCSVParser, - TBenevityDonation, -} from '../create-payment/benevity/helpers/benevity.types' - -enableStaticRendering(typeof window === 'undefined') - -export const benevityInitialValues: TBenevityCSVParser = { - charityId: '', - charityName: '', - currency: '', - donations: [], - periodEnding: '', - paymentMethod: '', - disbursementId: '', - checkFee: '', - totalDonationsGross: '', - netTotalPayment: 0, - transactionAmount: 0, - exchangeRate: 0, - note: '', -} - -export const benevityDonationInitialValues: TBenevityDonation = { - company: '', - project: '', - donationDate: '', - donorFirstName: '', - donorLastName: '', - email: '', - address: '', - city: '', - stateProvince: '', - postalCode: '', - activity: '', - comment: '', - transactionId: '', - donationFrequency: '', - currency: '', - projectRemoteId: '', - source: '', - reason: '', - totalDonationToBeAcknowledged: 0, - matchAmount: 0, - causeSupportFee: 0, - merchantFee: 0, - feeComment: '', - totalFee: 0, - totalAmount: 0, -} - -interface ICreatePaymentStoreImpl { - isImportModalOpen: boolean - showImportModal: () => void - hideImportModal: () => void -} - -export class CreatePaymentStoreImpl implements ICreatePaymentStoreImpl { - isImportModalOpen = true - - constructor() { - makeObservable(this, { - isImportModalOpen: observable, - showImportModal: action, - hideImportModal: action, - }) - } - - showImportModal = () => { - this.isImportModalOpen = true - } - - hideImportModal = () => { - this.isImportModalOpen = false - } -} - -export const CreatePaymentStore = new CreatePaymentStoreImpl()