@collabdt/core 0.0.57 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +29 -43
  2. package/dist/core/components/AppSidebarContent.d.ts.map +1 -1
  3. package/dist/core/components/AppSidebarContent.js +21 -27
  4. package/dist/core/components/AppSidebarContent.js.map +1 -1
  5. package/dist/core/components/authentication/AuthPage.d.ts.map +1 -1
  6. package/dist/core/components/authentication/AuthPage.js +32 -7
  7. package/dist/core/components/authentication/AuthPage.js.map +1 -1
  8. package/dist/core/components/authentication/SignInAuthHeader.d.ts +7 -0
  9. package/dist/core/components/authentication/SignInAuthHeader.d.ts.map +1 -0
  10. package/dist/core/components/authentication/SignInAuthHeader.js +32 -0
  11. package/dist/core/components/authentication/SignInAuthHeader.js.map +1 -0
  12. package/dist/core/components/authentication/Signin.d.ts.map +1 -1
  13. package/dist/core/components/authentication/Signin.js +308 -10
  14. package/dist/core/components/authentication/Signin.js.map +1 -1
  15. package/dist/core/components/settings/index.js +3 -3
  16. package/dist/core/components/settings/index.js.map +1 -1
  17. package/dist/core/components/settings/src/OrganizationSettingsPanel.d.ts.map +1 -1
  18. package/dist/core/components/settings/src/OrganizationSettingsPanel.js +5 -4
  19. package/dist/core/components/settings/src/OrganizationSettingsPanel.js.map +1 -1
  20. package/dist/core/components/settings/src/SettingsSidebar.d.ts.map +1 -1
  21. package/dist/core/components/settings/src/SettingsSidebar.js +12 -2
  22. package/dist/core/components/settings/src/SettingsSidebar.js.map +1 -1
  23. package/dist/core/components/support/BugReportDialog.d.ts.map +1 -1
  24. package/dist/core/components/support/BugReportDialog.js +122 -9
  25. package/dist/core/components/support/BugReportDialog.js.map +1 -1
  26. package/dist/core/components/support/FeatureRequestDialog.d.ts.map +1 -1
  27. package/dist/core/components/support/FeatureRequestDialog.js +61 -2
  28. package/dist/core/components/support/FeatureRequestDialog.js.map +1 -1
  29. package/dist/core/components/ui/AppSidebar.d.ts.map +1 -1
  30. package/dist/core/components/ui/AppSidebar.js +52 -9
  31. package/dist/core/components/ui/AppSidebar.js.map +1 -1
  32. package/dist/core/components/ui/DataTable.js +3 -3
  33. package/dist/core/components/ui/DataTable.js.map +1 -1
  34. package/dist/core/components/ui/Dialog.js +1 -1
  35. package/dist/core/components/ui/Dialog.js.map +1 -1
  36. package/dist/core/components/ui/InfoSidebar/Container.js +1 -1
  37. package/dist/core/components/ui/InfoSidebar/Container.js.map +1 -1
  38. package/dist/core/components/ui/Sensors/CollapsibleSensorItem.d.ts.map +1 -1
  39. package/dist/core/components/ui/Sensors/CollapsibleSensorItem.js +2 -2
  40. package/dist/core/components/ui/Sensors/CollapsibleSensorItem.js.map +1 -1
  41. package/dist/core/components/ui/Sensors/SensorsSection.d.ts.map +1 -1
  42. package/dist/core/components/ui/Sensors/SensorsSection.js +2 -1
  43. package/dist/core/components/ui/Sensors/SensorsSection.js.map +1 -1
  44. package/dist/core/components/ui/Sensors/sensorUtils.d.ts +9 -0
  45. package/dist/core/components/ui/Sensors/sensorUtils.d.ts.map +1 -1
  46. package/dist/core/components/ui/Sensors/sensorUtils.js +6 -0
  47. package/dist/core/components/ui/Sensors/sensorUtils.js.map +1 -1
  48. package/dist/core/components/ui/Sidebar.d.ts +4 -0
  49. package/dist/core/components/ui/Sidebar.d.ts.map +1 -1
  50. package/dist/core/components/ui/Sidebar.js +11 -5
  51. package/dist/core/components/ui/Sidebar.js.map +1 -1
  52. package/dist/core/components/ui/Table.js +2 -2
  53. package/dist/core/components/ui/Table.js.map +1 -1
  54. package/dist/core/components/viewers/Data/DataMenu.d.ts.map +1 -1
  55. package/dist/core/components/viewers/Data/DataMenu.js +17 -9
  56. package/dist/core/components/viewers/Data/DataMenu.js.map +1 -1
  57. package/dist/core/components/viewers/Data/HeaderButtons.d.ts.map +1 -1
  58. package/dist/core/components/viewers/Data/HeaderButtons.js +5 -12
  59. package/dist/core/components/viewers/Data/HeaderButtons.js.map +1 -1
  60. package/dist/core/components/viewers/Data/MoreOptions.d.ts.map +1 -1
  61. package/dist/core/components/viewers/Data/MoreOptions.js +14 -27
  62. package/dist/core/components/viewers/Data/MoreOptions.js.map +1 -1
  63. package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.d.ts.map +1 -1
  64. package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.js +26 -0
  65. package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.js.map +1 -1
  66. package/dist/core/components/viewers/Data/buildingDetails/FieldRenderer.d.ts.map +1 -1
  67. package/dist/core/components/viewers/Data/buildingDetails/FieldRenderer.js +13 -18
  68. package/dist/core/components/viewers/Data/buildingDetails/FieldRenderer.js.map +1 -1
  69. package/dist/core/components/viewers/Data/details/DetailHeader.js +14 -14
  70. package/dist/core/components/viewers/Data/details/DetailHeader.js.map +1 -1
  71. package/dist/core/components/viewers/Data/details/FieldValue.d.ts.map +1 -1
  72. package/dist/core/components/viewers/Data/details/FieldValue.js +13 -3
  73. package/dist/core/components/viewers/Data/details/FieldValue.js.map +1 -1
  74. package/dist/core/components/viewers/Data/details/TabSidebar.d.ts.map +1 -1
  75. package/dist/core/components/viewers/Data/details/TabSidebar.js +22 -0
  76. package/dist/core/components/viewers/Data/details/TabSidebar.js.map +1 -1
  77. package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.d.ts.map +1 -1
  78. package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.js +19 -0
  79. package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.js.map +1 -1
  80. package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.d.ts +2 -0
  81. package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.d.ts.map +1 -1
  82. package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.js +84 -3
  83. package/dist/core/components/viewers/Data/siteDetails/AssociatedBuildings.js.map +1 -1
  84. package/dist/core/components/viewers/Data/siteDetails/SiteDetails.d.ts.map +1 -1
  85. package/dist/core/components/viewers/Data/siteDetails/SiteDetails.js +42 -2
  86. package/dist/core/components/viewers/Data/siteDetails/SiteDetails.js.map +1 -1
  87. package/dist/core/components/viewers/Data/utils/Columns.js +2 -2
  88. package/dist/core/components/viewers/Data/utils/Columns.js.map +1 -1
  89. package/dist/core/components/viewers/Data/utils/positionParser.d.ts.map +1 -1
  90. package/dist/core/components/viewers/Data/utils/positionParser.js +6 -5
  91. package/dist/core/components/viewers/Data/utils/positionParser.js.map +1 -1
  92. package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.d.ts.map +1 -1
  93. package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.js +8 -1
  94. package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.js.map +1 -1
  95. package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.d.ts.map +1 -1
  96. package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.js +7 -1
  97. package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.js.map +1 -1
  98. package/dist/core/components/viewers/map/datasets/RowActions.js +6 -6
  99. package/dist/core/components/viewers/map/datasets/RowActions.js.map +1 -1
  100. package/dist/core/components/viewers/map/datasets/data.js +1 -1
  101. package/dist/core/components/viewers/map/datasets/data.js.map +1 -1
  102. package/dist/core/components/viewers/map/datasets/index.d.ts.map +1 -1
  103. package/dist/core/components/viewers/map/datasets/index.js +28 -9
  104. package/dist/core/components/viewers/map/datasets/index.js.map +1 -1
  105. package/dist/core/components/viewers/map/src/MapLayers/index.d.ts.map +1 -1
  106. package/dist/core/components/viewers/map/src/MapLayers/index.js +2 -0
  107. package/dist/core/components/viewers/map/src/MapLayers/index.js.map +1 -1
  108. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.d.ts +19 -0
  109. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.d.ts.map +1 -0
  110. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.js +163 -0
  111. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteContextMenu.js.map +1 -0
  112. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.d.ts +3 -0
  113. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.d.ts.map +1 -0
  114. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.js +310 -0
  115. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/SiteLayer.js.map +1 -0
  116. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.d.ts +30 -0
  117. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.d.ts.map +1 -0
  118. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.js +122 -0
  119. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/siteGeometry.js.map +1 -0
  120. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.d.ts +13 -0
  121. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.d.ts.map +1 -0
  122. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.js +58 -0
  123. package/dist/core/components/viewers/map/src/MapLayers/src/SiteLayer/useShowSitesOnMap.js.map +1 -0
  124. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.d.ts +18 -0
  125. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.d.ts.map +1 -0
  126. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.js +102 -0
  127. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.js.map +1 -0
  128. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.d.ts.map +1 -1
  129. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.js +243 -176
  130. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.js.map +1 -1
  131. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.d.ts +24 -0
  132. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.d.ts.map +1 -0
  133. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.js +46 -0
  134. package/dist/core/components/viewers/map/src/tools/AddTools/AddSite/persistSite.js.map +1 -0
  135. package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.d.ts +1 -0
  136. package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.d.ts.map +1 -1
  137. package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.js +1 -0
  138. package/dist/core/components/viewers/map/utils/MapEventManager/MapClickManager.js.map +1 -1
  139. package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.d.ts.map +1 -1
  140. package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.js +54 -31
  141. package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.js.map +1 -1
  142. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.js +1 -1
  143. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.js.map +1 -1
  144. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.d.ts +2 -1
  145. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.d.ts.map +1 -1
  146. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.js +5 -4
  147. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.js.map +1 -1
  148. package/dist/core/hooks/sites/createSiteHooks.d.ts.map +1 -1
  149. package/dist/core/hooks/sites/createSiteHooks.js +0 -1
  150. package/dist/core/hooks/sites/createSiteHooks.js.map +1 -1
  151. package/dist/core/hooks/useCaptureScreenshot.d.ts.map +1 -1
  152. package/dist/core/hooks/useCaptureScreenshot.js +111 -33
  153. package/dist/core/hooks/useCaptureScreenshot.js.map +1 -1
  154. package/dist/core/hooks/useCurrentViewerPosition.d.ts +18 -0
  155. package/dist/core/hooks/useCurrentViewerPosition.d.ts.map +1 -0
  156. package/dist/core/hooks/useCurrentViewerPosition.js +66 -0
  157. package/dist/core/hooks/useCurrentViewerPosition.js.map +1 -0
  158. package/dist/core/hooks/useShareUrl.d.ts +2 -0
  159. package/dist/core/hooks/useShareUrl.d.ts.map +1 -0
  160. package/dist/core/hooks/useShareUrl.js +74 -0
  161. package/dist/core/hooks/useShareUrl.js.map +1 -0
  162. package/dist/core/store/CombineProviders.d.ts.map +1 -1
  163. package/dist/core/store/CombineProviders.js +2 -0
  164. package/dist/core/store/CombineProviders.js.map +1 -1
  165. package/dist/core/store/MapSites/context.d.ts +16 -0
  166. package/dist/core/store/MapSites/context.d.ts.map +1 -0
  167. package/dist/core/store/MapSites/context.js +29 -0
  168. package/dist/core/store/MapSites/context.js.map +1 -0
  169. package/dist/core/store/MapSites/reducer.d.ts +52 -0
  170. package/dist/core/store/MapSites/reducer.d.ts.map +1 -0
  171. package/dist/core/store/MapSites/reducer.js +70 -0
  172. package/dist/core/store/MapSites/reducer.js.map +1 -0
  173. package/dist/core/store/index.d.ts +1 -0
  174. package/dist/core/store/index.d.ts.map +1 -1
  175. package/dist/core/store/index.js +1 -0
  176. package/dist/core/store/index.js.map +1 -1
  177. package/dist/core/styles/globals.css +46 -33
  178. package/package.json +1 -1
