Files
accounted/app/(dashboard)/invoices/[id]/credit/page.tsx
T
2026-02-13 14:00:46 +01:00

317 lines
10 KiB
TypeScript

'use client'
import { useState, useEffect, use } from 'react'
import { useRouter } from 'next/navigation'
import { createClient } from '@/lib/supabase/client'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { useToast } from '@/components/ui/use-toast'
import { formatCurrency, formatDate } from '@/lib/utils'
import { getVatTreatmentLabel } from '@/lib/invoice/vat-rules'
import { Loader2, ArrowLeft, AlertTriangle } from 'lucide-react'
import type { Invoice, InvoiceItem, Customer } from '@/types'
interface InvoiceWithRelations extends Invoice {
customer: Customer
items: InvoiceItem[]
}
export default function CreateCreditNotePage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
const router = useRouter()
const { toast } = useToast()
const supabase = createClient()
const [invoice, setInvoice] = useState<InvoiceWithRelations | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isSubmitting, setIsSubmitting] = useState(false)
const [reason, setReason] = useState('')
useEffect(() => {
fetchInvoice()
}, [id])
async function fetchInvoice() {
setIsLoading(true)
const { data, error } = await supabase
.from('invoices')
.select(`
*,
customer:customers(*),
items:invoice_items(*)
`)
.eq('id', id)
.single()
if (error || !data) {
toast({
title: 'Fel',
description: 'Kunde inte hämta faktura',
variant: 'destructive',
})
router.push('/invoices')
return
}
// Check if invoice can be credited
if (!['sent', 'paid', 'overdue'].includes(data.status)) {
toast({
title: 'Kan inte krediteras',
description: 'Endast skickade, betalda eller förfallna fakturor kan krediteras',
variant: 'destructive',
})
router.push(`/invoices/${id}`)
return
}
if (data.status === 'credited') {
toast({
title: 'Redan krediterad',
description: 'Denna faktura har redan krediterats',
variant: 'destructive',
})
router.push(`/invoices/${id}`)
return
}
// Sort items by sort_order
if (data.items) {
data.items.sort((a: InvoiceItem, b: InvoiceItem) => a.sort_order - b.sort_order)
}
setInvoice(data as InvoiceWithRelations)
setReason(`Krediterar faktura ${data.invoice_number}`)
setIsLoading(false)
}
async function handleSubmit() {
if (!invoice) return
setIsSubmitting(true)
try {
const response = await fetch('/api/invoices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
credited_invoice_id: invoice.id,
reason,
}),
})
if (!response.ok) {
const data = await response.json()
throw new Error(data.error || 'Failed to create credit note')
}
const { data: creditNote } = await response.json()
toast({
title: 'Kreditfaktura skapad',
description: `Kreditfaktura ${creditNote.invoice_number} har skapats`,
})
router.push(`/invoices/${creditNote.id}`)
} catch (error) {
toast({
title: 'Fel',
description: error instanceof Error ? error.message : 'Kunde inte skapa kreditfaktura',
variant: 'destructive',
})
}
setIsSubmitting(false)
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
)
}
if (!invoice) {
return null
}
const customer = invoice.customer
return (
<div className="space-y-6 max-w-3xl mx-auto">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h1 className="text-3xl font-bold tracking-tight">Skapa kreditfaktura</h1>
<p className="text-muted-foreground">
Krediterar faktura {invoice.invoice_number}
</p>
</div>
</div>
{/* Warning */}
<Card className="border-warning/50 bg-warning/5">
<CardContent className="flex items-start gap-4 pt-6">
<AlertTriangle className="h-5 w-5 text-warning flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium">Viktigt om kreditfakturor</p>
<p className="text-sm text-muted-foreground mt-1">
En kreditfaktura makulerar den ursprungliga fakturan helt.
Alla belopp blir negativa och den ursprungliga fakturan markeras som krediterad.
Denna åtgärd kan inte ångras.
</p>
</div>
</CardContent>
</Card>
{/* Original invoice info */}
<Card>
<CardHeader>
<CardTitle>Ursprunglig faktura</CardTitle>
<CardDescription>
Kreditfakturan baseras denna faktura
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Fakturanummer:</span>
<span className="ml-2 font-medium">{invoice.invoice_number}</span>
</div>
<div>
<span className="text-muted-foreground">Datum:</span>
<span className="ml-2">{formatDate(invoice.invoice_date)}</span>
</div>
<div>
<span className="text-muted-foreground">Kund:</span>
<span className="ml-2">{customer.name}</span>
</div>
<div>
<span className="text-muted-foreground">Momsbehandling:</span>
<span className="ml-2">{getVatTreatmentLabel(invoice.vat_treatment)}</span>
</div>
</div>
</CardContent>
</Card>
{/* Credit note preview */}
<Card>
<CardHeader>
<CardTitle>Kreditfaktura förhandsgranskning</CardTitle>
<CardDescription>
Kreditfakturanummer: KR-{invoice.invoice_number}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Header */}
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-muted-foreground border-b pb-2">
<div className="col-span-5">Beskrivning</div>
<div className="col-span-2 text-right">Antal</div>
<div className="col-span-1 text-center">Enhet</div>
<div className="col-span-2 text-right">à-pris</div>
<div className="col-span-2 text-right">Summa</div>
</div>
{/* Items (negated) */}
{invoice.items.map((item) => (
<div key={item.id} className="grid grid-cols-12 gap-4 text-sm">
<div className="col-span-5">{item.description}</div>
<div className="col-span-2 text-right text-destructive">
-{Math.abs(item.quantity)}
</div>
<div className="col-span-1 text-center">{item.unit}</div>
<div className="col-span-2 text-right">
{formatCurrency(item.unit_price, invoice.currency)}
</div>
<div className="col-span-2 text-right font-medium text-destructive">
{formatCurrency(-Math.abs(item.line_total), invoice.currency)}
</div>
</div>
))}
<Separator />
{/* Totals (negated) */}
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-muted-foreground">Delsumma</span>
<span className="text-destructive">
{formatCurrency(-Math.abs(invoice.subtotal), invoice.currency)}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Moms ({invoice.vat_rate}%)</span>
<span className="text-destructive">
{formatCurrency(-Math.abs(invoice.vat_amount), invoice.currency)}
</span>
</div>
<Separator />
<div className="flex justify-between font-bold text-lg">
<span>Totalt</span>
<span className="text-destructive">
{formatCurrency(-Math.abs(invoice.total), invoice.currency)}
</span>
</div>
{invoice.currency !== 'SEK' && invoice.total_sek && (
<div className="flex justify-between text-sm text-muted-foreground">
<span>I SEK (kurs {invoice.exchange_rate})</span>
<span className="text-destructive">
{formatCurrency(-Math.abs(invoice.total_sek))}
</span>
</div>
)}
</div>
</div>
</CardContent>
</Card>
{/* Reason */}
<Card>
<CardHeader>
<CardTitle>Anledning</CardTitle>
<CardDescription>
Ange anledning till kreditering (visas kreditfakturan)
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Label htmlFor="reason">Anledning</Label>
<Textarea
id="reason"
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="T.ex. Felaktig fakturering, returnerade varor..."
rows={3}
/>
</div>
</CardContent>
</Card>
{/* Actions */}
<div className="flex justify-end gap-4">
<Button variant="outline" onClick={() => router.back()}>
Avbryt
</Button>
<Button onClick={handleSubmit} disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Skapar...
</>
) : (
'Skapa kreditfaktura'
)}
</Button>
</div>
</div>
)
}