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:
@@ -4,7 +4,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 IconButton from "@mui/material/IconButton";
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
CheckboxField,
|
||||
ConfirmDialog,
|
||||
LoadingState,
|
||||
PageEnter,
|
||||
} from "../ui";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
@@ -286,13 +286,9 @@ export default function ProjectDetail() {
|
||||
const canEdit = hasPermission("projects.edit");
|
||||
|
||||
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",
|
||||
@@ -347,324 +343,301 @@ export default function ProjectDetail() {
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Box>
|
||||
|
||||
{/* Basic info form */}
|
||||
<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>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Field label="Číslo projektu">
|
||||
<TextField value={project.project_number} fullWidth disabled />
|
||||
</Field>
|
||||
<Field label="Název">
|
||||
<TextField
|
||||
value={form.name}
|
||||
onChange={(e) => updateForm("name", e.target.value)}
|
||||
placeholder="Název projektu"
|
||||
fullWidth
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Zákazník">
|
||||
<TextField
|
||||
value={project.customer_name || "—"}
|
||||
fullWidth
|
||||
disabled
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Zodpovědná osoba">
|
||||
<Select
|
||||
value={form.responsible_user_id}
|
||||
onChange={(v) => updateForm("responsible_user_id", v)}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<MenuItem value="">— Nevybráno —</MenuItem>
|
||||
{users.map((u) => (
|
||||
<MenuItem key={u.id} value={String(u.id)}>
|
||||
{u.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Field label="Stav">
|
||||
<Select
|
||||
value={form.status}
|
||||
onChange={(v) => updateForm("status", v)}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<MenuItem value="aktivni">Aktivní</MenuItem>
|
||||
<MenuItem value="dokonceny">Dokončený</MenuItem>
|
||||
<MenuItem value="zruseny">Zrušený</MenuItem>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="Datum zahájení">
|
||||
<DateField
|
||||
value={form.start_date}
|
||||
onChange={(val) => updateForm("start_date", val)}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Datum ukončení">
|
||||
<DateField
|
||||
value={form.end_date}
|
||||
onChange={(val) => updateForm("end_date", val)}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</Field>
|
||||
</Box>
|
||||
</Card>
|
||||
</motion.div>
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
Základní údaje
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Field label="Číslo projektu">
|
||||
<TextField value={project.project_number} fullWidth disabled />
|
||||
</Field>
|
||||
<Field label="Název">
|
||||
<TextField
|
||||
value={form.name}
|
||||
onChange={(e) => updateForm("name", e.target.value)}
|
||||
placeholder="Název projektu"
|
||||
fullWidth
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Zákazník">
|
||||
<TextField
|
||||
value={project.customer_name || "—"}
|
||||
fullWidth
|
||||
disabled
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Zodpovědná osoba">
|
||||
<Select
|
||||
value={form.responsible_user_id}
|
||||
onChange={(v) => updateForm("responsible_user_id", v)}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<MenuItem value="">— Nevybráno —</MenuItem>
|
||||
{users.map((u) => (
|
||||
<MenuItem key={u.id} value={String(u.id)}>
|
||||
{u.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Field label="Stav">
|
||||
<Select
|
||||
value={form.status}
|
||||
onChange={(v) => updateForm("status", v)}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<MenuItem value="aktivni">Aktivní</MenuItem>
|
||||
<MenuItem value="dokonceny">Dokončený</MenuItem>
|
||||
<MenuItem value="zruseny">Zrušený</MenuItem>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="Datum zahájení">
|
||||
<DateField
|
||||
value={form.start_date}
|
||||
onChange={(val) => updateForm("start_date", val)}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Datum ukončení">
|
||||
<DateField
|
||||
value={form.end_date}
|
||||
onChange={(val) => updateForm("end_date", val)}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</Field>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Notes */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.08 }}
|
||||
>
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
Poznámky
|
||||
</Typography>
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
Poznámky
|
||||
</Typography>
|
||||
|
||||
{/* Add note */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<TextField
|
||||
value={newNote}
|
||||
onChange={(e) => setNewNote(e.target.value)}
|
||||
multiline
|
||||
minRows={2}
|
||||
placeholder="Napište poznámku..."
|
||||
fullWidth
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && e.ctrlKey && newNote.trim()) {
|
||||
handleAddNote();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={handleAddNote}
|
||||
disabled={addingNote || !newNote.trim()}
|
||||
>
|
||||
{addingNote ? "Přidávání..." : "Přidat poznámku"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Legacy notes (read-only) */}
|
||||
{project.notes && (
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
bgcolor: "action.hover",
|
||||
borderRadius: 2,
|
||||
mb: 1,
|
||||
}}
|
||||
{/* Add note */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<TextField
|
||||
value={newNote}
|
||||
onChange={(e) => setNewNote(e.target.value)}
|
||||
multiline
|
||||
minRows={2}
|
||||
placeholder="Napište poznámku..."
|
||||
fullWidth
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && e.ctrlKey && newNote.trim()) {
|
||||
handleAddNote();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={handleAddNote}
|
||||
disabled={addingNote || !newNote.trim()}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: "block", mb: 0.5 }}
|
||||
>
|
||||
Starší poznámka (před zavedením systému)
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
{project.notes}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{addingNote ? "Přidávání..." : "Přidat poznámku"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Notes list */}
|
||||
{notes.length === 0 && !project.notes && (
|
||||
{/* Legacy notes (read-only) */}
|
||||
{project.notes && (
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
bgcolor: "action.hover",
|
||||
borderRadius: 2,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: "block", mb: 0.5 }}
|
||||
>
|
||||
Starší poznámka (před zavedením systému)
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: "center", py: 2 }}
|
||||
sx={{ whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
Zatím žádné poznámky
|
||||
{project.notes}
|
||||
</Typography>
|
||||
)}
|
||||
{(notes.length > 0 || project.notes) && (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{notes.map((note) => (
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Notes list */}
|
||||
{notes.length === 0 && !project.notes && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: "center", py: 2 }}
|
||||
>
|
||||
Zatím žádné poznámky
|
||||
</Typography>
|
||||
)}
|
||||
{(notes.length > 0 || project.notes) && (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{notes.map((note) => (
|
||||
<Box
|
||||
key={note.id}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
bgcolor: "action.hover",
|
||||
borderRadius: 2,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
key={note.id}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
bgcolor: "action.hover",
|
||||
borderRadius: 2,
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{note.user_name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatNoteDate(note.created_at)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ whiteSpace: "pre-wrap", lineHeight: 1.5 }}
|
||||
>
|
||||
{note.content}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{note.user_name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatNoteDate(note.created_at)}
|
||||
</Typography>
|
||||
</Box>
|
||||
{isAdmin && (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => handleDeleteNote(note.id)}
|
||||
title="Smazat poznámku"
|
||||
aria-label="Smazat poznámku"
|
||||
disabled={deletingNoteId === note.id}
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
{TrashIcon}
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ whiteSpace: "pre-wrap", lineHeight: 1.5 }}
|
||||
>
|
||||
{note.content}
|
||||
</Typography>
|
||||
</Box>
|
||||
{isAdmin && (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => handleDeleteNote(note.id)}
|
||||
title="Smazat poznámku"
|
||||
aria-label="Smazat poznámku"
|
||||
disabled={deletingNoteId === note.id}
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
{TrashIcon}
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Project File Manager */}
|
||||
<motion.div
|
||||
style={{ marginBottom: "1rem" }}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.12 }}
|
||||
>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<ProjectFileManager
|
||||
projectId={project.id}
|
||||
projectNumber={project.project_number}
|
||||
hasPermission={hasPermission}
|
||||
hasNasFolder={project.has_nas_folder ?? false}
|
||||
/>
|
||||
</motion.div>
|
||||
</Box>
|
||||
|
||||
{/* Links */}
|
||||
<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 }}>
|
||||
Propojení
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Objednávka
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{project.order_id ? (
|
||||
<Box
|
||||
component={RouterLink}
|
||||
to={`/orders/${project.order_id}`}
|
||||
sx={{
|
||||
color: "primary.main",
|
||||
textDecoration: "none",
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
{project.order_number}
|
||||
{project.order_status && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: "text.secondary", ml: 1 }}
|
||||
>
|
||||
(
|
||||
{STATUS_LABELS[project.order_status] ||
|
||||
project.order_status}
|
||||
)
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Nabídka
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{project.quotation_id ? (
|
||||
<Box
|
||||
component={RouterLink}
|
||||
to={`/offers/${project.quotation_id}`}
|
||||
sx={{
|
||||
color: "primary.main",
|
||||
textDecoration: "none",
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
{project.quotation_number}
|
||||
</Box>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
Propojení
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Objednávka
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{project.order_id ? (
|
||||
<Box
|
||||
component={RouterLink}
|
||||
to={`/orders/${project.order_id}`}
|
||||
sx={{
|
||||
color: "primary.main",
|
||||
textDecoration: "none",
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
{project.order_number}
|
||||
{project.order_status && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: "text.secondary", ml: 1 }}
|
||||
>
|
||||
(
|
||||
{STATUS_LABELS[project.order_status] ||
|
||||
project.order_status}
|
||||
)
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
</motion.div>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Nabídka
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{project.quotation_id ? (
|
||||
<Box
|
||||
component={RouterLink}
|
||||
to={`/offers/${project.quotation_id}`}
|
||||
sx={{
|
||||
color: "primary.main",
|
||||
textDecoration: "none",
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
{project.quotation_number}
|
||||
</Box>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={deleteConfirm}
|
||||
@@ -687,6 +660,6 @@ export default function ProjectDetail() {
|
||||
/>
|
||||
)}
|
||||
</ConfirmDialog>
|
||||
</Box>
|
||||
</PageEnter>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user