refactor: sjednoceni skeleton loading a animaci napric vsemi moduly

- AttendanceAdmin: full-page skeleton misto castecneho (header+filtry+karty+tabulka)
- AttendanceHistory: fond karta vzdy v DOM se skeleton behem loading (fix pozdni animace)
- Trips: skeleton odpovida realne strukture (stat cards + tabulka misto circle rows)
- ReceivedInvoices: 3 skeleton radky zvyseny na 5 (konzistence)
- ProjectDetail: notes spinner nahrazen skeleton bloky
- Settings: 2FA text "Nacitani..." nahrazen skeleton line
- Sjednoceny animation delays: 0.05/0.12 opraveny na standardni 0.1/0.15
- OffersTemplates: pridany chybejici stagger delays
- Invoices: opraveny duplicitni delays na spravny stagger
- Attendance sidebar: delay snizen na 0.1 (soucasne s hlavnim obsahem)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 20:26:34 +01:00
parent b2c2ab6e7a
commit 36a864c852
12 changed files with 121 additions and 103 deletions

View File

@@ -606,7 +606,7 @@ export default function Attendance() {
className="attendance-sidebar"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.2 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
{/* Leave Balance Card */}
<div className="attendance-balance-card">

View File

@@ -48,6 +48,56 @@ export default function AttendanceAdmin() {
if (!hasPermission('attendance.admin')) return <Forbidden />
if (loading) {
return (
<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>
<div className="admin-skeleton-row" style={{ gap: '0.5rem' }}>
<div className="admin-skeleton-line h-10" style={{ width: '120px', borderRadius: '8px' }} />
<div className="admin-skeleton-line h-10" style={{ width: '120px', borderRadius: '8px' }} />
<div className="admin-skeleton-line h-10" style={{ width: '140px', borderRadius: '8px' }} />
</div>
</div>
<div className="admin-card">
<div className="admin-skeleton" style={{ gap: '0.75rem', padding: '1rem' }}>
<div className="admin-skeleton-row">
<div className="admin-skeleton-line h-10" style={{ flex: 1, borderRadius: '8px' }} />
<div className="admin-skeleton-line h-10" style={{ flex: 1, borderRadius: '8px' }} />
</div>
</div>
</div>
<div className="admin-grid admin-grid-3">
{[0, 1, 2].map(i => (
<div key={i} className="admin-card">
<div className="admin-card-body">
<div className="admin-skeleton" style={{ gap: '0.75rem' }}>
<div className="admin-skeleton-line w-1/2" />
<div className="admin-skeleton-line h-8" style={{ width: '80px' }} />
<div className="admin-skeleton-line w-1/3" style={{ height: '10px' }} />
<div className="admin-skeleton-line w-full" style={{ height: '4px' }} />
</div>
</div>
</div>
))}
</div>
<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 w-1/4" />
<div className="admin-skeleton-line w-1/3" />
<div className="admin-skeleton-line w-1/4" />
</div>
))}
</div>
</div>
</div>
)
}
return (
<div>
<motion.div
@@ -129,23 +179,7 @@ export default function AttendanceAdmin() {
</motion.div>
{/* User Totals */}
{loading && (
<div className="admin-grid admin-grid-3" style={{ marginBottom: '1.5rem' }}>
{[0, 1, 2].map(i => (
<div key={i} className="admin-card">
<div className="admin-card-body">
<div className="admin-skeleton" style={{ gap: '0.75rem' }}>
<div className="admin-skeleton-line w-1/2" />
<div className="admin-skeleton-line h-8" style={{ width: '80px' }} />
<div className="admin-skeleton-line w-1/3" style={{ height: '10px' }} />
<div className="admin-skeleton-line w-full" style={{ height: '4px' }} />
</div>
</div>
</div>
))}
</div>
)}
{!loading && Object.keys(data.user_totals).length > 0 && (
{Object.keys(data.user_totals).length > 0 && (
<motion.div
className="admin-grid admin-grid-3"
style={{ marginBottom: '1.5rem' }}
@@ -225,24 +259,11 @@ export default function AttendanceAdmin() {
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 && (
<AttendanceShiftTable
records={data.records}
onEdit={openEditModal}
onDelete={(record) => setDeleteConfirm({ show: true, record })}
/>
)}
<AttendanceShiftTable
records={data.records}
onEdit={openEditModal}
onDelete={(record) => setDeleteConfirm({ show: true, record })}
/>
</div>
</motion.div>

View File

@@ -255,15 +255,27 @@ export default function AttendanceHistory() {
</motion.div>
{/* Monthly Fund Card */}
{!loading && data.monthly_fund && (
<motion.div
className="admin-card"
style={{ marginBottom: '1.5rem' }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.15 }}
>
<div className="admin-card-body">
<motion.div
className="admin-card"
style={{ marginBottom: '1.5rem' }}
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 style={{ 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">
@@ -310,9 +322,14 @@ export default function AttendanceHistory() {
</div>
</div>
</div>
</div>
</motion.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

View File

@@ -201,7 +201,7 @@ export default function AuditLog() {
className="admin-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.05 }}
transition={{ duration: 0.4, delay: 0.1 }}
style={{ marginBottom: '1rem' }}
>
<div className="admin-card-body">
@@ -263,7 +263,7 @@ export default function AuditLog() {
className="admin-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
transition={{ duration: 0.4, delay: 0.15 }}
>
<div className="admin-table-wrapper">
<table className="admin-table">

View File

@@ -527,7 +527,7 @@ export default function CompanySettings() {
className="admin-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.12 }}
transition={{ duration: 0.4, delay: 0.15 }}
>
<div className="admin-card-header">
<h3 className="admin-card-title">Bankovní účty</h3>

View File

@@ -327,7 +327,7 @@ export default function Invoices() {
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
transition={{ duration: 0.4, delay: 0.15 }}
>
<Suspense fallback={
<div className="dash-kpi-grid dash-kpi-4" style={{ marginBottom: '1.5rem' }}>
@@ -348,7 +348,7 @@ export default function Invoices() {
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
transition={{ duration: 0.4, delay: 0.15 }}
>
{!hasLoadedOnce.current && statsLoading ? (
<div className="dash-kpi-grid dash-kpi-4" style={{ marginBottom: '1.5rem' }}>
@@ -425,7 +425,7 @@ export default function Invoices() {
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.15 }}
transition={{ duration: 0.4, delay: 0.2 }}
>
<div className="offers-tabs" style={{ marginBottom: '1.5rem' }}>
{STATUS_FILTERS.map(f => (
@@ -445,7 +445,7 @@ export default function Invoices() {
className="admin-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.2 }}
transition={{ duration: 0.4, delay: 0.25 }}
>
<div className="admin-card-body">
<div className="admin-search-bar" style={{ marginBottom: '1rem' }}>

View File

@@ -215,7 +215,7 @@ export default function LeaveApproval() {
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.05 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<div className="offers-tabs" style={{ marginBottom: '1.5rem' }}>
<button
@@ -243,7 +243,7 @@ export default function LeaveApproval() {
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
transition={{ duration: 0.4, delay: 0.15 }}
>
{pendingRequests.length === 0 ? (
<div className="admin-card">
@@ -314,7 +314,7 @@ export default function LeaveApproval() {
className="admin-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
transition={{ duration: 0.4, delay: 0.15 }}
>
<div className="admin-card-body">
{processedRequests.length === 0 ? (

View File

@@ -169,7 +169,7 @@ function ItemTemplatesTab() {
className="admin-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<div className="admin-card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 className="admin-card-title">Šablony položek ({templates.length})</h3>
@@ -442,7 +442,7 @@ function ScopeTemplatesTab() {
className="admin-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<div className="admin-card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 className="admin-card-title">Šablony rozsahu ({templates.length})</h3>

View File

@@ -428,8 +428,10 @@ export default function ProjectDetail() {
{/* Notes list */}
{notesLoading && (
<div style={{ textAlign: 'center', padding: '1rem' }}>
<div className="admin-spinner" style={{ width: 20, height: 20, borderWidth: 2 }} />
<div className="admin-skeleton" style={{ gap: '0.75rem' }}>
{[0, 1, 2].map(i => (
<div key={i} className="admin-skeleton-line" style={{ height: '52px', borderRadius: '8px' }} />
))}
</div>
)}
{!notesLoading && notes.length === 0 && !project.notes && (

View File

@@ -468,7 +468,7 @@ export default function ReceivedInvoicesProps({ statsMonth, statsYear, uploadOpe
{loading && (
<div className="admin-skeleton" style={{ gap: '1rem' }}>
{[0, 1, 2].map(i => (
{[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/4" />

View File

@@ -292,7 +292,9 @@ export default function Settings() {
const isAdminRole = (role) => role.name === 'admin'
function get2FADescription() {
if (require2FALoading) return 'Načítání...'
if (require2FALoading) {
return <div className="admin-skeleton-line" style={{ width: '200px', height: '12px' }} />
}
if (require2FA) return 'Všichni uživatelé musí mít aktivní 2FA pro přístup do systému'
return '2FA je volitelná - uživatelé si ji mohou aktivovat v profilu'
}

View File

@@ -216,48 +216,24 @@ export default function Trips() {
</div>
<div className="admin-skeleton-line h-10" style={{ width: '140px', borderRadius: '8px' }} />
</div>
<div className="admin-grid admin-grid-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: '1.25rem' }}>
<div 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' }} />
{[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 className="admin-skeleton-line w-1/4" />
</div>
<div className="admin-skeleton-row">
<div className="admin-skeleton-line circle" />
<div style={{ flex: 1 }}>
<div className="admin-skeleton-line w-1/2" style={{ marginBottom: '0.5rem' }} />
<div className="admin-skeleton-line w-1/3" style={{ height: '10px' }} />
</div>
<div className="admin-skeleton-line w-1/4" />
</div>
<div className="admin-skeleton-row">
<div className="admin-skeleton-line circle" />
<div style={{ flex: 1 }}>
<div className="admin-skeleton-line w-3/4" 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 className="admin-skeleton-row">
<div className="admin-skeleton-line circle" />
<div style={{ flex: 1 }}>
<div className="admin-skeleton-line w-1/2" style={{ marginBottom: '0.5rem' }} />
<div className="admin-skeleton-line w-1/3" style={{ height: '10px' }} />
</div>
<div className="admin-skeleton-line w-1/4" />
</div>
<div 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>
</div>