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
15 changes: 13 additions & 2 deletions src/components/dashboard/settings/SettingAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import InvoiceDetail from '@/components/dashboard/settings/sections/invoice/Invo
import AccountMapping from '@/components/dashboard/settings/sections/account/AccountMapping'
import ProductMapping from '@/components/dashboard/settings/sections/product/ProductMapping'
import Accordion from '@/components/ui/Accordion'
import ConfirmModal from '@/components/ui/ConfirmModal'
import Divider from '@/components/ui/Divider'
import {
useInvoiceDetailSettings,
Expand Down Expand Up @@ -39,14 +40,17 @@ export default function SettingAccordion({

const {
settingState,
submitInvoiceSettings,
cancelInvoiceSettings,
isLoading,
changeSettings,
showButton: showInvoiceButton,
bankAccountOptions,
bankAccountsError,
canSave,
showBankDepositWarning,
requestInvoiceSettingsSave,
confirmBankDepositChange,
cancelBankDepositChange,
} = useInvoiceDetailSettings()

const {
Expand Down Expand Up @@ -172,7 +176,7 @@ export default function SettingAccordion({
variant="primary"
prefixIcon="Check"
disabled={!canSave}
onClick={submitInvoiceSettings}
onClick={requestInvoiceSettingsSave}
/>
</>
)}
Expand Down Expand Up @@ -202,6 +206,13 @@ export default function SettingAccordion({
</div>
)
})}
<ConfirmModal
open={showBankDepositWarning}
title="Change bank deposit setting?"
description="This applies only to invoices created from now on; existing invoices are unaffected. If a Stripe payout mixes invoices from before and after the change, you'll need to reconcile that payout manually in QuickBooks."
onConfirm={confirmBankDepositChange}
onCancel={cancelBankDepositChange}
/>
</div>
)
}
92 changes: 92 additions & 0 deletions src/components/ui/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use client'
import { useEffect, useId, useRef } from 'react'
import { createPortal } from 'react-dom'
import { Button } from 'copilot-design-system'

type ConfirmModalProps = {
open: boolean
title: string
description: string
confirmLabel?: string
cancelLabel?: string
onConfirm: () => void
onCancel: () => void
}

export default function ConfirmModal({
open,
title,
description,
confirmLabel = 'Continue',
cancelLabel = 'Cancel',
onConfirm,
onCancel,
}: ConfirmModalProps) {
const titleId = useId()
const descId = useId()
const dialogRef = useRef<HTMLDivElement>(null)

// On open, focus into the dialog; on close, restore focus to the opener.
useEffect(() => {
if (!open) return
const previouslyFocused = document.activeElement as HTMLElement | null
const buttons = dialogRef.current?.querySelectorAll<HTMLElement>('button')
buttons?.[0]?.focus()
return () => previouslyFocused?.focus()
}, [open])

// Escape cancels; Tab is trapped between the dialog's buttons.
useEffect(() => {
if (!open) return
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') return onCancel()
if (e.key !== 'Tab') return
const buttons = Array.from(
dialogRef.current?.querySelectorAll<HTMLElement>('button') ?? [],
)
if (buttons.length === 0) return
const first = buttons[0]
const last = buttons[buttons.length - 1]
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
last.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
first.focus()
}
}
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [open, onCancel])

if (!open) return null

return createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={onCancel}
>
<div
ref={dialogRef}
role="dialog"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={descId}
className="mx-4 w-full max-w-sm rounded-lg bg-white p-6 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<h2 id={titleId} className="mb-2 text-base font-semibold">
{title}
</h2>
<p id={descId} className="mb-6 text-sm text-gray-600">
{description}
</p>
<div className="flex justify-end gap-2">
<Button label={cancelLabel} variant="text" onClick={onCancel} />
<Button label={confirmLabel} variant="primary" onClick={onConfirm} />
</div>
</div>
</div>,
document.body,
)
}
26 changes: 25 additions & 1 deletion src/hook/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ export const useInvoiceDetailSettings = () => {
initialInvoiceSetting,
)
const [showButton, setShowButton] = useState(false)
const [showBankDepositWarning, setShowBankDepositWarning] = useState(false)
const [intialSettingState, setIntialSettingState] = useState<
InvoiceSettingType | undefined
>()
Expand Down Expand Up @@ -522,17 +523,40 @@ export const useInvoiceDetailSettings = () => {
setSettingState(intialSettingState || initialInvoiceSetting)
}

// Warn only when the bank-deposit flag actually changed vs the saved value.
const bankDepositFlagChanged =
!!intialSettingState &&
settingState.bankDepositFeeFlag !== intialSettingState.bankDepositFeeFlag

const requestInvoiceSettingsSave = () => {
if (bankDepositFlagChanged) {
setShowBankDepositWarning(true)
return
}
submitInvoiceSettings()
}

const confirmBankDepositChange = () => {
setShowBankDepositWarning(false)
submitInvoiceSettings()
}

const cancelBankDepositChange = () => setShowBankDepositWarning(false)

return {
settingState,
changeSettings,
submitInvoiceSettings,
cancelInvoiceSettings,
error,
isLoading,
showButton,
bankAccountOptions,
bankAccountsError,
canSave,
showBankDepositWarning,
requestInvoiceSettingsSave,
confirmBankDepositChange,
cancelBankDepositChange,
}
}

Expand Down
Loading