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

@@ -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>
);
}