Delete invoice feature (#64)
* I mplement delete API for draft invoices with ownership verification * Color change dark mode
This commit is contained in:
@@ -32,6 +32,14 @@ import {
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import PaymentBookingDialog from '@/components/invoices/PaymentBookingDialog'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import type { Invoice, InvoiceItem, Customer, InvoiceStatus, InvoiceReminder, InvoiceDocumentType } from '@/types'
|
||||
|
||||
const statusConfig: Record<InvoiceStatus, { label: string; variant: 'default' | 'secondary' | 'success' | 'warning' | 'destructive'; icon: React.ElementType }> = {
|
||||
@@ -72,6 +80,8 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
const [isUpdating, setIsUpdating] = useState(false)
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const [isSendingEmail, setIsSendingEmail] = useState(false)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchInvoice()
|
||||
@@ -326,6 +336,39 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
setIsDownloading(false)
|
||||
}
|
||||
|
||||
async function deleteInvoice() {
|
||||
if (!invoice) return
|
||||
|
||||
setIsDeleting(true)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/invoices/${invoice.id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.error || 'Kunde inte ta bort fakturan')
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Faktura borttagen',
|
||||
description: `Utkast ${invoice.invoice_number} har tagits bort`,
|
||||
})
|
||||
|
||||
router.push('/invoices')
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Kunde inte ta bort fakturan',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
|
||||
setIsDeleting(false)
|
||||
setShowDeleteDialog(false)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
@@ -885,8 +928,8 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full text-destructive hover:text-destructive"
|
||||
onClick={() => updateStatus('cancelled')}
|
||||
disabled={isUpdating}
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Ta bort utkast
|
||||
@@ -925,6 +968,27 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete confirmation dialog */}
|
||||
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Ta bort fakturautkast</DialogTitle>
|
||||
<DialogDescription>
|
||||
Är du säker på att du vill ta bort utkast {invoice.invoice_number}? Detta kan inte ångras.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowDeleteDialog(false)} disabled={isDeleting}>
|
||||
Avbryt
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={deleteInvoice} disabled={isDeleting}>
|
||||
{isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Ta bort
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<PaymentBookingDialog
|
||||
open={showPaymentDialog}
|
||||
onOpenChange={setShowPaymentDialog}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* DELETE /api/invoices/[id]
|
||||
*
|
||||
* Permanently deletes a draft invoice and its items.
|
||||
* Only invoices with status 'draft' can be deleted — committed invoices
|
||||
* are immutable per BFL and must be reversed via credit note instead.
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Fetch invoice to verify ownership and status
|
||||
const { data: invoice, error: fetchError } = await supabase
|
||||
.from('invoices')
|
||||
.select('id, status, user_id')
|
||||
.eq('id', id)
|
||||
.eq('user_id', user.id)
|
||||
.single()
|
||||
|
||||
if (fetchError || !invoice) {
|
||||
return NextResponse.json({ error: 'Invoice not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (invoice.status !== 'draft') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Endast utkast kan tas bort. Bokförda fakturor måste krediteras istället.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Delete items first (FK constraint), then the invoice
|
||||
const { error: itemsError } = await supabase
|
||||
.from('invoice_items')
|
||||
.delete()
|
||||
.eq('invoice_id', id)
|
||||
|
||||
if (itemsError) {
|
||||
return NextResponse.json({ error: itemsError.message }, { status: 500 })
|
||||
}
|
||||
|
||||
const { error: deleteError } = await supabase
|
||||
.from('invoices')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
.eq('user_id', user.id)
|
||||
|
||||
if (deleteError) {
|
||||
return NextResponse.json({ error: deleteError.message }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: { deleted: true } })
|
||||
}
|
||||
+1
-1
@@ -105,7 +105,7 @@
|
||||
--success-foreground: 0 0% 100%;
|
||||
|
||||
--warning: 38 50% 55%;
|
||||
--warning-foreground: 0 0% 9%;
|
||||
--warning-foreground: 38 50% 90%;
|
||||
|
||||
--warm-accent: 38 42% 58%;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user