@ifc-lite/viewer 1.17.4 → 1.18.0
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.
- package/.turbo/turbo-build.log +20 -17
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +630 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +13 -13
- package/src/App.tsx +16 -2
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +259 -93
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -13
- package/src/components/viewer/PropertiesPanel.tsx +237 -23
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +25 -4
- package/src/components/viewer/Viewport.tsx +25 -3
- package/src/components/viewer/ViewportContainer.tsx +51 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +23 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +550 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +20 -2
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/main.tsx +1 -10
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/api-keys.ts +73 -0
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/constants.ts +20 -2
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +82 -6
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/store.ts +14 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TaskEditCard — Inspector card that renders when a single Gantt task is
|
|
7
|
+
* selected, exposing the edit fields from P1 of the schedule-editing plan.
|
|
8
|
+
*
|
|
9
|
+
* Scope (P1):
|
|
10
|
+
* • Identity: Name, Identification, Description, PredefinedType, Milestone
|
|
11
|
+
* • Time: Start / Finish / Duration (any-two-of-three → third is derived)
|
|
12
|
+
* • Products: count + add-from-3D-selection / remove-from-3D-selection
|
|
13
|
+
* • Delete task (cascades sequences + descendants in the slice)
|
|
14
|
+
*
|
|
15
|
+
* Not in P1: dependency editing, inline rename, bar-drag (those are P2/P4).
|
|
16
|
+
*
|
|
17
|
+
* The card is controlled: every field reads from `scheduleData` via the
|
|
18
|
+
* store and writes through the slice's edit actions. Those actions push
|
|
19
|
+
* snapshots onto the undo stack so every field change is reversible.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
23
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
24
|
+
import { Button } from '@/components/ui/button';
|
|
25
|
+
import { Input } from '@/components/ui/input';
|
|
26
|
+
import { Label } from '@/components/ui/label';
|
|
27
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
28
|
+
import { Switch } from '@/components/ui/switch';
|
|
29
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
30
|
+
import {
|
|
31
|
+
ClipboardList, ChevronDown, Diamond, Plus, Minus, Trash2, Info,
|
|
32
|
+
} from 'lucide-react';
|
|
33
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
34
|
+
import { useViewerStore } from '@/store';
|
|
35
|
+
import type { ScheduleTaskInfo } from '@ifc-lite/parser';
|
|
36
|
+
|
|
37
|
+
/** IfcTaskTypeEnum values — same list as the Generate dialog. */
|
|
38
|
+
const TASK_TYPES: readonly string[] = [
|
|
39
|
+
'CONSTRUCTION', 'INSTALLATION', 'DEMOLITION', 'DISMANTLE', 'DISPOSAL',
|
|
40
|
+
'MAINTENANCE', 'LOGISTIC', 'MOVE', 'OPERATION', 'REMOVAL', 'RENOVATION',
|
|
41
|
+
'ATTENDANCE', 'USERDEFINED', 'NOTDEFINED',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const MS_PER_DAY = 86_400_000;
|
|
45
|
+
|
|
46
|
+
interface TaskEditCardProps {
|
|
47
|
+
/** Global ID of the task being edited. */
|
|
48
|
+
taskGlobalId: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const TaskEditCard = memo(function TaskEditCard({ taskGlobalId }: TaskEditCardProps) {
|
|
52
|
+
// Pull the current task + sibling store actions in a single selector so
|
|
53
|
+
// re-renders stay predictable.
|
|
54
|
+
const {
|
|
55
|
+
task,
|
|
56
|
+
updateTask,
|
|
57
|
+
updateTaskTime,
|
|
58
|
+
assignProducts,
|
|
59
|
+
unassignProducts,
|
|
60
|
+
deleteTask,
|
|
61
|
+
selectedEntityIds,
|
|
62
|
+
scheduleIsEdited,
|
|
63
|
+
scheduleUndoDepth,
|
|
64
|
+
} = useViewerStore(useShallow((s) => ({
|
|
65
|
+
task: s.scheduleData?.tasks.find((t) => t.globalId === taskGlobalId) ?? null,
|
|
66
|
+
updateTask: s.updateTask,
|
|
67
|
+
updateTaskTime: s.updateTaskTime,
|
|
68
|
+
assignProducts: s.assignProductsToTask,
|
|
69
|
+
unassignProducts: s.unassignProductsFromTask,
|
|
70
|
+
deleteTask: s.deleteTask,
|
|
71
|
+
selectedEntityIds: s.selectedEntityIds,
|
|
72
|
+
scheduleIsEdited: s.scheduleIsEdited,
|
|
73
|
+
scheduleUndoDepth: s.scheduleUndoStack.length,
|
|
74
|
+
})));
|
|
75
|
+
|
|
76
|
+
// Local draft for text inputs so typing doesn't round-trip through the
|
|
77
|
+
// store on every keystroke. Committed on blur / Enter.
|
|
78
|
+
const [nameDraft, setNameDraft] = useState<string>('');
|
|
79
|
+
const [identDraft, setIdentDraft] = useState<string>('');
|
|
80
|
+
|
|
81
|
+
// Date / duration drafts — held locally and pushed to the store after a
|
|
82
|
+
// short debounce so rapid typing or picker-spinning doesn't produce a
|
|
83
|
+
// per-keystroke undo snapshot + re-render storm. Committed on blur
|
|
84
|
+
// immediately to make the Tab-away flow feel instant.
|
|
85
|
+
const { startLocal, finishLocal, durationDays } = useMemo(
|
|
86
|
+
() => deriveTimeFields(task),
|
|
87
|
+
[task],
|
|
88
|
+
);
|
|
89
|
+
const [startDraft, setStartDraft] = useState<string>('');
|
|
90
|
+
const [finishDraft, setFinishDraft] = useState<string>('');
|
|
91
|
+
const [durationDraft, setDurationDraft] = useState<string>('');
|
|
92
|
+
|
|
93
|
+
// Sync drafts from authoritative state whenever the task changes or an
|
|
94
|
+
// undo/redo snaps back to a different value.
|
|
95
|
+
useMemo(() => {
|
|
96
|
+
setNameDraft(task?.name ?? '');
|
|
97
|
+
setIdentDraft(task?.identification ?? '');
|
|
98
|
+
setStartDraft(startLocal);
|
|
99
|
+
setFinishDraft(finishLocal);
|
|
100
|
+
setDurationDraft(durationDays === 0 ? '' : String(durationDays));
|
|
101
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
|
+
}, [taskGlobalId, task?.name, task?.identification, startLocal, finishLocal, durationDays, scheduleUndoDepth]);
|
|
103
|
+
|
|
104
|
+
// Debounce handle shared across the three time fields. Flush on unmount
|
|
105
|
+
// or when the user switches tasks so no edit is silently dropped.
|
|
106
|
+
const timeCommitRef = useRef<{ timer: number | null; flush: (() => void) | null }>({ timer: null, flush: null });
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
return () => {
|
|
109
|
+
if (timeCommitRef.current.timer !== null) {
|
|
110
|
+
window.clearTimeout(timeCommitRef.current.timer);
|
|
111
|
+
timeCommitRef.current.flush?.();
|
|
112
|
+
timeCommitRef.current.timer = null;
|
|
113
|
+
timeCommitRef.current.flush = null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}, [taskGlobalId]);
|
|
117
|
+
|
|
118
|
+
const scheduleTimeCommit = useCallback((flush: () => void) => {
|
|
119
|
+
if (timeCommitRef.current.timer !== null) {
|
|
120
|
+
window.clearTimeout(timeCommitRef.current.timer);
|
|
121
|
+
}
|
|
122
|
+
timeCommitRef.current.flush = flush;
|
|
123
|
+
timeCommitRef.current.timer = window.setTimeout(() => {
|
|
124
|
+
timeCommitRef.current.flush?.();
|
|
125
|
+
timeCommitRef.current.timer = null;
|
|
126
|
+
timeCommitRef.current.flush = null;
|
|
127
|
+
}, 200);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
const flushTimeCommit = useCallback(() => {
|
|
131
|
+
if (timeCommitRef.current.timer !== null) {
|
|
132
|
+
window.clearTimeout(timeCommitRef.current.timer);
|
|
133
|
+
timeCommitRef.current.flush?.();
|
|
134
|
+
timeCommitRef.current.timer = null;
|
|
135
|
+
timeCommitRef.current.flush = null;
|
|
136
|
+
}
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
140
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
141
|
+
|
|
142
|
+
const onCommitName = useCallback(() => {
|
|
143
|
+
if (task && nameDraft !== task.name) updateTask(taskGlobalId, { name: nameDraft });
|
|
144
|
+
}, [nameDraft, task, taskGlobalId, updateTask]);
|
|
145
|
+
|
|
146
|
+
const onCommitIdentification = useCallback(() => {
|
|
147
|
+
if (task && identDraft !== (task.identification ?? '')) {
|
|
148
|
+
updateTask(taskGlobalId, { identification: identDraft || undefined });
|
|
149
|
+
}
|
|
150
|
+
}, [identDraft, task, taskGlobalId, updateTask]);
|
|
151
|
+
|
|
152
|
+
if (!task) return null;
|
|
153
|
+
|
|
154
|
+
// Product assignment buttons act on whatever the user has selected in
|
|
155
|
+
// the 3D viewport. Gated when that set is empty.
|
|
156
|
+
const viewport3DCount = selectedEntityIds.size;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Collapsible
|
|
160
|
+
defaultOpen
|
|
161
|
+
className="border-2 border-primary/40 bg-primary/5 w-full max-w-full overflow-hidden"
|
|
162
|
+
>
|
|
163
|
+
<CollapsibleTrigger className="flex items-center gap-2 w-full p-2.5 hover:bg-primary/10 text-left transition-colors overflow-hidden">
|
|
164
|
+
<ClipboardList className="h-3.5 w-3.5 text-primary shrink-0" />
|
|
165
|
+
<span className="font-bold text-xs text-primary truncate flex-1 min-w-0">
|
|
166
|
+
Edit task
|
|
167
|
+
</span>
|
|
168
|
+
{scheduleIsEdited && (
|
|
169
|
+
<span className="text-[10px] font-medium bg-amber-100 dark:bg-amber-900/40 px-1.5 py-0.5 border border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-300 shrink-0">
|
|
170
|
+
● Pending
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
174
|
+
</CollapsibleTrigger>
|
|
175
|
+
|
|
176
|
+
<CollapsibleContent>
|
|
177
|
+
<div className="border-t-2 border-primary/40 p-3 grid gap-3">
|
|
178
|
+
{/* Identity */}
|
|
179
|
+
<div className="grid gap-1.5">
|
|
180
|
+
<Label htmlFor="task-name" className="text-[11px]">Name</Label>
|
|
181
|
+
<Input
|
|
182
|
+
id="task-name"
|
|
183
|
+
value={nameDraft}
|
|
184
|
+
onChange={(e) => setNameDraft(e.target.value)}
|
|
185
|
+
onBlur={onCommitName}
|
|
186
|
+
onKeyDown={(e) => { if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur(); }}
|
|
187
|
+
placeholder="Untitled task"
|
|
188
|
+
className="h-8 text-sm"
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div className="grid grid-cols-[1fr_auto] gap-2 items-end">
|
|
193
|
+
<div className="grid gap-1.5">
|
|
194
|
+
<Label htmlFor="task-type" className="text-[11px]">Predefined type</Label>
|
|
195
|
+
<Select
|
|
196
|
+
value={task.predefinedType || 'NOTDEFINED'}
|
|
197
|
+
onValueChange={(v) => updateTask(taskGlobalId, { predefinedType: v })}
|
|
198
|
+
>
|
|
199
|
+
<SelectTrigger id="task-type" className="h-8 text-xs">
|
|
200
|
+
<SelectValue />
|
|
201
|
+
</SelectTrigger>
|
|
202
|
+
<SelectContent>
|
|
203
|
+
{TASK_TYPES.map((t) => (
|
|
204
|
+
<SelectItem key={t} value={t}>{t}</SelectItem>
|
|
205
|
+
))}
|
|
206
|
+
</SelectContent>
|
|
207
|
+
</Select>
|
|
208
|
+
</div>
|
|
209
|
+
<ToggleRow
|
|
210
|
+
label="Milestone"
|
|
211
|
+
icon={<Diamond className="h-3 w-3" />}
|
|
212
|
+
checked={task.isMilestone}
|
|
213
|
+
onChange={(v) => updateTask(taskGlobalId, { isMilestone: v })}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* Time — stacked layout. Two datetime-local inputs side-by-side
|
|
218
|
+
overflow the narrow default Inspector width (22 % of viewport);
|
|
219
|
+
stacking reads cleaner even at wide widths and gives every
|
|
220
|
+
input enough room to render the browser's picker UI. */}
|
|
221
|
+
<div className="grid gap-2 rounded border border-border/60 p-2">
|
|
222
|
+
<div className="grid gap-1">
|
|
223
|
+
<Label htmlFor="task-start" className="text-[10px]">Start</Label>
|
|
224
|
+
<Input
|
|
225
|
+
id="task-start"
|
|
226
|
+
type="datetime-local"
|
|
227
|
+
value={startDraft}
|
|
228
|
+
onChange={(e) => {
|
|
229
|
+
const v = e.target.value;
|
|
230
|
+
setStartDraft(v);
|
|
231
|
+
scheduleTimeCommit(() => {
|
|
232
|
+
updateTaskTime(taskGlobalId, {
|
|
233
|
+
scheduleStart: v ? `${v}:00` : undefined,
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}}
|
|
237
|
+
onBlur={flushTimeCommit}
|
|
238
|
+
className="h-7 w-full text-xs font-mono"
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="grid gap-1">
|
|
242
|
+
<Label htmlFor="task-finish" className="text-[10px]">Finish</Label>
|
|
243
|
+
<Input
|
|
244
|
+
id="task-finish"
|
|
245
|
+
type="datetime-local"
|
|
246
|
+
value={finishDraft}
|
|
247
|
+
disabled={task.isMilestone}
|
|
248
|
+
onChange={(e) => {
|
|
249
|
+
const v = e.target.value;
|
|
250
|
+
setFinishDraft(v);
|
|
251
|
+
scheduleTimeCommit(() => {
|
|
252
|
+
updateTaskTime(taskGlobalId, {
|
|
253
|
+
scheduleFinish: v ? `${v}:00` : undefined,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}}
|
|
257
|
+
onBlur={flushTimeCommit}
|
|
258
|
+
className="h-7 w-full text-xs font-mono"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
<div className="grid gap-1">
|
|
262
|
+
<Label htmlFor="task-dur" className="text-[10px]">Duration (days)</Label>
|
|
263
|
+
<Input
|
|
264
|
+
id="task-dur"
|
|
265
|
+
type="number"
|
|
266
|
+
min={0}
|
|
267
|
+
step={0.5}
|
|
268
|
+
value={durationDraft}
|
|
269
|
+
disabled={task.isMilestone}
|
|
270
|
+
onChange={(e) => {
|
|
271
|
+
const v = e.target.value;
|
|
272
|
+
setDurationDraft(v);
|
|
273
|
+
const n = parseFloat(v);
|
|
274
|
+
if (!Number.isFinite(n) || n < 0) return;
|
|
275
|
+
const iso = daysToIso(n);
|
|
276
|
+
scheduleTimeCommit(() => {
|
|
277
|
+
updateTaskTime(taskGlobalId, { scheduleDuration: iso });
|
|
278
|
+
});
|
|
279
|
+
}}
|
|
280
|
+
onBlur={flushTimeCommit}
|
|
281
|
+
className="h-7 w-full text-xs font-mono"
|
|
282
|
+
/>
|
|
283
|
+
<p className="text-[10px] text-muted-foreground flex items-start gap-1">
|
|
284
|
+
<Info className="h-3 w-3 shrink-0 mt-px" />
|
|
285
|
+
Editing start or duration keeps finish consistent; editing finish keeps start consistent.
|
|
286
|
+
</p>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Products */}
|
|
291
|
+
<div className="grid gap-2 rounded border border-border/60 p-2">
|
|
292
|
+
<div className="flex items-center justify-between">
|
|
293
|
+
<Label className="text-[11px]">Products</Label>
|
|
294
|
+
<span className="text-[11px] font-mono text-muted-foreground">
|
|
295
|
+
{task.productExpressIds.length} assigned
|
|
296
|
+
</span>
|
|
297
|
+
</div>
|
|
298
|
+
<div className="grid grid-cols-2 gap-1.5">
|
|
299
|
+
<Tooltip>
|
|
300
|
+
<TooltipTrigger asChild>
|
|
301
|
+
<Button
|
|
302
|
+
size="sm"
|
|
303
|
+
variant="secondary"
|
|
304
|
+
onClick={() => assignProducts(taskGlobalId, Array.from(selectedEntityIds))}
|
|
305
|
+
disabled={viewport3DCount === 0}
|
|
306
|
+
className="gap-1 h-7 text-xs"
|
|
307
|
+
>
|
|
308
|
+
<Plus className="h-3 w-3" />
|
|
309
|
+
Add {viewport3DCount > 0 ? `(${viewport3DCount})` : ''}
|
|
310
|
+
</Button>
|
|
311
|
+
</TooltipTrigger>
|
|
312
|
+
<TooltipContent>
|
|
313
|
+
{viewport3DCount > 0
|
|
314
|
+
? `Add the ${viewport3DCount} object(s) currently selected in the 3D viewport to this task.`
|
|
315
|
+
: 'Select objects in the 3D viewport first.'}
|
|
316
|
+
</TooltipContent>
|
|
317
|
+
</Tooltip>
|
|
318
|
+
<Tooltip>
|
|
319
|
+
<TooltipTrigger asChild>
|
|
320
|
+
<Button
|
|
321
|
+
size="sm"
|
|
322
|
+
variant="secondary"
|
|
323
|
+
onClick={() => unassignProducts(taskGlobalId, Array.from(selectedEntityIds))}
|
|
324
|
+
disabled={viewport3DCount === 0}
|
|
325
|
+
className="gap-1 h-7 text-xs"
|
|
326
|
+
>
|
|
327
|
+
<Minus className="h-3 w-3" />
|
|
328
|
+
Remove {viewport3DCount > 0 ? `(${viewport3DCount})` : ''}
|
|
329
|
+
</Button>
|
|
330
|
+
</TooltipTrigger>
|
|
331
|
+
<TooltipContent>
|
|
332
|
+
Remove the selected 3D objects from this task.
|
|
333
|
+
</TooltipContent>
|
|
334
|
+
</Tooltip>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
{/* Details (identification + description) behind disclosure */}
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
onClick={() => setShowDetails((s) => !s)}
|
|
342
|
+
className="flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
|
|
343
|
+
>
|
|
344
|
+
<ChevronDown className={`h-3 w-3 transition-transform ${showDetails ? '' : '-rotate-90'}`} />
|
|
345
|
+
Details
|
|
346
|
+
</button>
|
|
347
|
+
{showDetails && (
|
|
348
|
+
<div className="grid gap-2">
|
|
349
|
+
<div className="grid gap-1.5">
|
|
350
|
+
<Label htmlFor="task-ident" className="text-[11px]">Identification</Label>
|
|
351
|
+
<Input
|
|
352
|
+
id="task-ident"
|
|
353
|
+
value={identDraft}
|
|
354
|
+
onChange={(e) => setIdentDraft(e.target.value)}
|
|
355
|
+
onBlur={onCommitIdentification}
|
|
356
|
+
onKeyDown={(e) => { if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur(); }}
|
|
357
|
+
placeholder="—"
|
|
358
|
+
className="h-7 text-xs font-mono"
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
<div className="grid gap-1.5">
|
|
362
|
+
<Label className="text-[11px]">Global ID</Label>
|
|
363
|
+
<div className="text-[10px] font-mono text-muted-foreground truncate" title={task.globalId}>
|
|
364
|
+
{task.globalId}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
{/* Delete */}
|
|
371
|
+
<div className="flex items-center justify-end gap-2 pt-1 border-t border-border/40">
|
|
372
|
+
{!confirmDelete ? (
|
|
373
|
+
<Button
|
|
374
|
+
size="sm"
|
|
375
|
+
variant="ghost"
|
|
376
|
+
onClick={() => setConfirmDelete(true)}
|
|
377
|
+
className="gap-1 h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
378
|
+
>
|
|
379
|
+
<Trash2 className="h-3 w-3" />
|
|
380
|
+
Delete task
|
|
381
|
+
</Button>
|
|
382
|
+
) : (
|
|
383
|
+
<>
|
|
384
|
+
<span className="text-[11px] text-muted-foreground">Delete{task.childGlobalIds.length > 0 ? ` + ${task.childGlobalIds.length} descendants` : ''}?</span>
|
|
385
|
+
<Button
|
|
386
|
+
size="sm"
|
|
387
|
+
variant="ghost"
|
|
388
|
+
onClick={() => setConfirmDelete(false)}
|
|
389
|
+
className="h-7 text-xs"
|
|
390
|
+
>
|
|
391
|
+
Cancel
|
|
392
|
+
</Button>
|
|
393
|
+
<Button
|
|
394
|
+
size="sm"
|
|
395
|
+
variant="destructive"
|
|
396
|
+
onClick={() => {
|
|
397
|
+
deleteTask(taskGlobalId);
|
|
398
|
+
setConfirmDelete(false);
|
|
399
|
+
}}
|
|
400
|
+
className="h-7 text-xs"
|
|
401
|
+
>
|
|
402
|
+
Delete
|
|
403
|
+
</Button>
|
|
404
|
+
</>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
</CollapsibleContent>
|
|
409
|
+
</Collapsible>
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
414
|
+
// Helpers (date / duration formatting for the HTML `datetime-local` input)
|
|
415
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
416
|
+
|
|
417
|
+
interface DerivedTimeFields {
|
|
418
|
+
/** yyyy-MM-ddTHH:mm for `<input type=datetime-local>` — empty if unset. */
|
|
419
|
+
startLocal: string;
|
|
420
|
+
finishLocal: string;
|
|
421
|
+
/** Days as a float; 0 when no taskTime exists. */
|
|
422
|
+
durationDays: number;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function deriveTimeFields(task: ScheduleTaskInfo | null): DerivedTimeFields {
|
|
426
|
+
if (!task?.taskTime) return { startLocal: '', finishLocal: '', durationDays: 0 };
|
|
427
|
+
const startLocal = toDatetimeLocal(task.taskTime.scheduleStart);
|
|
428
|
+
const finishLocal = toDatetimeLocal(task.taskTime.scheduleFinish);
|
|
429
|
+
let durationDays = 0;
|
|
430
|
+
const start = parseIso(task.taskTime.scheduleStart);
|
|
431
|
+
const finish = parseIso(task.taskTime.scheduleFinish);
|
|
432
|
+
if (start !== undefined && finish !== undefined) {
|
|
433
|
+
durationDays = Math.round(((finish - start) / MS_PER_DAY) * 100) / 100;
|
|
434
|
+
} else if (task.taskTime.scheduleDuration) {
|
|
435
|
+
durationDays = isoDurationToDays(task.taskTime.scheduleDuration);
|
|
436
|
+
}
|
|
437
|
+
return { startLocal, finishLocal, durationDays };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** ISO-8601 → `datetime-local` string (strips seconds and any TZ). */
|
|
441
|
+
function toDatetimeLocal(iso?: string): string {
|
|
442
|
+
if (!iso) return '';
|
|
443
|
+
// Accept both `2024-05-01T08:00:00` and `2024-05-01T08:00:00Z`; the
|
|
444
|
+
// `<input type=datetime-local>` doesn't take TZ info, so we just
|
|
445
|
+
// trim whatever's past the minute segment.
|
|
446
|
+
const trimmed = iso.replace(/(Z|[+-]\d{2}:?\d{2})$/, '');
|
|
447
|
+
return trimmed.slice(0, 16);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function parseIso(iso?: string): number | undefined {
|
|
451
|
+
if (!iso) return undefined;
|
|
452
|
+
const hasTz = /Z$|[+-]\d{2}:?\d{2}$/.test(iso);
|
|
453
|
+
const t = Date.parse(hasTz ? iso : `${iso}Z`);
|
|
454
|
+
return Number.isNaN(t) ? undefined : t;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function isoDurationToDays(iso: string): number {
|
|
458
|
+
const match = iso.match(
|
|
459
|
+
/^P(?:(\d+(?:\.\d+)?)Y)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/,
|
|
460
|
+
);
|
|
461
|
+
if (!match) return 0;
|
|
462
|
+
const [, y, mo, w, d, h, mi, s] = match;
|
|
463
|
+
const days =
|
|
464
|
+
(y ? parseFloat(y) * 365.2425 : 0) +
|
|
465
|
+
(mo ? parseFloat(mo) * 30.4369 : 0) +
|
|
466
|
+
(w ? parseFloat(w) * 7 : 0) +
|
|
467
|
+
(d ? parseFloat(d) : 0) +
|
|
468
|
+
(h ? parseFloat(h) / 24 : 0) +
|
|
469
|
+
(mi ? parseFloat(mi) / 1440 : 0) +
|
|
470
|
+
(s ? parseFloat(s) / 86_400 : 0);
|
|
471
|
+
return Math.round(days * 100) / 100;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function daysToIso(days: number): string {
|
|
475
|
+
if (days === 0) return 'PT0S';
|
|
476
|
+
const wholeDays = Math.floor(days);
|
|
477
|
+
const fractionalMs = Math.round((days - wholeDays) * MS_PER_DAY);
|
|
478
|
+
const hours = Math.floor(fractionalMs / 3_600_000);
|
|
479
|
+
const mins = Math.floor((fractionalMs - hours * 3_600_000) / 60_000);
|
|
480
|
+
let out = 'P';
|
|
481
|
+
if (wholeDays > 0) out += `${wholeDays}D`;
|
|
482
|
+
if (hours > 0 || mins > 0) {
|
|
483
|
+
out += 'T';
|
|
484
|
+
if (hours > 0) out += `${hours}H`;
|
|
485
|
+
if (mins > 0) out += `${mins}M`;
|
|
486
|
+
}
|
|
487
|
+
return out === 'P' ? 'P0D' : out;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
491
|
+
// ToggleRow — tiny labelled switch. Kept local to avoid leaking into the
|
|
492
|
+
// shared ui/ toolkit until we need it elsewhere.
|
|
493
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
494
|
+
|
|
495
|
+
interface ToggleRowProps {
|
|
496
|
+
label: string;
|
|
497
|
+
icon?: React.ReactNode;
|
|
498
|
+
checked: boolean;
|
|
499
|
+
onChange: (next: boolean) => void;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function ToggleRow({ label, icon, checked, onChange }: ToggleRowProps) {
|
|
503
|
+
return (
|
|
504
|
+
<label className="flex items-center gap-1.5 cursor-pointer select-none">
|
|
505
|
+
{icon && <span className="text-muted-foreground">{icon}</span>}
|
|
506
|
+
<span className="text-[11px] font-medium">{label}</span>
|
|
507
|
+
<Switch checked={checked} onCheckedChange={onChange} />
|
|
508
|
+
</label>
|
|
509
|
+
);
|
|
510
|
+
}
|