chore(mui): delete 8 dead legacy components replaced by the kit

Removed (zero importers, build-verified): FormField, Pagination, AdminDatePicker, SortIcon, warehouse Supplier/Location/Batch selects, WarehouseMovementTable. FormModal + ConfirmModal kept (still used by OrderConfirmationModal + ProjectFileManager — to be migrated before full removal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 08:34:58 +02:00
parent 9b43d4e827
commit 088fe39bab
8 changed files with 0 additions and 812 deletions

View File

@@ -1,233 +0,0 @@
import { forwardRef } 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;
placeholder?: string;
}
const modeToInputType: Record<string, string> = {
month: "month",
time: "time",
};
function NativeInput({
mode,
value,
onChange,
required,
minDate,
maxDate,
disabled,
placeholder,
}: 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"
placeholder={placeholder}
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 = isTouchDevice();
// Mode-aware default placeholder so empty date fields always show a format
// hint (callers can still override, e.g. "Od"/"Do" on range filters).
const effectivePlaceholder =
placeholder ??
(mode === "time" ? "hh:mm" : mode === "month" ? "mm.rrrr" : "dd.mm.rrrr");
if (useNative) {
return (
<NativeInput
mode={mode}
value={value}
onChange={onChange}
required={required}
minDate={minDate}
maxDate={maxDate}
disabled={disabled}
placeholder={effectivePlaceholder}
/>
);
}
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 = (
<CustomInput
required={required}
placeholder={effectivePlaceholder}
disabled={disabled}
/>
);
const commonProps = {
selected: toDate(value),
onChange: handleChange,
locale: "cs",
customInput,
placeholderText: effectivePlaceholder,
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" />;
}

View File

@@ -1,43 +0,0 @@
import {
type CSSProperties,
type ReactNode,
isValidElement,
cloneElement,
useId,
} from "react";
interface FormFieldProps {
label: ReactNode;
children: ReactNode;
error?: string;
required?: boolean;
style?: React.CSSProperties;
}
export default function FormField({
label,
children,
error,
required,
style,
}: FormFieldProps) {
const generatedId = useId();
const childProps = isValidElement(children)
? (children.props as Record<string, unknown>)
: null;
const childId = childProps?.id ? String(childProps.id) : generatedId;
const childWithId = isValidElement(children)
? cloneElement(children, { id: childId } as React.Attributes)
: children;
return (
<div className="admin-form-group" style={style}>
<label className="admin-form-label" htmlFor={childId}>
{label}
{required && <span className="admin-form-required"> *</span>}
</label>
{childWithId}
{error && <span className="admin-form-error">{error}</span>}
</div>
);
}

View File

@@ -1,113 +0,0 @@
interface PaginationProps {
pagination: {
total: number;
page: number;
per_page: number;
total_pages: number;
} | null;
onPageChange: (page: number) => void;
onPerPageChange?: (perPage: number) => void;
}
export default function Pagination({
pagination,
onPageChange,
onPerPageChange,
}: PaginationProps) {
if (!pagination || pagination.total_pages <= 1) return null;
const { page, total_pages, total } = pagination;
const getPages = () => {
const pages: (number | string)[] = [];
const delta = 2;
for (let i = 1; i <= total_pages; i++) {
if (
i === 1 ||
i === total_pages ||
(i >= page - delta && i <= page + delta)
) {
pages.push(i);
} else if (pages[pages.length - 1] !== "...") {
pages.push("...");
}
}
return pages;
};
return (
<div
className="admin-pagination"
role="navigation"
aria-label="Stránkování"
>
<div className="admin-pagination-info">{total} záznamů</div>
<div className="admin-pagination-controls">
<button
disabled={page <= 1}
onClick={() => onPageChange(page - 1)}
className="admin-pagination-page"
aria-label="Předchozí stránka"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
{getPages().map((p, i) =>
typeof p === "string" ? (
<span key={`dots-${i}`} className="admin-pagination-ellipsis">
...
</span>
) : (
<button
key={p}
onClick={() => onPageChange(p)}
className={`admin-pagination-page ${p === page ? "active" : ""}`}
aria-label={`Stránka ${p}`}
aria-current={p === page ? "page" : undefined}
>
{p}
</button>
),
)}
<button
disabled={page >= total_pages}
onClick={() => onPageChange(page + 1)}
className="admin-pagination-page"
aria-label="Další stránka"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M9 18l6-6-6-6" />
</svg>
</button>
</div>
{onPerPageChange && (
<select
value={pagination.per_page}
onChange={(e) => onPerPageChange(Number(e.target.value))}
className="admin-pagination-select"
>
{[10, 25, 50, 100].map((n) => (
<option key={n} value={n}>
{n} / stránka
</option>
))}
</select>
)}
</div>
);
}

View File

@@ -1,36 +0,0 @@
interface SortIconProps {
column: string;
sort: string | null;
order: string;
}
export default function SortIcon({ column, sort, order }: SortIconProps) {
if (sort !== column) {
return (
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
style={{ opacity: 0.3, marginLeft: 4 }}
>
<path d="M7 15l5 5 5-5M7 9l5-5 5 5" />
</svg>
);
}
return (
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
style={{ marginLeft: 4 }}
>
{order === "asc" ? <path d="M7 15l5 5 5-5" /> : <path d="M7 9l5-5 5 5" />}
</svg>
);
}

View File

@@ -1,74 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { jsonQuery } from "../../lib/apiAdapter";
interface Batch {
id: number;
quantity: number;
unit_price: number;
received_at: string;
is_consumed: boolean;
}
interface BatchesResponse {
batches: Batch[];
total_stock: number;
reserved_quantity: number;
available_quantity: number;
}
interface BatchPickerProps {
itemId: number | null;
value: number | null;
onChange: (batchId: number, unitPrice: number) => void;
}
export default function BatchPicker({
itemId,
value,
onChange,
}: BatchPickerProps) {
const { data: response } = useQuery({
queryKey: ["warehouse", "batches", itemId],
queryFn: () =>
jsonQuery<BatchesResponse>(
`/api/admin/warehouse/items/${itemId}/batches`,
),
enabled: !!itemId,
});
const batches = response?.batches ?? [];
return (
<div>
{response && response.reserved_quantity > 0 && (
<div
style={{
fontSize: "0.75rem",
color: "var(--text-tertiary)",
marginBottom: "0.25rem",
}}
>
Dostupné: {response.available_quantity} ks (z toho rezervováno:{" "}
{response.reserved_quantity} ks)
</div>
)}
<select
className="admin-form-select"
value={value ?? ""}
onChange={(e) => {
const batchId = Number(e.target.value);
const batch = batches.find((b) => b.id === batchId);
if (batch) onChange(batch.id, Number(batch.unit_price));
}}
>
<option value="">-- auto FIFO --</option>
{batches.map((b) => (
<option key={b.id} value={b.id}>
{new Date(b.received_at).toLocaleDateString("cs-CZ")} | {b.quantity}{" "}
ks | {Number(b.unit_price).toFixed(2)}
</option>
))}
</select>
</div>
);
}

View File

@@ -1,31 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import {
warehouseLocationListOptions,
type WarehouseLocation,
} from "../../lib/queries/warehouse";
interface LocationSelectProps {
value: number | null;
onChange: (locationId: number | null) => void;
}
export default function LocationSelect({
value,
onChange,
}: LocationSelectProps) {
const { data: locations } = useQuery(warehouseLocationListOptions());
return (
<select
className="admin-form-select"
value={value ?? ""}
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : null)}
>
<option value="">-- bez lokace --</option>
{locations?.map((loc: WarehouseLocation) => (
<option key={loc.id} value={loc.id}>
{loc.code} - {loc.name}
</option>
))}
</select>
);
}

