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 { useState, useEffect, useRef } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { parseISO, isValid } from "date-fns";
|
import { parseISO, isValid } from "date-fns";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
@@ -30,6 +29,7 @@ import {
|
|||||||
StatusChip,
|
StatusChip,
|
||||||
TextField,
|
TextField,
|
||||||
ProgressBar,
|
ProgressBar,
|
||||||
|
PageEnter,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
@@ -614,24 +614,18 @@ export default function Attendance() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<Box sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="h4">Docházka</Typography>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
transition={{ duration: 0.25 }}
|
{new Date().toLocaleDateString("cs-CZ", {
|
||||||
>
|
weekday: "long",
|
||||||
<Box sx={{ mb: 3 }}>
|
day: "numeric",
|
||||||
<Typography variant="h4">Docházka</Typography>
|
month: "long",
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
year: "numeric",
|
||||||
{new Date().toLocaleDateString("cs-CZ", {
|
})}
|
||||||
weekday: "long",
|
</Typography>
|
||||||
day: "numeric",
|
</Box>
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -642,11 +636,7 @@ export default function Attendance() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Left Column - Clock In/Out */}
|
{/* Left Column - Clock In/Out */}
|
||||||
<motion.div
|
<>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
|
||||||
>
|
|
||||||
<Card>
|
<Card>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -986,35 +976,63 @@ export default function Attendance() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</>
|
||||||
|
|
||||||
{/* Right Column - Stats & Quick Links */}
|
{/* Right Column - Stats & Quick Links */}
|
||||||
<motion.div
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
{/* Leave Balance Card */}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<StatCard
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
icon={VacationIcon}
|
||||||
>
|
color="success"
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
label={`Dovolená ${new Date().getFullYear()}`}
|
||||||
{/* Leave Balance Card */}
|
value={
|
||||||
|
<>
|
||||||
|
{vacationDaysRemaining}{" "}
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ fontFamily: "inherit" }}
|
||||||
|
>
|
||||||
|
{czechPlural(vacationDaysRemaining, "den", "dny", "dnů")}
|
||||||
|
{vacationHoursRemaining > 0 && ` ${vacationHoursRemaining}h`}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
mb: 0.75,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Celkem: {leaveBalance.vacation_total}h</span>
|
||||||
|
<span>Čerpáno: {leaveBalance.vacation_used}h</span>
|
||||||
|
</Box>
|
||||||
|
<ProgressBar
|
||||||
|
value={
|
||||||
|
leaveBalance.vacation_total > 0
|
||||||
|
? (leaveBalance.vacation_remaining /
|
||||||
|
leaveBalance.vacation_total) *
|
||||||
|
100
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
color="success"
|
||||||
|
height={6}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Monthly Fund Card */}
|
||||||
|
{data.monthly_fund && (
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={VacationIcon}
|
icon={CalendarIcon}
|
||||||
color="success"
|
color="info"
|
||||||
label={`Dovolená ${new Date().getFullYear()}`}
|
label={data.monthly_fund.month_name}
|
||||||
value={
|
value={`${data.monthly_fund.worked}h / ${data.monthly_fund.fund}h`}
|
||||||
<>
|
|
||||||
{vacationDaysRemaining}{" "}
|
|
||||||
<Typography
|
|
||||||
component="span"
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ fontFamily: "inherit" }}
|
|
||||||
>
|
|
||||||
{czechPlural(vacationDaysRemaining, "den", "dny", "dnů")}
|
|
||||||
{vacationHoursRemaining > 0 &&
|
|
||||||
` ${vacationHoursRemaining}h`}
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
footer={
|
footer={
|
||||||
<Box>
|
<Box>
|
||||||
<Box
|
<Box
|
||||||
@@ -1024,137 +1042,102 @@ export default function Attendance() {
|
|||||||
mb: 0.75,
|
mb: 0.75,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Celkem: {leaveBalance.vacation_total}h</span>
|
<span>Odpracováno: {data.monthly_fund.worked}h</span>
|
||||||
<span>Čerpáno: {leaveBalance.vacation_used}h</span>
|
{data.monthly_fund.overtime > 0 ? (
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{ color: "warning.main", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Přesčas: +{data.monthly_fund.overtime}h
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<span>Zbývá: {data.monthly_fund.remaining}h</span>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
value={
|
value={
|
||||||
leaveBalance.vacation_total > 0
|
data.monthly_fund.fund > 0
|
||||||
? (leaveBalance.vacation_remaining /
|
? Math.min(
|
||||||
leaveBalance.vacation_total) *
|
100,
|
||||||
100
|
(data.monthly_fund.covered /
|
||||||
|
data.monthly_fund.fund) *
|
||||||
|
100,
|
||||||
|
)
|
||||||
: 0
|
: 0
|
||||||
}
|
}
|
||||||
color="success"
|
color={getFundBarColor(data.monthly_fund)}
|
||||||
height={6}
|
height={6}
|
||||||
/>
|
/>
|
||||||
|
{data.monthly_fund.leave_hours > 0 && (
|
||||||
|
<Box sx={{ mt: 0.5, color: "text.disabled" }}>
|
||||||
|
{"Pokryto: "}
|
||||||
|
{data.monthly_fund.covered}h (práce{" "}
|
||||||
|
{data.monthly_fund.worked}h
|
||||||
|
{data.monthly_fund.vacation_hours > 0 &&
|
||||||
|
` + dovolená ${data.monthly_fund.vacation_hours}h`}
|
||||||
|
{data.monthly_fund.sick_hours > 0 &&
|
||||||
|
` + nemoc ${data.monthly_fund.sick_hours}h`}
|
||||||
|
{data.monthly_fund.unpaid_hours > 0 &&
|
||||||
|
` + neplacené ${data.monthly_fund.unpaid_hours}h`}
|
||||||
|
)
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Monthly Fund Card */}
|
{/* Sick Leave Card */}
|
||||||
{data.monthly_fund && (
|
<StatCard
|
||||||
<StatCard
|
icon={SickIcon}
|
||||||
icon={CalendarIcon}
|
color="error"
|
||||||
color="info"
|
label={`Nemoc ${new Date().getFullYear()}`}
|
||||||
label={data.monthly_fund.month_name}
|
value={`${leaveBalance.sick_used}h čerpáno`}
|
||||||
value={`${data.monthly_fund.worked}h / ${data.monthly_fund.fund}h`}
|
/>
|
||||||
footer={
|
|
||||||
<Box>
|
{/* Quick Links */}
|
||||||
<Box
|
<Card>
|
||||||
sx={{
|
<Typography
|
||||||
display: "flex",
|
variant="caption"
|
||||||
justifyContent: "space-between",
|
color="text.secondary"
|
||||||
mb: 0.75,
|
sx={{
|
||||||
}}
|
display: "block",
|
||||||
>
|
textTransform: "uppercase",
|
||||||
<span>Odpracováno: {data.monthly_fund.worked}h</span>
|
letterSpacing: ".06em",
|
||||||
{data.monthly_fund.overtime > 0 ? (
|
fontWeight: 700,
|
||||||
<Box
|
mb: 1,
|
||||||
component="span"
|
}}
|
||||||
sx={{ color: "warning.main", fontWeight: 600 }}
|
>
|
||||||
>
|
Rychlé odkazy
|
||||||
Přesčas: +{data.monthly_fund.overtime}h
|
</Typography>
|
||||||
</Box>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.25 }}>
|
||||||
) : (
|
<QuickLink
|
||||||
<span>Zbývá: {data.monthly_fund.remaining}h</span>
|
to="/attendance/requests"
|
||||||
)}
|
icon={RequestsIcon}
|
||||||
</Box>
|
label="Moje žádosti"
|
||||||
<ProgressBar
|
|
||||||
value={
|
|
||||||
data.monthly_fund.fund > 0
|
|
||||||
? Math.min(
|
|
||||||
100,
|
|
||||||
(data.monthly_fund.covered /
|
|
||||||
data.monthly_fund.fund) *
|
|
||||||
100,
|
|
||||||
)
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
color={getFundBarColor(data.monthly_fund)}
|
|
||||||
height={6}
|
|
||||||
/>
|
|
||||||
{data.monthly_fund.leave_hours > 0 && (
|
|
||||||
<Box sx={{ mt: 0.5, color: "text.disabled" }}>
|
|
||||||
{"Pokryto: "}
|
|
||||||
{data.monthly_fund.covered}h (práce{" "}
|
|
||||||
{data.monthly_fund.worked}h
|
|
||||||
{data.monthly_fund.vacation_hours > 0 &&
|
|
||||||
` + dovolená ${data.monthly_fund.vacation_hours}h`}
|
|
||||||
{data.monthly_fund.sick_hours > 0 &&
|
|
||||||
` + nemoc ${data.monthly_fund.sick_hours}h`}
|
|
||||||
{data.monthly_fund.unpaid_hours > 0 &&
|
|
||||||
` + neplacené ${data.monthly_fund.unpaid_hours}h`}
|
|
||||||
)
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<QuickLink
|
||||||
|
to="/attendance/history"
|
||||||
{/* Sick Leave Card */}
|
icon={HistoryIcon}
|
||||||
<StatCard
|
label="Historie docházky"
|
||||||
icon={SickIcon}
|
/>
|
||||||
color="error"
|
{hasPermission("attendance.manage") && (
|
||||||
label={`Nemoc ${new Date().getFullYear()}`}
|
|
||||||
value={`${leaveBalance.sick_used}h čerpáno`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Quick Links */}
|
|
||||||
<Card>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{
|
|
||||||
display: "block",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: ".06em",
|
|
||||||
fontWeight: 700,
|
|
||||||
mb: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Rychlé odkazy
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.25 }}>
|
|
||||||
<QuickLink
|
<QuickLink
|
||||||
to="/attendance/requests"
|
to="/attendance/admin"
|
||||||
icon={RequestsIcon}
|
icon={ManageIcon}
|
||||||
label="Moje žádosti"
|
label="Správa docházky"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{hasPermission("attendance.balances") && (
|
||||||
<QuickLink
|
<QuickLink
|
||||||
to="/attendance/history"
|
to="/attendance/balances"
|
||||||
icon={HistoryIcon}
|
icon={BalancesIcon}
|
||||||
label="Historie docházky"
|
label="Správa bilancí"
|
||||||
/>
|
/>
|
||||||
{hasPermission("attendance.manage") && (
|
)}
|
||||||
<QuickLink
|
</Box>
|
||||||
to="/attendance/admin"
|
</Card>
|
||||||
icon={ManageIcon}
|
</Box>
|
||||||
label="Správa docházky"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{hasPermission("attendance.balances") && (
|
|
||||||
<QuickLink
|
|
||||||
to="/attendance/balances"
|
|
||||||
icon={BalancesIcon}
|
|
||||||
label="Správa bilancí"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Leave Modal */}
|
{/* Leave Modal */}
|
||||||
@@ -1273,6 +1256,6 @@ export default function Attendance() {
|
|||||||
cancelText="Zrušit"
|
cancelText="Zrušit"
|
||||||
confirmVariant="primary"
|
confirmVariant="primary"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
@@ -18,6 +17,7 @@ import {
|
|||||||
FilterBar,
|
FilterBar,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
MonthField,
|
MonthField,
|
||||||
|
PageEnter,
|
||||||
Select,
|
Select,
|
||||||
StatCard,
|
StatCard,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
@@ -165,262 +165,236 @@ export default function AttendanceAdmin() {
|
|||||||
const hasTotals = Object.keys(data.user_totals).length > 0;
|
const hasTotals = Object.keys(data.user_totals).length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25 }}
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<Typography variant="h4">Správa docházky</Typography>
|
||||||
|
<Box sx={{ display: "flex", gap: 1.5, flexWrap: "wrap" }}>
|
||||||
|
{hasData && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
startIcon={PrintIcon}
|
||||||
|
onClick={handlePrint}
|
||||||
|
title="Tisk docházky"
|
||||||
|
>
|
||||||
|
Tisk
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="outlined" color="inherit" onClick={openBulkModal}>
|
||||||
|
Vyplnit měsíc
|
||||||
|
</Button>
|
||||||
|
<Button startIcon={AddIcon} onClick={openCreateModal}>
|
||||||
|
Přidat záznam
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<FilterBar>
|
||||||
|
<Box sx={{ flex: "0 0 200px" }}>
|
||||||
|
<Field label="Měsíc">
|
||||||
|
<MonthField value={month} onChange={(val) => setMonth(val)} />
|
||||||
|
</Field>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flex: "1 1 240px" }}>
|
||||||
|
<Field label="Zaměstnanec">
|
||||||
|
<Select
|
||||||
|
value={filterUserId}
|
||||||
|
onChange={setFilterUserId}
|
||||||
|
options={[
|
||||||
|
{ value: "", label: "Všichni" },
|
||||||
|
...data.users.map((user) => ({
|
||||||
|
value: String(user.id),
|
||||||
|
label: user.name,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Box>
|
||||||
|
</FilterBar>
|
||||||
|
|
||||||
|
{/* User Totals (KPI cards) */}
|
||||||
|
{hasTotals && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "grid",
|
||||||
alignItems: "flex-start",
|
gridTemplateColumns: {
|
||||||
justifyContent: "space-between",
|
xs: "1fr",
|
||||||
flexWrap: "wrap",
|
sm: "repeat(2, 1fr)",
|
||||||
|
lg: "repeat(3, 1fr)",
|
||||||
|
},
|
||||||
gap: 2,
|
gap: 2,
|
||||||
mb: 3,
|
mb: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h4">Správa docházky</Typography>
|
{Object.entries(data.user_totals).map(([uid, userData]) => {
|
||||||
<Box sx={{ display: "flex", gap: 1.5, flexWrap: "wrap" }}>
|
const ut = userData as UserTotalData;
|
||||||
{hasData && (
|
const balance = data.leave_balances[uid];
|
||||||
<Button
|
const statColor: StatCardColor = ut.working ? "success" : "default";
|
||||||
variant="outlined"
|
return (
|
||||||
color="inherit"
|
<StatCard
|
||||||
startIcon={PrintIcon}
|
key={uid}
|
||||||
onClick={handlePrint}
|
label={ut.name}
|
||||||
title="Tisk docházky"
|
value={
|
||||||
>
|
<Box
|
||||||
Tisk
|
component="span"
|
||||||
</Button>
|
sx={{
|
||||||
)}
|
display: "inline-flex",
|
||||||
<Button variant="outlined" color="inherit" onClick={openBulkModal}>
|
alignItems: "center",
|
||||||
Vyplnit měsíc
|
gap: 1,
|
||||||
</Button>
|
}}
|
||||||
<Button startIcon={AddIcon} onClick={openCreateModal}>
|
>
|
||||||
Přidat záznam
|
{formatMinutes(ut.minutes)}
|
||||||
</Button>
|
<StatusChip
|
||||||
</Box>
|
color={ut.working ? "success" : "default"}
|
||||||
</Box>
|
label={ut.working ? "✓" : "✗"}
|
||||||
</motion.div>
|
/>
|
||||||
|
</Box>
|
||||||
{/* Filters */}
|
}
|
||||||
<motion.div
|
color={statColor}
|
||||||
initial={{ opacity: 0, y: 12 }}
|
footer={
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<Box>
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
{/* Leave-type badges */}
|
||||||
>
|
|
||||||
<FilterBar>
|
|
||||||
<Box sx={{ flex: "0 0 200px" }}>
|
|
||||||
<Field label="Měsíc">
|
|
||||||
<MonthField value={month} onChange={(val) => setMonth(val)} />
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ flex: "1 1 240px" }}>
|
|
||||||
<Field label="Zaměstnanec">
|
|
||||||
<Select
|
|
||||||
value={filterUserId}
|
|
||||||
onChange={setFilterUserId}
|
|
||||||
options={[
|
|
||||||
{ value: "", label: "Všichni" },
|
|
||||||
...data.users.map((user) => ({
|
|
||||||
value: String(user.id),
|
|
||||||
label: user.name,
|
|
||||||
})),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
</FilterBar>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* User Totals (KPI cards) */}
|
|
||||||
{hasTotals && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.09 }}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: {
|
|
||||||
xs: "1fr",
|
|
||||||
sm: "repeat(2, 1fr)",
|
|
||||||
lg: "repeat(3, 1fr)",
|
|
||||||
},
|
|
||||||
gap: 2,
|
|
||||||
mb: 3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.entries(data.user_totals).map(([uid, userData]) => {
|
|
||||||
const ut = userData as UserTotalData;
|
|
||||||
const balance = data.leave_balances[uid];
|
|
||||||
const statColor: StatCardColor = ut.working
|
|
||||||
? "success"
|
|
||||||
: "default";
|
|
||||||
return (
|
|
||||||
<StatCard
|
|
||||||
key={uid}
|
|
||||||
label={ut.name}
|
|
||||||
value={
|
|
||||||
<Box
|
<Box
|
||||||
component="span"
|
|
||||||
sx={{
|
sx={{
|
||||||
display: "inline-flex",
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 0.5,
|
||||||
|
mb: 1,
|
||||||
|
minHeight: "1.75rem",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formatMinutes(ut.minutes)}
|
{ut.vacation_hours > 0 && (
|
||||||
<StatusChip
|
<StatusChip
|
||||||
color={ut.working ? "success" : "default"}
|
color="info"
|
||||||
label={ut.working ? "✓" : "✗"}
|
label={`Dov: ${ut.vacation_hours}h`}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{ut.sick_hours > 0 && (
|
||||||
|
<StatusChip
|
||||||
|
color="error"
|
||||||
|
label={`Nem: ${ut.sick_hours}h`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{ut.svatek !== undefined && ut.svatek > 0 && (
|
||||||
|
<StatusChip
|
||||||
|
color="warning"
|
||||||
|
label={`Sv: ${Math.round(ut.svatek)}h`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{ut.unpaid_hours > 0 && (
|
||||||
|
<StatusChip
|
||||||
|
color="default"
|
||||||
|
label={`Nep: ${ut.unpaid_hours}h`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
}
|
|
||||||
color={statColor}
|
|
||||||
footer={
|
|
||||||
<Box>
|
|
||||||
{/* Leave-type badges */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: 0.5,
|
|
||||||
mb: 1,
|
|
||||||
minHeight: "1.75rem",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ut.vacation_hours > 0 && (
|
|
||||||
<StatusChip
|
|
||||||
color="info"
|
|
||||||
label={`Dov: ${ut.vacation_hours}h`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{ut.sick_hours > 0 && (
|
|
||||||
<StatusChip
|
|
||||||
color="error"
|
|
||||||
label={`Nem: ${ut.sick_hours}h`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{ut.svatek !== undefined && ut.svatek > 0 && (
|
|
||||||
<StatusChip
|
|
||||||
color="warning"
|
|
||||||
label={`Sv: ${Math.round(ut.svatek)}h`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{ut.unpaid_hours > 0 && (
|
|
||||||
<StatusChip
|
|
||||||
color="default"
|
|
||||||
label={`Nep: ${ut.unpaid_hours}h`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Fond usage */}
|
{/* Fond usage */}
|
||||||
{ut.fund !== null &&
|
{ut.fund !== null &&
|
||||||
(() => {
|
(() => {
|
||||||
// Fond "used" = real_worked + vacation +
|
// Fond "used" = real_worked + vacation +
|
||||||
// worked_holiday + free_holiday (everything the user
|
// worked_holiday + free_holiday (everything the user
|
||||||
// actually got paid for this month).
|
// actually got paid for this month).
|
||||||
const fondUsed = getFondUsed(ut);
|
const fondUsed = getFondUsed(ut);
|
||||||
const fundVal = ut.fund ?? 0;
|
const fundVal = ut.fund ?? 0;
|
||||||
const delta = fondUsed - fundVal;
|
const delta = fondUsed - fundVal;
|
||||||
const pct = Math.min(
|
const pct = Math.min(
|
||||||
100,
|
100,
|
||||||
(fondUsed / (ut.fund || 1)) * 100,
|
(fondUsed / (ut.fund || 1)) * 100,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
mb: 0.5,
|
mb: 0.5,
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
>
|
>
|
||||||
|
Fond: {formatHoursDecimal(fondUsed * 60)}h /{" "}
|
||||||
|
{formatHoursDecimal(fundVal * 60)}h
|
||||||
|
</Typography>
|
||||||
|
{delta > 0 && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "warning.main",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Fond: {formatHoursDecimal(fondUsed * 60)}h /{" "}
|
+{formatHoursDecimal(delta * 60)}h
|
||||||
{formatHoursDecimal(fundVal * 60)}h
|
|
||||||
</Typography>
|
</Typography>
|
||||||
{delta > 0 && (
|
)}
|
||||||
<Typography
|
{delta < 0 && (
|
||||||
variant="caption"
|
<Typography
|
||||||
sx={{
|
variant="caption"
|
||||||
fontWeight: 600,
|
sx={{
|
||||||
color: "warning.main",
|
fontWeight: 600,
|
||||||
}}
|
color: "error.main",
|
||||||
>
|
}}
|
||||||
+{formatHoursDecimal(delta * 60)}h
|
>
|
||||||
</Typography>
|
-{formatHoursDecimal(Math.abs(delta) * 60)}h
|
||||||
)}
|
</Typography>
|
||||||
{delta < 0 && (
|
)}
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "error.main",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
-{formatHoursDecimal(Math.abs(delta) * 60)}h
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<ProgressBar
|
|
||||||
value={pct}
|
|
||||||
color={getFundBarColor(ut)}
|
|
||||||
height={4}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
<ProgressBar
|
||||||
})()}
|
value={pct}
|
||||||
|
color={getFundBarColor(ut)}
|
||||||
|
height={4}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Remaining vacation */}
|
{/* Remaining vacation */}
|
||||||
<Box sx={{ mt: 1.5 }}>
|
<Box sx={{ mt: 1.5 }}>
|
||||||
{balance ? (
|
{balance ? (
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
Zbývá dovolené:{" "}
|
Zbývá dovolené:{" "}
|
||||||
{balance.vacation_remaining.toFixed(1)}h /{" "}
|
{balance.vacation_remaining.toFixed(1)}h /{" "}
|
||||||
{balance.vacation_total}h
|
{balance.vacation_total}h
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
sx={{ visibility: "hidden" }}
|
sx={{ visibility: "hidden" }}
|
||||||
>
|
>
|
||||||
—
|
—
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
</Box>
|
||||||
/>
|
}
|
||||||
);
|
/>
|
||||||
})}
|
);
|
||||||
</Box>
|
})}
|
||||||
</motion.div>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Records Table */}
|
{/* Records Table */}
|
||||||
<motion.div
|
<Card>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<AttendanceShiftTable
|
||||||
animate={{ opacity: 1, y: 0 }}
|
records={data.records}
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
onEdit={openEditModal}
|
||||||
>
|
onDelete={(record) => setDeleteConfirm({ show: true, record })}
|
||||||
<Card>
|
/>
|
||||||
<AttendanceShiftTable
|
</Card>
|
||||||
records={data.records}
|
|
||||||
onEdit={openEditModal}
|
|
||||||
onDelete={(record) => setDeleteConfirm({ show: true, record })}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
<BulkAttendanceModal
|
<BulkAttendanceModal
|
||||||
@@ -481,6 +455,6 @@ export default function AttendanceAdmin() {
|
|||||||
confirmText="Smazat"
|
confirmText="Smazat"
|
||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
ProgressBar,
|
ProgressBar,
|
||||||
@@ -392,7 +393,7 @@ export default function AttendanceBalances() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Správa bilancí"
|
title="Správa bilancí"
|
||||||
actions={
|
actions={
|
||||||
@@ -815,6 +816,6 @@ export default function AttendanceBalances() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={resetMutation.isPending}
|
loading={resetMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
|
|
||||||
import { userListOptions } from "../lib/queries/users";
|
import { userListOptions } from "../lib/queries/users";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
DateField,
|
DateField,
|
||||||
Field,
|
Field,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
PageEnter,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
Select,
|
Select,
|
||||||
TextField,
|
TextField,
|
||||||
@@ -118,230 +118,214 @@ export default function AttendanceCreate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Přidat záznam docházky"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
actions={
|
||||||
transition={{ duration: 0.25 }}
|
<Button
|
||||||
>
|
variant="outlined"
|
||||||
<PageHeader
|
color="inherit"
|
||||||
title="Přidat záznam docházky"
|
component={RouterLink}
|
||||||
actions={
|
to="/attendance/admin"
|
||||||
|
>
|
||||||
|
← Zpět na správu
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card sx={{ maxWidth: 640 }}>
|
||||||
|
<Box component="form" onSubmit={handleSubmit}>
|
||||||
|
{/* Employee + Shift date */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Field label="Zaměstnanec" required>
|
||||||
|
<Select
|
||||||
|
value={form.user_id}
|
||||||
|
onChange={(val) => setForm({ ...form, user_id: val })}
|
||||||
|
options={[
|
||||||
|
{ value: "", label: "Vyberte zaměstnance" },
|
||||||
|
...users.map((u) => ({
|
||||||
|
value: String(u.id),
|
||||||
|
label: u.name,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Datum směny" required>
|
||||||
|
<DateField
|
||||||
|
value={form.shift_date}
|
||||||
|
onChange={(val) => handleShiftDateChange(val)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Record type */}
|
||||||
|
<Field label="Typ záznamu" required>
|
||||||
|
<Select
|
||||||
|
value={form.leave_type}
|
||||||
|
onChange={(val) => setForm({ ...form, leave_type: val })}
|
||||||
|
options={[
|
||||||
|
{ value: "work", label: "Práce" },
|
||||||
|
{ value: "vacation", label: "Dovolená" },
|
||||||
|
{ value: "sick", label: "Nemoc" },
|
||||||
|
{ value: "unpaid", label: "Neplacené volno" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{/* Leave hours — shown only for non-work types */}
|
||||||
|
{!isWorkType && (
|
||||||
|
<Field label="Počet hodin" hint="Výchozí 8 hodin pro celý den">
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
value={form.leave_hours}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
leave_hours: parseFloat(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
slotProps={{ htmlInput: { min: 0.5, max: 24, step: 0.5 } }}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Work-type time fields */}
|
||||||
|
{isWorkType && (
|
||||||
|
<>
|
||||||
|
{/* Arrival */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Field label="Příchod - datum">
|
||||||
|
<DateField
|
||||||
|
value={form.arrival_date}
|
||||||
|
onChange={(val) => setForm({ ...form, arrival_date: val })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Příchod - čas">
|
||||||
|
<TimeField
|
||||||
|
value={form.arrival_time}
|
||||||
|
onChange={(val) => setForm({ ...form, arrival_time: val })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Break start */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Field label="Začátek pauzy - datum">
|
||||||
|
<DateField
|
||||||
|
value={form.break_start_date}
|
||||||
|
onChange={(val) =>
|
||||||
|
setForm({ ...form, break_start_date: val })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Začátek pauzy - čas">
|
||||||
|
<TimeField
|
||||||
|
value={form.break_start_time}
|
||||||
|
onChange={(val) =>
|
||||||
|
setForm({ ...form, break_start_time: val })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Break end */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Field label="Konec pauzy - datum">
|
||||||
|
<DateField
|
||||||
|
value={form.break_end_date}
|
||||||
|
onChange={(val) =>
|
||||||
|
setForm({ ...form, break_end_date: val })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Konec pauzy - čas">
|
||||||
|
<TimeField
|
||||||
|
value={form.break_end_time}
|
||||||
|
onChange={(val) =>
|
||||||
|
setForm({ ...form, break_end_time: val })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Departure */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Field label="Odchod - datum">
|
||||||
|
<DateField
|
||||||
|
value={form.departure_date}
|
||||||
|
onChange={(val) =>
|
||||||
|
setForm({ ...form, departure_date: val })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Odchod - čas">
|
||||||
|
<TimeField
|
||||||
|
value={form.departure_time}
|
||||||
|
onChange={(val) =>
|
||||||
|
setForm({ ...form, departure_time: val })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
<Field label="Poznámka">
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
minRows={3}
|
||||||
|
value={form.notes}
|
||||||
|
onChange={(e) => setForm({ ...form, notes: e.target.value })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<Box sx={{ display: "flex", gap: 1.5, justifyContent: "flex-end" }}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to="/attendance/admin"
|
to="/attendance/admin"
|
||||||
>
|
>
|
||||||
← Zpět na správu
|
Zrušit
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={submitting}>
|
||||||
|
{submitting ? "Ukládám..." : "Uložit"}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
|
||||||
>
|
|
||||||
<Card sx={{ maxWidth: 640 }}>
|
|
||||||
<Box component="form" onSubmit={handleSubmit}>
|
|
||||||
{/* Employee + Shift date */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Field label="Zaměstnanec" required>
|
|
||||||
<Select
|
|
||||||
value={form.user_id}
|
|
||||||
onChange={(val) => setForm({ ...form, user_id: val })}
|
|
||||||
options={[
|
|
||||||
{ value: "", label: "Vyberte zaměstnance" },
|
|
||||||
...users.map((u) => ({
|
|
||||||
value: String(u.id),
|
|
||||||
label: u.name,
|
|
||||||
})),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Datum směny" required>
|
|
||||||
<DateField
|
|
||||||
value={form.shift_date}
|
|
||||||
onChange={(val) => handleShiftDateChange(val)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Record type */}
|
|
||||||
<Field label="Typ záznamu" required>
|
|
||||||
<Select
|
|
||||||
value={form.leave_type}
|
|
||||||
onChange={(val) => setForm({ ...form, leave_type: val })}
|
|
||||||
options={[
|
|
||||||
{ value: "work", label: "Práce" },
|
|
||||||
{ value: "vacation", label: "Dovolená" },
|
|
||||||
{ value: "sick", label: "Nemoc" },
|
|
||||||
{ value: "unpaid", label: "Neplacené volno" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
{/* Leave hours — shown only for non-work types */}
|
|
||||||
{!isWorkType && (
|
|
||||||
<Field label="Počet hodin" hint="Výchozí 8 hodin pro celý den">
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
value={form.leave_hours}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm({
|
|
||||||
...form,
|
|
||||||
leave_hours: parseFloat(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
slotProps={{ htmlInput: { min: 0.5, max: 24, step: 0.5 } }}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Work-type time fields */}
|
|
||||||
{isWorkType && (
|
|
||||||
<>
|
|
||||||
{/* Arrival */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Field label="Příchod - datum">
|
|
||||||
<DateField
|
|
||||||
value={form.arrival_date}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm({ ...form, arrival_date: val })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Příchod - čas">
|
|
||||||
<TimeField
|
|
||||||
value={form.arrival_time}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm({ ...form, arrival_time: val })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Break start */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Field label="Začátek pauzy - datum">
|
|
||||||
<DateField
|
|
||||||
value={form.break_start_date}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm({ ...form, break_start_date: val })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Začátek pauzy - čas">
|
|
||||||
<TimeField
|
|
||||||
value={form.break_start_time}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm({ ...form, break_start_time: val })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Break end */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Field label="Konec pauzy - datum">
|
|
||||||
<DateField
|
|
||||||
value={form.break_end_date}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm({ ...form, break_end_date: val })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Konec pauzy - čas">
|
|
||||||
<TimeField
|
|
||||||
value={form.break_end_time}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm({ ...form, break_end_time: val })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Departure */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Field label="Odchod - datum">
|
|
||||||
<DateField
|
|
||||||
value={form.departure_date}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm({ ...form, departure_date: val })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Odchod - čas">
|
|
||||||
<TimeField
|
|
||||||
value={form.departure_time}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm({ ...form, departure_time: val })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Notes */}
|
|
||||||
<Field label="Poznámka">
|
|
||||||
<TextField
|
|
||||||
multiline
|
|
||||||
minRows={3}
|
|
||||||
value={form.notes}
|
|
||||||
onChange={(e) => setForm({ ...form, notes: e.target.value })}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<Box sx={{ display: "flex", gap: 1.5, justifyContent: "flex-end" }}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
component={RouterLink}
|
|
||||||
to="/attendance/admin"
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={submitting}>
|
|
||||||
{submitting ? "Ukládám..." : "Uložit"}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Box>
|
||||||
</motion.div>
|
</Card>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useMemo, useRef } from "react";
|
import { useState, useMemo, useRef } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
@@ -36,6 +35,7 @@ import {
|
|||||||
ProgressBar,
|
ProgressBar,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
@@ -494,196 +494,169 @@ export default function AttendanceHistory() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Historie docházky"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={computed.monthName}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
records.length > 0 ? (
|
||||||
<PageHeader
|
<Button
|
||||||
title="Historie docházky"
|
variant="outlined"
|
||||||
subtitle={computed.monthName}
|
color="inherit"
|
||||||
actions={
|
startIcon={PrintIcon}
|
||||||
records.length > 0 ? (
|
onClick={handlePrint}
|
||||||
<Button
|
title="Tisk docházky"
|
||||||
variant="outlined"
|
>
|
||||||
color="inherit"
|
Tisk
|
||||||
startIcon={PrintIcon}
|
</Button>
|
||||||
onClick={handlePrint}
|
) : undefined
|
||||||
title="Tisk docházky"
|
}
|
||||||
>
|
/>
|
||||||
Tisk
|
|
||||||
</Button>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<motion.div
|
<FilterBar>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Box sx={{ flex: "1 1 200px" }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<Field label="Měsíc">
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
<MonthField value={month} onChange={(val) => setMonth(val)} />
|
||||||
>
|
</Field>
|
||||||
<FilterBar>
|
</Box>
|
||||||
<Box sx={{ flex: "1 1 200px" }}>
|
</FilterBar>
|
||||||
<Field label="Měsíc">
|
|
||||||
<MonthField value={month} onChange={(val) => setMonth(val)} />
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
</FilterBar>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Monthly Fund Card */}
|
{/* Monthly Fund Card */}
|
||||||
<motion.div
|
<Card sx={{ mt: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
{isPending ? (
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<LoadingState />
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
) : (
|
||||||
>
|
<Box
|
||||||
<Card sx={{ mt: 3 }}>
|
sx={{
|
||||||
{isPending ? (
|
display: "flex",
|
||||||
<LoadingState />
|
alignItems: "center",
|
||||||
) : (
|
gap: 2,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 2,
|
justifyContent: "center",
|
||||||
flexWrap: "wrap",
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: "info.light",
|
||||||
|
color: "info.main",
|
||||||
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6" />
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6" />
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10" />
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flex: 1, minWidth: "200px" }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
justifyContent: "center",
|
alignItems: "baseline",
|
||||||
width: 44,
|
mb: 0.75,
|
||||||
height: 44,
|
|
||||||
borderRadius: 2,
|
|
||||||
bgcolor: "info.light",
|
|
||||||
color: "info.main",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<Typography sx={{ fontWeight: 600, fontSize: "1rem" }}>
|
||||||
width="24"
|
Fond: {fund.worked}h / {fund.fund}h
|
||||||
height="24"
|
</Typography>
|
||||||
viewBox="0 0 24 24"
|
<Typography
|
||||||
fill="none"
|
variant="body2"
|
||||||
stroke="currentColor"
|
color="text.secondary"
|
||||||
strokeWidth="2"
|
sx={{ fontSize: "0.8125rem" }}
|
||||||
>
|
>
|
||||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
{fund.business_days} prac. dnů
|
||||||
<line x1="16" y1="2" x2="16" y2="6" />
|
{fund.holiday_count > 0 && ` + ${fund.holiday_count} svátky`}
|
||||||
<line x1="8" y1="2" x2="8" y2="6" />
|
</Typography>
|
||||||
<line x1="3" y1="10" x2="21" y2="10" />
|
|
||||||
</svg>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ flex: 1, minWidth: "200px" }}>
|
<ProgressBar value={progressValue} color={progressColor} />
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
flexWrap: "wrap",
|
||||||
alignItems: "baseline",
|
gap: 0.5,
|
||||||
mb: 0.75,
|
mt: 1,
|
||||||
}}
|
minHeight: "1.5rem",
|
||||||
>
|
alignItems: "center",
|
||||||
<Typography sx={{ fontWeight: 600, fontSize: "1rem" }}>
|
justifyContent: "space-between",
|
||||||
Fond: {fund.worked}h / {fund.fund}h
|
}}
|
||||||
</Typography>
|
>
|
||||||
<Typography
|
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
||||||
variant="body2"
|
{computed.totalMinutes > 0 && (
|
||||||
color="text.secondary"
|
<StatusChip
|
||||||
sx={{ fontSize: "0.8125rem" }}
|
color="info"
|
||||||
>
|
label={`Práce: ${formatHoursDecimal(computed.totalMinutes)}h`}
|
||||||
{fund.business_days} prac. dnů
|
title="Odpracováno (reálně)"
|
||||||
{fund.holiday_count > 0 &&
|
/>
|
||||||
` + ${fund.holiday_count} svátky`}
|
)}
|
||||||
</Typography>
|
{computed.vacationHours > 0 && (
|
||||||
|
<StatusChip
|
||||||
|
color="success"
|
||||||
|
label={`Dov: ${computed.vacationHours}h`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{computed.sickHours > 0 && (
|
||||||
|
<StatusChip
|
||||||
|
color="error"
|
||||||
|
label={`Nem: ${computed.sickHours}h`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{computed.freeHolidayHours > 0 && (
|
||||||
|
<StatusChip
|
||||||
|
color="warning"
|
||||||
|
label={`Sv: ${Math.round(computed.freeHolidayHours)}h`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{computed.unpaidHours > 0 && (
|
||||||
|
<StatusChip
|
||||||
|
color="default"
|
||||||
|
label={`Nep: ${computed.unpaidHours}h`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<ProgressBar value={progressValue} color={progressColor} />
|
<Typography
|
||||||
<Box
|
variant="caption"
|
||||||
sx={{
|
sx={{ fontWeight: 600, color: deltaColor }}
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: 0.5,
|
|
||||||
mt: 1,
|
|
||||||
minHeight: "1.5rem",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
{computed.delta > 0
|
||||||
{computed.totalMinutes > 0 && (
|
? `Přesčas: +${formatHoursDecimal(computed.delta * 60)}h`
|
||||||
<StatusChip
|
: computed.delta < 0
|
||||||
color="info"
|
? `Zbývá: ${formatHoursDecimal(-computed.delta * 60)}h`
|
||||||
label={`Práce: ${formatHoursDecimal(computed.totalMinutes)}h`}
|
: "Fond splněn"}
|
||||||
title="Odpracováno (reálně)"
|
</Typography>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{computed.vacationHours > 0 && (
|
|
||||||
<StatusChip
|
|
||||||
color="success"
|
|
||||||
label={`Dov: ${computed.vacationHours}h`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{computed.sickHours > 0 && (
|
|
||||||
<StatusChip
|
|
||||||
color="error"
|
|
||||||
label={`Nem: ${computed.sickHours}h`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{computed.freeHolidayHours > 0 && (
|
|
||||||
<StatusChip
|
|
||||||
color="warning"
|
|
||||||
label={`Sv: ${Math.round(computed.freeHolidayHours)}h`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{computed.unpaidHours > 0 && (
|
|
||||||
<StatusChip
|
|
||||||
color="default"
|
|
||||||
label={`Nep: ${computed.unpaidHours}h`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{ fontWeight: 600, color: deltaColor }}
|
|
||||||
>
|
|
||||||
{computed.delta > 0
|
|
||||||
? `Přesčas: +${formatHoursDecimal(computed.delta * 60)}h`
|
|
||||||
: computed.delta < 0
|
|
||||||
? `Zbývá: ${formatHoursDecimal(-computed.delta * 60)}h`
|
|
||||||
: "Fond splněn"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
</Box>
|
||||||
</Card>
|
)}
|
||||||
</motion.div>
|
</Card>
|
||||||
|
|
||||||
{/* Records Table */}
|
{/* Records Table */}
|
||||||
<motion.div
|
<Card sx={{ mt: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
{isPending ? (
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<LoadingState />
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
) : (
|
||||||
>
|
<DataTable<AttendanceRecord>
|
||||||
<Card sx={{ mt: 3 }}>
|
columns={columns}
|
||||||
{isPending ? (
|
rows={records}
|
||||||
<LoadingState />
|
rowKey={(record) => record.id}
|
||||||
) : (
|
empty={<EmptyState title="Za tento měsíc nejsou žádné záznamy." />}
|
||||||
<DataTable<AttendanceRecord>
|
/>
|
||||||
columns={columns}
|
)}
|
||||||
rows={records}
|
</Card>
|
||||||
rowKey={(record) => record.id}
|
|
||||||
empty={
|
|
||||||
<EmptyState title="Za tento měsíc nejsou žádné záznamy." />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Hidden Print Content */}
|
{/* Hidden Print Content */}
|
||||||
{records.length > 0 && (
|
{records.length > 0 && (
|
||||||
@@ -888,7 +861,7 @@ export default function AttendanceHistory() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useAlert } from "../context/AlertContext";
|
|||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { useNavigate, useParams, Link as RouterLink } from "react-router-dom";
|
import { useNavigate, useParams, Link as RouterLink } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ import {
|
|||||||
attendanceLocationOptions,
|
attendanceLocationOptions,
|
||||||
type LocationRecord,
|
type LocationRecord,
|
||||||
} from "../lib/queries/attendance";
|
} from "../lib/queries/attendance";
|
||||||
import { Button, Card, LoadingState, PageHeader } from "../ui";
|
import { Button, Card, LoadingState, PageEnter, PageHeader } from "../ui";
|
||||||
|
|
||||||
const BackIcon = (
|
const BackIcon = (
|
||||||
<svg
|
<svg
|
||||||
@@ -187,79 +186,127 @@ export default function AttendanceLocation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Poloha záznamu"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
actions={
|
||||||
transition={{ duration: 0.25 }}
|
<Button
|
||||||
>
|
component={RouterLink}
|
||||||
<PageHeader
|
to={`/attendance/admin?month=${month}`}
|
||||||
title="Poloha záznamu"
|
variant="outlined"
|
||||||
actions={
|
color="inherit"
|
||||||
<Button
|
startIcon={BackIcon}
|
||||||
component={RouterLink}
|
>
|
||||||
to={`/attendance/admin?month=${month}`}
|
Zpět na správu
|
||||||
variant="outlined"
|
</Button>
|
||||||
color="inherit"
|
}
|
||||||
startIcon={BackIcon}
|
/>
|
||||||
>
|
|
||||||
Zpět na správu
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Info + map card */}
|
{/* Info + map card */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
{record.user_name} — {formatDate(record.shift_date)}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
</Typography>
|
||||||
>
|
|
||||||
<Card sx={{ mb: 3 }}>
|
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
|
||||||
{record.user_name} — {formatDate(record.shift_date)}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* Leaflet map — kept entirely as-is */}
|
{/* Leaflet map — kept entirely as-is */}
|
||||||
{hasAnyLocation && (
|
{hasAnyLocation && (
|
||||||
<div ref={mapRef} className="attendance-location-map" />
|
<div ref={mapRef} className="attendance-location-map" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Location detail grid */}
|
{/* Location detail grid */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||||
|
gap: 2,
|
||||||
|
mt: hasAnyLocation ? 2 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Arrival */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "grid",
|
p: 2,
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
borderRadius: 1,
|
||||||
gap: 2,
|
border: "1px solid",
|
||||||
mt: hasAnyLocation ? 2 : 0,
|
borderColor: "divider",
|
||||||
|
opacity: hasArrivalLocation ? 1 : 0.6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Arrival */}
|
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
|
||||||
|
Příchod
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontFamily: "'DM Mono', Menlo, monospace", mb: 1 }}
|
||||||
|
>
|
||||||
|
{record.arrival_time
|
||||||
|
? formatDatetimeLocal(record.arrival_time)
|
||||||
|
: "—"}
|
||||||
|
</Typography>
|
||||||
|
{hasArrivalLocation ? (
|
||||||
|
<>
|
||||||
|
<Typography variant="body2" sx={{ mb: 0.5 }}>
|
||||||
|
{record.arrival_address || <em>Adresa nezjištěna</em>}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "'DM Mono', Menlo, monospace",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: "text.secondary",
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
GPS: {record.arrival_lat}, {record.arrival_lng}
|
||||||
|
{record.arrival_accuracy &&
|
||||||
|
` (přesnost: ${Math.round(Number(record.arrival_accuracy))}m)`}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={`https://www.google.com/maps?q=${record.arrival_lat},${record.arrival_lng}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
Otevřít v Google Maps
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
<em>Poloha nebyla zaznamenána</em>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Departure */}
|
||||||
|
{(hasDepartureLocation || record.departure_time) && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
p: 2,
|
p: 2,
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
border: "1px solid",
|
border: "1px solid",
|
||||||
borderColor: "divider",
|
borderColor: "divider",
|
||||||
opacity: hasArrivalLocation ? 1 : 0.6,
|
opacity: hasDepartureLocation ? 1 : 0.6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
|
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
|
||||||
Příchod
|
Odchod
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ fontFamily: "'DM Mono', Menlo, monospace", mb: 1 }}
|
sx={{ fontFamily: "'DM Mono', Menlo, monospace", mb: 1 }}
|
||||||
>
|
>
|
||||||
{record.arrival_time
|
{record.departure_time
|
||||||
? formatDatetimeLocal(record.arrival_time)
|
? formatDatetimeLocal(record.departure_time)
|
||||||
: "—"}
|
: "—"}
|
||||||
</Typography>
|
</Typography>
|
||||||
{hasArrivalLocation ? (
|
{hasDepartureLocation ? (
|
||||||
<>
|
<>
|
||||||
<Typography variant="body2" sx={{ mb: 0.5 }}>
|
<Typography variant="body2" sx={{ mb: 0.5 }}>
|
||||||
{record.arrival_address || <em>Adresa nezjištěna</em>}
|
{record.departure_address || <em>Adresa nezjištěna</em>}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
@@ -270,13 +317,13 @@ export default function AttendanceLocation() {
|
|||||||
mb: 1,
|
mb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
GPS: {record.arrival_lat}, {record.arrival_lng}
|
GPS: {record.departure_lat}, {record.departure_lng}
|
||||||
{record.arrival_accuracy &&
|
{record.departure_accuracy &&
|
||||||
` (přesnost: ${Math.round(Number(record.arrival_accuracy))}m)`}
|
` (přesnost: ${Math.round(Number(record.departure_accuracy))}m)`}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
component="a"
|
||||||
href={`https://www.google.com/maps?q=${record.arrival_lat},${record.arrival_lng}`}
|
href={`https://www.google.com/maps?q=${record.departure_lat},${record.departure_lng}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -292,69 +339,9 @@ export default function AttendanceLocation() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
{/* Departure */}
|
</Box>
|
||||||
{(hasDepartureLocation || record.departure_time) && (
|
</Card>
|
||||||
<Box
|
</PageEnter>
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
borderRadius: 1,
|
|
||||||
border: "1px solid",
|
|
||||||
borderColor: "divider",
|
|
||||||
opacity: hasDepartureLocation ? 1 : 0.6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
|
|
||||||
Odchod
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontFamily: "'DM Mono', Menlo, monospace", mb: 1 }}
|
|
||||||
>
|
|
||||||
{record.departure_time
|
|
||||||
? formatDatetimeLocal(record.departure_time)
|
|
||||||
: "—"}
|
|
||||||
</Typography>
|
|
||||||
{hasDepartureLocation ? (
|
|
||||||
<>
|
|
||||||
<Typography variant="body2" sx={{ mb: 0.5 }}>
|
|
||||||
{record.departure_address || <em>Adresa nezjištěna</em>}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
fontFamily: "'DM Mono', Menlo, monospace",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
color: "text.secondary",
|
|
||||||
mb: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
GPS: {record.departure_lat}, {record.departure_lng}
|
|
||||||
{record.departure_accuracy &&
|
|
||||||
` (přesnost: ${Math.round(Number(record.departure_accuracy))}m)`}
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
href={`https://www.google.com/maps?q=${record.departure_lat},${record.departure_lng}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
Otevřít v Google Maps
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
<em>Poloha nebyla zaznamenána</em>
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -267,30 +267,24 @@ export default function AuditLog() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Audit log"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={
|
||||||
transition={{ duration: 0.25 }}
|
pagination
|
||||||
>
|
? `${pagination.total} ${czechPlural(pagination.total, "záznam", "záznamy", "záznamů")}`
|
||||||
<PageHeader
|
: undefined
|
||||||
title="Audit log"
|
}
|
||||||
subtitle={
|
actions={
|
||||||
pagination
|
<Button
|
||||||
? `${pagination.total} ${czechPlural(pagination.total, "záznam", "záznamy", "záznamů")}`
|
variant="outlined"
|
||||||
: undefined
|
startIcon={TrashIcon}
|
||||||
}
|
onClick={() => setShowCleanup(true)}
|
||||||
actions={
|
>
|
||||||
<Button
|
Vyčistit
|
||||||
variant="outlined"
|
</Button>
|
||||||
startIcon={TrashIcon}
|
}
|
||||||
onClick={() => setShowCleanup(true)}
|
/>
|
||||||
>
|
|
||||||
Vyčistit
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Cleanup Modal */}
|
{/* Cleanup Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
@@ -371,6 +365,6 @@ export default function AuditLog() {
|
|||||||
onChange={setPage}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
@@ -12,7 +11,7 @@ import { dashboardOptions } from "../lib/queries/dashboard";
|
|||||||
import { require2FAOptions } from "../lib/queries/settings";
|
import { require2FAOptions } from "../lib/queries/settings";
|
||||||
import { getCzechDate } from "../utils/dashboardHelpers";
|
import { getCzechDate } from "../utils/dashboardHelpers";
|
||||||
import { useApiMutation } from "../lib/queries/mutations";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import { Card, Button, StatusChip } from "../ui";
|
import { Card, Button, StatusChip, PageEnter } from "../ui";
|
||||||
import DashKpiCards from "../components/dashboard/DashKpiCards";
|
import DashKpiCards from "../components/dashboard/DashKpiCards";
|
||||||
import DashQuickActions from "../components/dashboard/DashQuickActions";
|
import DashQuickActions from "../components/dashboard/DashQuickActions";
|
||||||
import DashActivityFeed from "../components/dashboard/DashActivityFeed";
|
import DashActivityFeed from "../components/dashboard/DashActivityFeed";
|
||||||
@@ -221,15 +220,9 @@ export default function Dashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box
|
<Box sx={{ mb: 3 }}>
|
||||||
component={motion.div}
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25 }}
|
|
||||||
sx={{ mb: 3 }}
|
|
||||||
>
|
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">
|
||||||
Vítejte zpět, {user?.fullName || user?.username}
|
Vítejte zpět, {user?.fullName || user?.username}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -240,75 +233,69 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* 2FA Required Banner */}
|
{/* 2FA Required Banner */}
|
||||||
{user?.require2FA && !user?.totpEnabled && (
|
{user?.require2FA && !user?.totpEnabled && (
|
||||||
<motion.div
|
<Card
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
mb: 3,
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
border: 2,
|
||||||
|
borderColor: "error.main",
|
||||||
|
bgcolor: "error.light",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
mb: 3,
|
display: "flex",
|
||||||
border: 2,
|
alignItems: "center",
|
||||||
borderColor: "error.main",
|
justifyContent: "space-between",
|
||||||
bgcolor: "error.light",
|
gap: 2,
|
||||||
|
flexWrap: "wrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
||||||
sx={{
|
<Box
|
||||||
display: "flex",
|
sx={{
|
||||||
alignItems: "center",
|
width: 40,
|
||||||
justifyContent: "space-between",
|
height: 40,
|
||||||
gap: 2,
|
borderRadius: "50%",
|
||||||
flexWrap: "wrap",
|
display: "flex",
|
||||||
}}
|
alignItems: "center",
|
||||||
>
|
justifyContent: "center",
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
bgcolor: "error.light",
|
||||||
<Box
|
color: "error.main",
|
||||||
sx={{
|
flexShrink: 0,
|
||||||
width: 40,
|
}}
|
||||||
height: 40,
|
|
||||||
borderRadius: "50%",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
bgcolor: "error.light",
|
|
||||||
color: "error.main",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
||||||
<line x1="12" y1="9" x2="12" y2="13" />
|
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
||||||
</svg>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography sx={{ fontWeight: 600 }}>
|
|
||||||
Dvoufaktorové ověření je povinné
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
Administrátor vyžaduje aktivaci 2FA. Dokud ji neaktivujete,
|
|
||||||
nemáte přístup k ostatním sekcím systému.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
onClick={handleStart2FASetup}
|
|
||||||
disabled={totpSubmitting}
|
|
||||||
sx={{ flexShrink: 0 }}
|
|
||||||
>
|
>
|
||||||
{totpSubmitting ? "Generuji..." : "Aktivovat 2FA nyní"}
|
<svg
|
||||||
</Button>
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ fontWeight: 600 }}>
|
||||||
|
Dvoufaktorové ověření je povinné
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Administrátor vyžaduje aktivaci 2FA. Dokud ji neaktivujete,
|
||||||
|
nemáte přístup k ostatním sekcím systému.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
<Button
|
||||||
</motion.div>
|
onClick={handleStart2FASetup}
|
||||||
|
disabled={totpSubmitting}
|
||||||
|
sx={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
{totpSubmitting ? "Generuji..." : "Aktivovat 2FA nyní"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Loading spinner */}
|
{/* Loading spinner */}
|
||||||
@@ -339,10 +326,6 @@ export default function Dashboard() {
|
|||||||
{/* Main content grid */}
|
{/* Main content grid */}
|
||||||
{!dashLoading && (
|
{!dashLoading && (
|
||||||
<Box
|
<Box
|
||||||
component={motion.div}
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
|
||||||
sx={{
|
sx={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: { xs: "1fr", lg: "1fr 1fr 1fr" },
|
gridTemplateColumns: { xs: "1fr", lg: "1fr 1fr 1fr" },
|
||||||
@@ -523,6 +506,6 @@ export default function Dashboard() {
|
|||||||
<DashSessions />
|
<DashSessions />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Link as RouterLink, useSearchParams } from "react-router-dom";
|
import { Link as RouterLink, useSearchParams } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
FilterBar,
|
FilterBar,
|
||||||
Tabs,
|
Tabs,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
type TabDef,
|
type TabDef,
|
||||||
type StatCardColor,
|
type StatCardColor,
|
||||||
@@ -644,43 +644,33 @@ export default function Invoices() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Faktury"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={subtitle}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
hasPermission("invoices.create") ? (
|
||||||
<PageHeader
|
activeTab === "received" ? (
|
||||||
title="Faktury"
|
<Button
|
||||||
subtitle={subtitle}
|
startIcon={UploadIcon}
|
||||||
actions={
|
onClick={() => setReceivedUploadOpen(true)}
|
||||||
hasPermission("invoices.create") ? (
|
>
|
||||||
activeTab === "received" ? (
|
Nahrát faktury
|
||||||
<Button
|
</Button>
|
||||||
startIcon={UploadIcon}
|
) : (
|
||||||
onClick={() => setReceivedUploadOpen(true)}
|
<Button
|
||||||
>
|
component={RouterLink}
|
||||||
Nahrát faktury
|
to="/invoices/new"
|
||||||
</Button>
|
startIcon={PlusIcon}
|
||||||
) : (
|
>
|
||||||
<Button
|
Nová faktura
|
||||||
component={RouterLink}
|
</Button>
|
||||||
to="/invoices/new"
|
)
|
||||||
startIcon={PlusIcon}
|
) : undefined
|
||||||
>
|
}
|
||||||
Nová faktura
|
/>
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<Box>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -722,49 +712,31 @@ export default function Invoices() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</Box>
|
||||||
|
|
||||||
{activeTab === "received" ? (
|
{activeTab === "received" ? (
|
||||||
<motion.div
|
<Suspense fallback={<LoadingState />}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<ReceivedInvoices
|
||||||
animate={{ opacity: 1, y: 0 }}
|
statsMonth={statsMonth}
|
||||||
transition={{ duration: 0.25, delay: 0.1 }}
|
statsYear={statsYear}
|
||||||
>
|
uploadOpen={receivedUploadOpen}
|
||||||
<Suspense fallback={<LoadingState />}>
|
setUploadOpen={setReceivedUploadOpen}
|
||||||
<ReceivedInvoices
|
/>
|
||||||
statsMonth={statsMonth}
|
</Suspense>
|
||||||
statsYear={statsYear}
|
|
||||||
uploadOpen={receivedUploadOpen}
|
|
||||||
setUploadOpen={setReceivedUploadOpen}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</motion.div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<motion.div
|
{renderKpi()}
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.1 }}
|
|
||||||
>
|
|
||||||
{renderKpi()}
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<Box sx={{ mb: 2.5 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Tabs
|
||||||
animate={{ opacity: 1, y: 0 }}
|
value={statusFilter}
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
onChange={(v) => {
|
||||||
>
|
setStatusFilter(v);
|
||||||
<Box sx={{ mb: 2.5 }}>
|
setPage(1);
|
||||||
<Tabs
|
}}
|
||||||
value={statusFilter}
|
tabs={statusTabs}
|
||||||
onChange={(v) => {
|
/>
|
||||||
setStatusFilter(v);
|
</Box>
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
tabs={statusTabs}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<Box sx={{ flex: "1 1 320px" }}>
|
<Box sx={{ flex: "1 1 320px" }}>
|
||||||
@@ -845,40 +817,32 @@ export default function Invoices() {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<motion.div
|
<Card sx={{ opacity: loading ? 0.6 : 1, transition: "opacity .2s" }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<DataTable<Invoice>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
columns={columns}
|
||||||
transition={{ duration: 0.25, delay: 0.15 }}
|
rows={invoices}
|
||||||
>
|
rowKey={(inv) => inv.id}
|
||||||
<Card
|
rowSx={rowSx}
|
||||||
sx={{ opacity: loading ? 0.6 : 1, transition: "opacity .2s" }}
|
sortBy={sort}
|
||||||
>
|
sortDir={order}
|
||||||
<DataTable<Invoice>
|
onSort={handleSort}
|
||||||
columns={columns}
|
empty={
|
||||||
rows={invoices}
|
<EmptyState
|
||||||
rowKey={(inv) => inv.id}
|
title="Zatím nejsou žádné faktury."
|
||||||
rowSx={rowSx}
|
description={
|
||||||
sortBy={sort}
|
hasPermission("invoices.create")
|
||||||
sortDir={order}
|
? "Vytvořte první fakturu tlačítkem výše."
|
||||||
onSort={handleSort}
|
: undefined
|
||||||
empty={
|
}
|
||||||
<EmptyState
|
/>
|
||||||
title="Zatím nejsou žádné faktury."
|
}
|
||||||
description={
|
/>
|
||||||
hasPermission("invoices.create")
|
<Pagination
|
||||||
? "Vytvořte první fakturu tlačítkem výše."
|
page={page}
|
||||||
: undefined
|
pageCount={pagination?.total_pages ?? 1}
|
||||||
}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
}
|
</Card>
|
||||||
/>
|
|
||||||
<Pagination
|
|
||||||
page={page}
|
|
||||||
pageCount={pagination?.total_pages ?? 1}
|
|
||||||
onChange={setPage}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={deleteConfirm.show}
|
isOpen={deleteConfirm.show}
|
||||||
@@ -893,6 +857,6 @@ export default function Invoices() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState } from "react";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
||||||
@@ -23,6 +22,7 @@ import {
|
|||||||
Field,
|
Field,
|
||||||
TextField,
|
TextField,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -304,33 +304,21 @@ export default function LeaveApproval() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Schvalování nepřítomnosti"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={
|
||||||
transition={{ duration: 0.25 }}
|
pendingCount > 0
|
||||||
>
|
? `${pendingCount} ${czechPlural(pendingCount, "žádost čeká", "žádosti čekají", "žádostí čeká")} na schválení`
|
||||||
<PageHeader
|
: "Žádné čekající žádosti"
|
||||||
title="Schvalování nepřítomnosti"
|
}
|
||||||
subtitle={
|
/>
|
||||||
pendingCount > 0
|
|
||||||
? `${pendingCount} ${czechPlural(pendingCount, "žádost čeká", "žádosti čekají", "žádostí čeká")} na schválení`
|
|
||||||
: "Žádné čekající žádosti"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<Tabs
|
||||||
initial={{ opacity: 0, y: 12 }}
|
value={activeTab}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
onChange={(v) => setActiveTab(v as "pending" | "processed")}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
tabs={tabs}
|
||||||
>
|
/>
|
||||||
<Tabs
|
|
||||||
value={activeTab}
|
|
||||||
onChange={(v) => setActiveTab(v as "pending" | "processed")}
|
|
||||||
tabs={tabs}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Pending Tab */}
|
{/* Pending Tab */}
|
||||||
<TabPanel value="pending" current={activeTab}>
|
<TabPanel value="pending" current={activeTab}>
|
||||||
@@ -498,6 +486,6 @@ export default function LeaveApproval() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState } from "react";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
||||||
@@ -15,6 +14,7 @@ import {
|
|||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
@@ -237,37 +237,25 @@ export default function LeaveRequests() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Moje žádosti"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle="Přehled žádostí o nepřítomnost"
|
||||||
transition={{ duration: 0.25 }}
|
/>
|
||||||
>
|
|
||||||
<PageHeader
|
|
||||||
title="Moje žádosti"
|
|
||||||
subtitle="Přehled žádostí o nepřítomnost"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<Card>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<DataTable<LeaveRequest>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
columns={columns}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
rows={requests}
|
||||||
>
|
rowKey={(req) => req.id}
|
||||||
<Card>
|
empty={
|
||||||
<DataTable<LeaveRequest>
|
<EmptyState
|
||||||
columns={columns}
|
title="Zatím nemáte žádné žádosti"
|
||||||
rows={requests}
|
description="Novou žádost můžete podat na stránce Docházka"
|
||||||
rowKey={(req) => req.id}
|
/>
|
||||||
empty={
|
}
|
||||||
<EmptyState
|
/>
|
||||||
title="Zatím nemáte žádné žádosti"
|
</Card>
|
||||||
description="Novou žádost můžete podat na stránce Docházka"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={cancelModal.open}
|
isOpen={cancelModal.open}
|
||||||
@@ -278,6 +266,6 @@ export default function LeaveRequests() {
|
|||||||
confirmText="Zrušit žádost"
|
confirmText="Zrušit žádost"
|
||||||
loading={cancelMutation.isPending}
|
loading={cancelMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
FilterBar,
|
FilterBar,
|
||||||
Tabs,
|
Tabs,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
type TabDef,
|
type TabDef,
|
||||||
@@ -686,58 +686,46 @@ export default function Offers() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Nabídky"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={subtitle}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
<>
|
||||||
<PageHeader
|
{hasPermission("settings.templates") && (
|
||||||
title="Nabídky"
|
<Button
|
||||||
subtitle={subtitle}
|
component={RouterLink}
|
||||||
actions={
|
to="/offers/templates"
|
||||||
<>
|
variant="outlined"
|
||||||
{hasPermission("settings.templates") && (
|
color="inherit"
|
||||||
<Button
|
startIcon={TemplatesIcon}
|
||||||
component={RouterLink}
|
>
|
||||||
to="/offers/templates"
|
Šablony
|
||||||
variant="outlined"
|
</Button>
|
||||||
color="inherit"
|
)}
|
||||||
startIcon={TemplatesIcon}
|
{hasPermission("offers.create") && (
|
||||||
>
|
<Button
|
||||||
Šablony
|
component={RouterLink}
|
||||||
</Button>
|
to="/offers/new"
|
||||||
)}
|
startIcon={PlusIcon}
|
||||||
{hasPermission("offers.create") && (
|
>
|
||||||
<Button
|
Nová nabídka
|
||||||
component={RouterLink}
|
</Button>
|
||||||
to="/offers/new"
|
)}
|
||||||
startIcon={PlusIcon}
|
</>
|
||||||
>
|
}
|
||||||
Nová nabídka
|
/>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<Box sx={{ mb: 2.5 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Tabs
|
||||||
animate={{ opacity: 1, y: 0 }}
|
value={statusFilter}
|
||||||
transition={{ duration: 0.25, delay: 0.1 }}
|
onChange={(v) => {
|
||||||
>
|
setStatusFilter(v);
|
||||||
<Box sx={{ mb: 2.5 }}>
|
setPage(1);
|
||||||
<Tabs
|
}}
|
||||||
value={statusFilter}
|
tabs={tabs}
|
||||||
onChange={(v) => {
|
/>
|
||||||
setStatusFilter(v);
|
</Box>
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
tabs={tabs}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<Box sx={{ flex: "1 1 320px" }}>
|
<Box sx={{ flex: "1 1 320px" }}>
|
||||||
@@ -838,44 +826,38 @@ export default function Offers() {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<motion.div
|
<Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<DataTable<Quotation>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
columns={columns}
|
||||||
transition={{ duration: 0.25, delay: 0.15 }}
|
rows={quotations}
|
||||||
>
|
rowKey={(q) => q.id}
|
||||||
<Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
|
rowSx={rowSx}
|
||||||
<DataTable<Quotation>
|
sortBy={sort}
|
||||||
columns={columns}
|
sortDir={order}
|
||||||
rows={quotations}
|
onSort={handleSort}
|
||||||
rowKey={(q) => q.id}
|
empty={
|
||||||
rowSx={rowSx}
|
debouncedSearch ? (
|
||||||
sortBy={sort}
|
<EmptyState title="Žádné nabídky odpovídající hledání." />
|
||||||
sortDir={order}
|
) : (
|
||||||
onSort={handleSort}
|
<EmptyState
|
||||||
empty={
|
title="Zatím nejsou žádné nabídky."
|
||||||
debouncedSearch ? (
|
action={
|
||||||
<EmptyState title="Žádné nabídky odpovídající hledání." />
|
hasPermission("offers.create") ? (
|
||||||
) : (
|
<Button component={RouterLink} to="/offers/new">
|
||||||
<EmptyState
|
Vytvořit první nabídku
|
||||||
title="Zatím nejsou žádné nabídky."
|
</Button>
|
||||||
action={
|
) : undefined
|
||||||
hasPermission("offers.create") ? (
|
}
|
||||||
<Button component={RouterLink} to="/offers/new">
|
/>
|
||||||
Vytvořit první nabídku
|
)
|
||||||
</Button>
|
}
|
||||||
) : undefined
|
/>
|
||||||
}
|
<Pagination
|
||||||
/>
|
page={page}
|
||||||
)
|
pageCount={pagination?.total_pages ?? 1}
|
||||||
}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
<Pagination
|
</Card>
|
||||||
page={page}
|
|
||||||
pageCount={pagination?.total_pages ?? 1}
|
|
||||||
onChange={setPage}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={deleteConfirm.show}
|
isOpen={deleteConfirm.show}
|
||||||
@@ -935,6 +917,6 @@ export default function Offers() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Field>
|
</Field>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useCallback, useRef } from "react";
|
import { useState, useCallback, useRef } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -24,6 +23,7 @@ import {
|
|||||||
CheckboxField,
|
CheckboxField,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -468,24 +468,18 @@ export default function OffersCustomers() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Zákazníci"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle="Správa zákazníků pro nabídky"
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
hasPermission("customers.create") ? (
|
||||||
<PageHeader
|
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
||||||
title="Zákazníci"
|
Přidat zákazníka
|
||||||
subtitle="Správa zákazníků pro nabídky"
|
</Button>
|
||||||
actions={
|
) : undefined
|
||||||
hasPermission("customers.create") ? (
|
}
|
||||||
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
/>
|
||||||
Přidat zákazníka
|
|
||||||
</Button>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<Box sx={{ flex: "1 1 320px" }}>
|
<Box sx={{ flex: "1 1 320px" }}>
|
||||||
@@ -816,6 +810,6 @@ export default function OffersCustomers() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -142,29 +142,17 @@ export default function OffersTemplates() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Šablony"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle="Šablony položek a rozsahu projektu"
|
||||||
transition={{ duration: 0.25 }}
|
/>
|
||||||
>
|
|
||||||
<PageHeader
|
|
||||||
title="Šablony"
|
|
||||||
subtitle="Šablony položek a rozsahu projektu"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<Tabs
|
||||||
initial={{ opacity: 0, y: 12 }}
|
value={activeTab}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
onChange={(v) => setActiveTab(v as "items" | "scopes")}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
tabs={tabs}
|
||||||
>
|
/>
|
||||||
<Tabs
|
|
||||||
value={activeTab}
|
|
||||||
onChange={(v) => setActiveTab(v as "items" | "scopes")}
|
|
||||||
tabs={tabs}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<TabPanel value="items" current={activeTab}>
|
<TabPanel value="items" current={activeTab}>
|
||||||
<ItemTemplatesTab />
|
<ItemTemplatesTab />
|
||||||
@@ -172,7 +160,7 @@ export default function OffersTemplates() {
|
|||||||
<TabPanel value="scopes" current={activeTab}>
|
<TabPanel value="scopes" current={activeTab}>
|
||||||
<ScopeTemplatesTab />
|
<ScopeTemplatesTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { useApiMutation } from "../lib/queries/mutations";
|
|||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import OrderConfirmationModal from "../components/OrderConfirmationModal";
|
import OrderConfirmationModal from "../components/OrderConfirmationModal";
|
||||||
@@ -31,6 +30,7 @@ import {
|
|||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -390,13 +390,9 @@ export default function OrderDetail() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<Box>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25 }}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -498,339 +494,315 @@ export default function OrderDetail() {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</Box>
|
||||||
|
|
||||||
{/* Info card */}
|
{/* Info card */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Informace
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
</Typography>
|
||||||
>
|
<Box
|
||||||
<Card sx={{ mb: 3 }}>
|
sx={{
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
display: "grid",
|
||||||
Informace
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
|
||||||
</Typography>
|
gap: 2,
|
||||||
<Box
|
}}
|
||||||
sx={{
|
>
|
||||||
display: "grid",
|
<Box>
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
|
<Typography variant="caption" color="text.secondary">
|
||||||
gap: 2,
|
Nabídka
|
||||||
}}
|
</Typography>
|
||||||
>
|
<Typography
|
||||||
<Box>
|
variant="body2"
|
||||||
<Typography variant="caption" color="text.secondary">
|
sx={{
|
||||||
Nabídka
|
fontFamily: "'DM Mono', Menlo, monospace",
|
||||||
</Typography>
|
fontWeight: 500,
|
||||||
<Typography
|
}}
|
||||||
variant="body2"
|
>
|
||||||
|
<Box
|
||||||
|
component={RouterLink}
|
||||||
|
to={`/offers/${order.quotation_id}`}
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: "'DM Mono', Menlo, monospace",
|
color: "primary.main",
|
||||||
fontWeight: 500,
|
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
|
<Box
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to={`/offers/${order.quotation_id}`}
|
to={`/projects/${order.project.id}`}
|
||||||
sx={{
|
sx={{
|
||||||
color: "primary.main",
|
color: "primary.main",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
"&:hover": { textDecoration: "underline" },
|
"&:hover": { textDecoration: "underline" },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{order.quotation_number}
|
{order.project.project_number} — {order.project.name}
|
||||||
</Box>
|
</Box>
|
||||||
{order.project_code && (
|
) : (
|
||||||
<Box component="span" sx={{ color: "text.secondary", ml: 1 }}>
|
"—"
|
||||||
({order.project_code})
|
)}
|
||||||
</Box>
|
</Typography>
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
<Box>
|
||||||
</motion.div>
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Zákazník
|
||||||
{/* Items (read-only) */}
|
</Typography>
|
||||||
<motion.div
|
<Typography
|
||||||
initial={{ opacity: 0, y: 12 }}
|
variant="body2"
|
||||||
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
|
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
fontFamily: "'DM Mono', Menlo, monospace",
|
||||||
justifyContent: "space-between",
|
fontWeight: 500,
|
||||||
pt: 0.5,
|
|
||||||
mt: 0.5,
|
|
||||||
borderTop: 1,
|
|
||||||
borderColor: "divider",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
{order.customer_name || "—"}
|
||||||
Celkem k úhradě:
|
</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>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
||||||
fontFamily: "'DM Mono', Menlo, monospace",
|
|
||||||
fontWeight: 700,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{formatCurrency(totals.total, order.currency)}
|
{formatCurrency(totals.vatAmount, order.currency)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</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>
|
</Box>
|
||||||
</Card>
|
</Box>
|
||||||
</motion.div>
|
</Card>
|
||||||
|
|
||||||
{/* Sections (read-only) */}
|
{/* Sections (read-only) */}
|
||||||
{order.sections?.length > 0 && (
|
{order.sections?.length > 0 && (
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Rozsah projektu
|
||||||
transition={{ duration: 0.25, delay: 0.15 }}
|
</Typography>
|
||||||
>
|
{order.scope_title && (
|
||||||
<Card sx={{ mb: 3 }}>
|
<Typography variant="body2" sx={{ fontWeight: 500, mb: 1 }}>
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
{order.scope_title}
|
||||||
Rozsah projektu
|
|
||||||
</Typography>
|
</Typography>
|
||||||
{order.scope_title && (
|
)}
|
||||||
<Typography variant="body2" sx={{ fontWeight: 500, mb: 1 }}>
|
{order.scope_description && (
|
||||||
{order.scope_title}
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
</Typography>
|
{order.scope_description}
|
||||||
)}
|
</Typography>
|
||||||
{order.scope_description && (
|
)}
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
||||||
{order.scope_description}
|
{order.sections.map((section, index) => (
|
||||||
</Typography>
|
<Box
|
||||||
)}
|
key={section.id || index}
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
sx={{
|
||||||
{order.sections.map((section, index) => (
|
border: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
key={section.id || index}
|
|
||||||
sx={{
|
sx={{
|
||||||
border: 1,
|
display: "flex",
|
||||||
borderColor: "divider",
|
alignItems: "center",
|
||||||
borderRadius: 2,
|
gap: 1,
|
||||||
overflow: "hidden",
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
bgcolor: "action.hover",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Typography
|
||||||
sx={{
|
variant="body2"
|
||||||
display: "flex",
|
color="text.secondary"
|
||||||
alignItems: "center",
|
sx={{ fontWeight: 600 }}
|
||||||
gap: 1,
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
bgcolor: "action.hover",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Typography
|
{index + 1}.
|
||||||
variant="body2"
|
</Typography>
|
||||||
color="text.secondary"
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
sx={{ fontWeight: 600 }}
|
{(order.language === "CZ"
|
||||||
>
|
? section.title_cz || section.title
|
||||||
{index + 1}.
|
: section.title || section.title_cz) ||
|
||||||
</Typography>
|
`Sekce ${index + 1}`}
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
</Typography>
|
||||||
{(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),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
{section.content && (
|
||||||
</Box>
|
<Box
|
||||||
</Card>
|
className="admin-rich-text-view"
|
||||||
</motion.div>
|
sx={{ p: 2 }}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(section.content),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Notes (editable) */}
|
{/* Notes (editable) */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Poznámky
|
||||||
transition={{ duration: 0.25, delay: 0.2 }}
|
</Typography>
|
||||||
>
|
<Field label="Poznámky">
|
||||||
<Card sx={{ mb: 3 }}>
|
<TextField
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
value={notes}
|
||||||
Poznámky
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
</Typography>
|
multiline
|
||||||
<Field label="Poznámky">
|
minRows={4}
|
||||||
<TextField
|
placeholder="Interní poznámky k objednávce..."
|
||||||
value={notes}
|
fullWidth
|
||||||
onChange={(e) => setNotes(e.target.value)}
|
disabled={!hasPermission("orders.edit")}
|
||||||
multiline
|
/>
|
||||||
minRows={4}
|
</Field>
|
||||||
placeholder="Interní poznámky k objednávce..."
|
{hasPermission("orders.edit") && (
|
||||||
fullWidth
|
<Box sx={{ mt: 1 }}>
|
||||||
disabled={!hasPermission("orders.edit")}
|
<Button
|
||||||
/>
|
variant="outlined"
|
||||||
</Field>
|
color="inherit"
|
||||||
{hasPermission("orders.edit") && (
|
onClick={handleSaveNotes}
|
||||||
<Box sx={{ mt: 1 }}>
|
disabled={saving}
|
||||||
<Button
|
>
|
||||||
variant="outlined"
|
{saving ? "Ukládání..." : "Uložit poznámky"}
|
||||||
color="inherit"
|
</Button>
|
||||||
onClick={handleSaveNotes}
|
</Box>
|
||||||
disabled={saving}
|
)}
|
||||||
>
|
</Card>
|
||||||
{saving ? "Ukládání..." : "Uložit poznámky"}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Status change confirmation */}
|
{/* Status change confirmation */}
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -887,6 +859,6 @@ export default function OrderDetail() {
|
|||||||
applyVat={!!order.apply_vat}
|
applyVat={!!order.apply_vat}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
@@ -32,6 +31,7 @@ import {
|
|||||||
CheckboxField,
|
CheckboxField,
|
||||||
FileUpload,
|
FileUpload,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -463,24 +463,18 @@ export default function Orders() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Objednávky"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={subtitle}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
hasPermission("orders.create") ? (
|
||||||
<PageHeader
|
<Button startIcon={PlusIcon} onClick={openCreate}>
|
||||||
title="Objednávky"
|
Vytvořit objednávku
|
||||||
subtitle={subtitle}
|
</Button>
|
||||||
actions={
|
) : undefined
|
||||||
hasPermission("orders.create") ? (
|
}
|
||||||
<Button startIcon={PlusIcon} onClick={openCreate}>
|
/>
|
||||||
Vytvořit objednávku
|
|
||||||
</Button>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<Box sx={{ flex: "1 1 320px" }}>
|
<Box sx={{ flex: "1 1 320px" }}>
|
||||||
@@ -496,33 +490,27 @@ export default function Orders() {
|
|||||||
</Box>
|
</Box>
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
||||||
<motion.div
|
<Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<DataTable<Order>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
columns={columns}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
rows={orders}
|
||||||
>
|
rowKey={(o) => o.id}
|
||||||
<Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
|
sortBy={sort}
|
||||||
<DataTable<Order>
|
sortDir={order}
|
||||||
columns={columns}
|
onSort={handleSort}
|
||||||
rows={orders}
|
empty={
|
||||||
rowKey={(o) => o.id}
|
<EmptyState
|
||||||
sortBy={sort}
|
title="Zatím nejsou žádné objednávky."
|
||||||
sortDir={order}
|
description="Objednávky se vytvářejí z nabídek."
|
||||||
onSort={handleSort}
|
/>
|
||||||
empty={
|
}
|
||||||
<EmptyState
|
/>
|
||||||
title="Zatím nejsou žádné objednávky."
|
<Pagination
|
||||||
description="Objednávky se vytvářejí z nabídek."
|
page={page}
|
||||||
/>
|
pageCount={pagination?.total_pages ?? 1}
|
||||||
}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
<Pagination
|
</Card>
|
||||||
page={page}
|
|
||||||
pageCount={pagination?.total_pages ?? 1}
|
|
||||||
onChange={setPage}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={deleteConfirm.show}
|
isOpen={deleteConfirm.show}
|
||||||
@@ -711,6 +699,6 @@ export default function Orders() {
|
|||||||
onChange={(v) => setCreateForm({ ...createForm, create_project: v })}
|
onChange={(v) => setCreateForm({ ...createForm, create_project: v })}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import PlanGrid from "../components/PlanGrid";
|
|||||||
import PlanCellModal from "../components/PlanCellModal";
|
import PlanCellModal from "../components/PlanCellModal";
|
||||||
import PlanCategoriesModal from "../components/PlanCategoriesModal";
|
import PlanCategoriesModal from "../components/PlanCategoriesModal";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { Button, Alert, LoadingState } from "../ui";
|
import { Button, Alert, LoadingState, PageEnter } from "../ui";
|
||||||
import { projectListOptions, type Project } from "../lib/queries/projects";
|
import { projectListOptions, type Project } from "../lib/queries/projects";
|
||||||
import { planCategoriesOptions, type PlanCategory } from "../lib/queries/plan";
|
import { planCategoriesOptions, type PlanCategory } from "../lib/queries/plan";
|
||||||
// plan.css is imported once globally in AdminApp.tsx — no per-page re-import.
|
// plan.css is imported once globally in AdminApp.tsx — no per-page re-import.
|
||||||
@@ -545,130 +545,107 @@ export default function PlanWork() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="plan-work-page">
|
<PageEnter sx={{ position: "relative" }}>
|
||||||
<motion.div
|
<Typography variant="h4" sx={{ fontWeight: 700, mb: 2 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
Plán prací
|
||||||
animate={{ opacity: 1, y: 0 }}
|
</Typography>
|
||||||
transition={{ duration: 0.25 }}
|
|
||||||
>
|
|
||||||
<Typography variant="h4" sx={{ fontWeight: 700, mb: 2 }}>
|
|
||||||
Plán prací
|
|
||||||
</Typography>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{!canEdit && (
|
{!canEdit && (
|
||||||
<motion.div
|
<Box role="status" sx={{ mb: 2 }}>
|
||||||
role="status"
|
<Alert severity="info">
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<strong>Režim náhledu</strong> — můžete pouze prohlížet plán. Úpravy
|
||||||
animate={{ opacity: 1, y: 0 }}
|
jsou dostupné oprávněným uživatelům.
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
</Alert>
|
||||||
>
|
</Box>
|
||||||
<Box sx={{ mb: 2 }}>
|
|
||||||
<Alert severity="info">
|
|
||||||
<strong>Režim náhledu</strong> — můžete pouze prohlížet plán.
|
|
||||||
Úpravy jsou dostupné oprávněným uživatelům.
|
|
||||||
</Alert>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
flexWrap: "wrap",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1.5,
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Nav buttons grouped so they stay on one row when the toolbar
|
||||||
|
wraps on mobile. */}
|
||||||
|
<Box sx={{ display: "inline-flex", gap: 1 }}>
|
||||||
|
<Button variant="outlined" color="inherit" onClick={goToToday}>
|
||||||
|
Dnes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
onClick={goPrev}
|
||||||
|
aria-label="Předchozí"
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
onClick={goNext}
|
||||||
|
aria-label="Další"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 220,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexWrap: "wrap",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
|
||||||
gap: 1.5,
|
|
||||||
mb: 2,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Nav buttons grouped so they stay on one row when the toolbar
|
<AnimatePresence mode="popLayout" initial={false}>
|
||||||
wraps on mobile. */}
|
<motion.div
|
||||||
<Box sx={{ display: "inline-flex", gap: 1 }}>
|
key={`${isoDate(range.from)}-${view}`}
|
||||||
<Button variant="outlined" color="inherit" onClick={goToToday}>
|
initial={{ opacity: 0 }}
|
||||||
Dnes
|
animate={{ opacity: 1 }}
|
||||||
</Button>
|
exit={{ opacity: 0 }}
|
||||||
<Button
|
transition={{ duration: 0.15 }}
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
onClick={goPrev}
|
|
||||||
aria-label="Předchozí"
|
|
||||||
>
|
>
|
||||||
←
|
<Typography
|
||||||
</Button>
|
variant="h6"
|
||||||
<Button
|
sx={{ fontWeight: 600, whiteSpace: "nowrap" }}
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
onClick={goNext}
|
|
||||||
aria-label="Další"
|
|
||||||
>
|
|
||||||
→
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 220,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AnimatePresence mode="popLayout" initial={false}>
|
|
||||||
<motion.div
|
|
||||||
key={`${isoDate(range.from)}-${view}`}
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
>
|
||||||
<Typography
|
{formatRangeLabel(isoDate(range.from), isoDate(range.to), view)}
|
||||||
variant="h6"
|
</Typography>
|
||||||
sx={{ fontWeight: 600, whiteSpace: "nowrap" }}
|
</motion.div>
|
||||||
>
|
</AnimatePresence>
|
||||||
{formatRangeLabel(
|
|
||||||
isoDate(range.from),
|
|
||||||
isoDate(range.to),
|
|
||||||
view,
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
role="group"
|
|
||||||
aria-label="Měřítko zobrazení"
|
|
||||||
sx={{ display: "inline-flex", gap: 1 }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant={view === "week" ? "contained" : "outlined"}
|
|
||||||
color={view === "week" ? "primary" : "inherit"}
|
|
||||||
onClick={() => setViewWithAnim("week")}
|
|
||||||
>
|
|
||||||
Týden
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={view === "month" ? "contained" : "outlined"}
|
|
||||||
color={view === "month" ? "primary" : "inherit"}
|
|
||||||
onClick={() => setViewWithAnim("month")}
|
|
||||||
>
|
|
||||||
Měsíc
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
{canEdit && (
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
onClick={() => setCatModalOpen(true)}
|
|
||||||
>
|
|
||||||
Správa kategorií
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
<Box
|
||||||
|
role="group"
|
||||||
|
aria-label="Měřítko zobrazení"
|
||||||
|
sx={{ display: "inline-flex", gap: 1 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant={view === "week" ? "contained" : "outlined"}
|
||||||
|
color={view === "week" ? "primary" : "inherit"}
|
||||||
|
onClick={() => setViewWithAnim("week")}
|
||||||
|
>
|
||||||
|
Týden
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={view === "month" ? "contained" : "outlined"}
|
||||||
|
color={view === "month" ? "primary" : "inherit"}
|
||||||
|
onClick={() => setViewWithAnim("month")}
|
||||||
|
>
|
||||||
|
Měsíc
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
{canEdit && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
onClick={() => setCatModalOpen(true)}
|
||||||
|
>
|
||||||
|
Správa kategorií
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
{gridLoading && <LoadingState />}
|
{gridLoading && <LoadingState />}
|
||||||
|
|
||||||
@@ -689,20 +666,14 @@ export default function PlanWork() {
|
|||||||
patch the cache in place and pulse a single cell via pulseKey.
|
patch the cache in place and pulse a single cell via pulseKey.
|
||||||
*/}
|
*/}
|
||||||
{grid ? (
|
{grid ? (
|
||||||
<motion.div
|
<PlanGrid
|
||||||
initial={{ opacity: 0, y: 12 }}
|
data={grid}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
projects={projects}
|
||||||
transition={{ duration: 0.25, ease: "easeOut", delay: 0.18 }}
|
categories={categories}
|
||||||
>
|
canEdit={canEdit}
|
||||||
<PlanGrid
|
pulseKey={lastMutated}
|
||||||
data={grid}
|
onCellClick={openCell}
|
||||||
projects={projects}
|
/>
|
||||||
categories={categories}
|
|
||||||
canEdit={canEdit}
|
|
||||||
pulseKey={lastMutated}
|
|
||||||
onCellClick={openCell}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<PlanCellModal
|
<PlanCellModal
|
||||||
@@ -726,6 +697,6 @@ export default function PlanWork() {
|
|||||||
onClose={() => setCatModalOpen(false)}
|
onClose={() => setCatModalOpen(false)}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useApiMutation } from "../lib/queries/mutations";
|
|||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
CheckboxField,
|
CheckboxField,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
PageEnter,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
@@ -286,13 +286,9 @@ export default function ProjectDetail() {
|
|||||||
const canEdit = hasPermission("projects.edit");
|
const canEdit = hasPermission("projects.edit");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<Box>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25 }}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -347,324 +343,301 @@ export default function ProjectDetail() {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</Box>
|
||||||
|
|
||||||
{/* Basic info form */}
|
{/* Basic info form */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Základní údaje
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
</Typography>
|
||||||
>
|
<Box
|
||||||
<Card sx={{ mb: 3 }}>
|
sx={{
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
display: "grid",
|
||||||
Základní údaje
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||||
</Typography>
|
gap: 2,
|
||||||
<Box
|
}}
|
||||||
sx={{
|
>
|
||||||
display: "grid",
|
<Field label="Číslo projektu">
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
<TextField value={project.project_number} fullWidth disabled />
|
||||||
gap: 2,
|
</Field>
|
||||||
}}
|
<Field label="Název">
|
||||||
>
|
<TextField
|
||||||
<Field label="Číslo projektu">
|
value={form.name}
|
||||||
<TextField value={project.project_number} fullWidth disabled />
|
onChange={(e) => updateForm("name", e.target.value)}
|
||||||
</Field>
|
placeholder="Název projektu"
|
||||||
<Field label="Název">
|
fullWidth
|
||||||
<TextField
|
disabled={!canEdit}
|
||||||
value={form.name}
|
/>
|
||||||
onChange={(e) => updateForm("name", e.target.value)}
|
</Field>
|
||||||
placeholder="Název projektu"
|
<Field label="Zákazník">
|
||||||
fullWidth
|
<TextField
|
||||||
disabled={!canEdit}
|
value={project.customer_name || "—"}
|
||||||
/>
|
fullWidth
|
||||||
</Field>
|
disabled
|
||||||
<Field label="Zákazník">
|
/>
|
||||||
<TextField
|
</Field>
|
||||||
value={project.customer_name || "—"}
|
<Field label="Zodpovědná osoba">
|
||||||
fullWidth
|
<Select
|
||||||
disabled
|
value={form.responsible_user_id}
|
||||||
/>
|
onChange={(v) => updateForm("responsible_user_id", v)}
|
||||||
</Field>
|
disabled={!canEdit}
|
||||||
<Field label="Zodpovědná osoba">
|
>
|
||||||
<Select
|
<MenuItem value="">— Nevybráno —</MenuItem>
|
||||||
value={form.responsible_user_id}
|
{users.map((u) => (
|
||||||
onChange={(v) => updateForm("responsible_user_id", v)}
|
<MenuItem key={u.id} value={String(u.id)}>
|
||||||
disabled={!canEdit}
|
{u.name}
|
||||||
>
|
</MenuItem>
|
||||||
<MenuItem value="">— Nevybráno —</MenuItem>
|
))}
|
||||||
{users.map((u) => (
|
</Select>
|
||||||
<MenuItem key={u.id} value={String(u.id)}>
|
</Field>
|
||||||
{u.name}
|
</Box>
|
||||||
</MenuItem>
|
<Box
|
||||||
))}
|
sx={{
|
||||||
</Select>
|
display: "grid",
|
||||||
</Field>
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
|
||||||
</Box>
|
gap: 2,
|
||||||
<Box
|
}}
|
||||||
sx={{
|
>
|
||||||
display: "grid",
|
<Field label="Stav">
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
|
<Select
|
||||||
gap: 2,
|
value={form.status}
|
||||||
}}
|
onChange={(v) => updateForm("status", v)}
|
||||||
>
|
disabled={!canEdit}
|
||||||
<Field label="Stav">
|
>
|
||||||
<Select
|
<MenuItem value="aktivni">Aktivní</MenuItem>
|
||||||
value={form.status}
|
<MenuItem value="dokonceny">Dokončený</MenuItem>
|
||||||
onChange={(v) => updateForm("status", v)}
|
<MenuItem value="zruseny">Zrušený</MenuItem>
|
||||||
disabled={!canEdit}
|
</Select>
|
||||||
>
|
</Field>
|
||||||
<MenuItem value="aktivni">Aktivní</MenuItem>
|
<Field label="Datum zahájení">
|
||||||
<MenuItem value="dokonceny">Dokončený</MenuItem>
|
<DateField
|
||||||
<MenuItem value="zruseny">Zrušený</MenuItem>
|
value={form.start_date}
|
||||||
</Select>
|
onChange={(val) => updateForm("start_date", val)}
|
||||||
</Field>
|
disabled={!canEdit}
|
||||||
<Field label="Datum zahájení">
|
/>
|
||||||
<DateField
|
</Field>
|
||||||
value={form.start_date}
|
<Field label="Datum ukončení">
|
||||||
onChange={(val) => updateForm("start_date", val)}
|
<DateField
|
||||||
disabled={!canEdit}
|
value={form.end_date}
|
||||||
/>
|
onChange={(val) => updateForm("end_date", val)}
|
||||||
</Field>
|
disabled={!canEdit}
|
||||||
<Field label="Datum ukončení">
|
/>
|
||||||
<DateField
|
</Field>
|
||||||
value={form.end_date}
|
</Box>
|
||||||
onChange={(val) => updateForm("end_date", val)}
|
</Card>
|
||||||
disabled={!canEdit}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Notes */}
|
{/* Notes */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Poznámky
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
</Typography>
|
||||||
>
|
|
||||||
<Card sx={{ mb: 3 }}>
|
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
|
||||||
Poznámky
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* Add note */}
|
{/* Add note */}
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
<TextField
|
<TextField
|
||||||
value={newNote}
|
value={newNote}
|
||||||
onChange={(e) => setNewNote(e.target.value)}
|
onChange={(e) => setNewNote(e.target.value)}
|
||||||
multiline
|
multiline
|
||||||
minRows={2}
|
minRows={2}
|
||||||
placeholder="Napište poznámku..."
|
placeholder="Napište poznámku..."
|
||||||
fullWidth
|
fullWidth
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" && e.ctrlKey && newNote.trim()) {
|
if (e.key === "Enter" && e.ctrlKey && newNote.trim()) {
|
||||||
handleAddNote();
|
handleAddNote();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ mt: 1 }}>
|
<Box sx={{ mt: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={handleAddNote}
|
onClick={handleAddNote}
|
||||||
disabled={addingNote || !newNote.trim()}
|
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,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Typography
|
{addingNote ? "Přidávání..." : "Přidat poznámku"}
|
||||||
variant="caption"
|
</Button>
|
||||||
color="text.secondary"
|
</Box>
|
||||||
sx={{ display: "block", mb: 0.5 }}
|
</Box>
|
||||||
>
|
|
||||||
Starší poznámka (před zavedením systému)
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ whiteSpace: "pre-wrap" }}
|
|
||||||
>
|
|
||||||
{project.notes}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Notes list */}
|
{/* Legacy notes (read-only) */}
|
||||||
{notes.length === 0 && !project.notes && (
|
{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
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ textAlign: "center", py: 2 }}
|
sx={{ whiteSpace: "pre-wrap" }}
|
||||||
>
|
>
|
||||||
Zatím žádné poznámky
|
{project.notes}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
</Box>
|
||||||
{(notes.length > 0 || project.notes) && (
|
)}
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
|
||||||
{notes.map((note) => (
|
{/* 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
|
<Box
|
||||||
key={note.id}
|
|
||||||
sx={{
|
sx={{
|
||||||
p: 1.5,
|
display: "flex",
|
||||||
bgcolor: "action.hover",
|
justifyContent: "space-between",
|
||||||
borderRadius: 2,
|
alignItems: "flex-start",
|
||||||
position: "relative",
|
gap: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box sx={{ flex: 1 }}>
|
||||||
sx={{
|
<Box
|
||||||
display: "flex",
|
sx={{
|
||||||
justifyContent: "space-between",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "center",
|
||||||
gap: 1,
|
gap: 1,
|
||||||
}}
|
mb: 0.5,
|
||||||
>
|
}}
|
||||||
<Box sx={{ flex: 1 }}>
|
>
|
||||||
<Box
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
sx={{
|
{note.user_name}
|
||||||
display: "flex",
|
</Typography>
|
||||||
alignItems: "center",
|
<Typography variant="caption" color="text.secondary">
|
||||||
gap: 1,
|
{formatNoteDate(note.created_at)}
|
||||||
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}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{isAdmin && (
|
<Typography
|
||||||
<IconButton
|
variant="body2"
|
||||||
size="small"
|
sx={{ whiteSpace: "pre-wrap", lineHeight: 1.5 }}
|
||||||
color="error"
|
>
|
||||||
onClick={() => handleDeleteNote(note.id)}
|
{note.content}
|
||||||
title="Smazat poznámku"
|
</Typography>
|
||||||
aria-label="Smazat poznámku"
|
|
||||||
disabled={deletingNoteId === note.id}
|
|
||||||
sx={{ flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
{TrashIcon}
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Box>
|
</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>
|
||||||
))}
|
</Box>
|
||||||
</Box>
|
))}
|
||||||
)}
|
</Box>
|
||||||
</Card>
|
)}
|
||||||
</motion.div>
|
</Card>
|
||||||
|
|
||||||
{/* Project File Manager */}
|
{/* Project File Manager */}
|
||||||
<motion.div
|
<Box sx={{ mb: 2 }}>
|
||||||
style={{ marginBottom: "1rem" }}
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
|
||||||
>
|
|
||||||
<ProjectFileManager
|
<ProjectFileManager
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
projectNumber={project.project_number}
|
projectNumber={project.project_number}
|
||||||
hasPermission={hasPermission}
|
hasPermission={hasPermission}
|
||||||
hasNasFolder={project.has_nas_folder ?? false}
|
hasNasFolder={project.has_nas_folder ?? false}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</Box>
|
||||||
|
|
||||||
{/* Links */}
|
{/* Links */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Propojení
|
||||||
transition={{ duration: 0.25, delay: 0.15 }}
|
</Typography>
|
||||||
>
|
<Box
|
||||||
<Card sx={{ mb: 3 }}>
|
sx={{
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
display: "grid",
|
||||||
Propojení
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||||
</Typography>
|
gap: 2,
|
||||||
<Box
|
}}
|
||||||
sx={{
|
>
|
||||||
display: "grid",
|
<Box>
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
<Typography variant="caption" color="text.secondary">
|
||||||
gap: 2,
|
Objednávka
|
||||||
}}
|
</Typography>
|
||||||
>
|
<Typography variant="body2">
|
||||||
<Box>
|
{project.order_id ? (
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Box
|
||||||
Objednávka
|
component={RouterLink}
|
||||||
</Typography>
|
to={`/orders/${project.order_id}`}
|
||||||
<Typography variant="body2">
|
sx={{
|
||||||
{project.order_id ? (
|
color: "primary.main",
|
||||||
<Box
|
textDecoration: "none",
|
||||||
component={RouterLink}
|
"&:hover": { textDecoration: "underline" },
|
||||||
to={`/orders/${project.order_id}`}
|
}}
|
||||||
sx={{
|
>
|
||||||
color: "primary.main",
|
{project.order_number}
|
||||||
textDecoration: "none",
|
{project.order_status && (
|
||||||
"&:hover": { textDecoration: "underline" },
|
<Box
|
||||||
}}
|
component="span"
|
||||||
>
|
sx={{ color: "text.secondary", ml: 1 }}
|
||||||
{project.order_number}
|
>
|
||||||
{project.order_status && (
|
(
|
||||||
<Box
|
{STATUS_LABELS[project.order_status] ||
|
||||||
component="span"
|
project.order_status}
|
||||||
sx={{ color: "text.secondary", ml: 1 }}
|
)
|
||||||
>
|
</Box>
|
||||||
(
|
)}
|
||||||
{STATUS_LABELS[project.order_status] ||
|
</Box>
|
||||||
project.order_status}
|
) : (
|
||||||
)
|
"—"
|
||||||
</Box>
|
)}
|
||||||
)}
|
</Typography>
|
||||||
</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>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
<Box>
|
||||||
</motion.div>
|
<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
|
<ConfirmDialog
|
||||||
isOpen={deleteConfirm}
|
isOpen={deleteConfirm}
|
||||||
@@ -687,6 +660,6 @@ export default function ProjectDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -32,6 +31,7 @@ import {
|
|||||||
StatusChip,
|
StatusChip,
|
||||||
CheckboxField,
|
CheckboxField,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -346,30 +346,24 @@ export default function Projects() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25 }}
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
mb: 3,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Typography variant="h4">Projekty</Typography>
|
||||||
sx={{
|
{hasPermission("projects.create") && (
|
||||||
display: "flex",
|
<Button startIcon={PlusIcon} onClick={openCreate}>
|
||||||
alignItems: "center",
|
Přidat projekt
|
||||||
justifyContent: "space-between",
|
</Button>
|
||||||
mb: 3,
|
)}
|
||||||
flexWrap: "wrap",
|
</Box>
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h4">Projekty</Typography>
|
|
||||||
{hasPermission("projects.create") && (
|
|
||||||
<Button startIcon={PlusIcon} onClick={openCreate}>
|
|
||||||
Přidat projekt
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -383,38 +377,32 @@ export default function Projects() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<motion.div
|
<Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<DataTable<Project>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
columns={columns}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
rows={projects}
|
||||||
>
|
rowKey={(p) => p.id}
|
||||||
<Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
|
sortBy={sort}
|
||||||
<DataTable<Project>
|
sortDir={order}
|
||||||
columns={columns}
|
onSort={handleSort}
|
||||||
rows={projects}
|
empty={
|
||||||
rowKey={(p) => p.id}
|
<Box sx={{ textAlign: "center", py: 6 }}>
|
||||||
sortBy={sort}
|
<Typography color="text.secondary" gutterBottom>
|
||||||
sortDir={order}
|
Zatím nejsou žádné projekty.
|
||||||
onSort={handleSort}
|
</Typography>
|
||||||
empty={
|
<Typography variant="body2" color="text.secondary">
|
||||||
<Box sx={{ textAlign: "center", py: 6 }}>
|
Projekty vznikají z objednávek nebo je můžete vytvořit ručně
|
||||||
<Typography color="text.secondary" gutterBottom>
|
tlačítkem „Přidat projekt".
|
||||||
Zatím nejsou žádné projekty.
|
</Typography>
|
||||||
</Typography>
|
</Box>
|
||||||
<Typography variant="body2" color="text.secondary">
|
}
|
||||||
Projekty vznikají z objednávek nebo je můžete vytvořit ručně
|
/>
|
||||||
tlačítkem „Přidat projekt".
|
<Pagination
|
||||||
</Typography>
|
page={page}
|
||||||
</Box>
|
pageCount={pagination?.total_pages ?? 1}
|
||||||
}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
<Pagination
|
</Card>
|
||||||
page={page}
|
|
||||||
pageCount={pagination?.total_pages ?? 1}
|
|
||||||
onChange={setPage}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={!!deleteTarget}
|
isOpen={!!deleteTarget}
|
||||||
@@ -538,6 +526,6 @@ export default function Projects() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Navigate, useSearchParams } from "react-router-dom";
|
import { Navigate, useSearchParams } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
type TabDef,
|
type TabDef,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
@@ -780,23 +780,17 @@ export default function Settings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<Box sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="h4">Nastavení</Typography>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
transition={{ duration: 0.25 }}
|
{activeTab === "system"
|
||||||
>
|
? "Systémová nastavení"
|
||||||
<Box sx={{ mb: 3 }}>
|
: activeTab === "firma"
|
||||||
<Typography variant="h4">Nastavení</Typography>
|
? "Informace o firmě"
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
: "Role"}
|
||||||
{activeTab === "system"
|
</Typography>
|
||||||
? "Systémová nastavení"
|
</Box>
|
||||||
: activeTab === "firma"
|
|
||||||
? "Informace o firmě"
|
|
||||||
: "Role"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{tabDefs.length > 0 && (
|
{tabDefs.length > 0 && (
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -808,43 +802,32 @@ export default function Settings() {
|
|||||||
|
|
||||||
{/* Roles tab */}
|
{/* Roles tab */}
|
||||||
<TabPanel value="roles" current={activeTab}>
|
<TabPanel value="roles" current={activeTab}>
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Typography variant="h6">Role</Typography>
|
||||||
sx={{
|
<Button startIcon={PlusIcon} size="small" onClick={openCreateModal}>
|
||||||
display: "flex",
|
Přidat roli
|
||||||
justifyContent: "space-between",
|
</Button>
|
||||||
alignItems: "center",
|
</Box>
|
||||||
mb: 2,
|
<Card>
|
||||||
}}
|
<DataTable<Role>
|
||||||
>
|
columns={roleColumns}
|
||||||
<Typography variant="h6">Role</Typography>
|
rows={roles}
|
||||||
<Button startIcon={PlusIcon} size="small" onClick={openCreateModal}>
|
rowKey={(role) => role.id}
|
||||||
Přidat roli
|
/>
|
||||||
</Button>
|
</Card>
|
||||||
</Box>
|
|
||||||
<Card>
|
|
||||||
<DataTable<Role>
|
|
||||||
columns={roleColumns}
|
|
||||||
rows={roles}
|
|
||||||
rowKey={(role) => role.id}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
{/* System settings tab */}
|
{/* System settings tab */}
|
||||||
<TabPanel value="system" current={activeTab}>
|
<TabPanel value="system" current={activeTab}>
|
||||||
{canManageSystem && (
|
{canManageSystem && (
|
||||||
<motion.div
|
<Box sx={{ mb: "1.5rem" }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
|
||||||
style={{ marginBottom: "1.5rem" }}
|
|
||||||
>
|
|
||||||
<Card>
|
<Card>
|
||||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
Zabezpečení
|
Zabezpečení
|
||||||
@@ -908,16 +891,11 @@ export default function Settings() {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canManageSystem && (
|
{canManageSystem && (
|
||||||
<motion.div
|
<Box sx={{ mb: "1.5rem" }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
|
||||||
style={{ marginBottom: "1.5rem" }}
|
|
||||||
>
|
|
||||||
<Card>
|
<Card>
|
||||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
Přihlašování
|
Přihlašování
|
||||||
@@ -968,7 +946,7 @@ export default function Settings() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sysSettingsLoading && !sysFormInitialized ? (
|
{sysSettingsLoading && !sysFormInitialized ? (
|
||||||
@@ -976,12 +954,7 @@ export default function Settings() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Section 1: Docházka */}
|
{/* Section 1: Docházka */}
|
||||||
<motion.div
|
<Box sx={{ mb: "1.5rem" }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
|
||||||
style={{ marginBottom: "1.5rem" }}
|
|
||||||
>
|
|
||||||
<Card>
|
<Card>
|
||||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
Docházka
|
Docházka
|
||||||
@@ -1051,15 +1024,10 @@ export default function Settings() {
|
|||||||
</Field>
|
</Field>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</Box>
|
||||||
|
|
||||||
{/* Section 2: Emailové notifikace */}
|
{/* Section 2: Emailové notifikace */}
|
||||||
<motion.div
|
<Box sx={{ mb: "1.5rem" }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
|
||||||
style={{ marginBottom: "1.5rem" }}
|
|
||||||
>
|
|
||||||
<Card>
|
<Card>
|
||||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
Emailové notifikace
|
Emailové notifikace
|
||||||
@@ -1124,15 +1092,10 @@ export default function Settings() {
|
|||||||
</Field>
|
</Field>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</Box>
|
||||||
|
|
||||||
{/* Section 4: Omezení požadavků */}
|
{/* Section 4: Omezení požadavků */}
|
||||||
<motion.div
|
<Box sx={{ mb: "1.5rem" }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.24 }}
|
|
||||||
style={{ marginBottom: "1.5rem" }}
|
|
||||||
>
|
|
||||||
<Card>
|
<Card>
|
||||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
Omezení požadavků
|
Omezení požadavků
|
||||||
@@ -1154,39 +1117,26 @@ export default function Settings() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</Box>
|
||||||
|
|
||||||
{/* Section 5: Informace o aplikaci */}
|
{/* Section 5: Informace o aplikaci */}
|
||||||
<motion.div
|
<Box sx={{ mb: "1.5rem" }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.36 }}
|
|
||||||
style={{ marginBottom: "1.5rem" }}
|
|
||||||
>
|
|
||||||
<Card>
|
<Card>
|
||||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
Informace o aplikaci
|
Informace o aplikaci
|
||||||
</Typography>
|
</Typography>
|
||||||
{renderSystemInfo()}
|
{renderSystemInfo()}
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</Box>
|
||||||
|
|
||||||
{/* Save button */}
|
{/* Save button */}
|
||||||
<motion.div
|
<Button
|
||||||
initial={{ opacity: 0, y: 12 }}
|
fullWidth
|
||||||
animate={{ opacity: 1, y: 0 }}
|
onClick={handleSaveSystemSettings}
|
||||||
transition={{ duration: 0.25, delay: 0.42 }}
|
disabled={saveSystemSettingsMutation.isPending}
|
||||||
>
|
>
|
||||||
<Button
|
{saveSystemSettingsMutation.isPending ? "Ukládání..." : "Uložit"}
|
||||||
fullWidth
|
</Button>
|
||||||
onClick={handleSaveSystemSettings}
|
|
||||||
disabled={saveSystemSettingsMutation.isPending}
|
|
||||||
>
|
|
||||||
{saveSystemSettingsMutation.isPending
|
|
||||||
? "Ukládání..."
|
|
||||||
: "Uložit"}
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
@@ -1399,6 +1349,6 @@ export default function Settings() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={deleteRoleMutation.isPending}
|
loading={deleteRoleMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -30,6 +29,7 @@ import {
|
|||||||
StatusChip,
|
StatusChip,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
StatCard,
|
StatCard,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -418,124 +418,106 @@ export default function Trips() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25 }}
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4">Kniha jízd</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
|
{new Date().toLocaleDateString("cs-CZ", {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
||||||
|
Přidat jízdu
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: "1fr",
|
||||||
|
sm: "1fr 1fr",
|
||||||
|
lg: "repeat(4, 1fr)",
|
||||||
|
},
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StatCard
|
||||||
|
label="Počet jízd"
|
||||||
|
value={totals.count}
|
||||||
|
icon={TripsIcon}
|
||||||
|
color="info"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Celkem naježděno"
|
||||||
|
value={`${formatKm(totals.total)} km`}
|
||||||
|
icon={TotalIcon}
|
||||||
|
color="default"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Služební"
|
||||||
|
value={`${formatKm(totals.business)} km`}
|
||||||
|
icon={BusinessIcon}
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Soukromé"
|
||||||
|
value={`${formatKm(totals.private)} km`}
|
||||||
|
icon={PrivateIcon}
|
||||||
|
color="warning"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Recent Trips */}
|
||||||
|
<Card sx={{ mt: 3 }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
flexWrap: "wrap",
|
mb: 2,
|
||||||
gap: 2,
|
|
||||||
mb: 3,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Typography variant="h6">Poslední jízdy</Typography>
|
||||||
<Typography variant="h4">Kniha jízd</Typography>
|
<Button
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
component={RouterLink}
|
||||||
{new Date().toLocaleDateString("cs-CZ", {
|
to="/trips/history"
|
||||||
month: "long",
|
variant="outlined"
|
||||||
year: "numeric",
|
color="inherit"
|
||||||
})}
|
size="small"
|
||||||
</Typography>
|
>
|
||||||
</Box>
|
Zobrazit historii
|
||||||
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
|
||||||
Přidat jízdu
|
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
<DataTable<BackendTrip>
|
||||||
|
columns={columns}
|
||||||
{/* Stats Cards */}
|
rows={recentTrips}
|
||||||
<motion.div
|
rowKey={(trip) => trip.id}
|
||||||
initial={{ opacity: 0, y: 12 }}
|
empty={
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<Box sx={{ textAlign: "center", py: 6 }}>
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
<Typography color="text.secondary" gutterBottom>
|
||||||
>
|
Zatím nemáte žádné záznamy jízd.
|
||||||
<Box
|
</Typography>
|
||||||
sx={{
|
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
||||||
display: "grid",
|
Přidat první jízdu
|
||||||
gridTemplateColumns: {
|
</Button>
|
||||||
xs: "1fr",
|
</Box>
|
||||||
sm: "1fr 1fr",
|
}
|
||||||
lg: "repeat(4, 1fr)",
|
/>
|
||||||
},
|
</Card>
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StatCard
|
|
||||||
label="Počet jízd"
|
|
||||||
value={totals.count}
|
|
||||||
icon={TripsIcon}
|
|
||||||
color="info"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="Celkem naježděno"
|
|
||||||
value={`${formatKm(totals.total)} km`}
|
|
||||||
icon={TotalIcon}
|
|
||||||
color="default"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="Služební"
|
|
||||||
value={`${formatKm(totals.business)} km`}
|
|
||||||
icon={BusinessIcon}
|
|
||||||
color="success"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="Soukromé"
|
|
||||||
value={`${formatKm(totals.private)} km`}
|
|
||||||
icon={PrivateIcon}
|
|
||||||
color="warning"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Recent Trips */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
|
||||||
>
|
|
||||||
<Card sx={{ mt: 3 }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
mb: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6">Poslední jízdy</Typography>
|
|
||||||
<Button
|
|
||||||
component={RouterLink}
|
|
||||||
to="/trips/history"
|
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
Zobrazit historii
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<DataTable<BackendTrip>
|
|
||||||
columns={columns}
|
|
||||||
rows={recentTrips}
|
|
||||||
rowKey={(trip) => trip.id}
|
|
||||||
empty={
|
|
||||||
<Box sx={{ textAlign: "center", py: 6 }}>
|
|
||||||
<Typography color="text.secondary" gutterBottom>
|
|
||||||
Zatím nemáte žádné záznamy jízd.
|
|
||||||
</Typography>
|
|
||||||
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
|
||||||
Přidat první jízdu
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Add/Edit Modal */}
|
{/* Add/Edit Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
@@ -696,6 +678,6 @@ export default function Trips() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={deleteMutation.isPending}
|
loading={deleteMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -33,6 +32,7 @@ import {
|
|||||||
LoadingState,
|
LoadingState,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
StatCard,
|
StatCard,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -529,171 +529,147 @@ export default function TripsAdmin() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25 }}
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Typography variant="h4">Správa knihy jízd</Typography>
|
||||||
sx={{
|
<Box sx={{ display: "flex", gap: 1.5, flexWrap: "wrap" }}>
|
||||||
display: "flex",
|
{trips.length > 0 && (
|
||||||
alignItems: "flex-start",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: 2,
|
|
||||||
mb: 3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h4">Správa knihy jízd</Typography>
|
|
||||||
<Box sx={{ display: "flex", gap: 1.5, flexWrap: "wrap" }}>
|
|
||||||
{trips.length > 0 && (
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
startIcon={PrintIcon}
|
|
||||||
onClick={handlePrint}
|
|
||||||
title="Tisk knihy jízd"
|
|
||||||
>
|
|
||||||
Tisk
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
component={RouterLink}
|
|
||||||
to="/vehicles"
|
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
startIcon={PrintIcon}
|
||||||
|
onClick={handlePrint}
|
||||||
|
title="Tisk knihy jízd"
|
||||||
>
|
>
|
||||||
Vozidla
|
Tisk
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
)}
|
||||||
|
<Button
|
||||||
|
component={RouterLink}
|
||||||
|
to="/vehicles"
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
Vozidla
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</Box>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<motion.div
|
<FilterBar>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Box sx={{ flex: "0 0 160px" }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<Field label="Měsíc">
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
<Select
|
||||||
>
|
value={filterMonth}
|
||||||
<FilterBar>
|
onChange={setFilterMonth}
|
||||||
<Box sx={{ flex: "0 0 160px" }}>
|
options={Array.from({ length: 12 }, (_, i) => ({
|
||||||
<Field label="Měsíc">
|
value: String(i + 1),
|
||||||
<Select
|
label: new Date(2000, i).toLocaleString("cs-CZ", {
|
||||||
value={filterMonth}
|
month: "long",
|
||||||
onChange={setFilterMonth}
|
}),
|
||||||
options={Array.from({ length: 12 }, (_, i) => ({
|
}))}
|
||||||
value: String(i + 1),
|
/>
|
||||||
label: new Date(2000, i).toLocaleString("cs-CZ", {
|
</Field>
|
||||||
month: "long",
|
</Box>
|
||||||
}),
|
<Box sx={{ flex: "0 0 120px" }}>
|
||||||
}))}
|
<Field label="Rok">
|
||||||
/>
|
<Select
|
||||||
</Field>
|
value={filterYear}
|
||||||
</Box>
|
onChange={setFilterYear}
|
||||||
<Box sx={{ flex: "0 0 120px" }}>
|
options={Array.from({ length: 5 }, (_, i) => {
|
||||||
<Field label="Rok">
|
const y = new Date().getFullYear() - 2 + i;
|
||||||
<Select
|
return { value: String(y), label: String(y) };
|
||||||
value={filterYear}
|
})}
|
||||||
onChange={setFilterYear}
|
/>
|
||||||
options={Array.from({ length: 5 }, (_, i) => {
|
</Field>
|
||||||
const y = new Date().getFullYear() - 2 + i;
|
</Box>
|
||||||
return { value: String(y), label: String(y) };
|
<Box sx={{ flex: "1 1 200px" }}>
|
||||||
})}
|
<Field label="Vozidlo">
|
||||||
/>
|
<Select
|
||||||
</Field>
|
value={filterVehicleId}
|
||||||
</Box>
|
onChange={setFilterVehicleId}
|
||||||
<Box sx={{ flex: "1 1 200px" }}>
|
options={[
|
||||||
<Field label="Vozidlo">
|
{ value: "", label: "Všechna vozidla" },
|
||||||
<Select
|
...vehicles.map((v) => ({
|
||||||
value={filterVehicleId}
|
value: String(v.id),
|
||||||
onChange={setFilterVehicleId}
|
label: `${v.spz} - ${v.name}`,
|
||||||
options={[
|
})),
|
||||||
{ value: "", label: "Všechna vozidla" },
|
]}
|
||||||
...vehicles.map((v) => ({
|
/>
|
||||||
value: String(v.id),
|
</Field>
|
||||||
label: `${v.spz} - ${v.name}`,
|
</Box>
|
||||||
})),
|
<Box sx={{ flex: "1 1 200px" }}>
|
||||||
]}
|
<Field label="Řidič">
|
||||||
/>
|
<Select
|
||||||
</Field>
|
value={filterUserId}
|
||||||
</Box>
|
onChange={setFilterUserId}
|
||||||
<Box sx={{ flex: "1 1 200px" }}>
|
options={[
|
||||||
<Field label="Řidič">
|
{ value: "", label: "Všichni řidiči" },
|
||||||
<Select
|
...tripUsers.map((u) => ({
|
||||||
value={filterUserId}
|
value: String(u.id),
|
||||||
onChange={setFilterUserId}
|
label: u.name,
|
||||||
options={[
|
})),
|
||||||
{ value: "", label: "Všichni řidiči" },
|
]}
|
||||||
...tripUsers.map((u) => ({
|
/>
|
||||||
value: String(u.id),
|
</Field>
|
||||||
label: u.name,
|
</Box>
|
||||||
})),
|
</FilterBar>
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
</FilterBar>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "grid",
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
gridTemplateColumns: {
|
||||||
|
xs: "1fr",
|
||||||
|
sm: "repeat(3, 1fr)",
|
||||||
|
},
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<StatCard
|
||||||
sx={{
|
label="Počet jízd"
|
||||||
display: "grid",
|
value={totals.count}
|
||||||
gridTemplateColumns: {
|
icon={TripsIcon}
|
||||||
xs: "1fr",
|
color="info"
|
||||||
sm: "repeat(3, 1fr)",
|
/>
|
||||||
},
|
<StatCard
|
||||||
gap: 2,
|
label="Celkem naježděno"
|
||||||
}}
|
value={`${formatKm(totals.total)} km`}
|
||||||
>
|
icon={TotalIcon}
|
||||||
<StatCard
|
color="default"
|
||||||
label="Počet jízd"
|
/>
|
||||||
value={totals.count}
|
<StatCard
|
||||||
icon={TripsIcon}
|
label="Služební km"
|
||||||
color="info"
|
value={`${formatKm(totals.business)} km`}
|
||||||
/>
|
icon={BusinessIcon}
|
||||||
<StatCard
|
color="success"
|
||||||
label="Celkem naježděno"
|
/>
|
||||||
value={`${formatKm(totals.total)} km`}
|
</Box>
|
||||||
icon={TotalIcon}
|
|
||||||
color="default"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="Služební km"
|
|
||||||
value={`${formatKm(totals.business)} km`}
|
|
||||||
icon={BusinessIcon}
|
|
||||||
color="success"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Trips Table */}
|
{/* Trips Table */}
|
||||||
<motion.div
|
<Card sx={{ mt: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
{isPending ? (
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<LoadingState />
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
) : (
|
||||||
>
|
<DataTable<Trip>
|
||||||
<Card sx={{ mt: 3 }}>
|
columns={columns}
|
||||||
{isPending ? (
|
rows={trips}
|
||||||
<LoadingState />
|
rowKey={(trip) => trip.id}
|
||||||
) : (
|
empty={
|
||||||
<DataTable<Trip>
|
<EmptyState title="Žádné záznamy jízd pro vybrané období." />
|
||||||
columns={columns}
|
}
|
||||||
rows={trips}
|
/>
|
||||||
rowKey={(trip) => trip.id}
|
)}
|
||||||
empty={
|
</Card>
|
||||||
<EmptyState title="Žádné záznamy jízd pro vybrané období." />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Edit Modal */}
|
{/* Edit Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
@@ -962,6 +938,6 @@ export default function TripsAdmin() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
@@ -15,6 +14,7 @@ import {
|
|||||||
MonthField,
|
MonthField,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -216,104 +216,80 @@ export default function TripsHistory() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader title="Historie jízd" subtitle={getMonthName(month)} />
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25 }}
|
|
||||||
>
|
|
||||||
<PageHeader title="Historie jízd" subtitle={getMonthName(month)} />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<motion.div
|
<FilterBar>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Box sx={{ flex: "1 1 200px" }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<Field label="Měsíc">
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
<MonthField value={month} onChange={(val) => setMonth(val)} />
|
||||||
>
|
</Field>
|
||||||
<FilterBar>
|
|
||||||
<Box sx={{ flex: "1 1 200px" }}>
|
|
||||||
<Field label="Měsíc">
|
|
||||||
<MonthField value={month} onChange={(val) => setMonth(val)} />
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ flex: "1 1 200px" }}>
|
|
||||||
<Field label="Vozidlo">
|
|
||||||
<Select
|
|
||||||
value={vehicleId}
|
|
||||||
onChange={(value) => setVehicleId(value)}
|
|
||||||
options={[
|
|
||||||
{ value: "", label: "Všechna vozidla" },
|
|
||||||
...vehicles.map((v) => ({
|
|
||||||
value: String(v.id),
|
|
||||||
label: `${v.spz} - ${v.name}`,
|
|
||||||
})),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
</FilterBar>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: {
|
|
||||||
xs: "1fr",
|
|
||||||
sm: "1fr 1fr",
|
|
||||||
lg: "repeat(3, 1fr)",
|
|
||||||
},
|
|
||||||
gap: 2,
|
|
||||||
mt: 3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StatCard
|
|
||||||
label="Počet jízd"
|
|
||||||
value={totals.count}
|
|
||||||
icon={TripsIcon}
|
|
||||||
color="info"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="Celkem naježděno"
|
|
||||||
value={`${formatKm(totals.total)} km`}
|
|
||||||
icon={TotalIcon}
|
|
||||||
color="default"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="Služební km"
|
|
||||||
value={`${formatKm(totals.business)} km`}
|
|
||||||
icon={BusinessIcon}
|
|
||||||
color="success"
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
<Box sx={{ flex: "1 1 200px" }}>
|
||||||
|
<Field label="Vozidlo">
|
||||||
|
<Select
|
||||||
|
value={vehicleId}
|
||||||
|
onChange={(value) => setVehicleId(value)}
|
||||||
|
options={[
|
||||||
|
{ value: "", label: "Všechna vozidla" },
|
||||||
|
...vehicles.map((v) => ({
|
||||||
|
value: String(v.id),
|
||||||
|
label: `${v.spz} - ${v.name}`,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Box>
|
||||||
|
</FilterBar>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: "1fr",
|
||||||
|
sm: "1fr 1fr",
|
||||||
|
lg: "repeat(3, 1fr)",
|
||||||
|
},
|
||||||
|
gap: 2,
|
||||||
|
mt: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StatCard
|
||||||
|
label="Počet jízd"
|
||||||
|
value={totals.count}
|
||||||
|
icon={TripsIcon}
|
||||||
|
color="info"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Celkem naježděno"
|
||||||
|
value={`${formatKm(totals.total)} km`}
|
||||||
|
icon={TotalIcon}
|
||||||
|
color="default"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Služební km"
|
||||||
|
value={`${formatKm(totals.business)} km`}
|
||||||
|
icon={BusinessIcon}
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Trips Table */}
|
{/* Trips Table */}
|
||||||
<motion.div
|
<Card sx={{ mt: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
{isPending ? (
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<LoadingState />
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
) : (
|
||||||
>
|
<DataTable<Trip>
|
||||||
<Card sx={{ mt: 3 }}>
|
columns={columns}
|
||||||
{isPending ? (
|
rows={trips}
|
||||||
<LoadingState />
|
rowKey={(trip) => trip.id}
|
||||||
) : (
|
empty={
|
||||||
<DataTable<Trip>
|
<EmptyState title="Žádné záznamy jízd pro vybrané období." />
|
||||||
columns={columns}
|
}
|
||||||
rows={trips}
|
/>
|
||||||
rowKey={(trip) => trip.id}
|
)}
|
||||||
empty={
|
</Card>
|
||||||
<EmptyState title="Žádné záznamy jízd pro vybrané období." />
|
</PageEnter>
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Avatar from "@mui/material/Avatar";
|
import Avatar from "@mui/material/Avatar";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -324,53 +324,41 @@ export default function Users() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25 }}
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
mb: 3,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Typography variant="h4">Správa uživatelů</Typography>
|
||||||
sx={{
|
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
||||||
display: "flex",
|
Přidat uživatele
|
||||||
alignItems: "center",
|
</Button>
|
||||||
justifyContent: "space-between",
|
</Box>
|
||||||
mb: 3,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h4">Správa uživatelů</Typography>
|
|
||||||
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
|
||||||
Přidat uživatele
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<Card>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<DataTable<User>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
columns={columns}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
rows={users}
|
||||||
>
|
rowKey={(u) => u.id}
|
||||||
<Card>
|
rowInactive={(u) => !u.is_active}
|
||||||
<DataTable<User>
|
empty={
|
||||||
columns={columns}
|
<Box sx={{ textAlign: "center", py: 6 }}>
|
||||||
rows={users}
|
<Typography color="text.secondary" gutterBottom>
|
||||||
rowKey={(u) => u.id}
|
Zatím nejsou žádní uživatelé.
|
||||||
rowInactive={(u) => !u.is_active}
|
</Typography>
|
||||||
empty={
|
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
||||||
<Box sx={{ textAlign: "center", py: 6 }}>
|
Přidat prvního uživatele
|
||||||
<Typography color="text.secondary" gutterBottom>
|
</Button>
|
||||||
Zatím nejsou žádní uživatelé.
|
</Box>
|
||||||
</Typography>
|
}
|
||||||
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
/>
|
||||||
Přidat prvního uživatele
|
</Card>
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showModal}
|
isOpen={showModal}
|
||||||
@@ -478,6 +466,6 @@ export default function Users() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={deleteUser.isPending}
|
loading={deleteUser.isPending}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Chip from "@mui/material/Chip";
|
import Chip from "@mui/material/Chip";
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
SwitchField,
|
SwitchField,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -299,53 +299,41 @@ export default function Vehicles() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25 }}
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
mb: 3,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Typography variant="h4">Správa vozidel</Typography>
|
||||||
sx={{
|
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
||||||
display: "flex",
|
Přidat vozidlo
|
||||||
alignItems: "center",
|
</Button>
|
||||||
justifyContent: "space-between",
|
</Box>
|
||||||
mb: 3,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h4">Správa vozidel</Typography>
|
|
||||||
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
|
||||||
Přidat vozidlo
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<Card>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<DataTable<Vehicle>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
columns={columns}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
rows={vehicles}
|
||||||
>
|
rowKey={(v) => v.id}
|
||||||
<Card>
|
rowInactive={(v) => !v.is_active}
|
||||||
<DataTable<Vehicle>
|
empty={
|
||||||
columns={columns}
|
<Box sx={{ textAlign: "center", py: 6 }}>
|
||||||
rows={vehicles}
|
<Typography color="text.secondary" gutterBottom>
|
||||||
rowKey={(v) => v.id}
|
Zatím nejsou žádná vozidla.
|
||||||
rowInactive={(v) => !v.is_active}
|
</Typography>
|
||||||
empty={
|
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
||||||
<Box sx={{ textAlign: "center", py: 6 }}>
|
Přidat první vozidlo
|
||||||
<Typography color="text.secondary" gutterBottom>
|
</Button>
|
||||||
Zatím nejsou žádná vozidla.
|
</Box>
|
||||||
</Typography>
|
}
|
||||||
<Button startIcon={PlusIcon} onClick={openCreateModal}>
|
/>
|
||||||
Přidat první vozidlo
|
</Card>
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showModal}
|
isOpen={showModal}
|
||||||
@@ -438,6 +426,6 @@ export default function Vehicles() {
|
|||||||
confirmText="Smazat"
|
confirmText="Smazat"
|
||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
type StatCardColor,
|
type StatCardColor,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
@@ -193,74 +193,64 @@ export default function Warehouse() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Sklad"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle="Přehled skladových zásob"
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
<Box sx={{ display: "flex", gap: 1.5, flexWrap: "wrap" }}>
|
||||||
<PageHeader
|
{hasPermission("warehouse.operate") && (
|
||||||
title="Sklad"
|
<>
|
||||||
subtitle="Přehled skladových zásob"
|
|
||||||
actions={
|
|
||||||
<Box sx={{ display: "flex", gap: 1.5, flexWrap: "wrap" }}>
|
|
||||||
{hasPermission("warehouse.operate") && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
component={RouterLink}
|
|
||||||
to="/warehouse/receipts/new"
|
|
||||||
startIcon={ReceiptIcon}
|
|
||||||
>
|
|
||||||
Nový příjem
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
component={RouterLink}
|
|
||||||
to="/warehouse/issues/new"
|
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
startIcon={IssueIcon}
|
|
||||||
>
|
|
||||||
Nový výdej
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{hasPermission("warehouse.manage") && (
|
|
||||||
<Button
|
<Button
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to="/warehouse/categories"
|
to="/warehouse/receipts/new"
|
||||||
|
startIcon={ReceiptIcon}
|
||||||
|
>
|
||||||
|
Nový příjem
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
component={RouterLink}
|
||||||
|
to="/warehouse/issues/new"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
startIcon={IssueIcon}
|
||||||
>
|
>
|
||||||
Kategorie
|
Nový výdej
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
|
{hasPermission("warehouse.manage") && (
|
||||||
<Button
|
<Button
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to="/warehouse/items"
|
to="/warehouse/categories"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
>
|
>
|
||||||
Položky
|
Kategorie
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
)}
|
||||||
component={RouterLink}
|
<Button
|
||||||
to="/warehouse/reports"
|
component={RouterLink}
|
||||||
variant="outlined"
|
to="/warehouse/items"
|
||||||
color="inherit"
|
variant="outlined"
|
||||||
>
|
color="inherit"
|
||||||
Reporty
|
>
|
||||||
</Button>
|
Položky
|
||||||
</Box>
|
</Button>
|
||||||
}
|
<Button
|
||||||
/>
|
component={RouterLink}
|
||||||
</motion.div>
|
to="/warehouse/reports"
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
Reporty
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* KPI cards */}
|
{/* KPI cards */}
|
||||||
<Box
|
<Box
|
||||||
component={motion.div}
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
|
||||||
sx={{
|
sx={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: {
|
gridTemplateColumns: {
|
||||||
@@ -296,13 +286,7 @@ export default function Warehouse() {
|
|||||||
|
|
||||||
{/* Below minimum alert */}
|
{/* Below minimum alert */}
|
||||||
{belowMin.length > 0 && (
|
{belowMin.length > 0 && (
|
||||||
<Box
|
<Box sx={{ mb: 3 }}>
|
||||||
component={motion.div}
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.14 }}
|
|
||||||
sx={{ mb: 3 }}
|
|
||||||
>
|
|
||||||
<Card sx={{ borderColor: "error.main", borderWidth: 1 }}>
|
<Card sx={{ borderColor: "error.main", borderWidth: 1 }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -342,12 +326,7 @@ export default function Warehouse() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Recent movements */}
|
{/* Recent movements */}
|
||||||
<Box
|
<Box>
|
||||||
component={motion.div}
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.16 }}
|
|
||||||
>
|
|
||||||
<Card>
|
<Card>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -381,6 +360,6 @@ export default function Warehouse() {
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Field,
|
Field,
|
||||||
TextField,
|
TextField,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
@@ -238,7 +239,7 @@ export default function WarehouseCategories() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Kategorie skladu"
|
title="Kategorie skladu"
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
@@ -327,6 +328,6 @@ export default function WarehouseCategories() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={deleteMutation.isPending}
|
loading={deleteMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -127,25 +127,19 @@ export default function WarehouseInventory() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Inventury"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={subtitle}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
<Button
|
||||||
<PageHeader
|
startIcon={PlusIcon}
|
||||||
title="Inventury"
|
onClick={() => navigate("/warehouse/inventory/new")}
|
||||||
subtitle={subtitle}
|
>
|
||||||
actions={
|
Nová inventura
|
||||||
<Button
|
</Button>
|
||||||
startIcon={PlusIcon}
|
}
|
||||||
onClick={() => navigate("/warehouse/inventory/new")}
|
/>
|
||||||
>
|
|
||||||
Nová inventura
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<Box sx={{ flex: "0 0 200px" }}>
|
<Box sx={{ flex: "0 0 200px" }}>
|
||||||
@@ -199,6 +193,6 @@ export default function WarehouseInventory() {
|
|||||||
onChange={setPage}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams, Link as RouterLink } from "react-router-dom";
|
import { useParams, Link as RouterLink } from "react-router-dom";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
@@ -23,6 +22,7 @@ import {
|
|||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
@@ -187,117 +187,99 @@ export default function WarehouseInventoryDetail() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title={s.session_number || "Inventura"}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
actions={
|
||||||
transition={{ duration: 0.25 }}
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
>
|
<Button
|
||||||
<PageHeader
|
component={RouterLink}
|
||||||
title={s.session_number || "Inventura"}
|
to="/warehouse/inventory"
|
||||||
actions={
|
variant="outlined"
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
color="inherit"
|
||||||
|
startIcon={BackIcon}
|
||||||
|
>
|
||||||
|
Zpět
|
||||||
|
</Button>
|
||||||
|
<StatusChip
|
||||||
|
label={STATUS_LABEL[s.status] ?? s.status}
|
||||||
|
color={STATUS_COLOR[s.status] ?? "default"}
|
||||||
|
/>
|
||||||
|
{s.status === "DRAFT" && (
|
||||||
<Button
|
<Button
|
||||||
component={RouterLink}
|
onClick={() => setShowConfirmModal(true)}
|
||||||
to="/warehouse/inventory"
|
disabled={confirming}
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
startIcon={BackIcon}
|
|
||||||
>
|
>
|
||||||
Zpět
|
{confirming ? "Potvrzování..." : "Potvrdit inventuru"}
|
||||||
</Button>
|
</Button>
|
||||||
<StatusChip
|
|
||||||
label={STATUS_LABEL[s.status] ?? s.status}
|
|
||||||
color={STATUS_COLOR[s.status] ?? "default"}
|
|
||||||
/>
|
|
||||||
{s.status === "DRAFT" && (
|
|
||||||
<Button
|
|
||||||
onClick={() => setShowConfirmModal(true)}
|
|
||||||
disabled={confirming}
|
|
||||||
>
|
|
||||||
{confirming ? "Potvrzování..." : "Potvrdit inventuru"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Header 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>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Číslo inventury
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
fontFamily: "'DM Mono', Menlo, monospace",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{s.session_number || "—"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Vytvořeno
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
|
||||||
>
|
|
||||||
{formatDate(s.created_at)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
{s.notes && (
|
|
||||||
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Poznámky
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
|
|
||||||
{s.notes}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
}
|
||||||
</motion.div>
|
/>
|
||||||
|
|
||||||
|
{/* Header info */}
|
||||||
|
<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,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Číslo inventury
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "'DM Mono', Menlo, monospace",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.session_number || "—"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Vytvořeno
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
||||||
|
>
|
||||||
|
{formatDate(s.created_at)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{s.notes && (
|
||||||
|
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Poznámky
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
|
||||||
|
{s.notes}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Lines table */}
|
{/* Lines table */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Položky
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
</Typography>
|
||||||
>
|
<DataTable<WarehouseInventoryItem>
|
||||||
<Card sx={{ mb: 3 }}>
|
columns={itemColumns}
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
rows={items}
|
||||||
Položky
|
rowKey={(row) => row.id}
|
||||||
</Typography>
|
empty={<EmptyState title="Žádné řádky" />}
|
||||||
<DataTable<WarehouseInventoryItem>
|
/>
|
||||||
columns={itemColumns}
|
</Card>
|
||||||
rows={items}
|
|
||||||
rowKey={(row) => row.id}
|
|
||||||
empty={<EmptyState title="Žádné řádky" />}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Confirm modal */}
|
{/* Confirm modal */}
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -309,6 +291,6 @@ export default function WarehouseInventoryDetail() {
|
|||||||
confirmText="Potvrdit inventuru"
|
confirmText="Potvrdit inventuru"
|
||||||
loading={confirming}
|
loading={confirming}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useId, useRef } from "react";
|
import { useState, useId, useRef } from "react";
|
||||||
import { useNavigate, Link as RouterLink } from "react-router-dom";
|
import { useNavigate, Link as RouterLink } from "react-router-dom";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -14,7 +13,7 @@ import {
|
|||||||
type WarehouseLocation,
|
type WarehouseLocation,
|
||||||
} from "../lib/queries/warehouse";
|
} from "../lib/queries/warehouse";
|
||||||
import { useApiMutation } from "../lib/queries/mutations";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import { Button, Card, TextField, Select, Field } from "../ui";
|
import { Button, Card, TextField, Select, Field, PageEnter } from "../ui";
|
||||||
|
|
||||||
interface InventoryItem {
|
interface InventoryItem {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -165,139 +164,121 @@ export default function WarehouseInventoryForm() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25 }}
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
||||||
sx={{
|
<IconButton
|
||||||
display: "flex",
|
component={RouterLink}
|
||||||
alignItems: "flex-start",
|
to="/warehouse/inventory"
|
||||||
justifyContent: "space-between",
|
title="Zpět na seznam"
|
||||||
flexWrap: "wrap",
|
aria-label="Zpět na seznam"
|
||||||
gap: 2,
|
>
|
||||||
mb: 3,
|
{BackIcon}
|
||||||
}}
|
</IconButton>
|
||||||
>
|
<Typography variant="h4">Nová inventura</Typography>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
|
||||||
<IconButton
|
|
||||||
component={RouterLink}
|
|
||||||
to="/warehouse/inventory"
|
|
||||||
title="Zpět na seznam"
|
|
||||||
aria-label="Zpět na seznam"
|
|
||||||
>
|
|
||||||
{BackIcon}
|
|
||||||
</IconButton>
|
|
||||||
<Typography variant="h4">Nová inventura</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
||||||
<Button onClick={handleSubmit} disabled={saving}>
|
|
||||||
{saving ? "Ukládání..." : "Vytvořit inventuru"}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
|
<Button onClick={handleSubmit} disabled={saving}>
|
||||||
|
{saving ? "Ukládání..." : "Vytvořit inventuru"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Notes */}
|
{/* Notes */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Základní údaje
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
</Typography>
|
||||||
>
|
<Field label="Poznámky">
|
||||||
<Card sx={{ mb: 3 }}>
|
<TextField
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
multiline
|
||||||
Základní údaje
|
minRows={3}
|
||||||
</Typography>
|
value={notes}
|
||||||
<Field label="Poznámky">
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
<TextField
|
placeholder="Volitelné poznámky k inventuře"
|
||||||
multiline
|
/>
|
||||||
minRows={3}
|
</Field>
|
||||||
value={notes}
|
</Card>
|
||||||
onChange={(e) => setNotes(e.target.value)}
|
|
||||||
placeholder="Volitelné poznámky k inventuře"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Lines */}
|
{/* Lines */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Položky
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
</Typography>
|
||||||
>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
||||||
<Card sx={{ mb: 3 }}>
|
{items.map((item, index) => (
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
<Box
|
||||||
Položky
|
key={item.key}
|
||||||
</Typography>
|
sx={{
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
display: "grid",
|
||||||
{items.map((item, index) => (
|
gridTemplateColumns: {
|
||||||
<Box
|
xs: "1fr",
|
||||||
key={item.key}
|
md: "minmax(180px, 2fr) minmax(140px, 1.5fr) 120px auto",
|
||||||
sx={{
|
},
|
||||||
display: "grid",
|
gap: 1,
|
||||||
gridTemplateColumns: {
|
alignItems: "start",
|
||||||
xs: "1fr",
|
}}
|
||||||
md: "minmax(180px, 2fr) minmax(140px, 1.5fr) 120px auto",
|
>
|
||||||
},
|
<ItemPicker
|
||||||
gap: 1,
|
value={item.item_id}
|
||||||
alignItems: "start",
|
onChange={(id) => updateItem(index, "item_id", id)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
item.location_id === null ? "" : String(item.location_id)
|
||||||
|
}
|
||||||
|
onChange={(val) =>
|
||||||
|
updateItem(index, "location_id", val ? Number(val) : null)
|
||||||
|
}
|
||||||
|
options={locationOptions}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
value={item.actual_qty || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateItem(index, "actual_qty", Number(e.target.value))
|
||||||
|
}
|
||||||
|
inputProps={{
|
||||||
|
min: 0,
|
||||||
|
step: 0.001,
|
||||||
|
inputMode: "decimal",
|
||||||
|
pattern: "[0-9]+([\\.,][0-9]+)?",
|
||||||
|
"aria-label": `Skutečné množství, řádek ${index + 1}`,
|
||||||
}}
|
}}
|
||||||
|
placeholder="Skutečné množství"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
color="error"
|
||||||
|
onClick={() => removeItem(index)}
|
||||||
|
title="Odebrat řádek"
|
||||||
|
aria-label="Odebrat řádek"
|
||||||
|
sx={{ justifySelf: { xs: "start", md: "center" } }}
|
||||||
>
|
>
|
||||||
<ItemPicker
|
{RemoveIcon}
|
||||||
value={item.item_id}
|
</IconButton>
|
||||||
onChange={(id) => updateItem(index, "item_id", id)}
|
</Box>
|
||||||
/>
|
))}
|
||||||
<Select
|
</Box>
|
||||||
value={
|
<Button
|
||||||
item.location_id === null ? "" : String(item.location_id)
|
variant="outlined"
|
||||||
}
|
color="inherit"
|
||||||
onChange={(val) =>
|
startIcon={PlusIcon}
|
||||||
updateItem(index, "location_id", val ? Number(val) : null)
|
onClick={addItem}
|
||||||
}
|
sx={{ mt: 2 }}
|
||||||
options={locationOptions}
|
>
|
||||||
/>
|
Přidat položku
|
||||||
<TextField
|
</Button>
|
||||||
type="number"
|
</Card>
|
||||||
value={item.actual_qty || ""}
|
</PageEnter>
|
||||||
onChange={(e) =>
|
|
||||||
updateItem(index, "actual_qty", Number(e.target.value))
|
|
||||||
}
|
|
||||||
inputProps={{
|
|
||||||
min: 0,
|
|
||||||
step: 0.001,
|
|
||||||
inputMode: "decimal",
|
|
||||||
pattern: "[0-9]+([\\.,][0-9]+)?",
|
|
||||||
"aria-label": `Skutečné množství, řádek ${index + 1}`,
|
|
||||||
}}
|
|
||||||
placeholder="Skutečné množství"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
color="error"
|
|
||||||
onClick={() => removeItem(index)}
|
|
||||||
title="Odebrat řádek"
|
|
||||||
aria-label="Odebrat řádek"
|
|
||||||
sx={{ justifySelf: { xs: "start", md: "center" } }}
|
|
||||||
>
|
|
||||||
{RemoveIcon}
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
startIcon={PlusIcon}
|
|
||||||
onClick={addItem}
|
|
||||||
sx={{ mt: 2 }}
|
|
||||||
>
|
|
||||||
Přidat položku
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
@@ -24,6 +23,7 @@ import {
|
|||||||
LoadingState,
|
LoadingState,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -223,57 +223,42 @@ export default function WarehouseIssueDetail() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title={iss.issue_number || "Nová výdejka"}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={
|
||||||
transition={{ duration: 0.25 }}
|
iss.project
|
||||||
>
|
? `${iss.project.project_number} – ${iss.project.name}`
|
||||||
<PageHeader
|
: undefined
|
||||||
title={iss.issue_number || "Nová výdejka"}
|
}
|
||||||
subtitle={
|
actions={
|
||||||
iss.project
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
? `${iss.project.project_number} – ${iss.project.name}`
|
<Button
|
||||||
: undefined
|
component={RouterLink}
|
||||||
}
|
to="/warehouse/issues"
|
||||||
actions={
|
variant="outlined"
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
color="inherit"
|
||||||
<Button
|
startIcon={BackIcon}
|
||||||
component={RouterLink}
|
>
|
||||||
to="/warehouse/issues"
|
Zpět
|
||||||
variant="outlined"
|
</Button>
|
||||||
color="inherit"
|
<StatusChip
|
||||||
startIcon={BackIcon}
|
label={STATUS_LABEL[iss.status] ?? iss.status}
|
||||||
>
|
color={STATUS_COLOR[iss.status] ?? "default"}
|
||||||
Zpět
|
/>
|
||||||
</Button>
|
{canOperate && iss.status === "DRAFT" && (
|
||||||
<StatusChip
|
<>
|
||||||
label={STATUS_LABEL[iss.status] ?? iss.status}
|
<Button
|
||||||
color={STATUS_COLOR[iss.status] ?? "default"}
|
variant="outlined"
|
||||||
/>
|
color="inherit"
|
||||||
{canOperate && iss.status === "DRAFT" && (
|
onClick={() => navigate(`/warehouse/issues/${iss.id}/edit`)}
|
||||||
<>
|
>
|
||||||
<Button
|
Upravit
|
||||||
variant="outlined"
|
</Button>
|
||||||
color="inherit"
|
<Button onClick={handleConfirm} disabled={confirming}>
|
||||||
onClick={() => navigate(`/warehouse/issues/${iss.id}/edit`)}
|
{confirming ? "Potvrzování..." : "Potvrdit"}
|
||||||
>
|
</Button>
|
||||||
Upravit
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleConfirm} disabled={confirming}>
|
|
||||||
{confirming ? "Potvrzování..." : "Potvrdit"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
onClick={() => setCancelConfirm(true)}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{canOperate && iss.status === "CONFIRMED" && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="error"
|
color="error"
|
||||||
@@ -281,126 +266,123 @@ export default function WarehouseIssueDetail() {
|
|||||||
>
|
>
|
||||||
Zrušit
|
Zrušit
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</>
|
||||||
</Box>
|
)}
|
||||||
}
|
{canOperate && iss.status === "CONFIRMED" && (
|
||||||
/>
|
<Button
|
||||||
</motion.div>
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
{/* Basic info */}
|
onClick={() => setCancelConfirm(true)}
|
||||||
<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 1fr" },
|
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Číslo dokladu
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
fontFamily: "'DM Mono', Menlo, monospace",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{iss.issue_number || "—"}
|
Zrušit
|
||||||
</Typography>
|
</Button>
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Projekt
|
|
||||||
</Typography>
|
|
||||||
{iss.project ? (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
component="a"
|
|
||||||
href={`/projects/${iss.project.id}`}
|
|
||||||
sx={{
|
|
||||||
fontFamily: "'DM Mono', Menlo, monospace",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "primary.main",
|
|
||||||
textDecoration: "none",
|
|
||||||
"&:hover": { textDecoration: "underline" },
|
|
||||||
display: "block",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{iss.project.project_number} – {iss.project.name}
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<Typography variant="body2">—</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Vydal
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
fontFamily: "'DM Mono', Menlo, monospace",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{iss.issued_by_user
|
|
||||||
? `${iss.issued_by_user.first_name} ${iss.issued_by_user.last_name}`
|
|
||||||
: "—"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Vytvořeno
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
|
||||||
>
|
|
||||||
{formatDate(iss.created_at)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
{iss.notes && (
|
|
||||||
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Poznámky
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
|
|
||||||
{iss.notes}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
}
|
||||||
</motion.div>
|
/>
|
||||||
|
|
||||||
|
{/* Basic info */}
|
||||||
|
<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 1fr" },
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Číslo dokladu
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "'DM Mono', Menlo, monospace",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{iss.issue_number || "—"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Projekt
|
||||||
|
</Typography>
|
||||||
|
{iss.project ? (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
component="a"
|
||||||
|
href={`/projects/${iss.project.id}`}
|
||||||
|
sx={{
|
||||||
|
fontFamily: "'DM Mono', Menlo, monospace",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: "primary.main",
|
||||||
|
textDecoration: "none",
|
||||||
|
"&:hover": { textDecoration: "underline" },
|
||||||
|
display: "block",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{iss.project.project_number} – {iss.project.name}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2">—</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Vydal
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "'DM Mono', Menlo, monospace",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{iss.issued_by_user
|
||||||
|
? `${iss.issued_by_user.first_name} ${iss.issued_by_user.last_name}`
|
||||||
|
: "—"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Vytvořeno
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
||||||
|
>
|
||||||
|
{formatDate(iss.created_at)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{iss.notes && (
|
||||||
|
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Poznámky
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
|
||||||
|
{iss.notes}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Lines table */}
|
{/* Lines table */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Položky
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
</Typography>
|
||||||
>
|
<DataTable<WarehouseIssueItem>
|
||||||
<Card sx={{ mb: 3 }}>
|
columns={itemColumns}
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
rows={items}
|
||||||
Položky
|
rowKey={(row) => row.id}
|
||||||
</Typography>
|
empty={<EmptyState title="Žádné řádky" />}
|
||||||
<DataTable<WarehouseIssueItem>
|
/>
|
||||||
columns={itemColumns}
|
</Card>
|
||||||
rows={items}
|
|
||||||
rowKey={(row) => row.id}
|
|
||||||
empty={<EmptyState title="Žádné řádky" />}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Cancel confirmation dialog */}
|
{/* Cancel confirmation dialog */}
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -413,6 +395,6 @@ export default function WarehouseIssueDetail() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={cancelling}
|
loading={cancelling}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Link as RouterLink,
|
Link as RouterLink,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -23,7 +22,15 @@ import {
|
|||||||
} from "../lib/queries/warehouse";
|
} from "../lib/queries/warehouse";
|
||||||
import { projectListOptions, type Project } from "../lib/queries/projects";
|
import { projectListOptions, type Project } from "../lib/queries/projects";
|
||||||
import { useApiMutation } from "../lib/queries/mutations";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import { Button, Card, TextField, Select, Field, LoadingState } from "../ui";
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
Field,
|
||||||
|
LoadingState,
|
||||||
|
PageEnter,
|
||||||
|
} from "../ui";
|
||||||
|
|
||||||
interface IssueForm {
|
interface IssueForm {
|
||||||
project_id: number | null;
|
project_id: number | null;
|
||||||
@@ -422,199 +429,181 @@ export default function WarehouseIssueForm() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25 }}
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
||||||
sx={{
|
<IconButton
|
||||||
display: "flex",
|
component={RouterLink}
|
||||||
alignItems: "flex-start",
|
to="/warehouse/issues"
|
||||||
justifyContent: "space-between",
|
title="Zpět na seznam"
|
||||||
flexWrap: "wrap",
|
aria-label="Zpět na seznam"
|
||||||
gap: 2,
|
>
|
||||||
mb: 3,
|
{BackIcon}
|
||||||
}}
|
</IconButton>
|
||||||
>
|
<Typography variant="h4">
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
{isEdit ? "Upravit výdejku" : "Nová výdejka"}
|
||||||
<IconButton
|
</Typography>
|
||||||
component={RouterLink}
|
|
||||||
to="/warehouse/issues"
|
|
||||||
title="Zpět na seznam"
|
|
||||||
aria-label="Zpět na seznam"
|
|
||||||
>
|
|
||||||
{BackIcon}
|
|
||||||
</IconButton>
|
|
||||||
<Typography variant="h4">
|
|
||||||
{isEdit ? "Upravit výdejku" : "Nová výdejka"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
onClick={handleSaveDraft}
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
{saving ? "Ukládání..." : "Uložit jako návrh"}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSaveAndConfirm} disabled={saving}>
|
|
||||||
{saving ? "Ukládání..." : "Uložit a potvrdit"}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
|
|
||||||
{/* Header fields */}
|
|
||||||
<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>
|
|
||||||
<Field label="Projekt" error={errors.project_id}>
|
|
||||||
<Select
|
|
||||||
value={form.project_id === null ? "" : String(form.project_id)}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
project_id: val === "" ? null : Number(val),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
options={projectOptions}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Poznámky">
|
|
||||||
<TextField
|
|
||||||
multiline
|
|
||||||
minRows={3}
|
|
||||||
value={form.notes}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((prev) => ({ ...prev, notes: e.target.value }))
|
|
||||||
}
|
|
||||||
placeholder="Volitelné poznámky"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Lines */}
|
|
||||||
<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 }}>
|
|
||||||
Položky
|
|
||||||
</Typography>
|
|
||||||
{errors.items && (
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="error"
|
|
||||||
sx={{ display: "block", mb: 1.5 }}
|
|
||||||
>
|
|
||||||
{errors.items}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<Box
|
|
||||||
key={item.key}
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: {
|
|
||||||
xs: "1fr",
|
|
||||||
md: "minmax(160px, 1.6fr) minmax(160px, 1.6fr) 100px 100px minmax(130px, 1.1fr) minmax(120px, 1.2fr) auto",
|
|
||||||
},
|
|
||||||
gap: 1,
|
|
||||||
alignItems: "start",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ItemPicker
|
|
||||||
value={item.item_id}
|
|
||||||
itemName={item.item_name}
|
|
||||||
onChange={(id) => updateItem(index, "item_id", id)}
|
|
||||||
/>
|
|
||||||
<BatchSelect
|
|
||||||
itemId={item.item_id}
|
|
||||||
value={item.batch_id}
|
|
||||||
onChange={(batchId, unitPrice) => {
|
|
||||||
updateItem(index, "batch_id", batchId);
|
|
||||||
updateItem(index, "unit_price", unitPrice);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
value={item.quantity || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateItem(index, "quantity", parseDecimal(e.target.value))
|
|
||||||
}
|
|
||||||
inputProps={{
|
|
||||||
min: 0,
|
|
||||||
step: 0.001,
|
|
||||||
inputMode: "decimal",
|
|
||||||
pattern: "[0-9]+([\\.,][0-9]+)?",
|
|
||||||
"aria-label": `Množství, řádek ${index + 1}`,
|
|
||||||
}}
|
|
||||||
placeholder="Množství"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
value={item.unit_price || ""}
|
|
||||||
disabled
|
|
||||||
inputProps={{
|
|
||||||
min: 0,
|
|
||||||
step: 0.01,
|
|
||||||
inputMode: "decimal",
|
|
||||||
pattern: "[0-9]+([\\.,][0-9]+)?",
|
|
||||||
"aria-label": `Cena za kus, řádek ${index + 1}`,
|
|
||||||
}}
|
|
||||||
placeholder="Cena/ks"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={
|
|
||||||
item.location_id === null ? "" : String(item.location_id)
|
|
||||||
}
|
|
||||||
onChange={(val) =>
|
|
||||||
updateItem(index, "location_id", val ? Number(val) : null)
|
|
||||||
}
|
|
||||||
options={locationOptions}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
value={item.notes ?? ""}
|
|
||||||
onChange={(e) => updateItem(index, "notes", e.target.value)}
|
|
||||||
placeholder="Poznámka"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
color="error"
|
|
||||||
onClick={() => removeItem(index)}
|
|
||||||
title="Odebrat řádek"
|
|
||||||
aria-label="Odebrat řádek"
|
|
||||||
sx={{ justifySelf: { xs: "start", md: "center" } }}
|
|
||||||
>
|
|
||||||
{RemoveIcon}
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
startIcon={PlusIcon}
|
onClick={handleSaveDraft}
|
||||||
onClick={addItem}
|
disabled={saving}
|
||||||
sx={{ mt: 2 }}
|
|
||||||
>
|
>
|
||||||
Přidat položku
|
{saving ? "Ukládání..." : "Uložit jako návrh"}
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
<Button onClick={handleSaveAndConfirm} disabled={saving}>
|
||||||
</motion.div>
|
{saving ? "Ukládání..." : "Uložit a potvrdit"}
|
||||||
</Box>
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Header fields */}
|
||||||
|
<Card sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
|
Základní údaje
|
||||||
|
</Typography>
|
||||||
|
<Field label="Projekt" error={errors.project_id}>
|
||||||
|
<Select
|
||||||
|
value={form.project_id === null ? "" : String(form.project_id)}
|
||||||
|
onChange={(val) =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
project_id: val === "" ? null : Number(val),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
options={projectOptions}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Poznámky">
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
minRows={3}
|
||||||
|
value={form.notes}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((prev) => ({ ...prev, notes: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="Volitelné poznámky"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Lines */}
|
||||||
|
<Card sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
|
Položky
|
||||||
|
</Typography>
|
||||||
|
{errors.items && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="error"
|
||||||
|
sx={{ display: "block", mb: 1.5 }}
|
||||||
|
>
|
||||||
|
{errors.items}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Box
|
||||||
|
key={item.key}
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: "1fr",
|
||||||
|
md: "minmax(160px, 1.6fr) minmax(160px, 1.6fr) 100px 100px minmax(130px, 1.1fr) minmax(120px, 1.2fr) auto",
|
||||||
|
},
|
||||||
|
gap: 1,
|
||||||
|
alignItems: "start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemPicker
|
||||||
|
value={item.item_id}
|
||||||
|
itemName={item.item_name}
|
||||||
|
onChange={(id) => updateItem(index, "item_id", id)}
|
||||||
|
/>
|
||||||
|
<BatchSelect
|
||||||
|
itemId={item.item_id}
|
||||||
|
value={item.batch_id}
|
||||||
|
onChange={(batchId, unitPrice) => {
|
||||||
|
updateItem(index, "batch_id", batchId);
|
||||||
|
updateItem(index, "unit_price", unitPrice);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
value={item.quantity || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateItem(index, "quantity", parseDecimal(e.target.value))
|
||||||
|
}
|
||||||
|
inputProps={{
|
||||||
|
min: 0,
|
||||||
|
step: 0.001,
|
||||||
|
inputMode: "decimal",
|
||||||
|
pattern: "[0-9]+([\\.,][0-9]+)?",
|
||||||
|
"aria-label": `Množství, řádek ${index + 1}`,
|
||||||
|
}}
|
||||||
|
placeholder="Množství"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
value={item.unit_price || ""}
|
||||||
|
disabled
|
||||||
|
inputProps={{
|
||||||
|
min: 0,
|
||||||
|
step: 0.01,
|
||||||
|
inputMode: "decimal",
|
||||||
|
pattern: "[0-9]+([\\.,][0-9]+)?",
|
||||||
|
"aria-label": `Cena za kus, řádek ${index + 1}`,
|
||||||
|
}}
|
||||||
|
placeholder="Cena/ks"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
item.location_id === null ? "" : String(item.location_id)
|
||||||
|
}
|
||||||
|
onChange={(val) =>
|
||||||
|
updateItem(index, "location_id", val ? Number(val) : null)
|
||||||
|
}
|
||||||
|
options={locationOptions}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
value={item.notes ?? ""}
|
||||||
|
onChange={(e) => updateItem(index, "notes", e.target.value)}
|
||||||
|
placeholder="Poznámka"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
color="error"
|
||||||
|
onClick={() => removeItem(index)}
|
||||||
|
title="Odebrat řádek"
|
||||||
|
aria-label="Odebrat řádek"
|
||||||
|
sx={{ justifySelf: { xs: "start", md: "center" } }}
|
||||||
|
>
|
||||||
|
{RemoveIcon}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
startIcon={PlusIcon}
|
||||||
|
onClick={addItem}
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
>
|
||||||
|
Přidat položku
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import useDebounce from "../hooks/useDebounce";
|
import useDebounce from "../hooks/useDebounce";
|
||||||
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
@@ -24,6 +23,7 @@ import {
|
|||||||
DateField,
|
DateField,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -166,27 +166,21 @@ export default function WarehouseIssues() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Výdejky"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={subtitle}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
canOperate ? (
|
||||||
<PageHeader
|
<Button
|
||||||
title="Výdejky"
|
startIcon={PlusIcon}
|
||||||
subtitle={subtitle}
|
onClick={() => navigate("/warehouse/issues/new")}
|
||||||
actions={
|
>
|
||||||
canOperate ? (
|
Nová výdejka
|
||||||
<Button
|
</Button>
|
||||||
startIcon={PlusIcon}
|
) : undefined
|
||||||
onClick={() => navigate("/warehouse/issues/new")}
|
}
|
||||||
>
|
/>
|
||||||
Nová výdejka
|
|
||||||
</Button>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<Box sx={{ flex: "1 1 220px" }}>
|
<Box sx={{ flex: "1 1 220px" }}>
|
||||||
@@ -292,6 +286,6 @@ export default function WarehouseIssues() {
|
|||||||
onChange={setPage}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
LoadingState,
|
LoadingState,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -356,145 +356,35 @@ export default function WarehouseItemDetail() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title={title}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={subtitle}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
<PageHeader
|
<Button
|
||||||
title={title}
|
component={RouterLink}
|
||||||
subtitle={subtitle}
|
to="/warehouse/items"
|
||||||
actions={
|
variant="outlined"
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
color="inherit"
|
||||||
<Button
|
startIcon={BackIcon}
|
||||||
component={RouterLink}
|
>
|
||||||
to="/warehouse/items"
|
Zpět
|
||||||
variant="outlined"
|
</Button>
|
||||||
color="inherit"
|
{headerActions}
|
||||||
startIcon={BackIcon}
|
</Box>
|
||||||
>
|
}
|
||||||
Zpět
|
/>
|
||||||
</Button>
|
|
||||||
{headerActions}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Basic info */}
|
{/* Basic info */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Základní údaje
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
</Typography>
|
||||||
>
|
|
||||||
<Card sx={{ mb: 3 }}>
|
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
|
||||||
Základní údaje
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{editing || isNew ? (
|
{editing || isNew ? (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
<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>
|
|
||||||
) : (
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
@@ -502,166 +392,246 @@ export default function WarehouseItemDetail() {
|
|||||||
gap: 2,
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Číslo položky */}
|
<Field label="Číslo položky" error={errors.item_number}>
|
||||||
<Box>
|
<TextField
|
||||||
<Typography variant="caption" color="text.secondary">
|
value={form.item_number}
|
||||||
Číslo položky
|
onChange={(e) => {
|
||||||
</Typography>
|
updateForm("item_number", e.target.value);
|
||||||
<Typography
|
setErrors((prev) => ({ ...prev, item_number: "" }));
|
||||||
variant="body2"
|
}}
|
||||||
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
placeholder="Např. SKL-001"
|
||||||
>
|
fullWidth
|
||||||
{item?.item_number || "—"}
|
/>
|
||||||
</Typography>
|
</Field>
|
||||||
</Box>
|
<Field label="Název" required error={errors.name}>
|
||||||
{/* Název */}
|
<TextField
|
||||||
<Box>
|
value={form.name}
|
||||||
<Typography variant="caption" color="text.secondary">
|
error={!!errors.name}
|
||||||
Název
|
onChange={(e) => {
|
||||||
</Typography>
|
updateForm("name", e.target.value);
|
||||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
setErrors((prev) => ({ ...prev, name: "" }));
|
||||||
{item?.name || "—"}
|
}}
|
||||||
</Typography>
|
placeholder="Název položky"
|
||||||
</Box>
|
fullWidth
|
||||||
{/* Popis */}
|
/>
|
||||||
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
|
</Field>
|
||||||
<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>
|
</Box>
|
||||||
)}
|
<Field label="Popis">
|
||||||
</Card>
|
<TextField
|
||||||
</motion.div>
|
multiline
|
||||||
|
minRows={3}
|
||||||
{/* Stock info — only in view mode, for existing items */}
|
value={form.description}
|
||||||
{id !== "new" && !editing && item && (
|
onChange={(e) => updateForm("description", e.target.value)}
|
||||||
<motion.div
|
placeholder="Volitelný popis položky"
|
||||||
initial={{ opacity: 0, y: 12 }}
|
fullWidth
|
||||||
animate={{ opacity: 1, y: 0 }}
|
/>
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
</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
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: {
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||||
xs: "1fr 1fr",
|
|
||||||
md: "1fr 1fr 1fr 1fr",
|
|
||||||
},
|
|
||||||
gap: 2,
|
gap: 2,
|
||||||
mb: 3,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StatCard
|
{/* Číslo položky */}
|
||||||
label="Celkové množství"
|
<Box>
|
||||||
value={`${item.total_quantity ?? 0} ${item.unit}`}
|
<Typography variant="caption" color="text.secondary">
|
||||||
color="info"
|
Číslo položky
|
||||||
/>
|
</Typography>
|
||||||
<StatCard
|
<Typography
|
||||||
label="K dispozici"
|
variant="body2"
|
||||||
value={`${item.available_quantity ?? 0} ${item.unit}`}
|
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
||||||
color="success"
|
>
|
||||||
/>
|
{item?.item_number || "—"}
|
||||||
<StatCard
|
</Typography>
|
||||||
label="Hodnota zásob"
|
</Box>
|
||||||
value={
|
{/* Název */}
|
||||||
item.stock_value != null
|
<Box>
|
||||||
? formatCurrency(item.stock_value, "CZK")
|
<Typography variant="caption" color="text.secondary">
|
||||||
: "0,00 Kč"
|
Název
|
||||||
}
|
</Typography>
|
||||||
color="warning"
|
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||||
/>
|
{item?.name || "—"}
|
||||||
<StatCard
|
</Typography>
|
||||||
label="Pod minimem"
|
</Box>
|
||||||
value={item.below_minimum ? "Ano" : "Ne"}
|
{/* Popis */}
|
||||||
color={item.below_minimum ? "error" : "info"}
|
<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>
|
</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 */}
|
{/* Batches table — only in view mode, for existing items */}
|
||||||
{id !== "new" && !editing && (
|
{id !== "new" && !editing && (
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Dávky (FIFO)
|
||||||
transition={{ duration: 0.25, delay: 0.1 }}
|
</Typography>
|
||||||
>
|
<DataTable<Batch>
|
||||||
<Card sx={{ mb: 3 }}>
|
columns={batchColumns}
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
rows={batches}
|
||||||
Dávky (FIFO)
|
rowKey={(b) => b.id}
|
||||||
</Typography>
|
empty={<EmptyState title="Zatím žádné dávky" />}
|
||||||
<DataTable<Batch>
|
/>
|
||||||
columns={batchColumns}
|
</Card>
|
||||||
rows={batches}
|
|
||||||
rowKey={(b) => b.id}
|
|
||||||
empty={<EmptyState title="Zatím žádné dávky" />}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Locations table — only in view mode, for existing items */}
|
{/* Locations table — only in view mode, for existing items */}
|
||||||
{id !== "new" && !editing && (
|
{id !== "new" && !editing && (
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Umístění
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
</Typography>
|
||||||
>
|
<DataTable<ItemLocation>
|
||||||
<Card sx={{ mb: 3 }}>
|
columns={locationColumns}
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
rows={locations}
|
||||||
Umístění
|
rowKey={(l) => l.id}
|
||||||
</Typography>
|
empty={<EmptyState title="Zatím žádná umístění" />}
|
||||||
<DataTable<ItemLocation>
|
/>
|
||||||
columns={locationColumns}
|
</Card>
|
||||||
rows={locations}
|
|
||||||
rowKey={(l) => l.id}
|
|
||||||
empty={<EmptyState title="Zatím žádná umístění" />}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete confirmation */}
|
{/* Delete confirmation */}
|
||||||
@@ -675,6 +645,6 @@ export default function WarehouseItemDetail() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={deleteMutation.isPending}
|
loading={deleteMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
@@ -24,6 +23,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -153,27 +153,21 @@ export default function WarehouseItems() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Skladové položky"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={subtitle}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
canManage ? (
|
||||||
<PageHeader
|
<Button
|
||||||
title="Skladové položky"
|
startIcon={PlusIcon}
|
||||||
subtitle={subtitle}
|
onClick={() => navigate("/warehouse/items/new")}
|
||||||
actions={
|
>
|
||||||
canManage ? (
|
Přidat položku
|
||||||
<Button
|
</Button>
|
||||||
startIcon={PlusIcon}
|
) : undefined
|
||||||
onClick={() => navigate("/warehouse/items/new")}
|
}
|
||||||
>
|
/>
|
||||||
Přidat položku
|
|
||||||
</Button>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<Box sx={{ flex: "1 1 280px" }}>
|
<Box sx={{ flex: "1 1 280px" }}>
|
||||||
@@ -243,6 +237,6 @@ export default function WarehouseItems() {
|
|||||||
onChange={setPage}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
@@ -303,7 +304,7 @@ export default function WarehouseLocations() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Umístění skladu"
|
title="Umístění skladu"
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
@@ -401,6 +402,6 @@ export default function WarehouseLocations() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={deleteMutation.isPending}
|
loading={deleteMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
LoadingState,
|
LoadingState,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
PageEnter,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
@@ -295,53 +295,38 @@ export default function WarehouseReceiptDetail() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title={r.receipt_number || "Nový doklad"}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={r.supplier?.name}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
<PageHeader
|
<Button
|
||||||
title={r.receipt_number || "Nový doklad"}
|
component={RouterLink}
|
||||||
subtitle={r.supplier?.name}
|
to="/warehouse/receipts"
|
||||||
actions={
|
variant="outlined"
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
color="inherit"
|
||||||
<Button
|
startIcon={BackIcon}
|
||||||
component={RouterLink}
|
>
|
||||||
to="/warehouse/receipts"
|
Zpět
|
||||||
variant="outlined"
|
</Button>
|
||||||
color="inherit"
|
<StatusChip
|
||||||
startIcon={BackIcon}
|
label={STATUS_LABEL[r.status] ?? r.status}
|
||||||
>
|
color={STATUS_COLOR[r.status] ?? "default"}
|
||||||
Zpět
|
/>
|
||||||
</Button>
|
{canOperate && r.status === "DRAFT" && (
|
||||||
<StatusChip
|
<>
|
||||||
label={STATUS_LABEL[r.status] ?? r.status}
|
<Button
|
||||||
color={STATUS_COLOR[r.status] ?? "default"}
|
variant="outlined"
|
||||||
/>
|
color="inherit"
|
||||||
{canOperate && r.status === "DRAFT" && (
|
onClick={() => navigate(`/warehouse/receipts/${r.id}/edit`)}
|
||||||
<>
|
>
|
||||||
<Button
|
Upravit
|
||||||
variant="outlined"
|
</Button>
|
||||||
color="inherit"
|
<Button onClick={handleConfirm} disabled={confirming}>
|
||||||
onClick={() => navigate(`/warehouse/receipts/${r.id}/edit`)}
|
{confirming ? "Potvrzování..." : "Potvrdit"}
|
||||||
>
|
</Button>
|
||||||
Upravit
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleConfirm} disabled={confirming}>
|
|
||||||
{confirming ? "Potvrzování..." : "Potvrdit"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
onClick={() => setCancelConfirm(true)}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{canOperate && r.status === "CONFIRMED" && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="error"
|
color="error"
|
||||||
@@ -349,157 +334,148 @@ export default function WarehouseReceiptDetail() {
|
|||||||
>
|
>
|
||||||
Zrušit
|
Zrušit
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</>
|
||||||
</Box>
|
)}
|
||||||
}
|
{canOperate && r.status === "CONFIRMED" && (
|
||||||
/>
|
<Button
|
||||||
</motion.div>
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
{/* Basic info */}
|
onClick={() => setCancelConfirm(true)}
|
||||||
<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,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Číslo dokladu
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
fontFamily: "'DM Mono', Menlo, monospace",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{r.receipt_number || "—"}
|
Zrušit
|
||||||
</Typography>
|
</Button>
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Dodavatel
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
fontFamily: "'DM Mono', Menlo, monospace",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{r.supplier?.name || "—"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Číslo dodacího listu
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
|
||||||
>
|
|
||||||
{r.delivery_note_number || "—"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Datum dodacího listu
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
|
||||||
>
|
|
||||||
{formatDate(r.delivery_note_date)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Přijal
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
fontFamily: "'DM Mono', Menlo, monospace",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{r.received_by_user
|
|
||||||
? `${r.received_by_user.first_name} ${r.received_by_user.last_name}`
|
|
||||||
: "—"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Vytvořeno
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
|
||||||
>
|
|
||||||
{formatDate(r.created_at)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
{r.notes && (
|
|
||||||
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Poznámky
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
|
|
||||||
{r.notes}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
}
|
||||||
</motion.div>
|
/>
|
||||||
|
|
||||||
|
{/* Basic info */}
|
||||||
|
<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,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Číslo dokladu
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "'DM Mono', Menlo, monospace",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{r.receipt_number || "—"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Dodavatel
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "'DM Mono', Menlo, monospace",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{r.supplier?.name || "—"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Číslo dodacího listu
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
||||||
|
>
|
||||||
|
{r.delivery_note_number || "—"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Datum dodacího listu
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
||||||
|
>
|
||||||
|
{formatDate(r.delivery_note_date)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Přijal
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "'DM Mono', Menlo, monospace",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{r.received_by_user
|
||||||
|
? `${r.received_by_user.first_name} ${r.received_by_user.last_name}`
|
||||||
|
: "—"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Vytvořeno
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
||||||
|
>
|
||||||
|
{formatDate(r.created_at)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{r.notes && (
|
||||||
|
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Poznámky
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
|
||||||
|
{r.notes}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Lines table */}
|
{/* Lines table */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Položky
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
</Typography>
|
||||||
>
|
<DataTable<WarehouseReceiptItem>
|
||||||
<Card sx={{ mb: 3 }}>
|
columns={itemColumns}
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
rows={items}
|
||||||
Položky
|
rowKey={(row) => row.id}
|
||||||
</Typography>
|
empty={<EmptyState title="Žádné řádky" />}
|
||||||
<DataTable<WarehouseReceiptItem>
|
/>
|
||||||
columns={itemColumns}
|
</Card>
|
||||||
rows={items}
|
|
||||||
rowKey={(row) => row.id}
|
|
||||||
empty={<EmptyState title="Žádné řádky" />}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Attachments */}
|
{/* Attachments */}
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Přílohy
|
||||||
transition={{ duration: 0.25, delay: 0.1 }}
|
</Typography>
|
||||||
>
|
<DataTable<WarehouseReceiptAttachment>
|
||||||
<Card sx={{ mb: 3 }}>
|
columns={attachmentColumns}
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
rows={attachments}
|
||||||
Přílohy
|
rowKey={(att) => att.id}
|
||||||
</Typography>
|
empty={<EmptyState title="Žádné přílohy" />}
|
||||||
<DataTable<WarehouseReceiptAttachment>
|
/>
|
||||||
columns={attachmentColumns}
|
</Card>
|
||||||
rows={attachments}
|
|
||||||
rowKey={(att) => att.id}
|
|
||||||
empty={<EmptyState title="Žádné přílohy" />}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Cancel confirmation dialog */}
|
{/* Cancel confirmation dialog */}
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -512,6 +488,6 @@ export default function WarehouseReceiptDetail() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={cancelling}
|
loading={cancelling}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useCallback, useId } from "react";
|
import { useState, useEffect, useRef, useCallback, useId } from "react";
|
||||||
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@@ -28,6 +27,7 @@ import {
|
|||||||
DateField,
|
DateField,
|
||||||
Field,
|
Field,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
PageEnter,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
|
|
||||||
interface ReceiptForm {
|
interface ReceiptForm {
|
||||||
@@ -410,294 +410,266 @@ export default function WarehouseReceiptForm() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<Box
|
||||||
initial={{ opacity: 0, y: 12 }}
|
sx={{
|
||||||
animate={{ opacity: 1, y: 0 }}
|
display: "flex",
|
||||||
transition={{ duration: 0.25 }}
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
||||||
sx={{
|
<IconButton
|
||||||
display: "flex",
|
component={RouterLink}
|
||||||
alignItems: "flex-start",
|
to="/warehouse/receipts"
|
||||||
justifyContent: "space-between",
|
title="Zpět na seznam"
|
||||||
flexWrap: "wrap",
|
aria-label="Zpět na seznam"
|
||||||
gap: 2,
|
|
||||||
mb: 3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
|
||||||
<IconButton
|
|
||||||
component={RouterLink}
|
|
||||||
to="/warehouse/receipts"
|
|
||||||
title="Zpět na seznam"
|
|
||||||
aria-label="Zpět na seznam"
|
|
||||||
>
|
|
||||||
{BackIcon}
|
|
||||||
</IconButton>
|
|
||||||
<Typography variant="h4">
|
|
||||||
{isEdit ? "Upravit příjmový doklad" : "Nový příjmový doklad"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="inherit"
|
|
||||||
onClick={handleSaveDraft}
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
{saving ? "Ukládání..." : "Uložit jako návrh"}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSaveAndConfirm} disabled={saving}>
|
|
||||||
{saving ? "Ukládání..." : "Uložit a potvrdit"}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Header fields */}
|
|
||||||
<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>
|
|
||||||
<Field label="Dodavatel">
|
|
||||||
<Select
|
|
||||||
value={form.supplier_id === null ? "" : String(form.supplier_id)}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
supplier_id: val ? Number(val) : null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
options={supplierOptions}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
|
||||||
gap: 2,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Field label="Číslo dodacího listu">
|
{BackIcon}
|
||||||
<TextField
|
</IconButton>
|
||||||
value={form.delivery_note_number}
|
<Typography variant="h4">
|
||||||
onChange={(e) =>
|
{isEdit ? "Upravit příjmový doklad" : "Nový příjmový doklad"}
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
delivery_note_number: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
placeholder="Např. DL-2024-001"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Datum dodacího listu">
|
|
||||||
<DateField
|
|
||||||
value={form.delivery_note_date}
|
|
||||||
onChange={(val) =>
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
delivery_note_date: val,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
<Field label="Poznámky">
|
|
||||||
<TextField
|
|
||||||
multiline
|
|
||||||
minRows={3}
|
|
||||||
value={form.notes}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((prev) => ({ ...prev, notes: e.target.value }))
|
|
||||||
}
|
|
||||||
placeholder="Volitelné poznámky"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Lines */}
|
|
||||||
<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 }}>
|
|
||||||
Položky
|
|
||||||
</Typography>
|
</Typography>
|
||||||
{errors.items && (
|
</Box>
|
||||||
<Typography
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
variant="caption"
|
|
||||||
color="error"
|
|
||||||
sx={{ display: "block", mb: 1.5 }}
|
|
||||||
>
|
|
||||||
{errors.items}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<Box
|
|
||||||
key={item.key}
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: {
|
|
||||||
xs: "1fr",
|
|
||||||
md: "minmax(180px, 2fr) 110px 110px minmax(140px, 1.2fr) minmax(140px, 1.5fr) auto",
|
|
||||||
},
|
|
||||||
gap: 1,
|
|
||||||
alignItems: "start",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ItemPicker
|
|
||||||
value={item.item_id}
|
|
||||||
itemName={item.item_name}
|
|
||||||
onChange={(id) => updateItem(index, "item_id", id)}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
value={item.quantity || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateItem(index, "quantity", parseDecimal(e.target.value))
|
|
||||||
}
|
|
||||||
inputProps={{
|
|
||||||
min: 0,
|
|
||||||
step: 0.001,
|
|
||||||
inputMode: "decimal",
|
|
||||||
pattern: "[0-9]+([\\.,][0-9]+)?",
|
|
||||||
"aria-label": `Množství, řádek ${index + 1}`,
|
|
||||||
}}
|
|
||||||
placeholder="Množství"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
value={item.unit_price || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateItem(
|
|
||||||
index,
|
|
||||||
"unit_price",
|
|
||||||
parseDecimal(e.target.value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
inputProps={{
|
|
||||||
min: 0,
|
|
||||||
step: 0.01,
|
|
||||||
inputMode: "decimal",
|
|
||||||
pattern: "[0-9]+([\\.,][0-9]+)?",
|
|
||||||
"aria-label": `Cena za kus, řádek ${index + 1}`,
|
|
||||||
}}
|
|
||||||
placeholder="Cena/ks"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={
|
|
||||||
item.location_id === null ? "" : String(item.location_id)
|
|
||||||
}
|
|
||||||
onChange={(val) =>
|
|
||||||
updateItem(index, "location_id", val ? Number(val) : null)
|
|
||||||
}
|
|
||||||
options={locationOptions}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
value={item.notes ?? ""}
|
|
||||||
onChange={(e) => updateItem(index, "notes", e.target.value)}
|
|
||||||
placeholder="Poznámka"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
color="error"
|
|
||||||
onClick={() => removeItem(index)}
|
|
||||||
title="Odebrat řádek"
|
|
||||||
aria-label="Odebrat řádek"
|
|
||||||
sx={{ justifySelf: { xs: "start", md: "center" } }}
|
|
||||||
>
|
|
||||||
{RemoveIcon}
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
startIcon={PlusIcon}
|
onClick={handleSaveDraft}
|
||||||
onClick={addItem}
|
disabled={saving}
|
||||||
sx={{ mt: 2 }}
|
|
||||||
>
|
>
|
||||||
Přidat položku
|
{saving ? "Ukládání..." : "Uložit jako návrh"}
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
<Button onClick={handleSaveAndConfirm} disabled={saving}>
|
||||||
</motion.div>
|
{saving ? "Ukládání..." : "Uložit a potvrdit"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Header fields */}
|
||||||
|
<Card sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
|
Základní údaje
|
||||||
|
</Typography>
|
||||||
|
<Field label="Dodavatel">
|
||||||
|
<Select
|
||||||
|
value={form.supplier_id === null ? "" : String(form.supplier_id)}
|
||||||
|
onChange={(val) =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
supplier_id: val ? Number(val) : null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
options={supplierOptions}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Field label="Číslo dodacího listu">
|
||||||
|
<TextField
|
||||||
|
value={form.delivery_note_number}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
delivery_note_number: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Např. DL-2024-001"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Datum dodacího listu">
|
||||||
|
<DateField
|
||||||
|
value={form.delivery_note_date}
|
||||||
|
onChange={(val) =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
delivery_note_date: val,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Box>
|
||||||
|
<Field label="Poznámky">
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
minRows={3}
|
||||||
|
value={form.notes}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((prev) => ({ ...prev, notes: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="Volitelné poznámky"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Lines */}
|
||||||
|
<Card sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
|
Položky
|
||||||
|
</Typography>
|
||||||
|
{errors.items && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="error"
|
||||||
|
sx={{ display: "block", mb: 1.5 }}
|
||||||
|
>
|
||||||
|
{errors.items}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Box
|
||||||
|
key={item.key}
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: "1fr",
|
||||||
|
md: "minmax(180px, 2fr) 110px 110px minmax(140px, 1.2fr) minmax(140px, 1.5fr) auto",
|
||||||
|
},
|
||||||
|
gap: 1,
|
||||||
|
alignItems: "start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemPicker
|
||||||
|
value={item.item_id}
|
||||||
|
itemName={item.item_name}
|
||||||
|
onChange={(id) => updateItem(index, "item_id", id)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
value={item.quantity || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateItem(index, "quantity", parseDecimal(e.target.value))
|
||||||
|
}
|
||||||
|
inputProps={{
|
||||||
|
min: 0,
|
||||||
|
step: 0.001,
|
||||||
|
inputMode: "decimal",
|
||||||
|
pattern: "[0-9]+([\\.,][0-9]+)?",
|
||||||
|
"aria-label": `Množství, řádek ${index + 1}`,
|
||||||
|
}}
|
||||||
|
placeholder="Množství"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
value={item.unit_price || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateItem(index, "unit_price", parseDecimal(e.target.value))
|
||||||
|
}
|
||||||
|
inputProps={{
|
||||||
|
min: 0,
|
||||||
|
step: 0.01,
|
||||||
|
inputMode: "decimal",
|
||||||
|
pattern: "[0-9]+([\\.,][0-9]+)?",
|
||||||
|
"aria-label": `Cena za kus, řádek ${index + 1}`,
|
||||||
|
}}
|
||||||
|
placeholder="Cena/ks"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
item.location_id === null ? "" : String(item.location_id)
|
||||||
|
}
|
||||||
|
onChange={(val) =>
|
||||||
|
updateItem(index, "location_id", val ? Number(val) : null)
|
||||||
|
}
|
||||||
|
options={locationOptions}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
value={item.notes ?? ""}
|
||||||
|
onChange={(e) => updateItem(index, "notes", e.target.value)}
|
||||||
|
placeholder="Poznámka"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
color="error"
|
||||||
|
onClick={() => removeItem(index)}
|
||||||
|
title="Odebrat řádek"
|
||||||
|
aria-label="Odebrat řádek"
|
||||||
|
sx={{ justifySelf: { xs: "start", md: "center" } }}
|
||||||
|
>
|
||||||
|
{RemoveIcon}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
startIcon={PlusIcon}
|
||||||
|
onClick={addItem}
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
>
|
||||||
|
Přidat položku
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* File upload — only in edit mode for existing receipts */}
|
{/* File upload — only in edit mode for existing receipts */}
|
||||||
{isEdit && (
|
{isEdit && (
|
||||||
<motion.div
|
<Card sx={{ mb: 3 }}>
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
Přílohy
|
||||||
transition={{ duration: 0.25, delay: 0.1 }}
|
</Typography>
|
||||||
>
|
<Box
|
||||||
<Card sx={{ mb: 3 }}>
|
onDrop={handleDrop}
|
||||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
onDragOver={handleDragOver}
|
||||||
Přílohy
|
onClick={() => {
|
||||||
</Typography>
|
const input = document.createElement("input");
|
||||||
<Box
|
input.type = "file";
|
||||||
onDrop={handleDrop}
|
input.multiple = true;
|
||||||
onDragOver={handleDragOver}
|
input.onchange = () => {
|
||||||
onClick={() => {
|
if (input.files && input.files.length > 0) {
|
||||||
const input = document.createElement("input");
|
handleFileUpload(input.files);
|
||||||
input.type = "file";
|
}
|
||||||
input.multiple = true;
|
};
|
||||||
input.onchange = () => {
|
input.click();
|
||||||
if (input.files && input.files.length > 0) {
|
}}
|
||||||
handleFileUpload(input.files);
|
sx={{
|
||||||
}
|
display: "flex",
|
||||||
};
|
flexDirection: "column",
|
||||||
input.click();
|
alignItems: "center",
|
||||||
}}
|
justifyContent: "center",
|
||||||
sx={{
|
gap: 1,
|
||||||
display: "flex",
|
p: 4,
|
||||||
flexDirection: "column",
|
border: "2px dashed",
|
||||||
alignItems: "center",
|
borderColor: "divider",
|
||||||
justifyContent: "center",
|
borderRadius: 1,
|
||||||
gap: 1,
|
color: "text.secondary",
|
||||||
p: 4,
|
cursor: "pointer",
|
||||||
border: "2px dashed",
|
opacity: uploadingFiles ? 0.6 : 1,
|
||||||
borderColor: "divider",
|
transition: "border-color .2s, background-color .2s",
|
||||||
borderRadius: 1,
|
"&:hover": {
|
||||||
color: "text.secondary",
|
borderColor: "primary.main",
|
||||||
cursor: "pointer",
|
bgcolor: "action.hover",
|
||||||
opacity: uploadingFiles ? 0.6 : 1,
|
},
|
||||||
transition: "border-color .2s, background-color .2s",
|
}}
|
||||||
"&:hover": {
|
>
|
||||||
borderColor: "primary.main",
|
{uploadingFiles ? (
|
||||||
bgcolor: "action.hover",
|
<>
|
||||||
},
|
<CircularProgress size={28} />
|
||||||
}}
|
<Typography variant="body2">Nahrávání...</Typography>
|
||||||
>
|
</>
|
||||||
{uploadingFiles ? (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CircularProgress size={28} />
|
{UploadIcon}
|
||||||
<Typography variant="body2">Nahrávání...</Typography>
|
<Typography variant="body2">
|
||||||
</>
|
Přetáhněte soubory sem nebo klikněte pro výběr
|
||||||
) : (
|
</Typography>
|
||||||
<>
|
<Typography variant="caption" color="text.secondary">
|
||||||
{UploadIcon}
|
Dodací listy, faktury a další dokumenty
|
||||||
<Typography variant="body2">
|
</Typography>
|
||||||
Přetáhněte soubory sem nebo klikněte pro výběr
|
</>
|
||||||
</Typography>
|
)}
|
||||||
<Typography variant="caption" color="text.secondary">
|
</Box>
|
||||||
Dodací listy, faktury a další dokumenty
|
</Card>
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import useDebounce from "../hooks/useDebounce";
|
import useDebounce from "../hooks/useDebounce";
|
||||||
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
@@ -24,6 +23,7 @@ import {
|
|||||||
DateField,
|
DateField,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -165,27 +165,21 @@ export default function WarehouseReceipts() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Příjmové doklady"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={subtitle}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
canOperate ? (
|
||||||
<PageHeader
|
<Button
|
||||||
title="Příjmové doklady"
|
startIcon={PlusIcon}
|
||||||
subtitle={subtitle}
|
onClick={() => navigate("/warehouse/receipts/new")}
|
||||||
actions={
|
>
|
||||||
canOperate ? (
|
Nový doklad
|
||||||
<Button
|
</Button>
|
||||||
startIcon={PlusIcon}
|
) : undefined
|
||||||
onClick={() => navigate("/warehouse/receipts/new")}
|
}
|
||||||
>
|
/>
|
||||||
Nový doklad
|
|
||||||
</Button>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<Box sx={{ flex: "1 1 220px" }}>
|
<Box sx={{ flex: "1 1 220px" }}>
|
||||||
@@ -291,6 +285,6 @@ export default function WarehouseReceipts() {
|
|||||||
onChange={setPage}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState } from "react";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { projectListOptions } from "../lib/queries/projects";
|
import { projectListOptions } from "../lib/queries/projects";
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
DateField,
|
DateField,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -89,26 +89,14 @@ export default function WarehouseReports() {
|
|||||||
if (!hasPermission("warehouse.view")) return <Forbidden />;
|
if (!hasPermission("warehouse.view")) return <Forbidden />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader title="Skladové reporty" />
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25 }}
|
|
||||||
>
|
|
||||||
<PageHeader title="Skladové reporty" />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<Tabs
|
||||||
initial={{ opacity: 0, y: 12 }}
|
value={activeTab}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
onChange={(v) => setActiveTab(v as TabId)}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
tabs={TABS}
|
||||||
>
|
/>
|
||||||
<Tabs
|
|
||||||
value={activeTab}
|
|
||||||
onChange={(v) => setActiveTab(v as TabId)}
|
|
||||||
tabs={TABS}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<TabPanel value="stock-status" current={activeTab}>
|
<TabPanel value="stock-status" current={activeTab}>
|
||||||
<StockStatusTab categoryId={categoryId} setCategoryId={setCategoryId} />
|
<StockStatusTab categoryId={categoryId} setCategoryId={setCategoryId} />
|
||||||
@@ -139,7 +127,7 @@ export default function WarehouseReports() {
|
|||||||
<TabPanel value="below-minimum" current={activeTab}>
|
<TabPanel value="below-minimum" current={activeTab}>
|
||||||
<BelowMinimumTab />
|
<BelowMinimumTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
StatusChip,
|
StatusChip,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
PageEnter,
|
||||||
FilterBar,
|
FilterBar,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -325,27 +325,21 @@ export default function WarehouseReservations() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageEnter>
|
||||||
<motion.div
|
<PageHeader
|
||||||
initial={{ opacity: 0, y: 12 }}
|
title="Rezervace"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
subtitle={subtitle}
|
||||||
transition={{ duration: 0.25 }}
|
actions={
|
||||||
>
|
canOperate ? (
|
||||||
<PageHeader
|
<Button
|
||||||
title="Rezervace"
|
startIcon={PlusIcon}
|
||||||
subtitle={subtitle}
|
onClick={() => setShowCreateModal(true)}
|
||||||
actions={
|
>
|
||||||
canOperate ? (
|
Nová rezervace
|
||||||
<Button
|
</Button>
|
||||||
startIcon={PlusIcon}
|
) : undefined
|
||||||
onClick={() => setShowCreateModal(true)}
|
}
|
||||||
>
|
/>
|
||||||
Nová rezervace
|
|
||||||
</Button>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<Box sx={{ flex: "0 0 160px" }}>
|
<Box sx={{ flex: "0 0 160px" }}>
|
||||||
@@ -490,6 +484,6 @@ export default function WarehouseReservations() {
|
|||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={cancelling}
|
loading={cancelling}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user