- 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>
226 lines
5.3 KiB
TypeScript
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" />;
|
|
}
|