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
{pagination?.total ?? invoices.length} {czechPlural(pagination?.total ?? invoices.length, 'faktura', 'faktury', 'faktur')}
Zatím nejsou žádné faktury.
{hasPermission('invoices.create') && (Vytvořte první fakturu tlačítkem výše.
)}| handleSort('invoice_number')}>
Číslo |
Zákazník | handleSort('status')}>
Stav |
handleSort('issue_date')}>
Vystaveno |
handleSort('due_date')}>
Splatnost |
Celkem | Akce |
|---|---|---|---|---|---|---|
| Koncept {draft.savedAt && ( {' · '}{new Date(draft.savedAt).toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })} )} | {draft.form.customer_name || '—'} | — | {draft.form.issue_date ? formatDate(draft.form.issue_date) : '—'} | {draft.form.due_date ? formatDate(draft.form.due_date) : '—'} |
|
|
| {inv.invoice_number} | {inv.customer_name || '—'} | {inv.status === 'paid' ? ( {STATUS_LABELS[inv.status]} ) : ( )} | {formatDate(inv.issue_date)} | {formatDate(inv.due_date)} | {formatCurrency(inv.total, inv.currency)} |
{hasPermission('invoices.export') && (
)}
{hasPermission('invoices.delete') && (
)}
|
V jakém jazyce chcete vygenerovat fakturu?