gendesign/frontend/src/lib/api/weightProfiles.ts
bot-backend 84a65d40dd
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m21s
Deploy / deploy (push) Successful in 1m13s
fix(site-finder): §4.1 «Применить» действительно применяет веса POI (#2790) (#2810)
2026-08-10 08:39:59 +00:00

151 lines
6.6 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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiFetch } from "@/lib/api";
// ── Types ─────────────────────────────────────────────────────────────────────
// Manually typed to match backend Pydantic schemas (sub-PR 3).
// After PR #138 merges run `npm run codegen` to sync with generated openapi types.
export interface WeightProfile {
id: number;
user_id: string;
profile_name: string;
weights: Record<string, number>;
is_default: boolean;
description: string | null;
created_at: string;
updated_at: string;
}
export interface WeightProfileCreate {
user_id: string;
profile_name: string;
weights: Record<string, number>;
is_default?: boolean;
description?: string | null;
}
// ── Constants ─────────────────────────────────────────────────────────────────
/**
* Владелец системных пресетов (Эконом / Комфорт / Бизнес) — mirrors
* `SYSTEM_USER_ID` в backend/app/services/site_finder/weight_profiles.py.
* Профили с этим user_id общие для всех и НЕ адресуемы через `profile_id`:
* `resolve_weights()` ищет профиль в области владельца, у чужого пользователя
* его не найдёт и молча вернёт системные веса с ответом `source="profile"`
* (#2782). Их веса уходят в analyze inline — см. WeightProfilePanel.
*/
export const SYSTEM_PROFILE_USER_ID = "__system__";
// ALLOWED_CATEGORIES — mirrors backend weight_profiles.py ALLOWED_CATEGORIES.
// Keep in sync with backend; source of truth is `_POI_WEIGHTS` in parcels.py.
export const POI_CATEGORIES = [
"school",
"kindergarten",
"pharmacy",
"hospital",
"shop_mall",
"shop_supermarket",
"shop_small",
"park",
"bus_stop",
"metro_stop",
"tram_stop",
] as const;
export type PoiCategoryKey = (typeof POI_CATEGORIES)[number];
// System defaults — mirrors _POI_WEIGHTS in backend/app/api/v1/parcels.py.
export const POI_DEFAULT_WEIGHTS: Record<PoiCategoryKey, number> = {
school: 1.5,
kindergarten: 1.5,
pharmacy: 0.8,
hospital: 0.6,
shop_mall: 1.2,
shop_supermarket: 1.0,
shop_small: 0.5,
park: 1.8,
bus_stop: 0.3,
metro_stop: 1.5,
tram_stop: -0.5,
};
export const POI_LABELS: Record<PoiCategoryKey, string> = {
school: "Школы",
kindergarten: "Детсады",
pharmacy: "Аптеки",
hospital: "Больницы",
shop_mall: "ТРЦ / Молы",
shop_supermarket: "Супермаркеты",
shop_small: "Магазины у дома",
park: "Парки",
bus_stop: "Автобусные ост.",
metro_stop: "Метро",
tram_stop: "Трамвайные ост. ()",
};
// Weight range bounds — mirrors MIN_WEIGHT / MAX_WEIGHT in backend.
export const POI_WEIGHT_MIN = -2;
export const POI_WEIGHT_MAX = 3;
// ── Helpers ───────────────────────────────────────────────────────────────────
const BASE_PATH = "/api/v1/admin/site-finder/weight-profiles";
// Никакого X-Admin-Token: сервер перестал его читать в #437, а последний
// `verify_admin_token` удалён в #2775. Заголовок отправлялся ещё год после этого и
// ничего не решал — проверено живым запросом на проде (#2782): один и тот же 200
// с корректным токеном, с мусорным и без заголовка вовсе.
//
// Реальная защита `/api/v1/admin/*` — два живых слоя, оба прод-проверены:
// 1) Caddy basic_auth на gendsgn.ru → без валидных кред 401 ещё на периметре
// (подставленный клиентом X-Authenticated-User туда же не проходит);
// 2) app/main.py rbac_guard → `role != "admin"` даёт 403 "admin only".
// ── Hooks ─────────────────────────────────────────────────────────────────────
/**
* Профили пользователя + системные пресеты (#2790).
*
* `include_system=true` домешивает в конец списка три общих пресета (Эконом /
* Комфорт / Бизнес, засеяны `data/sql/100_user_weight_profiles_default_seed.sql`).
* Без него у пользователя без своих профилей дропдаун пустой — пресеты лежали в
* проде с 16.05.2026 и не были видны никому.
*/
export function useWeightProfiles(userId: string) {
return useQuery<WeightProfile[]>({
queryKey: ["weight-profiles", userId],
queryFn: () =>
apiFetch<WeightProfile[]>(
`${BASE_PATH}?user_id=${encodeURIComponent(userId)}&include_system=true`,
),
enabled: !!userId,
});
}
/** Create a new weight profile. Invalidates the list query on success. */
export function useCreateProfile() {
const qc = useQueryClient();
return useMutation<WeightProfile, Error, WeightProfileCreate>({
mutationFn: (payload) =>
apiFetch<WeightProfile>(BASE_PATH, {
method: "POST",
body: JSON.stringify(payload),
}),
onSuccess: (_, variables) => {
void qc.invalidateQueries({
queryKey: ["weight-profiles", variables.user_id],
});
},
});
}
// useUpdateProfile / useDeleteProfile здесь больше нет (#2790 п.3). Их не звали
// ниоткуда: в UI есть список и создание, кнопок «переименовать» / «удалить» нет.
// Спрос за 3 месяца по проду: 1 профиль на всю базу (`admin`, создан 15.05.2026,
// updated_at = created_at) + 3 системных пресета — ни одного изменения и ни
// одной попытки удаления. PUT/DELETE-эндпоинты живы и покрыты тестами бэкенда;
// понадобится UI — хуки вернутся из истории (мертвее они там не станут).