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: 'EUR',
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" placeholder="Název položky" readOnly={readOnly} style={{ fontWeight: 500 }} />
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 }>>([])
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 [lockedBy, setLockedBy] = useState<{ user_id: number; username: string; full_name: string } | null>(null)
const heartbeatRef = useRef | null>(null)
useModalLock(showOrderModal)
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())
// Load data
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 || 'EUR',
language: d.language || 'EN',
vat_rate: d.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])
// 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(() => {})
}, 2 * 60 * 1000) // every 2 minutes
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])
// Fetch next quotation number for new offers
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))
}
// Totals
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) {
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
setPdfLoading(true)
try {
const response = await apiFetch(`${API_BASE}/offers-pdf/${id}`)
if (response.status === 401) return
if (!response.ok) {
alert.error('Nepodařilo se vygenerovat PDF')
return
}
const html = await response.text()
const w = window.open('', '_blank')
if (w) {
w.document.open()
w.document.write(html)
w.document.close()
w.onload = () => w.print()
} else {
alert.error('Prohlížeč zablokoval vyskakovací okno')
}
} catch {
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 ? (
<>
PDF...
>
) : (
<>
PDF
>
)}
)}
{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}
>
EUR
USD
CZK
GBP
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
{(isInvalidated || isLockedByOther) ? (
Prázdný obsah' }}
/>
) : (
setSections(prev => prev.map((s, i) => i === idx ? { ...s, content: val } : s))}
placeholder="Obsah sekce..."
minHeight="120px"
/>
)}
))}
)}
{/* 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}
/>
)
}