'use client' import { useEffect, useRef, useState } from 'react' import { useTranslations } from 'next-intl' import { Loader2, Palette, Trash2, Upload } from 'lucide-react' import { Button } from '@/components/ui/button' import { EmptyState } from '@/components/ui/empty-state' import { useToast } from '@/components/ui/use-toast' import { SettingsGroup, SettingsInput, SettingsRow } from '@/components/settings/SettingsRows' import { SettingsLoadingSkeleton } from '@/components/settings/SettingsLoadingSkeleton' import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message' import { LOGO_UPLOAD_MAX_BYTES } from '@/lib/invoices/branding-constants' const ALLOWED_TYPES = ['image/png', 'image/jpeg', 'image/webp'] interface ByraBrand { hasBrand: boolean domain: string | null appName: string | null logoUrl: string | null canEdit: boolean } /** * Byrå settings: Varumärke (WL-17). Byråer edit the logo and the app name * themselves; domain and colors render read-only (ops-managed, the * white-glove decision). Upload interaction mirrors the company invoice * logo (components/settings/LogoUpload.tsx) against the byrå brand route. * The logo saves on upload; the app name has an inline save that appears * when the field is dirty (SettingsFormWrapper is wired to /api/settings, * so this single team-scoped field saves itself). */ export function BrandSettingsContent() { const t = useTranslations('settings_brand') const { toast } = useToast() const [brand, setBrand] = useState(null) const [loadFailed, setLoadFailed] = useState(false) const [isUploading, setIsUploading] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [isDragging, setIsDragging] = useState(false) const [appNameDraft, setAppNameDraft] = useState('') const [isSavingName, setIsSavingName] = useState(false) const inputRef = useRef(null) useEffect(() => { let cancelled = false fetch('/api/byra/brand') .then(async (res) => { if (!res.ok) throw new Error() const body = (await res.json()) as { data: ByraBrand } if (!cancelled) { setBrand(body.data) setAppNameDraft(body.data.appName ?? '') } }) .catch(() => { if (!cancelled) setLoadFailed(true) }) return () => { cancelled = true } }, []) const appNameDirty = !!brand?.hasBrand && brand.canEdit && appNameDraft.trim().length > 0 && appNameDraft.trim() !== brand.appName async function handleSaveAppName() { if (!appNameDirty || isSavingName) return setIsSavingName(true) try { const response = await fetch('/api/byra/brand', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ appName: appNameDraft.trim() }), }) const result = await response.json() if (!response.ok) { throw new Error(result.error || t('app_name_save_failed')) } setBrand((prev) => (prev ? { ...prev, appName: result.data.app_name } : prev)) setAppNameDraft(result.data.app_name) } catch (error) { toast({ title: t('app_name_save_failed'), description: error instanceof Error ? getUserErrorMessage(error) : undefined, variant: 'destructive', }) } setIsSavingName(false) } function validateAndUpload(file: File) { if (!ALLOWED_TYPES.includes(file.type)) { toast({ title: t('logo_disallowed_type'), variant: 'destructive' }) return } if (file.size > LOGO_UPLOAD_MAX_BYTES) { toast({ title: t('logo_too_large'), variant: 'destructive' }) return } void handleUpload(file) } async function handleUpload(file: File) { setIsUploading(true) const formData = new FormData() formData.append('file', file) try { const response = await fetch('/api/byra/brand/logo', { method: 'POST', body: formData }) const result = await response.json() if (!response.ok) { throw new Error(result.error || t('logo_upload_failed')) } setBrand((prev) => (prev ? { ...prev, logoUrl: result.data.logo_url } : prev)) } catch (error) { toast({ title: t('logo_upload_failed'), description: error instanceof Error ? getUserErrorMessage(error) : undefined, variant: 'destructive', }) } setIsUploading(false) } async function handleDelete() { setIsDeleting(true) try { const response = await fetch('/api/byra/brand/logo', { method: 'DELETE' }) if (!response.ok) throw new Error() setBrand((prev) => (prev ? { ...prev, logoUrl: null } : prev)) if (inputRef.current) inputRef.current.value = '' } catch { toast({ title: t('logo_delete_failed'), variant: 'destructive' }) } setIsDeleting(false) } function handleDrop(e: React.DragEvent) { e.preventDefault() setIsDragging(false) const file = e.dataTransfer.files?.[0] if (file) validateAndUpload(file) } function handleDragOver(e: React.DragEvent) { e.preventDefault() setIsDragging(true) } function handleDragLeave(e: React.DragEvent) { e.preventDefault() setIsDragging(false) } if (loadFailed) { return

{t('load_failed')}

} if (!brand) { return } if (!brand.hasBrand) { return ( ) } return ( {brand.logoUrl ? ( <> {/* eslint-disable-next-line @next/next/no-img-element */} {t('logo_alt')} {brand.canEdit && ( <> )} ) : brand.canEdit ? ( ) : ( {t('logo_none')} )} {brand.domain} {brand.canEdit ? ( <> setAppNameDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault() void handleSaveAppName() } }} /> {appNameDirty && ( )} ) : ( {brand.appName} )} { const file = e.target.files?.[0] if (file) validateAndUpload(file) }} /> ) }