unused-vars (25): leftover imports/types across pages, routes and services; write-only sickHours accumulator; unused holidayCount pair; test helpers authPatch/authDelete; TOut dropped from ApiMutationOptions (the hook keeps its own generic — no call-site changes); requireAuth no longer importable in customers/warehouse routes (matches the no-bare-auth-reads convention). useless-assignment (5): initializers proven overwritten on every path (systemQty x2, hours, writtenSize -> let x: number) and the seed's dead adminUser reassignment (create kept, assignment dropped). useless-escape (4): [eE+\-] -> [eE+-] and [\/...] -> [/...] — identical semantics. Behavior-neutral; lint 102 -> 68 warnings (0 errors), suite 641 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1476 lines
47 KiB
TypeScript
1476 lines
47 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 { companySettingsOptions } from "../lib/queries/settings";
|
||
import { bankAccountsOptions, type BankAccount } from "../lib/queries/common";
|
||
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,
|
||
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<string, string> = {
|
||
street: "Ulice",
|
||
city_postal: "Město + PSČ",
|
||
country: "Země",
|
||
company_id: "IČO",
|
||
vat_id: "DIČ",
|
||
};
|
||
|
||
const PlusIcon = (
|
||
<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>
|
||
);
|
||
const EditIcon = (
|
||
<svg
|
||
width="14"
|
||
height="14"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="2"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
>
|
||
<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>
|
||
);
|
||
const RemoveIcon = (
|
||
<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>
|
||
);
|
||
const UpIcon = (
|
||
<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>
|
||
);
|
||
const DownIcon = (
|
||
<svg
|
||
width="12"
|
||
height="12"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="2"
|
||
>
|
||
<path d="M6 9l6 6 6-6" />
|
||
</svg>
|
||
);
|
||
|
||
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<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: "",
|
||
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<CustomField[]>([]);
|
||
const [fieldOrder, setFieldOrder] = useState<string[]>([
|
||
...DEFAULT_FIELD_ORDER,
|
||
]);
|
||
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 saveSettingsMutation = useApiMutation<
|
||
Record<string, unknown>,
|
||
{ 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<number, { message?: string }>({
|
||
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 <Forbidden />;
|
||
|
||
if (settingsLoading) {
|
||
return <LoadingState />;
|
||
}
|
||
|
||
const fullFieldOrder = getFullFieldOrder();
|
||
|
||
const bankColumns: DataColumn<BankAccount>[] = [
|
||
{
|
||
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 ? (
|
||
<Box component="span" sx={{ color: "primary.main", fontWeight: 600 }}>
|
||
✓
|
||
</Box>
|
||
) : (
|
||
"–"
|
||
),
|
||
},
|
||
{
|
||
key: "actions",
|
||
header: "",
|
||
width: 80,
|
||
align: "right",
|
||
render: (acc) => (
|
||
<Box sx={{ display: "flex", gap: 0.5, justifyContent: "flex-end" }}>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => startEditBank(acc)}
|
||
title="Upravit"
|
||
aria-label="Upravit"
|
||
>
|
||
{EditIcon}
|
||
</IconButton>
|
||
<IconButton
|
||
size="small"
|
||
color="error"
|
||
onClick={() => handleBankDelete(acc.id)}
|
||
title="Smazat"
|
||
aria-label="Smazat"
|
||
>
|
||
{RemoveIcon}
|
||
</IconButton>
|
||
</Box>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<Box>
|
||
{!embedded && (
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
alignItems: "flex-start",
|
||
justifyContent: "space-between",
|
||
flexWrap: "wrap",
|
||
gap: 2,
|
||
mb: 3,
|
||
}}
|
||
>
|
||
<Box>
|
||
<Typography variant="h4">Nastavení firmy</Typography>
|
||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||
Firemní údaje a bankovní účty
|
||
</Typography>
|
||
</Box>
|
||
<Button onClick={handleSave} disabled={saving}>
|
||
{saving ? "Ukládání..." : "Uložit nastavení"}
|
||
</Button>
|
||
</Box>
|
||
)}
|
||
|
||
<Box
|
||
sx={{
|
||
display: "grid",
|
||
gridTemplateColumns: { xs: "1fr", lg: "1fr 1fr" },
|
||
gap: 3,
|
||
}}
|
||
>
|
||
{/* Company Info */}
|
||
{(canEditCompany || !embedded) && (
|
||
<Card>
|
||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||
Firemní údaje
|
||
</Typography>
|
||
<Field label="Název firmy">
|
||
<TextField
|
||
value={form.company_name}
|
||
onChange={(e) => updateField("company_name", e.target.value)}
|
||
/>
|
||
</Field>
|
||
<Box
|
||
sx={{
|
||
display: "grid",
|
||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Field label="Ulice">
|
||
<TextField
|
||
value={form.street}
|
||
onChange={(e) => updateField("street", e.target.value)}
|
||
/>
|
||
</Field>
|
||
<Field label="Město">
|
||
<TextField
|
||
value={form.city}
|
||
onChange={(e) => updateField("city", e.target.value)}
|
||
/>
|
||
</Field>
|
||
</Box>
|
||
<Box
|
||
sx={{
|
||
display: "grid",
|
||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Field label="PSČ">
|
||
<TextField
|
||
value={form.postal_code}
|
||
onChange={(e) => updateField("postal_code", e.target.value)}
|
||
/>
|
||
</Field>
|
||
<Field label="Země">
|
||
<TextField
|
||
value={form.country}
|
||
onChange={(e) => updateField("country", e.target.value)}
|
||
/>
|
||
</Field>
|
||
</Box>
|
||
<Box
|
||
sx={{
|
||
display: "grid",
|
||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Field label="IČO">
|
||
<TextField
|
||
value={form.company_id}
|
||
onChange={(e) => updateField("company_id", e.target.value)}
|
||
/>
|
||
</Field>
|
||
<Field label="DIČ">
|
||
<TextField
|
||
value={form.vat_id}
|
||
onChange={(e) => updateField("vat_id", e.target.value)}
|
||
/>
|
||
</Field>
|
||
</Box>
|
||
<Box>
|
||
<Typography
|
||
variant="body2"
|
||
sx={{
|
||
display: "block",
|
||
mb: 0.5,
|
||
fontWeight: 600,
|
||
color: "text.secondary",
|
||
}}
|
||
>
|
||
Vlastní pole
|
||
</Typography>
|
||
{customFields.map((field, idx) => (
|
||
<Box key={field._key} sx={{ mb: 1 }}>
|
||
<Box
|
||
sx={{
|
||
display: "grid",
|
||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||
gap: 2,
|
||
alignItems: "flex-start",
|
||
}}
|
||
>
|
||
<TextField
|
||
label={idx === 0 ? "Název" : undefined}
|
||
value={field.name}
|
||
onChange={(e) => {
|
||
const updated = [...customFields];
|
||
updated[idx] = {
|
||
...updated[idx],
|
||
name: e.target.value,
|
||
};
|
||
setCustomFields(updated);
|
||
}}
|
||
placeholder="Např. Tel."
|
||
/>
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
gap: 0.5,
|
||
alignItems: idx === 0 ? "flex-end" : "center",
|
||
}}
|
||
>
|
||
<TextField
|
||
label={idx === 0 ? "Hodnota" : undefined}
|
||
value={field.value}
|
||
onChange={(e) => {
|
||
const updated = [...customFields];
|
||
updated[idx] = {
|
||
...updated[idx],
|
||
value: e.target.value,
|
||
};
|
||
setCustomFields(updated);
|
||
}}
|
||
sx={{ flex: 1 }}
|
||
/>
|
||
<IconButton
|
||
size="small"
|
||
color="error"
|
||
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),
|
||
);
|
||
}}
|
||
title="Odebrat pole"
|
||
aria-label="Odebrat pole"
|
||
>
|
||
{RemoveIcon}
|
||
</IconButton>
|
||
</Box>
|
||
</Box>
|
||
<Box sx={{ mt: 0.5 }}>
|
||
<CheckboxField
|
||
label={
|
||
<Typography variant="body2" sx={{ fontSize: "0.8rem" }}>
|
||
Zobrazit název v PDF
|
||
</Typography>
|
||
}
|
||
checked={field.showLabel !== false}
|
||
onChange={(checked) => {
|
||
const updated = [...customFields];
|
||
updated[idx] = {
|
||
...updated[idx],
|
||
showLabel: checked,
|
||
};
|
||
setCustomFields(updated);
|
||
}}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
))}
|
||
<Button
|
||
variant="outlined"
|
||
color="inherit"
|
||
size="small"
|
||
startIcon={PlusIcon}
|
||
onClick={() =>
|
||
setCustomFields([
|
||
...customFields,
|
||
{
|
||
name: "",
|
||
value: "",
|
||
showLabel: true,
|
||
_key: `cf-${Date.now()}`,
|
||
},
|
||
])
|
||
}
|
||
sx={{ mt: 0.5 }}
|
||
>
|
||
Přidat pole
|
||
</Button>
|
||
</Box>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Bank Accounts */}
|
||
{(canManageBanking || !embedded) && (
|
||
<Card>
|
||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||
Bankovní účty
|
||
</Typography>
|
||
{bankLoading ? (
|
||
<LoadingState />
|
||
) : (
|
||
<>
|
||
{bankAccountsList.length > 0 && (
|
||
<Box sx={{ mb: 2 }}>
|
||
<DataTable<BankAccount>
|
||
columns={bankColumns}
|
||
rows={bankAccountsList}
|
||
rowKey={(acc) => acc.id}
|
||
rowSx={(acc) =>
|
||
editingBank === acc.id
|
||
? { bgcolor: "action.selected" }
|
||
: {}
|
||
}
|
||
/>
|
||
</Box>
|
||
)}
|
||
|
||
<Box
|
||
sx={{
|
||
bgcolor: "action.hover",
|
||
borderRadius: 2,
|
||
p: 2,
|
||
}}
|
||
>
|
||
<Typography
|
||
variant="subtitle2"
|
||
color="text.secondary"
|
||
sx={{ mb: 1.5 }}
|
||
>
|
||
{editingBank !== null ? "Upravit účet" : "Přidat nový účet"}
|
||
</Typography>
|
||
<Box
|
||
sx={{
|
||
display: "grid",
|
||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Field label="Název účtu" required>
|
||
<TextField
|
||
value={bankForm.account_name}
|
||
onChange={(e) =>
|
||
setBankForm((f) => ({
|
||
...f,
|
||
account_name: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="Např. Hlavní CZK účet"
|
||
/>
|
||
</Field>
|
||
<Field label="Název banky">
|
||
<TextField
|
||
value={bankForm.bank_name}
|
||
onChange={(e) =>
|
||
setBankForm((f) => ({
|
||
...f,
|
||
bank_name: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="Např. MONETA Money Bank, a.s."
|
||
/>
|
||
</Field>
|
||
<Field label="Číslo účtu">
|
||
<TextField
|
||
value={bankForm.account_number}
|
||
onChange={(e) =>
|
||
setBankForm((f) => ({
|
||
...f,
|
||
account_number: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="123456789/0600"
|
||
/>
|
||
</Field>
|
||
<Field label="Měna">
|
||
<Select
|
||
value={bankForm.currency}
|
||
onChange={(v) =>
|
||
setBankForm((f) => ({
|
||
...f,
|
||
currency: v,
|
||
}))
|
||
}
|
||
options={form.available_currencies.map((c) => ({
|
||
value: c,
|
||
label: c,
|
||
}))}
|
||
/>
|
||
</Field>
|
||
<Field label="IBAN">
|
||
<TextField
|
||
value={bankForm.iban}
|
||
onChange={(e) =>
|
||
setBankForm((f) => ({
|
||
...f,
|
||
iban: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="CZ65 0800 0000 1920 0014 5399"
|
||
/>
|
||
</Field>
|
||
<Field label="BIC / SWIFT">
|
||
<TextField
|
||
value={bankForm.bic}
|
||
onChange={(e) =>
|
||
setBankForm((f) => ({
|
||
...f,
|
||
bic: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="GIBACZPX"
|
||
/>
|
||
</Field>
|
||
</Box>
|
||
<CheckboxField
|
||
label="Výchozí účet (použije se automaticky při vytváření faktury)"
|
||
checked={bankForm.is_default}
|
||
onChange={(checked) =>
|
||
setBankForm((f) => ({
|
||
...f,
|
||
is_default: checked,
|
||
}))
|
||
}
|
||
/>
|
||
<Box sx={{ display: "flex", gap: 1, mt: 1 }}>
|
||
<Button
|
||
size="small"
|
||
startIcon={editingBank === null ? PlusIcon : undefined}
|
||
onClick={handleBankSave}
|
||
disabled={bankSaving}
|
||
>
|
||
{bankSaving
|
||
? "Ukládání..."
|
||
: editingBank !== null
|
||
? "Uložit změny"
|
||
: "Přidat účet"}
|
||
</Button>
|
||
{editingBank !== null && (
|
||
<Button
|
||
size="small"
|
||
variant="outlined"
|
||
color="inherit"
|
||
onClick={resetBankForm}
|
||
>
|
||
Zrušit
|
||
</Button>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
</>
|
||
)}
|
||
</Card>
|
||
)}
|
||
|
||
{/* PDF Field Order */}
|
||
{(canEditCompany || !embedded) && (
|
||
<Card>
|
||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||
Pořadí polí dodavatele v PDF
|
||
</Typography>
|
||
<Typography
|
||
variant="caption"
|
||
color="text.secondary"
|
||
sx={{ display: "block", mb: 1.5 }}
|
||
>
|
||
Určuje pořadí řádků v adresním bloku dodavatele na PDF nabídce.
|
||
</Typography>
|
||
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.5 }}>
|
||
{fullFieldOrder.map((key, index) => (
|
||
<Box
|
||
key={key}
|
||
sx={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 1,
|
||
px: 1,
|
||
py: 0.5,
|
||
border: 1,
|
||
borderColor: "divider",
|
||
borderRadius: 1,
|
||
}}
|
||
>
|
||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => moveField(index, -1)}
|
||
disabled={index === 0}
|
||
title="Nahoru"
|
||
aria-label="Nahoru"
|
||
sx={{ p: 0.25 }}
|
||
>
|
||
{UpIcon}
|
||
</IconButton>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => moveField(index, 1)}
|
||
disabled={index === fullFieldOrder.length - 1}
|
||
title="Dolů"
|
||
aria-label="Dolů"
|
||
sx={{ p: 0.25 }}
|
||
>
|
||
{DownIcon}
|
||
</IconButton>
|
||
</Box>
|
||
<Typography
|
||
variant="body2"
|
||
sx={{
|
||
color: key.startsWith("custom_")
|
||
? "primary.main"
|
||
: "text.primary",
|
||
fontWeight: key.startsWith("custom_") ? 600 : 400,
|
||
}}
|
||
>
|
||
{getFieldDisplayName(key)}
|
||
</Typography>
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Document Numbering */}
|
||
{(canEditCompany || !embedded) && (
|
||
<Card>
|
||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||
Číslování dokladů
|
||
</Typography>
|
||
<Box
|
||
sx={{
|
||
p: 1.5,
|
||
bgcolor: "action.hover",
|
||
borderRadius: 1,
|
||
fontSize: "0.8rem",
|
||
color: "text.secondary",
|
||
mb: 2,
|
||
}}
|
||
>
|
||
<strong>Dostupné zástupné znaky:</strong> <code>{"{YYYY}"}</code>{" "}
|
||
rok, <code>{"{YY}"}</code> rok (2 číslice),{" "}
|
||
<code>{"{PREFIX}"}</code> prefix (nabídky / sklad),{" "}
|
||
<code>{"{CODE}"}</code> typový kód, <code>{"{NNN}"}</code> pořadí
|
||
(3 číslice), <code>{"{NNNN}"}</code> pořadí (4 číslice),{" "}
|
||
<code>{"{NNNNN}"}</code> pořadí (5 číslic)
|
||
</Box>
|
||
{[
|
||
{
|
||
label: "Nabídky",
|
||
patternKey: "offer_number_pattern" as const,
|
||
defaultPattern: "{YYYY}/{PREFIX}/{NNN}",
|
||
prefixKey: "quotation_prefix" as const,
|
||
prefixLabel: "Prefix",
|
||
codeKey: null,
|
||
},
|
||
{
|
||
label: "Objednávky přijaté",
|
||
patternKey: "order_number_pattern" as const,
|
||
defaultPattern: "{YY}{CODE}{NNNN}",
|
||
prefixKey: null,
|
||
codeKey: "order_type_code" as const,
|
||
codeLabel: "Typový kód",
|
||
// Mirror the server generator's code fallback so the preview
|
||
// matches the real generated number when the field is empty.
|
||
defaultCode: "71",
|
||
},
|
||
{
|
||
label: "Objednávky vydané",
|
||
patternKey: "issued_order_number_pattern" as const,
|
||
defaultPattern: "{YY}{CODE}{NNNN}",
|
||
prefixKey: null,
|
||
codeKey: "issued_order_type_code" as const,
|
||
codeLabel: "Typový kód",
|
||
defaultCode: "72",
|
||
},
|
||
{
|
||
label: "Projekty",
|
||
patternKey: "project_number_pattern" as const,
|
||
defaultPattern: "{YY}{CODE}{NNNN}",
|
||
prefixKey: null,
|
||
codeKey: "project_type_code" as const,
|
||
codeLabel: "Typový kód",
|
||
defaultCode: "73",
|
||
},
|
||
{
|
||
label: "Faktury",
|
||
patternKey: "invoice_number_pattern" as const,
|
||
defaultPattern: "{YY}{CODE}{NNNN}",
|
||
prefixKey: null,
|
||
codeKey: "invoice_type_code" as const,
|
||
codeLabel: "Typový kód",
|
||
defaultCode: "81",
|
||
},
|
||
{
|
||
label: "Sklad — Příjmy",
|
||
patternKey: "warehouse_receipt_number_pattern" as const,
|
||
defaultPattern: "{PREFIX}-{YYYY}-{NNN}",
|
||
prefixKey: "warehouse_receipt_prefix" as const,
|
||
prefixLabel: "Prefix",
|
||
codeKey: null,
|
||
},
|
||
{
|
||
label: "Sklad — Výdeje",
|
||
patternKey: "warehouse_issue_number_pattern" as const,
|
||
defaultPattern: "{PREFIX}-{YYYY}-{NNN}",
|
||
prefixKey: "warehouse_issue_prefix" as const,
|
||
prefixLabel: "Prefix",
|
||
codeKey: null,
|
||
},
|
||
{
|
||
label: "Sklad — Inventarizace",
|
||
patternKey: "warehouse_inventory_number_pattern" as const,
|
||
defaultPattern: "{PREFIX}-{YYYY}-{NNN}",
|
||
prefixKey: "warehouse_inventory_prefix" as const,
|
||
prefixLabel: "Prefix",
|
||
codeKey: null,
|
||
},
|
||
].map((cfg, idx) => {
|
||
const pattern = form[cfg.patternKey] || cfg.defaultPattern;
|
||
const yyyy = String(new Date().getFullYear());
|
||
const yy = yyyy.slice(-2);
|
||
const prefix = cfg.prefixKey ? form[cfg.prefixKey] || "NA" : "";
|
||
const code = cfg.codeKey
|
||
? form[cfg.codeKey] ||
|
||
(cfg as { defaultCode?: string }).defaultCode ||
|
||
""
|
||
: "";
|
||
const preview = pattern.replace(
|
||
/\{(\w+)\}/g,
|
||
(m: string, k: string) => {
|
||
if (k === "YYYY") return yyyy;
|
||
if (k === "YY") return yy;
|
||
if (k === "PREFIX") return prefix;
|
||
if (k === "CODE") return code;
|
||
if (/^N+$/.test(k)) return "1".padStart(k.length, "0");
|
||
return m;
|
||
},
|
||
);
|
||
return (
|
||
<Box key={cfg.patternKey}>
|
||
{idx > 0 && (
|
||
<Box
|
||
sx={{
|
||
borderTop: 1,
|
||
borderColor: "divider",
|
||
my: 2,
|
||
}}
|
||
/>
|
||
)}
|
||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
|
||
{cfg.label}
|
||
</Typography>
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexWrap: "wrap",
|
||
gap: 2,
|
||
alignItems: "flex-start",
|
||
}}
|
||
>
|
||
<Box sx={{ flex: "1 1 200px" }}>
|
||
<Field label="Formát">
|
||
<TextField
|
||
value={form[cfg.patternKey]}
|
||
onChange={(e) =>
|
||
updateField(cfg.patternKey, e.target.value)
|
||
}
|
||
placeholder={cfg.defaultPattern}
|
||
/>
|
||
</Field>
|
||
</Box>
|
||
{cfg.prefixKey && (
|
||
<Box sx={{ width: 120 }}>
|
||
<Field label={cfg.prefixLabel || "Prefix"}>
|
||
<TextField
|
||
value={form[cfg.prefixKey]}
|
||
onChange={(e) =>
|
||
updateField(cfg.prefixKey, e.target.value)
|
||
}
|
||
/>
|
||
</Field>
|
||
</Box>
|
||
)}
|
||
{cfg.codeKey && (
|
||
<Box sx={{ width: 120 }}>
|
||
<Field label={cfg.codeLabel || "Kód"}>
|
||
<TextField
|
||
value={form[cfg.codeKey]}
|
||
onChange={(e) =>
|
||
updateField(cfg.codeKey, e.target.value)
|
||
}
|
||
/>
|
||
</Field>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
<Typography variant="caption" color="text.secondary">
|
||
Ukázka:{" "}
|
||
<Box component="strong" sx={{ color: "text.primary" }}>
|
||
{preview}
|
||
</Box>
|
||
</Typography>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Card>
|
||
)}
|
||
|
||
{/* Currency & VAT */}
|
||
{(canEditCompany || !embedded) && (
|
||
<Card>
|
||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||
Měna a DPH
|
||
</Typography>
|
||
<Box
|
||
sx={{
|
||
display: "grid",
|
||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Field label="Výchozí měna">
|
||
<Select
|
||
value={form.default_currency}
|
||
onChange={(v) => updateField("default_currency", v)}
|
||
options={form.available_currencies.map((c) => ({
|
||
value: c,
|
||
label: c,
|
||
}))}
|
||
/>
|
||
</Field>
|
||
<Field label="Výchozí sazba DPH (%)">
|
||
<TextField
|
||
type="number"
|
||
value={form.default_vat_rate}
|
||
onChange={(e) =>
|
||
updateField("default_vat_rate", Number(e.target.value))
|
||
}
|
||
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
||
/>
|
||
</Field>
|
||
<Field label="Dostupné měny">
|
||
<TextField
|
||
value={form.available_currencies.join(", ")}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
available_currencies: e.target.value
|
||
.split(",")
|
||
.map((s) => s.trim())
|
||
.filter(Boolean),
|
||
}))
|
||
}
|
||
placeholder="CZK, EUR, USD"
|
||
/>
|
||
</Field>
|
||
<Field label="Dostupné sazby DPH (%)">
|
||
<TextField
|
||
value={form.available_vat_rates.join(", ")}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
available_vat_rates: e.target.value
|
||
.split(",")
|
||
.map((s) => Number(s.trim()))
|
||
.filter((n) => !isNaN(n)),
|
||
}))
|
||
}
|
||
placeholder="0, 12, 21"
|
||
/>
|
||
</Field>
|
||
</Box>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Logo */}
|
||
{(canEditCompany || !embedded) && (
|
||
<Card>
|
||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||
Logo
|
||
</Typography>
|
||
<Box
|
||
sx={{
|
||
display: "grid",
|
||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Box>
|
||
<Typography
|
||
variant="body2"
|
||
sx={{
|
||
display: "block",
|
||
mb: 0.5,
|
||
fontWeight: 600,
|
||
color: "text.secondary",
|
||
}}
|
||
>
|
||
Logo (světlý režim)
|
||
</Typography>
|
||
{logoUrl && (
|
||
<Box
|
||
sx={{
|
||
mb: 1,
|
||
p: 1,
|
||
border: 1,
|
||
borderColor: "divider",
|
||
borderRadius: 1,
|
||
bgcolor: "background.paper",
|
||
display: "inline-block",
|
||
}}
|
||
>
|
||
<Box
|
||
component="img"
|
||
src={logoUrl}
|
||
alt="Logo (světlý režim)"
|
||
sx={{ maxWidth: 200, maxHeight: 80, display: "block" }}
|
||
/>
|
||
</Box>
|
||
)}
|
||
<FileUpload
|
||
files={[]}
|
||
accept="image/*"
|
||
buttonLabel={uploadingLogo ? "Nahrávání..." : "Nahrát logo"}
|
||
disabled={uploadingLogo}
|
||
onFilesChange={(files) => {
|
||
if (files[0]) handleLogoUpload(files[0], "light");
|
||
}}
|
||
/>
|
||
<Typography
|
||
variant="caption"
|
||
color="text.secondary"
|
||
sx={{ display: "block", mt: 0.5 }}
|
||
>
|
||
PNG, JPEG, GIF nebo WebP, max 5 MB
|
||
</Typography>
|
||
</Box>
|
||
<Box>
|
||
<Typography
|
||
variant="body2"
|
||
sx={{
|
||
display: "block",
|
||
mb: 0.5,
|
||
fontWeight: 600,
|
||
color: "text.secondary",
|
||
}}
|
||
>
|
||
Logo (tmavý režim)
|
||
</Typography>
|
||
{logoUrlDark && (
|
||
<Box
|
||
sx={{
|
||
mb: 1,
|
||
p: 1,
|
||
border: 1,
|
||
borderColor: "divider",
|
||
borderRadius: 1,
|
||
bgcolor: "background.paper",
|
||
display: "inline-block",
|
||
}}
|
||
>
|
||
<Box
|
||
component="img"
|
||
src={logoUrlDark}
|
||
alt="Logo (tmavý režim)"
|
||
sx={{ maxWidth: 200, maxHeight: 80, display: "block" }}
|
||
/>
|
||
</Box>
|
||
)}
|
||
<FileUpload
|
||
files={[]}
|
||
accept="image/*"
|
||
buttonLabel={
|
||
uploadingLogoDark ? "Nahrávání..." : "Nahrát logo"
|
||
}
|
||
disabled={uploadingLogoDark}
|
||
onFilesChange={(files) => {
|
||
if (files[0]) handleLogoUpload(files[0], "dark");
|
||
}}
|
||
/>
|
||
<Typography
|
||
variant="caption"
|
||
color="text.secondary"
|
||
sx={{ display: "block", mt: 0.5 }}
|
||
>
|
||
PNG, JPEG, GIF nebo WebP, max 5 MB
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
</Card>
|
||
)}
|
||
</Box>
|
||
|
||
{embedded && canEditCompany && (
|
||
<Button onClick={handleSave} fullWidth sx={{ mt: 2 }} disabled={saving}>
|
||
{saving ? "Ukládání..." : "Uložit nastavení firmy"}
|
||
</Button>
|
||
)}
|
||
|
||
<ConfirmDialog
|
||
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"
|
||
confirmVariant="danger"
|
||
/>
|
||
</Box>
|
||
);
|
||
}
|