From 5aef92ce8b03448a87f4a77b2c7b851907b23a6c Mon Sep 17 00:00:00 2001 From: dev-rminds Date: Wed, 14 Jan 2026 20:22:46 +0200 Subject: [PATCH 01/14] clean removed dependency param from organizations endpoint --- react/src/dashboard/contexts/MainContext.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/react/src/dashboard/contexts/MainContext.tsx b/react/src/dashboard/contexts/MainContext.tsx index 1c068bfd2..089f5bbd0 100644 --- a/react/src/dashboard/contexts/MainContext.tsx +++ b/react/src/dashboard/contexts/MainContext.tsx @@ -46,7 +46,6 @@ const MainProvider = ({ children }: { children: React.ReactElement }) => { if (envData && authIdentity) { return organizationService .list({ - dependency: 'permissions,logo', order_by: `is_${envData.client_type}`, order_dir: 'desc', per_page: 500, From 352168fba8002b772c7829618a9e85bcad5994fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 08:06:43 +0000 Subject: [PATCH 02/14] Bump diff in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [diff](https://github.com/kpdecker/jsdiff). Updates `diff` from 8.0.2 to 8.0.3 - [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md) - [Commits](https://github.com/kpdecker/jsdiff/compare/v8.0.2...v8.0.3) --- updated-dependencies: - dependency-name: diff dependency-version: 8.0.3 dependency-type: direct:production dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22a7bd66d..6ef44f51e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "clean-webpack-plugin": "^4.0.0", "date-fns": "^4.1.0", "deepl-node": "^1.15.0", - "diff": "^8.0.2", + "diff": "^8.0.3", "dompurify": "^3.2.4", "easyqrcodejs": "^4.4.13", "file-saver": "^2.0.5", @@ -6766,9 +6766,9 @@ "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==" }, "node_modules/diff": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" diff --git a/package.json b/package.json index 7226c5f1d..7bd410ec3 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "clean-webpack-plugin": "^4.0.0", "date-fns": "^4.1.0", "deepl-node": "^1.15.0", - "diff": "^8.0.2", + "diff": "^8.0.3", "dompurify": "^3.2.4", "easyqrcodejs": "^4.4.13", "file-saver": "^2.0.5", From 1a698701c3aa3bf67fab1c9f7e438543ea2e30c8 Mon Sep 17 00:00:00 2001 From: dev-rminds Date: Thu, 15 Jan 2026 16:03:28 +0200 Subject: [PATCH 03/14] fix employee and reservations page double api fetch --- react/src/dashboard/components/pages/employees/Employees.tsx | 1 + .../src/dashboard/components/pages/reservations/Reservations.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/react/src/dashboard/components/pages/employees/Employees.tsx b/react/src/dashboard/components/pages/employees/Employees.tsx index a3b8b291e..52b77f789 100644 --- a/react/src/dashboard/components/pages/employees/Employees.tsx +++ b/react/src/dashboard/components/pages/employees/Employees.tsx @@ -66,6 +66,7 @@ export default function Employees() { { q: '', per_page: paginatorService.getPerPage(paginatorKey), + page: 1, }, { queryParams: { diff --git a/react/src/dashboard/components/pages/reservations/Reservations.tsx b/react/src/dashboard/components/pages/reservations/Reservations.tsx index 417e7b1ca..60ef65ea6 100644 --- a/react/src/dashboard/components/pages/reservations/Reservations.tsx +++ b/react/src/dashboard/components/pages/reservations/Reservations.tsx @@ -68,6 +68,7 @@ export default function Reservations() { order_by: 'created_at', order_dir: 'desc', per_page: paginatorService.getPerPage(paginatorKey), + page: 1, }, { queryParams: { From 0c6c5afd8d724d4dffb9c1c41e67ea23f9b24aa2 Mon Sep 17 00:00:00 2001 From: dev-rminds Date: Mon, 19 Jan 2026 04:45:00 +0200 Subject: [PATCH 04/14] voucher payouts - round 2 --- .../components/modals/ModalPayoutEdit.tsx | 191 +++++++++++++++++- .../components/pages/payouts/Payouts.tsx | 7 +- .../pages/payouts/elements/PayoutsTable.tsx | 1 + .../elements/VoucherTransactionsCard.tsx | 1 + .../i18n/nl/modals/modal-payout-create.js | 7 + .../elements/aside/LayoutAsideSponsor.tsx | 1 + .../props/models/PayoutBankAccount.tsx | 7 + .../props/models/Sponsor/SponsorIdentity.tsx | 4 +- .../services/PayoutTransactionService.ts | 5 + .../components/modals/ModalVoucherPayout.tsx | 74 ++++++- .../components/pages/payouts/Payouts.tsx | 5 +- .../pages/products-show/ProductsShow.tsx | 4 +- .../components/pages/profile/Profile.tsx | 6 + .../vouchers-show/elements/VoucherActions.tsx | 4 +- .../hooks/usePayoutEligibleVouchers.ts | 15 +- .../hooks/useFundRequestBankAccounts.ts | 32 +++ react/src/webshop/i18n/nl/pages/profile.mjs | 1 + 17 files changed, 341 insertions(+), 24 deletions(-) create mode 100644 react/src/dashboard/props/models/PayoutBankAccount.tsx create mode 100644 react/src/webshop/hooks/useFundRequestBankAccounts.ts diff --git a/react/src/dashboard/components/modals/ModalPayoutEdit.tsx b/react/src/dashboard/components/modals/ModalPayoutEdit.tsx index 23dd7c923..92d583889 100644 --- a/react/src/dashboard/components/modals/ModalPayoutEdit.tsx +++ b/react/src/dashboard/components/modals/ModalPayoutEdit.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { ModalState } from '../../modules/modals/context/ModalContext'; import useFormBuilder from '../../hooks/useFormBuilder'; import Fund from '../../props/models/Fund'; @@ -12,8 +12,17 @@ import FormGroup from '../elements/forms/elements/FormGroup'; import usePushApiError from '../../hooks/usePushApiError'; import usePayoutTransactionService from '../../services/PayoutTransactionService'; import PayoutTransaction from '../../props/models/PayoutTransaction'; +import PayoutBankAccount from '../../props/models/PayoutBankAccount'; type AmountType = 'custom' | 'predefined'; +type BankAccountSource = 'manual' | 'fund_request'; + +type PayoutBankAccountOption = { + id: number | null; + iban?: string; + iban_name?: string; + label: string; +}; export default function ModalPayoutEdit({ funds, @@ -39,6 +48,9 @@ export default function ModalPayoutEdit({ const payoutTransactionService = usePayoutTransactionService(); const [fund, setFund] = useState(funds?.[0]); + const [bankAccountSource, setBankAccountSource] = useState('manual'); + const [bankAccounts, setBankAccounts] = useState>(null); + const [bankAccountsLoading, setBankAccountsLoading] = useState(false); const assignTypes = useMemo(() => { if (transaction) { @@ -72,6 +84,36 @@ export default function ModalPayoutEdit({ })); }, [fund]); + const bankAccountSourceOptions = useMemo(() => { + return [ + { + key: 'manual', + label: translate('modals.modal_payout_create.options.bank_account_source_manual'), + }, + { + key: 'fund_request', + label: translate('modals.modal_payout_create.options.bank_account_source_fund_request'), + }, + ] as Array<{ key: BankAccountSource; label: string }>; + }, [translate]); + + const bankAccountOptions = useMemo((): Array => { + const options = (bankAccounts || []).map((bankAccount) => ({ + id: bankAccount.id, + iban: bankAccount.iban, + iban_name: bankAccount.iban_name, + label: `Aanvraag #${bankAccount.id} - ${bankAccount.iban} / ${bankAccount.iban_name}`, + })); + + return [ + { + id: null, + label: translate('modals.modal_payout_create.options.bank_account_select_placeholder'), + }, + ...options, + ]; + }, [bankAccounts, translate]); + const form = useFormBuilder<{ target_iban: string; target_name: string; @@ -81,6 +123,7 @@ export default function ModalPayoutEdit({ description: string; email: string; bsn: string; + fund_request_id?: number; }>( { amount: transaction?.amount || '', @@ -95,14 +138,19 @@ export default function ModalPayoutEdit({ description: transaction?.description || '', email: '', bsn: '', + fund_request_id: null, }, (values) => { setProgress(0); const data = { description: values.description, - target_iban: values.target_iban, - target_name: values.target_name, + ...(bankAccountSource === 'fund_request' + ? { fund_request_id: values.fund_request_id || undefined } + : { + target_iban: values.target_iban, + target_name: values.target_name, + }), amount: values.allocate_by === 'custom' ? values.amount : undefined, amount_preset_id: values.allocate_by === 'predefined' ? values.amount_preset_id : undefined, ...{ @@ -136,12 +184,72 @@ export default function ModalPayoutEdit({ }); }, ); + const formUpdate = form.update; + + useEffect(() => { + if (transaction || bankAccountSource !== 'fund_request' || !organization?.id || !fund?.id) { + return; + } + + let canceled = false; + + const fetchBankAccounts = async () => { + formUpdate({ fund_request_id: null, target_iban: '', target_name: '' }); + setProgress(0); + setBankAccountsLoading(true); + + const collected: Array = []; + let page = 1; + let lastPage = 1; + + try { + do { + const res = await payoutTransactionService.bankAccounts(organization.id, { + page, + per_page: 1000, + }); + + collected.push(...(res.data?.data || [])); + lastPage = res.data?.meta?.last_page || page; + page += 1; + } while (!canceled && page <= lastPage); + + if (!canceled) { + setBankAccounts(collected); + } + } catch (err) { + if (!canceled) { + setBankAccounts([]); + pushApiError(err); + } + } finally { + if (!canceled) { + setBankAccountsLoading(false); + setProgress(100); + } + } + }; + + fetchBankAccounts().then(); + + return () => { + canceled = true; + }; + }, [ + bankAccountSource, + fund?.id, + organization?.id, + payoutTransactionService, + pushApiError, + setProgress, + transaction, + formUpdate, + ]); return (
+ className={`modal modal-animated modal-voucher-create ${modal.loading ? 'modal-loading' : ''} ${className}`} + data-dusk="payoutCreateModal">
@@ -159,9 +267,10 @@ export default function ModalPayoutEdit({ value={fund} propValue={'name'} disabled={!!transaction?.id} + dusk="payoutFundSelect" onChange={(fund: Fund) => { setFund(fund); - form.update({ allocate_by: amountOptions?.[0]?.key }); + form.update({ allocate_by: amountOptions?.[0]?.key, fund_request_id: null }); }} options={funds} allowSearch={false} @@ -198,6 +307,7 @@ export default function ModalPayoutEdit({ type={'number'} className="form-control" placeholder={translate('modals.modal_payout_create.labels.amount')} + data-dusk="payoutAmount" value={form.values.amount || ''} step=".01" min="0.01" @@ -257,15 +367,74 @@ export default function ModalPayoutEdit({ /> )} + {!transaction && ( + ( + { + setBankAccountSource(value); + form.update({ + fund_request_id: null, + ...(value === 'fund_request' + ? { target_iban: '', target_name: '' } + : {}), + }); + }} + options={bankAccountSourceOptions} + allowSearch={false} + /> + )} + /> + )} + + {!transaction && bankAccountSource === 'fund_request' && ( + ( + { + const selected = bankAccountOptions.find( + (option) => option.id == fund_request_id, + ); + + form.update({ + fund_request_id, + target_iban: selected?.iban || '', + target_name: selected?.iban_name || '', + }); + }} + options={bankAccountOptions} + allowSearch={true} + disabled={bankAccountsLoading} + /> + )} + error={form.errors?.fund_request_id} + /> + )} + ( form.update({ target_iban: e.target.value })} /> )} @@ -273,14 +442,16 @@ export default function ModalPayoutEdit({ /> ( form.update({ target_name: e.target.value })} /> )} @@ -308,7 +479,7 @@ export default function ModalPayoutEdit({ {translate('modals.modal_payout_create.buttons.cancel')} -
diff --git a/react/src/dashboard/components/pages/payouts/Payouts.tsx b/react/src/dashboard/components/pages/payouts/Payouts.tsx index d0a746a01..48a474342 100644 --- a/react/src/dashboard/components/pages/payouts/Payouts.tsx +++ b/react/src/dashboard/components/pages/payouts/Payouts.tsx @@ -175,7 +175,7 @@ export default function Payouts() { } return ( -
+
{translate('payouts.header.title')} ({transactions.meta.total}) @@ -184,7 +184,10 @@ export default function Payouts() {
{fundsWithPayouts?.length > 0 && ( - diff --git a/react/src/dashboard/components/pages/payouts/elements/PayoutsTable.tsx b/react/src/dashboard/components/pages/payouts/elements/PayoutsTable.tsx index f2eda1f86..2cce14692 100644 --- a/react/src/dashboard/components/pages/payouts/elements/PayoutsTable.tsx +++ b/react/src/dashboard/components/pages/payouts/elements/PayoutsTable.tsx @@ -118,6 +118,7 @@ export default function PayoutsTable({ {transactions.data.map((transaction) => ( + Bekijken
)} diff --git a/react/src/dashboard/i18n/nl/modals/modal-payout-create.js b/react/src/dashboard/i18n/nl/modals/modal-payout-create.js index 1322dbaf8..e16c607f8 100644 --- a/react/src/dashboard/i18n/nl/modals/modal-payout-create.js +++ b/react/src/dashboard/i18n/nl/modals/modal-payout-create.js @@ -5,6 +5,8 @@ export default { allocate_by: 'Toewijzingsmethode', assign_by_type: 'Methode', amount: 'Bedrag', + bank_account_source: 'Bron bankrekening', + bank_account: 'Aanvraag', iban: 'Naar IBAN', iban_name: 'Tenaamstelling IBAN', description: 'Omschrijving', @@ -21,4 +23,9 @@ export default { cancel: 'Annuleren', submit: 'Bevestigen', }, + options: { + bank_account_source_manual: 'Handmatige invoer', + bank_account_source_fund_request: 'Zoeken via aanvraag', + bank_account_select_placeholder: 'Selecteer aanvraag', + }, }; diff --git a/react/src/dashboard/layout/elements/aside/LayoutAsideSponsor.tsx b/react/src/dashboard/layout/elements/aside/LayoutAsideSponsor.tsx index a9f426b95..2e6c5b10e 100644 --- a/react/src/dashboard/layout/elements/aside/LayoutAsideSponsor.tsx +++ b/react/src/dashboard/layout/elements/aside/LayoutAsideSponsor.tsx @@ -220,6 +220,7 @@ export default function LayoutAsideSponsor({ organization }: { organization: Org state: DashboardRoutes.PAYOUTS, stateParams: { organizationId: organization?.id }, show: organization.allow_payouts && hasPermission(organization, Permission.MANAGE_PAYOUTS), + dusk: 'payoutsNav', }, { id: 'physical_cards', diff --git a/react/src/dashboard/props/models/PayoutBankAccount.tsx b/react/src/dashboard/props/models/PayoutBankAccount.tsx new file mode 100644 index 000000000..9db4ae51c --- /dev/null +++ b/react/src/dashboard/props/models/PayoutBankAccount.tsx @@ -0,0 +1,7 @@ +export default interface PayoutBankAccount { + id: number; + iban: string; + iban_name: string; + created_at?: string; + created_at_locale?: string; +} diff --git a/react/src/dashboard/props/models/Sponsor/SponsorIdentity.tsx b/react/src/dashboard/props/models/Sponsor/SponsorIdentity.tsx index 899d3ba76..153cdbd29 100644 --- a/react/src/dashboard/props/models/Sponsor/SponsorIdentity.tsx +++ b/react/src/dashboard/props/models/Sponsor/SponsorIdentity.tsx @@ -7,11 +7,13 @@ export type SponsorIdentityCounts = { }; export type ProfileBankAccount = { - id: number; + id?: number; name: string; iban: string; created_by: string; created_by_locale: string; + type?: 'profile_bank_account' | 'reimbursement' | 'payout' | 'fund_request'; + type_id?: number; created_at: string; created_at_locale: string; updated_at: string; diff --git a/react/src/dashboard/services/PayoutTransactionService.ts b/react/src/dashboard/services/PayoutTransactionService.ts index 8f8259bcf..e61e33442 100644 --- a/react/src/dashboard/services/PayoutTransactionService.ts +++ b/react/src/dashboard/services/PayoutTransactionService.ts @@ -5,6 +5,7 @@ import Transaction from '../props/models/Transaction'; import { ConfigurableTableColumn } from '../components/pages/vouchers/hooks/useConfigurableTable'; import Papa from 'papaparse'; import PayoutTransaction from '../props/models/PayoutTransaction'; +import PayoutBankAccount from '../props/models/PayoutBankAccount'; export class PayoutTransactionService { /** @@ -23,6 +24,10 @@ export class PayoutTransactionService { return this.apiRequest.get(`${this.prefix}/${organizationId}/sponsor/payouts`, data); } + public bankAccounts(organizationId: number, data: object = {}): Promise> { + return this.apiRequest.get(`${this.prefix}/${organizationId}/sponsor/payouts/bank-accounts`, data); + } + public store(organizationId: number, data: object = {}): Promise> { return this.apiRequest.post(`${this.prefix}/${organizationId}/sponsor/payouts`, data); } diff --git a/react/src/webshop/components/modals/ModalVoucherPayout.tsx b/react/src/webshop/components/modals/ModalVoucherPayout.tsx index 4a9123dad..611ddd417 100644 --- a/react/src/webshop/components/modals/ModalVoucherPayout.tsx +++ b/react/src/webshop/components/modals/ModalVoucherPayout.tsx @@ -16,6 +16,7 @@ import UIControlCheckbox from '../../../dashboard/components/elements/forms/ui-c import { currencyFormat } from '../../../dashboard/helpers/string'; import BlockWarning from '../elements/block-warning/BlockWarning'; import TranslateHtml from '../../../dashboard/components/elements/translate-html/TranslateHtml'; +import useFundRequestBankAccounts from '../../hooks/useFundRequestBankAccounts'; export default function ModalVoucherPayout({ modal, @@ -36,7 +37,9 @@ export default function ModalVoucherPayout({ const [state, setState] = useState<'form' | 'success'>('form'); const [voucherList] = useState>(vouchers || []); - const eligibleVouchers = usePayoutEligibleVouchers(voucherList); + + const { fundRequestAccounts } = useFundRequestBankAccounts(); + const eligibleVouchers = usePayoutEligibleVouchers(voucherList, fundRequestAccounts); const finish = useCallback(() => { modal.close(); @@ -48,12 +51,17 @@ export default function ModalVoucherPayout({ voucher_id: selectedVoucher?.id || eligibleVouchers?.[0]?.id || null, amount: '', accept_compliance_rules: false, + fund_request_id: fundRequestAccounts?.[0]?.type_id || null, }, (values) => { setProgress(0); payoutService - .store({ voucher_id: values.voucher_id, amount: values.amount }) + .store({ + voucher_id: values.voucher_id, + amount: values.amount, + fund_request_id: values.fund_request_id, + }) .then(() => { setState('success'); }) @@ -70,6 +78,17 @@ export default function ModalVoucherPayout({ return eligibleVouchers.find((voucher) => voucher.id === form.values.voucher_id); }, [eligibleVouchers, form.values.voucher_id]); + const selectedBankAccount = useMemo(() => { + return fundRequestAccounts?.find((account) => account.type_id === form.values.fund_request_id); + }, [fundRequestAccounts, form.values.fund_request_id]); + + const fundRequestOptions = useMemo(() => { + return (fundRequestAccounts || []).map((account) => ({ + id: account.type_id, + name: `${account.created_by_locale} #${account.type_id} - ${account.iban} / ${account.name}`, + })); + }, [fundRequestAccounts]); + const selectedVoucherId = selectedVoucherItem?.id; const updateForm = form.update; @@ -150,11 +169,35 @@ export default function ModalVoucherPayout({ } }, [fixedPayoutAmount, selectedVoucherId, updateForm]); + useEffect(() => { + if (!fundRequestAccounts?.length) { + return; + } + + if (!form.values.fund_request_id) { + updateForm({ fund_request_id: fundRequestAccounts[0].type_id }); + return; + } + + if (!fundRequestAccounts.find((account) => account.type_id === form.values.fund_request_id)) { + updateForm({ fund_request_id: fundRequestAccounts[0].type_id }); + } + }, [fundRequestAccounts, form.values.fund_request_id, updateForm]); + + useEffect(() => { + if (!eligibleVouchers.length || form.values.voucher_id) { + return; + } + + updateForm({ voucher_id: eligibleVouchers[0].id }); + }, [eligibleVouchers, form.values.voucher_id, updateForm]); + const disableSubmit = useMemo(() => { return ( form.isLocked || !form.values.voucher_id || !form.values.amount || + !form.values.fund_request_id || !form.values.accept_compliance_rules || Boolean(warningMessage) ); @@ -162,6 +205,7 @@ export default function ModalVoucherPayout({ form.isLocked, form.values.accept_compliance_rules, form.values.amount, + form.values.fund_request_id, form.values.voucher_id, warningMessage, ]); @@ -218,6 +262,28 @@ export default function ModalVoucherPayout({ {!warningMessage && ( + {fundRequestOptions.length > 1 && ( + ( + + form.update({ fund_request_id }) + } + dusk="voucherPayoutFundRequestSelect" + /> + )} + /> + )} + )} @@ -279,7 +345,7 @@ export default function ModalVoucherPayout({ className="form-control" type="text" disabled={true} - value={selectedVoucherItem?.fund_request?.iban_name} + value={selectedBankAccount?.name || ''} placeholder={translate('voucher.payout.iban_name')} /> )} diff --git a/react/src/webshop/components/pages/payouts/Payouts.tsx b/react/src/webshop/components/pages/payouts/Payouts.tsx index 32d174466..f6e08b0f9 100644 --- a/react/src/webshop/components/pages/payouts/Payouts.tsx +++ b/react/src/webshop/components/pages/payouts/Payouts.tsx @@ -19,6 +19,7 @@ import usePayoutEligibleVouchers from '../vouchers-show/hooks/usePayoutEligibleV import useOpenModal from '../../../../dashboard/hooks/useOpenModal'; import ModalVoucherPayout from '../../modals/ModalVoucherPayout'; import IconPayout from '../../../../../assets/forus-webshop/resources/_webshop-common/assets/img/icon-payout.svg'; +import useFundRequestBankAccounts from '../../../hooks/useFundRequestBankAccounts'; export default function Payouts() { const envData = useEnvData(); @@ -32,7 +33,9 @@ export default function Payouts() { const [payouts, setPayoutTransactions] = useState>(null); const [vouchers, setVouchers] = useState>(null); - const payoutEligibleVouchers = usePayoutEligibleVouchers(vouchers); + + const { fundRequestAccounts } = useFundRequestBankAccounts(); + const payoutEligibleVouchers = usePayoutEligibleVouchers(vouchers, fundRequestAccounts); const [filterValues, filterValuesActive, filterUpdate] = useFilterNext<{ q: string }>( { q: '' }, diff --git a/react/src/webshop/components/pages/products-show/ProductsShow.tsx b/react/src/webshop/components/pages/products-show/ProductsShow.tsx index 32bd35e06..a7438b856 100644 --- a/react/src/webshop/components/pages/products-show/ProductsShow.tsx +++ b/react/src/webshop/components/pages/products-show/ProductsShow.tsx @@ -41,6 +41,7 @@ import useOpenModal from '../../../../dashboard/hooks/useOpenModal'; import ModalVoucherPayout from '../../modals/ModalVoucherPayout'; import usePayoutEligibleVouchers from '../vouchers-show/hooks/usePayoutEligibleVouchers'; import useIsPayoutInfoProduct from './hooks/useIsPayoutInfoProduct'; +import useFundRequestBankAccounts from '../../../hooks/useFundRequestBankAccounts'; export default function ProductsShow() { const { id } = useParams(); @@ -70,7 +71,8 @@ export default function ProductsShow() { const [vouchers, setVouchers] = useState>(null); const isPayoutInfoProduct = useIsPayoutInfoProduct(product, appConfigs); - const payoutEligibleVouchers = usePayoutEligibleVouchers(vouchers); + const { fundRequestAccounts } = useFundRequestBankAccounts(); + const payoutEligibleVouchers = usePayoutEligibleVouchers(vouchers, fundRequestAccounts); const { showBack } = useStateParams<{ showBack: boolean }>(); const price = useProductPriceMinLocale(product); diff --git a/react/src/webshop/components/pages/profile/Profile.tsx b/react/src/webshop/components/pages/profile/Profile.tsx index 8bd1e96d2..b152f5643 100644 --- a/react/src/webshop/components/pages/profile/Profile.tsx +++ b/react/src/webshop/components/pages/profile/Profile.tsx @@ -211,6 +211,12 @@ export default function Profile() { label: translate('profile.bank_accounts.name'), value: bank_account.name, }, + { + label: translate('profile.bank_accounts.source'), + value: bank_account.type_id + ? `${bank_account.created_by_locale} #${bank_account.type_id}` + : bank_account.created_by_locale, + }, ]} />
diff --git a/react/src/webshop/components/pages/vouchers-show/elements/VoucherActions.tsx b/react/src/webshop/components/pages/vouchers-show/elements/VoucherActions.tsx index fb016e733..9ec454536 100644 --- a/react/src/webshop/components/pages/vouchers-show/elements/VoucherActions.tsx +++ b/react/src/webshop/components/pages/vouchers-show/elements/VoucherActions.tsx @@ -20,6 +20,7 @@ import IconReimbursement from '../../../../../../assets/forus-webshop/resources/ import { WebshopRoutes } from '../../../../modules/state_router/RouterBuilder'; import ModalVoucherPayout from '../../../modals/ModalVoucherPayout'; import usePayoutEligibleVouchers from '../hooks/usePayoutEligibleVouchers'; +import useFundRequestBankAccounts from '../../../../hooks/useFundRequestBankAccounts'; export default function VoucherActions({ voucher, @@ -41,7 +42,8 @@ export default function VoucherActions({ const voucherCard = useVoucherCard(voucher); const showPhysicalCardsOption = useShowPhysicalCardsOption(voucher); - const payoutEligibleVouchers = usePayoutEligibleVouchers([voucher].filter(Boolean)); + const { fundRequestAccounts } = useFundRequestBankAccounts(); + const payoutEligibleVouchers = usePayoutEligibleVouchers([voucher].filter(Boolean), fundRequestAccounts); const fundPhysicalCardTypes = useMemo(() => { return voucher?.fund?.fund_physical_card_types; diff --git a/react/src/webshop/components/pages/vouchers-show/hooks/usePayoutEligibleVouchers.ts b/react/src/webshop/components/pages/vouchers-show/hooks/usePayoutEligibleVouchers.ts index c51ff17ad..2facc8f81 100644 --- a/react/src/webshop/components/pages/vouchers-show/hooks/usePayoutEligibleVouchers.ts +++ b/react/src/webshop/components/pages/vouchers-show/hooks/usePayoutEligibleVouchers.ts @@ -1,9 +1,17 @@ import { useMemo } from 'react'; import Voucher from '../../../../../dashboard/props/models/Voucher'; +import { ProfileBankAccount } from '../../../../../dashboard/props/models/Sponsor/SponsorIdentity'; -export default function usePayoutEligibleVouchers(vouchers?: Array | null): Array { +export default function usePayoutEligibleVouchers( + vouchers?: Array | null, + fundRequestAccounts?: Array | null, +): Array { return useMemo(() => { - if (!Array.isArray(vouchers)) { + if (!Array.isArray(vouchers) || vouchers.length === 0) { + return []; + } + + if (!Array.isArray(fundRequestAccounts) || fundRequestAccounts.length === 0) { return []; } @@ -11,7 +19,6 @@ export default function usePayoutEligibleVouchers(vouchers?: Array | nu .filter((voucher) => voucher?.fund?.allow_voucher_payouts) .filter((voucher) => voucher?.type !== 'product') .filter((voucher) => !voucher?.product_reservation?.id) - .filter((voucher) => voucher?.fund_request?.iban && voucher?.fund_request?.iban_name) .filter((voucher) => !voucher?.expired && !voucher?.deactivated && !voucher?.external); - }, [vouchers]); + }, [fundRequestAccounts, vouchers]); } diff --git a/react/src/webshop/hooks/useFundRequestBankAccounts.ts b/react/src/webshop/hooks/useFundRequestBankAccounts.ts new file mode 100644 index 000000000..589dd7174 --- /dev/null +++ b/react/src/webshop/hooks/useFundRequestBankAccounts.ts @@ -0,0 +1,32 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useProfileService } from '../../dashboard/services/ProfileService'; +import { ProfileBankAccount } from '../../dashboard/props/models/Sponsor/SponsorIdentity'; +import useAuthIdentity from './useAuthIdentity'; + +export default function useFundRequestBankAccounts() { + const profileService = useProfileService(); + const authIdentity = useAuthIdentity(); + const [bankAccounts, setBankAccounts] = useState>(null); + + const fetchProfile = useCallback(() => { + profileService + .profile() + .then((res) => setBankAccounts(res.data?.bank_accounts || [])) + .catch(() => setBankAccounts([])); + }, [profileService]); + + useEffect(() => { + if (!authIdentity?.profile) { + setBankAccounts([]); + return; + } + + fetchProfile(); + }, [authIdentity?.profile, fetchProfile]); + + const fundRequestAccounts = useMemo(() => { + return (bankAccounts || []).filter((account) => account.type === 'fund_request'); + }, [bankAccounts]); + + return { bankAccounts, fundRequestAccounts }; +} diff --git a/react/src/webshop/i18n/nl/pages/profile.mjs b/react/src/webshop/i18n/nl/pages/profile.mjs index 5dafa145c..4e2ce2323 100644 --- a/react/src/webshop/i18n/nl/pages/profile.mjs +++ b/react/src/webshop/i18n/nl/pages/profile.mjs @@ -51,5 +51,6 @@ export default { title: 'Bankrekening', iban: 'IBAN', name: 'Tenaam stelling', + source: 'Bron', }, }; From 54150ab9ffe1aa817b1f544c8eed5ae32e27d5ee Mon Sep 17 00:00:00 2001 From: dev-rminds Date: Mon, 19 Jan 2026 08:56:54 +0200 Subject: [PATCH 05/14] refactoring and cleanup --- .../blocks/block-financial-dashboard.scss | 12 +- .../elements/announcements/Accouncements.tsx | 13 +- .../elements/app-links/AppLinks.tsx | 5 +- .../auth2fa-info-box/Auth2FAInfoBox.tsx | 3 +- .../block-card-emails/BlockCardEmails.tsx | 22 +- .../block-label-tabs/BlockLabelTabs.tsx | 11 +- .../BlockReimbursementCategories.tsx | 2 +- .../elements/empty-card/EmptyCard.tsx | 22 +- .../faq-editor-funds/FaqEditorItem.tsx | 90 ++++---- .../forms/controls/NumericControl.tsx | 5 +- .../elements/forms/controls/PhoneControl.tsx | 3 +- .../forms/controls/PincodeControl.tsx | 3 +- .../elements/forms/controls/ToggleControl.tsx | 3 + .../elements/forms/elements/FormGroup.tsx | 26 ++- .../elements/forms/errors/FormError.tsx | 13 +- .../forms/markdown-editor/MarkdownEditor.tsx | 7 +- .../forms/ui-controls/UIControlCheckbox.tsx | 11 +- .../forms/ui-controls/UIControlDate.tsx | 3 +- .../forms/ui-controls/UIControlNumber.tsx | 7 +- .../forms/ui-controls/UIControlText.tsx | 2 +- .../FundCriteriaEditorItem.tsx | 46 ++-- .../FundsProviderProductsRequiredTable.tsx | 3 +- .../SelectControlOptionsCountryCodes.tsx | 5 +- .../templates/SelectControlOptionsLang.tsx | 2 +- .../SelectControlOptionsOrganization.tsx | 20 +- .../elements/table-config/TableConfig.tsx | 16 +- .../elements/tables/EventLogsTable.tsx | 63 +++--- .../tables/TableTopScrollerConfigTh.tsx | 8 +- .../components/elements/tables/ThSortable.tsx | 8 +- .../tables/elements/CardHeaderFilterNext.tsx | 53 +++-- .../tables/elements/FilterItemToggle.tsx | 3 +- .../tables/elements/TableCheckboxControl.tsx | 3 +- .../components/elements/tooltip/Tooltip.tsx | 16 +- .../components/modals/Modal2FADeactivate.tsx | 46 ++-- .../components/modals/Modal2FASetup.tsx | 16 +- .../components/modals/ModalAddNote.tsx | 26 ++- .../components/modals/ModalAuthPincode.tsx | 20 +- .../modals/ModalCreatePrevalidation.tsx | 9 +- .../components/modals/ModalDangerZone.tsx | 21 +- .../modals/ModalDuplicatesPicker.tsx | 9 +- .../components/modals/ModalEmployeeEdit.tsx | 108 ++++----- .../modals/ModalExportDataSelect.tsx | 39 ++-- .../ModalFundCriteriaDescriptionEdit.tsx | 86 ++++--- .../ModalFundEditPhysicalCardSettings.tsx | 5 +- .../modals/ModalFundInviteProviders.tsx | 37 +-- .../modals/ModalFundProviderChatMessage.tsx | 29 ++- .../modals/ModalFundProviderChatProvider.tsx | 18 +- .../modals/ModalFundProviderChatSponsor.tsx | 21 +- .../ModalFundRequestAssignValidator.tsx | 32 +-- .../modals/ModalFundRequestDecline.tsx | 25 +- .../modals/ModalFundRequestDisregard.tsx | 53 +++-- .../modals/ModalFundRequestRecordCreate.tsx | 30 +-- .../modals/ModalFundRequestRecordEdit.tsx | 74 +++--- .../components/modals/ModalFundTopUp.tsx | 3 +- .../modals/ModalFundUnsubscribe.tsx | 28 +-- .../components/modals/ModalLogEmailShow.tsx | 59 ++--- .../components/modals/ModalMailPreview.tsx | 3 +- .../modals/ModalMarkdownCustomLink.tsx | 126 +++++------ .../modals/ModalOrderPhysicalCard.tsx | 213 +++++++++--------- .../components/modals/ModalPayoutEdit.tsx | 18 +- .../components/modals/ModalPayoutsUpload.tsx | 4 +- .../components/modals/ModalPhotoUploader.tsx | 36 ++- .../ModalPreCheckEditFundExclusions.tsx | 28 +-- .../ModalPrevalidationRequestsUpload.tsx | 5 +- .../modals/ModalPrevalidationsUpload.tsx | 5 +- .../ModalReimbursementCategoriesEdit.tsx | 11 +- .../modals/ModalReimbursementCategoryEdit.tsx | 28 ++- .../modals/ModalReimbursementDetailsEdit.tsx | 36 +-- .../modals/ModalReimbursementResolve.tsx | 88 +++++--- .../modals/ModalReservationReject.tsx | 21 +- .../modals/ModalReservationUpload.tsx | 9 +- .../modals/ModalSocialMediaEdit.tsx | 99 ++++---- .../ModalSwitchBankConnectionAccount.tsx | 40 ++-- .../ModalTransferOrganizationOwnership.tsx | 38 ++-- .../modals/ModalVoucherActivate.tsx | 32 +-- .../components/modals/ModalVoucherCreate.tsx | 23 +- .../modals/ModalVoucherDeactivation.tsx | 47 ++-- .../components/modals/ModalVoucherQRCode.tsx | 10 +- .../modals/ModalVoucherRecordEdit.tsx | 107 +++++---- .../ModalVoucherTransaction.tsx | 31 ++- .../modals/ModalVoucherTransactionsUpload.tsx | 9 +- .../components/modals/ModalVouchersUpload.tsx | 4 +- .../components/pages/features/Features.tsx | 23 +- .../elements/FinancialChart.tsx | 22 +- .../elements/FinancialFilter.tsx | 33 ++- .../elements/FinancialFilters.tsx | 28 +-- .../FundBackofficeEdit.tsx | 21 +- .../elements/FundRequestRecordTabs.tsx | 71 +++--- .../pages/fund-requests/FundRequests.tsx | 25 +- .../modals/elements/ProfileRecordsField.tsx | 1 + .../elements/NotificationPreferenceCard.tsx | 36 +-- .../ImplementationNotificationSend.tsx | 42 ++-- .../ImplementationsInlineBlockEditor.tsx | 3 +- .../ImplementationDigid.tsx | 21 +- .../ImplementationTermsAndPrivacy.tsx | 8 +- .../ImplementationsBlockEditorItem.tsx | 23 +- .../OrganizationsFundsShowDetailsCard.tsx | 22 +- .../OrganizationsFundsShowIdentitiesCard.tsx | 23 +- ...anizationsFundsShowImplementationsCard.tsx | 23 +- .../OrganizationsFundsShowTopUpsCard.tsx | 23 +- .../organizations-funds/OrganizationFunds.tsx | 34 ++- .../OrganizationsSecurity.tsx | 27 +-- .../pages/provider-funds/ProviderFunds.tsx | 102 ++++----- .../pages/reimbursements/Reimbursements.tsx | 29 +-- .../pages/reservations/Reservations.tsx | 62 +++-- .../SponsorProviderOrganizations.tsx | 25 +- .../pages/transactions/Transactions.tsx | 27 +-- .../dashboard/props/models/Announcement.tsx | 2 +- .../modals/ModalPhysicalCardType.tsx | 2 +- .../components/modals/ModalVoucherPayout.tsx | 26 +-- .../webshop/components/pages/me-app/MeApp.tsx | 2 +- .../ReimbursementsEdit.tsx | 2 +- 112 files changed, 1669 insertions(+), 1493 deletions(-) diff --git a/react/assets/forus-platform/scss/_common/blocks/block-financial-dashboard.scss b/react/assets/forus-platform/scss/_common/blocks/block-financial-dashboard.scss index c352c7503..495d9b0fc 100644 --- a/react/assets/forus-platform/scss/_common/blocks/block-financial-dashboard.scss +++ b/react/assets/forus-platform/scss/_common/blocks/block-financial-dashboard.scss @@ -77,10 +77,6 @@ .checkbox-label { font: 600 13px/16px var(--base-font); color: #646f79; - - &.active { - color: #009ef4; - } } .block-option-count { @@ -92,6 +88,10 @@ font: inherit; color: #646f79; } + + &.block-option-active .checkbox-label { + color: #009ef4; + } } .block-option-empty { @@ -108,6 +108,10 @@ .card-section { border-bottom-color: var(--border-color); + + &>.financial-chart { + height: 350px; + } } .financial-totals { diff --git a/react/src/dashboard/components/elements/announcements/Accouncements.tsx b/react/src/dashboard/components/elements/announcements/Accouncements.tsx index 3f4f50e55..93624aca6 100644 --- a/react/src/dashboard/components/elements/announcements/Accouncements.tsx +++ b/react/src/dashboard/components/elements/announcements/Accouncements.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; +import classNames from 'classnames'; import Announcement from '../../../props/models/Announcement'; export const Announcements = ({ announcements }: { announcements: Array }) => { @@ -43,9 +44,15 @@ export const Announcements = ({ announcements }: { announcements: Array (
+ className={classNames( + 'announcement', + announcement.type === 'warning' && 'announcement-warning', + announcement.type === 'danger' && 'announcement-danger', + announcement.type === 'success' && 'announcement-success', + announcement.type === 'primary' && 'announcement-primary', + announcement.type === 'default' && 'announcement-default', + announcement.dismissed && 'dismissed', + )}>
{announcement.title}
diff --git a/react/src/dashboard/components/elements/app-links/AppLinks.tsx b/react/src/dashboard/components/elements/app-links/AppLinks.tsx index c38accd74..35507101e 100644 --- a/react/src/dashboard/components/elements/app-links/AppLinks.tsx +++ b/react/src/dashboard/components/elements/app-links/AppLinks.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import classNames from 'classnames'; import useEnvData from '../../../hooks/useEnvData'; import useAssetUrl from '../../../hooks/useAssetUrl'; @@ -9,7 +10,7 @@ export default function AppLinks({ showIosButton = true, showAndroidButton = true, }: { - type?: string; + type?: 'lg'; iosId?: string; androidId?: string; showIosButton?: boolean; @@ -19,7 +20,7 @@ export default function AppLinks({ const assetUrl = useAssetUrl(); return ( -
+
{showAndroidButton && ( +
{envData.client_type == 'sponsor' && (
diff --git a/react/src/dashboard/components/elements/block-card-emails/BlockCardEmails.tsx b/react/src/dashboard/components/elements/block-card-emails/BlockCardEmails.tsx index c4141c0f8..b91212e7e 100644 --- a/react/src/dashboard/components/elements/block-card-emails/BlockCardEmails.tsx +++ b/react/src/dashboard/components/elements/block-card-emails/BlockCardEmails.tsx @@ -23,6 +23,7 @@ import Organization from '../../../props/models/Organization'; import useConfigurableTable from '../../pages/vouchers/hooks/useConfigurableTable'; import TableTopScroller from '../tables/TableTopScroller'; import { FilterModel } from '../../../modules/filter_next/types/FilterParams'; +import FormGroup from '../forms/elements/FormGroup'; export default function BlockCardEmails({ organization, @@ -118,15 +119,18 @@ export default function BlockCardEmails({
-
- filterUpdate({ q: e.target.value })} - /> -
+ ( + filterUpdate({ q: e.target.value })} + /> + )} + />
diff --git a/react/src/dashboard/components/elements/block-label-tabs/BlockLabelTabs.tsx b/react/src/dashboard/components/elements/block-label-tabs/BlockLabelTabs.tsx index 5829b1eb9..7516a31d3 100644 --- a/react/src/dashboard/components/elements/block-label-tabs/BlockLabelTabs.tsx +++ b/react/src/dashboard/components/elements/block-label-tabs/BlockLabelTabs.tsx @@ -5,10 +5,12 @@ export default function BlockLabelTabs({ value, setValue, tabs, + size = 'sm', }: { value: t; setValue: (state: t) => void; - tabs: Array<{ value: t; label: string }>; + tabs: Array<{ value: t; label: React.ReactNode; dusk?: string }>; + size?: 'sm' | null; }) { return (
@@ -16,7 +18,12 @@ export default function BlockLabelTabs({ {tabs.map((tab, index) => (
setValue(tab.value)}> {tab.label}
diff --git a/react/src/dashboard/components/elements/block-reimbursement-categories/BlockReimbursementCategories.tsx b/react/src/dashboard/components/elements/block-reimbursement-categories/BlockReimbursementCategories.tsx index 920c47f6f..96ede7a8d 100644 --- a/react/src/dashboard/components/elements/block-reimbursement-categories/BlockReimbursementCategories.tsx +++ b/react/src/dashboard/components/elements/block-reimbursement-categories/BlockReimbursementCategories.tsx @@ -205,7 +205,7 @@ export default function BlockReimbursementCategories({
{categories?.meta.total > 0 && ( -
+
-
+
{imageIconImg && (
{''} @@ -94,7 +103,12 @@ export default function EmptyCard({ key={index} to={button.to} onClick={button.onClick} - className={`button button-${button.type || 'default'}`} + className={classNames( + 'button', + (!button.type || button.type === 'default') && 'button-default', + button.type === 'primary' && 'button-primary', + button.type === 'danger' && 'button-danger', + )} data-dusk={button.dusk || 'btnEmptyBlock'}> {button.icon && (!button.iconPosition || button.iconPosition == 'start') && ( diff --git a/react/src/dashboard/components/elements/faq-editor-funds/FaqEditorItem.tsx b/react/src/dashboard/components/elements/faq-editor-funds/FaqEditorItem.tsx index 9bd2cc899..33e5a1d29 100644 --- a/react/src/dashboard/components/elements/faq-editor-funds/FaqEditorItem.tsx +++ b/react/src/dashboard/components/elements/faq-editor-funds/FaqEditorItem.tsx @@ -1,11 +1,12 @@ -import FormError from '../forms/errors/FormError'; import MarkdownEditor from '../forms/markdown-editor/MarkdownEditor'; import React, { useMemo } from 'react'; +import classNames from 'classnames'; import { ResponseErrorData } from '../../../props/ApiResponses'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import Faq from '../../../props/models/Faq'; import useTranslate from '../../../hooks/useTranslate'; +import FormGroup from '../forms/elements/FormGroup'; export default function FaqEditorItem({ id, @@ -43,13 +44,14 @@ export default function FaqEditorItem({ {faqItem.type === 'question' ? (
+ className={classNames( + 'question-icon', + isCollapsed && (!faqItem.title || !faqItem.description) && 'text-danger', + )}>
) : ( -
+
)} @@ -97,43 +99,53 @@ export default function FaqEditorItem({ {!isCollapsed && (
-
- - onChange({ title: e.target.value })} - placeholder="Title..." - /> -
Max. 200 tekens
- -
+ ( + onChange({ title: e.target.value })} + placeholder="Title..." + /> + )} + /> {faqItem.type === 'question' ? ( -
- - onChange({ description: description })} - extendedOptions={true} - placeholder={translate('organization_edit.labels.description')} - /> -
Max. 5000 tekens
- -
+ ( + onChange({ description: description })} + extendedOptions={true} + placeholder={translate('organization_edit.labels.description')} + /> + )} + /> ) : ( -
- - -
+ ( +