Compare commits

...

6 Commits

Author SHA1 Message Date
BOHA
dafa6d6aab chore(release): v1.9.4 — modal UX unification + project responsible-person filter
- Unify modals: ConfirmModal now locks scroll; 8 ad-hoc modals migrated to the
  shared FormModal (consistent open/close animation, blurred backdrop, ESC,
  click-outside, scroll-lock, mobile, reduced-motion). FormModal gains
  submitDisabled + subtitle props.
- Projects: the responsible-person picker (create modal + edit page) lists only
  active users with projects.view (edit page keeps the current assignee).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:54:32 +02:00
BOHA
80803cc868 refactor(modals): migrate 8 ad-hoc modals to the shared FormModal
BulkAttendanceModal, ShiftFormModal, OrderConfirmationModal, DashQuickActions
trip modal, the Attendance leave modal, and DashProfile's 3 modals (edit
profile, 2FA setup, 2FA disable) now use the shared FormModal instead of
hand-rolled overlays, inheriting the canonical open/close animation, blurred
backdrop, ESC-to-close, click-outside, scroll-lock, staggered reveal, mobile
full-screen and reduced-motion. Footers stay pinned via the real FormModal
footer + submitDisabled; multi-step modals (2FA setup, order confirmation)
drive title/footer/close-guards per step. Behaviour preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:53:28 +02:00
BOHA
55fa4045ad feat(modals): FormModal gains submitDisabled + subtitle props
submitDisabled greys out the primary (so callers can gate it on validity
instead of a clickable-but-inert button); subtitle renders a muted line under
the title in the header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:53:28 +02:00
BOHA
7f156cc721 fix(modals): ConfirmModal locks body scroll + respects loading on backdrop
ConfirmModal never called useModalLock, so the page scrolled behind every
confirmation dialog (8+ usages). Add the ref-counted scroll lock and guard
backdrop-click against the loading state, matching FormModal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:34:48 +02:00
BOHA
e0d2fccf50 fix(projects): edit-page responsible picker shows only active users (keeps current assignee)
Mirror the create-modal fix on ProjectDetail: filter the 'Zodpovědná osoba'
select to active users (already scoped to projects.view), but keep the
project's currently-assigned user selectable even if inactive, labelled
'(neaktivní)', so the saved value still shows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:24:41 +02:00
BOHA
a1c251650b fix(projects): responsible-person picker shows only active users with projects.view
The create-project modal's 'Zodpovědná osoba' select listed every user
(including inactive ones). Filter the query server-side by projects.view and
hide inactive users client-side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:19:15 +02:00
13 changed files with 1291 additions and 1625 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "app-ts", "name": "app-ts",
"version": "1.9.3", "version": "1.9.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "app-ts", "name": "app-ts",
"version": "1.9.3", "version": "1.9.4",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "app-ts", "name": "app-ts",
"version": "1.9.3", "version": "1.9.4",
"description": "", "description": "",
"main": "dist/server.js", "main": "dist/server.js",
"scripts": { "scripts": {

View File

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

View File

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

View File

@@ -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"

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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>