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

@@ -13,7 +13,6 @@ import { useApiMutation } from "../lib/queries/mutations";
import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext";
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 OrderConfirmationModal from "../components/OrderConfirmationModal";
@@ -31,6 +30,7 @@ import {
ConfirmDialog,
EmptyState,
LoadingState,
PageEnter,
type DataColumn,
} from "../ui";
@@ -390,13 +390,9 @@ export default function OrderDetail() {
];
return (
<Box>
<PageEnter>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
>
<Box>
<Box
sx={{
display: "flex",
@@ -498,339 +494,315 @@ export default function OrderDetail() {
)}
</Box>
</Box>
</motion.div>
</Box>
{/* Info card */}
<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 }}>
Informace
</Typography>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
gap: 2,
}}
>
<Box>
<Typography variant="caption" color="text.secondary">
Nabídka
</Typography>
<Typography
variant="body2"
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Informace
</Typography>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
gap: 2,
}}
>
<Box>
<Typography variant="caption" color="text.secondary">
Nabídka
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 500,
}}
>
<Box
component={RouterLink}
to={`/offers/${order.quotation_id}`}
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 500,
color: "primary.main",
textDecoration: "none",
"&:hover": { textDecoration: "underline" },
}}
>
{order.quotation_number}
</Box>
{order.project_code && (
<Box component="span" sx={{ color: "text.secondary", ml: 1 }}>
({order.project_code})
</Box>
)}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Projekt
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 500,
}}
>
{order.project ? (
<Box
component={RouterLink}
to={`/offers/${order.quotation_id}`}
to={`/projects/${order.project.id}`}
sx={{
color: "primary.main",
textDecoration: "none",
"&:hover": { textDecoration: "underline" },
}}
>
{order.quotation_number}
{order.project.project_number} {order.project.name}
</Box>
{order.project_code && (
<Box component="span" sx={{ color: "text.secondary", ml: 1 }}>
({order.project_code})
</Box>
)}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Projekt
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 500,
}}
>
{order.project ? (
<Box
component={RouterLink}
to={`/projects/${order.project.id}`}
sx={{
color: "primary.main",
textDecoration: "none",
"&:hover": { textDecoration: "underline" },
}}
>
{order.project.project_number} {order.project.name}
</Box>
) : (
"—"
)}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Zákazník
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 500,
}}
>
{order.customer_name || "—"}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Číslo obj. zákazníka
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{order.customer_order_number || "—"}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Měna
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{order.currency}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Datum vytvoření
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{formatDate(order.created_at)}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Příloha
</Typography>
<Box sx={{ mt: 0.5 }}>
{order.attachment_name ? (
<Button
variant="outlined"
color="inherit"
startIcon={FileIcon}
onClick={handleViewAttachment}
disabled={attachmentLoading}
>
{order.attachment_name}
</Button>
) : (
<Typography variant="body2"></Typography>
)}
</Box>
</Box>
) : (
"—"
)}
</Typography>
</Box>
</Card>
</motion.div>
{/* Items (read-only) */}
<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 }}>
Položky
</Typography>
<DataTable<ItemRow>
columns={itemColumns}
rows={itemRows}
rowKey={(item) => item.id ?? item._index}
empty={<EmptyState title="Žádné položky." />}
/>
{/* Totals */}
<Box
sx={{
mt: 2,
ml: "auto",
maxWidth: 360,
display: "flex",
flexDirection: "column",
gap: 0.5,
}}
>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="body2" color="text.secondary">
Mezisoučet:
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{formatCurrency(totals.subtotal, order.currency)}
</Typography>
</Box>
{Number(order.apply_vat) > 0 && (
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="body2" color="text.secondary">
DPH ({order.vat_rate}%):
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{formatCurrency(totals.vatAmount, order.currency)}
</Typography>
</Box>
)}
<Box
<Box>
<Typography variant="caption" color="text.secondary">
Zákazník
</Typography>
<Typography
variant="body2"
sx={{
display: "flex",
justifyContent: "space-between",
pt: 0.5,
mt: 0.5,
borderTop: 1,
borderColor: "divider",
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 500,
}}
>
<Typography variant="body2" sx={{ fontWeight: 700 }}>
Celkem k úhradě:
{order.customer_name || "—"}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Číslo obj. zákazníka
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{order.customer_order_number || "—"}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Měna
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{order.currency}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Datum vytvoření
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{formatDate(order.created_at)}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Příloha
</Typography>
<Box sx={{ mt: 0.5 }}>
{order.attachment_name ? (
<Button
variant="outlined"
color="inherit"
startIcon={FileIcon}
onClick={handleViewAttachment}
disabled={attachmentLoading}
>
{order.attachment_name}
</Button>
) : (
<Typography variant="body2"></Typography>
)}
</Box>
</Box>
</Box>
</Card>
{/* Items (read-only) */}
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Položky
</Typography>
<DataTable<ItemRow>
columns={itemColumns}
rows={itemRows}
rowKey={(item) => item.id ?? item._index}
empty={<EmptyState title="Žádné položky." />}
/>
{/* Totals */}
<Box
sx={{
mt: 2,
ml: "auto",
maxWidth: 360,
display: "flex",
flexDirection: "column",
gap: 0.5,
}}
>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="body2" color="text.secondary">
Mezisoučet:
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{formatCurrency(totals.subtotal, order.currency)}
</Typography>
</Box>
{Number(order.apply_vat) > 0 && (
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="body2" color="text.secondary">
DPH ({order.vat_rate}%):
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 700,
}}
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{formatCurrency(totals.total, order.currency)}
{formatCurrency(totals.vatAmount, order.currency)}
</Typography>
</Box>
)}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
pt: 0.5,
mt: 0.5,
borderTop: 1,
borderColor: "divider",
}}
>
<Typography variant="body2" sx={{ fontWeight: 700 }}>
Celkem k úhradě:
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 700,
}}
>
{formatCurrency(totals.total, order.currency)}
</Typography>
</Box>
</Card>
</motion.div>
</Box>
</Card>
{/* Sections (read-only) */}
{order.sections?.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.15 }}
>
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Rozsah projektu
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Rozsah projektu
</Typography>
{order.scope_title && (
<Typography variant="body2" sx={{ fontWeight: 500, mb: 1 }}>
{order.scope_title}
</Typography>
{order.scope_title && (
<Typography variant="body2" sx={{ fontWeight: 500, mb: 1 }}>
{order.scope_title}
</Typography>
)}
{order.scope_description && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{order.scope_description}
</Typography>
)}
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
{order.sections.map((section, index) => (
)}
{order.scope_description && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{order.scope_description}
</Typography>
)}
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
{order.sections.map((section, index) => (
<Box
key={section.id || index}
sx={{
border: 1,
borderColor: "divider",
borderRadius: 2,
overflow: "hidden",
}}
>
<Box
key={section.id || index}
sx={{
border: 1,
borderColor: "divider",
borderRadius: 2,
overflow: "hidden",
display: "flex",
alignItems: "center",
gap: 1,
px: 2,
py: 1,
bgcolor: "action.hover",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
px: 2,
py: 1,
bgcolor: "action.hover",
}}
<Typography
variant="body2"
color="text.secondary"
sx={{ fontWeight: 600 }}
>
<Typography
variant="body2"
color="text.secondary"
sx={{ fontWeight: 600 }}
>
{index + 1}.
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{(order.language === "CZ"
? section.title_cz || section.title
: section.title || section.title_cz) ||
`Sekce ${index + 1}`}
</Typography>
</Box>
{section.content && (
<Box
className="admin-rich-text-view"
sx={{ p: 2 }}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(section.content),
}}
/>
)}
{index + 1}.
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{(order.language === "CZ"
? section.title_cz || section.title
: section.title || section.title_cz) ||
`Sekce ${index + 1}`}
</Typography>
</Box>
))}
</Box>
</Card>
</motion.div>
{section.content && (
<Box
className="admin-rich-text-view"
sx={{ p: 2 }}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(section.content),
}}
/>
)}
</Box>
))}
</Box>
</Card>
)}
{/* Notes (editable) */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.2 }}
>
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Poznámky
</Typography>
<Field label="Poznámky">
<TextField
value={notes}
onChange={(e) => setNotes(e.target.value)}
multiline
minRows={4}
placeholder="Interní poznámky k objednávce..."
fullWidth
disabled={!hasPermission("orders.edit")}
/>
</Field>
{hasPermission("orders.edit") && (
<Box sx={{ mt: 1 }}>
<Button
variant="outlined"
color="inherit"
onClick={handleSaveNotes}
disabled={saving}
>
{saving ? "Ukládání..." : "Uložit poznámky"}
</Button>
</Box>
)}
</Card>
</motion.div>
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Poznámky
</Typography>
<Field label="Poznámky">
<TextField
value={notes}
onChange={(e) => setNotes(e.target.value)}
multiline
minRows={4}
placeholder="Interní poznámky k objednávce..."
fullWidth
disabled={!hasPermission("orders.edit")}
/>
</Field>
{hasPermission("orders.edit") && (
<Box sx={{ mt: 1 }}>
<Button
variant="outlined"
color="inherit"
onClick={handleSaveNotes}
disabled={saving}
>
{saving ? "Ukládání..." : "Uložit poznámky"}
</Button>
</Box>
)}
</Card>
{/* Status change confirmation */}
<ConfirmDialog
@@ -887,6 +859,6 @@ export default function OrderDetail() {
applyVat={!!order.apply_vat}
/>
)}
</Box>
</PageEnter>
);
}