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,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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user