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

446 lines
14 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { use } from 'react'
import Link from 'next/link'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { useToast } from '@/components/ui/use-toast'
import CustomerForm from '@/components/customers/CustomerForm'
import { CampaignStatusBadge } from '@/components/campaigns/CampaignStatusBadge'
import {
ArrowLeft,
Building,
Globe,
User,
Mail,
Phone,
MapPin,
Edit2,
Trash2,
FileText,
Loader2,
Receipt,
Briefcase,
} from 'lucide-react'
import type { Customer, CustomerType, CreateCustomerInput, CampaignStatus } from '@/types'
const customerTypeLabels: Record<CustomerType, string> = {
individual: 'Privatperson',
swedish_business: 'Svenskt foretag',
eu_business: 'EU-foretag',
non_eu_business: 'Utanfor EU',
}
const customerTypeIcons: Record<CustomerType, React.ElementType> = {
individual: User,
swedish_business: Building,
eu_business: Globe,
non_eu_business: Globe,
}
interface RelatedCampaign {
id: string
name: string
status: CampaignStatus
total_value: number | null
currency: string | null
publication_date: string | null
brand_name: string | null
}
interface RelatedInvoice {
id: string
invoice_number: string
invoice_date: string
due_date: string
status: string
total: number
currency: string
payment_status: string
}
interface CustomerWithRelations extends Customer {
campaigns: RelatedCampaign[]
invoices: RelatedInvoice[]
}
export default function CustomerDetailPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = use(params)
const router = useRouter()
const { toast } = useToast()
const [customer, setCustomer] = useState<CustomerWithRelations | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isEditOpen, setIsEditOpen] = useState(false)
const [isUpdating, setIsUpdating] = useState(false)
useEffect(() => {
fetchCustomer()
}, [id])
async function fetchCustomer() {
setIsLoading(true)
try {
const response = await fetch(`/api/customers/${id}`)
if (!response.ok) {
throw new Error('Not found')
}
const { data } = await response.json()
setCustomer(data)
} catch {
toast({
title: 'Fel',
description: 'Kunde inte hitta kunden',
variant: 'destructive',
})
router.push('/customers')
} finally {
setIsLoading(false)
}
}
async function handleUpdate(data: CreateCustomerInput) {
setIsUpdating(true)
try {
const response = await fetch(`/api/customers/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) {
throw new Error('Update failed')
}
toast({
title: 'Kund uppdaterad',
description: data.name,
})
setIsEditOpen(false)
fetchCustomer()
} catch {
toast({
title: 'Fel',
description: 'Kunde inte uppdatera kunden',
variant: 'destructive',
})
} finally {
setIsUpdating(false)
}
}
async function handleDelete() {
if (!customer) return
if (!confirm(`Ta bort "${customer.name}"? Detta kan inte angras.`)) return
try {
const response = await fetch(`/api/customers/${id}`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error('Delete failed')
}
toast({
title: 'Kund borttagen',
description: customer.name,
})
router.push('/customers')
} catch {
toast({
title: 'Fel',
description: 'Kunde inte ta bort kunden',
variant: 'destructive',
})
}
}
const formatCurrency = (amount: number | null, currency: string | null) => {
if (!amount) return '-'
return new Intl.NumberFormat('sv-SE', {
style: 'currency',
currency: currency || 'SEK',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount)
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
)
}
if (!customer) return null
const Icon = customerTypeIcons[customer.customer_type]
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div>
<Link
href="/customers"
className="text-sm text-muted-foreground hover:text-foreground flex items-center gap-1 mb-2"
>
<ArrowLeft className="h-4 w-4" />
Tillbaka till kunder
</Link>
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
<Icon className="h-6 w-6 text-primary" />
</div>
<div>
<h1 className="text-3xl font-bold tracking-tight">{customer.name}</h1>
<Badge variant="secondary">{customerTypeLabels[customer.customer_type]}</Badge>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setIsEditOpen(true)}>
<Edit2 className="h-4 w-4 mr-1" />
Redigera
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDelete}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4 mr-1" />
Ta bort
</Button>
</div>
</div>
{/* Info cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{/* Contact */}
<Card>
<CardHeader>
<CardTitle className="text-base">Kontaktuppgifter</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{customer.email && (
<div className="flex items-center gap-2 text-sm">
<Mail className="h-4 w-4 text-muted-foreground" />
<a href={`mailto:${customer.email}`} className="hover:underline">
{customer.email}
</a>
</div>
)}
{customer.phone && (
<div className="flex items-center gap-2 text-sm">
<Phone className="h-4 w-4 text-muted-foreground" />
{customer.phone}
</div>
)}
{(customer.address_line1 || customer.city) && (
<div className="flex items-start gap-2 text-sm">
<MapPin className="h-4 w-4 text-muted-foreground mt-0.5" />
<div>
{customer.address_line1 && <p>{customer.address_line1}</p>}
{customer.address_line2 && <p>{customer.address_line2}</p>}
{(customer.postal_code || customer.city) && (
<p>{[customer.postal_code, customer.city].filter(Boolean).join(' ')}</p>
)}
{customer.country && <p>{customer.country}</p>}
</div>
</div>
)}
{!customer.email && !customer.phone && !customer.address_line1 && !customer.city && (
<p className="text-sm text-muted-foreground">Inga kontaktuppgifter</p>
)}
</CardContent>
</Card>
{/* Business details */}
<Card>
<CardHeader>
<CardTitle className="text-base">Foretagsuppgifter</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{customer.org_number && (
<div className="text-sm">
<span className="text-muted-foreground">Org.nr: </span>
{customer.org_number}
</div>
)}
{customer.vat_number && (
<div className="text-sm flex items-center gap-2">
<span className="text-muted-foreground">VAT: </span>
{customer.vat_number}
{customer.vat_number_validated && (
<Badge variant="success" className="text-xs">Verifierad</Badge>
)}
</div>
)}
<div className="text-sm">
<span className="text-muted-foreground">Betalningsvillkor: </span>
{customer.default_payment_terms || 30} dagar
</div>
{!customer.org_number && !customer.vat_number && (
<p className="text-sm text-muted-foreground">Inga foretagsuppgifter</p>
)}
</CardContent>
</Card>
{/* Summary */}
<Card>
<CardHeader>
<CardTitle className="text-base">Oversikt</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2 text-sm">
<Briefcase className="h-4 w-4 text-muted-foreground" />
<span>{customer.campaigns?.length || 0} samarbeten</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Receipt className="h-4 w-4 text-muted-foreground" />
<span>{customer.invoices?.length || 0} fakturor</span>
</div>
</CardContent>
</Card>
</div>
{/* Notes */}
{customer.notes && (
<Card>
<CardHeader>
<CardTitle className="text-base">Anteckningar</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{customer.notes}</p>
</CardContent>
</Card>
)}
{/* Related campaigns */}
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Briefcase className="h-4 w-4" />
Samarbeten
{customer.campaigns?.length > 0 && (
<Badge variant="secondary">{customer.campaigns.length}</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent>
{customer.campaigns?.length > 0 ? (
<div className="space-y-2">
{customer.campaigns.map((campaign) => (
<Link
key={campaign.id}
href={`/campaigns/${campaign.id}`}
className="flex items-center justify-between p-3 rounded-lg border hover:bg-muted/50 transition-colors"
>
<div>
<p className="font-medium">{campaign.name}</p>
{campaign.brand_name && (
<p className="text-sm text-muted-foreground">{campaign.brand_name}</p>
)}
</div>
<div className="flex items-center gap-3">
<span className="text-sm tabular-nums">
{formatCurrency(campaign.total_value, campaign.currency)}
</span>
<CampaignStatusBadge status={campaign.status} />
</div>
</Link>
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-4">
Inga samarbeten kopplade till denna kund
</p>
)}
</CardContent>
</Card>
{/* Related invoices */}
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Receipt className="h-4 w-4" />
Fakturor
{customer.invoices?.length > 0 && (
<Badge variant="secondary">{customer.invoices.length}</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent>
{customer.invoices?.length > 0 ? (
<div className="space-y-2">
{customer.invoices.map((invoice) => (
<Link
key={invoice.id}
href={`/invoices/${invoice.id}`}
className="flex items-center justify-between p-3 rounded-lg border hover:bg-muted/50 transition-colors"
>
<div>
<p className="font-medium">{invoice.invoice_number}</p>
<p className="text-sm text-muted-foreground">{invoice.invoice_date}</p>
</div>
<div className="flex items-center gap-3">
<span className="text-sm tabular-nums">
{formatCurrency(invoice.total, invoice.currency)}
</span>
<Badge variant={invoice.payment_status === 'paid' ? 'success' : 'secondary'}>
{invoice.payment_status === 'paid' ? 'Betald' : invoice.payment_status === 'overdue' ? 'Forsenad' : 'Obestallt'}
</Badge>
</div>
</Link>
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-4">
Inga fakturor kopplade till denna kund
</p>
)}
</CardContent>
</Card>
{/* Edit dialog */}
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Redigera kund</DialogTitle>
</DialogHeader>
<CustomerForm
onSubmit={handleUpdate}
isLoading={isUpdating}
initialData={{
name: customer.name,
customer_type: customer.customer_type,
email: customer.email || undefined,
phone: customer.phone || undefined,
address_line1: customer.address_line1 || undefined,
address_line2: customer.address_line2 || undefined,
postal_code: customer.postal_code || undefined,
city: customer.city || undefined,
country: customer.country || undefined,
org_number: customer.org_number || undefined,
vat_number: customer.vat_number || undefined,
default_payment_terms: customer.default_payment_terms || undefined,
notes: customer.notes || undefined,
}}
/>
</DialogContent>
</Dialog>
</div>
)
}