gendesign/frontend/src/components/site-finder/CustomPoiLayer.tsx
lekss361 6ecd7a9269 feat(#254,#114): custom POI UI + score breakdown stacked-bar viz
Task A (#254): Leaflet click-to-add custom POIs
- sessionId.ts: getOrCreateSessionId() from localStorage
- types/customPoi.ts: CustomPoi, CustomPoiCreate, CustomPoiUpdate
- hooks/useCustomPois.ts: useCustomPois / useAddCustomPoi / useUpdateCustomPoi / useDeleteCustomPoi (TanStack Query, X-Session-Id header)
- CustomPoiLayer.tsx: LayerGroup with CircleMarker (green/red/gray by weight), popup with edit/delete
- CustomPoiToggleButton.tsx: crosshair mode toggle button
- CustomPoiAddModal.tsx: modal form (name, 26 categories, weight -5..+5, notes)
- CustomPoiEditModal.tsx: same form for editing existing POI
- SiteMap.tsx: MapClickHandler (useMapEvents), integrated all custom POI components
- page.tsx: useCustomPois hook + passes customPois + parcelCad to SiteMap

Task B (#114): ScoreBreakdownStackedBar
- ScoreBreakdownStackedBar.tsx: horizontal stacked bar by category with positive/negative split, toggle between category and factor views, custom POIs highlighted orange with dashed outline
- OverviewTab.tsx: renders ScoreBreakdownStackedBar below ScoreBreakdownPanel

Requires backend PR with /api/v1/custom-pois endpoints merged before POI CRUD works.
2026-05-17 09:35:35 +03:00

125 lines
3.8 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 { CircleMarker, LayerGroup, Popup } from "react-leaflet";
import type { CustomPoi } from "@/types/customPoi";
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
interface Props {
pois: CustomPoi[];
onEdit: (poi: CustomPoi) => void;
onDelete: (id: number) => void;
}
// ---------------------------------------------------------------------------
// Color helpers
// ---------------------------------------------------------------------------
function markerColor(weight: number): string {
if (weight > 0) return "#16a34a"; // green — positive
if (weight < 0) return "#dc2626"; // red — negative
return "#6b7280"; // gray — neutral
}
function weightLabel(weight: number): string {
if (weight > 0) return `+${weight}`;
return String(weight);
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function CustomPoiLayer({ pois, onEdit, onDelete }: Props) {
if (!pois.length) return null;
return (
<LayerGroup>
{pois.map((poi) => {
const color = markerColor(poi.weight);
return (
<CircleMarker
key={poi.id}
center={[poi.lat, poi.lon]}
radius={9}
pathOptions={{
color,
fillColor: color,
fillOpacity: 0.85,
weight: 2.5,
dashArray: "4 2",
}}
>
<Popup>
<div style={{ fontSize: 13, lineHeight: 1.6, minWidth: 160 }}>
<div
style={{ fontWeight: 700, marginBottom: 2, color: "#111827" }}
>
{poi.name}
</div>
{poi.category && (
<div
style={{ fontSize: 11, color: "#6b7280", marginBottom: 4 }}
>
{poi.category}
</div>
)}
<div
style={{
fontWeight: 700,
color,
marginBottom: 6,
fontSize: 14,
}}
>
Вес: {weightLabel(poi.weight)}
</div>
{poi.notes && (
<div
style={{ fontSize: 12, color: "#374151", marginBottom: 8 }}
>
{poi.notes}
</div>
)}
<div style={{ display: "flex", gap: 8 }}>
<button
type="button"
onClick={() => onEdit(poi)}
style={{
padding: "4px 10px",
fontSize: 12,
borderRadius: 5,
border: "1px solid #d1d5db",
background: "#f9fafb",
cursor: "pointer",
color: "#374151",
}}
>
Изменить
</button>
<button
type="button"
onClick={() => onDelete(poi.id)}
style={{
padding: "4px 10px",
fontSize: 12,
borderRadius: 5,
border: "none",
background: "#fef2f2",
color: "#dc2626",
cursor: "pointer",
}}
>
Удалить
</button>
</div>
</div>
</Popup>
</CircleMarker>
);
})}
</LayerGroup>
);
}