Merge pull request 'feat(tradein): CRM-поля в форме оценки (#395)' (#409) from feat/tradein-crm-fields into main
This commit is contained in:
commit
4c51406fda
5 changed files with 101 additions and 0 deletions
|
|
@ -22,6 +22,11 @@ class TradeInEstimateInput(BaseModel):
|
|||
house_type: Literal["panel", "brick", "monolith", "monolith_brick", "other"] | None = None
|
||||
repair_state: Literal["needs_repair", "standard", "good", "excellent"] | None = None
|
||||
has_balcony: bool | None = None
|
||||
# CRM-поля (#395) — операционные, на расчёт оценки не влияют
|
||||
ownership_type: str | None = Field(default=None, max_length=100)
|
||||
has_mortgage: bool | None = None
|
||||
client_name: str | None = Field(default=None, max_length=200)
|
||||
client_phone: str | None = Field(default=None, max_length=50)
|
||||
|
||||
|
||||
class AnalogLot(BaseModel):
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ async def estimate_quality(
|
|||
id, address, lat, lon,
|
||||
area_m2, rooms, floor, total_floors,
|
||||
year_built, house_type, repair_state, has_balcony,
|
||||
ownership_type, has_mortgage, client_name, client_phone,
|
||||
median_price, range_low, range_high, median_price_per_m2,
|
||||
confidence, confidence_explanation, n_analogs,
|
||||
analogs, actual_deals,
|
||||
|
|
@ -205,6 +206,7 @@ async def estimate_quality(
|
|||
:address, :lat, :lon,
|
||||
:area, :rooms, :floor, :total_floors,
|
||||
:year_built, :house_type, :repair_state, :has_balcony,
|
||||
:ownership_type, :has_mortgage, :client_name, :client_phone,
|
||||
:median_price, :range_low, :range_high, :median_ppm2,
|
||||
:confidence, :explanation, :n_analogs,
|
||||
CAST(:analogs_json AS jsonb),
|
||||
|
|
@ -228,6 +230,10 @@ async def estimate_quality(
|
|||
"house_type": target_house_type,
|
||||
"repair_state": payload.repair_state,
|
||||
"has_balcony": payload.has_balcony,
|
||||
"ownership_type": payload.ownership_type,
|
||||
"has_mortgage": payload.has_mortgage,
|
||||
"client_name": payload.client_name,
|
||||
"client_phone": payload.client_phone,
|
||||
"median_price": median_price,
|
||||
"range_low": range_low,
|
||||
"range_high": range_high,
|
||||
|
|
|
|||
16
tradein-mvp/backend/data/sql/008_crm_fields.sql
Normal file
16
tradein-mvp/backend/data/sql/008_crm_fields.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- Trade-In MVP — CRM-поля оценки (#395).
|
||||
-- Операционные поля трейд-ин менеджера: тип собственности, ипотека/обременение,
|
||||
-- контакт клиента. На расчёт оценки не влияют — хранятся вместе с записью.
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE trade_in_estimates
|
||||
ADD COLUMN IF NOT EXISTS ownership_type text,
|
||||
ADD COLUMN IF NOT EXISTS has_mortgage boolean,
|
||||
ADD COLUMN IF NOT EXISTS client_name text,
|
||||
ADD COLUMN IF NOT EXISTS client_phone text;
|
||||
|
||||
COMMENT ON COLUMN trade_in_estimates.client_phone IS
|
||||
'#395 — контакт клиента (CRM). PII — доступ только у трейд-ин менеджера.';
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -25,6 +25,10 @@ interface FormState {
|
|||
house_type: string;
|
||||
repair_state: string;
|
||||
has_balcony: boolean;
|
||||
ownership_type: string;
|
||||
has_mortgage: boolean;
|
||||
client_name: string;
|
||||
client_phone: string;
|
||||
}
|
||||
|
||||
const INITIAL: FormState = {
|
||||
|
|
@ -37,6 +41,10 @@ const INITIAL: FormState = {
|
|||
house_type: "",
|
||||
repair_state: "",
|
||||
has_balcony: false,
|
||||
ownership_type: "",
|
||||
has_mortgage: false,
|
||||
client_name: "",
|
||||
client_phone: "",
|
||||
};
|
||||
|
||||
function validate(f: FormState): string | null {
|
||||
|
|
@ -69,6 +77,10 @@ function toPayload(f: FormState): TradeInEstimateInput {
|
|||
if (f.year_built !== "") out.year_built = Number(f.year_built);
|
||||
if (f.house_type) out.house_type = f.house_type as HouseType;
|
||||
if (f.repair_state) out.repair_state = f.repair_state as RepairState;
|
||||
if (f.ownership_type) out.ownership_type = f.ownership_type;
|
||||
if (f.has_mortgage) out.has_mortgage = true;
|
||||
if (f.client_name.trim()) out.client_name = f.client_name.trim();
|
||||
if (f.client_phone.trim()) out.client_phone = f.client_phone.trim();
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
@ -271,6 +283,63 @@ export function EstimateForm({ onSubmit, isPending, error }: Props) {
|
|||
</label>
|
||||
</div>
|
||||
|
||||
{/* CRM — для менеджера (#395) */}
|
||||
<div style={{ borderTop: "1px solid var(--border, #e5e5e5)", paddingTop: 14, marginTop: 4 }}>
|
||||
<div style={{ marginBottom: 10, fontSize: 11, fontWeight: 600, color: "var(--muted)", textTransform: "uppercase", letterSpacing: 0.4 }}>
|
||||
CRM — для менеджера
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="field-label" htmlFor="ownership">
|
||||
Тип собственности <span className="hint">опц.</span>
|
||||
</label>
|
||||
<select className="control" id="ownership" value={form.ownership_type} onChange={field("ownership_type")}>
|
||||
<option value="">— не указано —</option>
|
||||
<option value="индивидуальная">Индивидуальная</option>
|
||||
<option value="долевая">Долевая</option>
|
||||
<option value="совместная">Совместная</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="field-label" style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.has_mortgage}
|
||||
onChange={field("has_mortgage")}
|
||||
style={{ width: 14, height: 14, cursor: "pointer" }}
|
||||
/>
|
||||
<span style={{ color: "var(--fg)" }}>Ипотека / обременение</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="field-row">
|
||||
<div className="field">
|
||||
<label className="field-label" htmlFor="cname">
|
||||
Имя клиента <span className="hint">опц.</span>
|
||||
</label>
|
||||
<input
|
||||
className="control"
|
||||
id="cname"
|
||||
type="text"
|
||||
placeholder="Иван Петров"
|
||||
value={form.client_name}
|
||||
onChange={field("client_name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="field-label" htmlFor="cphone">
|
||||
Телефон <span className="hint">опц.</span>
|
||||
</label>
|
||||
<input
|
||||
className="control"
|
||||
id="cphone"
|
||||
type="tel"
|
||||
placeholder="+7 999 …"
|
||||
value={form.client_phone}
|
||||
onChange={field("client_phone")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{touched && valError && (
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ export interface TradeInEstimateInput {
|
|||
house_type?: HouseType;
|
||||
repair_state?: RepairState;
|
||||
has_balcony?: boolean;
|
||||
// CRM-поля (#395)
|
||||
ownership_type?: string;
|
||||
has_mortgage?: boolean;
|
||||
client_name?: string;
|
||||
client_phone?: string;
|
||||
}
|
||||
|
||||
export interface AnalogLot {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue