import { useState, useEffect, useCallback, useRef } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import FormField from "../components/FormField"; import ConfirmModal from "../components/ConfirmModal"; import { companySettingsOptions } from "../lib/queries/settings"; import { bankAccountsOptions } from "../lib/queries/common"; import { motion } from "framer-motion"; import apiFetch from "../utils/api"; import { Skeleton } from "boneyard-js/react"; import CompanySettingsFixture from "../fixtures/CompanySettingsFixture"; const API_BASE = "/api/admin"; const DEFAULT_FIELD_ORDER = [ "street", "city_postal", "country", "company_id", "vat_id", ]; const FIELD_LABELS: Record = { street: "Ulice", city_postal: "Město + PSČ", country: "Země", company_id: "IČO", vat_id: "DIČ", }; interface CustomField { name: string; value: string; showLabel: boolean; _key: string; } interface CompanyForm { company_name: string; street: string; city: string; postal_code: string; country: string; company_id: string; vat_id: string; } interface BankAccount { id: number; account_name: string; bank_name: string; account_number: string; iban: string; bic: string; currency: string; is_default: boolean; } interface BankForm { account_name: string; bank_name: string; account_number: string; iban: string; bic: string; currency: string; is_default: boolean; } export default function CompanySettings({ embedded, }: { embedded?: boolean } = {}) { const alert = useAlert(); const { hasPermission } = useAuth(); const queryClient = useQueryClient(); const [saving, setSaving] = useState(false); const [uploadingLogo, setUploadingLogo] = useState(false); const [uploadingLogoDark, setUploadingLogoDark] = useState(false); const [logoUrl, setLogoUrl] = useState(null); const [logoUrlDark, setLogoUrlDark] = useState(null); const logoUrlRef = useRef(null); const logoUrlDarkRef = useRef(null); const [form, setForm] = useState({ company_name: "", street: "", city: "", postal_code: "", country: "", company_id: "", vat_id: "", }); const [customFields, setCustomFields] = useState([]); const [fieldOrder, setFieldOrder] = useState([ ...DEFAULT_FIELD_ORDER, ]); const [availableCurrencies, setAvailableCurrencies] = useState([ "CZK", "EUR", "USD", "GBP", ]); const [bankSaving, setBankSaving] = useState(false); const [editingBank, setEditingBank] = useState(null); const [bankDeleteConfirm, setBankDeleteConfirm] = useState<{ isOpen: boolean; id: number | null; }>({ isOpen: false, id: null }); const [bankForm, setBankForm] = useState({ account_name: "", bank_name: "", account_number: "", iban: "", bic: "", currency: "CZK", is_default: false, }); const getFullFieldOrder = useCallback((): string[] => { const allBuiltIn = [...DEFAULT_FIELD_ORDER]; const order = [...fieldOrder].filter((k) => k !== "company_name"); for (const f of allBuiltIn) { if (!order.includes(f)) order.push(f); } for (let i = 0; i < customFields.length; i++) { const key = `custom_${i}`; if (!order.includes(key)) order.push(key); } return order.filter((key) => { if (key.startsWith("custom_")) { const idx = parseInt(key.split("_")[1]); return idx < customFields.length; } return true; }); }, [fieldOrder, customFields]); const moveField = (index: number, direction: number) => { const order = getFullFieldOrder(); const newIndex = index + direction; if (newIndex < 0 || newIndex >= order.length) return; const updated = [...order]; [updated[index], updated[newIndex]] = [updated[newIndex], updated[index]]; setFieldOrder(updated); }; const getFieldDisplayName = (key: string): string => { if (FIELD_LABELS[key]) return FIELD_LABELS[key]; if (key.startsWith("custom_")) { const idx = parseInt(key.split("_")[1]); const cf = customFields[idx]; if (cf) return cf.name ? `${cf.name}: ${cf.value || "..."}` : cf.value || `Vlastní pole ${idx + 1}`; } return key; }; const fetchLogo = useCallback(async (variant: "light" | "dark" = "light") => { try { const resp = await apiFetch( `${API_BASE}/company-settings/logo?variant=${variant}`, ); if (resp.ok) { const blob = await resp.blob(); if (variant === "dark") { setLogoUrlDark((prev) => { if (prev) URL.revokeObjectURL(prev); const url = URL.createObjectURL(blob); logoUrlDarkRef.current = url; return url; }); } else { setLogoUrl((prev) => { if (prev) URL.revokeObjectURL(prev); const url = URL.createObjectURL(blob); logoUrlRef.current = url; return url; }); } } } catch { // ignore - no logo } }, []); // ── TanStack Query: company settings ── const { data: settingsData, isPending: settingsLoading } = useQuery( companySettingsOptions(), ); // ── TanStack Query: bank accounts ── const { data: bankAccountsData, isPending: bankLoading } = useQuery( bankAccountsOptions(), ); const bankAccountsList: BankAccount[] = Array.isArray(bankAccountsData) ? (bankAccountsData as unknown as BankAccount[]) : []; // Populate form state when settings data arrives useEffect(() => { if (!settingsData) return; const d = settingsData as Record; setForm({ company_name: (d.company_name as string) || "", street: (d.street as string) || "", city: (d.city as string) || "", postal_code: (d.postal_code as string) || "", country: (d.country as string) || "", company_id: (d.company_id as string) || "", vat_id: (d.vat_id as string) || "", }); const cf: CustomField[] = Array.isArray(d.custom_fields) && d.custom_fields.length > 0 ? (d.custom_fields as CustomField[]).map((f, i) => ({ ...f, showLabel: f.showLabel !== false, _key: f._key || `cf-${Date.now()}-${i}`, })) : []; setCustomFields(cf); if ( Array.isArray(d.supplier_field_order) && d.supplier_field_order.length > 0 ) { setFieldOrder(d.supplier_field_order as string[]); } else { setFieldOrder([...DEFAULT_FIELD_ORDER]); } if ( Array.isArray(d.available_currencies) && d.available_currencies.length > 0 ) { setAvailableCurrencies(d.available_currencies as string[]); } if (d.has_logo) { fetchLogo("light"); } if (d.has_logo_dark) { fetchLogo("dark"); } }, [settingsData, fetchLogo]); const resetBankForm = () => { setEditingBank(null); setBankForm({ account_name: "", bank_name: "", account_number: "", iban: "", bic: "", currency: "CZK", is_default: false, }); }; const handleBankSave = async () => { if (!bankForm.account_name.trim()) { alert.error("Název účtu je povinný"); return; } setBankSaving(true); try { const isEdit = editingBank !== null; const url = isEdit ? `${API_BASE}/bank-accounts/${editingBank}` : `${API_BASE}/bank-accounts`; const response = await apiFetch(url, { method: isEdit ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(bankForm), }); const result = await response.json(); if (result.success) { alert.success(result.message); resetBankForm(); queryClient.invalidateQueries({ queryKey: ["bank-accounts"] }); } else { alert.error(result.error || "Chyba při ukládání"); } } catch { alert.error("Chyba připojení"); } finally { setBankSaving(false); } }; const handleBankDelete = (id: number) => { setBankDeleteConfirm({ isOpen: true, id }); }; const confirmBankDelete = async () => { if (bankDeleteConfirm.id == null) return; try { const response = await apiFetch( `${API_BASE}/bank-accounts/${bankDeleteConfirm.id}`, { method: "DELETE", }, ); const result = await response.json(); if (result.success) { alert.success(result.message); if (editingBank === bankDeleteConfirm.id) resetBankForm(); queryClient.invalidateQueries({ queryKey: ["bank-accounts"] }); } else { alert.error(result.error || "Chyba při mazání"); } } catch { alert.error("Chyba připojení"); } finally { setBankDeleteConfirm({ isOpen: false, id: null }); } }; const startEditBank = (account: BankAccount) => { setEditingBank(account.id); setBankForm({ account_name: account.account_name || "", bank_name: account.bank_name || "", account_number: account.account_number || "", iban: account.iban || "", bic: account.bic || "", currency: account.currency || "CZK", is_default: !!account.is_default, }); }; // Cleanup blob URLs on unmount useEffect(() => { return () => { if (logoUrlRef.current) URL.revokeObjectURL(logoUrlRef.current); if (logoUrlDarkRef.current) URL.revokeObjectURL(logoUrlDarkRef.current); }; }, []); const handleSave = async () => { setSaving(true); try { const payload = { ...form, custom_fields: customFields.filter( (f) => f.name.trim() || f.value.trim(), ), supplier_field_order: getFullFieldOrder(), }; const response = await apiFetch(`${API_BASE}/company-settings`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const result = await response.json(); if (result.success) { alert.success(result.message || "Nastavení bylo uloženo"); queryClient.invalidateQueries({ queryKey: ["company-settings"] }); } else { alert.error(result.error || "Nepodařilo se uložit nastavení"); } } catch { alert.error("Chyba připojení"); } finally { setSaving(false); } }; const handleLogoUpload = async ( e: React.ChangeEvent, variant: "light" | "dark" = "light", ) => { const file = e.target.files?.[0]; if (!file) return; const setUploading = variant === "dark" ? setUploadingLogoDark : setUploadingLogo; setUploading(true); try { const formData = new FormData(); formData.append("logo", file); const response = await apiFetch( `${API_BASE}/company-settings/logo?variant=${variant}`, { method: "POST", body: formData, }, ); const result = await response.json(); if (result.success) { alert.success(result.message || "Logo bylo nahráno"); queryClient.invalidateQueries({ queryKey: ["company-settings"] }); fetchLogo(variant); } else { alert.error(result.error || "Nepodařilo se nahrát logo"); } } catch { alert.error("Chyba připojení"); } finally { setUploading(false); e.target.value = ""; } }; const updateField = (field: keyof CompanyForm, value: string | number) => { setForm((prev) => ({ ...prev, [field]: value })); }; if (!embedded && !hasPermission("settings.manage")) return ; if (settingsLoading) { return ( } >
); } const fullFieldOrder = getFullFieldOrder(); const renderBankButtonContent = (): React.ReactNode => { if (bankSaving) { return ( <>
Ukládání... ); } if (editingBank !== null) return "Uložit změny"; return ( <> Přidat účet ); }; return (
{!embedded && (

Nastavení firmy

Firemní údaje a bankovní účty

)}
{/* Company Info */}

Firemní údaje

updateField("company_name", e.target.value)} className="admin-form-input" />
updateField("street", e.target.value)} className="admin-form-input" /> updateField("city", e.target.value)} className="admin-form-input" />
updateField("postal_code", e.target.value)} className="admin-form-input" /> updateField("country", e.target.value)} className="admin-form-input" />
updateField("company_id", e.target.value)} className="admin-form-input" /> updateField("vat_id", e.target.value)} className="admin-form-input" />
{customFields.map((field, idx) => (
{ const updated = [...customFields]; updated[idx] = { ...updated[idx], name: e.target.value, }; setCustomFields(updated); }} className="admin-form-input" placeholder="Např. Tel." />
{ const updated = [...customFields]; updated[idx] = { ...updated[idx], value: e.target.value, }; setCustomFields(updated); }} className="admin-form-input" style={{ flex: 1 }} />
))}
{/* Bank Accounts */}

