Files
app/src/admin/pages/Invoices.jsx
Simon 10fbb9ebc7 refactor: CSS utility tridy + slouceni badge souboru
- pridano 20 utility trid (flex-1, mb-2, text-right, fw-500, admin-spinner-sm, atd.)
- nahrazeno ~100 opakovanych inline stylu ve 39 JSX souborech
- slouceno leave.css, orders.css, projects.css do admin.css (status badges)
- bundle size: 228.91 -> 228.43 kB (-0.48 kB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:27:15 +01:00

690 lines
30 KiB
JavaScript

import { useState, useEffect, useCallback, useRef, lazy, Suspense } from 'react'
import { useAlert } from '../context/AlertContext'
import { useAuth } from '../context/AuthContext'
import { Link } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import ConfirmModal from '../components/ConfirmModal'
import Forbidden from '../components/Forbidden'
import apiFetch from '../utils/api'
import { formatCurrency, formatDate, czechPlural } from '../utils/formatters'
import SortIcon from '../components/SortIcon'
import useTableSort from '../hooks/useTableSort'
import useListData from '../hooks/useListData'
import Pagination from '../components/Pagination'
const ReceivedInvoices = lazy(() => import('./ReceivedInvoices'))
const API_BASE = '/api/admin'
const DRAFT_KEY = 'boha_invoice_draft'
const MONTH_NAMES = [
'leden', 'únor', 'březen', 'duben', 'květen', 'červen',
'červenec', 'srpen', 'září', 'říjen', 'listopad', 'prosinec'
]
function formatMultiCurrency(amounts) {
if (!amounts || amounts.length === 0) return '0 Kč'
return amounts.map(a => formatCurrency(a.amount, a.currency)).join(' · ')
}
function formatCzkWithDetail(amounts, totalCzk) {
if (!amounts || amounts.length === 0) return { value: '0 Kč', detail: null }
const hasForeign = amounts.some(a => a.currency !== 'CZK')
if (hasForeign && totalCzk !== null && totalCzk !== undefined) {
return {
value: formatCurrency(totalCzk, 'CZK'),
detail: formatMultiCurrency(amounts),
}
}
return { value: formatMultiCurrency(amounts), detail: null }
}
const STATUS_LABELS = {
issued: 'Vystavena',
paid: 'Zaplacena',
overdue: 'Po splatnosti'
}
const STATUS_CLASSES = {
issued: 'admin-badge-invoice-issued',
paid: 'admin-badge-invoice-paid',
overdue: 'admin-badge-invoice-overdue'
}
const STATUS_FILTERS = [
{ value: '', label: 'Vše' },
{ value: 'issued', label: 'Vystavené' },
{ value: 'paid', label: 'Zaplacené' },
{ value: 'overdue', label: 'Po splatnosti' }
]
export default function Invoices() {
const alert = useAlert()
const { hasPermission } = useAuth()
const [activeTab, setActiveTab] = useState('issued')
const [receivedUploadOpen, setReceivedUploadOpen] = useState(false)
const { sort, order, handleSort, activeSort } = useTableSort('invoice_number')
const [search, setSearch] = useState('')
const [page, setPage] = useState(1)
const [statusFilter, setStatusFilter] = useState('')
const now = new Date()
const [statsMonth, setStatsMonth] = useState(now.getMonth() + 1)
const [statsYear, setStatsYear] = useState(now.getFullYear())
const [stats, setStats] = useState(null)
const [statsLoading, setStatsLoading] = useState(true)
const hasLoadedOnce = useRef(false)
const slideDirection = useRef(0)
const [slideKey, setSlideKey] = useState(0)
const isCurrentMonth = statsMonth === now.getMonth() + 1 && statsYear === now.getFullYear()
const monthLabel = `${MONTH_NAMES[statsMonth - 1]} ${statsYear}`
const fetchStats = useCallback(async () => {
setStatsLoading(true)
try {
const res = await apiFetch(`${API_BASE}/invoices.php?action=stats&month=${statsMonth}&year=${statsYear}`)
const data = await res.json()
if (data.success) {
setStats(data.data)
hasLoadedOnce.current = true
setSlideKey(k => k + 1)
}
} catch { /* ignore */ } finally {
setStatsLoading(false)
}
}, [statsMonth, statsYear])
useEffect(() => { fetchStats() }, [fetchStats])
const prevMonth = () => {
slideDirection.current = -1
if (statsMonth === 1) {
setStatsMonth(12)
setStatsYear(y => y - 1)
} else {
setStatsMonth(m => m - 1)
}
}
const nextMonth = () => {
if (isCurrentMonth) return
slideDirection.current = 1
if (statsMonth === 12) {
setStatsMonth(1)
setStatsYear(y => y + 1)
} else {
setStatsMonth(m => m + 1)
}
}
const [deleteConfirm, setDeleteConfirm] = useState({ show: false, invoice: null })
const [deleting, setDeleting] = useState(false)
const [pdfLoading, setPdfLoading] = useState(null)
const [langModal, setLangModal] = useState(null)
const [draft, setDraft] = useState(null)
useEffect(() => {
try {
const raw = localStorage.getItem(DRAFT_KEY)
if (!raw) return
const parsed = JSON.parse(raw)
if (parsed && parsed.form && Array.isArray(parsed.items)) {
setDraft(parsed)
}
} catch { /* ignore */ }
}, [])
const discardDraft = () => {
try { localStorage.removeItem(DRAFT_KEY) } catch { /* ignore */ }
setDraft(null)
}
const { items: invoices, loading, pagination, refetch: fetchData } = useListData('invoices.php', {
dataKey: 'invoices', search, sort, order, page,
extraParams: statusFilter ? { status: statusFilter } : {},
errorMsg: 'Nepodařilo se načíst faktury'
})
if (!hasPermission('invoices.view')) return <Forbidden />
const handleDelete = async () => {
if (!deleteConfirm.invoice) return
setDeleting(true)
try {
const response = await apiFetch(`${API_BASE}/invoices.php?id=${deleteConfirm.invoice.id}`, {
method: 'DELETE'
})
const result = await response.json()
if (result.success) {
setDeleteConfirm({ show: false, invoice: null })
alert.success(result.message || 'Faktura byla smazána')
fetchData()
fetchStats()
} else {
alert.error(result.error || 'Nepodařilo se smazat fakturu')
}
} catch {
alert.error('Chyba připojení')
} finally {
setDeleting(false)
}
}
const toggleStatus = async (inv) => {
if (inv.status === 'paid') return
try {
const res = await apiFetch(`${API_BASE}/invoices.php?id=${inv.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'paid' }),
})
const data = await res.json()
if (data.success) {
alert.success('Faktura označena jako zaplacená')
fetchData()
fetchStats()
} else {
alert.error(data.error || 'Nepodařilo se změnit stav')
}
} catch {
alert.error('Chyba připojení')
}
}
const handlePdf = async (inv, lang = 'cs') => {
if (pdfLoading) return
setLangModal(null)
setPdfLoading(inv.id)
try {
const response = await apiFetch(`${API_BASE}/invoices-pdf.php?id=${inv.id}&lang=${encodeURIComponent(lang)}`)
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(null)
}
}
if (loading) {
return (
<div>
<div className="admin-skeleton" style={{ padding: 0, gap: '1.5rem' }}>
<div className="admin-skeleton-row" style={{ justifyContent: 'space-between' }}>
<div>
<div className="admin-skeleton-line h-8" style={{ width: '200px', marginBottom: '0.5rem' }} />
<div className="admin-skeleton-line" style={{ width: '140px' }} />
</div>
<div className="admin-skeleton-line h-10" style={{ width: '140px', borderRadius: '8px' }} />
</div>
<div className="dash-kpi-grid dash-kpi-4">
{[0, 1, 2, 3].map(i => (
<div key={i} className="admin-stat-card">
<div className="admin-skeleton-line" style={{ width: '60%', height: '11px', marginBottom: '0.5rem' }} />
<div className="admin-skeleton-line" style={{ width: '40%', height: '28px', marginBottom: '0.5rem' }} />
<div className="admin-skeleton-line" style={{ width: '50%', height: '12px' }} />
</div>
))}
</div>
<div className="admin-card">
<div className="admin-skeleton" style={{ gap: '1rem' }}>
{[0, 1, 2, 3, 4].map(i => (
<div key={i} className="admin-skeleton-row">
<div className="admin-skeleton-line" style={{ width: '80px' }} />
<div className="admin-skeleton-line w-1/4" />
<div className="admin-skeleton-line" style={{ width: '70px' }} />
<div className="admin-skeleton-line" style={{ width: '90px' }} />
<div className="admin-skeleton-line" style={{ width: '90px' }} />
<div className="admin-skeleton-line" style={{ width: '100px' }} />
</div>
))}
</div>
</div>
</div>
</div>
)
}
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">Faktury</h1>
<p className="admin-page-subtitle">
{pagination?.total ?? invoices.length} {czechPlural(pagination?.total ?? invoices.length, 'faktura', 'faktury', 'faktur')}
</p>
</div>
{hasPermission('invoices.create') && (
<div className="admin-page-actions">
{activeTab === 'received' ? (
<button className="admin-btn admin-btn-primary" onClick={() => setReceivedUploadOpen(true)}>
<svg width="18" height="18" 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>
Nahrát faktury
</button>
) : (
<Link to="/invoices/new" className="admin-btn admin-btn-primary">
<svg width="18" height="18" 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>
Nová faktura
</Link>
)}
</div>
)}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<div className="invoice-month-nav">
<button className="invoice-month-btn" onClick={prevMonth} aria-label="Předchozí měsíc">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="15 18 9 12 15 6" /></svg>
</button>
<span>{monthLabel}</span>
<button className="invoice-month-btn" onClick={nextMonth} disabled={isCurrentMonth} aria-label="Následující měsíc">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="9 18 15 12 9 6" /></svg>
</button>
</div>
<div className="offers-tabs mb-4" style={{ justifyContent: 'center' }}>
<button className={`offers-tab ${activeTab === 'issued' ? 'active' : ''}`} onClick={() => setActiveTab('issued')}>
Vydané
</button>
<button className={`offers-tab ${activeTab === 'received' ? 'active' : ''}`} onClick={() => setActiveTab('received')}>
Přijaté
</button>
</div>
</motion.div>
{activeTab === 'received' ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.15 }}
>
<Suspense fallback={
<div className="dash-kpi-grid dash-kpi-4" style={{ marginBottom: '1.5rem' }}>
{[0, 1, 2, 3].map(i => (
<div key={i} className="admin-stat-card">
<div className="admin-skeleton-line" style={{ width: '60%', height: '11px', marginBottom: '0.5rem' }} />
<div className="admin-skeleton-line" style={{ width: '40%', height: '28px', marginBottom: '0.5rem' }} />
<div className="admin-skeleton-line" style={{ width: '50%', height: '12px' }} />
</div>
))}
</div>
}>
<ReceivedInvoices statsMonth={statsMonth} statsYear={statsYear} uploadOpen={receivedUploadOpen} setUploadOpen={setReceivedUploadOpen} />
</Suspense>
</motion.div>
) : (
<>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.15 }}
>
{!hasLoadedOnce.current && statsLoading ? (
<div className="dash-kpi-grid dash-kpi-4" style={{ marginBottom: '1.5rem' }}>
{[0, 1, 2, 3].map(i => (
<div key={i} className="admin-stat-card">
<div className="admin-skeleton-line" style={{ width: '60%', height: '11px', marginBottom: '0.5rem' }} />
<div className="admin-skeleton-line" style={{ width: '40%', height: '28px', marginBottom: '0.5rem' }} />
<div className="admin-skeleton-line" style={{ width: '50%', height: '12px' }} />
</div>
))}
</div>
) : stats && (
<div style={{ overflow: 'hidden', marginBottom: '1.5rem' }}>
<AnimatePresence mode="popLayout" initial={false} custom={slideDirection.current}>
<motion.div
key={slideKey}
className="dash-kpi-grid dash-kpi-4"
custom={slideDirection.current}
variants={{
enter: (dir) => ({ x: `${(dir || 0) * 105}%`, opacity: 0 }),
center: { x: '0%', opacity: 1 },
exit: (dir) => ({ x: `${(dir || 0) * -105}%`, opacity: 0 }),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
{(() => {
const paid = formatCzkWithDetail(stats.paid_month, stats.paid_month_czk)
const wait = formatCzkWithDetail(stats.awaiting, stats.awaiting_czk)
const over = formatCzkWithDetail(stats.overdue, stats.overdue_czk)
const vat = formatCzkWithDetail(stats.vat_month, stats.vat_month_czk)
const countFooter = (count, zero) => count > 0
? `${count} ${czechPlural(count, 'faktura', 'faktury', 'faktur')}`
: zero
return (
<>
<div className="admin-stat-card success">
<div className="admin-stat-label">Uhrazeno ({MONTH_NAMES[statsMonth - 1]})</div>
<div className="admin-stat-value admin-mono">{paid.value}</div>
<div className="admin-stat-footer">
{[paid.detail, countFooter(stats.paid_month_count, 'žádné úhrady')].filter(Boolean).join(' · ')}
</div>
</div>
<div className="admin-stat-card warning">
<div className="admin-stat-label">Čeká úhrada <span style={{ fontWeight: 400, opacity: 0.7 }}>· celkově</span></div>
<div className="admin-stat-value admin-mono">{wait.value}</div>
<div className="admin-stat-footer">
{[wait.detail, countFooter(stats.awaiting_count, 'vše uhrazeno')].filter(Boolean).join(' · ')}
</div>
</div>
<div className="admin-stat-card danger">
<div className="admin-stat-label">Po splatnosti <span style={{ fontWeight: 400, opacity: 0.7 }}>· celkově</span></div>
<div className="admin-stat-value admin-mono">{over.value}</div>
<div className="admin-stat-footer">
{[over.detail, stats.overdue_count === 0 ? 'vše v pořádku' : countFooter(stats.overdue_count, '')].filter(Boolean).join(' · ')}
</div>
</div>
<div className="admin-stat-card info">
<div className="admin-stat-label">DPH ({MONTH_NAMES[statsMonth - 1]})</div>
<div className="admin-stat-value admin-mono">{vat.value}</div>
<div className="admin-stat-footer">{vat.detail || 'z vydaných faktur'}</div>
</div>
</>
)
})()}
</motion.div>
</AnimatePresence>
</div>
)}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.2 }}
>
<div className="offers-tabs mb-6">
{STATUS_FILTERS.map(f => (
<button
key={f.value}
className={`offers-tab ${statusFilter === f.value ? 'active' : ''}`}
onClick={() => { setStatusFilter(f.value); setPage(1) }}
>
{f.label}
</button>
))}
</div>
</motion.div>
<motion.div
className="admin-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.25 }}
>
<div className="admin-card-body">
<div className="admin-search-bar mb-4">
<input
type="text"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1) }}
className="admin-form-input"
placeholder="Hledat podle čísla faktury, zákazníka nebo IČ..."
/>
</div>
{invoices.length === 0 && !(draft && !statusFilter) ? (
<div className="admin-empty-state">
<div className="admin-empty-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<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="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
</div>
<p>Zatím nejsou žádné faktury.</p>
{hasPermission('invoices.create') && (
<p className="text-tertiary" style={{ fontSize: '0.875rem' }}>
Vytvořte první fakturu tlačítkem výše.
</p>
)}
</div>
) : (
<div className="admin-table-responsive">
<table className="admin-table">
<thead>
<tr>
<th style={{ cursor: 'pointer' }} onClick={() => handleSort('invoice_number')}>
Číslo <SortIcon column="invoice_number" sort={activeSort} order={order} />
</th>
<th>Zákazník</th>
<th style={{ cursor: 'pointer' }} onClick={() => handleSort('status')}>
Stav <SortIcon column="status" sort={activeSort} order={order} />
</th>
<th style={{ cursor: 'pointer' }} onClick={() => handleSort('issue_date')}>
Vystaveno <SortIcon column="issue_date" sort={activeSort} order={order} />
</th>
<th style={{ cursor: 'pointer' }} onClick={() => handleSort('due_date')}>
Splatnost <SortIcon column="due_date" sort={activeSort} order={order} />
</th>
<th className="text-right">Celkem</th>
<th>Akce</th>
</tr>
</thead>
<tbody>
{draft && !search && !statusFilter && (
<tr className="offers-draft-row">
<td>
<span className="offers-draft-row-label">
Koncept
{draft.savedAt && (
<span style={{ fontWeight: 400, opacity: 0.8 }}>
{' · '}{new Date(draft.savedAt).toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })}
</span>
)}
</span>
</td>
<td>{draft.form.customer_name || '—'}</td>
<td></td>
<td className="admin-mono">
{draft.form.issue_date ? formatDate(draft.form.issue_date) : '—'}
</td>
<td className="admin-mono">
{draft.form.due_date ? formatDate(draft.form.due_date) : '—'}
</td>
<td />
<td>
<div className="admin-table-actions">
<Link to="/invoices/new" className="admin-btn-icon" title="Pokračovat v konceptu" aria-label="Pokračovat v konceptu">
<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>
</Link>
<button
onClick={discardDraft}
className="admin-btn-icon danger"
title="Zahodit koncept"
>
<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>
)}
{invoices.map((inv) => {
const isOverdue = inv.status === 'overdue' || (inv.status === 'issued' && inv.due_date && new Date(inv.due_date) < new Date(new Date().toDateString()))
return (
<tr key={inv.id} className={isOverdue ? 'offers-expired-row' : ''}>
<td className="admin-mono">
<Link to={`/invoices/${inv.id}`} className="link-accent">
{inv.invoice_number}
</Link>
</td>
<td>{inv.customer_name || '—'}</td>
<td>
{inv.status === 'paid' ? (
<span className={`admin-badge ${STATUS_CLASSES[inv.status]}`}>
{STATUS_LABELS[inv.status]}
</span>
) : (
<button
onClick={() => toggleStatus(inv)}
className={`admin-badge ${STATUS_CLASSES[inv.status] || ''}`}
style={{ cursor: 'pointer' }}
>
{STATUS_LABELS[inv.status] || inv.status}
</button>
)}
</td>
<td className="admin-mono">
{formatDate(inv.issue_date)}
</td>
<td className="admin-mono" style={inv.status === 'overdue' ? { color: 'var(--danger)', fontWeight: 600 } : undefined}>
{formatDate(inv.due_date)}
</td>
<td className="admin-mono" style={{ textAlign: 'right', fontWeight: 500 }}>
{formatCurrency(inv.total, inv.currency)}
</td>
<td>
<div className="admin-table-actions">
<Link to={`/invoices/${inv.id}`} className="admin-btn-icon" title="Detail" aria-label="Detail">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
</Link>
{hasPermission('invoices.export') && (
<button
onClick={() => setLangModal(inv)}
className="admin-btn-icon"
title="PDF"
disabled={pdfLoading === inv.id}
>
{pdfLoading === inv.id ? (
<div className="admin-spinner" style={{ width: 18, height: 18, borderWidth: 2 }} />
) : (
<svg width="18" height="18" 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="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
</svg>
)}
</button>
)}
{hasPermission('invoices.delete') && (
<button
onClick={() => setDeleteConfirm({ show: true, invoice: inv })}
className="admin-btn-icon danger"
title="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>
)}
<Pagination pagination={pagination} onPageChange={setPage} />
</div>
</motion.div>
<ConfirmModal
isOpen={deleteConfirm.show}
onClose={() => setDeleteConfirm({ show: false, invoice: null })}
onConfirm={handleDelete}
title="Smazat fakturu"
message={`Opravdu chcete smazat fakturu "${deleteConfirm.invoice?.invoice_number}"? Tato akce je nevratná.`}
confirmText="Smazat"
cancelText="Zrušit"
type="danger"
loading={deleting}
/>
<AnimatePresence>
{langModal && (
<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={() => setLangModal(null)} />
<motion.div
className="admin-modal admin-confirm-modal"
role="dialog"
aria-modal="true"
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-body admin-confirm-content">
<div className="admin-confirm-icon admin-confirm-icon-info">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" />
<path d="M2 12h20" />
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg>
</div>
<h2 className="admin-confirm-title">Jazyk faktury</h2>
<p className="admin-confirm-message">V jakém jazyce chcete vygenerovat fakturu?</p>
</div>
<div className="admin-modal-footer">
<button type="button" onClick={() => handlePdf(langModal, 'cs')} className="admin-btn admin-btn-primary">
Čeština
</button>
<button type="button" onClick={() => handlePdf(langModal, 'en')} className="admin-btn admin-btn-primary">
English
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
)}
</div>
)
}