New /api/admin/ares proxy (browser cannot reach ares.gov.cz - CORS+CSP): GET /ico/:ico and GET /search?q= map the MFCR REST API to the form shape (street incl. orientation numbers, "511 01" PSC format, DIC). Guarded by customers.create/edit + warehouse.manage; token errors map to Czech messages. Shared AresAdornment button sits in the Nazev and ICO fields of both modals: ICO mode fills directly, name mode searches with a result picker. Tested against live ARES (BOHA, ICO 22599851). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
import { useState, useRef } from "react";
|
|
import InputAdornment from "@mui/material/InputAdornment";
|
|
import IconButton from "@mui/material/IconButton";
|
|
import Tooltip from "@mui/material/Tooltip";
|
|
import Menu from "@mui/material/Menu";
|
|
import MenuItem from "@mui/material/MenuItem";
|
|
import Typography from "@mui/material/Typography";
|
|
import Box from "@mui/material/Box";
|
|
import CircularProgress from "@mui/material/CircularProgress";
|
|
import { jsonQuery } from "../lib/apiAdapter";
|
|
import { useAlert } from "../context/AlertContext";
|
|
|
|
/** Normalized company record returned by the /api/admin/ares proxy. */
|
|
export interface AresCompany {
|
|
ico: string;
|
|
dic: string | null;
|
|
name: string;
|
|
street: string;
|
|
city: string;
|
|
postal_code: string;
|
|
country: string;
|
|
}
|
|
|
|
interface AresAdornmentProps {
|
|
/** "ico" looks the query up directly; "name" searches and offers a picker. */
|
|
mode: "ico" | "name";
|
|
/** Current value of the host field (IČO or name fragment). */
|
|
query: string;
|
|
/** Called with the chosen company — the modal prefills its form from it. */
|
|
onFill: (company: AresCompany) => void;
|
|
}
|
|
|
|
/**
|
|
* "ARES" end-adornment button for the Název/IČO fields of the customer and
|
|
* supplier modals. IČO mode fetches the subject directly; name mode searches
|
|
* ARES and shows a result picker (filling straight away on a single hit).
|
|
* Pass via the TextField's `InputProps.endAdornment`.
|
|
*/
|
|
export default function AresAdornment({
|
|
mode,
|
|
query,
|
|
onFill,
|
|
}: AresAdornmentProps) {
|
|
const alert = useAlert();
|
|
const [loading, setLoading] = useState(false);
|
|
const [results, setResults] = useState<AresCompany[]>([]);
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const anchorRef = useRef<HTMLButtonElement | null>(null);
|
|
|
|
const trimmed = query.trim();
|
|
const enabled =
|
|
mode === "ico"
|
|
? /^\d{8}$/.test(trimmed.replace(/\s+/g, ""))
|
|
: trimmed.length >= 2;
|
|
const tooltip =
|
|
mode === "ico"
|
|
? enabled
|
|
? "Načíst údaje z ARES podle IČO"
|
|
: "Vyplňte 8místné IČO"
|
|
: enabled
|
|
? "Vyhledat firmu v ARES podle názvu"
|
|
: "Vyplňte alespoň 2 znaky názvu";
|
|
|
|
const lookup = async () => {
|
|
if (!enabled || loading) return;
|
|
setLoading(true);
|
|
try {
|
|
if (mode === "ico") {
|
|
const company = await jsonQuery<AresCompany>(
|
|
`/api/admin/ares/ico/${encodeURIComponent(trimmed.replace(/\s+/g, ""))}`,
|
|
);
|
|
onFill(company);
|
|
} else {
|
|
const found = await jsonQuery<AresCompany[]>(
|
|
`/api/admin/ares/search?q=${encodeURIComponent(trimmed)}`,
|
|
);
|
|
if (found.length === 0) {
|
|
alert.error("Žádná firma tohoto názvu nebyla v ARES nalezena");
|
|
} else if (found.length === 1) {
|
|
onFill(found[0]);
|
|
} else {
|
|
setResults(found);
|
|
setMenuOpen(true);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
alert.error(
|
|
e instanceof Error ? e.message : "Registr ARES je nedostupný",
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<InputAdornment position="end">
|
|
<Tooltip title={tooltip}>
|
|
<span>
|
|
<IconButton
|
|
ref={anchorRef}
|
|
size="small"
|
|
onClick={lookup}
|
|
disabled={!enabled || loading}
|
|
aria-label="Načíst z ARES"
|
|
edge="end"
|
|
>
|
|
{loading ? (
|
|
<CircularProgress size={16} color="inherit" />
|
|
) : (
|
|
<Typography
|
|
component="span"
|
|
sx={{
|
|
fontSize: "0.65rem",
|
|
fontWeight: 800,
|
|
letterSpacing: "0.06em",
|
|
color: enabled ? "primary.main" : "text.disabled",
|
|
lineHeight: 1,
|
|
}}
|
|
>
|
|
ARES
|
|
</Typography>
|
|
)}
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
<Menu
|
|
anchorEl={anchorRef.current}
|
|
open={menuOpen}
|
|
onClose={() => setMenuOpen(false)}
|
|
>
|
|
{results.map((c) => (
|
|
<MenuItem
|
|
key={c.ico}
|
|
onClick={() => {
|
|
setMenuOpen(false);
|
|
onFill(c);
|
|
}}
|
|
>
|
|
<Box>
|
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
|
{c.name}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
IČO {c.ico}
|
|
{c.city ? ` · ${c.city}` : ""}
|
|
</Typography>
|
|
</Box>
|
|
</MenuItem>
|
|
))}
|
|
</Menu>
|
|
</InputAdornment>
|
|
);
|
|
}
|