Bankovní účty

{bankLoading ? ( } >
) : ( <> {bankAccountsList.length > 0 && (
{bankAccountsList.map((acc) => ( ))}
Název Banka Číslo účtu IBAN BIC/SWIFT Měna Výchozí
{acc.account_name} {acc.bank_name} {acc.account_number} {acc.iban} {acc.bic} {acc.currency} {acc.is_default ? ( ) : ( "\u2013" )}
)}

{editingBank !== null ? "Upravit účet" : "Přidat nový účet"}

setBankForm((f) => ({ ...f, account_name: e.target.value, })) } className="admin-form-input" placeholder="Např. Hlavní CZK účet" /> setBankForm((f) => ({ ...f, bank_name: e.target.value, })) } className="admin-form-input" placeholder="Např. MONETA Money Bank, a.s." />
setBankForm((f) => ({ ...f, account_number: e.target.value, })) } className="admin-form-input" placeholder="123456789/0600" />
setBankForm((f) => ({ ...f, iban: e.target.value })) } className="admin-form-input" placeholder="CZ65 0800 0000 1920 0014 5399" /> setBankForm((f) => ({ ...f, bic: e.target.value })) } className="admin-form-input" placeholder="GIBACZPX" />
{editingBank !== null && ( )}
)}
{/* PDF Field Order */}

Pořadí polí dodavatele v PDF

Určuje pořadí řádků v adresním bloku dodavatele na PDF nabídce.
{fullFieldOrder.map((key, index) => (
{getFieldDisplayName(key)}
))}
{/* Logo */}

Logo

{logoUrl && (
Logo (světlý režim)
)}
{embedded && ( )} setBankDeleteConfirm({ isOpen: false, id: null })} onConfirm={confirmBankDelete} title="Smazat bankovní účet" message="Opravdu chcete smazat tento bankovní účet?" confirmText="Smazat" cancelText="Zrušit" type="danger" />
); }