import {
useState,
useEffect,
useCallback,
useRef,
type ChangeEvent,
} from "react";
import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext";
import { useParams, useNavigate, Link } from "react-router-dom";
import { motion, AnimatePresence } from "framer-motion";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
TouchSensor,
useSensor,
useSensors,
type DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
useSortable,
arrayMove,
} from "@dnd-kit/sortable";
import {
restrictToVerticalAxis,
restrictToParentElement,
} from "@dnd-kit/modifiers";
import { CSS } from "@dnd-kit/utilities";
import ConfirmModal from "../components/ConfirmModal";
import FormField from "../components/FormField";
import Forbidden from "../components/Forbidden";
import AdminDatePicker from "../components/AdminDatePicker";
import RichEditor from "../components/RichEditor";
import useModalLock from "../hooks/useModalLock";
import useDebounce from "../hooks/useDebounce";
import apiFetch from "../utils/api";
import { formatCurrency } from "../utils/formatters";
const API_BASE = "/api/admin";
const DRAFT_KEY = "boha_offer_draft";
interface OfferItem {
_key: string;
id?: number;
description: string;
item_description: string;
quantity: number;
unit: string;
unit_price: number;
is_included_in_total: boolean;
}
let _itemKeyCounter = 0;
const nextItemKey = () => `item-${++_itemKeyCounter}`;
interface ScopeSection {
title: string;
title_cz: string;
content: string;
}
interface OfferForm {
quotation_number: string;
project_code: string;
customer_id: number | null;
customer_name: string;
created_at: string;
valid_until: string;
currency: string;
language: string;
vat_rate: number;
apply_vat: boolean;
exchange_rate: string;
scope_title: string;
scope_description: string;
}
interface Customer {
id: number;
name: string;
city?: string;
}
interface OrderInfo {
id: number;
order_number: string;
}
const emptyForm: OfferForm = {
quotation_number: "",
project_code: "",
customer_id: null,
customer_name: "",
created_at: new Date().toISOString().split("T")[0],
valid_until: "",
currency: "CZK",
language: "EN",
vat_rate: 21,
apply_vat: false,
exchange_rate: "",
scope_title: "",
scope_description: "",
};
const emptyScopeSection = (): ScopeSection => ({
title: "",
title_cz: "",
content: "",
});
const emptyItem = (): OfferItem => ({
_key: nextItemKey(),
description: "",
item_description: "",
quantity: 1,
unit: "ks",
unit_price: 0,
is_included_in_total: true,
});
function SortableItemRow({
item,
index,
currency,
readOnly,
canDelete,
onUpdate,
onRemove,
}: {
item: OfferItem;
index: number;
currency: string;
readOnly: boolean;
canDelete: boolean;
onUpdate: (field: string, value: unknown) => void;
onRemove: () => void;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: item._key, disabled: readOnly });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
background: isDragging ? "var(--bg-secondary)" : undefined,
position: "relative" as const,
zIndex: isDragging ? 10 : undefined,
};
const lineTotal =
(Number(item.quantity) || 0) * (Number(item.unit_price) || 0);
return (
{!readOnly && (
)}
{index + 1}
onUpdate("description", e.target.value)}
className="admin-form-input fw-500"
placeholder="Název položky"
readOnly={readOnly}
/>
onUpdate("item_description", e.target.value)}
className="admin-form-input"
placeholder="Podrobný popis (volitelný)"
readOnly={readOnly}
style={{ fontSize: "0.8rem", opacity: 0.8 }}
/>
onUpdate("quantity", parseFloat(e.target.value) || 0)
}
className="admin-form-input"
step="1"
readOnly={readOnly}
/>
onUpdate("unit", e.target.value)}
className="admin-form-input"
readOnly={readOnly}
/>
onUpdate("unit_price", parseFloat(e.target.value) || 0)
}
className="admin-form-input"
step="0.01"
readOnly={readOnly}
/>
onUpdate("is_included_in_total", e.target.checked)}
disabled={readOnly}
/>
{formatCurrency(lineTotal, currency)}
{!readOnly && (
)}
);
}
export default function OfferDetail() {
const { id } = useParams();
const isEdit = Boolean(id);
const alert = useAlert();
const { hasPermission } = useAuth();
const navigate = useNavigate();
const dndSensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
useSensor(TouchSensor, {
activationConstraint: { delay: 200, tolerance: 5 },
}),
useSensor(KeyboardSensor),
);
const [loading, setLoading] = useState(isEdit);
const [saving, setSaving] = useState(false);
const [errors, setErrors] = useState>({});
const [form, setForm] = useState(emptyForm);
const [items, setItems] = useState([emptyItem()]);
const [sections, setSections] = useState([]);
const [scopeTemplates, setScopeTemplates] = useState<
Array<{
id: number;
name: string;
description?: string;
scope_template_sections?: Array<{
title?: string;
title_cz?: string;
content?: string;
}>;
}>
>([]);
const [customers, setCustomers] = useState([]);
const [customerSearch, setCustomerSearch] = useState("");
const [showCustomerDropdown, setShowCustomerDropdown] = useState(false);
const [orderInfo, setOrderInfo] = useState(null);
const [offerStatus, setOfferStatus] = useState("");
const [deleteConfirm, setDeleteConfirm] = useState(false);
const [deleting, setDeleting] = useState(false);
const [creatingOrder, setCreatingOrder] = useState(false);
const [showOrderModal, setShowOrderModal] = useState(false);
const [invalidateConfirm, setInvalidateConfirm] = useState(false);
const [invalidatingOffer, setInvalidatingOffer] = useState(false);
const [customerOrderNumber, setCustomerOrderNumber] = useState("");
const [orderAttachment, setOrderAttachment] = useState(null);
const [pdfLoading, setPdfLoading] = useState(false);
const [companySettings, setCompanySettings] = useState<{
default_currency: string;
default_vat_rate: number;
available_currencies: string[];
available_vat_rates: number[];
} | null>(null);
const [lockedBy, setLockedBy] = useState<{
user_id: number;
username: string;
full_name: string;
} | null>(null);
const heartbeatRef = useRef | null>(null);
useModalLock(showOrderModal);
useEffect(() => {
apiFetch(`${API_BASE}/company-settings`)
.then((r) => r.json())
.then((d) => {
if (d.success) setCompanySettings(d.data);
})
.catch(() => {});
}, []);
useEffect(() => {
if (companySettings && !isEdit) {
setForm((prev) => ({
...prev,
currency:
prev.currency === "CZK"
? companySettings.default_currency || "CZK"
: prev.currency,
vat_rate:
prev.vat_rate === 21
? (companySettings.default_vat_rate ?? 21)
: prev.vat_rate,
}));
}
}, [companySettings, isEdit]);
const isInvalidated = offerStatus === "invalidated";
const isLockedByOther = !!lockedBy;
const isExpiredNotInvalidated =
isEdit &&
!isInvalidated &&
!orderInfo &&
form.valid_until &&
new Date(form.valid_until) < new Date(new Date().toDateString());
const fetchDetail = useCallback(async () => {
if (!id) return;
try {
const response = await apiFetch(`${API_BASE}/offers/${id}`);
if (response.status === 401) return;
const result = await response.json();
if (result.success) {
const d = result.data;
setForm({
quotation_number: d.quotation_number || "",
project_code: d.project_code || "",
customer_id: d.customer_id || null,
customer_name: d.customer_name || "",
created_at: d.created_at ? String(d.created_at).substring(0, 10) : "",
valid_until: d.valid_until
? String(d.valid_until).substring(0, 10)
: "",
currency: d.currency || companySettings?.default_currency || "CZK",
language: d.language || "EN",
vat_rate: d.vat_rate ?? companySettings?.default_vat_rate ?? 21,
apply_vat: !!d.apply_vat,
exchange_rate: d.exchange_rate || "",
scope_title: d.scope_title || "",
scope_description: d.scope_description || "",
});
setItems(
d.items?.length
? d.items.map((it: any) => ({ ...it, _key: nextItemKey() }))
: [emptyItem()],
);
setSections(
d.sections?.length
? d.sections.map((s: any) => ({
title: s.title || "",
title_cz: s.title_cz || "",
content: s.content || "",
}))
: [],
);
setOfferStatus(d.status || "");
setOrderInfo(d.order || null);
setLockedBy(d.locked_by || null);
// Try to acquire lock if not locked by someone else and not invalidated
if (
!d.locked_by &&
d.status !== "invalidated" &&
hasPermission("offers.edit")
) {
apiFetch(`${API_BASE}/offers/${id}/lock`, { method: "POST" }).catch(
() => {},
);
}
} else {
alert.error(result.error || "Nepodařilo se načíst nabídku");
navigate("/offers");
}
} catch {
alert.error("Chyba připojení");
navigate("/offers");
} finally {
setLoading(false);
}
}, [id, alert, navigate, hasPermission, companySettings]);
// Heartbeat to keep lock alive + cleanup on unmount
useEffect(() => {
if (!isEdit || !id || isLockedByOther || isInvalidated) return;
heartbeatRef.current = setInterval(() => {
apiFetch(`${API_BASE}/offers/${id}/heartbeat`, { method: "POST" }).catch(
() => {},
);
}, 10 * 1000); // every 10 seconds
return () => {
if (heartbeatRef.current) clearInterval(heartbeatRef.current);
// Release lock on unmount
apiFetch(`${API_BASE}/offers/${id}/unlock`, { method: "POST" }).catch(
() => {},
);
};
}, [isEdit, id, isLockedByOther, isInvalidated]);
useEffect(() => {
if (isEdit) fetchDetail();
}, [isEdit, fetchDetail]);
useEffect(() => {
const loadCustomers = async () => {
try {
const res = await apiFetch(`${API_BASE}/customers`);
if (res.status === 401) return;
const data = await res.json();
if (data.success)
setCustomers(
Array.isArray(data.data) ? data.data : data.data?.customers || [],
);
} catch {
/* silent */
}
};
const loadScopeTemplates = async () => {
try {
const res = await apiFetch(`${API_BASE}/offers-templates`);
if (res.status === 401) return;
const data = await res.json();
if (data.success && Array.isArray(data.data)) {
setScopeTemplates(data.data);
}
} catch {
/* silent */
}
};
loadCustomers();
loadScopeTemplates();
}, []);
// Close dropdown on outside click
useEffect(() => {
const handleClickOutside = () => setShowCustomerDropdown(false);
if (showCustomerDropdown) {
document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside);
}
}, [showCustomerDropdown]);
useEffect(() => {
if (isEdit) return;
const fetchNextNumber = async () => {
try {
const res = await apiFetch(`${API_BASE}/offers/next-number`);
if (res.status === 401) return;
const data = await res.json();
if (data.success) {
setForm((prev) => ({
...prev,
quotation_number: data.data?.next_number || data.data?.number || "",
}));
}
} catch {
/* silent */
}
};
fetchNextNumber();
}, [isEdit]);
// Restore draft from localStorage on mount (create mode only)
const draftRestoredRef = useRef(false);
useEffect(() => {
if (isEdit || draftRestoredRef.current) return;
draftRestoredRef.current = true;
try {
const raw = localStorage.getItem(DRAFT_KEY);
if (!raw) return;
const draft = JSON.parse(raw);
if (draft && draft.form) {
setForm((prev) => ({
...prev,
project_code: draft.form.project_code || prev.project_code,
customer_name: draft.form.customer_name || prev.customer_name,
created_at: draft.form.created_at || prev.created_at,
valid_until: draft.form.valid_until || prev.valid_until,
currency: draft.form.currency || prev.currency,
}));
if (draft.form.customer_id) {
setForm((prev) => ({ ...prev, customer_id: draft.form.customer_id }));
}
}
if (draft && Array.isArray(draft.items) && draft.items.length > 0) {
setItems(draft.items);
}
if (draft && Array.isArray(draft.sections) && draft.sections.length > 0) {
setSections(draft.sections);
}
} catch {
/* ignore corrupt data */
}
}, [isEdit]);
// Auto-save draft to localStorage (create mode only)
const draftPayload = JSON.stringify({ form, items, sections });
const debouncedDraft = useDebounce(draftPayload, 1500);
useEffect(() => {
if (isEdit) return;
try {
const draft = {
form: {
project_code: form.project_code,
customer_id: form.customer_id,
customer_name: form.customer_name,
created_at: form.created_at,
valid_until: form.valid_until,
currency: form.currency,
},
items,
sections,
savedAt: new Date().toISOString(),
};
localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
} catch {
/* localStorage full or unavailable */
}
}, [debouncedDraft]); // eslint-disable-line react-hooks/exhaustive-deps
const updateForm = (field: keyof OfferForm, value: unknown) => {
setForm((prev) => ({ ...prev, [field]: value }));
setErrors((prev) => ({ ...prev, [field]: undefined }));
};
const selectCustomer = (c: Customer) => {
setForm((prev) => ({ ...prev, customer_id: c.id, customer_name: c.name }));
setErrors((prev) => ({ ...prev, customer_id: undefined }));
setCustomerSearch("");
setShowCustomerDropdown(false);
};
const clearCustomer = () => {
setForm((prev) => ({ ...prev, customer_id: null, customer_name: "" }));
};
const updateItem = (
index: number,
field: keyof OfferItem,
value: unknown,
) => {
setItems((prev) =>
prev.map((item, i) => (i === index ? { ...item, [field]: value } : item)),
);
};
const addItem = () => setItems((prev) => [...prev, emptyItem()]);
const removeItem = (index: number) => {
setItems((prev) => prev.filter((_, i) => i !== index));
};
const subtotal = items.reduce((sum, item) => {
if (item.is_included_in_total) {
return (
sum + (Number(item.quantity) || 0) * (Number(item.unit_price) || 0)
);
}
return sum;
}, 0);
const vatAmount = form.apply_vat ? subtotal * (form.vat_rate / 100) : 0;
const total = subtotal + vatAmount;
const filteredCustomers = customerSearch
? customers.filter((c) =>
c.name.toLowerCase().includes(customerSearch.toLowerCase()),
)
: customers;
const handleSave = async () => {
const newErrors: Record = {};
if (!form.created_at) newErrors.created_at = "Datum je povinné";
if (!form.valid_until) newErrors.valid_until = "Platnost je povinná";
if (items.length === 0) newErrors.items = "Přidejte alespoň jednu položku";
setErrors(newErrors);
if (Object.keys(newErrors).length > 0) return;
setSaving(true);
try {
const url = isEdit ? `${API_BASE}/offers/${id}` : `${API_BASE}/offers`;
const response = await apiFetch(url, {
method: isEdit ? "PUT" : "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...form,
items: items.map((item, i) => ({ ...item, position: i })),
sections: sections.map((s, i) => ({ ...s, position: i })),
}),
});
const result = await response.json();
if (result.success) {
const offerId = isEdit ? id : result.data?.id;
if (offerId) {
await apiFetch(`${API_BASE}/offers-pdf/${offerId}?save=1`).catch(
() => {},
);
}
alert.success(
result.message ||
(isEdit ? "Nabídka byla aktualizována" : "Nabídka byla vytvořena"),
);
if (!isEdit) {
try {
localStorage.removeItem(DRAFT_KEY);
} catch {
/* ignore */
}
}
if (!isEdit && result.data?.id) {
navigate(`/offers/${result.data.id}`);
}
} else {
alert.error(result.error || "Nepodařilo se uložit nabídku");
}
} catch {
alert.error("Chyba připojení");
} finally {
setSaving(false);
}
};
const handleCreateOrder = async () => {
if (!customerOrderNumber.trim()) {
alert.error("Číslo objednávky zákazníka je povinné");
return;
}
setCreatingOrder(true);
try {
let fetchOptions: RequestInit;
if (orderAttachment) {
// With attachment: send as multipart/form-data
const formData = new FormData();
formData.append("quotationId", String(id));
formData.append("customerOrderNumber", customerOrderNumber.trim());
formData.append("attachment", orderAttachment);
fetchOptions = { method: "POST", body: formData };
} else {
// Without attachment: send as JSON (avoids multipart content-type issues)
fetchOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
quotationId: id,
customerOrderNumber: customerOrderNumber.trim(),
}),
};
}
const response = await apiFetch(`${API_BASE}/orders`, fetchOptions);
const result = await response.json();
if (result.success) {
setShowOrderModal(false);
alert.success(result.message || "Objednávka byla vytvořena");
navigate(`/orders/${result.data.order_id}`);
} else {
alert.error(result.error || "Nepodařilo se vytvořit objednávku");
}
} catch {
alert.error("Chyba připojení");
} finally {
setCreatingOrder(false);
}
};
const handleInvalidateOffer = async () => {
setInvalidatingOffer(true);
try {
const response = await apiFetch(`${API_BASE}/offers/${id}/invalidate`, {
method: "POST",
});
const result = await response.json();
if (result.success) {
setInvalidateConfirm(false);
setOfferStatus("invalidated");
alert.success(result.message || "Nabídka byla zneplatněna");
} else {
alert.error(result.error || "Nepodařilo se zneplatnit nabídku");
}
} catch {
alert.error("Chyba připojení");
} finally {
setInvalidatingOffer(false);
}
};
const handleDelete = async () => {
setDeleting(true);
try {
const response = await apiFetch(`${API_BASE}/offers/${id}`, {
method: "DELETE",
});
const result = await response.json();
if (result.success) {
alert.success(result.message || "Nabídka byla smazána");
navigate("/offers");
} else {
alert.error(result.error || "Nepodařilo se smazat nabídku");
}
} catch {
alert.error("Chyba připojení");
} finally {
setDeleting(false);
setDeleteConfirm(false);
}
};
const handlePdf = async () => {
if (!isEdit || pdfLoading) return;
const newWindow = window.open("", "_blank");
setPdfLoading(true);
try {
const response = await apiFetch(`${API_BASE}/offers/${id}/file`);
if (response.status === 401) {
newWindow?.close();
return;
}
if (!response.ok) {
newWindow?.close();
alert.error("PDF soubor nenalezen — uložte nabídku pro vygenerování");
return;
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
if (newWindow) newWindow.location.href = url;
setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch {
newWindow?.close();
alert.error("Chyba při generování PDF");
} finally {
setPdfLoading(false);
}
};
const getRequiredPerm = () => {
if (!isEdit) return "offers.create";
return isInvalidated ? "offers.view" : "offers.edit";
};
const requiredPerm = getRequiredPerm();
if (!hasPermission(requiredPerm)) return ;
if (loading) {
return (
{[0, 1, 2, 3].map((i) => (
))}
);
}
return (
{/* Header */}
{isEdit ? `Nabídka ${form.quotation_number}` : "Nová nabídka"}
{isInvalidated && (
Zneplatněna
)}
{isEdit && hasPermission("offers.export") && (
{pdfLoading ? (
) : (
)}
Zobrazit nabídku
)}
{isEdit &&
!isInvalidated &&
hasPermission("orders.create") &&
!orderInfo && (
{
setCustomerOrderNumber("");
setOrderAttachment(null);
setShowOrderModal(true);
}}
className="admin-btn admin-btn-secondary"
>
Vytvořit objednávku
)}
{isEdit && orderInfo && (
Objednávka {orderInfo.order_number}
)}
{isExpiredNotInvalidated && hasPermission("offers.edit") && (
setInvalidateConfirm(true)}
className="admin-btn admin-btn-secondary"
>
Zneplatnit
)}
{!isInvalidated && !isLockedByOther && (
{saving ? (
<>
Ukládání...
>
) : (
"Uložit"
)}
)}
{isEdit && hasPermission("offers.delete") && (
setDeleteConfirm(true)}
className="admin-btn admin-btn-primary"
>
Smazat
)}
{/* Lock banner */}
{isLockedByOther && (
Nabídku právě upravuje {lockedBy!.full_name} .
Můžete ji pouze prohlížet.
)}
{/* Quotation Form */}
Základní údaje
setForm((prev) => ({
...prev,
quotation_number: e.target.value,
}))
}
className="admin-form-input"
/>
updateForm("project_code", e.target.value)}
className="admin-form-input"
placeholder="Volitelný kód projektu"
readOnly={isInvalidated || isLockedByOther}
/>
{form.customer_id ? (
{form.customer_name}
{!isInvalidated && !isLockedByOther && (
)}
) : (
e.stopPropagation()}
>
{
setCustomerSearch(e.target.value);
setShowCustomerDropdown(true);
}}
onFocus={() => setShowCustomerDropdown(true)}
className="admin-form-input"
placeholder="Hledat zákazníka..."
readOnly={isInvalidated || isLockedByOther}
/>
{showCustomerDropdown && !isInvalidated && (
{filteredCustomers.length === 0 ? (
Žádní zákazníci
) : (
filteredCustomers.slice(0, 20).map((c) => (
selectCustomer(c)}
>
{c.name}
{c.city &&
{c.city}
}
))
)}
)}
)}
updateForm("currency", e.target.value)}
className="admin-form-select"
disabled={isInvalidated || isLockedByOther}
>
{(
companySettings?.available_currencies || [
"CZK",
"EUR",
"USD",
"GBP",
]
).map((c) => (
{c}
))}
updateForm("language", e.target.value)}
className="admin-form-select"
disabled={isInvalidated || isLockedByOther}
>
English
Čeština
{/* Items Section with drag-and-drop */}
Položky
{!isInvalidated && !isLockedByOther && (
+ Přidat položku
)}
{errors.items && (
{errors.items}
)}
{
const { active, over } = event;
if (!over || active.id === over.id) return;
setItems((prev) => {
const oldIndex = prev.findIndex(
(i) => i._key === String(active.id),
);
const newIndex = prev.findIndex(
(i) => i._key === String(over.id),
);
if (oldIndex === -1 || newIndex === -1) return prev;
return arrayMove(prev, oldIndex, newIndex);
});
}}
>
i._key)}
strategy={verticalListSortingStrategy}
>
{!isInvalidated && !isLockedByOther && (
)}
#
Popis
Množství
Jednotka
Cena/ks
V ceně
Celkem
{!isInvalidated && !isLockedByOther && (
)}
{items.map((item, index) => (
1}
onUpdate={(field, value) =>
updateItem(index, field, value)
}
onRemove={() => removeItem(index)}
/>
))}
{/* Totals */}
Mezisoučet:
{formatCurrency(subtotal, form.currency)}
{form.apply_vat && (
DPH ({form.vat_rate}%):
{formatCurrency(vatAmount, form.currency)}
)}
Celkem:
{formatCurrency(total, form.currency)}
{/* Scope/Range Section */}
Rozsah projektu
{!isInvalidated && !isLockedByOther && (
{scopeTemplates.length > 0 && (
{
const templateId = Number(e.target.value);
if (!templateId) return;
const template = scopeTemplates.find(
(t) => t.id === templateId,
);
if (template?.scope_template_sections?.length) {
const newSections =
template.scope_template_sections.map((s: any) => ({
title: s.title || "",
title_cz: s.title_cz || "",
content: s.content || "",
}));
setSections((prev) => [...prev, ...newSections]);
if (template.description) {
setForm((prev) => ({
...prev,
scope_description:
template.description || prev.scope_description,
}));
}
alert.success(`Načtena šablona "${template.name}"`);
}
e.target.value = "";
}}
>
Ze šablony...
{scopeTemplates.map((t) => (
{t.name}
))}
)}
setSections((prev) => [...prev, emptyScopeSection()])
}
className="admin-btn admin-btn-secondary admin-btn-sm"
>
+ Přidat sekci
)}
{sections.length === 0 ? (
Žádné sekce rozsahu. Klikněte na "Přidat sekci" nebo vyberte
šablonu.
) : (
{sections.map((section, idx) => (
Sekce {idx + 1}
{(form.language === "CZ"
? section.title_cz
: section.title) && (
—{" "}
{form.language === "CZ"
? section.title_cz || section.title
: section.title}
)}
{!isInvalidated && !isLockedByOther && (
{idx > 0 && (
setSections((prev) => {
const arr = [...prev];
[arr[idx - 1], arr[idx]] = [
arr[idx],
arr[idx - 1],
];
return arr;
})
}
className="admin-btn-icon"
title="Posunout nahoru"
>
)}
{idx < sections.length - 1 && (
setSections((prev) => {
const arr = [...prev];
[arr[idx], arr[idx + 1]] = [
arr[idx + 1],
arr[idx],
];
return arr;
})
}
className="admin-btn-icon"
title="Posunout dolů"
>
)}
setSections((prev) =>
prev.filter((_, i) => i !== idx),
)
}
className="admin-btn-icon danger"
title="Odebrat sekci"
>
)}
EN Název
sekce
>
}
>
setSections((prev) =>
prev.map((s, i) =>
i === idx ? { ...s, title: e.target.value } : s,
),
)
}
className="admin-form-input"
placeholder="Název sekce (anglicky)"
readOnly={isInvalidated || isLockedByOther}
/>
CZ
Název sekce
>
}
>
setSections((prev) =>
prev.map((s, i) =>
i === idx
? { ...s, title_cz: e.target.value }
: s,
),
)
}
className="admin-form-input"
placeholder="Název sekce (česky)"
readOnly={isInvalidated || isLockedByOther}
/>
Obsah
setSections((prev) =>
prev.map((s, i) =>
i === idx ? { ...s, content: val } : s,
),
)
}
placeholder="Obsah sekce..."
minHeight="120px"
readOnly={isInvalidated || isLockedByOther}
/>
))}
)}
{/* Order modal */}
{showOrderModal && (
!creatingOrder && setShowOrderModal(false)}
/>
Vytvořit objednávku
setShowOrderModal(false)}
className="admin-btn admin-btn-secondary"
disabled={creatingOrder}
>
Zrušit
{creatingOrder ? "Vytváření..." : "Vytvořit"}
)}
setInvalidateConfirm(false)}
onConfirm={handleInvalidateOffer}
title="Zneplatnit nabídku"
message={`Opravdu chcete zneplatnit nabídku "${form.quotation_number}"? Nabídka bude pouze pro čtení a nepůjde upravovat.`}
confirmText="Zneplatnit"
cancelText="Zrušit"
type="danger"
loading={invalidatingOffer}
/>
setDeleteConfirm(false)}
onConfirm={handleDelete}
title="Smazat nabídku"
message={`Opravdu chcete smazat nabídku "${form.quotation_number}"? Budou smazány i všechny položky a sekce. Tato akce je nevratná.`}
confirmText="Smazat"
cancelText="Zrušit"
type="danger"
loading={deleting}
/>
);
}