"use client"; import { type CSSProperties } from "react"; import { Entity } from "@/api/entities"; import { Feature } from "@/lib/useEditorState"; import { 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; isEntitySubmitting: boolean; onCreateEntityAndAttach: () => 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, isEntitySubmitting, onCreateEntityAndAttach, onApplyEntitiesForSelectedGeometry, changeCount, entityFormStatus, }: Props) { const groupedEntityTypeOptions = groupEntityTypeOptions(entityTypeOptions); const allowedGroupIds = selectedFeature ? getAllowedGroupIdsForGeometry(selectedFeature.geometry.type) : []; const visibleGroupedEntityTypeOptions = groupedEntityTypeOptions.filter((group) => allowedGroupIds.includes(group.id) ); const selectedTypeOption = findEntityTypeOption(entityForm.type_id); const hasCurrentTypeOption = entityTypeOptions.some((option) => option.value === entityForm.type_id); const geometryBucket = selectedFeature ? toGeometryBucket(selectedFeature.geometry.type) : null; const selectedTypeBucket = selectedTypeOption ? toTypeBucket(selectedTypeOption) : null; const isGeometryCompatible = geometryBucket && selectedTypeBucket ? geometryBucket === selectedTypeBucket : true; return (
Selected Geometry
{!selectedFeature ? (
Vào mode Select và chọn 1 geometry để điền entity.
) : (
ID: {String(selectedFeature.properties.id)}
Entities hiện tại: {selectedFeatureEntitySummary}
Binding hiện tại: {selectedFeatureBindingSummary}
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.
)} onEntitySearchQueryChange(event.target.value)} placeholder="Search entity theo name..." disabled={isEntitySubmitting} style={entityInputStyle} /> {isEntitySearchLoading ? (
Đang tìm entity...
) : null}
Geometry phải có ít nhất 1 entity để Save.
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} />
Entity type presets
Chọn nhanh theo nhóm hình học bạn cần vẽ.
{visibleGroupedEntityTypeOptions.map((group) => { const color = GROUP_COLORS[group.id]; return (
{group.label} {group.geometryLabel}
{group.options.map((option) => { const active = option.value === entityForm.type_id; return ( ); })}
); })}
{selectedTypeOption ? (
Type đang chọn: {selectedTypeOption.label} ({selectedTypeOption.groupLabel})
) : entityForm.type_id ? (
Type đang chọn: {entityForm.type_id}
) : null} {!isGeometryCompatible && selectedTypeOption ? (
Type {selectedTypeOption.label} hợp với {selectedTypeBucket}, nhưng geometry hiện tại là {geometryBucket}.
) : null} 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} /> {changeCount > 0 ? (
Geometry mới sẽ lưu entity khi bấm Save.
) : 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", }; const GROUP_COLORS: Record< EntityTypeGroupId, { background: string; border: string; text: string; mutedText: string; chipBackground: string; chipBorder: string; activeBackground: string; activeBorder: string; activeText: string; } > = { split: { background: "#3f1d24", border: "#7f1d1d", text: "#fecaca", mutedText: "#fda4af", chipBackground: "#4c1d27", chipBorder: "#7f1d1d", activeBackground: "#7f1d1d", activeBorder: "#fecaca", activeText: "#ffffff", }, route: { background: "#082f49", border: "#0c4a6e", text: "#bae6fd", mutedText: "#7dd3fc", chipBackground: "#0c4a6e", chipBorder: "#155e75", activeBackground: "#0369a1", activeBorder: "#bae6fd", activeText: "#f0f9ff", }, area_polygon: { background: "#1f3a2a", border: "#166534", text: "#bbf7d0", mutedText: "#86efac", chipBackground: "#14532d", chipBorder: "#166534", activeBackground: "#15803d", activeBorder: "#bbf7d0", activeText: "#f0fdf4", }, area_circle: { background: "#3f2b05", border: "#92400e", text: "#fde68a", mutedText: "#fcd34d", chipBackground: "#78350f", chipBorder: "#92400e", activeBackground: "#b45309", activeBorder: "#fde68a", activeText: "#fffbeb", }, point: { background: "#2e1065", border: "#6d28d9", text: "#ddd6fe", mutedText: "#c4b5fd", chipBackground: "#4c1d95", chipBorder: "#6d28d9", activeBackground: "#7c3aed", activeBorder: "#ddd6fe", activeText: "#f5f3ff", }, }; type GeometryBucket = "point" | "line" | "polygon"; function toGeometryBucket(geometryType: Feature["geometry"]["type"]): GeometryBucket { if (geometryType === "Point" || geometryType === "MultiPoint") { return "point"; } if (geometryType === "LineString" || geometryType === "MultiLineString") { return "line"; } return "polygon"; } function toTypeBucket(option: EntityTypeOption): GeometryBucket { if (option.geometryPreset === "point") { return "point"; } if (option.geometryPreset === "line") { return "line"; } return "polygon"; } function getAllowedGroupIdsForGeometry( geometryType: Feature["geometry"]["type"] ): EntityTypeGroupId[] { if (geometryType === "Point" || geometryType === "MultiPoint") { return ["point"]; } if (geometryType === "LineString" || geometryType === "MultiLineString") { return ["split", "route"]; } return ["area_polygon", "area_circle"]; }