'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Badge } from '@/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { useToast } from '@/components/ui/use-toast' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Loader2, Trash2, Plus, ChevronDown, Download, Upload, Building2, Users, Globe } from 'lucide-react' import { TEMPLATE_CATEGORY_LABELS } from '@/lib/bookkeeping/template-library' import { useCanWrite } from '@/lib/hooks/use-can-write' import type { BookingTemplateLibrary, BookingTemplateCategory, BookingTemplateLibraryLine } from '@/types' const ENTITY_LABELS: Record = { all: 'Alla', enskild_firma: 'Enskild firma', aktiebolag: 'Aktiebolag', } export function BookingTemplatesPanel() { const { toast } = useToast() const { canWrite } = useCanWrite() const [templates, setTemplates] = useState([]) const [isLoading, setIsLoading] = useState(true) const [deletingId, setDeletingId] = useState(null) const [expandedId, setExpandedId] = useState(null) const [showCreate, setShowCreate] = useState(false) const importRef = useRef(null) const fetchTemplates = useCallback(async () => { try { const res = await fetch('/api/settings/booking-templates') const json = await res.json() if (json.data) setTemplates(json.data) } catch { toast({ title: 'Fel', description: 'Kunde inte hämta mallar', variant: 'destructive' }) } finally { setIsLoading(false) } }, [toast]) useEffect(() => { fetchTemplates() }, [fetchTemplates]) async function handleDelete(id: string) { setDeletingId(id) try { const res = await fetch('/api/settings/booking-templates', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }) if (!res.ok) { toast({ title: 'Fel', description: 'Kunde inte ta bort mall', variant: 'destructive' }) return } setTemplates((prev) => prev.filter((t) => t.id !== id)) toast({ title: 'Mall borttagen' }) } finally { setDeletingId(null) } } async function handleExport() { try { const res = await fetch('/api/settings/booking-templates/export') const blob = await res.blob() const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'bokforingsmallar.json' a.click() URL.revokeObjectURL(url) } catch { toast({ title: 'Fel', description: 'Kunde inte exportera mallar', variant: 'destructive' }) } } async function handleImport(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return try { const text = await file.text() const payload = JSON.parse(text) const res = await fetch('/api/settings/booking-templates/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) const json = await res.json() if (!res.ok) { toast({ title: 'Importfel', description: json.error || 'Kunde inte importera', variant: 'destructive' }) return } toast({ title: 'Import klar', description: `${json.imported} mall(ar) importerade.` }) fetchTemplates() } catch { toast({ title: 'Importfel', description: 'Ogiltig fil', variant: 'destructive' }) } finally { // Reset input so same file can be imported again if (importRef.current) importRef.current.value = '' } } // Group templates by scope const systemTemplates = templates.filter((t) => t.is_system) const teamTemplates = templates.filter((t) => t.team_id && !t.is_system) const companyTemplates = templates.filter((t) => t.company_id && !t.is_system) return (
Bokföringsmallar Återanvändbara mallar för vanliga bokföringstransaktioner. Standardmallar visas för alla, egna mallar kan skapas och delas.
{canWrite && (
Skapa bokföringsmall { setShowCreate(false) fetchTemplates() }} />
)}
{isLoading ? (
) : templates.length === 0 ? (

Inga mallar hittades.

) : (
{/* System templates */} {systemTemplates.length > 0 && ( )} {/* Team templates */} {teamTemplates.length > 0 && ( )} {/* Company templates */} {companyTemplates.length > 0 && ( )}
)}
) } function TemplateSection({ title, icon: Icon, templates, expandedId, onToggle, deletingId, onDelete, canDelete, }: { title: string icon: React.ComponentType<{ className?: string }> templates: BookingTemplateLibrary[] expandedId: string | null onToggle: (id: string | null) => void deletingId: string | null onDelete: (id: string) => void canDelete: boolean }) { return (

{title}

{templates.length}
{templates.map((t) => { const isExpanded = expandedId === t.id return (
)} {isExpanded && (
{t.description && (

{t.description}

)} {t.lines.map((line: BookingTemplateLibraryLine, i: number) => ( ))}
Konto Beskrivning Typ Debet Kredit
{line.account} {line.label} {line.type === 'vat' && line.vat_rate ? `Moms ${(line.vat_rate * 100).toFixed(0)}%` : line.type === 'settlement' ? 'Betalning' : 'Kostnad/Intäkt'} {line.side === 'debit' ? 'D' : ''} {line.side === 'credit' ? 'K' : ''}
)}
) })}
) } function CreateTemplateForm({ onCreated }: { onCreated: () => void }) { const { toast } = useToast() const [isSubmitting, setIsSubmitting] = useState(false) const [name, setName] = useState('') const [description, setDescription] = useState('') const [category, setCategory] = useState('other') const [entityType, setEntityType] = useState<'all' | 'enskild_firma' | 'aktiebolag'>('all') const [lines, setLines] = useState([ { account: '', label: '', side: 'debit', type: 'business', ratio: 1 }, { account: '', label: '', side: 'credit', type: 'settlement', ratio: 1 }, ]) function updateLine(index: number, field: keyof BookingTemplateLibraryLine, value: string | number) { setLines((prev) => { const updated = [...prev] updated[index] = { ...updated[index], [field]: value } return updated }) } function addLine() { setLines((prev) => [...prev, { account: '', label: '', side: 'debit', type: 'business', ratio: 1 }]) } function removeLine(index: number) { if (lines.length <= 2) return setLines((prev) => prev.filter((_, i) => i !== index)) } async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!name || lines.some((l) => !l.account || !l.label)) { toast({ title: 'Fyll i alla fält', variant: 'destructive' }) return } setIsSubmitting(true) try { const res = await fetch('/api/settings/booking-templates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, description, category, entity_type: entityType, lines }), }) if (!res.ok) { const json = await res.json() toast({ title: 'Fel', description: json.error || 'Kunde inte skapa mall', variant: 'destructive' }) return } toast({ title: 'Mall skapad' }) onCreated() } finally { setIsSubmitting(false) } } return (
setName(e.target.value)} placeholder="T.ex. Inköp EU-varor" />