Files
app/src/admin/components/BulkAttendanceModal.tsx
BOHA aa6c1b5094 refactor: fix all Low findings from FLAWS_REPORT audit
- Auth: TOTP params from config, JWT error logging, audit log failure
  logging, replaced_by_hash validation on token rotation
- Invoices: remove dead VAT code, consistent PDF permissions,
  WebP magic-byte detection, deduped exchange-rate fetches
- Orders/Offers: multipart limit from config, use paginated() helper,
  payment method from DB in PDF
- Projects: verify project exists before creating note
- Attendance: action_type enum validation, consistent local-time
  shift_date construction, holiday attendance in work fund,
  trips.view permission on last-km query
- Users: paginated() helper usage, remove duplicate dashboard keys,
  parallel currency conversion, single hashToken implementation
- Frontend: memoized customInput, reliable print onload, modal prop
  standardization (isOpen), ConfirmModal type icons, id===0 key
  fallback, Login useCallback, CompanySettings ConfirmModal,
  Attendance timeout cleanup, Dashboard memoization, beforeunload
  dirty-state warnings on Invoice/Offer/Order detail
- Schema: invoice_alert_log timestamp, config/env comment on
  Date.prototype.toJSON override
- Utils: exchange-rate inflight dedup

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 08:45:37 +02:00

221 lines
7.3 KiB
TypeScript

import { motion, AnimatePresence } from "framer-motion";
import AdminDatePicker from "./AdminDatePicker";
import useModalLock from "../hooks/useModalLock";
interface BulkAttendanceForm {
month: string;
user_ids: string[];
arrival_time: string;
departure_time: string;
break_start_time: string;
break_end_time: string;
}
interface BulkAttendanceUser {
id: number | string;
name: string;
}
interface BulkAttendanceModalProps {
isOpen: boolean;
onClose: () => void;
form: BulkAttendanceForm;
setForm: (form: BulkAttendanceForm) => void;
users: BulkAttendanceUser[];
onSubmit: () => void;
submitting: boolean;
toggleUser: (userId: number | string) => void;
toggleAllUsers: () => void;
}
export default function BulkAttendanceModal({
isOpen,
onClose,
form,
setForm,
users,
onSubmit,
submitting,
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 }}
>
<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",
fontSize: "0.875rem",
}}
>
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>
<AdminDatePicker
mode="month"
value={form.month}
onChange={(val) => setForm({ ...form, month: val })}
/>
</div>
<div className="admin-form-group">
<label className="admin-form-label">
Zaměstnanci
<button
type="button"
onClick={toggleAllUsers}
style={{
marginLeft: "0.75rem",
background: "none",
border: "none",
color: "var(--accent-color)",
cursor: "pointer",
fontSize: "0.8125rem",
fontWeight: 500,
padding: 0,
}}
>
{form.user_ids.length === users.length
? "Odznačit vše"
: "Vybrat vše"}
</button>
</label>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "0.375rem",
maxHeight: "200px",
overflowY: "auto",
padding: "0.75rem",
background: "var(--bg-tertiary)",
borderRadius: "var(--border-radius-sm)",
border: "1px solid var(--border-color)",
}}
>
{users.map((user) => (
<label key={user.id} className="admin-form-checkbox">
<input
type="checkbox"
checked={form.user_ids.includes(String(user.id))}
onChange={() => toggleUser(user.id)}
/>
<span>{user.name}</span>
</label>
))}
</div>
<small className="admin-form-hint">
Vybráno: {form.user_ids.length} z {users.length}
</small>
</div>
<div className="admin-form-row">
<div className="admin-form-group">
<label className="admin-form-label">Příchod</label>
<AdminDatePicker
mode="time"
value={form.arrival_time}
onChange={(val) =>
setForm({ ...form, arrival_time: val })
}
/>
</div>
<div className="admin-form-group">
<label className="admin-form-label">Odchod</label>
<AdminDatePicker
mode="time"
value={form.departure_time}
onChange={(val) =>
setForm({ ...form, departure_time: val })
}
/>
</div>
</div>
<div className="admin-form-row">
<div className="admin-form-group">
<label className="admin-form-label">Začátek pauzy</label>
<AdminDatePicker
mode="time"
value={form.break_start_time}
onChange={(val) =>
setForm({ ...form, break_start_time: val })
}
/>
</div>
<div className="admin-form-group">
<label className="admin-form-label">Konec pauzy</label>
<AdminDatePicker
mode="time"
value={form.break_end_time}
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>
);
}