Files
app/src/admin/components/AdminDatePicker.tsx
BOHA d7c7fbad88 fix: security, validation, and data integrity fixes across 53 files
- Auth: HS256 algorithm restriction on JWT verify, timing-safe bcrypt
  for inactive/locked users, locked_until check in loadAuthData, TOTP
  fixes (async bcrypt, BigInt conversion, future-code counter fix)
- Validation: Zod enums for leave_type/status, numeric transforms on
  foreign keys, VAT 0% coercion fix (Number(v)||21 → v!=null checks)
- Permissions: requirePermission on attendance PUT, attendance_users
  and project_logs access checks, trips users filtered by trips.record
- Prisma queries: fixed roles.is:{OR} pattern (doesn't work on to-one
  relations), attendance_users now filters by attendance.record only
- Transactions: wrapped deleteOrder, createOrder, updateUser, deleteUser,
  duplicateOffer, bulkCreateAttendance, createLeave, scope-templates,
  leave-requests, company-settings, profile updates
- Frontend: mountedRef reset in useListData, blob URL cleanup on unmount,
  null checks on date fields, AdminDatePicker min/max for HH:mm
- Security headers: COOP, CORP, CSP frame-ancestors/form-action/base-uri
- Other: exchange-rate cache TTL, invoice-alert midnight comparison fix,
  numbering.service releaseSequence no-op, nas-offers filename sanitize,
  Content-Disposition header injection fix, mojibake Czech strings

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 08:40:38 +02:00

226 lines
5.3 KiB
TypeScript

import { forwardRef, useMemo } from "react";
import DatePicker, { registerLocale } from "react-datepicker";
import { cs } from "date-fns/locale";
import { parse, format } from "date-fns";
import "react-datepicker/dist/react-datepicker.css";
registerLocale("cs", cs);
// Ensure portal root exists
if (
typeof document !== "undefined" &&
!document.getElementById("datepicker-portal")
) {
const el = document.createElement("div");
el.id = "datepicker-portal";
document.body.appendChild(el);
}
const isTouchDevice = () =>
typeof window !== "undefined" &&
("ontouchstart" in window || navigator.maxTouchPoints > 0);
interface CustomInputProps {
value?: string;
onClick?: () => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
required?: boolean;
readOnly?: boolean;
disabled?: boolean;
}
const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>(
(
{ value, onClick, onChange, placeholder, required, readOnly, disabled },
ref,
) => (
<input
className="admin-form-input"
onClick={onClick}
onChange={onChange}
value={value}
placeholder={placeholder}
ref={ref}
required={required}
readOnly={readOnly}
disabled={disabled}
autoComplete="off"
/>
),
);
interface NativeInputProps {
mode: string;
value: string;
onChange: (value: string) => void;
required?: boolean;
minDate?: string;
maxDate?: string;
disabled?: boolean;
}
const modeToInputType: Record<string, string> = {
month: "month",
time: "time",
};
function NativeInput({
mode,
value,
onChange,
required,
minDate,
maxDate,
disabled,
}: NativeInputProps) {
const type = modeToInputType[mode] || "date";
// For time inputs, min/max must be in HH:mm format, not date format
const formatTimeMinMax = (val: string | undefined): string | undefined => {
if (!val) return undefined;
// If it looks like a date string (yyyy-MM-dd), extract time portion if present,
// otherwise it's not a valid time min/max — return undefined
if (val.includes("T")) return val.split("T")[1]?.substring(0, 5);
if (val.includes(":")) return val.substring(0, 5);
return undefined;
};
const minProp =
mode === "time" ? formatTimeMinMax(minDate) : minDate || undefined;
const maxProp =
mode === "time" ? formatTimeMinMax(maxDate) : maxDate || undefined;
return (
<input
type={type}
lang="cs"
value={value || ""}
onChange={(e) => onChange(e.target.value)}
className="admin-form-input"
required={required}
disabled={disabled}
min={minProp}
max={maxProp}
/>
);
}
interface AdminDatePickerProps {
mode?: "date" | "month" | "time";
value: string;
onChange: (value: string) => void;
minDate?: string;
maxDate?: string;
disabled?: boolean;
placeholder?: string;
required?: boolean;
}
export default function AdminDatePicker({
mode = "date",
value,
onChange,
required,
minDate,
maxDate,
disabled,
placeholder,
}: AdminDatePickerProps) {
const useNative = useMemo(() => isTouchDevice(), []);
if (useNative) {
return (
<NativeInput
mode={mode}
value={value}
onChange={onChange}
required={required}
minDate={minDate}
maxDate={maxDate}
disabled={disabled}
/>
);
}
const toDate = (val: string | null | undefined): Date | null => {
if (!val) return null;
try {
if (mode === "date") return parse(val, "yyyy-MM-dd", new Date());
if (mode === "time") {
const [h, m] = val.split(":");
const d = new Date();
d.setHours(parseInt(h, 10), parseInt(m, 10), 0, 0);
return d;
}
if (mode === "month") return parse(val, "yyyy-MM", new Date());
} catch {
return null;
}
return null;
};
const handleChange = (date: Date | null) => {
if (!date) {
onChange("");
return;
}
if (mode === "date") onChange(format(date, "yyyy-MM-dd"));
else if (mode === "time") onChange(format(date, "HH:mm"));
else if (mode === "month") onChange(format(date, "yyyy-MM"));
};
const parseMinMax = (val: string | undefined): Date | undefined => {
if (!val) return undefined;
try {
if (mode === "date") return parse(val, "yyyy-MM-dd", new Date());
if (mode === "month") return parse(val, "yyyy-MM", new Date());
} catch {
return undefined;
}
return undefined;
};
const customInput = useMemo(
() => (
<CustomInput
required={required}
placeholder={placeholder}
disabled={disabled}
/>
),
[required, placeholder, disabled],
);
const commonProps = {
selected: toDate(value),
onChange: handleChange,
locale: "cs",
customInput,
minDate: parseMinMax(minDate),
maxDate: parseMinMax(maxDate),
popperPlacement: "bottom-start" as const,
portalId: "datepicker-portal",
disabled,
};
if (mode === "time") {
return (
<DatePicker
{...commonProps}
showTimeSelect
showTimeSelectOnly
timeIntervals={5}
timeCaption="Čas"
dateFormat="HH:mm"
timeFormat="HH:mm"
/>
);
}
if (mode === "month") {
return (
<DatePicker {...commonProps} showMonthYearPicker dateFormat="MM/yyyy" />
);
}
return <DatePicker {...commonProps} dateFormat="dd.MM.yyyy" />;
}