View File

@@ -1,33 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import {
warehouseSupplierListOptions,
type WarehouseSupplier,
} from "../../lib/queries/warehouse";
interface SupplierSelectProps {
value: number | null;
onChange: (supplierId: number | null) => void;
}
export default function SupplierSelect({
value,
onChange,
}: SupplierSelectProps) {
const { data } = useQuery(warehouseSupplierListOptions({ perPage: 100 }));
const suppliers = data?.data ?? [];
return (
<select
className="admin-form-select"
value={value ?? ""}
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : null)}
>
<option value="">-- bez dodavatele --</option>
{suppliers.map((s: WarehouseSupplier) => (
<option key={s.id} value={s.id}>
{s.name}
{s.ico ? ` (${s.ico})` : ""}
</option>
))}
</select>
);
}

View File

@@ -1,249 +0,0 @@
import { ReactNode, useId, useRef } from "react";
import ItemPicker from "./ItemPicker";
import LocationSelect from "./LocationSelect";
import BatchPicker from "./BatchPicker";
export interface MovementItem {
key: string;
item_id: number | null;
item_name?: string;
quantity: number;
unit_price: number;
location_id: number | null;
batch_id: number | null;
reservation_id: number | null;
notes: string | null;
}
export type MovementRowRenderer<T> = (
item: T,
index: number,
updateItem: (field: string, value: unknown) => void,
removeItem: () => void,
) => ReactNode;
interface WarehouseMovementTableProps<T extends { key: string }> {
items: T[];
onChange: (items: T[]) => void;
mode: "receipt" | "issue" | "inventory";
defaultItem?: () => Omit<T, "key">;
renderRow?: MovementRowRenderer<T>;
renderHeader?: () => ReactNode;
}
function parseDecimal(raw: string): number {
// Strip leading minus if you want negative support; here we want only positive
const cleaned = raw.replace(/[eE+\-]/g, "");
const n = Number(cleaned);
return Number.isFinite(n) ? n : 0;
}
const builtInDefaultMovementItem = (): Omit<MovementItem, "key"> => ({
item_id: null,
quantity: 0,
unit_price: 0,
location_id: null,
batch_id: null,
reservation_id: null,
notes: null,
});
export default function WarehouseMovementTable<T extends { key: string }>({
items,
onChange,
mode,
defaultItem,
renderRow,
renderHeader,
}: WarehouseMovementTableProps<T>) {
const baseId = useId();
const counterRef = useRef(0);
const nextKey = () => `${baseId}-item-${counterRef.current++}`;
const addItem = () => {
const factory =
defaultItem ??
(builtInDefaultMovementItem as unknown as () => Omit<T, "key">);
onChange([...items, { ...(factory() as object), key: nextKey() } as T]);
};
const removeItem = (index: number) => {
onChange(items.filter((_, i) => i !== index));
};
const updateItem = (index: number, field: string, value: unknown) => {
const updated = [...items];
updated[index] = { ...updated[index], [field]: value };
onChange(updated);
};
// If a custom row renderer is supplied, the caller is fully in charge of
// the row layout (and typically the column headers too — see inventory mode).
if (renderRow) {
return (
<div>
<div className="admin-warehouse-movement-table">
<table className="admin-table">
{renderHeader && (
<thead>
<tr>{renderHeader()}</tr>
</thead>
)}
<tbody>
{items.map((item, index) => (
<tr key={item.key}>
{renderRow(
item,
index,
(field, value) => updateItem(index, field, value),
() => removeItem(index),
)}
</tr>
))}
</tbody>
</table>
</div>
<button
type="button"
className="admin-btn admin-btn-secondary"
onClick={addItem}
>
+ Přidat řádek
</button>
</div>
);
}
// Default rendering: only valid for "receipt" | "issue" — narrow at runtime.
const movementItems = items as unknown as MovementItem[];
return (
<div>
<div className="admin-warehouse-movement-table">
<table className="admin-table">
<thead>
<tr>
<th className="admin-warehouse-col-item">Položka</th>
{mode === "issue" && (
<th className="admin-warehouse-col-batch">Šarže</th>
)}
<th className="admin-warehouse-col-qty">Množství</th>
<th className="admin-warehouse-col-price">Cena/ks</th>
<th className="admin-warehouse-col-location">Lokace</th>
<th className="admin-warehouse-col-notes">Poznámka</th>
<th>Akce</th>
</tr>
</thead>
<tbody>
{movementItems.map((item, index) => (
<tr key={item.key}>
<td className="admin-warehouse-col-item">
<ItemPicker
value={item.item_id}
itemName={item.item_name}
onChange={(id) => updateItem(index, "item_id", id)}
/>
</td>
{mode === "issue" && (
<td className="admin-warehouse-col-batch">
<BatchPicker
itemId={item.item_id}
value={item.batch_id}
onChange={(batchId, unitPrice) => {
updateItem(index, "batch_id", batchId);
updateItem(index, "unit_price", unitPrice);
}}
/>
</td>
)}
<td className="admin-warehouse-col-qty">
<input
type="number"
className="admin-form-input"
value={item.quantity || ""}
onChange={(e) =>
updateItem(
index,
"quantity",
parseDecimal(e.target.value),
)
}
min="0"
step="0.001"
inputMode="decimal"
pattern="[0-9]+([\.,][0-9]+)?"
aria-label={`Množství, řádek ${index + 1}`}
/>
</td>
<td className="admin-warehouse-col-price">
<input
type="number"
className="admin-form-input"
value={item.unit_price || ""}
onChange={(e) =>
updateItem(
index,
"unit_price",
parseDecimal(e.target.value),
)
}
min="0"
step="0.01"
disabled={mode === "issue"}
inputMode="decimal"
pattern="[0-9]+([\.,][0-9]+)?"
aria-label={`Cena za kus, řádek ${index + 1}`}
/>
</td>
<td className="admin-warehouse-col-location">
<LocationSelect
value={item.location_id}
onChange={(id) => updateItem(index, "location_id", id)}
/>
</td>
<td className="admin-warehouse-col-notes">
<input
type="text"
className="admin-form-input"
value={item.notes ?? ""}
onChange={(e) => updateItem(index, "notes", e.target.value)}
/>
</td>
<td>
<div className="admin-table-actions">
<button
type="button"
className="admin-btn-icon danger"
onClick={() => removeItem(index)}
title="Odebrat řádek"
aria-label="Odebrat řádek"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<button
type="button"
className="admin-btn admin-btn-secondary"
onClick={addItem}
>
+ Přidat řádek
</button>
</div>
);
}