610 lines
24 KiB
JavaScript
610 lines
24 KiB
JavaScript
import { useState, useEffect, useMemo } from 'react'
|
|
import DOMPurify from 'dompurify'
|
|
import { useAlert } from '../context/AlertContext'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
|
import { motion } from 'framer-motion'
|
|
import ConfirmModal from '../components/ConfirmModal'
|
|
import Forbidden from '../components/Forbidden'
|
|
|
|
import apiFetch from '../utils/api'
|
|
import { formatCurrency, formatDate } from '../utils/formatters'
|
|
const API_BASE = '/api/admin'
|
|
|
|
const STATUS_LABELS = {
|
|
prijata: 'Přijatá',
|
|
v_realizaci: 'V realizaci',
|
|
dokoncena: 'Dokončená',
|
|
stornovana: 'Stornována'
|
|
}
|
|
|
|
const STATUS_CLASSES = {
|
|
prijata: 'admin-badge-order-prijata',
|
|
v_realizaci: 'admin-badge-order-realizace',
|
|
dokoncena: 'admin-badge-order-dokoncena',
|
|
stornovana: 'admin-badge-order-stornovana'
|
|
}
|
|
|
|
const TRANSITION_LABELS = {
|
|
v_realizaci: 'Zahájit realizaci',
|
|
dokoncena: 'Dokončit'
|
|
}
|
|
|
|
const TRANSITION_CLASSES = {
|
|
v_realizaci: 'admin-btn admin-btn-primary',
|
|
dokoncena: 'admin-btn admin-btn-primary'
|
|
}
|
|
|
|
export default function OrderDetail() {
|
|
const { id } = useParams()
|
|
const alert = useAlert()
|
|
const { hasPermission } = useAuth()
|
|
const navigate = useNavigate()
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
const [order, setOrder] = useState(null)
|
|
const [notes, setNotes] = useState('')
|
|
const [saving, setSaving] = useState(false)
|
|
const [statusChanging, setStatusChanging] = useState(null)
|
|
const [statusConfirm, setStatusConfirm] = useState({ show: false, status: null })
|
|
const [editingNumber, setEditingNumber] = useState(false)
|
|
const [orderNumber, setOrderNumber] = useState('')
|
|
const [savingNumber, setSavingNumber] = useState(false)
|
|
const [attachmentLoading, setAttachmentLoading] = useState(false)
|
|
const [deleteConfirm, setDeleteConfirm] = useState(false)
|
|
const [deleting, setDeleting] = useState(false)
|
|
|
|
const fetchDetail = async () => {
|
|
try {
|
|
const response = await apiFetch(`${API_BASE}/orders.php?action=detail&id=${id}`)
|
|
if (response.status === 401) return
|
|
const result = await response.json()
|
|
if (result.success) {
|
|
setOrder(result.data)
|
|
setNotes(result.data.notes || '')
|
|
} else {
|
|
alert.error(result.error || 'Nepodařilo se načíst objednávku')
|
|
navigate('/orders')
|
|
}
|
|
} catch {
|
|
alert.error('Chyba připojení')
|
|
navigate('/orders')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchDetail()
|
|
}, [id]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const totals = useMemo(() => {
|
|
if (!order?.items) return { subtotal: 0, vatAmount: 0, total: 0 }
|
|
const subtotal = order.items.reduce((sum, item) => {
|
|
if (Number(item.is_included_in_total)) {
|
|
return sum + (Number(item.quantity) || 0) * (Number(item.unit_price) || 0)
|
|
}
|
|
return sum
|
|
}, 0)
|
|
const vatAmount = Number(order.apply_vat) ? subtotal * ((Number(order.vat_rate) || 0) / 100) : 0
|
|
return { subtotal, vatAmount, total: subtotal + vatAmount }
|
|
}, [order])
|
|
|
|
if (!hasPermission('orders.view')) return <Forbidden />
|
|
|
|
const handleStatusChange = async () => {
|
|
if (!statusConfirm.status) return
|
|
setStatusChanging(statusConfirm.status)
|
|
setStatusConfirm({ show: false, status: null })
|
|
try {
|
|
const response = await apiFetch(`${API_BASE}/orders.php?id=${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status: statusConfirm.status })
|
|
})
|
|
const result = await response.json()
|
|
if (result.success) {
|
|
alert.success(result.message || 'Stav byl změněn')
|
|
fetchDetail()
|
|
} else {
|
|
alert.error(result.error || 'Nepodařilo se změnit stav')
|
|
}
|
|
} catch {
|
|
alert.error('Chyba připojení')
|
|
} finally {
|
|
setStatusChanging(null)
|
|
}
|
|
}
|
|
|
|
const handleStartEditNumber = () => {
|
|
setOrderNumber(order.order_number)
|
|
setEditingNumber(true)
|
|
}
|
|
|
|
const handleSaveNumber = async () => {
|
|
const trimmed = orderNumber.trim()
|
|
if (!trimmed) return
|
|
if (trimmed === order.order_number) {
|
|
setEditingNumber(false)
|
|
return
|
|
}
|
|
setSavingNumber(true)
|
|
try {
|
|
const response = await apiFetch(`${API_BASE}/orders.php?id=${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ order_number: trimmed })
|
|
})
|
|
const result = await response.json()
|
|
if (result.success) {
|
|
alert.success('Číslo objednávky bylo změněno')
|
|
setEditingNumber(false)
|
|
fetchDetail()
|
|
} else {
|
|
alert.error(result.error || 'Nepodařilo se změnit číslo')
|
|
}
|
|
} catch {
|
|
alert.error('Chyba připojení')
|
|
} finally {
|
|
setSavingNumber(false)
|
|
}
|
|
}
|
|
|
|
const handleSaveNotes = async () => {
|
|
setSaving(true)
|
|
try {
|
|
const response = await apiFetch(`${API_BASE}/orders.php?id=${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ notes: notes })
|
|
})
|
|
const result = await response.json()
|
|
if (result.success) {
|
|
alert.success('Poznámky byly uloženy')
|
|
} else {
|
|
alert.error(result.error || 'Nepodařilo se uložit poznámky')
|
|
}
|
|
} catch {
|
|
alert.error('Chyba připojení')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const handleViewAttachment = async () => {
|
|
const newWindow = window.open('', '_blank')
|
|
setAttachmentLoading(true)
|
|
try {
|
|
const response = await apiFetch(`${API_BASE}/orders.php?action=attachment&id=${id}`)
|
|
if (!response.ok) {
|
|
newWindow.close()
|
|
alert.error('Nepodařilo se stáhnout přílohu')
|
|
return
|
|
}
|
|
const blob = await response.blob()
|
|
const url = URL.createObjectURL(blob)
|
|
newWindow.location.href = url
|
|
setTimeout(() => URL.revokeObjectURL(url), 60000)
|
|
} catch {
|
|
newWindow.close()
|
|
alert.error('Chyba připojení')
|
|
} finally {
|
|
setAttachmentLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleDelete = async () => {
|
|
setDeleting(true)
|
|
try {
|
|
const response = await apiFetch(`${API_BASE}/orders.php?id=${id}`, { method: 'DELETE' })
|
|
const result = await response.json()
|
|
if (result.success) {
|
|
alert.success(result.message || 'Objednávka byla smazána')
|
|
navigate('/orders')
|
|
} else {
|
|
alert.error(result.error || 'Nepodařilo se smazat objednávku')
|
|
}
|
|
} catch {
|
|
alert.error('Chyba připojení')
|
|
} finally {
|
|
setDeleting(false)
|
|
setDeleteConfirm(false)
|
|
}
|
|
}
|
|
|
|
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>
|
|
)
|
|
}
|
|
|
|
if (!order) return null
|
|
|
|
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="/orders" 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" style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
|
{editingNumber ? (
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
Objednávka
|
|
<input
|
|
type="text"
|
|
value={orderNumber}
|
|
onChange={(e) => setOrderNumber(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') handleSaveNumber()
|
|
if (e.key === 'Escape') setEditingNumber(false)
|
|
}}
|
|
className="admin-form-input"
|
|
style={{ width: '10rem', fontSize: '1rem', padding: '0.25rem 0.5rem', height: 'auto' }}
|
|
autoFocus
|
|
disabled={savingNumber}
|
|
/>
|
|
<button onClick={handleSaveNumber} className="admin-btn-icon" title="Uložit" aria-label="Uložit" disabled={savingNumber}>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent-color)" strokeWidth="2">
|
|
<polyline points="20 6 9 17 4 12" />
|
|
</svg>
|
|
</button>
|
|
<button onClick={() => setEditingNumber(false)} className="admin-btn-icon" title="Zrušit" aria-label="Zrušit" disabled={savingNumber}>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M18 6L6 18M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</span>
|
|
) : (
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
Objednávka {order.order_number}
|
|
{hasPermission('orders.edit') && (
|
|
<button onClick={handleStartEditNumber} className="admin-btn-icon" title="Změnit číslo" aria-label="Změnit číslo" style={{ opacity: 0.5 }}>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</span>
|
|
)}
|
|
<span className={`admin-badge ${STATUS_CLASSES[order.status] || ''}`}>
|
|
{STATUS_LABELS[order.status] || order.status}
|
|
</span>
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
<div className="admin-page-actions">
|
|
{order.invoice ? (
|
|
<Link to={`/invoices/${order.invoice.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>
|
|
Faktura {order.invoice.invoice_number}
|
|
</Link>
|
|
) : (
|
|
hasPermission('invoices.create') && order.status === 'dokoncena' && (
|
|
<Link to={`/invoices/new?fromOrder=${order.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>
|
|
Vytvořit fakturu
|
|
</Link>
|
|
)
|
|
)}
|
|
{hasPermission('orders.edit') && order.valid_transitions?.filter(s => s !== 'stornovana').length > 0 && (
|
|
order.valid_transitions.filter(s => s !== 'stornovana').map(status => (
|
|
<button
|
|
key={status}
|
|
onClick={() => setStatusConfirm({ show: true, status })}
|
|
className={TRANSITION_CLASSES[status] || 'admin-btn admin-btn-secondary'}
|
|
disabled={statusChanging === status}
|
|
>
|
|
{statusChanging === status ? (
|
|
<div className="admin-spinner" style={{ width: 14, height: 14, borderWidth: 2 }} />
|
|
) : (
|
|
TRANSITION_LABELS[status] || status
|
|
)}
|
|
</button>
|
|
))
|
|
)}
|
|
{hasPermission('orders.delete') && (
|
|
<button
|
|
onClick={() => setDeleteConfirm(true)}
|
|
className="admin-btn admin-btn-primary"
|
|
>
|
|
Smazat
|
|
</button>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Info card */}
|
|
<motion.div
|
|
className="admin-card"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.1 }}
|
|
>
|
|
<div className="admin-card-body">
|
|
<h3 className="admin-card-title">Informace</h3>
|
|
<div className="admin-form-row" style={{ marginBottom: '0.5rem' }}>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Nabídka</label>
|
|
<div>
|
|
<Link to={`/offers/${order.quotation_id}`} className="link-accent">
|
|
{order.quotation_number}
|
|
</Link>
|
|
{order.project_code && (
|
|
<span className="text-tertiary" style={{ marginLeft: '0.5rem' }}>({order.project_code})</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Projekt</label>
|
|
<div>
|
|
{order.project ? (
|
|
<Link to={`/projects/${order.project.id}`} className="link-accent">
|
|
{order.project.project_number} — {order.project.name}
|
|
</Link>
|
|
) : '—'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="admin-form-row admin-form-row-3" style={{ marginBottom: '0.5rem' }}>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Zákazník</label>
|
|
<div style={{ fontWeight: 500 }}>{order.customer_name || '—'}</div>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Číslo obj. zákazníka</label>
|
|
<div>{order.customer_order_number || '—'}</div>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Měna</label>
|
|
<div>{order.currency}</div>
|
|
</div>
|
|
</div>
|
|
<div className="admin-form-row admin-form-row-3" style={{ marginBottom: '0.5rem' }}>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Datum vytvoření</label>
|
|
<div>{formatDate(order.created_at)}</div>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Příloha</label>
|
|
<div>
|
|
{order.attachment_name ? (
|
|
<button
|
|
onClick={handleViewAttachment}
|
|
className="admin-btn admin-btn-secondary admin-btn-sm"
|
|
style={{ display: 'inline-flex', alignItems: 'center', gap: '0.4rem' }}
|
|
disabled={attachmentLoading}
|
|
>
|
|
{attachmentLoading ? (
|
|
<div className="admin-spinner" style={{ width: 14, height: 14, borderWidth: 2 }} />
|
|
) : (
|
|
<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>
|
|
)}
|
|
{order.attachment_name}
|
|
</button>
|
|
) : '—'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Items (read-only) */}
|
|
<motion.div
|
|
className="admin-card"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.2 }}
|
|
>
|
|
<div className="admin-card-body">
|
|
<h3 className="admin-card-title">Položky</h3>
|
|
{order.items?.length > 0 ? (
|
|
<div className="admin-table-responsive">
|
|
<table className="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: '2.5rem', textAlign: 'center' }}>#</th>
|
|
<th>Popis</th>
|
|
<th style={{ width: '5.5rem', textAlign: 'center' }}>Množství</th>
|
|
<th style={{ width: '5.5rem', textAlign: 'center' }}>Jednotka</th>
|
|
<th style={{ width: '8rem', textAlign: 'right', whiteSpace: 'nowrap' }}>Jedn. cena</th>
|
|
<th style={{ width: '4rem', textAlign: 'center' }}>V ceně</th>
|
|
<th style={{ width: '9rem', textAlign: 'right', whiteSpace: 'nowrap' }}>Celkem</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{order.items.map((item, index) => {
|
|
const lineTotal = (Number(item.quantity) || 0) * (Number(item.unit_price) || 0)
|
|
return (
|
|
<tr key={item.id || index}>
|
|
<td style={{ color: 'var(--text-tertiary)', textAlign: 'center', fontWeight: 500 }}>{index + 1}</td>
|
|
<td>
|
|
<div style={{ fontWeight: 500 }}>{item.description || '—'}</div>
|
|
{item.item_description && (
|
|
<div style={{ fontSize: '0.8rem', color: 'var(--text-tertiary)', marginTop: '0.25rem' }}>{item.item_description}</div>
|
|
)}
|
|
</td>
|
|
<td style={{ textAlign: 'center' }}>{item.quantity}</td>
|
|
<td style={{ textAlign: 'center' }}>{item.unit || '—'}</td>
|
|
<td className="admin-mono" style={{ textAlign: 'right', whiteSpace: 'nowrap' }}>{formatCurrency(item.unit_price, order.currency)}</td>
|
|
<td style={{ textAlign: 'center' }}>{Number(item.is_included_in_total) ? 'Ano' : 'Ne'}</td>
|
|
<td className="admin-mono" style={{ textAlign: 'right', fontWeight: 600, whiteSpace: 'nowrap' }}>{formatCurrency(lineTotal, order.currency)}</td>
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : (
|
|
<p style={{ color: 'var(--text-tertiary)' }}>Žádné položky.</p>
|
|
)}
|
|
|
|
{/* Totals */}
|
|
<div className="offers-totals-summary">
|
|
<div className="offers-totals-row">
|
|
<span>Mezisoučet:</span>
|
|
<span>{formatCurrency(totals.subtotal, order.currency)}</span>
|
|
</div>
|
|
{Number(order.apply_vat) > 0 && (
|
|
<div className="offers-totals-row">
|
|
<span>DPH ({order.vat_rate}%):</span>
|
|
<span>{formatCurrency(totals.vatAmount, order.currency)}</span>
|
|
</div>
|
|
)}
|
|
<div className="offers-totals-row offers-totals-total">
|
|
<span>Celkem k úhradě:</span>
|
|
<span>{formatCurrency(totals.total, order.currency)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Sections (read-only) */}
|
|
{order.sections?.length > 0 && (
|
|
<motion.div
|
|
className="admin-card"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.3 }}
|
|
>
|
|
<div className="admin-card-body">
|
|
<h3 className="admin-card-title">Rozsah projektu</h3>
|
|
{order.scope_title && (
|
|
<div style={{ fontWeight: 500, marginBottom: '0.5rem' }}>{order.scope_title}</div>
|
|
)}
|
|
{order.scope_description && (
|
|
<div style={{ color: 'var(--text-secondary)', marginBottom: '1rem' }}>{order.scope_description}</div>
|
|
)}
|
|
<div className="offers-scope-list">
|
|
{order.sections.map((section, index) => (
|
|
<div key={section.id || index} className="offers-scope-section" style={{ cursor: 'default' }}>
|
|
<div className="offers-scope-section-header">
|
|
<span className="offers-scope-number">{index + 1}.</span>
|
|
<span className="offers-scope-title">{(order.language === 'CZ' ? (section.title_cz || section.title) : (section.title || section.title_cz)) || `Sekce ${index + 1}`}</span>
|
|
</div>
|
|
{section.content && (
|
|
<div
|
|
className="offers-scope-content rich-text-view"
|
|
style={{ padding: '1rem' }}
|
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(section.content) }}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Notes (editable) */}
|
|
<motion.div
|
|
className="admin-card"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.4 }}
|
|
>
|
|
<div className="admin-card-body">
|
|
<h3 className="admin-card-title">Poznámky</h3>
|
|
<div className="admin-form-group">
|
|
<textarea
|
|
value={notes}
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
className="admin-form-input"
|
|
rows={4}
|
|
placeholder="Interní poznámky k objednávce..."
|
|
disabled={!hasPermission('orders.edit')}
|
|
/>
|
|
</div>
|
|
{hasPermission('orders.edit') && (
|
|
<div style={{ marginTop: '0.5rem' }}>
|
|
<button
|
|
onClick={handleSaveNotes}
|
|
className="admin-btn admin-btn-secondary admin-btn-sm"
|
|
disabled={saving}
|
|
>
|
|
{saving ? 'Ukládání...' : 'Uložit poznámky'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Status change confirmation */}
|
|
<ConfirmModal
|
|
isOpen={statusConfirm.show}
|
|
onClose={() => setStatusConfirm({ show: false, status: null })}
|
|
onConfirm={handleStatusChange}
|
|
title="Změnit stav objednávky"
|
|
message={`Opravdu chcete změnit stav objednávky "${order.order_number}" na "${STATUS_LABELS[statusConfirm.status]}"?${statusConfirm.status === 'dokoncena' ? ' Projekt bude automaticky dokončen.' : ''}`}
|
|
confirmText={TRANSITION_LABELS[statusConfirm.status] || 'Potvrdit'}
|
|
cancelText="Zrušit"
|
|
type="default"
|
|
/>
|
|
|
|
{/* Delete confirmation */}
|
|
<ConfirmModal
|
|
isOpen={deleteConfirm}
|
|
onClose={() => setDeleteConfirm(false)}
|
|
onConfirm={handleDelete}
|
|
title="Smazat objednávku"
|
|
message={`Opravdu chcete smazat objednávku "${order.order_number}"? Bude smazán i přidružený projekt. Tato akce je nevratná.`}
|
|
confirmText="Smazat"
|
|
cancelText="Zrušit"
|
|
type="danger"
|
|
loading={deleting}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|