Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/components/dashboard/settings/SettingAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export default function SettingAccordion({
isLoading,
changeSettings,
showButton: showInvoiceButton,
bankAccounts,
isBankAccountsLoading,
selectBankAccount,
} = useInvoiceDetailSettings()

const accordionItems = [
Expand Down Expand Up @@ -72,6 +75,9 @@ export default function SettingAccordion({
settingState={settingState}
changeSettings={changeSettings}
isLoading={isLoading}
bankAccounts={bankAccounts}
isBankAccountsLoading={isBankAccountsLoading}
selectBankAccount={selectBankAccount}
/>
),
},
Expand Down
107 changes: 107 additions & 0 deletions src/components/dashboard/settings/sections/invoice/InvoiceDetail.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 <Spinner size={5} />
}

const selectedAccount = bankAccounts.find(
(acc) => acc.Id === settingState.bankAccountRef,
)

return (
<>
<div className="mt-2 mb-6">
Expand Down Expand Up @@ -53,6 +80,86 @@ export default function InvoiceDetail({
/>
</div>
)}
{settingState.absorbedFeeFlag && settingState.bankDepositFeeFlag && (
<div className="mb-5 ml-6" ref={dropdownRef}>
<label className="text-sm font-medium text-gray-700 mb-1 block">
Bank account for deposits
</label>
<p className="text-body-xs text-gray-500 mb-2">
Select the QuickBooks bank account where Stripe deposits land.
</p>
<div className="relative">
<button
type="button"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="w-full max-w-[320px] flex items-center justify-between rounded-sm border border-gray-200 bg-white px-3 py-2 text-sm text-left hover:bg-gray-100 transition-colors"
>
<span
className={
selectedAccount ? 'text-gray-700' : 'text-gray-400'
}
>
{isBankAccountsLoading
? 'Loading accounts...'
: selectedAccount
? selectedAccount.Name
: 'Select a bank account...'}
</span>
<svg
className={`h-4 w-4 text-gray-500 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{isDropdownOpen && (
<div className="absolute z-100 mt-1 w-full max-w-[320px] bg-white border border-gray-150 rounded-sm shadow-popover-050">
{isBankAccountsLoading ? (
<div className="flex items-center justify-center py-4">
<Spinner size={5} />
</div>
) : bankAccounts.length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">
No bank accounts found in QuickBooks
</div>
) : (
bankAccounts.map((account) => (
<button
key={account.Id}
type="button"
onClick={() => {
selectBankAccount(account.Id)
setIsDropdownOpen(false)
}}
className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-100 transition-colors cursor-pointer ${
account.Id === settingState.bankAccountRef
? 'bg-gray-100 text-gray-700 font-medium'
: 'text-gray-600'
}`}
>
{account.Name}
</button>
))
)}
</div>
)}
</div>
{!isBankAccountsLoading &&
!settingState.bankAccountRef &&
bankAccounts.length > 0 && (
<p className="text-body-xs text-amber-600 mt-1">
Please select a bank account to enable bank deposits.
</p>
)}
</div>
)}
<div className="mb-6">
<Checkbox
label={`Use ${getWorkspaceLabel(workspace).groupTerm} name when syncing invoices billed to ${getWorkspaceLabel(workspace).groupTermPlural}`}
Expand Down
71 changes: 53 additions & 18 deletions src/hook/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { getTimeInterval } from '@/utils/common'
import { QBO_ITEM_NAME_MAX_LENGTH } from '@/utils/string'
import {
ProductMappingItemArraySchema,

Check warning on line 12 in src/hook/useSettings.ts

View workflow job for this annotation

GitHub Actions / Run linters

'ProductMappingItemArraySchema' is defined but never used. Allowed unused vars must match /^_/u
ProductMappingItemType,
} from '@/db/schema/qbProductSync'
import { postFetcher } from '@/helper/fetch.helper'
Expand Down Expand Up @@ -98,7 +98,7 @@
if (!productSetting || !intialSettingState) return
const showButton = !equal(intialSettingState, productSetting)
setSettingShowConfirm(showButton)
}, [productSetting])

Check warning on line 101 in src/hook/useSettings.ts

View workflow job for this annotation

GitHub Actions / Run linters

React Hook useEffect has a missing dependency: 'intialSettingState'. Either include it or remove the dependency array

useEffect(() => {
if (setting && setting?.setting) {
Expand All @@ -116,7 +116,7 @@
false,
}))
}
}, [setting])

Check warning on line 119 in src/hook/useSettings.ts

View workflow job for this annotation

GitHub Actions / Run linters

React Hook useEffect has a missing dependency: 'setAppParams'. Either include it or remove the dependency array
// End of checkbox settings

const tableMappingSubmit = async () => {
Expand Down Expand Up @@ -506,11 +506,17 @@
}
}

export type BankAccountType = {
Id: string
Name: string
}

export const useInvoiceDetailSettings = () => {
const initialInvoiceSetting = {
const initialInvoiceSetting: InvoiceSettingType = {
absorbedFeeFlag: false,
bankDepositFeeFlag: false,
useCompanyNameFlag: false,
bankAccountRef: null,
}
const { token, setAppParams } = useApp()
const [settingState, setSettingState] = useState<InvoiceSettingType>(
Expand All @@ -520,6 +526,7 @@
const [intialSettingState, setIntialSettingState] = useState<
InvoiceSettingType | undefined
>()

const {
data: setting,
error,
Expand All @@ -529,26 +536,46 @@
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,
[flag]: state,
}))
}

const selectBankAccount = (ref: string) => {
setSettingState((prev) => ({ ...prev, bankAccountRef: ref }))
}

useEffect(() => {
if (!settingState || !intialSettingState) return
const showButton = !equal(intialSettingState, settingState)
setShowButton(showButton)
}, [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(() => {
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,
Expand All @@ -562,16 +589,21 @@

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)
}
}

Expand All @@ -588,6 +620,9 @@
error,
isLoading,
showButton,
bankAccounts,
isBankAccountsLoading,
selectBankAccount,
}
}

Expand Down
Loading