gendesign/frontend/src/components/concept/ConceptParamsForm.tsx
Light1YT 3bbbf25412
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m45s
Deploy / deploy (push) Successful in 1m3s
feat(concept): concept-компоненты (DrawMap/ParamsForm/VariantsResult/ResultMap/ExportButtons) (#57)
Дополняет UI: 5 компонентов которые импортит page.tsx (Leaflet draw, форма ConceptInput,
табы вариантов, placement-карта, экспорт). Не вошли в предыдущий commit (untracked-dir).
ConceptResultMap: Leaflet stroke #D1D5DB (CSS var не резолвится в SVG).
2026-06-13 16:42:19 +00:00

232 lines
6.2 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";
/**
* ConceptParamsForm — controlled form for the ConceptInput parameters
* (housing_class / target_floors / development_type / land_cost_rub). The
* parcel polygon is collected separately by the map; this form owns only the
* scalar fields and the submit CTA. Submit is disabled until the parent
* confirms a polygon is present (`hasPolygon`).
*/
import { Sparkles } from "lucide-react";
import {
DEVELOPMENT_TYPE_LABELS,
HOUSING_CLASS_LABELS,
type DevelopmentType,
type HousingClass,
} from "@/lib/concept-api";
export interface ConceptParams {
housing_class: HousingClass;
target_floors: number;
development_type: DevelopmentType;
land_cost_rub: number | null;
}
export const DEFAULT_PARAMS: ConceptParams = {
housing_class: "comfort",
target_floors: 9,
development_type: "mid_rise",
land_cost_rub: null,
};
const fieldLabelStyle: React.CSSProperties = {
display: "block",
fontSize: 12,
fontWeight: 500,
textTransform: "uppercase",
letterSpacing: "0.04em",
color: "var(--fg-secondary)",
marginBottom: 6,
};
const controlStyle: React.CSSProperties = {
width: "100%",
padding: "8px 12px",
fontSize: 14,
border: "1px solid var(--border-strong)",
borderRadius: 8,
outline: "none",
background: "var(--bg-card)",
color: "var(--fg-primary)",
};
interface Props {
params: ConceptParams;
onChange: (params: ConceptParams) => void;
onSubmit: () => void;
hasPolygon: boolean;
isPending: boolean;
}
export function ConceptParamsForm({
params,
onChange,
onSubmit,
hasPolygon,
isPending,
}: Props) {
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!hasPolygon || isPending) return;
onSubmit();
}
const disabled = !hasPolygon || isPending;
return (
<form onSubmit={handleSubmit} style={{ display: "grid", gap: 16 }}>
<div>
<label htmlFor="housing-class" style={fieldLabelStyle}>
Класс жилья
</label>
<select
id="housing-class"
value={params.housing_class}
onChange={(e) =>
onChange({
...params,
housing_class: e.target.value as HousingClass,
})
}
style={controlStyle}
>
{(Object.keys(HOUSING_CLASS_LABELS) as HousingClass[]).map((key) => (
<option key={key} value={key}>
{HOUSING_CLASS_LABELS[key]}
</option>
))}
</select>
</div>
<div>
<label htmlFor="dev-type" style={fieldLabelStyle}>
Тип застройки
</label>
<select
id="dev-type"
value={params.development_type}
onChange={(e) =>
onChange({
...params,
development_type: e.target.value as DevelopmentType,
})
}
style={controlStyle}
>
{(Object.keys(DEVELOPMENT_TYPE_LABELS) as DevelopmentType[]).map(
(key) => (
<option key={key} value={key}>
{DEVELOPMENT_TYPE_LABELS[key]}
</option>
),
)}
</select>
</div>
<div>
<label htmlFor="target-floors" style={fieldLabelStyle}>
Целевая этажность
</label>
<input
id="target-floors"
type="number"
min={1}
max={30}
value={params.target_floors}
onChange={(e) => {
const raw = Number(e.target.value);
const clamped = Number.isFinite(raw)
? Math.min(30, Math.max(1, Math.round(raw)))
: 1;
onChange({ ...params, target_floors: clamped });
}}
style={controlStyle}
/>
<p
style={{
margin: "4px 0 0",
fontSize: 11,
color: "var(--fg-tertiary)",
}}
>
От 1 до 30 этажей.
</p>
</div>
<div>
<label htmlFor="land-cost" style={fieldLabelStyle}>
Стоимость участка,
</label>
<input
id="land-cost"
type="number"
min={0}
step={1_000_000}
value={params.land_cost_rub ?? ""}
placeholder="Необязательно"
onChange={(e) => {
const raw = e.target.value.trim();
const value = raw === "" ? null : Number(raw);
onChange({
...params,
land_cost_rub:
value != null && Number.isFinite(value) && value >= 0
? value
: null,
});
}}
style={controlStyle}
/>
<p
style={{
margin: "4px 0 0",
fontSize: 11,
color: "var(--fg-tertiary)",
}}
>
Учитывается в финансовой модели как часть затрат.
</p>
</div>
<button
type="submit"
disabled={disabled}
aria-label="Рассчитать концепции застройки"
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
padding: "10px 18px",
height: 44,
background: disabled ? "var(--bg-card-alt)" : "var(--accent)",
color: disabled ? "var(--fg-tertiary)" : "#fff",
border: disabled ? "1px solid var(--border-card)" : "none",
borderRadius: 8,
fontSize: 14,
fontWeight: 600,
cursor: disabled ? "not-allowed" : "pointer",
transition: "opacity 150ms",
}}
>
<Sparkles size={16} strokeWidth={1.5} />
{isPending ? "Расчёт концепций…" : "Рассчитать концепции"}
</button>
{!hasPolygon && (
<p
style={{
margin: 0,
fontSize: 12,
color: "var(--fg-tertiary)",
}}
>
Нарисуйте полигон участка или укажите кадастровый номер, чтобы начать
расчёт.
</p>
)}
</form>
);
}