Files
app/src/admin/pages/CompanySettings.tsx
BOHA ba95723b61 v1.5.6: boneyard-js skeleton migration, TanStack Query refactor, rate-limit config
- Replace hand-coded skeleton CSS/JSX with boneyard-js auto-generated bones
- Remove skeleton.css and @keyframes shimmer from base.css
- Add <Skeleton> wrappers with fixtures to all 25+ page components
- Generate 20 bone captures via boneyard CLI (CDP auth-gated capture)
- Refactor data fetching from useEffect+useState to TanStack Query
- Extract query hooks into src/admin/lib/queries/ and apiAdapter
- Add usePaginatedQuery hook replacing useApiCall/useListData
- Fix parseFloat || 0 anti-pattern in OfferDetail and OffersTemplates inputs
- Fix customer_id mandatory validation on offer creation
- Fix leave-requests comma-separated status filter (Prisma enum in: [])
- Add cross-entity cache invalidation for orders/offers/invoices/projects
- Make rate limits configurable via env vars (RATE_LIMIT_MAX, RATE_LIMIT_REFRESH, etc.)
- Add boneyard.config.json with routes and breakpoints

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 22:35:43 +02:00

1188 lines
41 KiB
TypeScript

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<string, string> = {
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<string | null>(null);
const [logoUrlDark, setLogoUrlDark] = useState<string | null>(null);
const logoUrlRef = useRef<string | null>(null);
const logoUrlDarkRef = useRef<string | null>(null);
const [form, setForm] = useState<CompanyForm>({
company_name: "",
street: "",
city: "",
postal_code: "",
country: "",
company_id: "",
vat_id: "",
});
const [customFields, setCustomFields] = useState<CustomField[]>([]);
const [fieldOrder, setFieldOrder] = useState<string[]>([
...DEFAULT_FIELD_ORDER,
]);
const [availableCurrencies, setAvailableCurrencies] = useState<string[]>([
"CZK",
"EUR",
"USD",
"GBP",
]);
const [bankSaving, setBankSaving] = useState(false);
const [editingBank, setEditingBank] = useState<number | null>(null);
const [bankDeleteConfirm, setBankDeleteConfirm] = useState<{
isOpen: boolean;
id: number | null;
}>({ isOpen: false, id: null });
const [bankForm, setBankForm] = useState<BankForm>({
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<string, unknown>;
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<HTMLInputElement>,
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 <Forbidden />;
if (settingsLoading) {
return (
<Skeleton
name="company-settings"
loading={settingsLoading}
fixture={<CompanySettingsFixture />}
>
<div />
</Skeleton>
);
}
const fullFieldOrder = getFullFieldOrder();
const renderBankButtonContent = (): React.ReactNode => {
if (bankSaving) {
return (
<>
<div className="admin-spinner admin-spinner-sm" />
Ukládání...
</>
);
}
if (editingBank !== null) return "Uložit změny";
return (
<>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
Přidat účet
</>
);
};
return (
<div>
{!embedded && (
<motion.div
className="admin-page-header"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
>
<div>
<h1 className="admin-page-title">Nastavení firmy</h1>
<p className="admin-page-subtitle">Firemní údaje a bankovní účty</p>
</div>
<button
onClick={handleSave}
className="admin-btn admin-btn-primary"
disabled={saving}
>
{saving ? (
<>
<div className="admin-spinner admin-spinner-sm" />
Ukládání...
</>
) : (
"Uložit nastavení"
)}
</button>
</motion.div>
)}
<div className="admin-settings-grid">
{/* Company Info */}
<motion.div
className="admin-card"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.06 }}
>
<div className="admin-card-header">
<h3 className="admin-card-title">Firemní údaje</h3>
</div>
<div className="admin-card-body">
<div className="admin-form">
<FormField label="Název firmy">
<input
type="text"
value={form.company_name}
onChange={(e) => updateField("company_name", e.target.value)}
className="admin-form-input"
/>
</FormField>
<div className="admin-form-row">
<FormField label="Ulice">
<input
type="text"
value={form.street}
onChange={(e) => updateField("street", e.target.value)}
className="admin-form-input"
/>
</FormField>
<FormField label="Město">
<input
type="text"
value={form.city}
onChange={(e) => updateField("city", e.target.value)}
className="admin-form-input"
/>
</FormField>
</div>
<div className="admin-form-row">
<FormField label="PSČ">
<input
type="text"
value={form.postal_code}
onChange={(e) => updateField("postal_code", e.target.value)}
className="admin-form-input"
/>
</FormField>
<FormField label="Země">
<input
type="text"
value={form.country}
onChange={(e) => updateField("country", e.target.value)}
className="admin-form-input"
/>
</FormField>
</div>
<div className="admin-form-row">
<FormField label="IČO">
<input
type="text"
value={form.company_id}
onChange={(e) => updateField("company_id", e.target.value)}
className="admin-form-input"
/>
</FormField>
<FormField label="DIČ">
<input
type="text"
value={form.vat_id}
onChange={(e) => updateField("vat_id", e.target.value)}
className="admin-form-input"
/>
</FormField>
</div>
<div style={{ marginTop: 4 }}>
<label
className="admin-form-label"
style={{ display: "block", marginBottom: 4 }}
>
Vlastní pole
</label>
{customFields.map((field, idx) => (
<div key={field._key} style={{ marginBottom: 8 }}>
<div
className="admin-form-row"
style={{ marginBottom: 0, alignItems: "flex-end" }}
>
<FormField
label={idx === 0 ? "Název" : "\u00A0"}
style={{ flex: 1 }}
>
<input
type="text"
value={field.name}
onChange={(e) => {
const updated = [...customFields];
updated[idx] = {
...updated[idx],
name: e.target.value,
};
setCustomFields(updated);
}}
className="admin-form-input"
placeholder="Např. Tel."
/>
</FormField>
<FormField
label={idx === 0 ? "Hodnota" : "\u00A0"}
style={{ flex: 1 }}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<input
type="text"
value={field.value}
onChange={(e) => {
const updated = [...customFields];
updated[idx] = {
...updated[idx],
value: e.target.value,
};
setCustomFields(updated);
}}
className="admin-form-input"
style={{ flex: 1 }}
/>
<button
type="button"
onClick={() => {
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),
);
}}
className="admin-btn-icon danger"
title="Odebrat pole"
aria-label="Odebrat pole"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</FormField>
</div>
<label
className="admin-form-checkbox"
style={{ marginTop: 4 }}
>
<input
type="checkbox"
checked={field.showLabel !== false}
onChange={(e) => {
const updated = [...customFields];
updated[idx] = {
...updated[idx],
showLabel: e.target.checked,
};
setCustomFields(updated);
}}
/>
<span style={{ fontSize: "0.8rem" }}>
Zobrazit název v PDF
</span>
</label>
</div>
))}
<button
type="button"
onClick={() =>
setCustomFields([
...customFields,
{
name: "",
value: "",
showLabel: true,
_key: `cf-${Date.now()}`,
},
])
}
className="admin-btn admin-btn-secondary"
style={{ marginTop: 4, fontSize: "0.85rem" }}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
Přidat pole
</button>
</div>
</div>
</div>
</motion.div>
{/* Bank Accounts */}
<motion.div
className="admin-card"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.08 }}
>
<div className="admin-card-header">
<h3 className="admin-card-title">Bankovní účty</h3>
</div>
<div className="admin-card-body">
{bankLoading ? (
<Skeleton
name="company-settings-bank"
loading={bankLoading}
fixture={<CompanySettingsFixture />}
>
<div />
</Skeleton>
) : (
<>
{bankAccountsList.length > 0 && (
<div className="admin-table-responsive mb-4">
<table className="admin-table">
<thead>
<tr>
<th>Název</th>
<th>Banka</th>
<th>Číslo účtu</th>
<th>IBAN</th>
<th>BIC/SWIFT</th>
<th>Měna</th>
<th style={{ width: 70 }}>Výchozí</th>
<th style={{ width: 80 }}></th>
</tr>
</thead>
<tbody>
{bankAccountsList.map((acc) => (
<tr
key={acc.id}
style={
editingBank === acc.id
? { background: "var(--bg-tertiary)" }
: undefined
}
>
<td>{acc.account_name}</td>
<td>{acc.bank_name}</td>
<td className="admin-mono">{acc.account_number}</td>
<td className="admin-mono">{acc.iban}</td>
<td className="admin-mono">{acc.bic}</td>
<td>{acc.currency}</td>
<td className="text-center">
{acc.is_default ? (
<span className="text-accent fw-600"></span>
) : (
"\u2013"
)}
</td>
<td>
<div style={{ display: "flex", gap: 4 }}>
<button
type="button"
onClick={() => startEditBank(acc)}
className="admin-btn-icon"
title="Upravit"
aria-label="Upravit"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</button>
<button
type="button"
onClick={() => handleBankDelete(acc.id)}
className="admin-btn-icon danger"
title="Smazat"
aria-label="Smazat"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<div
style={{
background: "var(--bg-tertiary)",
borderRadius: "var(--border-radius)",
padding: 16,
}}
>
<h4
className="text-secondary"
style={{ margin: "0 0 12px", fontSize: "0.9rem" }}
>
{editingBank !== null ? "Upravit účet" : "Přidat nový účet"}
</h4>
<div className="admin-form">
<div className="admin-form-row">
<FormField label="Název účtu" required>
<input
type="text"
value={bankForm.account_name}
onChange={(e) =>
setBankForm((f) => ({
...f,
account_name: e.target.value,
}))
}
className="admin-form-input"
placeholder="Např. Hlavní CZK účet"
/>
</FormField>
<FormField label="Název banky">
<input
type="text"
value={bankForm.bank_name}
onChange={(e) =>
setBankForm((f) => ({
...f,
bank_name: e.target.value,
}))
}
className="admin-form-input"
placeholder="Např. MONETA Money Bank, a.s."
/>
</FormField>
</div>
<div className="admin-form-row">
<FormField label="Číslo účtu">
<input
type="text"
value={bankForm.account_number}
onChange={(e) =>
setBankForm((f) => ({
...f,
account_number: e.target.value,
}))
}
className="admin-form-input"
placeholder="123456789/0600"
/>
</FormField>
<FormField label="Měna">
<select
value={bankForm.currency}
onChange={(e) =>
setBankForm((f) => ({
...f,
currency: e.target.value,
}))
}
className="admin-form-select"
>
{availableCurrencies.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</FormField>
</div>
<div className="admin-form-row">
<FormField label="IBAN">
<input
type="text"
value={bankForm.iban}
onChange={(e) =>
setBankForm((f) => ({ ...f, iban: e.target.value }))
}
className="admin-form-input"
placeholder="CZ65 0800 0000 1920 0014 5399"
/>
</FormField>
<FormField label="BIC / SWIFT">
<input
type="text"
value={bankForm.bic}
onChange={(e) =>
setBankForm((f) => ({ ...f, bic: e.target.value }))
}
className="admin-form-input"
placeholder="GIBACZPX"
/>
</FormField>
</div>
<label className="admin-form-checkbox">
<input
type="checkbox"
checked={bankForm.is_default}
onChange={(e) =>
setBankForm((f) => ({
...f,
is_default: e.target.checked,
}))
}
/>
<span>
Výchozí účet (použije se automaticky při vytváření
faktury)
</span>
</label>
<div style={{ display: "flex", gap: 8, marginTop: 8 }}>
<button
type="button"
onClick={handleBankSave}
className="admin-btn admin-btn-primary"
disabled={bankSaving}
style={{ fontSize: "0.85rem" }}
>
{renderBankButtonContent()}
</button>
{editingBank !== null && (
<button
type="button"
onClick={resetBankForm}
className="admin-btn admin-btn-secondary"
style={{ fontSize: "0.85rem" }}
>
Zrušit
</button>
)}
</div>
</div>
</div>
</>
)}
</div>
</motion.div>
{/* PDF Field Order */}
<motion.div
className="admin-card"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.08 }}
>
<div className="admin-card-header">
<h3 className="admin-card-title">Pořadí polí dodavatele v PDF</h3>
</div>
<div className="admin-card-body">
<small
className="admin-form-hint"
style={{ display: "block", marginBottom: 12 }}
>
Určuje pořadí řádků v adresním bloku dodavatele na PDF nabídce.
</small>
<div className="admin-reorder-list">
{fullFieldOrder.map((key, index) => (
<div key={key} className="admin-reorder-item">
<div className="admin-reorder-arrows">
<button
type="button"
onClick={() => moveField(index, -1)}
disabled={index === 0}
className="admin-btn-icon"
title="Nahoru"
aria-label="Nahoru"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M18 15l-6-6-6 6" />
</svg>
</button>
<button
type="button"
onClick={() => moveField(index, 1)}
disabled={index === fullFieldOrder.length - 1}
className="admin-btn-icon"
title="Dolů"
aria-label="Dolů"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M6 9l6 6 6-6" />
</svg>
</button>
</div>
<span
className={`admin-reorder-label${key.startsWith("custom_") ? " accent" : ""}`}
>
{getFieldDisplayName(key)}
</span>
</div>
))}
</div>
</div>
</motion.div>
{/* Logo */}
<motion.div
className="admin-card"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.12 }}
>
<div className="admin-card-header">
<h3 className="admin-card-title">Logo</h3>
</div>
<div className="admin-card-body">
<div className="admin-form-row">
<div className="admin-logo-section">
<label
className="admin-form-label"
style={{ display: "block", marginBottom: 4 }}
>
Logo (světlý režim)
</label>
{logoUrl && (
<div className="admin-logo-preview">
<img src={logoUrl} alt="Logo (světlý režim)" />
</div>
)}
<label
className="admin-btn admin-btn-secondary"
style={{ cursor: "pointer" }}
>
{uploadingLogo ? (
<>
<div className="admin-spinner admin-spinner-sm" />
Nahrávání...
</>
) : (
<>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
Nahrát logo
</>
)}
<input
type="file"
accept="image/*"
onChange={(e) => handleLogoUpload(e, "light")}
style={{ display: "none" }}
disabled={uploadingLogo}
/>
</label>
<small className="admin-form-hint">
PNG, JPEG, GIF nebo WebP, max 5 MB
</small>
</div>
<div className="admin-logo-section">
<label
className="admin-form-label"
style={{ display: "block", marginBottom: 4 }}
>
Logo (tmavý režim)
</label>
{logoUrlDark && (
<div className="admin-logo-preview">
<img src={logoUrlDark} alt="Logo (tmavý režim)" />
</div>
)}
<label
className="admin-btn admin-btn-secondary"
style={{ cursor: "pointer" }}
>
{uploadingLogoDark ? (
<>
<div className="admin-spinner admin-spinner-sm" />
Nahrávání...
</>
) : (
<>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
Nahrát logo
</>
)}
<input
type="file"
accept="image/*"
onChange={(e) => handleLogoUpload(e, "dark")}
style={{ display: "none" }}
disabled={uploadingLogoDark}
/>
</label>
<small className="admin-form-hint">
PNG, JPEG, GIF nebo WebP, max 5 MB
</small>
</div>
</div>
</div>
</motion.div>
</div>
{embedded && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.2 }}
>
<button
onClick={handleSave}
className="admin-btn admin-btn-primary"
style={{ width: "100%", marginTop: "1rem" }}
disabled={saving}
>
{saving ? (
<>
<div className="admin-spinner admin-spinner-sm" />
Ukládání...
</>
) : (
"Uložit nastavení firmy"
)}
</button>
</motion.div>
)}
<ConfirmModal
isOpen={bankDeleteConfirm.isOpen}
onClose={() => 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"
/>
</div>
);
}