"use client"; import { type CSSProperties } from "react"; import { Entity } from "@/api/entities"; import { Feature } from "@/lib/useEditorState"; import { EntityGeometryPreset, EntityTypeGroupId, EntityTypeOption, findEntityTypeOption, groupEntityTypeOptions, } from "@/lib/entityTypeOptions"; type EntityFormState = { name: string; slug: string; type_id: string; }; type GeometryMetaFormState = { time_start: string; time_end: string; binding: string; }; type Props = { selectedFeature: Feature | null; selectedFeatureEntitySummary: string; selectedFeatureBindingSummary: string; entities: Entity[]; selectedGeometryEntityIds: string[]; onEntityIdsChange: (values: string[]) => void; entitySearchQuery: string; onEntitySearchQueryChange: (value: string) => void; entitySearchResults: Entity[]; selectedSearchEntityId: string | null; onSelectSearchEntityId: (value: string | null) => void; onAddSelectedSearchEntity: () => void; isEntitySearchLoading: boolean; entityForm: EntityFormState; onEntityFormChange: (key: keyof EntityFormState, value: string) => void; entityTypeOptions: EntityTypeOption[]; geometryMetaForm: GeometryMetaFormState; onGeometryMetaFormChange: (key: keyof GeometryMetaFormState, value: string) => void; bindingGeometrySearchQuery: string; onBindingGeometrySearchQueryChange: (value: string) => void; bindingGeometrySearchResults: Array<{ id: string; label: string; }>; selectedBindingGeometryId: string | null; onSelectBindingGeometryId: (value: string | null) => void; onAddSelectedBindingGeometry: () => void; isEntitySubmitting: boolean; onCreateEntityOnly: () => void; onApplyEntitiesForSelectedGeometry: () => void; changeCount: number; entityFormStatus: string | null; }; export default function SelectedGeometryPanel({ selectedFeature, selectedFeatureEntitySummary, selectedFeatureBindingSummary, entities, selectedGeometryEntityIds, onEntityIdsChange, entitySearchQuery, onEntitySearchQueryChange, entitySearchResults, selectedSearchEntityId, onSelectSearchEntityId, onAddSelectedSearchEntity, isEntitySearchLoading, entityForm, onEntityFormChange, entityTypeOptions, geometryMetaForm, onGeometryMetaFormChange, bindingGeometrySearchQuery, onBindingGeometrySearchQueryChange, bindingGeometrySearchResults, selectedBindingGeometryId, onSelectBindingGeometryId, onAddSelectedBindingGeometry, isEntitySubmitting, onCreateEntityOnly, onApplyEntitiesForSelectedGeometry, changeCount, entityFormStatus, }: Props) { const groupedEntityTypeOptions = groupEntityTypeOptions(entityTypeOptions); const featureGeometryPreset = selectedFeature ? resolveFeatureGeometryPreset(selectedFeature) : null; const allowedGroupIds = featureGeometryPreset ? getAllowedGroupIdsForPreset(featureGeometryPreset) : []; const visibleGroupedEntityTypeOptions = groupedEntityTypeOptions.filter((group) => allowedGroupIds.includes(group.id) ); const groupedEntityTypeOptionsForCreate = selectedFeature ? visibleGroupedEntityTypeOptions : groupedEntityTypeOptions; const selectedTypeOption = findEntityTypeOption(entityForm.type_id); const hasCurrentVisibleTypeOption = groupedEntityTypeOptionsForCreate.some((group) => group.options.some((option) => option.value === entityForm.type_id) ); return (
Entity & Geometry
{!selectedFeature ? (
Chưa chọn geometry. Tạo entity mới ở khối bên dưới, hoặc vào mode Select để bind entity cho geometry.
) : (
ID: {String(selectedFeature.properties.id)}
Entities hiện tại: {selectedFeatureEntitySummary}
Binding hiện tại: {selectedFeatureBindingSummary}
Geometry preset: {formatGeometryPresetLabel(featureGeometryPreset)}
Entities đã chọn:
{selectedGeometryEntityIds.length ? (
{selectedGeometryEntityIds.map((entityId) => { const entity = entities.find((item) => item.id === entityId) || null; const label = entity?.name ? `${entity.name} (${entityId})` : entityId; return (
{label}
); })}
) : (
Chưa có entity nào được gắn.
)}
Geometry phải có ít nhất 1 entity để Save.
Metadata geometry (chỉ áp dụng khi bind entity)
`time_start`, `time_end`, `binding` chỉ được áp dụng khi bấm nút bind entity cho geometry.
onGeometryMetaFormChange("time_start", event.target.value)} placeholder="time_start" disabled={isEntitySubmitting} style={entityInputStyle} /> onGeometryMetaFormChange("time_end", event.target.value)} placeholder="time_end" disabled={isEntitySubmitting} style={entityInputStyle} /> onGeometryMetaFormChange("binding", event.target.value)} placeholder="binding ids (vd: geo-id-1, geo-id-2)" disabled={isEntitySubmitting} style={entityInputStyle} /> onBindingGeometrySearchQueryChange(event.target.value) } placeholder="Search geometry để thêm vào binding..." disabled={isEntitySubmitting} style={entityInputStyle} />
Bind entity có sẵn
Dùng khi entity đã tồn tại. Tìm kiếm, thêm vào danh sách rồi bấm nút áp dụng.
onEntitySearchQueryChange(event.target.value)} placeholder="Search entity theo name..." disabled={isEntitySubmitting} style={entityInputStyle} /> {isEntitySearchLoading ? (
Đang tìm entity...
) : null}
{changeCount > 0 ? (
Geometry mới sẽ lưu entity khi bấm Save.
) : null}
)}
Tạo entity mới (độc lập)
Chỉ tạo entity, không tự bind vào geometry.
{selectedFeature ? (
Type đang bị giới hạn theo geometry: {formatGeometryPresetLabel(featureGeometryPreset)}.
) : null} onEntityFormChange("name", event.target.value)} placeholder="Tên entity mới" disabled={isEntitySubmitting} style={entityInputStyle} /> onEntityFormChange("slug", event.target.value)} placeholder="Slug" disabled={isEntitySubmitting} style={entityInputStyle} />
Chọn loại entity
{selectedTypeOption ? (
Type đang chọn: {selectedTypeOption.label} ({selectedTypeOption.groupLabel})
) : entityForm.type_id ? (
Type đang chọn: {entityForm.type_id}
) : null}
{entityFormStatus ? (
{entityFormStatus}
) : null}
); } const entityInputStyle: CSSProperties = { width: "100%", borderRadius: "6px", border: "1px solid #334155", background: "#111827", color: "#f8fafc", padding: "6px 8px", fontSize: "13px", }; const removeButtonStyle: CSSProperties = { border: "none", borderRadius: "6px", padding: "4px 8px", cursor: "pointer", background: "#7f1d1d", color: "#ffffff", fontSize: "12px", }; const secondaryActionButtonStyle: CSSProperties = { border: "none", borderRadius: "6px", padding: "7px 8px", cursor: "pointer", background: "#1d4ed8", color: "#ffffff", }; function resolveFeatureGeometryPreset(feature: Feature): EntityGeometryPreset { const explicitPreset = normalizeGeometryPreset(feature.properties.geometry_preset); if (explicitPreset) return explicitPreset; const semanticType = normalizeTypeId(feature.properties.type) || normalizeTypeId(feature.properties.entity_type_id); if (semanticType) { const option = findEntityTypeOption(semanticType); if (option) return option.geometryPreset; } return mapGeometryTypeToPreset(feature.geometry.type); } function normalizeGeometryPreset(value: unknown): EntityGeometryPreset | null { if (typeof value !== "string") return null; const normalized = value.trim().toLowerCase(); if ( normalized === "point" || normalized === "line" || normalized === "polygon" || normalized === "circle-area" ) { return normalized; } return null; } function normalizeTypeId(value: unknown): string | null { if (typeof value !== "string") return null; const normalized = value.trim().toLowerCase(); return normalized.length ? normalized : null; } function mapGeometryTypeToPreset( geometryType: Feature["geometry"]["type"] ): EntityGeometryPreset { if (geometryType === "Point" || geometryType === "MultiPoint") { return "point"; } if (geometryType === "LineString" || geometryType === "MultiLineString") { return "line"; } return "polygon"; } function getAllowedGroupIdsForPreset( geometryPreset: EntityGeometryPreset ): EntityTypeGroupId[] { if (geometryPreset === "point") { return ["point"]; } if (geometryPreset === "line") { return ["line"]; } if (geometryPreset === "circle-area") { return ["circle"]; } return ["polygon"]; } function formatGeometryPresetLabel(preset: EntityGeometryPreset | null): string { if (preset === "point") return "point - Điểm"; if (preset === "line") return "line - Tuyến"; if (preset === "circle-area") return "circle - Tròn"; if (preset === "polygon") return "polygon - Đa giác"; return "unknown"; }