@@ -0,0 +1,102 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { toast } from "sonner";
5
+ import { useTranslations } from "next-intl";
6
+ import { Building2 } from "lucide-react";
7
+ import { AddItemDialog } from "../../../../../../ui/AddItemDialog";
8
+ import { Button } from "../../../../../../ui/Button";
9
+ import { Checkbox } from "../../../../../../ui/Checkbox";
10
+ import { LoadingSpinner } from "../../../../../../ui/LoadingSpinner";
11
+ import { useSite } from "../../../../../../../hooks/sites/sites";
12
+ const AssociateBuildingsDialog = ({
13
+ siteId,
14
+ siteName,
15
+ buildings,
16
+ onClose,
17
+ onAssociated
18
+ }) => {
19
+ const t = useTranslations("AssociateBuildings");
20
+ const tf = (key, fallback, values) => t.has(key) ? t(key, values) : fallback;
21
+ const { updateSite } = useSite(String(siteId));
22
+ const [selected, setSelected] = React.useState(
23
+ () => new Set(buildings.map((b) => b.id))
24
+ );
25
+ const [isSaving, setIsSaving] = React.useState(false);
26
+ const toggle = (id) => setSelected((prev) => {
27
+ const next = new Set(prev);
28
+ if (next.has(id)) next.delete(id);
29
+ else next.add(id);
30
+ return next;
31
+ });
32
+ const allSelected = selected.size === buildings.length;
33
+ const toggleAll = () => setSelected(allSelected ? /* @__PURE__ */ new Set() : new Set(buildings.map((b) => b.id)));
34
+ const handleAssociate = async () => {
35
+ if (selected.size === 0) {
36
+ onClose();
37
+ return;
38
+ }
39
+ setIsSaving(true);
40
+ try {
41
+ await updateSite({ siteBuildings: { connect: [...selected].map((id) => ({ id })) } });
42
+ toast.success(tf("associated", `${selected.size} building(s) associated with "${siteName}".`, {
43
+ count: selected.size,
44
+ name: siteName
45
+ }));
46
+ onAssociated == null ? void 0 : onAssociated(buildings.filter((b) => selected.has(b.id)));
47
+ onClose();
48
+ } catch (e) {
49
+ toast.error(tf("associateFailed", "Failed to associate the buildings. Please try again."));
50
+ setIsSaving(false);
51
+ }
52
+ };
53
+ return /* @__PURE__ */ jsx(
54
+ AddItemDialog,
55
+ {
56
+ open: true,
57
+ onOpenChange: (open) => !open && !isSaving && onClose(),
58
+ title: tf("title", "Buildings found inside this site"),
59
+ description: tf("subtitle", `These buildings fall within "${siteName}". Choose which to associate with it.`, { name: siteName }),
60
+ icon: Building2,
61
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 pointer-events-auto min-w-0", children: [
62
+ /* @__PURE__ */ jsx(
63
+ "button",
64
+ {
65
+ type: "button",
66
+ onClick: toggleAll,
67
+ className: "self-start text-xs text-primary hover:underline",
68
+ children: allSelected ? tf("deselectAll", "Deselect all") : tf("selectAll", "Select all")
69
+ }
70
+ ),
71
+ /* @__PURE__ */ jsx("div", { className: "max-h-64 overflow-y-auto flex flex-col gap-1 rounded-md border p-1", children: buildings.map((b) => /* @__PURE__ */ jsxs(
72
+ "label",
73
+ {
74
+ className: "flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-accent cursor-pointer min-w-0",
75
+ children: [
76
+ /* @__PURE__ */ jsx(Checkbox, { className: "shrink-0", checked: selected.has(b.id), onCheckedChange: () => toggle(b.id) }),
77
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-w-0", children: [
78
+ /* @__PURE__ */ jsx("span", { className: "text-sm truncate", children: b.buildingName || tf("unnamed", "Unnamed building") }),
79
+ b.buildingAddress && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground truncate", children: b.buildingAddress })
80
+ ] })
81
+ ]
82
+ },
83
+ b.id
84
+ )) }),
85
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
86
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, disabled: isSaving, children: tf("skip", "Skip") }),
87
+ /* @__PURE__ */ jsxs(Button, { onClick: handleAssociate, disabled: isSaving || selected.size === 0, children: [
88
+ isSaving && /* @__PURE__ */ jsx(LoadingSpinner, {}),
89
+ tf("associate", "Associate"),
90
+ " (",
91
+ selected.size,
92
+ ")"
93
+ ] })
94
+ ] })
95
+ ] })
96
+ }
97
+ );
98
+ };
99
+ export {
100
+ AssociateBuildingsDialog
101
+ };
102
+ //# sourceMappingURL=AssociateBuildingsDialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/AssociateBuildingsDialog.tsx"],"sourcesContent":["'use client'\r\n\r\n// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport * as React from 'react'\r\nimport { toast } from 'sonner'\r\nimport { useTranslations } from 'next-intl'\r\nimport { Building2 } from 'lucide-react'\r\nimport { AddItemDialog } from '../../../../../../ui/AddItemDialog'\r\nimport { Button } from '../../../../../../ui/Button'\r\nimport { Checkbox } from '../../../../../../ui/Checkbox'\r\nimport { LoadingSpinner } from '../../../../../../ui/LoadingSpinner'\r\nimport { useSite } from '../../../../../../../hooks/sites/sites'\r\nimport type { Building } from '../../../../../../../types/dbTypes'\r\n\r\ninterface AssociateBuildingsDialogProps {\r\n siteId: number\r\n siteName: string\r\n buildings: Building[]\r\n onClose: () => void\r\n /** Called with the associated buildings after a successful connect. */\r\n onAssociated?: (buildings: Building[]) => void\r\n}\r\n\r\n/**\r\n * Shown after a site polygon is drawn when database buildings are detected\r\n * inside it. The user picks (multi-select) which buildings to associate with\r\n * the new site; selected buildings are connected via `updateSite`.\r\n */\r\nexport const AssociateBuildingsDialog = ({\r\n siteId, siteName, buildings, onClose, onAssociated,\r\n}: AssociateBuildingsDialogProps) => {\r\n const t = useTranslations('AssociateBuildings')\r\n const tf = (key: string, fallback: string, values?: Record<string, string | number>) =>\r\n (t.has(key) ? t(key, values) : fallback)\r\n\r\n const { updateSite } = useSite(String(siteId))\r\n const [selected, setSelected] = React.useState<Set<number>>(\r\n () => new Set(buildings.map(b => b.id)),\r\n )\r\n const [isSaving, setIsSaving] = React.useState(false)\r\n\r\n const toggle = (id: number) => setSelected((prev) => {\r\n const next = new Set(prev)\r\n if (next.has(id)) next.delete(id)\r\n else next.add(id)\r\n return next\r\n })\r\n\r\n const allSelected = selected.size === buildings.length\r\n const toggleAll = () =>\r\n setSelected(allSelected ? new Set() : new Set(buildings.map(b => b.id)))\r\n\r\n const handleAssociate = async () => {\r\n if (selected.size === 0) { onClose(); return }\r\n setIsSaving(true)\r\n try {\r\n await updateSite({ siteBuildings: { connect: [...selected].map(id => ({ id })) } })\r\n toast.success(tf('associated', `${selected.size} building(s) associated with \"${siteName}\".`, {\r\n count: selected.size,\r\n name: siteName,\r\n }))\r\n onAssociated?.(buildings.filter(b => selected.has(b.id)))\r\n onClose()\r\n }\r\n catch {\r\n toast.error(tf('associateFailed', 'Failed to associate the buildings. Please try again.'))\r\n setIsSaving(false)\r\n }\r\n }\r\n\r\n return (\r\n <AddItemDialog\r\n open\r\n onOpenChange={open => !open && !isSaving && onClose()}\r\n title={tf('title', 'Buildings found inside this site')}\r\n description={tf('subtitle', `These buildings fall within \"${siteName}\". Choose which to associate with it.`, { name: siteName })}\r\n icon={Building2}\r\n >\r\n <div className=\"flex flex-col gap-3 pointer-events-auto min-w-0\">\r\n <button\r\n type=\"button\"\r\n onClick={toggleAll}\r\n className=\"self-start text-xs text-primary hover:underline\"\r\n >\r\n {allSelected ? tf('deselectAll', 'Deselect all') : tf('selectAll', 'Select all')}\r\n </button>\r\n\r\n <div className=\"max-h-64 overflow-y-auto flex flex-col gap-1 rounded-md border p-1\">\r\n {buildings.map(b => (\r\n <label\r\n key={b.id}\r\n className=\"flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-accent cursor-pointer min-w-0\"\r\n >\r\n <Checkbox className=\"shrink-0\" checked={selected.has(b.id)} onCheckedChange={() => toggle(b.id)} />\r\n <div className=\"flex flex-col min-w-0\">\r\n <span className=\"text-sm truncate\">{b.buildingName || tf('unnamed', 'Unnamed building')}</span>\r\n {b.buildingAddress && (\r\n <span className=\"text-xs text-muted-foreground truncate\">{b.buildingAddress}</span>\r\n )}\r\n </div>\r\n </label>\r\n ))}\r\n </div>\r\n\r\n <div className=\"flex justify-end gap-2\">\r\n <Button variant=\"outline\" onClick={onClose} disabled={isSaving}>\r\n {tf('skip', 'Skip')}\r\n </Button>\r\n <Button onClick={handleAssociate} disabled={isSaving || selected.size === 0}>\r\n {isSaving && <LoadingSpinner />}\r\n {tf('associate', 'Associate')} ({selected.size})\r\n </Button>\r\n </div>\r\n </div>\r\n </AddItemDialog>\r\n )\r\n}\r\n"],"mappings":";AAiFQ,cAeM,YAfN;AA5ER,YAAY,WAAW;AACvB,SAAS,aAAa;AACtB,SAAS,uBAAuB;AAChC,SAAS,iBAAiB;AAC1B,SAAS,qBAAqB;AAC9B,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AAiBjB,MAAM,2BAA2B,CAAC;AAAA,EACvC;AAAA,EAAQ;AAAA,EAAU;AAAA,EAAW;AAAA,EAAS;AACxC,MAAqC;AACnC,QAAM,IAAI,gBAAgB,oBAAoB;AAC9C,QAAM,KAAK,CAAC,KAAa,UAAkB,WACxC,EAAE,IAAI,GAAG,IAAI,EAAE,KAAK,MAAM,IAAI;AAEjC,QAAM,EAAE,WAAW,IAAI,QAAQ,OAAO,MAAM,CAAC;AAC7C,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM;AAAA,IACpC,MAAM,IAAI,IAAI,UAAU,IAAI,OAAK,EAAE,EAAE,CAAC;AAAA,EACxC;AACA,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AAEpD,QAAM,SAAS,CAAC,OAAe,YAAY,CAAC,SAAS;AACnD,UAAM,OAAO,IAAI,IAAI,IAAI;AACzB,QAAI,KAAK,IAAI,EAAE,EAAG,MAAK,OAAO,EAAE;AAAA,QAC3B,MAAK,IAAI,EAAE;AAChB,WAAO;AAAA,EACT,CAAC;AAED,QAAM,cAAc,SAAS,SAAS,UAAU;AAChD,QAAM,YAAY,MAChB,YAAY,cAAc,oBAAI,IAAI,IAAI,IAAI,IAAI,UAAU,IAAI,OAAK,EAAE,EAAE,CAAC,CAAC;AAEzE,QAAM,kBAAkB,YAAY;AAClC,QAAI,SAAS,SAAS,GAAG;AAAE,cAAQ;AAAG;AAAA,IAAO;AAC7C,gBAAY,IAAI;AAChB,QAAI;AACF,YAAM,WAAW,EAAE,eAAe,EAAE,SAAS,CAAC,GAAG,QAAQ,EAAE,IAAI,SAAO,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;AAClF,YAAM,QAAQ,GAAG,cAAc,GAAG,SAAS,IAAI,iCAAiC,QAAQ,MAAM;AAAA,QAC5F,OAAO,SAAS;AAAA,QAChB,MAAM;AAAA,MACR,CAAC,CAAC;AACF,mDAAe,UAAU,OAAO,OAAK,SAAS,IAAI,EAAE,EAAE,CAAC;AACvD,cAAQ;AAAA,IACV,SACM;AACJ,YAAM,MAAM,GAAG,mBAAmB,sDAAsD,CAAC;AACzF,kBAAY,KAAK;AAAA,IACnB;AAAA,EACF;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAI;AAAA,MACJ,cAAc,UAAQ,CAAC,QAAQ,CAAC,YAAY,QAAQ;AAAA,MACpD,OAAO,GAAG,SAAS,kCAAkC;AAAA,MACrD,aAAa,GAAG,YAAY,gCAAgC,QAAQ,yCAAyC,EAAE,MAAM,SAAS,CAAC;AAAA,MAC/H,MAAM;AAAA,MAEN,+BAAC,SAAI,WAAU,mDACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAU;AAAA,YAET,wBAAc,GAAG,eAAe,cAAc,IAAI,GAAG,aAAa,YAAY;AAAA;AAAA,QACjF;AAAA,QAEA,oBAAC,SAAI,WAAU,sEACZ,oBAAU,IAAI,OACb;AAAA,UAAC;AAAA;AAAA,YAEC,WAAU;AAAA,YAEV;AAAA,kCAAC,YAAS,WAAU,YAAW,SAAS,SAAS,IAAI,EAAE,EAAE,GAAG,iBAAiB,MAAM,OAAO,EAAE,EAAE,GAAG;AAAA,cACjG,qBAAC,SAAI,WAAU,yBACb;AAAA,oCAAC,UAAK,WAAU,oBAAoB,YAAE,gBAAgB,GAAG,WAAW,kBAAkB,GAAE;AAAA,gBACvF,EAAE,mBACD,oBAAC,UAAK,WAAU,0CAA0C,YAAE,iBAAgB;AAAA,iBAEhF;AAAA;AAAA;AAAA,UATK,EAAE;AAAA,QAUT,CACD,GACH;AAAA,QAEA,qBAAC,SAAI,WAAU,0BACb;AAAA,8BAAC,UAAO,SAAQ,WAAU,SAAS,SAAS,UAAU,UACnD,aAAG,QAAQ,MAAM,GACpB;AAAA,UACA,qBAAC,UAAO,SAAS,iBAAiB,UAAU,YAAY,SAAS,SAAS,GACvE;AAAA,wBAAY,oBAAC,kBAAe;AAAA,YAC5B,GAAG,aAAa,WAAW;AAAA,YAAE;AAAA,YAAG,SAAS;AAAA,YAAK;AAAA,aACjD;AAAA,WACF;AAAA,SACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"SiteAdder.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAQ/B,UAAU,cAAc;IACtB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,eAAO,MAAM,SAAS,GAAI,sBAA8B,cAAc,sBAmVrE,CAAA"}
1
+ {"version":3,"file":"SiteAdder.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/components/viewers/map/src/tools/AddTools/AddSite/SiteAdder.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAe/B,UAAU,cAAc;IACtB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAKD,eAAO,MAAM,SAAS,GAAI,sBAA8B,cAAc,sBA0crE,CAAA"}
@@ -1,123 +1,154 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Input, Label } from "../../../../../../ui/";
4
4
  import * as React from "react";
