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 { companySettingsOptions, type CompanySettingsData, } from "../lib/queries/settings"; import { bankAccountsOptions, type BankAccount } from "../lib/queries/common"; import { motion } from "framer-motion"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; import apiFetch from "../utils/api"; import { useApiMutation } from "../lib/queries/mutations"; import { Button, Card, DataTable, ConfirmDialog, Field, TextField, Select, CheckboxField, StatusChip, FileUpload, LoadingState, type DataColumn, } from "../ui"; 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Č", }; const PlusIcon = ( ); const EditIcon = ( ); const RemoveIcon = ( ); const UpIcon = ( ); const DownIcon = ( ); 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; offer_number_pattern: string; order_number_pattern: string; invoice_number_pattern: string; issued_order_number_pattern: string; issued_order_type_code: string; project_number_pattern: string; project_type_code: string; quotation_prefix: string; order_type_code: string; invoice_type_code: string; warehouse_receipt_prefix: string; warehouse_receipt_number_pattern: string; warehouse_issue_prefix: string; warehouse_issue_number_pattern: string; warehouse_inventory_prefix: string; warehouse_inventory_number_pattern: string; default_currency: string; default_vat_rate: number; available_currencies: string[]; available_vat_rates: number[]; } 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 canEditCompany = hasPermission("settings.company"); const canManageBanking = hasPermission("settings.banking"); 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: "", offer_number_pattern: "", order_number_pattern: "", invoice_number_pattern: "", issued_order_number_pattern: "", issued_order_type_code: "", project_number_pattern: "", project_type_code: "", quotation_prefix: "", order_type_code: "", invoice_type_code: "", warehouse_receipt_prefix: "", warehouse_receipt_number_pattern: "", warehouse_issue_prefix: "", warehouse_issue_number_pattern: "", warehouse_inventory_prefix: "", warehouse_inventory_number_pattern: "", default_currency: "CZK", default_vat_rate: 21, available_currencies: ["CZK", "EUR", "USD", "GBP"], available_vat_rates: [0, 10, 12, 15, 21], }); const [customFields, setCustomFields] = useState([]); const [fieldOrder, setFieldOrder] = useState([ ...DEFAULT_FIELD_ORDER, ]); 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 saveSettingsMutation = useApiMutation< Record, { message?: string } >({ url: () => `${API_BASE}/company-settings`, method: () => "PUT", invalidate: [ "settings", "company-settings", "attendance", "offers", "orders", "invoices", ], }); const bankMutation = useApiMutation< BankForm & { id?: number }, { message?: string } >({ url: (input) => input.id != null ? `${API_BASE}/bank-accounts/${input.id}` : `${API_BASE}/bank-accounts`, method: (input) => (input.id != null ? "PUT" : "POST"), invalidate: ["settings", "bank-accounts", "invoices"], }); const bankDeleteMutation = useApiMutation({ url: (id) => `${API_BASE}/bank-accounts/${id}`, method: () => "DELETE", invalidate: ["settings", "bank-accounts", "invoices"], }); 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 : []; // Populate form state when settings data arrives useEffect(() => { if (!settingsData) return; setForm({ company_name: settingsData.company_name || "", street: settingsData.street || "", city: settingsData.city || "", postal_code: settingsData.postal_code || "", country: settingsData.country || "", company_id: settingsData.company_id || "", vat_id: settingsData.vat_id || "", offer_number_pattern: settingsData.offer_number_pattern || "", order_number_pattern: settingsData.order_number_pattern || "", invoice_number_pattern: settingsData.invoice_number_pattern || "", issued_order_number_pattern: settingsData.issued_order_number_pattern || "", issued_order_type_code: settingsData.issued_order_type_code || "", project_number_pattern: settingsData.project_number_pattern || "", project_type_code: settingsData.project_type_code || "", quotation_prefix: settingsData.quotation_prefix || "", order_type_code: settingsData.order_type_code || "", invoice_type_code: settingsData.invoice_type_code || "", warehouse_receipt_prefix: settingsData.warehouse_receipt_prefix || "", warehouse_receipt_number_pattern: settingsData.warehouse_receipt_number_pattern || "", warehouse_issue_prefix: settingsData.warehouse_issue_prefix || "", warehouse_issue_number_pattern: settingsData.warehouse_issue_number_pattern || "", warehouse_inventory_prefix: settingsData.warehouse_inventory_prefix || "", warehouse_inventory_number_pattern: settingsData.warehouse_inventory_number_pattern || "", default_currency: settingsData.default_currency || "CZK", default_vat_rate: settingsData.default_vat_rate ?? 21, available_currencies: Array.isArray(settingsData.available_currencies) && settingsData.available_currencies.length > 0 ? settingsData.available_currencies : ["CZK", "EUR", "USD", "GBP"], available_vat_rates: Array.isArray(settingsData.available_vat_rates) && settingsData.available_vat_rates.length > 0 ? settingsData.available_vat_rates : [0, 10, 12, 15, 21], }); const cf: CustomField[] = Array.isArray(settingsData.custom_fields) && settingsData.custom_fields.length > 0 ? settingsData.custom_fields.map((f, i) => ({ ...f, showLabel: f.showLabel !== false, _key: (f as CustomField)._key || `cf-${Date.now()}-${i}`, })) : []; setCustomFields(cf); if ( Array.isArray(settingsData.supplier_field_order) && settingsData.supplier_field_order.length > 0 ) { setFieldOrder(settingsData.supplier_field_order); } else { setFieldOrder([...DEFAULT_FIELD_ORDER]); } if (settingsData.has_logo) { fetchLogo("light"); } if (settingsData.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 result = await bankMutation.mutateAsync({ ...bankForm, id: editingBank ?? undefined, }); alert.success(result?.message || "Uloženo"); resetBankForm(); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { setBankSaving(false); } }; const handleBankDelete = (id: number) => { setBankDeleteConfirm({ isOpen: true, id }); }; const confirmBankDelete = async () => { if (bankDeleteConfirm.id == null) return; try { const result = await bankDeleteMutation.mutateAsync(bankDeleteConfirm.id); alert.success(result?.message || "Smazáno"); if (editingBank === bankDeleteConfirm.id) resetBankForm(); } catch (e) { alert.error(e instanceof Error ? e.message : "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 result = await saveSettingsMutation.mutateAsync(payload); alert.success(result?.message || "Nastavení bylo uloženo"); } catch (e) { alert.error( e instanceof Error ? e.message : "Nepodařilo se uložit nastavení", ); } finally { setSaving(false); } }; const handleLogoUpload = async ( file: File, variant: "light" | "dark" = "light", ) => { 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"] }); queryClient.invalidateQueries({ queryKey: ["offers"] }); queryClient.invalidateQueries({ queryKey: ["orders"] }); queryClient.invalidateQueries({ queryKey: ["invoices"] }); fetchLogo(variant); } else { alert.error(result.error || "Nepodařilo se nahrát logo"); } } catch { alert.error("Chyba připojení"); } finally { setUploading(false); } }; const updateField = (field: keyof CompanyForm, value: string | number) => { setForm((prev) => ({ ...prev, [field]: value })); }; if (!embedded && !canEditCompany) return ; if (settingsLoading) { return ; } const fullFieldOrder = getFullFieldOrder(); const bankColumns: DataColumn[] = [ { key: "account_name", header: "Název", render: (acc) => acc.account_name, }, { key: "bank_name", header: "Banka", render: (acc) => acc.bank_name, }, { key: "account_number", header: "Číslo účtu", mono: true, render: (acc) => acc.account_number, }, { key: "iban", header: "IBAN", mono: true, render: (acc) => acc.iban, }, { key: "bic", header: "BIC/SWIFT", mono: true, render: (acc) => acc.bic, }, { key: "currency", header: "Měna", render: (acc) => acc.currency, }, { key: "is_default", header: "Výchozí", width: 70, align: "right", render: (acc) => acc.is_default ? ( ) : ( "–" ), }, { key: "actions", header: "", width: 80, align: "right", render: (acc) => ( startEditBank(acc)} title="Upravit" aria-label="Upravit" > {EditIcon} handleBankDelete(acc.id)} title="Smazat" aria-label="Smazat" > {RemoveIcon} ), }, ]; return ( {!embedded && ( Nastavení firmy Firemní údaje a bankovní účty )} {/* Company Info */} {(canEditCompany || !embedded) && ( Firemní údaje updateField("company_name", e.target.value)} /> updateField("street", e.target.value)} /> updateField("city", e.target.value)} /> updateField("postal_code", e.target.value)} /> updateField("country", e.target.value)} /> updateField("company_id", e.target.value)} /> updateField("vat_id", e.target.value)} /> Vlastní pole {customFields.map((field, idx) => ( { const updated = [...customFields]; updated[idx] = { ...updated[idx], name: e.target.value, }; setCustomFields(updated); }} placeholder="Např. Tel." /> { const updated = [...customFields]; updated[idx] = { ...updated[idx], value: e.target.value, }; setCustomFields(updated); }} sx={{ flex: 1 }} /> { const key = `custom_${idx}`; setFieldOrder((prev) => prev .filter((k) => k !== key) .map((k) => { if (k.startsWith("custom_")) { const ki = parseInt(k.split("_")[1]); if (ki > idx) return `custom_${ki - 1}`; } return k; }), ); setCustomFields( customFields.filter((_, i) => i !== idx), ); }} title="Odebrat pole" aria-label="Odebrat pole" > {RemoveIcon} Zobrazit název v PDF } checked={field.showLabel !== false} onChange={(checked) => { const updated = [...customFields]; updated[idx] = { ...updated[idx], showLabel: checked, }; setCustomFields(updated); }} /> ))} )} {/* Bank Accounts */} {(canManageBanking || !embedded) && ( Bankovní účty {bankLoading ? ( ) : ( <> {bankAccountsList.length > 0 && ( columns={bankColumns} rows={bankAccountsList} rowKey={(acc) => acc.id} rowSx={(acc) => editingBank === acc.id ? { bgcolor: "action.selected" } : {} } /> )} {editingBank !== null ? "Upravit účet" : "Přidat nový účet"} setBankForm((f) => ({ ...f, account_name: e.target.value, })) } placeholder="Např. Hlavní CZK účet" /> setBankForm((f) => ({ ...f, bank_name: e.target.value, })) } placeholder="Např. MONETA Money Bank, a.s." /> setBankForm((f) => ({ ...f, account_number: e.target.value, })) } placeholder="123456789/0600" /> updateField("default_currency", v)} options={form.available_currencies.map((c) => ({ value: c, label: c, }))} /> updateField("default_vat_rate", Number(e.target.value)) } slotProps={{ htmlInput: { min: 0, step: 1 } }} /> setForm((prev) => ({ ...prev, available_currencies: e.target.value .split(",") .map((s) => s.trim()) .filter(Boolean), })) } placeholder="CZK, EUR, USD" /> setForm((prev) => ({ ...prev, available_vat_rates: e.target.value .split(",") .map((s) => Number(s.trim())) .filter((n) => !isNaN(n)), })) } placeholder="0, 12, 21" /> )} {/* Logo */} {(canEditCompany || !embedded) && ( Logo Logo (světlý režim) {logoUrl && ( )} { if (files[0]) handleLogoUpload(files[0], "light"); }} /> PNG, JPEG, GIF nebo WebP, max 5 MB Logo (tmavý režim) {logoUrlDark && ( )} { if (files[0]) handleLogoUpload(files[0], "dark"); }} /> PNG, JPEG, GIF nebo WebP, max 5 MB )} {embedded && canEditCompany && ( )} setBankDeleteConfirm({ isOpen: false, id: null })} onConfirm={confirmBankDelete} title="Smazat bankovní účet" message="Opravdu chcete smazat tento bankovní účet?" confirmText="Smazat" cancelText="Zrušit" confirmVariant="danger" /> ); }