gendesign/frontend/src/components/analytics/RecommendForm.tsx

252 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useDistricts } from "@/lib/analytics-api";
import type { RecommendClass, RecommendMixInput } from "@/types/analytics";
const CLASSES: RecommendClass[] = ["Comfort", "Comfort+", "Business", "Elite"];
const MONTHS_OPTIONS = [12, 18, 24];
interface Props {
value: RecommendMixInput;
onChange: (next: RecommendMixInput) => void;
onSubmit: () => void;
isPending: boolean;
error: string | null;
}
export function RecommendForm({
value,
onChange,
onSubmit,
isPending,
error,
}: Props) {
const districts = useDistricts();
const districtOptions = (districts.data ?? []).filter(
(d) => d.district_name !== "не определён",
);
const valid = value.district_name.length >= 2;
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (valid && !isPending) onSubmit();
}}
style={{
background: "#fff",
border: "1px solid #e6e8ec",
borderRadius: 12,
padding: 20,
display: "flex",
flexDirection: "column",
gap: 12,
}}
>
<h2 style={{ margin: 0, fontSize: 18 }}>Параметры участка</h2>
<label>
<span style={labelStyle}>Район ЕКБ</span>
<select
value={value.district_name}
onChange={(e) =>
onChange({ ...value, district_name: e.target.value })
}
style={inputStyle}
required
>
<option value=""> выберите район </option>
{districtOptions.map((d) => (
<option key={d.district_name} value={d.district_name}>
{d.district_name} · {d.zk_count} ЖК ·{" "}
{d.flat_count?.toLocaleString("ru")} кв
</option>
))}
</select>
{value.district_name.length >= 2
? (() => {
const selected = districtOptions.find(
(d) => d.district_name === value.district_name,
);
if (!selected) return null;
return (
<span style={hintStyle}>
{selected.zk_count ?? 0} ЖК · {selected.cad_quarter_count} с
кад. кварталом
</span>
);
})()
: null}
</label>
<label>
<span style={labelStyle}>Площадь продаваемых м² (опц.)</span>
<input
type="number"
inputMode="numeric"
min={100}
max={500_000}
step={100}
placeholder="50000"
value={value.area_total_m2 ?? ""}
onChange={(e) =>
onChange({
...value,
area_total_m2:
e.target.value === "" ? null : Number(e.target.value),
})
}
style={inputStyle}
/>
<span style={hintStyle}>
Без этого поля посчитаем только распределение и цены, без
allocation/выручки.
</span>
</label>
<label>
<span style={labelStyle}>Класс ЖК (опц.)</span>
<select
value={value.target_class ?? ""}
onChange={(e) =>
onChange({
...value,
target_class:
e.target.value === ""
? null
: (e.target.value as RecommendClass),
})
}
style={inputStyle}
>
<option value=""> любой </option>
{CLASSES.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</label>
<label>
<span style={labelStyle}>
Окно сделок{" "}
<span
style={{
background: "#eff6ff",
color: "#1d4ed8",
padding: "1px 6px",
borderRadius: 4,
fontSize: 10,
fontWeight: 600,
textTransform: "none",
letterSpacing: 0,
marginLeft: 4,
}}
title="ЦБ менял ключевую ставку, льготная ипотека, программы — данные >2 лет нерелевантны для прогноза"
>
📊 Данные: последние 24 месяца
</span>
</span>
<select
value={value.months_window}
onChange={(e) =>
onChange({ ...value, months_window: Number(e.target.value) })
}
style={inputStyle}
>
{MONTHS_OPTIONS.map((m) => (
<option key={m} value={m}>
последние {m} мес
</option>
))}
</select>
<span style={hintStyle}>
При &lt;30 сделок в любом бакете окно расширяется до max 24 мес.
</span>
</label>
<label>
<span style={labelStyle}>Целевой срок реализации (опц.)</span>
<input
type="number"
inputMode="numeric"
min={3}
max={120}
step={1}
placeholder="например 18"
value={value.target_months ?? ""}
onChange={(e) =>
onChange({
...value,
target_months:
e.target.value === "" ? null : Number(e.target.value),
})
}
style={inputStyle}
/>
<span style={hintStyle}>
Если задан система предложит требуемый коэффициент к рынку.
</span>
</label>
<button
type="submit"
disabled={!valid || isPending}
style={primaryBtn(valid && !isPending)}
>
{isPending ? "Считаю…" : "Рассчитать"}
</button>
{error ? (
<div
style={{
padding: 10,
background: "#fef2f2",
border: "1px solid #fecaca",
borderRadius: 6,
color: "#991b1b",
fontSize: 13,
}}
>
{error}
</div>
) : null}
</form>
);
}
const labelStyle: React.CSSProperties = {
display: "block",
fontSize: 12,
color: "#5b6066",
marginBottom: 4,
textTransform: "uppercase",
letterSpacing: 0.4,
};
const inputStyle: React.CSSProperties = {
padding: "8px 10px",
border: "1px solid #d1d5db",
borderRadius: 6,
fontSize: 14,
width: "100%",
boxSizing: "border-box",
};
const hintStyle: React.CSSProperties = {
display: "block",
fontSize: 11,
color: "#73767e",
marginTop: 4,
};
const primaryBtn = (enabled: boolean): React.CSSProperties => ({
padding: "10px 18px",
background: enabled ? "#1d4ed8" : "#9ca3af",
color: "#fff",
border: "none",
borderRadius: 6,
fontSize: 14,
fontWeight: 600,
cursor: enabled ? "pointer" : "not-allowed",
marginTop: 4,
});