feat: add review confirmation dialogs and BAS account number tooltips
Add modal review/confirmation dialogs before submitting invoices, supplier invoices, and journal entries. Since journal entries are legally immutable once posted, users now see a full summary with an amber warning before confirming. Add AccountNumber component with rich tooltips showing account name, class, type, and plain-language Swedish explanation for ~45 key BAS accounts across all report views. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
885f362a29
commit
ba94f60d06
@@ -7,6 +7,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ChevronDown, ChevronRight, Search } from 'lucide-react'
|
||||
import { AccountNumber } from '@/components/ui/account-number'
|
||||
import type { BASAccount } from '@/types'
|
||||
|
||||
const CLASS_LABELS: Record<number, string> = {
|
||||
@@ -150,7 +151,7 @@ export default function ChartOfAccounts() {
|
||||
!account.is_active ? 'opacity-50' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="py-2 font-mono">{account.account_number}</td>
|
||||
<td className="py-2"><AccountNumber number={account.account_number} name={account.account_name} /></td>
|
||||
<td className="py-2">{account.account_name}</td>
|
||||
<td className="py-2 text-center">
|
||||
{editingSRU === account.id ? (
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'
|
||||
import { JournalEntryReviewContent } from '@/components/bookkeeping/JournalEntryReviewContent'
|
||||
import type { CreateJournalEntryLineInput, FiscalPeriod } from '@/types'
|
||||
|
||||
interface Props {
|
||||
@@ -31,6 +33,7 @@ export default function JournalEntryForm({ onCreated }: Props) {
|
||||
{ account_number: '', debit_amount: '', credit_amount: '', line_description: '' },
|
||||
])
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [showReview, setShowReview] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchPeriods()
|
||||
@@ -75,9 +78,12 @@ export default function JournalEntryForm({ onCreated }: Props) {
|
||||
const totalCredit = lines.reduce((sum, l) => sum + (parseFloat(l.credit_amount) || 0), 0)
|
||||
const isBalanced = Math.abs(totalDebit - totalCredit) < 0.01 && totalDebit > 0
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const handleReview = () => {
|
||||
if (!selectedPeriod || !description || !isBalanced) return
|
||||
setShowReview(true)
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setIsSubmitting(true)
|
||||
|
||||
const entryLines: CreateJournalEntryLineInput[] = lines
|
||||
@@ -114,6 +120,7 @@ export default function JournalEntryForm({ onCreated }: Props) {
|
||||
title: 'Verifikation skapad',
|
||||
description: `Verifikation ${result.data?.voucher_series}${result.data?.voucher_number} har skapats.`,
|
||||
})
|
||||
setShowReview(false)
|
||||
// Reset form
|
||||
setDescription('')
|
||||
setLines([
|
||||
@@ -276,12 +283,30 @@ export default function JournalEntryForm({ onCreated }: Props) {
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
onClick={handleReview}
|
||||
disabled={!isBalanced || !description || !selectedPeriod || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Sparar...' : 'Skapa verifikation'}
|
||||
Granska & skapa
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={showReview}
|
||||
onOpenChange={setShowReview}
|
||||
onConfirm={handleConfirm}
|
||||
isSubmitting={isSubmitting}
|
||||
title="Granska verifikation"
|
||||
warningText="En verifikation skapas och kan inte ändras efteråt. Korrigeringar görs genom storno."
|
||||
>
|
||||
<JournalEntryReviewContent
|
||||
periodName={periods.find((p) => p.id === selectedPeriod)?.name || ''}
|
||||
entryDate={entryDate}
|
||||
description={description}
|
||||
lines={lines}
|
||||
totalDebit={totalDebit}
|
||||
totalCredit={totalCredit}
|
||||
/>
|
||||
</ConfirmationDialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { AccountNumber } from '@/components/ui/account-number'
|
||||
import type { JournalEntry, JournalEntryLine } from '@/types'
|
||||
|
||||
interface Props {
|
||||
@@ -138,7 +139,7 @@ export default function JournalEntryList({ periodId }: Props) {
|
||||
.sort((a, b) => a.sort_order - b.sort_order)
|
||||
.map((line) => (
|
||||
<tr key={line.id} className="border-b last:border-0">
|
||||
<td className="py-2 font-mono">{line.account_number}</td>
|
||||
<td className="py-2"><AccountNumber number={line.account_number} /></td>
|
||||
<td className="py-2 text-muted-foreground">
|
||||
{line.line_description || ''}
|
||||
</td>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
'use client'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { AccountNumber } from '@/components/ui/account-number'
|
||||
import { CheckCircle2 } from 'lucide-react'
|
||||
|
||||
interface ReviewLine {
|
||||
account_number: string
|
||||
debit_amount: string
|
||||
credit_amount: string
|
||||
line_description: string
|
||||
}
|
||||
|
||||
interface JournalEntryReviewContentProps {
|
||||
periodName: string
|
||||
entryDate: string
|
||||
description: string
|
||||
lines: ReviewLine[]
|
||||
totalDebit: number
|
||||
totalCredit: number
|
||||
}
|
||||
|
||||
function formatAmount(amount: number): string {
|
||||
return amount.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
export function JournalEntryReviewContent({
|
||||
periodName,
|
||||
entryDate,
|
||||
description,
|
||||
lines,
|
||||
totalDebit,
|
||||
totalCredit,
|
||||
}: JournalEntryReviewContentProps) {
|
||||
const activeLines = lines.filter(
|
||||
(l) => l.account_number && (l.debit_amount || l.credit_amount)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header info */}
|
||||
<div className="bg-muted rounded-lg p-4 space-y-2">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Räkenskapsår</span>
|
||||
<p className="font-medium">{periodName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Datum</span>
|
||||
<p className="font-medium">{entryDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Beskrivning</span>
|
||||
<p className="font-medium">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Balance status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
Debet = Kredit
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Debit/Credit table */}
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2 w-24">Konto</th>
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-28 text-right">Debet</th>
|
||||
<th className="py-2 w-28 text-right">Kredit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{activeLines.map((line, index) => (
|
||||
<tr key={index} className="border-b last:border-0">
|
||||
<td className="py-2">
|
||||
<AccountNumber number={line.account_number} />
|
||||
</td>
|
||||
<td className="py-2 text-muted-foreground">
|
||||
{line.line_description || ''}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{parseFloat(line.debit_amount) > 0
|
||||
? formatAmount(parseFloat(line.debit_amount))
|
||||
: ''}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{parseFloat(line.credit_amount) > 0
|
||||
? formatAmount(parseFloat(line.credit_amount))
|
||||
: ''}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="font-semibold border-t-2">
|
||||
<td colSpan={2} className="py-2">Summa</td>
|
||||
<td className="py-2 text-right text-green-600">{formatAmount(totalDebit)}</td>
|
||||
<td className="py-2 text-right text-green-600">{formatAmount(totalCredit)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
'use client'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { getVatTreatmentLabel } from '@/lib/invoice/vat-rules'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import type { Customer, Currency, VatTreatment } from '@/types'
|
||||
|
||||
interface ReviewItem {
|
||||
description: string
|
||||
quantity: number
|
||||
unit: string
|
||||
unit_price: number
|
||||
}
|
||||
|
||||
interface InvoiceReviewContentProps {
|
||||
customer: Customer
|
||||
invoiceDate: string
|
||||
dueDate: string
|
||||
currency: Currency
|
||||
items: ReviewItem[]
|
||||
subtotal: number
|
||||
vatRate: number
|
||||
vatAmount: number
|
||||
total: number
|
||||
vatTreatment: VatTreatment
|
||||
yourReference?: string
|
||||
ourReference?: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export function InvoiceReviewContent({
|
||||
customer,
|
||||
invoiceDate,
|
||||
dueDate,
|
||||
currency,
|
||||
items,
|
||||
subtotal,
|
||||
vatRate,
|
||||
vatAmount,
|
||||
total,
|
||||
vatTreatment,
|
||||
yourReference,
|
||||
ourReference,
|
||||
notes,
|
||||
}: InvoiceReviewContentProps) {
|
||||
const customerTypeLabel: Record<string, string> = {
|
||||
individual: 'Privatperson',
|
||||
swedish_business: 'Svenskt företag',
|
||||
eu_business: 'EU-företag',
|
||||
non_eu_business: 'Utanför EU',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Customer info */}
|
||||
<div className="bg-muted rounded-lg p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-base">{customer.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{customer.email}</p>
|
||||
</div>
|
||||
<Badge variant="outline">
|
||||
{customerTypeLabel[customer.customer_type] || customer.customer_type}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* VAT treatment */}
|
||||
<Badge className="text-sm px-3 py-1">
|
||||
{getVatTreatmentLabel(vatTreatment)}
|
||||
</Badge>
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Fakturadatum</span>
|
||||
<p className="font-medium">{invoiceDate}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Förfallodatum</span>
|
||||
<p className="font-medium">{dueDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Line items table */}
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-16 text-right">Antal</th>
|
||||
<th className="py-2 w-16 text-center">Enhet</th>
|
||||
<th className="py-2 w-24 text-right">À-pris</th>
|
||||
<th className="py-2 w-28 text-right">Belopp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => (
|
||||
<tr key={index} className="border-b last:border-0">
|
||||
<td className="py-2">{item.description}</td>
|
||||
<td className="py-2 text-right">{item.quantity}</td>
|
||||
<td className="py-2 text-center">{item.unit}</td>
|
||||
<td className="py-2 text-right">{formatCurrency(item.unit_price, currency)}</td>
|
||||
<td className="py-2 text-right">
|
||||
{formatCurrency(item.quantity * item.unit_price, currency)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Delsumma</span>
|
||||
<span>{formatCurrency(subtotal, currency)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Moms ({vatRate}%)</span>
|
||||
<span>{formatCurrency(vatAmount, currency)}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between font-bold text-2xl">
|
||||
<span>Totalt</span>
|
||||
<span>{formatCurrency(total, currency)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* References/notes */}
|
||||
{(yourReference || ourReference || notes) && (
|
||||
<div className="border-t pt-3 space-y-1 text-sm text-muted-foreground">
|
||||
{yourReference && <p>Er referens: {yourReference}</p>}
|
||||
{ourReference && <p>Vår referens: {ourReference}</p>}
|
||||
{notes && <p>Anteckning: {notes}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
'use client'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { AccountNumber } from '@/components/ui/account-number'
|
||||
import type { Supplier, VatTreatment } from '@/types'
|
||||
|
||||
interface ReviewLineItem {
|
||||
description: string
|
||||
quantity: number
|
||||
unit: string
|
||||
unit_price: number
|
||||
account_number: string
|
||||
vat_rate: number
|
||||
}
|
||||
|
||||
interface SupplierInvoiceReviewContentProps {
|
||||
supplier: Supplier
|
||||
invoiceNumber: string
|
||||
invoiceDate: string
|
||||
dueDate: string
|
||||
deliveryDate?: string
|
||||
currency: string
|
||||
exchangeRate?: string
|
||||
vatTreatment: VatTreatment
|
||||
reverseCharge: boolean
|
||||
paymentReference?: string
|
||||
items: ReviewLineItem[]
|
||||
subtotal: number
|
||||
totalVat: number
|
||||
total: number
|
||||
}
|
||||
|
||||
function formatAmount(amount: number): string {
|
||||
return amount.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
const VAT_TREATMENT_LABELS: Record<VatTreatment, string> = {
|
||||
standard_25: '25% moms',
|
||||
reduced_12: '12% moms',
|
||||
reduced_6: '6% moms',
|
||||
reverse_charge: 'Omvänd skattskyldighet',
|
||||
export: 'Export (0%)',
|
||||
exempt: 'Momsfritt',
|
||||
}
|
||||
|
||||
export function SupplierInvoiceReviewContent({
|
||||
supplier,
|
||||
invoiceNumber,
|
||||
invoiceDate,
|
||||
dueDate,
|
||||
deliveryDate,
|
||||
currency,
|
||||
exchangeRate,
|
||||
vatTreatment,
|
||||
reverseCharge,
|
||||
paymentReference,
|
||||
items,
|
||||
subtotal,
|
||||
totalVat,
|
||||
total,
|
||||
}: SupplierInvoiceReviewContentProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Supplier info */}
|
||||
<div className="bg-muted rounded-lg p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-base">{supplier.name}</p>
|
||||
<p className="text-sm text-muted-foreground">Fakturanr: {invoiceNumber}</p>
|
||||
</div>
|
||||
{reverseCharge && (
|
||||
<Badge variant="outline" className="border-orange-300 text-orange-700 dark:text-orange-400">
|
||||
Omvänd skattskyldighet
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* VAT + currency badges */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge className="text-sm px-3 py-1">
|
||||
{VAT_TREATMENT_LABELS[vatTreatment]}
|
||||
</Badge>
|
||||
{currency !== 'SEK' && (
|
||||
<Badge variant="outline" className="text-sm px-3 py-1">
|
||||
{currency}
|
||||
{exchangeRate && ` (kurs ${exchangeRate})`}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Fakturadatum</span>
|
||||
<p className="font-medium">{invoiceDate}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Förfallodatum</span>
|
||||
<p className="font-medium">{dueDate}</p>
|
||||
</div>
|
||||
{deliveryDate && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Leveransdatum</span>
|
||||
<p className="font-medium">{deliveryDate}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Line items table */}
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-16 text-right">Antal</th>
|
||||
<th className="py-2 w-24 text-right">À-pris</th>
|
||||
<th className="py-2 w-20">Konto</th>
|
||||
<th className="py-2 w-16 text-right">Moms</th>
|
||||
<th className="py-2 w-28 text-right">Belopp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
const lineTotal = Math.round((item.quantity || 0) * (item.unit_price || 0) * 100) / 100
|
||||
return (
|
||||
<tr key={index} className="border-b last:border-0">
|
||||
<td className="py-2">{item.description}</td>
|
||||
<td className="py-2 text-right">{item.quantity} {item.unit}</td>
|
||||
<td className="py-2 text-right">{formatAmount(item.unit_price)}</td>
|
||||
<td className="py-2">
|
||||
<AccountNumber number={item.account_number} size="sm" />
|
||||
</td>
|
||||
<td className="py-2 text-right">{Math.round(item.vat_rate * 100)}%</td>
|
||||
<td className="py-2 text-right">{formatAmount(lineTotal)}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Netto (exkl. moms)</span>
|
||||
<span>{formatAmount(subtotal)} kr</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Moms</span>
|
||||
<span>{formatAmount(totalVat)} kr</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between font-bold text-2xl">
|
||||
<span>Totalt</span>
|
||||
<span>{formatAmount(total)} kr</span>
|
||||
</div>
|
||||
{currency !== 'SEK' && exchangeRate && (
|
||||
<div className="flex justify-between text-muted-foreground">
|
||||
<span>SEK-belopp (vid kurs {exchangeRate})</span>
|
||||
<span>{formatAmount(total * parseFloat(exchangeRate))} kr</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Payment reference */}
|
||||
{paymentReference && (
|
||||
<div className="border-t pt-3 text-sm text-muted-foreground">
|
||||
<p>Betalningsreferens: {paymentReference}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
|
||||
import { getAccountDescription, type AccountType } from '@/lib/bookkeeping/account-descriptions'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
} from '@/components/ui/info-tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const TYPE_COLORS: Record<AccountType, string> = {
|
||||
asset: 'bg-green-500',
|
||||
liability: 'bg-orange-500',
|
||||
equity: 'bg-blue-500',
|
||||
revenue: 'bg-purple-500',
|
||||
expense: 'bg-red-500',
|
||||
}
|
||||
|
||||
const TYPE_LABELS: Record<AccountType, string> = {
|
||||
asset: 'Tillgång',
|
||||
liability: 'Skuld',
|
||||
equity: 'Eget kapital',
|
||||
revenue: 'Intäkt',
|
||||
expense: 'Kostnad',
|
||||
}
|
||||
|
||||
interface AccountNumberProps {
|
||||
number: string
|
||||
name?: string
|
||||
showName?: boolean
|
||||
size?: 'sm' | 'default'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function AccountNumber({
|
||||
number,
|
||||
name,
|
||||
showName,
|
||||
size = 'default',
|
||||
className,
|
||||
}: AccountNumberProps) {
|
||||
const desc = getAccountDescription(number)
|
||||
const displayName = desc?.name ?? name
|
||||
|
||||
const numberElement = (
|
||||
<span
|
||||
className={cn(
|
||||
'font-mono',
|
||||
size === 'sm' ? 'text-xs' : 'text-sm',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{number}
|
||||
</span>
|
||||
)
|
||||
|
||||
if (!desc) {
|
||||
return (
|
||||
<>
|
||||
{numberElement}
|
||||
{showName && displayName && (
|
||||
<span className="ml-1">{displayName}</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 cursor-help',
|
||||
'decoration-dotted underline underline-offset-4 decoration-muted-foreground/40',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'font-mono',
|
||||
size === 'sm' ? 'text-xs' : 'text-sm',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{number}
|
||||
</span>
|
||||
{showName && displayName && (
|
||||
<span className={size === 'sm' ? 'text-xs' : 'text-sm'}>
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-xs p-3">
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('h-2 w-2 rounded-full shrink-0', TYPE_COLORS[desc.type])} />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{desc.classLabel} · {TYPE_LABELS[desc.type]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
<span className="font-mono mr-1.5">{number}</span>
|
||||
{desc.name}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
{desc.explanation}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ClipboardCheck, Loader2, AlertTriangle } from 'lucide-react'
|
||||
|
||||
interface ConfirmationDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: () => void
|
||||
isSubmitting: boolean
|
||||
title: string
|
||||
warningText?: string
|
||||
confirmLabel?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function ConfirmationDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
isSubmitting,
|
||||
title,
|
||||
warningText = 'En verifikation skapas och kan inte ändras efteråt.',
|
||||
confirmLabel = 'Bekräfta & skapa',
|
||||
children,
|
||||
}: ConfirmationDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl border-t-2 border-primary p-0 gap-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
|
||||
<ClipboardCheck className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle className="text-xl">{title}</DialogTitle>
|
||||
<DialogDescription>Granska uppgifterna innan du bekräftar</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-y-auto max-h-[70vh] px-6 pb-4">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="border-t px-6 py-4 space-y-4">
|
||||
<div className="flex items-start gap-2 rounded-lg bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 p-3">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
|
||||
<p className="text-sm text-amber-800 dark:text-amber-300">{warningText}</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Tillbaka
|
||||
</Button>
|
||||
<Button onClick={onConfirm} disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Skapar...
|
||||
</>
|
||||
) : (
|
||||
confirmLabel
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user