- FormField.jsx: pridana podpora style prop - 23 stranek migrovano na FormField (166 vyskytu, -246 radku) - firebase/php-jwt upgrade v6.11 -> v7.0.3 (security advisory fix) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
597 lines
24 KiB
JavaScript
597 lines
24 KiB
JavaScript
import { useState } 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 ConfirmModal from '../components/ConfirmModal'
|
|
import FormField from '../components/FormField'
|
|
import Forbidden from '../components/Forbidden'
|
|
import AdminDatePicker from '../components/AdminDatePicker'
|
|
import OfferItemsSection from '../components/OfferItemsSection'
|
|
import OfferScopeSection from '../components/OfferScopeSection'
|
|
import OfferCustomerPicker from '../components/OfferCustomerPicker'
|
|
import useModalLock from '../hooks/useModalLock'
|
|
import useOfferForm from '../hooks/useOfferForm'
|
|
import apiFetch from '../utils/api'
|
|
const API_BASE = '/api/admin'
|
|
|
|
export default function OfferDetail() {
|
|
const { id } = useParams()
|
|
const isEdit = Boolean(id)
|
|
const alert = useAlert()
|
|
const { hasPermission } = useAuth()
|
|
const navigate = useNavigate()
|
|
|
|
const {
|
|
loading, saving, errors, setErrors,
|
|
form, updateForm, items, setItems, sections,
|
|
customers, itemTemplates, scopeTemplates,
|
|
orderInfo, offerStatus, setOfferStatus,
|
|
totals, draftSavedAtLabel,
|
|
selectCustomer, clearCustomer,
|
|
updateItem, addItem, removeItem, addItemFromTemplate,
|
|
addSection, removeSection, updateSection, moveSection,
|
|
loadScopeTemplate, handleSave
|
|
} = useOfferForm({ id, isEdit, alert, navigate })
|
|
|
|
const [showItemTemplateMenu, setShowItemTemplateMenu] = useState(false)
|
|
const [showScopeTemplateMenu, setShowScopeTemplateMenu] = useState(false)
|
|
|
|
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)
|
|
|
|
useModalLock(showOrderModal)
|
|
|
|
const isInvalidated = offerStatus === 'invalidated'
|
|
const isExpiredNotInvalidated = isEdit && !isInvalidated && !orderInfo && form.valid_until && new Date(form.valid_until) < new Date(new Date().toDateString())
|
|
|
|
const handleCreateOrder = async () => {
|
|
if (!customerOrderNumber.trim()) {
|
|
alert.error('Číslo objednávky zákazníka je povinné')
|
|
return
|
|
}
|
|
setCreatingOrder(true)
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('quotationId', id)
|
|
formData.append('customerOrderNumber', customerOrderNumber.trim())
|
|
if (orderAttachment) {
|
|
formData.append('attachment', orderAttachment)
|
|
}
|
|
const response = await apiFetch(`${API_BASE}/orders.php`, {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
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.php?action=invalidate&id=${id}`, {
|
|
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.php?id=${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.php?id=${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 <Forbidden />
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="admin-skeleton" style={{ padding: 0, gap: '1.5rem' }}>
|
|
<div className="admin-skeleton-row" style={{ justifyContent: 'space-between' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
|
<div className="admin-skeleton-line" style={{ width: '32px', height: '32px', borderRadius: '8px' }} />
|
|
<div className="admin-skeleton-line h-8" style={{ width: '200px' }} />
|
|
</div>
|
|
<div className="admin-skeleton-row" style={{ gap: '0.5rem' }}>
|
|
<div className="admin-skeleton-line h-10" style={{ width: '100px', borderRadius: '8px' }} />
|
|
<div className="admin-skeleton-line h-10" style={{ width: '100px', borderRadius: '8px' }} />
|
|
</div>
|
|
</div>
|
|
<div className="admin-card">
|
|
<div className="admin-skeleton" style={{ gap: '1.25rem' }}>
|
|
{[0, 1, 2, 3].map(i => (
|
|
<div key={i} className="admin-skeleton-row">
|
|
<div className="admin-skeleton-line w-1/4" />
|
|
<div className="admin-skeleton-line w-1/2" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="admin-card">
|
|
<div className="admin-skeleton" style={{ gap: '1.25rem' }}>
|
|
{[0, 1, 2].map(i => (
|
|
<div key={i} className="admin-skeleton-row">
|
|
<div style={{ flex: 1 }}><div className="admin-skeleton-line w-full" /></div>
|
|
<div style={{ flex: 1 }}><div className="admin-skeleton-line w-3/4" /></div>
|
|
<div style={{ flex: 1 }}><div className="admin-skeleton-line w-1/2" /></div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="admin-card">
|
|
<div className="admin-skeleton" style={{ gap: '1.25rem' }}>
|
|
{[0, 1, 2].map(i => (
|
|
<div key={i} className="admin-skeleton-row">
|
|
<div className="admin-skeleton-line w-1/3" />
|
|
<div className="admin-skeleton-line w-full" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* Header */}
|
|
<motion.div
|
|
className="admin-page-header"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4 }}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
|
<Link to="/offers" className="admin-btn-icon" title="Zpět" aria-label="Zpět">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
</svg>
|
|
</Link>
|
|
<div>
|
|
<h1 className="admin-page-title">
|
|
{isEdit ? `Nabídka ${form.quotation_number}` : 'Nová nabídka'}
|
|
{isInvalidated && (
|
|
<span className="admin-badge admin-badge-danger" style={{ marginLeft: '0.75rem', verticalAlign: 'middle', fontSize: '0.75rem' }}>
|
|
Zneplatněna
|
|
</span>
|
|
)}
|
|
</h1>
|
|
{!isEdit && draftSavedAtLabel && (
|
|
<div className="offers-draft-indicator">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
<polyline points="20 6 9 17 4 12" />
|
|
</svg>
|
|
Koncept uložen {draftSavedAtLabel}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="admin-page-actions">
|
|
{isEdit && hasPermission('offers.export') && (
|
|
<button onClick={handlePdf} className="admin-btn admin-btn-secondary" disabled={pdfLoading}>
|
|
{pdfLoading ? (
|
|
<>
|
|
<div className="admin-spinner" style={{ width: 16, height: 16, borderWidth: 2 }} />
|
|
PDF...
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
<polyline points="14 2 14 8 20 8" />
|
|
</svg>
|
|
PDF
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
{isEdit && !isInvalidated && hasPermission('orders.create') && !orderInfo && (
|
|
<button onClick={() => { setCustomerOrderNumber(''); setOrderAttachment(null); setShowOrderModal(true) }} className="admin-btn admin-btn-secondary">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
<polyline points="14 2 14 8 20 8" />
|
|
<line x1="12" y1="11" x2="12" y2="17" />
|
|
<line x1="9" y1="14" x2="15" y2="14" />
|
|
</svg>
|
|
Vytvořit objednávku
|
|
</button>
|
|
)}
|
|
{isEdit && orderInfo && (
|
|
<Link to={`/orders/${orderInfo.id}`} className="admin-btn admin-btn-secondary">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
<polyline points="14 2 14 8 20 8" />
|
|
</svg>
|
|
Objednávka {orderInfo.order_number}
|
|
</Link>
|
|
)}
|
|
{isExpiredNotInvalidated && hasPermission('offers.edit') && (
|
|
<button onClick={() => setInvalidateConfirm(true)} className="admin-btn admin-btn-secondary">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
|
|
</svg>
|
|
Zneplatnit
|
|
</button>
|
|
)}
|
|
{!isInvalidated && (
|
|
<button onClick={handleSave} className="admin-btn admin-btn-primary" disabled={saving}>
|
|
{saving ? (
|
|
<>
|
|
<div className="admin-spinner" style={{ width: 16, height: 16, borderWidth: 2 }} />
|
|
Ukládání...
|
|
</>
|
|
) : 'Uložit'}
|
|
</button>
|
|
)}
|
|
{isEdit && hasPermission('offers.delete') && (
|
|
<button
|
|
onClick={() => setDeleteConfirm(true)}
|
|
className="admin-btn admin-btn-primary"
|
|
>
|
|
Smazat
|
|
</button>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Quotation Form */}
|
|
<motion.div
|
|
className={`offers-editor-section${isInvalidated ? ' offers-readonly' : ''}`}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.1 }}
|
|
>
|
|
<h3 className="admin-card-title">Základní údaje</h3>
|
|
<div className="admin-form">
|
|
<div className="offers-form-row-3">
|
|
<FormField label="Číslo nabídky">
|
|
<input
|
|
type="text"
|
|
value={form.quotation_number}
|
|
className="admin-form-input"
|
|
readOnly
|
|
style={{ backgroundColor: 'var(--bg-secondary)', cursor: 'default' }}
|
|
/>
|
|
</FormField>
|
|
<FormField label="Kód projektu">
|
|
<input
|
|
type="text"
|
|
value={form.project_code}
|
|
onChange={(e) => updateForm('project_code', e.target.value)}
|
|
className="admin-form-input"
|
|
placeholder="Volitelný kód projektu"
|
|
readOnly={isInvalidated}
|
|
/>
|
|
</FormField>
|
|
<OfferCustomerPicker
|
|
customers={customers}
|
|
customerId={form.customer_id}
|
|
customerName={form.customer_name}
|
|
onSelect={selectCustomer}
|
|
onClear={clearCustomer}
|
|
error={errors.customer_id}
|
|
readOnly={isInvalidated}
|
|
/>
|
|
</div>
|
|
|
|
<div className="admin-form-row">
|
|
<FormField label="Datum vytvoření" error={errors.created_at} required>
|
|
{isInvalidated ? (
|
|
<input type="text" value={form.created_at} className="admin-form-input" readOnly />
|
|
) : (
|
|
<AdminDatePicker
|
|
mode="date"
|
|
value={form.created_at}
|
|
onChange={(val) => {
|
|
updateForm('created_at', val)
|
|
setErrors(prev => ({ ...prev, created_at: undefined }))
|
|
}}
|
|
/>
|
|
)}
|
|
</FormField>
|
|
<FormField label="Platnost do" error={errors.valid_until} required>
|
|
{isInvalidated ? (
|
|
<input type="text" value={form.valid_until} className="admin-form-input" readOnly />
|
|
) : (
|
|
<AdminDatePicker
|
|
mode="date"
|
|
value={form.valid_until}
|
|
onChange={(val) => {
|
|
updateForm('valid_until', val)
|
|
setErrors(prev => ({ ...prev, valid_until: undefined }))
|
|
}}
|
|
/>
|
|
)}
|
|
</FormField>
|
|
</div>
|
|
|
|
<div className="admin-form-row">
|
|
<FormField label="Měna">
|
|
<select
|
|
value={form.currency}
|
|
onChange={(e) => updateForm('currency', e.target.value)}
|
|
className="admin-form-select"
|
|
disabled={isInvalidated}
|
|
>
|
|
<option value="EUR">EUR (€)</option>
|
|
<option value="USD">USD ($)</option>
|
|
<option value="CZK">CZK (Kč)</option>
|
|
<option value="GBP">GBP (£)</option>
|
|
</select>
|
|
</FormField>
|
|
<FormField label="Jazyk nabídky">
|
|
<select
|
|
value={form.language}
|
|
onChange={(e) => updateForm('language', e.target.value)}
|
|
className="admin-form-select"
|
|
disabled={isInvalidated}
|
|
>
|
|
<option value="EN">English</option>
|
|
<option value="CZ">Čeština</option>
|
|
</select>
|
|
</FormField>
|
|
</div>
|
|
|
|
<div className="offers-form-row-3">
|
|
<FormField label="Sazba DPH (%)">
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
|
<input
|
|
type="number"
|
|
value={form.vat_rate}
|
|
onChange={(e) => updateForm('vat_rate', parseFloat(e.target.value) || 0)}
|
|
className="admin-form-input"
|
|
step="0.1"
|
|
style={{ flex: 1 }}
|
|
readOnly={isInvalidated}
|
|
/>
|
|
<label className="admin-form-checkbox" style={{ whiteSpace: 'nowrap' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={form.apply_vat}
|
|
onChange={(e) => updateForm('apply_vat', e.target.checked)}
|
|
disabled={isInvalidated}
|
|
/>
|
|
<span>Účtovat DPH</span>
|
|
</label>
|
|
</div>
|
|
</FormField>
|
|
<FormField label="Směnný kurz">
|
|
<input
|
|
type="number"
|
|
value={form.exchange_rate}
|
|
onChange={(e) => updateForm('exchange_rate', e.target.value)}
|
|
className="admin-form-input"
|
|
placeholder="Volitelný"
|
|
step="0.0001"
|
|
readOnly={isInvalidated}
|
|
/>
|
|
</FormField>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
<OfferItemsSection
|
|
items={items}
|
|
setItems={setItems}
|
|
updateItem={updateItem}
|
|
addItem={addItem}
|
|
removeItem={removeItem}
|
|
itemTemplates={itemTemplates}
|
|
showItemTemplateMenu={showItemTemplateMenu}
|
|
setShowItemTemplateMenu={setShowItemTemplateMenu}
|
|
addItemFromTemplate={addItemFromTemplate}
|
|
totals={totals}
|
|
currency={form.currency}
|
|
applyVat={form.apply_vat}
|
|
vatRate={form.vat_rate}
|
|
itemsError={errors.items}
|
|
readOnly={isInvalidated}
|
|
/>
|
|
|
|
<OfferScopeSection
|
|
sections={sections}
|
|
addSection={addSection}
|
|
removeSection={removeSection}
|
|
updateSection={updateSection}
|
|
moveSection={moveSection}
|
|
scopeTemplates={scopeTemplates}
|
|
showScopeTemplateMenu={showScopeTemplateMenu}
|
|
setShowScopeTemplateMenu={setShowScopeTemplateMenu}
|
|
loadScopeTemplate={loadScopeTemplate}
|
|
form={form}
|
|
updateForm={updateForm}
|
|
readOnly={isInvalidated}
|
|
/>
|
|
|
|
<AnimatePresence>
|
|
{showOrderModal && (
|
|
<motion.div
|
|
className="admin-modal-overlay"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div className="admin-modal-backdrop" onClick={() => !creatingOrder && setShowOrderModal(false)} />
|
|
<motion.div
|
|
className="admin-modal"
|
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div className="admin-modal-header">
|
|
<h2 className="admin-modal-title">Vytvořit objednávku</h2>
|
|
</div>
|
|
<div className="admin-modal-body">
|
|
<div className="admin-form">
|
|
<FormField label="Číslo objednávky zákazníka" required>
|
|
<input
|
|
type="text"
|
|
value={customerOrderNumber}
|
|
onChange={e => setCustomerOrderNumber(e.target.value)}
|
|
onKeyDown={e => e.key === 'Enter' && !creatingOrder && handleCreateOrder()}
|
|
className="admin-form-input"
|
|
placeholder="Např. PO-2026-001"
|
|
autoFocus
|
|
/>
|
|
</FormField>
|
|
<FormField label="Příloha (PDF)">
|
|
{orderAttachment ? (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent-color)" strokeWidth="2">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
<polyline points="14 2 14 8 20 8" />
|
|
</svg>
|
|
<span style={{ fontSize: '0.875rem' }}>
|
|
{orderAttachment.name} <span className="text-tertiary">({(orderAttachment.size / 1024).toFixed(0)} KB)</span>
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOrderAttachment(null)}
|
|
className="admin-btn-icon"
|
|
title="Odebrat"
|
|
style={{ marginLeft: 'auto' }}
|
|
>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M18 6L6 18M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<label className="admin-btn admin-btn-secondary admin-btn-sm" style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '0.4rem' }}>
|
|
<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>
|
|
Vybrat soubor
|
|
<input
|
|
type="file"
|
|
accept="application/pdf"
|
|
onChange={e => setOrderAttachment(e.target.files[0] || null)}
|
|
style={{ display: 'none' }}
|
|
/>
|
|
</label>
|
|
)}
|
|
<small className="admin-form-hint" style={{ marginTop: '0.25rem' }}>Max 10 MB</small>
|
|
</FormField>
|
|
</div>
|
|
</div>
|
|
<div className="admin-modal-footer">
|
|
<button onClick={() => setShowOrderModal(false)} className="admin-btn admin-btn-secondary" disabled={creatingOrder}>
|
|
Zrušit
|
|
</button>
|
|
<button onClick={handleCreateOrder} className="admin-btn admin-btn-primary" disabled={creatingOrder || !customerOrderNumber.trim()}>
|
|
{creatingOrder ? 'Vytváření...' : 'Vytvořit'}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
<ConfirmModal
|
|
isOpen={invalidateConfirm}
|
|
onClose={() => 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}
|
|
/>
|
|
|
|
<ConfirmModal
|
|
isOpen={deleteConfirm}
|
|
onClose={() => 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}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|