- 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>
528 lines
23 KiB
JavaScript
528 lines
23 KiB
JavaScript
import { useState, useEffect, useCallback, useRef } from 'react'
|
||
import DOMPurify from 'dompurify'
|
||
import { useAlert } from '../context/AlertContext'
|
||
import { useAuth } from '../context/AuthContext'
|
||
import Forbidden from '../components/Forbidden'
|
||
import { motion } from 'framer-motion'
|
||
import AdminDatePicker from '../components/AdminDatePicker'
|
||
import { formatDate, formatDatetime, formatTime, calculateWorkMinutes, formatMinutes, getLeaveTypeName, getLeaveTypeBadgeClass, calculateWorkMinutesPrint, formatTimeOrDatetimePrint } from '../utils/attendanceHelpers'
|
||
import FormField from '../components/FormField'
|
||
import apiFetch from '../utils/api'
|
||
|
||
const API_BASE = '/api/admin'
|
||
|
||
const formatBreakRange = (record) => {
|
||
if (record.break_start && record.break_end) {
|
||
return `${formatTime(record.break_start)} - ${formatTime(record.break_end)}`
|
||
}
|
||
if (record.break_start) {
|
||
return `${formatTime(record.break_start)} - ?`
|
||
}
|
||
return '—'
|
||
}
|
||
|
||
const renderProjectCell = (record) => {
|
||
if (record.project_logs && record.project_logs.length > 0) {
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.125rem' }}>
|
||
{record.project_logs.map((log, i) => {
|
||
let h, m, isActive = false
|
||
if (log.hours !== null && log.hours !== undefined) {
|
||
h = parseInt(log.hours) || 0
|
||
m = parseInt(log.minutes) || 0
|
||
} else {
|
||
isActive = !log.ended_at
|
||
const end = log.ended_at ? new Date(log.ended_at) : new Date()
|
||
const mins = Math.floor((end - new Date(log.started_at)) / 60000)
|
||
h = Math.floor(mins / 60)
|
||
m = mins % 60
|
||
}
|
||
return (
|
||
<span key={log.id || i} className="admin-badge" style={{ fontSize: '0.7rem', display: 'inline-block', background: isActive ? 'var(--accent-light)' : undefined }}>
|
||
{log.project_name || `#${log.project_id}`} ({h}:{String(m).padStart(2, '0')}h{isActive ? ' ▸' : ''})
|
||
</span>
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
}
|
||
if (record.project_name) {
|
||
return <span className="admin-badge admin-badge-wrap" style={{ fontSize: '0.75rem' }}>{record.project_name}</span>
|
||
}
|
||
return '—'
|
||
}
|
||
|
||
const renderPrintFundStatus = (fund) => {
|
||
if (fund.overtime > 0) {
|
||
return <span className="leave-badge badge-overtime">+{fund.overtime}h přesčas</span>
|
||
}
|
||
if (fund.remaining > 0) {
|
||
return <span style={{ color: '#dc2626' }}>−{fund.remaining}h</span>
|
||
}
|
||
return <span style={{ color: '#16a34a' }}>splněno</span>
|
||
}
|
||
|
||
export default function AttendanceHistory() {
|
||
const alert = useAlert()
|
||
const { user, hasPermission } = useAuth()
|
||
const [loading, setLoading] = useState(true)
|
||
const printRef = useRef(null)
|
||
const [month, setMonth] = useState(() => {
|
||
const now = new Date()
|
||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
||
})
|
||
const [data, setData] = useState({
|
||
records: [],
|
||
month_name: '',
|
||
year: new Date().getFullYear(),
|
||
total_minutes: 0,
|
||
vacation_hours: 0,
|
||
sick_hours: 0,
|
||
holiday_hours: 0,
|
||
unpaid_hours: 0,
|
||
leave_balance: null,
|
||
monthly_fund: null
|
||
})
|
||
|
||
const fetchData = useCallback(async () => {
|
||
setLoading(true)
|
||
try {
|
||
const response = await apiFetch(`${API_BASE}/attendance.php?action=history&month=${month}`)
|
||
if (response.status === 401) return
|
||
const result = await response.json()
|
||
if (result.success) {
|
||
setData(result.data)
|
||
}
|
||
} catch {
|
||
alert.error('Nepodařilo se načíst data')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [month, alert])
|
||
|
||
useEffect(() => {
|
||
fetchData()
|
||
}, [fetchData])
|
||
|
||
if (!hasPermission('attendance.history')) return <Forbidden />
|
||
|
||
const handlePrint = () => {
|
||
if (!printRef.current) return
|
||
const printWindow = window.open('', '_blank')
|
||
printWindow.document.write(`
|
||
<!DOCTYPE html>
|
||
<html lang="cs">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Docházka - ${data.month_name}</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
font-size: 11px;
|
||
line-height: 1.4;
|
||
color: #000;
|
||
background: #fff;
|
||
padding: 15mm;
|
||
}
|
||
.print-wrapper-table { width: 100%; border-collapse: collapse; border: none; }
|
||
.print-wrapper-table > thead > tr > td,
|
||
.print-wrapper-table > tbody > tr > td { padding: 0; border: none; background: none; }
|
||
.print-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 15px;
|
||
border-bottom: 2px solid #333;
|
||
}
|
||
.print-header-left { display: flex; align-items: center; gap: 12px; }
|
||
.print-logo { height: 40px; width: auto; }
|
||
.print-header-text { text-align: left; }
|
||
.print-header-right { text-align: right; }
|
||
.print-header h1 { font-size: 18px; font-weight: 700; margin-bottom: 3px; }
|
||
.print-header .company { font-size: 11px; color: #666; }
|
||
.print-header .period { font-size: 13px; font-weight: 600; color: #333; margin-bottom: 2px; }
|
||
.print-header .filters { font-size: 10px; color: #666; }
|
||
.print-header .generated { font-size: 9px; color: #888; margin-top: 5px; }
|
||
.user-section { margin-bottom: 25px; page-break-inside: avoid; }
|
||
.user-header {
|
||
background: #f5f5f5;
|
||
border: 1px solid #ddd;
|
||
padding: 10px 15px;
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.user-header h3 { font-size: 13px; font-weight: 600; }
|
||
.user-header .total { font-size: 12px; font-weight: 600; }
|
||
.leave-summary {
|
||
margin-top: 10px;
|
||
padding: 8px 15px;
|
||
background: #f9f9f9;
|
||
border: 1px solid #ddd;
|
||
font-size: 10px;
|
||
}
|
||
.user-section table { width: 100%; border-collapse: collapse; margin-bottom: 15px; }
|
||
.user-section th, .user-section td { border: 1px solid #333; padding: 6px 8px; text-align: left; }
|
||
.user-section th { background: #333; color: #fff; font-weight: 600; font-size: 10px; text-transform: uppercase; }
|
||
.user-section td { font-size: 10px; }
|
||
.user-section tr:nth-child(even) { background: #f9f9f9; }
|
||
.text-center { text-align: center; }
|
||
.text-right { text-align: right; }
|
||
.user-section tfoot td { background: #eee; font-weight: 600; }
|
||
.leave-badge {
|
||
display: inline-block;
|
||
padding: 2px 6px;
|
||
border-radius: 3px;
|
||
font-size: 9px;
|
||
font-weight: 500;
|
||
}
|
||
.badge-vacation { background: #dbeafe; color: #1d4ed8; }
|
||
.badge-sick { background: #fee2e2; color: #dc2626; }
|
||
.badge-holiday { background: #dcfce7; color: #16a34a; }
|
||
.badge-unpaid { background: #f3f4f6; color: #6b7280; }
|
||
.badge-overtime { background: #fef3c7; color: #d97706; }
|
||
@media print {
|
||
body { padding: 0; margin: 0; }
|
||
@page { size: A4 portrait; margin: 10mm; }
|
||
.user-section { page-break-inside: avoid; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
${DOMPurify.sanitize(printRef.current.innerHTML)}
|
||
</body>
|
||
</html>
|
||
`)
|
||
printWindow.document.close()
|
||
printWindow.onload = () => {
|
||
printWindow.print()
|
||
}
|
||
}
|
||
|
||
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">Historie docházky</h1>
|
||
<p className="admin-page-subtitle">{data.month_name}</p>
|
||
</div>
|
||
<div className="admin-page-actions">
|
||
{data.records.length > 0 && (
|
||
<button
|
||
onClick={handlePrint}
|
||
className="admin-btn admin-btn-secondary"
|
||
title="Tisk docházky"
|
||
>
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ marginRight: '0.5rem' }}>
|
||
<polyline points="6 9 6 2 18 2 18 9" />
|
||
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" />
|
||
<rect x="6" y="14" width="12" height="8" />
|
||
</svg>
|
||
Tisk
|
||
</button>
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Filters */}
|
||
<motion.div
|
||
className="admin-card mb-6"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.4, delay: 0.1 }}
|
||
>
|
||
<div className="admin-card-body">
|
||
<div className="admin-form-row">
|
||
<FormField label="Měsíc">
|
||
<AdminDatePicker
|
||
mode="month"
|
||
value={month}
|
||
onChange={(val) => setMonth(val)}
|
||
/>
|
||
</FormField>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Monthly Fund Card */}
|
||
<motion.div
|
||
className="admin-card mb-6"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.4, delay: 0.15 }}
|
||
>
|
||
<div className="admin-card-body">
|
||
{loading && (
|
||
<div className="admin-skeleton" style={{ gap: '0.5rem' }}>
|
||
<div className="admin-skeleton-row" style={{ gap: '1rem' }}>
|
||
<div className="admin-skeleton-line" style={{ width: '48px', height: '48px', borderRadius: '12px', flexShrink: 0 }} />
|
||
<div className="flex-1">
|
||
<div className="admin-skeleton-line w-1/2" style={{ marginBottom: '0.5rem' }} />
|
||
<div className="admin-skeleton-line w-full" style={{ height: '6px', borderRadius: '3px' }} />
|
||
<div className="admin-skeleton-line w-1/3" style={{ height: '10px', marginTop: '0.5rem' }} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{!loading && data.monthly_fund && (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
|
||
<div className="admin-stat-icon info">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||
<line x1="16" y1="2" x2="16" y2="6" />
|
||
<line x1="8" y1="2" x2="8" y2="6" />
|
||
<line x1="3" y1="10" x2="21" y2="10" />
|
||
</svg>
|
||
</div>
|
||
<div style={{ flex: 1, minWidth: '200px' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '0.375rem' }}>
|
||
<span style={{ fontWeight: 600, fontSize: '1rem', color: 'var(--text-primary)' }}>
|
||
Fond: {data.monthly_fund.worked}h / {data.monthly_fund.fund}h
|
||
</span>
|
||
<span className="text-secondary" style={{ fontSize: '0.8125rem' }}>
|
||
{data.monthly_fund.business_days} prac. dnů
|
||
</span>
|
||
</div>
|
||
<div className="attendance-balance-bar">
|
||
<div
|
||
className="attendance-balance-progress"
|
||
style={{
|
||
width: `${Math.min(100, data.monthly_fund.fund > 0 ? (data.monthly_fund.covered / data.monthly_fund.fund) * 100 : 0)}%`,
|
||
background: data.monthly_fund.covered >= data.monthly_fund.fund
|
||
? 'linear-gradient(135deg, var(--success), #059669)'
|
||
: 'var(--gradient)'
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="text-muted" style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem', marginTop: '0.375rem' }}>
|
||
<span>
|
||
{'Pokryto: '}{data.monthly_fund.covered}h (práce {data.monthly_fund.worked}h
|
||
{data.vacation_hours > 0 && ` + dovolená ${data.vacation_hours}h`}
|
||
{data.sick_hours > 0 && ` + nemoc ${data.sick_hours}h`}
|
||
{data.holiday_hours > 0 && ` + svátek ${data.holiday_hours}h`}
|
||
{data.unpaid_hours > 0 && ` + neplacené ${data.unpaid_hours}h`}
|
||
)
|
||
</span>
|
||
{data.monthly_fund.overtime > 0 ? (
|
||
<span className="text-warning fw-600">Přesčas: +{data.monthly_fund.overtime}h</span>
|
||
) : (
|
||
<span>Zbývá: {data.monthly_fund.remaining}h</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{!loading && !data.monthly_fund && (
|
||
<div className="text-muted" style={{ fontSize: '0.875rem', textAlign: 'center', padding: '0.5rem 0' }}>
|
||
Fond měsíce není k dispozici
|
||
</div>
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Records Table */}
|
||
<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">
|
||
{loading && (
|
||
<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 w-1/4" />
|
||
<div className="admin-skeleton-line w-1/3" />
|
||
<div className="admin-skeleton-line w-1/4" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
{!loading && data.records.length === 0 && (
|
||
<div className="admin-empty-state">
|
||
<p>Za tento měsíc nejsou žádné záznamy.</p>
|
||
</div>
|
||
)}
|
||
{!loading && data.records.length > 0 && (
|
||
<div className="admin-table-responsive">
|
||
<table className="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Datum</th>
|
||
<th>Typ</th>
|
||
<th>Příchod</th>
|
||
<th>Pauza</th>
|
||
<th>Odchod</th>
|
||
<th>Hodiny</th>
|
||
<th>Projekty</th>
|
||
<th>Poznámka</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.records.map((record) => {
|
||
const leaveType = record.leave_type || 'work'
|
||
const isLeave = leaveType !== 'work'
|
||
const workMinutes = isLeave
|
||
? (record.leave_hours || 8) * 60
|
||
: calculateWorkMinutes(record)
|
||
|
||
return (
|
||
<tr key={record.id}>
|
||
<td className="admin-mono">{formatDate(record.shift_date)}</td>
|
||
<td>
|
||
<span className={`attendance-leave-badge ${getLeaveTypeBadgeClass(leaveType)}`}>
|
||
{getLeaveTypeName(leaveType)}
|
||
</span>
|
||
</td>
|
||
<td className="admin-mono">{isLeave ? '—' : formatDatetime(record.arrival_time)}</td>
|
||
<td className="admin-mono">
|
||
{isLeave ? '—' : formatBreakRange(record)}
|
||
</td>
|
||
<td className="admin-mono">{isLeave ? '—' : formatDatetime(record.departure_time)}</td>
|
||
<td className="admin-mono">{workMinutes > 0 ? formatMinutes(workMinutes, true) : '—'}</td>
|
||
<td>
|
||
{renderProjectCell(record)}
|
||
</td>
|
||
<td style={{ maxWidth: '150px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
{record.notes || ''}
|
||
</td>
|
||
</tr>
|
||
)
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Hidden Print Content */}
|
||
{data.records.length > 0 && (
|
||
<div ref={printRef} style={{ display: 'none' }}>
|
||
<table className="print-wrapper-table">
|
||
<thead>
|
||
<tr><td>
|
||
<div className="print-header">
|
||
<div className="print-header-left">
|
||
<img src="/images/logo-light.png" alt="BOHA" className="print-logo" />
|
||
<div className="print-header-text">
|
||
<h1>EVIDENCE DOCHÁZKY</h1>
|
||
<div className="company">BOHA Automation s.r.o.</div>
|
||
</div>
|
||
</div>
|
||
<div className="print-header-right">
|
||
<div className="period">{data.month_name}</div>
|
||
<div className="filters">Zaměstnanec: {user?.fullName || ''}</div>
|
||
<div className="generated">Vygenerováno: {new Date().toLocaleString('cs-CZ')}</div>
|
||
</div>
|
||
</div>
|
||
</td></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td>
|
||
<div className="user-section">
|
||
<div className="user-header">
|
||
<h3>{user?.fullName || ''}</h3>
|
||
<span className="total">Odpracováno: {formatMinutes(data.total_minutes, true)}</span>
|
||
</div>
|
||
|
||
{data.leave_balance && (
|
||
<div className="leave-summary">
|
||
<strong>Dovolená {data.year}:</strong> Zbývá {data.leave_balance.vacation_remaining.toFixed(1)}h z {data.leave_balance.vacation_total}h
|
||
{data.vacation_hours > 0 && <> | <span className="leave-badge badge-vacation">Tento měsíc: {data.vacation_hours}h</span></>}
|
||
{data.sick_hours > 0 && <> | <span className="leave-badge badge-sick">Nemoc: {data.sick_hours}h</span></>}
|
||
{data.holiday_hours > 0 && <> | <span className="leave-badge badge-holiday">Svátek: {data.holiday_hours}h</span></>}
|
||
{data.monthly_fund?.overtime > 0 && <> | <span className="leave-badge badge-overtime">Přesčas: +{data.monthly_fund.overtime}h</span></>}
|
||
</div>
|
||
)}
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style={{ width: '70px' }}>Datum</th>
|
||
<th style={{ width: '70px' }}>Typ</th>
|
||
<th className="text-center" style={{ width: '70px' }}>Příchod</th>
|
||
<th className="text-center" style={{ width: '90px' }}>Pauza</th>
|
||
<th className="text-center" style={{ width: '70px' }}>Odchod</th>
|
||
<th className="text-center" style={{ width: '80px' }}>Hodiny</th>
|
||
<th>Projekty</th>
|
||
<th>Poznámka</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{[...data.records].sort((a, b) => a.shift_date.localeCompare(b.shift_date)).map((record) => {
|
||
const leaveType = record.leave_type || 'work'
|
||
const isLeave = leaveType !== 'work'
|
||
const workMinutes = calculateWorkMinutesPrint(record)
|
||
const hours = Math.floor(workMinutes / 60)
|
||
const mins = workMinutes % 60
|
||
|
||
return (
|
||
<tr key={record.id}>
|
||
<td>{formatDate(record.shift_date)}</td>
|
||
<td><span className={`leave-badge ${getLeaveTypeBadgeClass(leaveType)}`}>{getLeaveTypeName(leaveType)}</span></td>
|
||
<td className="text-center">{isLeave ? '—' : formatTimeOrDatetimePrint(record.arrival_time, record.shift_date)}</td>
|
||
<td className="text-center">
|
||
{isLeave || !record.break_start || !record.break_end
|
||
? '—'
|
||
: `${formatTimeOrDatetimePrint(record.break_start, record.shift_date)} - ${formatTimeOrDatetimePrint(record.break_end, record.shift_date)}`
|
||
}
|
||
</td>
|
||
<td className="text-center">{isLeave ? '—' : formatTimeOrDatetimePrint(record.departure_time, record.shift_date)}</td>
|
||
<td className="text-center">{workMinutes > 0 ? `${hours}:${String(mins).padStart(2, '0')}` : '—'}</td>
|
||
<td style={{ fontSize: '8px' }}>
|
||
{(record.project_logs && record.project_logs.length > 0)
|
||
? record.project_logs.map((log, i) => {
|
||
let h, m
|
||
if (log.hours !== null && log.hours !== undefined) {
|
||
h = parseInt(log.hours) || 0; m = parseInt(log.minutes) || 0
|
||
} else if (log.started_at && log.ended_at) {
|
||
const mins2 = Math.max(0, Math.floor((new Date(log.ended_at) - new Date(log.started_at)) / 60000))
|
||
h = Math.floor(mins2 / 60); m = mins2 % 60
|
||
} else { h = 0; m = 0 }
|
||
return <div key={log.id || i}>{log.project_name || `#${log.project_id}`} ({h}:{String(m).padStart(2, '0')}h)</div>
|
||
})
|
||
: record.project_name || '—'}
|
||
</td>
|
||
<td>{record.notes || ''}</td>
|
||
</tr>
|
||
)
|
||
})}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr>
|
||
<td colSpan={6} className="text-right">Odpracováno:</td>
|
||
<td className="text-center">{formatMinutes(data.total_minutes, true)}</td>
|
||
<td colSpan={2}></td>
|
||
</tr>
|
||
{data.monthly_fund && (
|
||
<tr>
|
||
<td colSpan={6} className="text-right">Fond měsíce:</td>
|
||
<td className="text-center">{data.monthly_fund.covered}h / {data.monthly_fund.fund}h</td>
|
||
<td colSpan={2}>
|
||
{renderPrintFundStatus(data.monthly_fund)}
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|