Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
996835dae5 | ||
|
|
c73b980ce7 | ||
|
|
dafa6d6aab | ||
|
|
80803cc868 | ||
|
|
55fa4045ad | ||
|
|
7f156cc721 | ||
|
|
e0d2fccf50 | ||
|
|
a1c251650b |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.9.3",
|
"version": "1.9.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.9.3",
|
"version": "1.9.5",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.9.3",
|
"version": "1.9.5",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -434,3 +434,22 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Mobile (responsive)
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
/* The Leaflet map at a fixed 400px dominates a phone viewport — let it scale
|
||||||
|
with the viewport height (with sensible min/max) on small screens. */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.attendance-location-map {
|
||||||
|
height: clamp(220px, 45vh, 400px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The clock card's 2rem padding crushes content at ~360px — tighten it. */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.attendance-clock-card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -255,12 +255,23 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-modal-heading {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-modal-title {
|
.admin-modal-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-modal-subtitle {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.admin-modal-close {
|
.admin-modal-close {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -834,7 +845,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.admin-kpi-grid {
|
.admin-kpi-grid,
|
||||||
|
.admin-kpi-4,
|
||||||
|
.admin-kpi-3 {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { motion, AnimatePresence } from "framer-motion";
|
import FormModal from "./FormModal";
|
||||||
import AdminDatePicker from "./AdminDatePicker";
|
import AdminDatePicker from "./AdminDatePicker";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
|
||||||
|
|
||||||
interface BulkAttendanceForm {
|
interface BulkAttendanceForm {
|
||||||
month: string;
|
month: string;
|
||||||
@@ -39,52 +38,29 @@ export default function BulkAttendanceModal({
|
|||||||
toggleUser,
|
toggleUser,
|
||||||
toggleAllUsers,
|
toggleAllUsers,
|
||||||
}: BulkAttendanceModalProps) {
|
}: BulkAttendanceModalProps) {
|
||||||
useModalLock(isOpen);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{isOpen && (
|
isOpen={isOpen}
|
||||||
<motion.div
|
onClose={onClose}
|
||||||
className="admin-modal-overlay"
|
title="Vyplnit docházku za měsíc"
|
||||||
initial={{ opacity: 0 }}
|
size="lg"
|
||||||
animate={{ opacity: 1 }}
|
loading={submitting}
|
||||||
exit={{ opacity: 0 }}
|
onSubmit={onSubmit}
|
||||||
transition={{ duration: 0.2 }}
|
submitLabel="Vyplnit měsíc"
|
||||||
|
submitDisabled={form.user_ids.length === 0}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => !submitting && onClose()}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal admin-modal-lg"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="bulk-attendance-modal-title"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2
|
|
||||||
id="bulk-attendance-modal-title"
|
|
||||||
className="admin-modal-title"
|
|
||||||
>
|
|
||||||
Vyplnit docházku za měsíc
|
|
||||||
</h2>
|
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
color: "var(--text-secondary)",
|
color: "var(--text-secondary)",
|
||||||
marginTop: "0.25rem",
|
marginTop: "0.25rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
fontSize: "0.875rem",
|
fontSize: "0.875rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Vytvoří záznamy pro všechny pracovní dny. Svátky se automaticky
|
Vytvoří záznamy pro všechny pracovní dny. Svátky se automaticky označí.
|
||||||
označí. Existující záznamy se přeskočí.
|
Existující záznamy se přeskočí.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label">Měsíc</label>
|
<label className="admin-form-label">Měsíc</label>
|
||||||
@@ -152,9 +128,7 @@ export default function BulkAttendanceModal({
|
|||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="time"
|
mode="time"
|
||||||
value={form.arrival_time}
|
value={form.arrival_time}
|
||||||
onChange={(val) =>
|
onChange={(val) => setForm({ ...form, arrival_time: val })}
|
||||||
setForm({ ...form, arrival_time: val })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
@@ -162,9 +136,7 @@ export default function BulkAttendanceModal({
|
|||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="time"
|
mode="time"
|
||||||
value={form.departure_time}
|
value={form.departure_time}
|
||||||
onChange={(val) =>
|
onChange={(val) => setForm({ ...form, departure_time: val })}
|
||||||
setForm({ ...form, departure_time: val })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,9 +147,7 @@ export default function BulkAttendanceModal({
|
|||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="time"
|
mode="time"
|
||||||
value={form.break_start_time}
|
value={form.break_start_time}
|
||||||
onChange={(val) =>
|
onChange={(val) => setForm({ ...form, break_start_time: val })}
|
||||||
setForm({ ...form, break_start_time: val })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
@@ -185,36 +155,11 @@ export default function BulkAttendanceModal({
|
|||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="time"
|
mode="time"
|
||||||
value={form.break_end_time}
|
value={form.break_end_time}
|
||||||
onChange={(val) =>
|
onChange={(val) => setForm({ ...form, break_end_time: val })}
|
||||||
setForm({ ...form, break_end_time: val })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
|
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={submitting}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onSubmit}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={submitting || form.user_ids.length === 0}
|
|
||||||
>
|
|
||||||
{submitting ? "Vytvářím záznamy..." : "Vyplnit měsíc"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, type ReactNode } from "react";
|
import { useEffect, type ReactNode } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import useReducedMotion from "../hooks/useReducedMotion";
|
import useReducedMotion from "../hooks/useReducedMotion";
|
||||||
|
import useModalLock from "../hooks/useModalLock";
|
||||||
|
|
||||||
interface ConfirmModalProps {
|
interface ConfirmModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -93,6 +94,8 @@ export default function ConfirmModal({
|
|||||||
loading,
|
loading,
|
||||||
}: ConfirmModalProps) {
|
}: ConfirmModalProps) {
|
||||||
const reducedMotion = useReducedMotion();
|
const reducedMotion = useReducedMotion();
|
||||||
|
// Lock body scroll while open (ref-counted, nesting-safe) — matches FormModal.
|
||||||
|
useModalLock(isOpen);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
|
|
||||||
@@ -119,7 +122,10 @@ export default function ConfirmModal({
|
|||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: reducedMotion ? 0 : 0.18 }}
|
transition={{ duration: reducedMotion ? 0 : 0.18 }}
|
||||||
>
|
>
|
||||||
<div className="admin-modal-backdrop" onClick={onClose} />
|
<div
|
||||||
|
className="admin-modal-backdrop"
|
||||||
|
onClick={() => !loading && onClose()}
|
||||||
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-modal admin-confirm-modal"
|
className="admin-modal admin-confirm-modal"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
|
|||||||
@@ -15,10 +15,15 @@ export interface FormModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit?: () => void; // called when user clicks primary button (only if provided)
|
onSubmit?: () => void; // called when user clicks primary button (only if provided)
|
||||||
title: string;
|
title: string;
|
||||||
|
/** Optional muted line shown under the title in the header. */
|
||||||
|
subtitle?: ReactNode;
|
||||||
children: ReactNode; // modal body (form fields)
|
children: ReactNode; // modal body (form fields)
|
||||||
submitLabel?: string; // primary button text
|
submitLabel?: string; // primary button text
|
||||||
cancelLabel?: string; // cancel button text
|
cancelLabel?: string; // cancel button text
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
/** Disable the primary (submit) button — e.g. until the form is valid.
|
||||||
|
* Keeps the button visibly greyed out instead of clickable-but-inert. */
|
||||||
|
submitDisabled?: boolean;
|
||||||
hideFooter?: boolean; // for "view-only" modals
|
hideFooter?: boolean; // for "view-only" modals
|
||||||
size?: "sm" | "md" | "lg"; // optional
|
size?: "sm" | "md" | "lg"; // optional
|
||||||
closeOnBackdrop?: boolean; // default true
|
closeOnBackdrop?: boolean; // default true
|
||||||
@@ -35,10 +40,12 @@ export default function FormModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
title,
|
title,
|
||||||
|
subtitle,
|
||||||
children,
|
children,
|
||||||
submitLabel = "Uložit",
|
submitLabel = "Uložit",
|
||||||
cancelLabel = "Zrušit",
|
cancelLabel = "Zrušit",
|
||||||
loading = false,
|
loading = false,
|
||||||
|
submitDisabled = false,
|
||||||
hideFooter = false,
|
hideFooter = false,
|
||||||
size = "md",
|
size = "md",
|
||||||
closeOnBackdrop = true,
|
closeOnBackdrop = true,
|
||||||
@@ -107,9 +114,12 @@ export default function FormModal({
|
|||||||
transition={{ duration, ease }}
|
transition={{ duration, ease }}
|
||||||
>
|
>
|
||||||
<div className="admin-modal-header">
|
<div className="admin-modal-header">
|
||||||
|
<div className="admin-modal-heading">
|
||||||
<h2 id={titleId} className="admin-modal-title">
|
<h2 id={titleId} className="admin-modal-title">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
|
{subtitle && <p className="admin-modal-subtitle">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
{!hideCloseButton && (
|
{!hideCloseButton && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -175,7 +185,7 @@ export default function FormModal({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handlePrimaryClick}
|
onClick={handlePrimaryClick}
|
||||||
className="admin-btn admin-btn-primary"
|
className="admin-btn admin-btn-primary"
|
||||||
disabled={loading}
|
disabled={loading || submitDisabled}
|
||||||
>
|
>
|
||||||
{loading ? "Zpracování..." : submitLabel}
|
{loading ? "Zpracování..." : submitLabel}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import FormModal from "./FormModal";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
|
|
||||||
interface ConfirmationItem {
|
interface ConfirmationItem {
|
||||||
@@ -103,38 +103,14 @@ export default function OrderConfirmationModal({
|
|||||||
}, [defaultVatRate]);
|
}, [defaultVatRate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{isOpen && (
|
isOpen={isOpen}
|
||||||
<motion.div
|
onClose={onClose}
|
||||||
className="admin-modal-overlay"
|
title={`Potvrzení objednávky ${orderNumber}`}
|
||||||
initial={{ opacity: 0 }}
|
size={step === "edit" ? "lg" : "md"}
|
||||||
animate={{ opacity: 1 }}
|
loading={loading}
|
||||||
exit={{ opacity: 0 }}
|
hideFooter
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div className="admin-modal-backdrop" onClick={onClose} />
|
|
||||||
<motion.div
|
|
||||||
className={
|
|
||||||
step === "edit" ? "admin-modal admin-modal-lg" : "admin-modal"
|
|
||||||
}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="order-confirmation-modal-title"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2
|
|
||||||
id="order-confirmation-modal-title"
|
|
||||||
className="admin-modal-title"
|
|
||||||
>
|
|
||||||
Potvrzení objednávky {orderNumber}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
{step === "choose" ? (
|
{step === "choose" ? (
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
@@ -195,10 +171,7 @@ export default function OrderConfirmationModal({
|
|||||||
|
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label">Obsah potvrzení</label>
|
<label className="admin-form-label">Obsah potvrzení</label>
|
||||||
<p
|
<p className="text-secondary" style={{ marginBottom: "0.75rem" }}>
|
||||||
className="text-secondary"
|
|
||||||
style={{ marginBottom: "0.75rem" }}
|
|
||||||
>
|
|
||||||
Jak chcete připravit potvrzení objednávky?
|
Jak chcete připravit potvrzení objednávky?
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
@@ -227,6 +200,17 @@ export default function OrderConfirmationModal({
|
|||||||
Upravit položky
|
Upravit položky
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Zrušit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
@@ -261,11 +245,7 @@ export default function OrderConfirmationModal({
|
|||||||
type="number"
|
type="number"
|
||||||
value={item.quantity}
|
value={item.quantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateItem(
|
updateItem(i, "quantity", Number(e.target.value) || 0)
|
||||||
i,
|
|
||||||
"quantity",
|
|
||||||
Number(e.target.value) || 0,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
style={{ width: "80px" }}
|
style={{ width: "80px" }}
|
||||||
@@ -276,9 +256,7 @@ export default function OrderConfirmationModal({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={item.unit}
|
value={item.unit}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateItem(i, "unit", e.target.value)}
|
||||||
updateItem(i, "unit", e.target.value)
|
|
||||||
}
|
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
style={{ width: "60px" }}
|
style={{ width: "60px" }}
|
||||||
/>
|
/>
|
||||||
@@ -304,11 +282,7 @@ export default function OrderConfirmationModal({
|
|||||||
type="number"
|
type="number"
|
||||||
value={item.vat_rate}
|
value={item.vat_rate}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateItem(
|
updateItem(i, "vat_rate", Number(e.target.value) || 0)
|
||||||
i,
|
|
||||||
"vat_rate",
|
|
||||||
Number(e.target.value) || 0,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
style={{ width: "70px" }}
|
style={{ width: "70px" }}
|
||||||
@@ -345,13 +319,8 @@ export default function OrderConfirmationModal({
|
|||||||
>
|
>
|
||||||
+ Přidat položku
|
+ Přidat položku
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-modal-footer">
|
<div className="admin-modal-footer">
|
||||||
{step === "edit" && (
|
|
||||||
<>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setStep("choose")}
|
onClick={() => setStep("choose")}
|
||||||
@@ -375,22 +344,9 @@ export default function OrderConfirmationModal({
|
|||||||
"Vygenerovat PDF"
|
"Vygenerovat PDF"
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{step === "choose" && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.div>
|
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</FormModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import AdminDatePicker from "./AdminDatePicker";
|
import AdminDatePicker from "./AdminDatePicker";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import FormModal from "./FormModal";
|
||||||
import {
|
import {
|
||||||
calcFormWorkMinutes,
|
calcFormWorkMinutes,
|
||||||
calcProjectMinutesTotal,
|
calcProjectMinutesTotal,
|
||||||
@@ -216,7 +215,6 @@ export default function ShiftFormModal({
|
|||||||
onShiftDateChange,
|
onShiftDateChange,
|
||||||
editingRecord,
|
editingRecord,
|
||||||
}: ShiftFormModalProps) {
|
}: ShiftFormModalProps) {
|
||||||
useModalLock(isOpen);
|
|
||||||
const isCreate = mode === "create";
|
const isCreate = mode === "create";
|
||||||
const isWorkType = form.leave_type === "work";
|
const isWorkType = form.leave_type === "work";
|
||||||
|
|
||||||
@@ -247,51 +245,25 @@ export default function ShiftFormModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{isOpen && (
|
isOpen={isOpen}
|
||||||
<motion.div
|
onClose={onClose}
|
||||||
className="admin-modal-overlay"
|
title={isCreate ? "Přidat záznam docházky" : "Upravit docházku"}
|
||||||
initial={{ opacity: 0 }}
|
subtitle={
|
||||||
animate={{ opacity: 1 }}
|
!isCreate && editingRecord
|
||||||
exit={{ opacity: 0 }}
|
? `${editingRecord.user_name} — ${formatDate(editingRecord.shift_date)}`
|
||||||
transition={{ duration: 0.2 }}
|
: undefined
|
||||||
|
}
|
||||||
|
size="lg"
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
submitLabel="Uložit"
|
||||||
|
cancelLabel="Zrušit"
|
||||||
>
|
>
|
||||||
<div className="admin-modal-backdrop" onClick={onClose} />
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal admin-modal-lg"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="shift-form-modal-title"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 id="shift-form-modal-title" className="admin-modal-title">
|
|
||||||
{isCreate ? "Přidat záznam docházky" : "Upravit docházku"}
|
|
||||||
</h2>
|
|
||||||
{!isCreate && editingRecord && (
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
color: "var(--text-secondary)",
|
|
||||||
marginTop: "0.25rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{editingRecord.user_name} —{" "}
|
|
||||||
{formatDate(editingRecord.shift_date)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
{isCreate ? (
|
{isCreate ? (
|
||||||
<div className="admin-form-row">
|
<div className="admin-form-row">
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label required">
|
<label className="admin-form-label required">Zaměstnanec</label>
|
||||||
Zaměstnanec
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={form.user_id}
|
value={form.user_id}
|
||||||
onChange={(e) => updateField("user_id", e.target.value)}
|
onChange={(e) => updateField("user_id", e.target.value)}
|
||||||
@@ -306,9 +278,7 @@ export default function ShiftFormModal({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label required">
|
<label className="admin-form-label required">Datum směny</label>
|
||||||
Datum směny
|
|
||||||
</label>
|
|
||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="date"
|
mode="date"
|
||||||
value={form.shift_date}
|
value={form.shift_date}
|
||||||
@@ -357,9 +327,7 @@ export default function ShiftFormModal({
|
|||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
/>
|
/>
|
||||||
{isCreate && (
|
{isCreate && (
|
||||||
<small className="admin-form-hint">
|
<small className="admin-form-hint">8 hodin = celý den</small>
|
||||||
8 hodin = celý den
|
|
||||||
</small>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -368,9 +336,7 @@ export default function ShiftFormModal({
|
|||||||
<>
|
<>
|
||||||
<div className="admin-form-row">
|
<div className="admin-form-row">
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label">
|
<label className="admin-form-label">Příchod - datum</label>
|
||||||
Příchod - datum
|
|
||||||
</label>
|
|
||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="date"
|
mode="date"
|
||||||
value={form.arrival_date}
|
value={form.arrival_date}
|
||||||
@@ -378,9 +344,7 @@ export default function ShiftFormModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label">
|
<label className="admin-form-label">Příchod - čas</label>
|
||||||
Příchod - čas
|
|
||||||
</label>
|
|
||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="time"
|
mode="time"
|
||||||
value={form.arrival_time}
|
value={form.arrival_time}
|
||||||
@@ -397,30 +361,22 @@ export default function ShiftFormModal({
|
|||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="date"
|
mode="date"
|
||||||
value={form.break_start_date}
|
value={form.break_start_date}
|
||||||
onChange={(val) =>
|
onChange={(val) => updateField("break_start_date", val)}
|
||||||
updateField("break_start_date", val)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label">
|
<label className="admin-form-label">Začátek pauzy - čas</label>
|
||||||
Začátek pauzy - čas
|
|
||||||
</label>
|
|
||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="time"
|
mode="time"
|
||||||
value={form.break_start_time}
|
value={form.break_start_time}
|
||||||
onChange={(val) =>
|
onChange={(val) => updateField("break_start_time", val)}
|
||||||
updateField("break_start_time", val)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="admin-form-row">
|
<div className="admin-form-row">
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label">
|
<label className="admin-form-label">Konec pauzy - datum</label>
|
||||||
Konec pauzy - datum
|
|
||||||
</label>
|
|
||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="date"
|
mode="date"
|
||||||
value={form.break_end_date}
|
value={form.break_end_date}
|
||||||
@@ -428,9 +384,7 @@ export default function ShiftFormModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label">
|
<label className="admin-form-label">Konec pauzy - čas</label>
|
||||||
Konec pauzy - čas
|
|
||||||
</label>
|
|
||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="time"
|
mode="time"
|
||||||
value={form.break_end_time}
|
value={form.break_end_time}
|
||||||
@@ -441,9 +395,7 @@ export default function ShiftFormModal({
|
|||||||
|
|
||||||
<div className="admin-form-row">
|
<div className="admin-form-row">
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label">
|
<label className="admin-form-label">Odchod - datum</label>
|
||||||
Odchod - datum
|
|
||||||
</label>
|
|
||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="date"
|
mode="date"
|
||||||
value={form.departure_date}
|
value={form.departure_date}
|
||||||
@@ -496,27 +448,6 @@ export default function ShiftFormModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
|
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onSubmit}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
>
|
|
||||||
Uložit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useAuth } from "../../context/AuthContext";
|
import { useAuth } from "../../context/AuthContext";
|
||||||
import { useAlert } from "../../context/AlertContext";
|
import { useAlert } from "../../context/AlertContext";
|
||||||
import useModalLock from "../../hooks/useModalLock";
|
|
||||||
import apiFetch from "../../utils/api";
|
import apiFetch from "../../utils/api";
|
||||||
|
import FormModal from "../FormModal";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -71,8 +71,6 @@ export default function DashProfile({
|
|||||||
last_name: "",
|
last_name: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
useModalLock(showModal);
|
|
||||||
|
|
||||||
const openEditModal = () => {
|
const openEditModal = () => {
|
||||||
const nameParts = (user?.fullName || "").split(" ");
|
const nameParts = (user?.fullName || "").split(" ");
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -261,30 +259,14 @@ export default function DashProfile({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Edit Profile Modal */}
|
{/* Edit Profile Modal */}
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{showModal && (
|
isOpen={showModal}
|
||||||
<motion.div
|
onClose={() => setShowModal(false)}
|
||||||
className="admin-modal-overlay"
|
title="Upravit profil"
|
||||||
initial={{ opacity: 0 }}
|
size="md"
|
||||||
animate={{ opacity: 1 }}
|
onSubmit={handleSubmit}
|
||||||
exit={{ opacity: 0 }}
|
submitLabel="Uložit změny"
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => setShowModal(false)}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">Upravit profil</h2>
|
|
||||||
</div>
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<div className="admin-form-row">
|
<div className="admin-form-row">
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
@@ -319,9 +301,7 @@ export default function DashProfile({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label">
|
<label className="admin-form-label">Uživatelské jméno</label>
|
||||||
Uživatelské jméno
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.username}
|
value={formData.username}
|
||||||
@@ -360,9 +340,7 @@ export default function DashProfile({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
<label className="admin-form-label">
|
<label className="admin-form-label">Nové heslo</label>
|
||||||
Nové heslo
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.new_password}
|
value={formData.new_password}
|
||||||
@@ -377,59 +355,41 @@ export default function DashProfile({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowModal(false)}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
>
|
|
||||||
Uložit změny
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* 2FA Setup Modal */}
|
{/* 2FA Setup Modal — multi-step: setup → backup codes */}
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{show2FASetup && (
|
isOpen={show2FASetup}
|
||||||
<motion.div
|
onClose={() => {
|
||||||
className="admin-modal-overlay"
|
// Only reachable on the setup step (backdrop/ESC/× are locked while
|
||||||
initial={{ opacity: 0 }}
|
// backupCodes is set — that branch is unreachable so removed).
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => {
|
|
||||||
if (!backupCodes) {
|
|
||||||
setShow2FASetup(false);
|
setShow2FASetup(false);
|
||||||
}
|
setTotpCode("");
|
||||||
}}
|
}}
|
||||||
/>
|
title={backupCodes ? "Záložní kódy" : "Nastavení 2FA"}
|
||||||
<motion.div
|
size="md"
|
||||||
className="admin-modal"
|
loading={backupCodes ? false : totpSubmitting}
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
onSubmit={
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
backupCodes
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
? () => {
|
||||||
transition={{ duration: 0.2 }}
|
setShow2FASetup(false);
|
||||||
|
setBackupCodes(null);
|
||||||
|
}
|
||||||
|
: onConfirm2FA
|
||||||
|
}
|
||||||
|
submitLabel={
|
||||||
|
backupCodes ? "Rozumím, uložil jsem si kódy" : "Aktivovat 2FA"
|
||||||
|
}
|
||||||
|
submitDisabled={backupCodes ? false : totpCode.length !== 6}
|
||||||
|
cancelLabel="Zrušit"
|
||||||
|
// On the backup-codes step the cancel/× would silently dismiss unsaved
|
||||||
|
// codes, so hide the footer + × and lock backdrop/ESC; the explicit
|
||||||
|
// in-body primary is the single deliberate exit.
|
||||||
|
hideFooter={!!backupCodes}
|
||||||
|
hideCloseButton={!!backupCodes}
|
||||||
|
closeOnBackdrop={!backupCodes}
|
||||||
|
closeOnEsc={!backupCodes}
|
||||||
>
|
>
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">
|
|
||||||
{backupCodes ? "Záložní kódy" : "Nastavení 2FA"}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
{backupCodes ? (
|
{backupCodes ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="admin-role-locked-notice mb-4">
|
<div className="admin-role-locked-notice mb-4">
|
||||||
@@ -445,9 +405,8 @@ export default function DashProfile({
|
|||||||
<line x1="12" y1="9" x2="12" y2="13" />
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
</svg>
|
</svg>
|
||||||
Uložte si tyto kódy na bezpečné místo. Každý kód lze
|
Uložte si tyto kódy na bezpečné místo. Každý kód lze použít pouze
|
||||||
použít pouze jednou. Po zavření tohoto okna je již
|
jednou. Po zavření tohoto okna je již neuvidíte.
|
||||||
neuvidíte.
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -477,9 +436,7 @@ export default function DashProfile({
|
|||||||
<div style={{ marginTop: "0.75rem" }}>
|
<div style={{ marginTop: "0.75rem" }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard?.writeText(
|
navigator.clipboard?.writeText(backupCodes.join("\n"));
|
||||||
backupCodes.join("\n"),
|
|
||||||
);
|
|
||||||
alert.success("Kódy zkopírovány");
|
alert.success("Kódy zkopírovány");
|
||||||
}}
|
}}
|
||||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||||
@@ -492,19 +449,25 @@ export default function DashProfile({
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
>
|
>
|
||||||
<rect
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||||
x="9"
|
|
||||||
y="9"
|
|
||||||
width="13"
|
|
||||||
height="13"
|
|
||||||
rx="2"
|
|
||||||
ry="2"
|
|
||||||
/>
|
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||||
</svg>
|
</svg>
|
||||||
Kopírovat kódy
|
Kopírovat kódy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Footer is hidden on this step; the single deliberate exit lives
|
||||||
|
in the body and also clears the codes. */}
|
||||||
|
<div style={{ marginTop: "1.25rem", textAlign: "right" }}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShow2FASetup(false);
|
||||||
|
setBackupCodes(null);
|
||||||
|
}}
|
||||||
|
className="admin-btn admin-btn-primary"
|
||||||
|
>
|
||||||
|
Rozumím, uložil jsem si kódy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
@@ -512,13 +475,11 @@ export default function DashProfile({
|
|||||||
className="text-secondary"
|
className="text-secondary"
|
||||||
style={{ fontSize: "0.875rem", marginBottom: "1rem" }}
|
style={{ fontSize: "0.875rem", marginBottom: "1rem" }}
|
||||||
>
|
>
|
||||||
Naskenujte QR kód v autentizační aplikaci (Google
|
Naskenujte QR kód v autentizační aplikaci (Google Authenticator,
|
||||||
Authenticator, Authy, Microsoft Authenticator apod.)
|
Authy, Microsoft Authenticator apod.)
|
||||||
</p>
|
</p>
|
||||||
{totpQrUri && (
|
{totpQrUri && (
|
||||||
<div
|
<div style={{ textAlign: "center", marginBottom: "1rem" }}>
|
||||||
style={{ textAlign: "center", marginBottom: "1rem" }}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(totpQrUri)}`}
|
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(totpQrUri)}`}
|
||||||
alt="TOTP QR Code"
|
alt="TOTP QR Code"
|
||||||
@@ -573,14 +534,7 @@ export default function DashProfile({
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
>
|
>
|
||||||
<rect
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||||
x="9"
|
|
||||||
y="9"
|
|
||||||
width="13"
|
|
||||||
height="13"
|
|
||||||
rx="2"
|
|
||||||
ry="2"
|
|
||||||
/>
|
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -598,9 +552,7 @@ export default function DashProfile({
|
|||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
value={totpCode}
|
value={totpCode}
|
||||||
onChange={(e) =>
|
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, ""))}
|
||||||
setTotpCode(e.target.value.replace(/\D/g, ""))
|
|
||||||
}
|
|
||||||
placeholder="000000"
|
placeholder="000000"
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
style={{
|
style={{
|
||||||
@@ -618,67 +570,20 @@ export default function DashProfile({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</FormModal>
|
||||||
<div className="admin-modal-footer">
|
|
||||||
{backupCodes ? (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShow2FASetup(false);
|
|
||||||
setBackupCodes(null);
|
|
||||||
}}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
>
|
|
||||||
Rozumím, uložil jsem si kódy
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => setShow2FASetup(false)}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={totpSubmitting}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={onConfirm2FA}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={totpSubmitting || totpCode.length !== 6}
|
|
||||||
>
|
|
||||||
{totpSubmitting ? "Ověřuji..." : "Aktivovat 2FA"}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* 2FA Disable Modal */}
|
{/* 2FA Disable Modal */}
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{show2FADisable && (
|
isOpen={show2FADisable}
|
||||||
<motion.div
|
onClose={() => setShow2FADisable(false)}
|
||||||
className="admin-modal-overlay"
|
title="Deaktivovat 2FA"
|
||||||
initial={{ opacity: 0 }}
|
size="md"
|
||||||
animate={{ opacity: 1 }}
|
loading={totpSubmitting}
|
||||||
exit={{ opacity: 0 }}
|
onSubmit={onDisable2FA}
|
||||||
transition={{ duration: 0.2 }}
|
submitDisabled={disableCode.length !== 6}
|
||||||
|
submitLabel="Deaktivovat 2FA"
|
||||||
|
cancelLabel="Zrušit"
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => setShow2FADisable(false)}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">Deaktivovat 2FA</h2>
|
|
||||||
</div>
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
color: "var(--text-secondary)",
|
color: "var(--text-secondary)",
|
||||||
@@ -697,9 +602,7 @@ export default function DashProfile({
|
|||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
value={disableCode}
|
value={disableCode}
|
||||||
onChange={(e) =>
|
onChange={(e) => setDisableCode(e.target.value.replace(/\D/g, ""))}
|
||||||
setDisableCode(e.target.value.replace(/\D/g, ""))
|
|
||||||
}
|
|
||||||
placeholder="000000"
|
placeholder="000000"
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
style={{
|
style={{
|
||||||
@@ -716,27 +619,7 @@ export default function DashProfile({
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
onClick={() => setShow2FADisable(false)}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={totpSubmitting}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={onDisable2FA}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={totpSubmitting || disableCode.length !== 6}
|
|
||||||
>
|
|
||||||
{totpSubmitting ? "Deaktivuji..." : "Deaktivovat 2FA"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useAuth } from "../../context/AuthContext";
|
import { useAuth } from "../../context/AuthContext";
|
||||||
import { useAlert } from "../../context/AlertContext";
|
import { useAlert } from "../../context/AlertContext";
|
||||||
import { formatKm, todayLocalStr } from "../../utils/formatters";
|
import { formatKm, todayLocalStr } from "../../utils/formatters";
|
||||||
import AdminDatePicker from "../AdminDatePicker";
|
import AdminDatePicker from "../AdminDatePicker";
|
||||||
import apiFetch from "../../utils/api";
|
import apiFetch from "../../utils/api";
|
||||||
import useModalLock from "../../hooks/useModalLock";
|
import FormModal from "../FormModal";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -69,8 +69,6 @@ export default function DashQuickActions({
|
|||||||
});
|
});
|
||||||
const [tripErrors, setTripErrors] = useState<TripErrors>({});
|
const [tripErrors, setTripErrors] = useState<TripErrors>({});
|
||||||
|
|
||||||
useModalLock(showTripModal);
|
|
||||||
|
|
||||||
const openTripModal = async () => {
|
const openTripModal = async () => {
|
||||||
setTripForm({
|
setTripForm({
|
||||||
vehicle_id: "",
|
vehicle_id: "",
|
||||||
@@ -316,38 +314,21 @@ export default function DashQuickActions({
|
|||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{showTripModal && (
|
isOpen={showTripModal}
|
||||||
<motion.div
|
onClose={() => setShowTripModal(false)}
|
||||||
className="admin-modal-overlay"
|
title="Přidat jízdu"
|
||||||
initial={{ opacity: 0 }}
|
size="lg"
|
||||||
animate={{ opacity: 1 }}
|
onSubmit={handleTripSubmit}
|
||||||
exit={{ opacity: 0 }}
|
submitLabel="Uložit"
|
||||||
transition={{ duration: 0.2 }}
|
loading={tripSubmitting}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => setShowTripModal(false)}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal admin-modal-lg"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">Přidat jízdu</h2>
|
|
||||||
</div>
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<div className="admin-form-row">
|
<div className="admin-form-row">
|
||||||
<div
|
<div
|
||||||
className={`admin-form-group${tripErrors.vehicle_id ? " has-error" : ""}`}
|
className={`admin-form-group${tripErrors.vehicle_id ? " has-error" : ""}`}
|
||||||
>
|
>
|
||||||
<label className="admin-form-label required">
|
<label className="admin-form-label required">Vozidlo</label>
|
||||||
Vozidlo
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={tripForm.vehicle_id}
|
value={tripForm.vehicle_id}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -375,9 +356,7 @@ export default function DashQuickActions({
|
|||||||
<div
|
<div
|
||||||
className={`admin-form-group${tripErrors.trip_date ? " has-error" : ""}`}
|
className={`admin-form-group${tripErrors.trip_date ? " has-error" : ""}`}
|
||||||
>
|
>
|
||||||
<label className="admin-form-label required">
|
<label className="admin-form-label required">Datum jízdy</label>
|
||||||
Datum jízdy
|
|
||||||
</label>
|
|
||||||
<AdminDatePicker
|
<AdminDatePicker
|
||||||
mode="date"
|
mode="date"
|
||||||
value={tripForm.trip_date}
|
value={tripForm.trip_date}
|
||||||
@@ -390,9 +369,7 @@ export default function DashQuickActions({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{tripErrors.trip_date && (
|
{tripErrors.trip_date && (
|
||||||
<span className="admin-form-error">
|
<span className="admin-form-error">{tripErrors.trip_date}</span>
|
||||||
{tripErrors.trip_date}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -422,9 +399,7 @@ export default function DashQuickActions({
|
|||||||
min="0"
|
min="0"
|
||||||
/>
|
/>
|
||||||
{tripErrors.start_km && (
|
{tripErrors.start_km && (
|
||||||
<span className="admin-form-error">
|
<span className="admin-form-error">{tripErrors.start_km}</span>
|
||||||
{tripErrors.start_km}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -451,9 +426,7 @@ export default function DashQuickActions({
|
|||||||
min="0"
|
min="0"
|
||||||
/>
|
/>
|
||||||
{tripErrors.end_km && (
|
{tripErrors.end_km && (
|
||||||
<span className="admin-form-error">
|
<span className="admin-form-error">{tripErrors.end_km}</span>
|
||||||
{tripErrors.end_km}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-group">
|
<div className="admin-form-group">
|
||||||
@@ -472,9 +445,7 @@ export default function DashQuickActions({
|
|||||||
<div
|
<div
|
||||||
className={`admin-form-group${tripErrors.route_from ? " has-error" : ""}`}
|
className={`admin-form-group${tripErrors.route_from ? " has-error" : ""}`}
|
||||||
>
|
>
|
||||||
<label className="admin-form-label required">
|
<label className="admin-form-label required">Místo odjezdu</label>
|
||||||
Místo odjezdu
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={tripForm.route_from}
|
value={tripForm.route_from}
|
||||||
@@ -520,9 +491,7 @@ export default function DashQuickActions({
|
|||||||
placeholder="Např. Brno"
|
placeholder="Např. Brno"
|
||||||
/>
|
/>
|
||||||
{tripErrors.route_to && (
|
{tripErrors.route_to && (
|
||||||
<span className="admin-form-error">
|
<span className="admin-form-error">{tripErrors.route_to}</span>
|
||||||
{tripErrors.route_to}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -560,29 +529,7 @@ export default function DashQuickActions({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowTripModal(false)}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={tripSubmitting}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleTripSubmit}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={tripSubmitting}
|
|
||||||
>
|
|
||||||
{tripSubmitting ? "Ukládám..." : "Uložit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,3 +197,15 @@
|
|||||||
.react-datepicker__close-icon::after {
|
.react-datepicker__close-icon::after {
|
||||||
background-color: var(--accent-color) !important;
|
background-color: var(--accent-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile safety net — keep the popper inside the viewport on small phones.
|
||||||
|
(Native date input is used on touch; this guards the rare desktop-style popper.) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.react-datepicker-popper {
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker {
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -169,3 +169,31 @@
|
|||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
File Manager — mobile
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
/* Toolbar wraps so actions drop below the path instead of overflowing */
|
||||||
|
.fm-toolbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New-folder input goes full width (override the 250px desktop cap) */
|
||||||
|
.fm-new-folder .admin-form-input {
|
||||||
|
max-width: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumb scrolls horizontally instead of overflowing the container */
|
||||||
|
.fm-breadcrumb {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fm-breadcrumb-segment {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -310,6 +310,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
.admin-form-row,
|
||||||
|
.admin-form-row-3,
|
||||||
.admin-form-row-4,
|
.admin-form-row-4,
|
||||||
.admin-form-row-5 {
|
.admin-form-row-5 {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -127,9 +127,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
/* Month-nav row may wrap so it never forces horizontal page overflow */
|
||||||
|
.invoice-month-nav {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 44px touch target for the prev/next month buttons */
|
||||||
.invoice-month-btn {
|
.invoice-month-btn {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.received-upload-row {
|
.received-upload-row {
|
||||||
|
|||||||
@@ -464,6 +464,26 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep Quill picker dropdowns on-screen on narrow phones.
|
||||||
|
The font picker hard-codes min-width: 11rem and the color picker a fixed
|
||||||
|
176px width — both overflow a ~360px viewport. */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.admin-rich-editor .ql-snow .ql-picker-options,
|
||||||
|
.admin-rich-editor .ql-snow .ql-font .ql-picker-options,
|
||||||
|
.admin-rich-editor .ql-snow .ql-size .ql-picker-options {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-rich-editor
|
||||||
|
.ql-snow
|
||||||
|
.ql-color-picker
|
||||||
|
.ql-picker-options[aria-hidden="false"] {
|
||||||
|
width: auto;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.offers-items-table .admin-table td .admin-form-input {
|
.offers-items-table .admin-table td .admin-form-input {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ 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 { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import FormModal from "../components/FormModal";
|
||||||
import {
|
import {
|
||||||
formatTime,
|
formatTime,
|
||||||
calculateWorkMinutes,
|
calculateWorkMinutes,
|
||||||
@@ -172,8 +172,6 @@ export default function Attendance() {
|
|||||||
}
|
}
|
||||||
}, [statusQuery.data]);
|
}, [statusQuery.data]);
|
||||||
|
|
||||||
useModalLock(isLeaveModalOpen);
|
|
||||||
|
|
||||||
if (!hasPermission("attendance.record")) return <Forbidden />;
|
if (!hasPermission("attendance.record")) return <Forbidden />;
|
||||||
|
|
||||||
const handlePunch = (action: string) => {
|
const handlePunch = (action: string) => {
|
||||||
@@ -931,31 +929,24 @@ export default function Attendance() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Leave Modal */}
|
{/* Leave Modal */}
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{isLeaveModalOpen && (
|
isOpen={isLeaveModalOpen}
|
||||||
<motion.div
|
onClose={() => setIsLeaveModalOpen(false)}
|
||||||
className="admin-modal-overlay"
|
title="Žádost o nepřítomnost"
|
||||||
initial={{ opacity: 0 }}
|
onSubmit={() => {
|
||||||
animate={{ opacity: 1 }}
|
// Preserve original guard: do not submit when there are 0 business days.
|
||||||
exit={{ opacity: 0 }}
|
if (
|
||||||
transition={{ duration: 0.2 }}
|
calculateBusinessDays(leaveForm.date_from, leaveForm.date_to) === 0
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
handleRequestSubmit();
|
||||||
|
}}
|
||||||
|
submitLabel="Odeslat žádost"
|
||||||
|
submitDisabled={
|
||||||
|
calculateBusinessDays(leaveForm.date_from, leaveForm.date_to) === 0
|
||||||
|
}
|
||||||
|
loading={requestSubmitting}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => setIsLeaveModalOpen(false)}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">Žádost o nepřítomnost</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<FormField label="Typ nepřítomnosti">
|
<FormField label="Typ nepřítomnosti">
|
||||||
<select
|
<select
|
||||||
@@ -1058,36 +1049,7 @@ export default function Attendance() {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
|
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsLeaveModalOpen(false)}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={requestSubmitting}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleRequestSubmit}
|
|
||||||
disabled={
|
|
||||||
requestSubmitting ||
|
|
||||||
calculateBusinessDays(
|
|
||||||
leaveForm.date_from,
|
|
||||||
leaveForm.date_to,
|
|
||||||
) === 0
|
|
||||||
}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
>
|
|
||||||
{requestSubmitting ? "Odesílám..." : "Odeslat žádost"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={gpsConfirm.isOpen}
|
isOpen={gpsConfirm.isOpen}
|
||||||
|
|||||||
@@ -81,9 +81,17 @@ export default function ProjectDetail() {
|
|||||||
const notes: ProjectNote[] = project?.project_notes || [];
|
const notes: ProjectNote[] = project?.project_notes || [];
|
||||||
|
|
||||||
const { data: usersData } = useQuery(userListOptions("projects.view"));
|
const { data: usersData } = useQuery(userListOptions("projects.view"));
|
||||||
const users: User[] = (usersData ?? []).map((u: ApiUser) => ({
|
// Only active users with projects.view — but keep the project's CURRENT
|
||||||
|
// assignee in the list even if they're now inactive, so the select still
|
||||||
|
// reflects the saved value (marked "(neaktivní)" so it's clear).
|
||||||
|
const assignedUserId = project?.responsible_user_id || "";
|
||||||
|
const users: User[] = (usersData ?? [])
|
||||||
|
.filter((u: ApiUser) => u.is_active || String(u.id) === assignedUserId)
|
||||||
|
.map((u: ApiUser) => ({
|
||||||
id: u.id,
|
id: u.id,
|
||||||
name: `${u.first_name || ""} ${u.last_name || ""}`.trim() || u.username,
|
name:
|
||||||
|
(`${u.first_name || ""} ${u.last_name || ""}`.trim() || u.username) +
|
||||||
|
(u.is_active ? "" : " (neaktivní)"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Reset form sync when navigating to a different project
|
// Reset form sync when navigating to a different project
|
||||||
|
|||||||
@@ -98,7 +98,12 @@ export default function Projects() {
|
|||||||
...offerCustomersOptions(),
|
...offerCustomersOptions(),
|
||||||
enabled: showCreate,
|
enabled: showCreate,
|
||||||
});
|
});
|
||||||
const usersQuery = useQuery({ ...userListOptions(), enabled: showCreate });
|
// Responsible-person picker: only users whose role can view projects
|
||||||
|
// (projects.view, filtered server-side) and who are active.
|
||||||
|
const usersQuery = useQuery({
|
||||||
|
...userListOptions("projects.view"),
|
||||||
|
enabled: showCreate,
|
||||||
|
});
|
||||||
const nextNumberQuery = useQuery({
|
const nextNumberQuery = useQuery({
|
||||||
...projectNextNumberOptions(),
|
...projectNextNumberOptions(),
|
||||||
enabled: showCreate,
|
enabled: showCreate,
|
||||||
@@ -512,7 +517,9 @@ export default function Projects() {
|
|||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
>
|
>
|
||||||
<option value="">— nepřiřazeno —</option>
|
<option value="">— nepřiřazeno —</option>
|
||||||
{(usersQuery.data ?? []).map((u) => (
|
{(usersQuery.data ?? [])
|
||||||
|
.filter((u) => u.is_active)
|
||||||
|
.map((u) => (
|
||||||
<option key={u.id} value={u.id}>
|
<option key={u.id} value={u.id}>
|
||||||
{u.first_name} {u.last_name}
|
{u.first_name} {u.last_name}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -65,6 +65,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Row actions — enlarge touch targets so icon buttons are tappable */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-table-actions a,
|
||||||
|
.admin-table-actions button,
|
||||||
|
.admin-table-actions .admin-btn-icon {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.admin-table-user {
|
.admin-table-user {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user