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:
BOHA
2026-06-07 10:13:06 +02:00
parent b273614128
commit 39fe84ce99
43 changed files with 4956 additions and 5709 deletions

View File

@@ -1,7 +1,6 @@
import { useState, useEffect, useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
import { motion } from "framer-motion";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import MenuItem from "@mui/material/MenuItem";
@@ -29,6 +28,7 @@ import {
LoadingState,
PageHeader,
ConfirmDialog,
PageEnter,
type DataColumn,
} from "../ui";
@@ -356,145 +356,35 @@ export default function WarehouseItemDetail() {
];
return (
<Box>
<PageEnter>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
>
<PageHeader
title={title}
subtitle={subtitle}
actions={
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Button
component={RouterLink}
to="/warehouse/items"
variant="outlined"
color="inherit"
startIcon={BackIcon}
>
Zpět
</Button>
{headerActions}
</Box>
}
/>
</motion.div>
<PageHeader
title={title}
subtitle={subtitle}
actions={
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Button
component={RouterLink}
to="/warehouse/items"
variant="outlined"
color="inherit"
startIcon={BackIcon}
>
Zpět
</Button>
{headerActions}
</Box>
}
/>
{/* Basic info */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.06 }}
>
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Základní údaje
</Typography>
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Základní údaje
</Typography>
{editing || isNew ? (
<Box sx={{ display: "flex", flexDirection: "column", gap: 0 }}>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
}}
>
<Field label="Číslo položky" error={errors.item_number}>
<TextField
value={form.item_number}
onChange={(e) => {
updateForm("item_number", e.target.value);
setErrors((prev) => ({ ...prev, item_number: "" }));
}}
placeholder="Např. SKL-001"
fullWidth
/>
</Field>
<Field label="Název" required error={errors.name}>
<TextField
value={form.name}
error={!!errors.name}
onChange={(e) => {
updateForm("name", e.target.value);
setErrors((prev) => ({ ...prev, name: "" }));
}}
placeholder="Název položky"
fullWidth
/>
</Field>
</Box>
<Field label="Popis">
<TextField
multiline
minRows={3}
value={form.description}
onChange={(e) => updateForm("description", e.target.value)}
placeholder="Volitelný popis položky"
fullWidth
/>
</Field>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
gap: 2,
}}
>
<Field label="Kategorie">
<Select
value={form.category_id}
onChange={(v) => updateForm("category_id", v)}
>
<MenuItem value=""> Bez kategorie </MenuItem>
{categories.map((cat) => (
<MenuItem key={cat.id} value={String(cat.id)}>
{cat.name}
</MenuItem>
))}
</Select>
</Field>
<Field label="Jednotka" required error={errors.unit}>
<TextField
value={form.unit}
error={!!errors.unit}
onChange={(e) => {
updateForm("unit", e.target.value);
setErrors((prev) => ({ ...prev, unit: "" }));
}}
placeholder="ks, m, kg..."
fullWidth
/>
</Field>
<Field
label="Minimální množství"
hint="Upozornění při poklesu pod tuto hranici"
>
<TextField
type="number"
inputProps={{ inputMode: "numeric", min: 0 }}
value={form.min_quantity}
onChange={(e) => updateForm("min_quantity", e.target.value)}
placeholder="0"
fullWidth
/>
</Field>
</Box>
<Field label="Poznámky">
<TextField
multiline
minRows={2}
value={form.notes}
onChange={(e) => updateForm("notes", e.target.value)}
placeholder="Volitelné poznámky"
fullWidth
/>
</Field>
</Box>
) : (
{editing || isNew ? (
<Box sx={{ display: "flex", flexDirection: "column", gap: 0 }}>
<Box
sx={{
display: "grid",
@@ -502,166 +392,246 @@ export default function WarehouseItemDetail() {
gap: 2,
}}
>
{/* Číslo položky */}
<Box>
<Typography variant="caption" color="text.secondary">
Číslo položky
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{item?.item_number || "—"}
</Typography>
</Box>
{/* Název */}
<Box>
<Typography variant="caption" color="text.secondary">
Název
</Typography>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{item?.name || "—"}
</Typography>
</Box>
{/* Popis */}
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
<Typography variant="caption" color="text.secondary">
Popis
</Typography>
<Typography variant="body2">
{item?.description || "—"}
</Typography>
</Box>
{/* Kategorie */}
<Box>
<Typography variant="caption" color="text.secondary">
Kategorie
</Typography>
<Typography variant="body2">
{item?.category?.name || "—"}
</Typography>
</Box>
{/* Jednotka */}
<Box>
<Typography variant="caption" color="text.secondary">
Jednotka
</Typography>
<Typography variant="body2">{item?.unit || "—"}</Typography>
</Box>
{/* Minimální množství */}
<Box>
<Typography variant="caption" color="text.secondary">
Minimální množství
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{item?.min_quantity != null ? item.min_quantity : "—"}
</Typography>
</Box>
{/* Poznámky */}
{item?.notes && (
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
<Typography variant="caption" color="text.secondary">
Poznámky
</Typography>
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
{item.notes}
</Typography>
</Box>
)}
<Field label="Číslo položky" error={errors.item_number}>
<TextField
value={form.item_number}
onChange={(e) => {
updateForm("item_number", e.target.value);
setErrors((prev) => ({ ...prev, item_number: "" }));
}}
placeholder="Např. SKL-001"
fullWidth
/>
</Field>
<Field label="Název" required error={errors.name}>
<TextField
value={form.name}
error={!!errors.name}
onChange={(e) => {
updateForm("name", e.target.value);
setErrors((prev) => ({ ...prev, name: "" }));
}}
placeholder="Název položky"
fullWidth
/>
</Field>
</Box>
)}
</Card>
</motion.div>
{/* Stock info — only in view mode, for existing items */}
{id !== "new" && !editing && item && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.08 }}
>
<Field label="Popis">
<TextField
multiline
minRows={3}
value={form.description}
onChange={(e) => updateForm("description", e.target.value)}
placeholder="Volitelný popis položky"
fullWidth
/>
</Field>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
gap: 2,
}}
>
<Field label="Kategorie">
<Select
value={form.category_id}
onChange={(v) => updateForm("category_id", v)}
>
<MenuItem value=""> Bez kategorie </MenuItem>
{categories.map((cat) => (
<MenuItem key={cat.id} value={String(cat.id)}>
{cat.name}
</MenuItem>
))}
</Select>
</Field>
<Field label="Jednotka" required error={errors.unit}>
<TextField
value={form.unit}
error={!!errors.unit}
onChange={(e) => {
updateForm("unit", e.target.value);
setErrors((prev) => ({ ...prev, unit: "" }));
}}
placeholder="ks, m, kg..."
fullWidth
/>
</Field>
<Field
label="Minimální množství"
hint="Upozornění při poklesu pod tuto hranici"
>
<TextField
type="number"
inputProps={{ inputMode: "numeric", min: 0 }}
value={form.min_quantity}
onChange={(e) => updateForm("min_quantity", e.target.value)}
placeholder="0"
fullWidth
/>
</Field>
</Box>
<Field label="Poznámky">
<TextField
multiline
minRows={2}
value={form.notes}
onChange={(e) => updateForm("notes", e.target.value)}
placeholder="Volitelné poznámky"
fullWidth
/>
</Field>
</Box>
) : (
<Box
sx={{
display: "grid",
gridTemplateColumns: {
xs: "1fr 1fr",
md: "1fr 1fr 1fr 1fr",
},
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
mb: 3,
}}
>
<StatCard
label="Celkové množství"
value={`${item.total_quantity ?? 0} ${item.unit}`}
color="info"
/>
<StatCard
label="K dispozici"
value={`${item.available_quantity ?? 0} ${item.unit}`}
color="success"
/>
<StatCard
label="Hodnota zásob"
value={
item.stock_value != null
? formatCurrency(item.stock_value, "CZK")
: "0,00 Kč"
}
color="warning"
/>
<StatCard
label="Pod minimem"
value={item.below_minimum ? "Ano" : "Ne"}
color={item.below_minimum ? "error" : "info"}
/>
{/* Číslo položky */}
<Box>
<Typography variant="caption" color="text.secondary">
Číslo položky
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{item?.item_number || "—"}
</Typography>
</Box>
{/* Název */}
<Box>
<Typography variant="caption" color="text.secondary">
Název
</Typography>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{item?.name || "—"}
</Typography>
</Box>
{/* Popis */}
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
<Typography variant="caption" color="text.secondary">
Popis
</Typography>
<Typography variant="body2">
{item?.description || "—"}
</Typography>
</Box>
{/* Kategorie */}
<Box>
<Typography variant="caption" color="text.secondary">
Kategorie
</Typography>
<Typography variant="body2">
{item?.category?.name || "—"}
</Typography>
</Box>
{/* Jednotka */}
<Box>
<Typography variant="caption" color="text.secondary">
Jednotka
</Typography>
<Typography variant="body2">{item?.unit || "—"}</Typography>
</Box>
{/* Minimální množství */}
<Box>
<Typography variant="caption" color="text.secondary">
Minimální množství
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{item?.min_quantity != null ? item.min_quantity : "—"}
</Typography>
</Box>
{/* Poznámky */}
{item?.notes && (
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
<Typography variant="caption" color="text.secondary">
Poznámky
</Typography>
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
{item.notes}
</Typography>
</Box>
)}
</Box>
</motion.div>
)}
</Card>
{/* Stock info — only in view mode, for existing items */}
{id !== "new" && !editing && item && (
<Box
sx={{
display: "grid",
gridTemplateColumns: {
xs: "1fr 1fr",
md: "1fr 1fr 1fr 1fr",
},
gap: 2,
mb: 3,
}}
>
<StatCard
label="Celkové množství"
value={`${item.total_quantity ?? 0} ${item.unit}`}
color="info"
/>
<StatCard
label="K dispozici"
value={`${item.available_quantity ?? 0} ${item.unit}`}
color="success"
/>
<StatCard
label="Hodnota zásob"
value={
item.stock_value != null
? formatCurrency(item.stock_value, "CZK")
: "0,00 Kč"
}
color="warning"
/>
<StatCard
label="Pod minimem"
value={item.below_minimum ? "Ano" : "Ne"}
color={item.below_minimum ? "error" : "info"}
/>
</Box>
)}
{/* Batches table — only in view mode, for existing items */}
{id !== "new" && !editing && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.1 }}
>
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Dávky (FIFO)
</Typography>
<DataTable<Batch>
columns={batchColumns}
rows={batches}
rowKey={(b) => b.id}
empty={<EmptyState title="Zatím žádné dávky" />}
/>
</Card>
</motion.div>
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Dávky (FIFO)
</Typography>
<DataTable<Batch>
columns={batchColumns}
rows={batches}
rowKey={(b) => b.id}
empty={<EmptyState title="Zatím žádné dávky" />}
/>
</Card>
)}
{/* Locations table — only in view mode, for existing items */}
{id !== "new" && !editing && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.12 }}
>
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Umístění
</Typography>
<DataTable<ItemLocation>
columns={locationColumns}
rows={locations}
rowKey={(l) => l.id}
empty={<EmptyState title="Zatím žádná umístění" />}
/>
</Card>
</motion.div>
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Umístění
</Typography>
<DataTable<ItemLocation>
columns={locationColumns}
rows={locations}
rowKey={(l) => l.id}
empty={<EmptyState title="Zatím žádná umístění" />}
/>
</Card>
)}
{/* Delete confirmation */}
@@ -675,6 +645,6 @@ export default function WarehouseItemDetail() {
confirmVariant="danger"
loading={deleteMutation.isPending}
/>
</Box>
</PageEnter>
);
}