5
- import { MapContext } from "../../../../../../../store";
5
+ import { MapContext, useMapSitesContext } from "../../../../../../../store";
6
6
  import { Button } from "../../../../../../ui/Button";
7
7
  import { AddItemDialog } from "../../../../../../ui/AddItemDialog";
8
8
  import { BoxSelect } from "lucide-react";
9
9
  import { useTranslations } from "next-intl";
10
+ import { toast } from "sonner";
11
+ import { useCreateSite } from "../../../../../../../hooks/sites/sites";
12
+ import { useBuildings } from "../../../../../../../hooks/buildings/buildings";
13
+ import { persistDrawnSite } from "./persistSite";
14
+ import { polygonCentroid, filterPointsInRing } from "../../../MapLayers/src/SiteLayer/siteGeometry";
15
+ import { AssociateBuildingsDialog } from "./AssociateBuildingsDialog";
16
+ const DRAW_TOAST_ID = "site-adder-draw";
10
17
  const SiteAdder = ({ isOpen = false, onCancel }) => {
11
18
  const t = useTranslations("SiteAdder");
19
+ const tf = React.useCallback(
20
+ (key, fallback, values) => t.has(key) ? t(key, values) : fallback,
21
+ [t]
22
+ );
12
23
  const [siteName, setSiteName] = React.useState("");
13
24
  const [isDrawing, setIsDrawing] = React.useState(false);
14
25
  const [drawingPoints, setDrawingPoints] = React.useState([]);
15
- const { dispatch: mapDispatch, state: mapState } = React.useContext(MapContext);
26
+ const { state: mapState } = React.useContext(MapContext);
16
27
  const { map } = mapState.map;
17
28
  const inputRef = React.useRef(null);
29
+ const { createSite } = useCreateSite();
30
+ const { dispatch: mapSitesDispatch } = useMapSitesContext();
31
+ const { buildings, isLoading: buildingsLoading } = useBuildings();
32
+ const [pendingDetection, setPendingDetection] = React.useState(null);
33
+ const [associateState, setAssociateState] = React.useState(null);
34
+ const isPersistingRef = React.useRef(false);
35
+ const drawingPointsRef = React.useRef([]);
18
36
  const hasMapApi = (mapInstance) => Boolean(mapInstance) && typeof mapInstance.getSource === "function" && typeof mapInstance.getLayer === "function" && typeof mapInstance.addSource === "function" && typeof mapInstance.addLayer === "function" && typeof mapInstance.on === "function" && typeof mapInstance.off === "function";
19
37
  const setCursor = (cursor) => {
20
38
  if (!map) return;
21
39
  if (map.getCanvas().style.cursor === cursor) return;
22
40
  map.getCanvas().style.cursor = cursor;
23
41
  };
42
+ const getSource = () => map == null ? void 0 : map.getSource("add-geojson");
24
43
  const updateDrawingFeatures = (points) => {
25
- if (!map) return;
26
- const source = map.getSource("add-geojson");
44
+ const source = getSource();
27
45
  if (!source) return;
46
+ const closable = points.length >= 3;
28
47
  const features = [];
29
- for (const [index, point] of points.entries()) {
48
+ points.forEach((point, index) => {
30
49
  features.push({
31
50
  type: "Feature",
32
- geometry: {
33
- type: "Point",
34
- coordinates: point
35
- },
51
+ geometry: { type: "Point", coordinates: point },
36
52
  properties: {
37
53
  isDrawPoint: true,
38
- pointIndex: index
54
+ pointIndex: index,
55
+ isFirstPoint: index === 0,
56
+ // The first point turns green once a click on it would close the site.
57
+ closable: closable && index === 0
39
58
  }
40
59
  });
41
- }
60
+ });
42
61
  if (points.length > 1) {
43
62
  features.push({
44
63
  type: "Feature",
45
64
  geometry: {
46
65
  type: "LineString",
47
- coordinates: points
66
+ // Close the loop visually so the user sees the final shape.
67
+ coordinates: closable ? [...points, points[0]] : points
48
68
  },
49
- properties: {
50
- isDrawing: true
51
- }
69
+ properties: { isDrawing: true }
52
70
  });
53
71
  }
54
- source.setData({
55
- type: "FeatureCollection",
56
- features
57
- });
72
+ if (closable) {
73
+ features.push({
74
+ type: "Feature",
75
+ geometry: { type: "Polygon", coordinates: [[...points, points[0]]] },
76
+ properties: { isDrawPreview: true }
77
+ });
78
+ }
79
+ source.setData({ type: "FeatureCollection", features });
80
+ };
81
+ const commitPoints = (points) => {
82
+ drawingPointsRef.current = points;
83
+ setDrawingPoints(points);
84
+ updateDrawingFeatures(points);
85
+ };
86
+ const isNearFirstPoint = (clickPoint, points, pixelThreshold = 14) => {
87
+ if (!map || points.length < 3) return false;
88
+ const first = map.project(points[0]);
89
+ const click = map.project(clickPoint);
90
+ return Math.hypot(first.x - click.x, first.y - click.y) <= pixelThreshold;
58
91
  };
59
- const addSite = (points) => {
60
- if (!map || points.length < 3) return;
61
- const source = map.getSource("add-geojson");
92
+ const clearDrawingFeatures = () => {
93
+ const source = getSource();
62
94
  if (!source) return;
63
- const closedPoints = [...points, points[0]];
64
- const siteFeature = {
65
- type: "Feature",
66
- geometry: {
67
- type: "Polygon",
68
- coordinates: [closedPoints]
69
- },
70
- properties: {
71
- type: "site",
72
- name: siteName || "Unnamed Site",
73
- id: Date.now().toString()
74
- }
75
- };
76
- const existingData = source._data;
77
- const existingSites = existingData.features.filter(
78
- (f) => {
79
- var _a;
80
- return ((_a = f.properties) == null ? void 0 : _a.type) === "site";
81
- }
82
- );
83
- source.setData({
84
- type: "FeatureCollection",
85
- features: [...existingSites, siteFeature]
86
- });
95
+ source.setData({ type: "FeatureCollection", features: [] });
87
96
  };
88
- const isPointNearExisting = (clickPoint, existingPoints, threshold = 1e-4) => {
89
- for (const [i, existingPoint] of existingPoints.entries()) {
90
- const distance = Math.sqrt(
91
- Math.pow(clickPoint[0] - existingPoint[0], 2) + Math.pow(clickPoint[1] - existingPoint[1], 2)
92
- );
93
- if (distance < threshold) {
94
- return i;
95
- }
96
- }
97
- return -1;
97
+ const resetDrawingState = () => {
98
+ drawingPointsRef.current = [];
99
+ setDrawingPoints([]);
100
+ setIsDrawing(false);
101
+ setCursor("");
102
+ toast.dismiss(DRAW_TOAST_ID);
98
103
  };
99
104
  const startDrawing = () => {
100
- setIsDrawing(true);
105
+ if (!hasMapApi(map) || !map.getSource("add-geojson")) {
106
+ toast.error(tf("mapNotReady", "The map is still loading. Please try again in a moment."));
107
+ return;
108
+ }
109
+ drawingPointsRef.current = [];
101
110
  setDrawingPoints([]);
111
+ setIsDrawing(true);
102
112
  setCursor("crosshair");
103
113
  };
104
- const stopDrawing = () => {
105
- setIsDrawing(false);
106
- setDrawingPoints([]);
107
- setCursor(null);
108
- const source = map == null ? void 0 : map.getSource("add-geojson");
109
- if (source) {
110
- const existingData = source._data;
111
- const existingSites = existingData.features.filter(
112
- (f) => {
113
- var _a;
114
- return ((_a = f.properties) == null ? void 0 : _a.type) === "site";
115
- }
116
- );
117
- source.setData({
118
- type: "FeatureCollection",
119
- features: existingSites
114
+ const undoLastPoint = () => {
115
+ const points = drawingPointsRef.current;
116
+ if (points.length === 0) return;
117
+ commitPoints(points.slice(0, -1));
118
+ };
119
+ const cancelDrawing = () => {
120
+ clearDrawingFeatures();
121
+ resetDrawingState();
122
+ toast.info(tf("drawCancelled", "Site drawing cancelled."));
123
+ };
124
+ const finishDrawing = async () => {
125
+ if (isPersistingRef.current) return;
126
+ const points = drawingPointsRef.current;
127
+ if (points.length < 3) {
128
+ toast.warning(tf("needThreePoints", "Add at least 3 points before closing the site."));
129
+ return;
130
+ }
131
+ const name = siteName.trim() || tf("unnamedSite", "Unnamed Site");
132
+ const ring = [...points];
133
+ const centroid = polygonCentroid(ring);
134
+ isPersistingRef.current = true;
135
+ const toastId = toast.loading(tf("savingSite", "Saving site\u2026"));
136
+ try {
137
+ const { siteId, assetId, fileId } = await persistDrawnSite({ name, ring, centroid, createSite });
138
+ mapSitesDispatch({
139
+ type: "SHOW_SITE",
140
+ payload: { site: { id: siteId, name, ring, assetId, fileId } }
120
141
  });
142
+ toast.success(tf("siteCreated", `Site "${name}" created.`, { name }), { id: toastId });
143
+ clearDrawingFeatures();
144
+ resetDrawingState();
145
+ setSiteName("");
146
+ isPersistingRef.current = false;
147
+ setPendingDetection({ siteId, siteName: name, ring });
148
+ onCancel == null ? void 0 : onCancel();
149
+ } catch (e) {
150
+ toast.error(tf("siteCreateFailed", "Failed to create the site. Please try again."), { id: toastId });
151
+ isPersistingRef.current = false;
121
152
  }
122
153
  };
123
154
  React.useEffect(() => {
@@ -132,16 +163,34 @@ const SiteAdder = ({ isOpen = false, onCancel }) => {
132
163
  data: geojson
133
164
  });
134
165
  }
166
+ if (!map.getLayer("add-draw-fill")) {
167
+ map.addLayer({
168
+ id: "add-draw-fill",
169
+ type: "fill",
170
+ source: "add-geojson",
171
+ paint: {
172
+ "fill-color": "#3b82f6",
173
+ "fill-opacity": 0.2
174
+ },
175
+ filter: [
176
+ "all",
177
+ ["in", "$type", "Polygon"],
178
+ ["==", "isDrawPreview", true]
179
+ ]
180
+ });
181
+ }
135
182
  if (!map.getLayer("add-draw-points")) {
136
183
  map.addLayer({
137
184
  id: "add-draw-points",
138
185
  type: "circle",
139
186
  source: "add-geojson",
140
187
  paint: {
141
- "circle-radius": 5,
142
- "circle-color": "#fff",
143
- "circle-stroke-width": 1,
144
- "circle-stroke-color": "#000"
188
+ // The first point is emphasised (and turns green once closing is
189
+ // possible) so it reads as "click here to close".
190
+ "circle-radius": ["case", ["==", ["get", "closable"], true], 8, 5],
191
+ "circle-color": ["case", ["==", ["get", "closable"], true], "#22c55e", "#ffffff"],
192
+ "circle-stroke-width": 2,
193
+ "circle-stroke-color": ["case", ["==", ["get", "closable"], true], "#15803d", "#000000"]
145
194
  },
146
195
  filter: [
147
196
  "all",
@@ -171,117 +220,135 @@ const SiteAdder = ({ isOpen = false, onCancel }) => {
171
220
  ]
172
221
  });
173
222
  }
174
- if (!map.getLayer("add-sites")) {
175
- map.addLayer({
176
- id: "add-sites",
177
- type: "fill",
178
- source: "add-geojson",
179
- paint: {
180
- "fill-color": "#3b82f6",
181
- "fill-opacity": 0.5
182
- },
183
- filter: [
184
- "all",
185
- ["in", "$type", "Polygon"],
186
- ["==", "type", "site"]
187
- ]
188
- });
189
- }
190
- if (!map.getLayer("add-site-outline")) {
191
- map.addLayer({
192
- id: "add-site-outline",
193
- type: "line",
194
- source: "add-geojson",
195
- layout: {
196
- "line-cap": "round",
197
- "line-join": "round"
198
- },
199
- paint: {
200
- "line-color": "#1c4587",
201
- "line-width": 2
202
- },
203
- filter: [
204
- "all",
205
- ["in", "$type", "Polygon"],
206
- ["==", "type", "site"]
207
- ]
208
- });
209
- }
210
- if (!map.getLayer("add-labels")) {
211
- map.addLayer({
212
- id: "add-labels",
213
- type: "symbol",
214
- source: "add-geojson",
215
- layout: {
216
- "text-field": ["get", "name"],
217
- "text-font": ["Inter"],
218
- "text-size": 12,
219
- "text-offset": [0, -1.5],
220
- "text-anchor": "center"
221
- },
222
- paint: {
223
- "text-color": "#fff",
224
- "text-halo-color": "#000",
225
- "text-halo-width": 1
226
- }
227
- });
228
- }
229
223
  }, [map]);
230
224
  React.useEffect(() => {
231
- if (!hasMapApi(map)) return;
225
+ if (!hasMapApi(map) || !isDrawing) return;
232
226
  const clickHandler = (e) => {
233
- if (!isDrawing) return;
234
227
  const clickPoint = [e.lngLat.lng, e.lngLat.lat];
235
- const nearPointIndex = isPointNearExisting(clickPoint, drawingPoints);
236
- if (nearPointIndex !== -1 && drawingPoints.length >= 3) {
237
- addSite(drawingPoints);
238
- stopDrawing();
239
- setSiteName("");
240
- } else {
241
- const newPoints = [...drawingPoints, clickPoint];
242
- setDrawingPoints(newPoints);
243
- updateDrawingFeatures(newPoints);
228
+ if (isNearFirstPoint(clickPoint, drawingPointsRef.current)) {
229
+ finishDrawing();
230
+ return;
244
231
  }
232
+ commitPoints([...drawingPointsRef.current, clickPoint]);
245
233
  };
246
- const mouseMoveHandler = (e) => {
247
- if (isDrawing) setCursor("crosshair");
248
- else setCursor(null);
249
- };
234
+ const mouseMoveHandler = () => setCursor("crosshair");
250
235
  map.on("click", clickHandler);
251
236
  map.on("mousemove", mouseMoveHandler);
252
237
  return () => {
253
238
  map.off("click", clickHandler);
254
239
  map.off("mousemove", mouseMoveHandler);
255
240
  };
256
- }, [map, isDrawing, drawingPoints, siteName]);
257
- if (isDrawing) return null;
258
- return /* @__PURE__ */ jsx(
259
- AddItemDialog,
260
- {
261
- open: isOpen,
262
- onOpenChange: (open) => !open && (onCancel == null ? void 0 : onCancel()),
263
- title: t("title"),
264
- icon: BoxSelect,
265
- children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 pointer-events-auto", children: [
266
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
267
- /* @__PURE__ */ jsx(Label, { htmlFor: "site-name", children: t("nameLabel") }),
268
- /* @__PURE__ */ jsx(
269
- Input,
270
- {
271
- id: "site-name",
272
- ref: inputRef,
273
- type: "text",
274
- placeholder: t("namePlaceholder"),
275
- value: siteName,
276
- onChange: (e) => setSiteName(e.target.value)
277
- }
278
- )
279
- ] }),
280
- /* @__PURE__ */ jsx("div", { className: "flex gap-2", children: /* @__PURE__ */ jsx(Button, { onClick: startDrawing, children: t("startDrawing") }) }),
281
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("startDrawingClick") })
282
- ] })
241
+ }, [map, isDrawing]);
242
+ React.useEffect(() => {
243
+ if (!isDrawing) return;
244
+ const onKey = (e) => {
245
+ if (e.key === "Enter") {
246
+ e.preventDefault();
247
+ finishDrawing();
248
+ } else if (e.key === "Escape") {
249
+ e.preventDefault();
250
+ cancelDrawing();
251
+ } else if (e.key === "Backspace") {
252
+ e.preventDefault();
253
+ undoLastPoint();
254
+ }
255
+ };
256
+ window.addEventListener("keydown", onKey);
257
+ return () => window.removeEventListener("keydown", onKey);
258
+ }, [isDrawing]);
259
+ React.useEffect(() => {
260
+ if (!isDrawing) {
261
+ toast.dismiss(DRAW_TOAST_ID);
262
+ return;
283
263
  }
284
- );
264
+ const count = drawingPoints.length;
265
+ const canFinish = count >= 3;
266
+ const title = canFinish ? tf("drawReadyTitle", "Ready to finish your site") : tf("drawTitle", "Draw your site");
267
+ const description = canFinish ? tf(
268
+ "drawReadyHint",
269
+ `${count} points placed. Click the first point, press Enter, or tap Finish to close it. Backspace removes the last point, Esc cancels.`,
270
+ { count }
271
+ ) : tf(
272
+ "drawHint",
273
+ `Click on the map to add each corner (${count} so far). Add at least 3 to form a site. Esc cancels.`,
274
+ { count }
275
+ );
276
+ toast.info(title, {
277
+ id: DRAW_TOAST_ID,
278
+ description,
279
+ duration: Infinity,
280
+ action: canFinish ? { label: tf("finish", "Finish"), onClick: () => finishDrawing() } : void 0,
281
+ cancel: { label: tf("cancel", "Cancel"), onClick: () => cancelDrawing() }
282
+ });
283
+ }, [isDrawing, drawingPoints.length]);
284
+ React.useEffect(() => {
285
+ if (!isOpen && isDrawing) {
286
+ clearDrawingFeatures();
287
+ resetDrawingState();
288
+ }
289
+ }, [isOpen, isDrawing]);
290
+ React.useEffect(() => () => {
291
+ toast.dismiss(DRAW_TOAST_ID);
292
+ }, []);
293
+ React.useEffect(() => {
294
+ if (!pendingDetection) return;
295
+ if (buildingsLoading && buildings.length === 0) return;
296
+ const inside = filterPointsInRing(pendingDetection.ring, buildings, (b) => typeof b.buildingLongitude === "number" && typeof b.buildingLatitude === "number" ? [b.buildingLongitude, b.buildingLatitude] : null);
297
+ if (inside.length > 0) {
298
+ setAssociateState({
299
+ siteId: pendingDetection.siteId,
300
+ siteName: pendingDetection.siteName,
301
+ buildings: inside
302
+ });
303
+ }
304
+ setPendingDetection(null);
305
+ }, [pendingDetection, buildings, buildingsLoading]);
306
+ if (isDrawing) return null;
307
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
308
+ associateState && /* @__PURE__ */ jsx(
309
+ AssociateBuildingsDialog,
310
+ {
311
+ siteId: associateState.siteId,
312
+ siteName: associateState.siteName,
313
+ buildings: associateState.buildings,
314
+ onClose: () => setAssociateState(null)
315
+ }
316
+ ),
317
+ /* @__PURE__ */ jsx(
318
+ AddItemDialog,
319
+ {
320
+ open: isOpen,
321
+ onOpenChange: (open) => !open && (onCancel == null ? void 0 : onCancel()),
322
+ title: t("title"),
323
+ icon: BoxSelect,
324
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 pointer-events-auto", children: [
325
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
326
+ /* @__PURE__ */ jsx(Label, { htmlFor: "site-name", children: t("nameLabel") }),
327
+ /* @__PURE__ */ jsx(
328
+ Input,
329
+ {
330
+ id: "site-name",
331
+ ref: inputRef,
332
+ type: "text",
333
+ placeholder: t("namePlaceholder"),
334
+ value: siteName,
335
+ onChange: (e) => setSiteName(e.target.value)
336
+ }
337
+ )
338
+ ] }),
339
+ /* @__PURE__ */ jsxs("ol", { className: "list-decimal pl-4 text-xs text-muted-foreground space-y-1", children: [
340
+ /* @__PURE__ */ jsx("li", { children: tf("step1", "Name your site (optional), then start drawing.") }),
341
+ /* @__PURE__ */ jsx("li", { children: tf("step2", "Click on the map to drop each corner.") }),
342
+ /* @__PURE__ */ jsx("li", { children: tf("step3", "Click the first corner (or press Enter) to close it.") })
343
+ ] }),
344
+ /* @__PURE__ */ jsx("div", { className: "flex gap-2", children: /* @__PURE__ */ jsxs(Button, { onClick: startDrawing, children: [
345
+ /* @__PURE__ */ jsx(BoxSelect, { className: "size-4" }),
346
+ t("startDrawing")
347
+ ] }) })
348
+ ] })
349
+ }
350
+ )
351
+ ] });
285
352
  };
286
353
  export {
287
354
  SiteAdder