Initial commit
This commit is contained in:
602
src/admin/pages/OffersTemplates.jsx
Normal file
602
src/admin/pages/OffersTemplates.jsx
Normal file
@@ -0,0 +1,602 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useAlert } from '../context/AlertContext'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import ConfirmModal from '../components/ConfirmModal'
|
||||
import Forbidden from '../components/Forbidden'
|
||||
import RichEditor from '../components/RichEditor'
|
||||
import useModalLock from '../hooks/useModalLock'
|
||||
|
||||
import apiFetch from '../utils/api'
|
||||
const API_BASE = '/api/admin'
|
||||
|
||||
export default function OffersTemplates() {
|
||||
const { hasPermission } = useAuth()
|
||||
const [activeTab, setActiveTab] = useState('items')
|
||||
|
||||
if (!hasPermission('offers.settings')) return <Forbidden />
|
||||
|
||||
return (
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Šablony</h1>
|
||||
<p className="admin-page-subtitle">Šablony položek a rozsahu projektu</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="offers-tabs">
|
||||
<button
|
||||
className={`offers-tab ${activeTab === 'items' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('items')}
|
||||
>
|
||||
Šablony položek
|
||||
</button>
|
||||
<button
|
||||
className={`offers-tab ${activeTab === 'scopes' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('scopes')}
|
||||
>
|
||||
Šablony rozsahu
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'items' ? <ItemTemplatesTab /> : <ScopeTemplatesTab />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Item Templates Tab ---
|
||||
|
||||
function ItemTemplatesTab() {
|
||||
const alert = useAlert()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [templates, setTemplates] = useState([])
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [editingTemplate, setEditingTemplate] = useState(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [form, setForm] = useState({ name: '', description: '', default_price: 0, category: '' })
|
||||
const [deleteConfirm, setDeleteConfirm] = useState({ show: false, template: null })
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
|
||||
useModalLock(showModal)
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/offers-templates.php?action=items`)
|
||||
if (response.status === 401) return
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
setTemplates(result.data.templates)
|
||||
}
|
||||
} catch {
|
||||
alert.error('Nepodařilo se načíst šablony')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [alert])
|
||||
|
||||
useEffect(() => { fetchData() }, [fetchData])
|
||||
|
||||
const openCreate = () => {
|
||||
setEditingTemplate(null)
|
||||
setForm({ name: '', description: '', default_price: 0, category: '' })
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const openEdit = (t) => {
|
||||
setEditingTemplate(t)
|
||||
setForm({ name: t.name || '', description: t.description || '', default_price: t.default_price || 0, category: t.category || '' })
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.name.trim()) {
|
||||
alert.error('Název šablony je povinný')
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
const body = editingTemplate ? { ...form, id: editingTemplate.id } : form
|
||||
const response = await apiFetch(`${API_BASE}/offers-templates.php?action=item`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
setShowModal(false)
|
||||
await new Promise(r => setTimeout(r, 300))
|
||||
alert.success(result.message)
|
||||
fetchData()
|
||||
} else {
|
||||
alert.error(result.error)
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteConfirm.template) return
|
||||
setDeleting(true)
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/offers-templates.php?action=item&id=${deleteConfirm.template.id}`, { method: 'DELETE' })
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
setDeleteConfirm({ show: false, template: null })
|
||||
alert.success(result.message)
|
||||
fetchData()
|
||||
} else {
|
||||
alert.error(result.error)
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="admin-card">
|
||||
<div className="admin-skeleton" style={{ gap: '1.25rem' }}>
|
||||
{[0, 1, 2, 3, 4].map(i => (
|
||||
<div key={i} className="admin-skeleton-row">
|
||||
<div className="admin-skeleton-line circle" />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className="admin-skeleton-line w-1/3" style={{ marginBottom: '0.5rem' }} />
|
||||
<div className="admin-skeleton-line w-1/4" style={{ height: '10px' }} />
|
||||
</div>
|
||||
<div className="admin-skeleton-line w-1/4" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="admin-card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h3 className="admin-card-title">Šablony položek ({templates.length})</h3>
|
||||
<button onClick={openCreate} className="admin-btn admin-btn-primary admin-btn-sm">
|
||||
<svg width="16" height="16" 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>
|
||||
Přidat
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-card-body">
|
||||
{templates.length === 0 ? (
|
||||
<div className="admin-empty-state"><p>Zatím žádné šablony položek.</p></div>
|
||||
) : (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Název</th>
|
||||
<th>Popis</th>
|
||||
<th>Cena</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{templates.map((t) => (
|
||||
<tr key={t.id}>
|
||||
<td style={{ fontWeight: 500 }}>{t.name}</td>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>{t.description || '—'}</td>
|
||||
<td>{Number(t.default_price).toFixed(2)}</td>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>{t.category || '—'}</td>
|
||||
<td>
|
||||
<div className="admin-table-actions">
|
||||
<button onClick={() => openEdit(t)} className="admin-btn-icon" title="Upravit" aria-label="Upravit">
|
||||
<svg width="18" height="18" 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>
|
||||
<button onClick={() => setDeleteConfirm({ show: true, template: t })} className="admin-btn-icon danger" title="Smazat" aria-label="Smazat">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Item Template Modal */}
|
||||
<AnimatePresence>
|
||||
{showModal && (
|
||||
<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={() => setShowModal(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">{editingTemplate ? 'Upravit šablonu' : 'Nová šablona položky'}</h2>
|
||||
</div>
|
||||
<div className="admin-modal-body">
|
||||
<div className="admin-form">
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label required">Název</label>
|
||||
<input type="text" value={form.name} onChange={(e) => setForm(p => ({ ...p, name: e.target.value }))} className="admin-form-input" />
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Popis</label>
|
||||
<textarea value={form.description} onChange={(e) => setForm(p => ({ ...p, description: e.target.value }))} className="admin-form-input" rows={2} />
|
||||
</div>
|
||||
<div className="admin-form-row">
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Výchozí cena</label>
|
||||
<input type="number" value={form.default_price} onChange={(e) => setForm(p => ({ ...p, default_price: parseFloat(e.target.value) || 0 }))} className="admin-form-input" step="0.01" />
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Kategorie</label>
|
||||
<input type="text" value={form.category} onChange={(e) => setForm(p => ({ ...p, category: e.target.value }))} className="admin-form-input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-modal-footer">
|
||||
<button type="button" onClick={() => setShowModal(false)} className="admin-btn admin-btn-secondary" disabled={saving}>Zrušit</button>
|
||||
<button type="button" onClick={handleSubmit} className="admin-btn admin-btn-primary" disabled={saving}>
|
||||
{saving && (<><div className="admin-spinner" style={{ width: 16, height: 16, borderWidth: 2 }} />Ukládání...</>)}
|
||||
{!saving && (editingTemplate ? 'Uložit' : 'Vytvořit')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={deleteConfirm.show}
|
||||
onClose={() => setDeleteConfirm({ show: false, template: null })}
|
||||
onConfirm={handleDelete}
|
||||
title="Smazat šablonu"
|
||||
message={`Opravdu chcete smazat šablonu "${deleteConfirm.template?.name}"?`}
|
||||
confirmText="Smazat"
|
||||
cancelText="Zrušit"
|
||||
type="danger"
|
||||
loading={deleting}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Scope Templates Tab ---
|
||||
|
||||
function ScopeTemplatesTab() {
|
||||
const alert = useAlert()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [templates, setTemplates] = useState([])
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [editingTemplate, setEditingTemplate] = useState(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [form, setForm] = useState({ name: '', sections: [] })
|
||||
const sectionKeyCounter = useRef(0)
|
||||
const [deleteConfirm, setDeleteConfirm] = useState({ show: false, template: null })
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
|
||||
useModalLock(showModal)
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/offers-templates.php?action=scopes`)
|
||||
if (response.status === 401) return
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
setTemplates(result.data.templates)
|
||||
}
|
||||
} catch {
|
||||
alert.error('Nepodařilo se načíst šablony')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [alert])
|
||||
|
||||
useEffect(() => { fetchData() }, [fetchData])
|
||||
|
||||
const openCreate = () => {
|
||||
setEditingTemplate(null)
|
||||
setForm({ name: '', sections: [{ _key: `sc-${++sectionKeyCounter.current}`, title: '', title_cz: '', content: '' }] })
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const openEdit = async (t) => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/offers-templates.php?action=scope_detail&id=${t.id}`)
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
setEditingTemplate(result.data)
|
||||
setForm({
|
||||
name: result.data.name || '',
|
||||
sections: result.data.sections?.length
|
||||
? result.data.sections.map(s => ({ _key: `sc-${++sectionKeyCounter.current}`, title: s.title || '', title_cz: s.title_cz || '', content: s.content || '' }))
|
||||
: [{ _key: `sc-${++sectionKeyCounter.current}`, title: '', title_cz: '', content: '' }]
|
||||
})
|
||||
setShowModal(true)
|
||||
}
|
||||
} catch {
|
||||
alert.error('Nepodařilo se načíst detail šablony')
|
||||
}
|
||||
}
|
||||
|
||||
const addSection = () => {
|
||||
setForm(prev => ({ ...prev, sections: [...prev.sections, { _key: `sc-${++sectionKeyCounter.current}`, title: '', title_cz: '', content: '' }] }))
|
||||
}
|
||||
|
||||
const removeSection = (index) => {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
sections: prev.sections.filter((_, i) => i !== index)
|
||||
}))
|
||||
}
|
||||
|
||||
const updateSection = (index, field, value) => {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
sections: prev.sections.map((s, i) => i === index ? { ...s, [field]: value } : s)
|
||||
}))
|
||||
}
|
||||
|
||||
const moveSection = (index, direction) => {
|
||||
setForm(prev => {
|
||||
const newSections = [...prev.sections]
|
||||
const targetIndex = index + direction
|
||||
if (targetIndex < 0 || targetIndex >= newSections.length) return prev
|
||||
;[newSections[index], newSections[targetIndex]] = [newSections[targetIndex], newSections[index]]
|
||||
return { ...prev, sections: newSections }
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.name.trim()) {
|
||||
alert.error('Název šablony je povinný')
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
const body = editingTemplate ? { ...form, id: editingTemplate.id } : form
|
||||
const response = await apiFetch(`${API_BASE}/offers-templates.php?action=scope`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
setShowModal(false)
|
||||
await new Promise(r => setTimeout(r, 300))
|
||||
alert.success(result.message)
|
||||
fetchData()
|
||||
} else {
|
||||
alert.error(result.error)
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteConfirm.template) return
|
||||
setDeleting(true)
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/offers-templates.php?action=scope&id=${deleteConfirm.template.id}`, { method: 'DELETE' })
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
setDeleteConfirm({ show: false, template: null })
|
||||
alert.success(result.message)
|
||||
fetchData()
|
||||
} else {
|
||||
alert.error(result.error)
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="admin-card">
|
||||
<div className="admin-skeleton" style={{ gap: '1.25rem' }}>
|
||||
{[0, 1, 2, 3, 4].map(i => (
|
||||
<div key={i} className="admin-skeleton-row">
|
||||
<div className="admin-skeleton-line circle" />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className="admin-skeleton-line w-1/3" style={{ marginBottom: '0.5rem' }} />
|
||||
<div className="admin-skeleton-line w-1/4" style={{ height: '10px' }} />
|
||||
</div>
|
||||
<div className="admin-skeleton-line w-1/4" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="admin-card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h3 className="admin-card-title">Šablony rozsahu ({templates.length})</h3>
|
||||
<button onClick={openCreate} className="admin-btn admin-btn-primary admin-btn-sm">
|
||||
<svg width="16" height="16" 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>
|
||||
Přidat
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-card-body">
|
||||
{templates.length === 0 ? (
|
||||
<div className="admin-empty-state"><p>Zatím žádné šablony rozsahu.</p></div>
|
||||
) : (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Název</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{templates.map((t) => (
|
||||
<tr key={t.id}>
|
||||
<td style={{ fontWeight: 500 }}>{t.name}</td>
|
||||
<td>
|
||||
<div className="admin-table-actions">
|
||||
<button onClick={() => openEdit(t)} className="admin-btn-icon" title="Upravit" aria-label="Upravit">
|
||||
<svg width="18" height="18" 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>
|
||||
<button onClick={() => setDeleteConfirm({ show: true, template: t })} className="admin-btn-icon danger" title="Smazat" aria-label="Smazat">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Scope Template Modal (large) */}
|
||||
<AnimatePresence>
|
||||
{showModal && (
|
||||
<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={() => setShowModal(false)} />
|
||||
<motion.div className="admin-modal admin-modal-lg" 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">{editingTemplate ? 'Upravit šablonu rozsahu' : 'Nová šablona rozsahu'}</h2>
|
||||
</div>
|
||||
<div className="admin-modal-body">
|
||||
<div className="admin-form">
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label required">Název šablony</label>
|
||||
<input type="text" value={form.name} onChange={(e) => setForm(p => ({ ...p, name: e.target.value }))} className="admin-form-input" />
|
||||
</div>
|
||||
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label" style={{ marginBottom: '0.5rem' }}>Sekce</label>
|
||||
<div className="offers-scope-list">
|
||||
{form.sections.map((section, index) => (
|
||||
<div key={section._key} className="offers-scope-section">
|
||||
<div className="offers-scope-section-header">
|
||||
<span className="offers-scope-number">{index + 1}.</span>
|
||||
<span className="offers-scope-title">{section.title || section.title_cz || `Sekce ${index + 1}`}</span>
|
||||
<div className="offers-scope-actions">
|
||||
<button type="button" onClick={() => moveSection(index, -1)} disabled={index === 0} className="admin-btn-icon" title="Posunout nahoru" aria-label="Posunout nahoru">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 15l-6-6-6 6" /></svg>
|
||||
</button>
|
||||
<button type="button" onClick={() => moveSection(index, 1)} disabled={index === form.sections.length - 1} className="admin-btn-icon" title="Posunout dolů" aria-label="Posunout dolů">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M6 9l6 6 6-6" /></svg>
|
||||
</button>
|
||||
{form.sections.length > 1 && (
|
||||
<button type="button" onClick={() => removeSection(index)} className="admin-btn-icon danger" title="Odebrat" aria-label="Odebrat">
|
||||
<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>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-form">
|
||||
<div className="admin-form-row">
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">
|
||||
<span className="offers-lang-badge">EN</span>
|
||||
Název sekce
|
||||
</label>
|
||||
<input type="text" value={section.title} onChange={(e) => updateSection(index, 'title', e.target.value)} className="admin-form-input" placeholder="Název sekce (anglicky)" />
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">
|
||||
<span className="offers-lang-badge offers-lang-badge-cz">CZ</span>
|
||||
Název sekce
|
||||
</label>
|
||||
<input type="text" value={section.title_cz} onChange={(e) => updateSection(index, 'title_cz', e.target.value)} className="admin-form-input" placeholder="Název sekce (česky)" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Obsah</label>
|
||||
<RichEditor
|
||||
value={section.content}
|
||||
onChange={(val) => updateSection(index, 'content', val)}
|
||||
placeholder="Obsah sekce..."
|
||||
minHeight="150px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: '0.75rem' }}>
|
||||
<button type="button" onClick={addSection} className="admin-btn admin-btn-secondary admin-btn-sm">
|
||||
+ Přidat sekci
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-modal-footer">
|
||||
<button type="button" onClick={() => setShowModal(false)} className="admin-btn admin-btn-secondary" disabled={saving}>Zrušit</button>
|
||||
<button type="button" onClick={handleSubmit} className="admin-btn admin-btn-primary" disabled={saving}>
|
||||
{saving && (<><div className="admin-spinner" style={{ width: 16, height: 16, borderWidth: 2 }} />Ukládání...</>)}
|
||||
{!saving && (editingTemplate ? 'Uložit' : 'Vytvořit')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={deleteConfirm.show}
|
||||
onClose={() => setDeleteConfirm({ show: false, template: null })}
|
||||
onConfirm={handleDelete}
|
||||
title="Smazat šablonu"
|
||||
message={`Opravdu chcete smazat šablonu "${deleteConfirm.template?.name}"?`}
|
||||
confirmText="Smazat"
|
||||
cancelText="Zrušit"
|
||||
type="danger"
|
||||
loading={deleting}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user