'use client' import { useState, useEffect, useMemo } from 'react' import { useBookingTemplates } from '@/lib/reference-data/hooks' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { useToast } from '@/components/ui/use-toast' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { BookOpen, Search, Building2, Users, Globe } from 'lucide-react' import { TEMPLATE_CATEGORY_LABELS, SCOPE_LABELS, getTemplateScope, applyTemplate } from '@/lib/bookkeeping/template-library' import type { BookingTemplateCategory, EntityType } from '@/types' import type { FormLine } from '@/components/bookkeeping/JournalEntryForm' interface Props { onApply: (lines: FormLine[], description: string, category?: BookingTemplateCategory) => void entityType?: EntityType /** Prefill the "total amount" field when the caller already knows it (e.g. * booking from an underlag with a known total). The user can still edit it. */ defaultAmount?: number } const SCOPE_ICONS = { system: Globe, team: Users, company: Building2, } as const export default function BookingTemplatePicker({ onApply, entityType, defaultAmount }: Props) { const { toast } = useToast() const [open, setOpen] = useState(false) // Session-cached (lib/reference-data): the list is there on the first // open and reopening costs no request; writes in the settings panel // invalidate it. const { templates, isLoading, error: templatesError } = useBookingTemplates() const [search, setSearch] = useState('') const [selectedCategory, setSelectedCategory] = useState('all') const [amount, setAmount] = useState('') const [selectedId, setSelectedId] = useState(null) // Prefill the amount from the caller's known total each time the picker // opens. Only when provided: callers without a known amount (e.g. the // journal-entry form) keep the blank-then-type behaviour. useEffect(() => { if (open && defaultAmount != null && defaultAmount > 0) { setAmount(String(Math.round(defaultAmount * 100) / 100)) } }, [open, defaultAmount]) useEffect(() => { if (open && templatesError) toast({ title: 'Kunde inte hämta mallar', variant: 'destructive' }) }, [open, templatesError, toast]) const filtered = useMemo(() => { // Templates hidden for this company (opt-in via the settings panel) // never surface in the picker; only settings shows them, for restore. let result = templates.filter((t) => !t.is_hidden) // Filter by entity type if (entityType) { result = result.filter((t) => t.entity_type === 'all' || t.entity_type === entityType) } // Filter by category if (selectedCategory !== 'all') { result = result.filter((t) => t.category === selectedCategory) } // Filter by search if (search) { const lower = search.toLowerCase() result = result.filter( (t) => t.name.toLowerCase().includes(lower) || t.description.toLowerCase().includes(lower), ) } return result }, [templates, entityType, selectedCategory, search]) // Unique categories present in visible templates. Built from the // hidden-filtered list so a category whose templates are all hidden does // not render a chip that can only ever match nothing. const availableCategories = useMemo(() => { const cats = new Set(templates.filter((t) => !t.is_hidden).map((t) => t.category)) return Array.from(cats).sort() }, [templates]) const selected = selectedId ? templates.find((t) => t.id === selectedId) : null function handleApply() { if (!selected) return const totalAmount = parseFloat(amount) if (!totalAmount || totalAmount <= 0) { toast({ title: 'Ange belopp', description: 'Ange ett belopp för att använda mallen.', variant: 'destructive' }) return } const lines = applyTemplate(selected.lines, totalAmount) // Fire-and-forget MRU bump so this template surfaces at the top next time. fetch(`/api/settings/booking-templates/${selected.id}/touch`, { method: 'POST' }).catch(() => {}) onApply(lines, selected.name, selected.category) setOpen(false) setSelectedId(null) setAmount('') setSearch('') } return ( Bokföringsmallar {/* Search + category filter */}
setSearch(e.target.value)} placeholder="Sök mall..." className="pl-9" autoFocus />
{availableCategories.map((cat) => ( ))}
{/* Template list */}
{isLoading ? (

Laddar mallar...

) : filtered.length === 0 ? (

Inga mallar hittades.

) : ( filtered.map((t) => { const scope = getTemplateScope(t) const ScopeIcon = SCOPE_ICONS[scope] const isSelected = selectedId === t.id return ( ) }) )}
{/* Apply section */} {selected && (
setAmount(e.target.value)} placeholder="0,00" min="0" step="0.01" inputMode="decimal" autoFocus />
)}
) }