From 47e104533546f245f14aa99a9f8310ac3d99c139 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Fri, 17 Apr 2026 19:31:54 +0545 Subject: [PATCH 1/2] feat(OUT-3609): add bank account selector dropdown to invoice settings UI - Add bank account dropdown in InvoiceDetail below bankDepositFeeFlag checkbox - Fetch QBO bank accounts via SWR (GET /api/quickbooks/setting/bank-account) - Include bankAccountRef in settingState for single-request save - Add click-outside-to-close, loading state, and amber warning when unselected - Pass bankAccounts, isBankAccountsLoading, selectBankAccount props via SettingAccordion Co-Authored-By: Claude Opus 4.6 (1M context) --- .../dashboard/settings/SettingAccordion.tsx | 6 + .../sections/invoice/InvoiceDetail.tsx | 107 ++++++++++++++++++ src/hook/useSettings.ts | 67 ++++++++--- 3 files changed, 162 insertions(+), 18 deletions(-) diff --git a/src/components/dashboard/settings/SettingAccordion.tsx b/src/components/dashboard/settings/SettingAccordion.tsx index cd482bd3..88a097ba 100644 --- a/src/components/dashboard/settings/SettingAccordion.tsx +++ b/src/components/dashboard/settings/SettingAccordion.tsx @@ -42,6 +42,9 @@ export default function SettingAccordion({ isLoading, changeSettings, showButton: showInvoiceButton, + bankAccounts, + isBankAccountsLoading, + selectBankAccount, } = useInvoiceDetailSettings() const accordionItems = [ @@ -72,6 +75,9 @@ export default function SettingAccordion({ settingState={settingState} changeSettings={changeSettings} isLoading={isLoading} + bankAccounts={bankAccounts} + isBankAccountsLoading={isBankAccountsLoading} + selectBankAccount={selectBankAccount} /> ), }, diff --git a/src/components/dashboard/settings/sections/invoice/InvoiceDetail.tsx b/src/components/dashboard/settings/sections/invoice/InvoiceDetail.tsx index 5c93dccd..cdfae987 100644 --- a/src/components/dashboard/settings/sections/invoice/InvoiceDetail.tsx +++ b/src/components/dashboard/settings/sections/invoice/InvoiceDetail.tsx @@ -1,25 +1,52 @@ import { useApp } from '@/app/context/AppContext' +import { BankAccountType } from '@/hook/useSettings' import { InvoiceSettingType } from '@/type/common' import { getWorkspaceLabel } from '@/utils/workspace' import { Checkbox, Spinner } from 'copilot-design-system' +import { useEffect, useRef, useState } from 'react' type InvoiceDetailProps = { settingState: InvoiceSettingType changeSettings: (flag: keyof InvoiceSettingType, state: boolean) => void isLoading: boolean + bankAccounts: BankAccountType[] + isBankAccountsLoading: boolean + selectBankAccount: (ref: string) => void } export default function InvoiceDetail({ settingState, changeSettings, isLoading, + bankAccounts, + isBankAccountsLoading, + selectBankAccount, }: InvoiceDetailProps) { const { workspace } = useApp() + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + const dropdownRef = useRef(null) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsDropdownOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) if (isLoading) { return } + const selectedAccount = bankAccounts.find( + (acc) => acc.Id === settingState.bankAccountRef, + ) + return ( <>
@@ -53,6 +80,86 @@ export default function InvoiceDetail({ />
)} + {settingState.absorbedFeeFlag && settingState.bankDepositFeeFlag && ( +
+ +

+ Select the QuickBooks bank account where Stripe deposits land. +

+
+ + {isDropdownOpen && ( +
+ {isBankAccountsLoading ? ( +
+ +
+ ) : bankAccounts.length === 0 ? ( +
+ No bank accounts found in QuickBooks +
+ ) : ( + bankAccounts.map((account) => ( + + )) + )} +
+ )} +
+ {!isBankAccountsLoading && + !settingState.bankAccountRef && + bankAccounts.length > 0 && ( +

+ Please select a bank account to enable bank deposits. +

+ )} +
+ )}
{ - const initialInvoiceSetting = { + const initialInvoiceSetting: InvoiceSettingType = { absorbedFeeFlag: false, bankDepositFeeFlag: false, useCompanyNameFlag: false, + bankAccountRef: null, } const { token, setAppParams } = useApp() const [settingState, setSettingState] = useState( @@ -520,6 +526,7 @@ export const useInvoiceDetailSettings = () => { const [intialSettingState, setIntialSettingState] = useState< InvoiceSettingType | undefined >() + const { data: setting, error, @@ -529,9 +536,18 @@ export const useInvoiceDetailSettings = () => { revalidateOnMount: false, }) - const changeSettings = async ( + const { data: bankAccountsData, isLoading: isBankAccountsLoading } = + useSwrHelper( + settingState.bankDepositFeeFlag + ? `/api/quickbooks/setting/bank-account?token=${token}` + : null, + { suspense: false, revalidateOnMount: true }, + ) + const bankAccounts: BankAccountType[] = bankAccountsData?.accounts || [] + + const changeSettings = ( flag: keyof InvoiceSettingType, - state: boolean, + state: boolean | string | null, ) => { setSettingState((prev) => ({ ...prev, @@ -539,16 +555,23 @@ export const useInvoiceDetailSettings = () => { })) } + const selectBankAccount = (ref: string) => { + setSettingState((prev) => ({ ...prev, bankAccountRef: ref })) + } + useEffect(() => { if (!settingState || !intialSettingState) return - const showButton = !equal(intialSettingState, settingState) - setShowButton(showButton) - }, [settingState]) + setShowButton(!equal(intialSettingState, settingState)) + }, [settingState, intialSettingState]) useEffect(() => { if (setting && setting?.setting) { - setSettingState(setting.setting) - setIntialSettingState(structuredClone(setting.setting)) + const loaded: InvoiceSettingType = { + ...setting.setting, + bankAccountRef: setting.bankAccountRef || null, + } + setSettingState(loaded) + setIntialSettingState(structuredClone(loaded)) setAppParams((prev) => ({ ...prev, initialInvoiceSettingMapFlag: setting.setting.initialInvoiceSettingMap, @@ -562,16 +585,21 @@ export const useInvoiceDetailSettings = () => { const submitInvoiceSettings = async () => { setShowButton(false) - const res = await postFetcher( - `/api/quickbooks/setting?type=${SettingType.INVOICE}&token=${token}`, - {}, - { ...settingState, type: SettingType.INVOICE }, - ) - if (!res || res?.error) { - setShowButton(true) // show the update settings button if error - console.error('Error submitting Invoice settings', { res }) - } else { - mutate(`/api/quickbooks/setting?type=invoice&token=${token}`) + try { + const res = await postFetcher( + `/api/quickbooks/setting?type=${SettingType.INVOICE}&token=${token}`, + {}, + { ...settingState, type: SettingType.INVOICE }, + ) + if (res?.error) { + setShowButton(true) + console.error('Error submitting Invoice settings', { res }) + } else { + mutate(`/api/quickbooks/setting?type=invoice&token=${token}`) + } + } catch (err) { + setShowButton(true) + console.error('Error submitting Invoice settings', err) } } @@ -588,6 +616,9 @@ export const useInvoiceDetailSettings = () => { error, isLoading, showButton, + bankAccounts, + isBankAccountsLoading, + selectBankAccount, } } From 18f49d02a7603d340288c65829a41dbbf05d12d3 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Mon, 20 Apr 2026 15:11:46 +0545 Subject: [PATCH 2/2] fix(OUT-3609): prevent settings save when bank deposit enabled without bank account Block the Confirm/Update button when bankDepositFeeFlag is on but no bank account is selected. Without a bank account, the webhook deposit flow throws on every payment.succeeded event. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hook/useSettings.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hook/useSettings.ts b/src/hook/useSettings.ts index 8f09e5e4..011fa4d8 100644 --- a/src/hook/useSettings.ts +++ b/src/hook/useSettings.ts @@ -561,7 +561,11 @@ export const useInvoiceDetailSettings = () => { useEffect(() => { if (!settingState || !intialSettingState) return - setShowButton(!equal(intialSettingState, settingState)) + const hasChanges = !equal(intialSettingState, settingState) + // Block submit if bank deposit is on but no bank account selected + const missingBankAccount = + settingState.bankDepositFeeFlag && !settingState.bankAccountRef + setShowButton(hasChanges && !missingBankAccount) }, [settingState, intialSettingState]) useEffect(() => {