diff --git a/public/locales/bg/donations.json b/public/locales/bg/donations.json index 036db7acb..1e34bafac 100644 --- a/public/locales/bg/donations.json +++ b/public/locales/bg/donations.json @@ -32,6 +32,7 @@ "edit": "Дарението беше редактирано успешно!", "delete": "Дарението беше изтрито успешно!", "refundSuccess": "Беше създадена успешна заявка за връщане на парите към Stripe.", + "invalidate": "Дарението беше маркирано като невалидно!", "editDonor": "Дарителят беше редактиран успешно!", "error": "Възникна грешка! Моля опитайте отново по-късно.", "requiredError": "Полето е задължително." @@ -66,5 +67,14 @@ "amount": "Сума:", "email": "Е-mail на дарител:", "confirm-button": "Възстанови парите от дарение" + }, + "invalidate": { + "icon": "Маркирай като невалидно", + "title": "Маркиране на дарение като невалидно", + "confirmation": "Сигурни ли сте, че искате да маркирате като невалидно дарението:", + "number": "Номер:", + "amount": "Сума:", + "email": "Е-mail на дарител:", + "confirm-button": "Маркирай като невалидно" } } diff --git a/public/locales/en/donations.json b/public/locales/en/donations.json index a87b2076f..f912bfdb4 100644 --- a/public/locales/en/donations.json +++ b/public/locales/en/donations.json @@ -32,6 +32,7 @@ "edit": "Document has been edited successfully!", "delete": "Document has been deleted successfully!", "refundSuccess": "A successful refund request was created with Stripe.", + "invalidate": "Document has been marked as invalid successfully!", "editDonor": "Donation donor has been edited successfully!", "error": "An error has occured! Please try again later.", "requiredError": "Fields is required!" @@ -66,5 +67,14 @@ "amount": "Amount:", "email": "Donator's email:", "confirm-button": "Refund a donation" + }, + "invalidate": { + "icon": "Mark as invalid", + "title": "Mark as invalid", + "confirmation": "Are you sure you want to refund the donation:", + "number": "Number:", + "amount": "Amount:", + "email": "Е-mail", + "confirm-button": "Mark as invalid" } } diff --git a/src/components/admin/donations/DonationsPage.tsx b/src/components/admin/donations/DonationsPage.tsx index 88e1a041f..b4678295d 100644 --- a/src/components/admin/donations/DonationsPage.tsx +++ b/src/components/admin/donations/DonationsPage.tsx @@ -9,6 +9,7 @@ import { RefundStoreImpl } from './store/RefundStore' export const ModalStore = new ModalStoreImpl() export const RefundStore = new RefundStoreImpl() +export const InvalidateStore = new ModalStoreImpl() export default function DocumentsPage() { const { t } = useTranslation() diff --git a/src/components/admin/donations/grid/Grid.tsx b/src/components/admin/donations/grid/Grid.tsx index 5899f3c6f..81bb99621 100644 --- a/src/components/admin/donations/grid/Grid.tsx +++ b/src/components/admin/donations/grid/Grid.tsx @@ -14,8 +14,8 @@ import { observer } from 'mobx-react' import { useDonationsList } from 'common/hooks/donation' import DetailsModal from '../modals/DetailsModal' -import DeleteModal from '../modals/DeleteModal' -import { ModalStore, RefundStore } from '../DonationsPage' +import InvalidateModal from '../modals/InvalidateModal' +import { ModalStore, RefundStore, InvalidateStore } from '../DonationsPage' import { getExactDateTime } from 'common/util/date' import { useRouter } from 'next/router' import { money } from 'common/util/money' @@ -26,8 +26,10 @@ import RenderEditPersonCell from './RenderEditPersonCell' import { useStores } from '../../../../common/hooks/useStores' import RenderEditBillingEmailCell from './RenderEditBillingEmailCell' import RestoreIcon from '@mui/icons-material/Restore' +import CancelIcon from '@mui/icons-material/Cancel' import RefundModal from '../modals/RefundModal' import { DonationStatus, PaymentProvider } from '../../../../gql/donations.enums' +import { useSession } from 'next-auth/react' interface RenderCellProps { params: GridRenderCellParams @@ -51,8 +53,16 @@ export default observer(function Grid() { const router = useRouter() const { isDetailsOpen } = ModalStore const { isRefundOpen } = RefundStore + const { + isDeleteOpen, + setSelectedRecord: setInvalidateRecord, + showDelete: showInvalidate, + } = InvalidateStore const campaignId = router.query.campaignId as string | undefined - + const { data: session } = useSession() + const canEditFinancials = session?.user?.realm_access?.roles.includes( + 'account-edit-financials-requests', + ) const { data: { items: donations, total: allDonationsCount } = { items: [], total: 0 }, // error: donationHistoryError, @@ -147,6 +157,11 @@ export default observer(function Grid() { showRefund() } + function invalidateClickHandler(id: string) { + setInvalidateRecord({ id, name: '' }) + showInvalidate() + } + const columns: GridColDef[] = [ { field: 'actions', @@ -155,20 +170,31 @@ export default observer(function Grid() { width: 120, resizable: false, renderCell: (params: GridRenderCellParams) => { - return params.row?.status === DonationStatus.succeeded && - params.row?.provider === PaymentProvider.stripe ? ( + if (!canEditFinancials || params.row?.status !== DonationStatus.succeeded) { + return '' + } + + return ( <> - + {params.row.provider === PaymentProvider.stripe && ( + + refundClickHandler(params.row.id)}> + + + + )} + refundClickHandler(params.row.id)}> - + onClick={() => invalidateClickHandler(params.row.id)}> + - ) : ( - '' ) }, }, @@ -292,7 +318,7 @@ export default observer(function Grid() { {/* making sure we don't sent requests to the API when not needed */} {isDetailsOpen && } {isRefundOpen && } - + {isDeleteOpen && } ) }) diff --git a/src/components/admin/donations/modals/InvalidateModal.tsx b/src/components/admin/donations/modals/InvalidateModal.tsx new file mode 100644 index 000000000..226ee569d --- /dev/null +++ b/src/components/admin/donations/modals/InvalidateModal.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { useRouter } from 'next/router' +import { useMutation } from '@tanstack/react-query' +import { observer } from 'mobx-react' +import { AxiosError, AxiosResponse } from 'axios' +import { useTranslation } from 'next-i18next' + +import { DonationResponse } from 'gql/donations' +import { ApiErrors } from 'service/apiErrors' +import { AlertStore } from 'stores/AlertStore' +import { routes } from 'common/routes' +import DeleteDialog from 'components/admin/DeleteDialog' +import { useInvalidateStripeDonation } from 'service/donation' + +import { InvalidateStore } from '../DonationsPage' + +type Props = { + onUpdate: () => void +} + +export default observer(function InvalidateModal({ onUpdate }: Props) { + const router = useRouter() + const { hideDelete, selectedRecord } = InvalidateStore + const { t } = useTranslation() + + const mutationFn = useInvalidateStripeDonation() + + const invalidateMutation = useMutation< + AxiosResponse, + AxiosError, + string + >({ + mutationFn, + onError: () => AlertStore.show(t('donations:alerts:error'), 'error'), + onSuccess: () => { + hideDelete() + AlertStore.show(t('donations:alerts:invalidate'), 'success') + router.push(routes.admin.donations.index) + onUpdate() + }, + }) + + function invalidateHandler() { + invalidateMutation.mutate(selectedRecord.id) + } + + return +}) diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index a18cee857..2b892a82a 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -118,6 +118,8 @@ export const endpoints = { createBankDonation: { url: '/donation/create-bank-payment', method: 'POST' }, refundStripePayment: (id: string) => { url: `/donation/refund-stripe-payment/${id}`, method: 'POST' }, + invalidateStripePayment: (id: string) => + { url: `/donation/${id}/invalidate`, method: 'PATCH' }, getDonation: (id: string) => { url: `/donation/${id}`, method: 'GET' }, donationsList: ( campaignId?: string, diff --git a/src/service/donation.ts b/src/service/donation.ts index 6090471fb..f1dba2b4f 100644 --- a/src/service/donation.ts +++ b/src/service/donation.ts @@ -78,6 +78,16 @@ export const useRefundStripeDonation = () => { ) } } +export const useInvalidateStripeDonation = () => { + const { data: session } = useSession() + return async (extPaymentId: string) => { + return await apiClient.patch( + endpoints.donation.invalidateStripePayment(extPaymentId).url, + '', + authConfig(session?.accessToken), + ) + } +} export const useUploadBankTransactionsFiles = () => { const { data: session } = useSession()