* feat(banking): the bank-connect payoff: point the first sync onward Fifth activation slice. Every successful connect passes through the sync-progress dialog's done state, which used to end on a bare Klar that stranded the user on the settings panel. Now it is the payoff: the imported count stays, the work now waiting gets named (N att bokföra · M matchar fakturor, from the same worklist counts endpoint the dashboard pane refetches), and the primary action becomes Visa N att bokföra -> /transactions, where realtime rows, the match pills and the Att bokföra badge already deliver the rest. Zero-import syncs keep the plain Klar. Also mounts the fully-built-but-orphaned BankSyncSinceLastVisit pill on the transactions footer, so returning users get the same payoff line for the nightly cron (N nya transaktioner sedan sist). Strings stay hardcoded Swedish inside the extension, matching every neighboring string in the dialog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(banking): review triage: no stale counts, no marker advance on error Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
95 lines
3.0 KiB
TypeScript
95 lines
3.0 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
import { X } from 'lucide-react'
|
|
import { createClient } from '@/lib/supabase/client'
|
|
import { useCompany } from '@/contexts/CompanyContext'
|
|
|
|
/**
|
|
* One-time pill telling the user how many new PSD2-synced transactions
|
|
* have arrived since they last visited /transactions. Helps make the
|
|
* nightly cron visible without polling or pushing notifications.
|
|
*
|
|
* State lives in localStorage keyed per company. On mount:
|
|
* - If no previous visit recorded: store now, render nothing.
|
|
* - Else: count rows added since lastVisit. If > 0, show the pill.
|
|
* - On dismiss: write `now` to localStorage and hide.
|
|
*/
|
|
export default function BankSyncSinceLastVisit() {
|
|
const t = useTranslations('transactions')
|
|
const { company } = useCompany()
|
|
const [count, setCount] = useState<number | null>(null)
|
|
const [dismissed, setDismissed] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!company?.id) return
|
|
if (typeof window === 'undefined') return
|
|
|
|
const storageKey = `gnubok.lastTransactionsVisit.${company.id}`
|
|
const lastVisitRaw = window.localStorage.getItem(storageKey)
|
|
const now = new Date().toISOString()
|
|
|
|
if (!lastVisitRaw) {
|
|
window.localStorage.setItem(storageKey, now)
|
|
return
|
|
}
|
|
|
|
let cancelled = false
|
|
const supabase = createClient()
|
|
supabase
|
|
.from('transactions')
|
|
.select('id', { count: 'exact', head: true })
|
|
.eq('company_id', company.id)
|
|
.eq('import_source', 'enable_banking')
|
|
.gt('created_at', lastVisitRaw)
|
|
.then(({ count: rowCount, error }) => {
|
|
if (cancelled) return
|
|
// A failed query must not advance the marker: that would silently
|
|
// swallow the notification for rows that DID arrive in the window.
|
|
if (error) return
|
|
if (rowCount && rowCount > 0) {
|
|
setCount(rowCount)
|
|
} else {
|
|
// Nothing new: refresh the timestamp so we don't keep checking
|
|
// the same window forever.
|
|
window.localStorage.setItem(storageKey, now)
|
|
}
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [company?.id])
|
|
|
|
if (dismissed || !count || count <= 0) return null
|
|
|
|
function handleDismiss() {
|
|
if (typeof window === 'undefined') return
|
|
if (company?.id) {
|
|
window.localStorage.setItem(
|
|
`gnubok.lastTransactionsVisit.${company.id}`,
|
|
new Date().toISOString(),
|
|
)
|
|
}
|
|
setDismissed(true)
|
|
}
|
|
|
|
return (
|
|
<div className="inline-flex items-center gap-2 rounded-md border border-border bg-secondary px-2.5 py-1 text-xs text-foreground">
|
|
<span>
|
|
{count === 1
|
|
? t('bank_sync_new_since_last_visit_one')
|
|
: t('bank_sync_new_since_last_visit_many', { count })}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={handleDismiss}
|
|
aria-label={t('bank_sync_new_since_last_visit_dismiss')}
|
|
className="ml-1 rounded-sm p-0.5 opacity-70 transition-opacity hover:opacity-100"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|