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