Files
accounted/app/(dashboard)/invoices/recurring/page.tsx
T
Mattsson e4488a900b feat: add user locale preference to user_preferences table (#555)
* feat: add user locale preference to user_preferences table

- Introduced a new column 'locale' in the user_preferences table to store per-user UI language preferences.
- Added a CHECK constraint to ensure only supported locales ('sv', 'en') are allowed.
- Triggered a schema reload notification for the changes.

chore: declare CSS module support in TypeScript

- Added a declaration for CSS modules in globals.d.ts to enable TypeScript support for importing CSS files.

* feat: add Swish as an invoice payment method in company settings
2026-05-21 21:33:22 +02:00

214 lines
7.2 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { PageHeader } from '@/components/ui/page-header'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { EmptyState } from '@/components/ui/empty-state'
import { useToast } from '@/components/ui/use-toast'
import { useCanWrite } from '@/lib/hooks/use-can-write'
import { formatDate } from '@/lib/utils'
import { Plus, Repeat, Lock, AlertTriangle } from 'lucide-react'
import type { RecurringInvoiceSchedule, Customer } from '@/types'
type ScheduleRow = RecurringInvoiceSchedule & {
customer?: Pick<Customer, 'id' | 'name' | 'email'>
}
export default function RecurringInvoicesPage() {
const [schedules, setSchedules] = useState<ScheduleRow[]>([])
const [isLoading, setIsLoading] = useState(true)
const { canWrite } = useCanWrite()
const { toast } = useToast()
const router = useRouter()
const t = useTranslations('invoice_recurring')
async function fetchSchedules() {
setIsLoading(true)
try {
const res = await fetch('/api/invoices/recurring')
if (!res.ok) throw new Error('failed')
const json = await res.json()
setSchedules(json.data ?? [])
} catch {
toast({
title: t('load_failed_title'),
description: t('load_failed_description'),
variant: 'destructive',
})
}
setIsLoading(false)
}
useEffect(() => {
fetchSchedules()
}, [])
async function togglePause(s: ScheduleRow) {
const next = s.status === 'active' ? 'paused' : 'active'
const res = await fetch(`/api/invoices/recurring/${s.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: next }),
})
if (res.ok) {
toast({
title: next === 'paused' ? t('schedule_paused_title') : t('schedule_resumed_title'),
})
fetchSchedules()
} else {
toast({
title: t('schedule_update_failed_title'),
variant: 'destructive',
})
}
}
async function deleteSchedule(s: ScheduleRow) {
if (!confirm(t('delete_confirm', { name: s.name }))) {
return
}
const res = await fetch(`/api/invoices/recurring/${s.id}`, { method: 'DELETE' })
if (res.ok) {
toast({ title: t('schedule_deleted_title') })
fetchSchedules()
} else {
toast({ title: t('schedule_delete_failed_title'), variant: 'destructive' })
}
}
return (
<div className="space-y-8">
<PageHeader
title={t('title')}
action={
canWrite ? (
<Link href="/invoices/recurring/new">
<Button>
<Plus className="mr-2 h-4 w-4" />
{t('new_schedule')}
</Button>
</Link>
) : (
<Button disabled title={t('viewer_disabled_tooltip')}>
<Lock className="mr-2 h-4 w-4" />
{t('new_schedule')}
</Button>
)
}
/>
{isLoading ? (
<Card>
<CardContent className="py-12 text-center text-sm text-muted-foreground">
{t('loading')}
</CardContent>
</Card>
) : schedules.length === 0 ? (
<Card>
<CardContent className="p-0">
<EmptyState
icon={Repeat}
title={t('empty_title')}
description={t('empty_description')}
actionLabel={canWrite ? t('new_schedule') : undefined}
actionHref={canWrite ? '/invoices/recurring/new' : undefined}
/>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('th_name')}</TableHead>
<TableHead>{t('th_customer')}</TableHead>
<TableHead className="tabular-nums">{t('th_day')}</TableHead>
<TableHead className="tabular-nums">{t('th_next_run')}</TableHead>
<TableHead>{t('th_status')}</TableHead>
<TableHead className="tabular-nums text-right">{t('th_generated')}</TableHead>
<TableHead className="text-right">{t('th_actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{schedules.map((s) => (
<TableRow
key={s.id}
className="cursor-pointer"
onClick={() => router.push(`/invoices/recurring/${s.id}`)}
>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
{s.name}
{s.last_run_warning && (
<AlertTriangle
className="h-4 w-4 text-warning-foreground"
aria-label={s.last_run_warning}
/>
)}
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{s.customer?.name ?? '—'}
</TableCell>
<TableCell className="tabular-nums">{s.day_of_month}</TableCell>
<TableCell className="tabular-nums">{formatDate(s.next_run_date)}</TableCell>
<TableCell>
{s.status === 'active' ? (
<Badge variant="success">{t('status_active')}</Badge>
) : (
<Badge variant="secondary">{t('status_paused')}</Badge>
)}
</TableCell>
<TableCell className="tabular-nums text-right">
{s.generated_count}
</TableCell>
<TableCell className="text-right">
<div
className="flex justify-end gap-2"
onClick={(e) => e.stopPropagation()}
>
{canWrite && (
<>
<Button
variant="secondary"
size="sm"
onClick={() => togglePause(s)}
>
{s.status === 'active' ? t('pause') : t('resume')}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => deleteSchedule(s)}
>
{t('delete')}
</Button>
</>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
</div>
)
}