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>
This commit is contained in:
BOHA
2026-06-06 14:53:28 +02:00
parent 55fa4045ad
commit 80803cc868
6 changed files with 1231 additions and 1607 deletions

View File

@@ -1,6 +1,5 @@
import { motion, AnimatePresence } from "framer-motion";
import FormModal from "./FormModal";
import AdminDatePicker from "./AdminDatePicker";
import useModalLock from "../hooks/useModalLock";
interface BulkAttendanceForm {
month: string;
@@ -39,52 +38,29 @@ export default function BulkAttendanceModal({
toggleUser,
toggleAllUsers,
}: BulkAttendanceModalProps) {
useModalLock(isOpen);
return (
<AnimatePresence>
{isOpen && (
<motion.div
className="admin-modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
<FormModal
isOpen={isOpen}
onClose={onClose}
title="Vyplnit docházku za měsíc"
size="lg"
loading={submitting}
onSubmit={onSubmit}
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
style={{
color: "var(--text-secondary)",
marginTop: "0.25rem",
marginBottom: "1rem",
fontSize: "0.875rem",
}}
>
Vytvoří záznamy pro všechny pracovní dny. Svátky se automaticky
označí. Existující záznamy se přeskočí.
Vytvoří záznamy pro všechny pracovní dny. Svátky se automaticky označí.
Existující záznamy se přeskočí.
</p>
</div>
<div className="admin-modal-body">
<div className="admin-form">
<div className="admin-form-group">
<label className="admin-form-label">Měsíc</label>
@@ -152,9 +128,7 @@ export default function BulkAttendanceModal({
<AdminDatePicker
mode="time"
value={form.arrival_time}
onChange={(val) =>
setForm({ ...form, arrival_time: val })
}
onChange={(val) => setForm({ ...form, arrival_time: val })}
/>
</div>
<div className="admin-form-group">
@@ -162,9 +136,7 @@ export default function BulkAttendanceModal({
<AdminDatePicker
mode="time"
value={form.departure_time}
onChange={(val) =>
setForm({ ...form, departure_time: val })
}
onChange={(val) => setForm({ ...form, departure_time: val })}
/>
</div>
</div>
@@ -175,9 +147,7 @@ export default function BulkAttendanceModal({
<AdminDatePicker
mode="time"
value={form.break_start_time}
onChange={(val) =>
setForm({ ...form, break_start_time: val })
}
onChange={(val) => setForm({ ...form, break_start_time: val })}
/>
</div>
<div className="admin-form-group">
@@ -185,36 +155,11 @@ export default function BulkAttendanceModal({
<AdminDatePicker
mode="time"
value={form.break_end_time}
onChange={(val) =>
setForm({ ...form, break_end_time: val })
}
onChange={(val) => setForm({ ...form, break_end_time: val })}
/>
</div>
</div>
</div>
</div>
<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>
</FormModal>
);
}

View File

@@ -1,5 +1,5 @@
import { useState, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import FormModal from "./FormModal";
import { useAlert } from "../context/AlertContext";
interface ConfirmationItem {
@@ -103,38 +103,14 @@ export default function OrderConfirmationModal({
}, [defaultVatRate]);
return (
<AnimatePresence>
{isOpen && (
<motion.div
className="admin-modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
<FormModal
isOpen={isOpen}
onClose={onClose}
title={`Potvrzení objednávky ${orderNumber}`}
size={step === "edit" ? "lg" : "md"}
loading={loading}
hideFooter
>
<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" ? (
<div className="admin-form">
<div className="admin-form-group">
@@ -195,10 +171,7 @@ export default function OrderConfirmationModal({
<div className="admin-form-group">
<label className="admin-form-label">Obsah potvrzení</label>
<p
className="text-secondary"
style={{ marginBottom: "0.75rem" }}
>
<p className="text-secondary" style={{ marginBottom: "0.75rem" }}>
Jak chcete připravit potvrzení objednávky?
</p>
<button
@@ -227,6 +200,17 @@ export default function OrderConfirmationModal({
Upravit položky
</button>
</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 className="admin-form">
@@ -261,11 +245,7 @@ export default function OrderConfirmationModal({
type="number"
value={item.quantity}
onChange={(e) =>
updateItem(
i,
"quantity",
Number(e.target.value) || 0,
)
updateItem(i, "quantity", Number(e.target.value) || 0)
}
className="admin-form-input"
style={{ width: "80px" }}
@@ -276,9 +256,7 @@ export default function OrderConfirmationModal({
<input
type="text"
value={item.unit}
onChange={(e) =>
updateItem(i, "unit", e.target.value)
}
onChange={(e) => updateItem(i, "unit", e.target.value)}
className="admin-form-input"
style={{ width: "60px" }}
/>
@@ -304,11 +282,7 @@ export default function OrderConfirmationModal({
type="number"
value={item.vat_rate}
onChange={(e) =>
updateItem(
i,
"vat_rate",
Number(e.target.value) || 0,
)
updateItem(i, "vat_rate", Number(e.target.value) || 0)
}
className="admin-form-input"
style={{ width: "70px" }}
@@ -345,13 +319,8 @@ export default function OrderConfirmationModal({
>
+ Přidat položku
</button>
</div>
)}
</div>
<div className="admin-modal-footer">
{step === "edit" && (
<>
<button
type="button"
onClick={() => setStep("choose")}
@@ -375,22 +344,9 @@ export default function OrderConfirmationModal({
"Vygenerovat PDF"
)}
</button>
</>
)}
{step === "choose" && (
<button
type="button"
onClick={onClose}
className="admin-btn admin-btn-secondary"
disabled={loading}
>
Zrušit
</button>
)}
</div>
</motion.div>
</motion.div>
</div>
)}
</AnimatePresence>
</FormModal>
);
}

View File

@@ -1,6 +1,5 @@
import { motion, AnimatePresence } from "framer-motion";
import AdminDatePicker from "./AdminDatePicker";
import useModalLock from "../hooks/useModalLock";
import FormModal from "./FormModal";
import {
calcFormWorkMinutes,
calcProjectMinutesTotal,
@@ -216,7 +215,6 @@ export default function ShiftFormModal({
onShiftDateChange,
editingRecord,
}: ShiftFormModalProps) {
useModalLock(isOpen);
const isCreate = mode === "create";
const isWorkType = form.leave_type === "work";
@@ -247,51 +245,25 @@ export default function ShiftFormModal({
};
return (
<AnimatePresence>
{isOpen && (
<motion.div
className="admin-modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
<FormModal
isOpen={isOpen}
onClose={onClose}
title={isCreate ? "Přidat záznam docházky" : "Upravit docházku"}
subtitle={
!isCreate && editingRecord
? `${editingRecord.user_name}${formatDate(editingRecord.shift_date)}`
: 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">
{isCreate ? (
<div className="admin-form-row">
<div className="admin-form-group">
<label className="admin-form-label required">
Zaměstnanec
</label>
<label className="admin-form-label required">Zaměstnanec</label>
<select
value={form.user_id}
onChange={(e) => updateField("user_id", e.target.value)}
@@ -306,9 +278,7 @@ export default function ShiftFormModal({
</select>
</div>
<div className="admin-form-group">
<label className="admin-form-label required">
Datum směny
</label>
<label className="admin-form-label required">Datum směny</label>
<AdminDatePicker
mode="date"
value={form.shift_date}
@@ -357,9 +327,7 @@ export default function ShiftFormModal({
className="admin-form-input"
/>
{isCreate && (
<small className="admin-form-hint">
8 hodin = celý den
</small>
<small className="admin-form-hint">8 hodin = celý den</small>
)}
</div>
)}
@@ -368,9 +336,7 @@ export default function ShiftFormModal({
<>
<div className="admin-form-row">
<div className="admin-form-group">
<label className="admin-form-label">
Příchod - datum
</label>
<label className="admin-form-label">Příchod - datum</label>
<AdminDatePicker
mode="date"
value={form.arrival_date}
@@ -378,9 +344,7 @@ export default function ShiftFormModal({
/>
</div>
<div className="admin-form-group">
<label className="admin-form-label">
Příchod - čas
</label>
<label className="admin-form-label">Příchod - čas</label>
<AdminDatePicker
mode="time"
value={form.arrival_time}
@@ -397,30 +361,22 @@ export default function ShiftFormModal({
<AdminDatePicker
mode="date"
value={form.break_start_date}
onChange={(val) =>
updateField("break_start_date", val)
}
onChange={(val) => updateField("break_start_date", val)}
/>
</div>
<div className="admin-form-group">
<label className="admin-form-label">
Začátek pauzy - čas
</label>
<label className="admin-form-label">Začátek pauzy - čas</label>
<AdminDatePicker
mode="time"
value={form.break_start_time}
onChange={(val) =>
updateField("break_start_time", val)
}
onChange={(val) => updateField("break_start_time", val)}
/>
</div>
</div>
<div className="admin-form-row">
<div className="admin-form-group">
<label className="admin-form-label">
Konec pauzy - datum
</label>
<label className="admin-form-label">Konec pauzy - datum</label>
<AdminDatePicker
mode="date"
value={form.break_end_date}
@@ -428,9 +384,7 @@ export default function ShiftFormModal({
/>
</div>
<div className="admin-form-group">
<label className="admin-form-label">
Konec pauzy - čas
</label>
<label className="admin-form-label">Konec pauzy - čas</label>
<AdminDatePicker
mode="time"
value={form.break_end_time}
@@ -441,9 +395,7 @@ export default function ShiftFormModal({
<div className="admin-form-row">
<div className="admin-form-group">
<label className="admin-form-label">
Odchod - datum
</label>
<label className="admin-form-label">Odchod - datum</label>
<AdminDatePicker
mode="date"
value={form.departure_date}
@@ -496,27 +448,6 @@ export default function ShiftFormModal({
/>
</div>
</div>
</div>
<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>
</FormModal>
);
}

View File

@@ -1,9 +1,9 @@
import { useState, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { motion } from "framer-motion";
import { useAuth } from "../../context/AuthContext";
import { useAlert } from "../../context/AlertContext";
import useModalLock from "../../hooks/useModalLock";
import apiFetch from "../../utils/api";
import FormModal from "../FormModal";
const API_BASE = "/api/admin";
@@ -71,8 +71,6 @@ export default function DashProfile({
last_name: "",
});
useModalLock(showModal);
const openEditModal = () => {
const nameParts = (user?.fullName || "").split(" ");
setFormData({
@@ -261,30 +259,14 @@ export default function DashProfile({
</motion.div>
{/* Edit Profile Modal */}
<AnimatePresence>
{showModal && (
<motion.div
className="admin-modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
<FormModal
isOpen={showModal}
onClose={() => setShowModal(false)}
title="Upravit profil"
size="md"
onSubmit={handleSubmit}
submitLabel="Uložit změny"
>
<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-row">
<div className="admin-form-group">
@@ -319,9 +301,7 @@ export default function DashProfile({
</div>
</div>
<div className="admin-form-group">
<label className="admin-form-label">
Uživatelské jméno
</label>
<label className="admin-form-label">Uživatelské jméno</label>
<input
type="text"
value={formData.username}
@@ -360,9 +340,7 @@ export default function DashProfile({
/>
</div>
<div className="admin-form-group">
<label className="admin-form-label">
Nové heslo
</label>
<label className="admin-form-label">Nové heslo</label>
<input
type="password"
value={formData.new_password}
@@ -377,59 +355,41 @@ export default function DashProfile({
/>
</div>
</div>
</div>
<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>
</FormModal>
{/* 2FA Setup Modal */}
<AnimatePresence>
{show2FASetup && (
<motion.div
className="admin-modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<div
className="admin-modal-backdrop"
onClick={() => {
if (!backupCodes) {
{/* 2FA Setup Modal — multi-step: setup → backup codes */}
<FormModal
isOpen={show2FASetup}
onClose={() => {
// Only reachable on the setup step (backdrop/ESC/× are locked while
// backupCodes is set — that branch is unreachable so removed).
setShow2FASetup(false);
}
setTotpCode("");
}}
/>
<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 }}
title={backupCodes ? "Záložní kódy" : "Nastavení 2FA"}
size="md"
loading={backupCodes ? false : totpSubmitting}
onSubmit={
backupCodes
? () => {
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 ? (
<div>
<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="17" x2="12.01" y2="17" />
</svg>
Uložte si tyto kódy na bezpečné místo. Každý kód lze
použít pouze jednou. Po zavření tohoto okna je již
neuvidíte.
Uložte si tyto kódy na bezpečné místo. Každý kód lze použít pouze
jednou. Po zavření tohoto okna je již neuvidíte.
</div>
<div
style={{
@@ -477,9 +436,7 @@ export default function DashProfile({
<div style={{ marginTop: "0.75rem" }}>
<button
onClick={() => {
navigator.clipboard?.writeText(
backupCodes.join("\n"),
);
navigator.clipboard?.writeText(backupCodes.join("\n"));
alert.success("Kódy zkopírovány");
}}
className="admin-btn admin-btn-secondary admin-btn-sm"
@@ -492,19 +449,25 @@ export default function DashProfile({
stroke="currentColor"
strokeWidth="2"
>
<rect
x="9"
y="9"
width="13"
height="13"
rx="2"
ry="2"
/>
<rect 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" />
</svg>
Kopírovat kódy
</button>
</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>
@@ -512,13 +475,11 @@ export default function DashProfile({
className="text-secondary"
style={{ fontSize: "0.875rem", marginBottom: "1rem" }}
>
Naskenujte QR kód v autentizační aplikaci (Google
Authenticator, Authy, Microsoft Authenticator apod.)
Naskenujte QR kód v autentizační aplikaci (Google Authenticator,
Authy, Microsoft Authenticator apod.)
</p>
{totpQrUri && (
<div
style={{ textAlign: "center", marginBottom: "1rem" }}
>
<div style={{ textAlign: "center", marginBottom: "1rem" }}>
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(totpQrUri)}`}
alt="TOTP QR Code"
@@ -573,14 +534,7 @@ export default function DashProfile({
stroke="currentColor"
strokeWidth="2"
>
<rect
x="9"
y="9"
width="13"
height="13"
rx="2"
ry="2"
/>
<rect 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" />
</svg>
</button>
@@ -598,9 +552,7 @@ export default function DashProfile({
pattern="[0-9]*"
maxLength={6}
value={totpCode}
onChange={(e) =>
setTotpCode(e.target.value.replace(/\D/g, ""))
}
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, ""))}
placeholder="000000"
className="admin-form-input"
style={{
@@ -618,67 +570,20 @@ export default function DashProfile({
</div>
</div>
)}
</div>
<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>
</FormModal>
{/* 2FA Disable Modal */}
<AnimatePresence>
{show2FADisable && (
<motion.div
className="admin-modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
<FormModal
isOpen={show2FADisable}
onClose={() => setShow2FADisable(false)}
title="Deaktivovat 2FA"
size="md"
loading={totpSubmitting}
onSubmit={onDisable2FA}
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
style={{
color: "var(--text-secondary)",
@@ -697,9 +602,7 @@ export default function DashProfile({
pattern="[0-9]*"
maxLength={6}
value={disableCode}
onChange={(e) =>
setDisableCode(e.target.value.replace(/\D/g, ""))
}
onChange={(e) => setDisableCode(e.target.value.replace(/\D/g, ""))}
placeholder="000000"
className="admin-form-input"
style={{
@@ -716,27 +619,7 @@ export default function DashProfile({
autoFocus
/>
</div>
</div>
<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>
</FormModal>
</>
);
}

View File

@@ -1,12 +1,12 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { motion, AnimatePresence } from "framer-motion";
import { motion } from "framer-motion";
import { useAuth } from "../../context/AuthContext";
import { useAlert } from "../../context/AlertContext";
import { formatKm, todayLocalStr } from "../../utils/formatters";
import AdminDatePicker from "../AdminDatePicker";
import apiFetch from "../../utils/api";
import useModalLock from "../../hooks/useModalLock";
import FormModal from "../FormModal";
const API_BASE = "/api/admin";
@@ -69,8 +69,6 @@ export default function DashQuickActions({
});
const [tripErrors, setTripErrors] = useState<TripErrors>({});
useModalLock(showTripModal);
const openTripModal = async () => {
setTripForm({
vehicle_id: "",
@@ -316,38 +314,21 @@ export default function DashQuickActions({
)}
</motion.div>
<AnimatePresence>
{showTripModal && (
<motion.div
className="admin-modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
<FormModal
isOpen={showTripModal}
onClose={() => setShowTripModal(false)}
title="Přidat jízdu"
size="lg"
onSubmit={handleTripSubmit}
submitLabel="Uložit"
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-row">
<div
className={`admin-form-group${tripErrors.vehicle_id ? " has-error" : ""}`}
>
<label className="admin-form-label required">
Vozidlo
</label>
<label className="admin-form-label required">Vozidlo</label>
<select
value={tripForm.vehicle_id}
onChange={(e) => {
@@ -375,9 +356,7 @@ export default function DashQuickActions({
<div
className={`admin-form-group${tripErrors.trip_date ? " has-error" : ""}`}
>
<label className="admin-form-label required">
Datum jízdy
</label>
<label className="admin-form-label required">Datum jízdy</label>
<AdminDatePicker
mode="date"
value={tripForm.trip_date}
@@ -390,9 +369,7 @@ export default function DashQuickActions({
}}
/>
{tripErrors.trip_date && (
<span className="admin-form-error">
{tripErrors.trip_date}
</span>
<span className="admin-form-error">{tripErrors.trip_date}</span>
)}
</div>
</div>
@@ -422,9 +399,7 @@ export default function DashQuickActions({
min="0"
/>
{tripErrors.start_km && (
<span className="admin-form-error">
{tripErrors.start_km}
</span>
<span className="admin-form-error">{tripErrors.start_km}</span>
)}
</div>
<div
@@ -451,9 +426,7 @@ export default function DashQuickActions({
min="0"
/>
{tripErrors.end_km && (
<span className="admin-form-error">
{tripErrors.end_km}
</span>
<span className="admin-form-error">{tripErrors.end_km}</span>
)}
</div>
<div className="admin-form-group">
@@ -472,9 +445,7 @@ export default function DashQuickActions({
<div
className={`admin-form-group${tripErrors.route_from ? " has-error" : ""}`}
>
<label className="admin-form-label required">
Místo odjezdu
</label>
<label className="admin-form-label required">Místo odjezdu</label>
<input
type="text"
value={tripForm.route_from}
@@ -520,9 +491,7 @@ export default function DashQuickActions({
placeholder="Např. Brno"
/>
{tripErrors.route_to && (
<span className="admin-form-error">
{tripErrors.route_to}
</span>
<span className="admin-form-error">{tripErrors.route_to}</span>
)}
</div>
</div>
@@ -560,29 +529,7 @@ export default function DashQuickActions({
/>
</div>
</div>
</div>
<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>
</FormModal>
</>
);
}

View File

@@ -3,10 +3,10 @@ import { useQuery } from "@tanstack/react-query";
import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext";
import { Link } from "react-router-dom";
import { motion, AnimatePresence } from "framer-motion";
import { motion } from "framer-motion";
import AdminDatePicker from "../components/AdminDatePicker";
import ConfirmModal from "../components/ConfirmModal";
import useModalLock from "../hooks/useModalLock";
import FormModal from "../components/FormModal";
import {
formatTime,
calculateWorkMinutes,
@@ -172,8 +172,6 @@ export default function Attendance() {
}
}, [statusQuery.data]);
useModalLock(isLeaveModalOpen);
if (!hasPermission("attendance.record")) return <Forbidden />;
const handlePunch = (action: string) => {
@@ -931,31 +929,24 @@ export default function Attendance() {
</div>
{/* Leave Modal */}
<AnimatePresence>
{isLeaveModalOpen && (
<motion.div
className="admin-modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
<FormModal
isOpen={isLeaveModalOpen}
onClose={() => setIsLeaveModalOpen(false)}
title="Žádost o nepřítomnost"
onSubmit={() => {
// Preserve original guard: do not submit when there are 0 business days.
if (
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">
<FormField label="Typ nepřítomnosti">
<select
@@ -1058,36 +1049,7 @@ export default function Attendance() {
/>
</FormField>
</div>
</div>
<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>
</FormModal>
<ConfirmModal
isOpen={gpsConfirm.isOpen}