feat(mui): consistent staggered page entrance across all 43 route pages
Adopt the PageEnter wrapper as every page's outermost render element and remove the ad-hoc per-page entrance motion.div wrappers. Every page now enters the same way — all top-level sections rise+fade in, staggered — so nothing appears instantly and the motion is identical app-wide. Presentation-only: no data/logic/hooks/invalidate/permissions touched. Embedded sub-tabs (CompanySettings, ReceivedInvoices), Login (auth shell) and the dev UiKit are intentionally excluded. Gates: tsc -b --noEmit=0, build=0, vitest 152/152. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { motion } from "framer-motion";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
@@ -36,6 +35,7 @@ import {
|
||||
ProgressBar,
|
||||
StatusChip,
|
||||
PageHeader,
|
||||
PageEnter,
|
||||
EmptyState,
|
||||
LoadingState,
|
||||
type DataColumn,
|
||||
@@ -494,196 +494,169 @@ export default function AttendanceHistory() {
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<PageHeader
|
||||
title="Historie docházky"
|
||||
subtitle={computed.monthName}
|
||||
actions={
|
||||
records.length > 0 ? (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
startIcon={PrintIcon}
|
||||
onClick={handlePrint}
|
||||
title="Tisk docházky"
|
||||
>
|
||||
Tisk
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
<PageEnter>
|
||||
<PageHeader
|
||||
title="Historie docházky"
|
||||
subtitle={computed.monthName}
|
||||
actions={
|
||||
records.length > 0 ? (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
startIcon={PrintIcon}
|
||||
onClick={handlePrint}
|
||||
title="Tisk docházky"
|
||||
>
|
||||
Tisk
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Filters */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
>
|
||||
<FilterBar>
|
||||
<Box sx={{ flex: "1 1 200px" }}>
|
||||
<Field label="Měsíc">
|
||||
<MonthField value={month} onChange={(val) => setMonth(val)} />
|
||||
</Field>
|
||||
</Box>
|
||||
</FilterBar>
|
||||
</motion.div>
|
||||
<FilterBar>
|
||||
<Box sx={{ flex: "1 1 200px" }}>
|
||||
<Field label="Měsíc">
|
||||
<MonthField value={month} onChange={(val) => setMonth(val)} />
|
||||
</Field>
|
||||
</Box>
|
||||
</FilterBar>
|
||||
|
||||
{/* Monthly Fund Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.08 }}
|
||||
>
|
||||
<Card sx={{ mt: 3 }}>
|
||||
{isPending ? (
|
||||
<LoadingState />
|
||||
) : (
|
||||
<Card sx={{ mt: 3 }}>
|
||||
{isPending ? (
|
||||
<LoadingState />
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 2,
|
||||
bgcolor: "info.light",
|
||||
color: "info.main",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: "200px" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 2,
|
||||
bgcolor: "info.light",
|
||||
color: "info.main",
|
||||
flexShrink: 0,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
mb: 0.75,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
<Typography sx={{ fontWeight: 600, fontSize: "1rem" }}>
|
||||
Fond: {fund.worked}h / {fund.fund}h
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: "0.8125rem" }}
|
||||
>
|
||||
<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>
|
||||
{fund.business_days} prac. dnů
|
||||
{fund.holiday_count > 0 && ` + ${fund.holiday_count} svátky`}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: "200px" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
mb: 0.75,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontWeight: 600, fontSize: "1rem" }}>
|
||||
Fond: {fund.worked}h / {fund.fund}h
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: "0.8125rem" }}
|
||||
>
|
||||
{fund.business_days} prac. dnů
|
||||
{fund.holiday_count > 0 &&
|
||||
` + ${fund.holiday_count} svátky`}
|
||||
</Typography>
|
||||
<ProgressBar value={progressValue} color={progressColor} />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 0.5,
|
||||
mt: 1,
|
||||
minHeight: "1.5rem",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
||||
{computed.totalMinutes > 0 && (
|
||||
<StatusChip
|
||||
color="info"
|
||||
label={`Práce: ${formatHoursDecimal(computed.totalMinutes)}h`}
|
||||
title="Odpracováno (reálně)"
|
||||
/>
|
||||
)}
|
||||
{computed.vacationHours > 0 && (
|
||||
<StatusChip
|
||||
color="success"
|
||||
label={`Dov: ${computed.vacationHours}h`}
|
||||
/>
|
||||
)}
|
||||
{computed.sickHours > 0 && (
|
||||
<StatusChip
|
||||
color="error"
|
||||
label={`Nem: ${computed.sickHours}h`}
|
||||
/>
|
||||
)}
|
||||
{computed.freeHolidayHours > 0 && (
|
||||
<StatusChip
|
||||
color="warning"
|
||||
label={`Sv: ${Math.round(computed.freeHolidayHours)}h`}
|
||||
/>
|
||||
)}
|
||||
{computed.unpaidHours > 0 && (
|
||||
<StatusChip
|
||||
color="default"
|
||||
label={`Nep: ${computed.unpaidHours}h`}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<ProgressBar value={progressValue} color={progressColor} />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 0.5,
|
||||
mt: 1,
|
||||
minHeight: "1.5rem",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ fontWeight: 600, color: deltaColor }}
|
||||
>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
||||
{computed.totalMinutes > 0 && (
|
||||
<StatusChip
|
||||
color="info"
|
||||
label={`Práce: ${formatHoursDecimal(computed.totalMinutes)}h`}
|
||||
title="Odpracováno (reálně)"
|
||||
/>
|
||||
)}
|
||||
{computed.vacationHours > 0 && (
|
||||
<StatusChip
|
||||
color="success"
|
||||
label={`Dov: ${computed.vacationHours}h`}
|
||||
/>
|
||||
)}
|
||||
{computed.sickHours > 0 && (
|
||||
<StatusChip
|
||||
color="error"
|
||||
label={`Nem: ${computed.sickHours}h`}
|
||||
/>
|
||||
)}
|
||||
{computed.freeHolidayHours > 0 && (
|
||||
<StatusChip
|
||||
color="warning"
|
||||
label={`Sv: ${Math.round(computed.freeHolidayHours)}h`}
|
||||
/>
|
||||
)}
|
||||
{computed.unpaidHours > 0 && (
|
||||
<StatusChip
|
||||
color="default"
|
||||
label={`Nep: ${computed.unpaidHours}h`}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ fontWeight: 600, color: deltaColor }}
|
||||
>
|
||||
{computed.delta > 0
|
||||
? `Přesčas: +${formatHoursDecimal(computed.delta * 60)}h`
|
||||
: computed.delta < 0
|
||||
? `Zbývá: ${formatHoursDecimal(-computed.delta * 60)}h`
|
||||
: "Fond splněn"}
|
||||
</Typography>
|
||||
</Box>
|
||||
{computed.delta > 0
|
||||
? `Přesčas: +${formatHoursDecimal(computed.delta * 60)}h`
|
||||
: computed.delta < 0
|
||||
? `Zbývá: ${formatHoursDecimal(-computed.delta * 60)}h`
|
||||
: "Fond splněn"}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Records Table */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.12 }}
|
||||
>
|
||||
<Card sx={{ mt: 3 }}>
|
||||
{isPending ? (
|
||||
<LoadingState />
|
||||
) : (
|
||||
<DataTable<AttendanceRecord>
|
||||
columns={columns}
|
||||
rows={records}
|
||||
rowKey={(record) => record.id}
|
||||
empty={
|
||||
<EmptyState title="Za tento měsíc nejsou žádné záznamy." />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</motion.div>
|
||||
<Card sx={{ mt: 3 }}>
|
||||
{isPending ? (
|
||||
<LoadingState />
|
||||
) : (
|
||||
<DataTable<AttendanceRecord>
|
||||
columns={columns}
|
||||
rows={records}
|
||||
rowKey={(record) => record.id}
|
||||
empty={<EmptyState title="Za tento měsíc nejsou žádné záznamy." />}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Hidden Print Content */}
|
||||
{records.length > 0 && (
|
||||
@@ -888,7 +861,7 @@ export default function AttendanceHistory() {
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
</PageEnter>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user