retold-facto 0.0.4

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 (92) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.dockerignore +8 -0
  3. package/.quackage.json +19 -0
  4. package/Dockerfile +26 -0
  5. package/bin/retold-facto.js +909 -0
  6. package/examples/facto-government-data.sqlite +0 -0
  7. package/examples/government-data-catalog.json +137 -0
  8. package/examples/government-data-loader.js +1432 -0
  9. package/package.json +91 -0
  10. package/scripts/facto-download.js +425 -0
  11. package/source/Retold-Facto.js +1042 -0
  12. package/source/services/Retold-Facto-BeaconProvider.js +511 -0
  13. package/source/services/Retold-Facto-CatalogManager.js +1252 -0
  14. package/source/services/Retold-Facto-DataLakeService.js +1642 -0
  15. package/source/services/Retold-Facto-DatasetManager.js +417 -0
  16. package/source/services/Retold-Facto-IngestEngine.js +1315 -0
  17. package/source/services/Retold-Facto-ProjectionEngine.js +3960 -0
  18. package/source/services/Retold-Facto-RecordManager.js +360 -0
  19. package/source/services/Retold-Facto-SchemaManager.js +1110 -0
  20. package/source/services/Retold-Facto-SourceFolderScanner.js +2243 -0
  21. package/source/services/Retold-Facto-SourceManager.js +730 -0
  22. package/source/services/Retold-Facto-StoreConnectionManager.js +441 -0
  23. package/source/services/Retold-Facto-ThroughputMonitor.js +478 -0
  24. package/source/services/web-app/codemirror-entry.js +7 -0
  25. package/source/services/web-app/pict-app/Pict-Application-Facto-Configuration.json +9 -0
  26. package/source/services/web-app/pict-app/Pict-Application-Facto.js +70 -0
  27. package/source/services/web-app/pict-app/Pict-Facto-Bundle.js +11 -0
  28. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto-UI.js +66 -0
  29. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto.js +69 -0
  30. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Catalog.js +93 -0
  31. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Connections.js +42 -0
  32. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Datasets.js +605 -0
  33. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Projections.js +188 -0
  34. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Scanner.js +80 -0
  35. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Schema.js +116 -0
  36. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Sources.js +104 -0
  37. package/source/services/web-app/pict-app/views/PictView-Facto-Catalog.js +526 -0
  38. package/source/services/web-app/pict-app/views/PictView-Facto-Datasets.js +173 -0
  39. package/source/services/web-app/pict-app/views/PictView-Facto-Ingest.js +259 -0
  40. package/source/services/web-app/pict-app/views/PictView-Facto-Layout.js +191 -0
  41. package/source/services/web-app/pict-app/views/PictView-Facto-Projections.js +231 -0
  42. package/source/services/web-app/pict-app/views/PictView-Facto-Records.js +326 -0
  43. package/source/services/web-app/pict-app/views/PictView-Facto-Scanner.js +624 -0
  44. package/source/services/web-app/pict-app/views/PictView-Facto-Sources.js +201 -0
  45. package/source/services/web-app/pict-app/views/PictView-Facto-Throughput.js +456 -0
  46. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full-Configuration.json +14 -0
  47. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full.js +391 -0
  48. package/source/services/web-app/pict-app-full/providers/PictRouter-Facto-Configuration.json +56 -0
  49. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-BottomBar.js +68 -0
  50. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Connections.js +340 -0
  51. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboard.js +149 -0
  52. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboards.js +819 -0
  53. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Datasets.js +178 -0
  54. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-IngestJobs.js +99 -0
  55. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Layout.js +62 -0
  56. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-MappingEditor.js +158 -0
  57. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-ProjectionDetail.js +1120 -0
  58. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Projections.js +172 -0
  59. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-QueryPanel.js +119 -0
  60. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-RecordViewer.js +663 -0
  61. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Records.js +648 -0
  62. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Scanner.js +1017 -0
  63. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDetail.js +1404 -0
  64. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDocEditor.js +1036 -0
  65. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaEditor.js +636 -0
  66. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaResearch.js +357 -0
  67. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceDetail.js +822 -0
  68. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceEditor.js +1036 -0
  69. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceResearch.js +487 -0
  70. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Sources.js +165 -0
  71. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Throughput.js +439 -0
  72. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-TopBar.js +335 -0
  73. package/source/services/web-app/pict-app-full/views/projections/Facto-Projections-Constants.js +71 -0
  74. package/source/services/web-app/web/chart.min.js +20 -0
  75. package/source/services/web-app/web/codemirror-bundle.js +30099 -0
  76. package/source/services/web-app/web/css/facto-themes.css +467 -0
  77. package/source/services/web-app/web/css/facto.css +502 -0
  78. package/source/services/web-app/web/index.html +28 -0
  79. package/source/services/web-app/web/retold-facto.js +12138 -0
  80. package/source/services/web-app/web/retold-facto.js.map +1 -0
  81. package/source/services/web-app/web/retold-facto.min.js +2 -0
  82. package/source/services/web-app/web/retold-facto.min.js.map +1 -0
  83. package/source/services/web-app/web/simple/index.html +17 -0
  84. package/test/Facto_Browser_Integration_tests.js +798 -0
  85. package/test/RetoldFacto_tests.js +4117 -0
  86. package/test/fixtures/weather-readings.csv +17 -0
  87. package/test/fixtures/weather-stations.csv +9 -0
  88. package/test/model/MeadowModel-Extended.json +8497 -0
  89. package/test/model/MeadowModel-PICT.json +1 -0
  90. package/test/model/MeadowModel.json +1355 -0
  91. package/test/model/ddl/Facto.ddl +225 -0
  92. package/test/model/fable-configuration.json +14 -0
@@ -0,0 +1,1120 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const libProjectionConstants = require('./projections/Facto-Projections-Constants.js');
4
+
5
+ const _ViewConfiguration =
6
+ {
7
+ ViewIdentifier: "Facto-Full-ProjectionDetail",
8
+
9
+ DefaultRenderable: "Facto-Full-ProjectionDetail-Content",
10
+ DefaultDestinationAddress: "#Facto-Full-Content-Container",
11
+
12
+ AutoRender: false,
13
+
14
+ CSS: /*css*/`
15
+ .facto-proj-detail-back {
16
+ display: inline-flex;
17
+ align-items: center;
18
+ gap: 0.35em;
19
+ color: var(--facto-text-secondary);
20
+ cursor: pointer;
21
+ font-size: 0.85em;
22
+ margin-bottom: 0.75em;
23
+ transition: color 0.15s;
24
+ }
25
+ .facto-proj-detail-back:hover {
26
+ color: var(--facto-brand);
27
+ }
28
+ .facto-proj-detail-title-row {
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: space-between;
32
+ margin-bottom: 1.25em;
33
+ }
34
+ .facto-proj-detail-title-row h1 {
35
+ margin: 0;
36
+ }
37
+ .facto-proj-detail-actions {
38
+ display: flex;
39
+ gap: 0.5em;
40
+ }
41
+ .facto-proj-meta-cards {
42
+ display: grid;
43
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
44
+ gap: 1em;
45
+ margin-bottom: 1.5em;
46
+ }
47
+ .facto-proj-meta-card {
48
+ background: var(--facto-bg-card);
49
+ border: 1px solid var(--facto-border-subtle);
50
+ border-radius: 8px;
51
+ padding: 1em;
52
+ }
53
+ .facto-proj-meta-card-label {
54
+ font-size: 0.7em;
55
+ font-weight: 600;
56
+ text-transform: uppercase;
57
+ letter-spacing: 0.5px;
58
+ color: var(--facto-text-tertiary);
59
+ margin-bottom: 0.35em;
60
+ }
61
+ .facto-proj-meta-card-value {
62
+ font-size: 1.1em;
63
+ font-weight: 600;
64
+ color: var(--facto-text-heading);
65
+ }
66
+ .facto-proj-section {
67
+ margin-bottom: 1.5em;
68
+ }
69
+ .facto-proj-section-header {
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: space-between;
73
+ margin-bottom: 0.75em;
74
+ }
75
+ .facto-proj-section-header h2 {
76
+ margin: 0;
77
+ font-size: 1em;
78
+ text-transform: uppercase;
79
+ letter-spacing: 0.05em;
80
+ color: var(--facto-text-secondary);
81
+ }
82
+ .facto-proj-ddl-preview {
83
+ background: var(--facto-bg-code, #f0e8d8);
84
+ border: 1px solid var(--facto-border-subtle);
85
+ border-radius: 6px;
86
+ padding: 0.75em;
87
+ font-family: 'SF Mono', Consolas, monospace;
88
+ font-size: 0.82em;
89
+ white-space: pre-wrap;
90
+ max-height: 150px;
91
+ overflow: auto;
92
+ color: var(--facto-text-secondary);
93
+ margin-bottom: 0.75em;
94
+ }
95
+ .facto-proj-detail-loading {
96
+ text-align: center;
97
+ padding: 3em;
98
+ color: var(--facto-text-tertiary);
99
+ }
100
+ .facto-proj-deploy-form {
101
+ display: grid;
102
+ grid-template-columns: 1fr 1fr auto auto;
103
+ gap: 0.75em;
104
+ align-items: end;
105
+ padding: 1em;
106
+ background: var(--facto-bg-surface, #fcf8f0);
107
+ border: 1px solid var(--facto-border, #d6c8ae);
108
+ border-radius: 8px;
109
+ margin-bottom: 0.75em;
110
+ }
111
+ .facto-proj-deploy-form label {
112
+ display: block;
113
+ font-size: 0.72em;
114
+ font-weight: 600;
115
+ text-transform: uppercase;
116
+ letter-spacing: 0.5px;
117
+ color: var(--facto-text-tertiary);
118
+ margin-bottom: 0.25em;
119
+ }
120
+ .facto-proj-deploy-log {
121
+ font-family: 'SF Mono', monospace;
122
+ font-size: 0.78em;
123
+ padding: 0.6em;
124
+ background: var(--facto-bg-code, #f0e8d8);
125
+ border: 1px solid var(--facto-border-subtle, #e8ddc8);
126
+ border-radius: 6px;
127
+ white-space: pre-wrap;
128
+ max-height: 200px;
129
+ overflow: auto;
130
+ margin-top: 0.5em;
131
+ color: var(--facto-text-secondary);
132
+ }
133
+ .facto-proj-import-form {
134
+ display: grid;
135
+ grid-template-columns: 1fr 1fr auto auto;
136
+ gap: 0.75em;
137
+ align-items: end;
138
+ padding: 1em;
139
+ background: var(--facto-bg-surface, #fcf8f0);
140
+ border: 1px solid var(--facto-border, #d6c8ae);
141
+ border-radius: 8px;
142
+ margin-bottom: 0.75em;
143
+ }
144
+ .facto-proj-import-form label {
145
+ display: block;
146
+ font-size: 0.72em;
147
+ font-weight: 600;
148
+ text-transform: uppercase;
149
+ letter-spacing: 0.5px;
150
+ color: var(--facto-text-tertiary);
151
+ margin-bottom: 0.25em;
152
+ }
153
+ .facto-proj-import-stage-row {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 0.5em;
157
+ grid-column: 1 / -1;
158
+ }
159
+ .facto-proj-import-stage-row label {
160
+ text-transform: none;
161
+ font-size: 0.82em;
162
+ margin-bottom: 0;
163
+ cursor: pointer;
164
+ }
165
+ .facto-proj-import-staging-link {
166
+ display: inline-block;
167
+ margin-top: 0.5em;
168
+ font-size: 0.82em;
169
+ color: var(--facto-brand);
170
+ cursor: pointer;
171
+ text-decoration: underline;
172
+ }
173
+ `,
174
+
175
+ Templates:
176
+ [
177
+ {
178
+ Hash: "Facto-Full-ProjectionDetail-Template",
179
+ Template: /*html*/`
180
+ <div class="facto-content">
181
+ <div class="facto-proj-detail-back" onclick="{~P~}.views['Facto-Full-ProjectionDetail'].goBack()">
182
+ &larr; Back to Projections
183
+ </div>
184
+
185
+ <div id="Facto-ProjectionDetail-Loading" class="facto-proj-detail-loading">Loading projection...</div>
186
+
187
+ <div id="Facto-ProjectionDetail-Container" style="display:none;">
188
+ <div class="facto-proj-detail-title-row">
189
+ <h1 id="Facto-ProjectionDetail-Title"></h1>
190
+ <div class="facto-proj-detail-actions">
191
+ <button class="facto-btn facto-btn-primary" onclick="{~P~}.views['Facto-Full-ProjectionDetail'].editSchema()">Edit Schema</button>
192
+ <button class="facto-btn facto-btn-secondary" onclick="{~P~}.views['Facto-Full-ProjectionDetail'].editMappings()">Mappings</button>
193
+ <button class="facto-btn facto-btn-secondary" onclick="{~P~}.views['Facto-Full-ProjectionDetail'].showQuery()">Query</button>
194
+ <button class="facto-btn facto-btn-danger" onclick="{~P~}.views['Facto-Full-ProjectionDetail'].deleteProjection()">Delete</button>
195
+ </div>
196
+ </div>
197
+
198
+ <div id="Facto-ProjectionDetail-Meta" class="facto-proj-meta-cards"></div>
199
+
200
+ <div class="facto-proj-section">
201
+ <div class="facto-proj-section-header">
202
+ <h2>Schema</h2>
203
+ <button class="facto-btn facto-btn-primary facto-btn-small" onclick="{~P~}.views['Facto-Full-ProjectionDetail'].editSchema()">Edit Schema</button>
204
+ </div>
205
+ <div id="Facto-ProjectionDetail-Schema"></div>
206
+ </div>
207
+
208
+ <div class="facto-proj-section">
209
+ <div class="facto-proj-section-header">
210
+ <h2>Deployed Stores</h2>
211
+ <button class="facto-btn facto-btn-primary facto-btn-small" onclick="{~P~}.views['Facto-Full-ProjectionDetail'].showDeployForm()">+ Deploy to Store</button>
212
+ </div>
213
+ <div id="Facto-ProjectionDetail-Deploy" style="display:none;">
214
+ <div class="facto-proj-deploy-form">
215
+ <div>
216
+ <label>Connection</label>
217
+ <select id="Facto-ProjectionDetail-Deploy-Connection"></select>
218
+ </div>
219
+ <div>
220
+ <label>Table Name</label>
221
+ <input type="text" id="Facto-ProjectionDetail-Deploy-TableName" placeholder="e.g. Schools">
222
+ </div>
223
+ <div>
224
+ <button class="facto-btn facto-btn-primary facto-btn-small" onclick="{~P~}.views['Facto-Full-ProjectionDetail'].deployToStore()">Deploy</button>
225
+ </div>
226
+ <div>
227
+ <button class="facto-btn facto-btn-secondary facto-btn-small" onclick="{~P~}.views['Facto-Full-ProjectionDetail'].hideDeployForm()">Cancel</button>
228
+ </div>
229
+ </div>
230
+ <div id="Facto-ProjectionDetail-Deploy-Log" class="facto-proj-deploy-log" style="display:none;"></div>
231
+ </div>
232
+ <div id="Facto-ProjectionDetail-Stores"></div>
233
+ </div>
234
+
235
+ <div class="facto-proj-section">
236
+ <div class="facto-proj-section-header">
237
+ <h2>Mappings</h2>
238
+ <button class="facto-btn facto-btn-primary facto-btn-small" onclick="{~P~}.views['Facto-Full-ProjectionDetail'].editMappings()">Manage Mappings</button>
239
+ </div>
240
+ <div id="Facto-ProjectionDetail-Mappings"></div>
241
+ </div>
242
+
243
+ <div class="facto-proj-section">
244
+ <div class="facto-proj-section-header">
245
+ <h2>Import Data</h2>
246
+ </div>
247
+ <div id="Facto-ProjectionDetail-Import"></div>
248
+ </div>
249
+ </div>
250
+
251
+ <div id="Facto-Proj-Schema-Editor-Container" style="display:none;"></div>
252
+ <div id="Facto-Proj-Mapping-Editor-Container" style="display:none;"></div>
253
+ <div id="Facto-Proj-Query-Container" style="display:none;"></div>
254
+ </div>
255
+ `
256
+ }
257
+ ],
258
+
259
+ Renderables:
260
+ [
261
+ {
262
+ RenderableHash: "Facto-Full-ProjectionDetail-Content",
263
+ TemplateHash: "Facto-Full-ProjectionDetail-Template",
264
+ DestinationAddress: "#Facto-Full-Content-Container",
265
+ RenderMethod: "replace"
266
+ }
267
+ ]
268
+ };
269
+
270
+ class FactoFullProjectionDetailView extends libPictView
271
+ {
272
+ constructor(pFable, pOptions, pServiceHash)
273
+ {
274
+ super(pFable, pOptions, pServiceHash);
275
+
276
+ this._CurrentIDDataset = null;
277
+ this._ProjectionData = null;
278
+ this._Schema = null;
279
+ this._Stores = [];
280
+ this._Mappings = [];
281
+ this._Sources = [];
282
+ this._Connections = [];
283
+ this._Stats = null;
284
+ }
285
+
286
+ loadProjection(pIDDataset)
287
+ {
288
+ this._CurrentIDDataset = parseInt(pIDDataset, 10) || 0;
289
+ this._ProjectionData = null;
290
+ this._Schema = null;
291
+ this._Stores = [];
292
+ this._Mappings = [];
293
+ this._Sources = [];
294
+ this._Connections = [];
295
+ this._Stats = null;
296
+
297
+ this.render();
298
+ }
299
+
300
+ onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
301
+ {
302
+ if (this._CurrentIDDataset)
303
+ {
304
+ this._fetchAndDisplayProjection();
305
+ }
306
+
307
+ return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
308
+ }
309
+
310
+ _fetchAndDisplayProjection()
311
+ {
312
+ let tmpLoading = document.getElementById('Facto-ProjectionDetail-Loading');
313
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Container');
314
+ if (tmpLoading) tmpLoading.style.display = 'block';
315
+ if (tmpContainer) tmpContainer.style.display = 'none';
316
+
317
+ Promise.all(
318
+ [
319
+ this.pict.providers.Facto.loadProjections(),
320
+ this.pict.providers.Facto.loadProjectionSchema(this._CurrentIDDataset),
321
+ this.pict.providers.Facto.loadProjectionStores(this._CurrentIDDataset),
322
+ this.pict.providers.Facto.loadProjectionMappings(this._CurrentIDDataset),
323
+ this.pict.providers.Facto.loadSources(),
324
+ this.pict.providers.Facto.loadDatasetStats(this._CurrentIDDataset),
325
+ this.pict.providers.Facto.loadStoreConnections()
326
+ ]).then(
327
+ (pResults) =>
328
+ {
329
+ // Find the projection dataset from the full list
330
+ let tmpProjections = (pResults[0] && pResults[0].Projections) ? pResults[0].Projections : [];
331
+ this._ProjectionData = null;
332
+ for (let i = 0; i < tmpProjections.length; i++)
333
+ {
334
+ if (tmpProjections[i].IDDataset == this._CurrentIDDataset)
335
+ {
336
+ this._ProjectionData = tmpProjections[i];
337
+ break;
338
+ }
339
+ }
340
+
341
+ this._Schema = pResults[1] || {};
342
+ this._Stores = (pResults[2] && pResults[2].Stores) ? pResults[2].Stores : [];
343
+ this._Mappings = (pResults[3] && pResults[3].Mappings) ? pResults[3].Mappings : [];
344
+ this._Sources = Array.isArray(pResults[4]) ? pResults[4] : [];
345
+ this._Stats = pResults[5] || {};
346
+ this._Connections = (pResults[6] && pResults[6].Connections) ? pResults[6].Connections : [];
347
+
348
+ this._renderProjectionDetail();
349
+ });
350
+ }
351
+
352
+ _renderProjectionDetail()
353
+ {
354
+ let tmpLoading = document.getElementById('Facto-ProjectionDetail-Loading');
355
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Container');
356
+ if (tmpLoading) tmpLoading.style.display = 'none';
357
+ if (tmpContainer) tmpContainer.style.display = 'block';
358
+
359
+ let tmpProj = this._ProjectionData || {};
360
+
361
+ // Title
362
+ let tmpTitle = document.getElementById('Facto-ProjectionDetail-Title');
363
+ if (tmpTitle) tmpTitle.textContent = tmpProj.Name || ('Projection #' + this._CurrentIDDataset);
364
+
365
+ // URL state
366
+ window.history.replaceState(null, '', '#/Projection/' + this._CurrentIDDataset);
367
+
368
+ this._renderMetaCards();
369
+ this._renderSchemaSection();
370
+ this._renderStoresSection();
371
+ this._renderMappingsSection();
372
+ this._renderImportSection();
373
+ }
374
+
375
+ // ================================================================
376
+ // Meta Cards
377
+ // ================================================================
378
+
379
+ _renderMetaCards()
380
+ {
381
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Meta');
382
+ if (!tmpContainer) return;
383
+
384
+ let tmpProj = this._ProjectionData || {};
385
+ let tmpSchema = this._Schema || {};
386
+ let tmpStats = this._Stats || {};
387
+
388
+ // Count columns from schema
389
+ let tmpColumnCount = 0;
390
+ if (tmpSchema.SchemaDefinition)
391
+ {
392
+ let tmpColumns = libProjectionConstants.microDDLToColumns(tmpSchema.SchemaDefinition);
393
+ tmpColumnCount = tmpColumns.length;
394
+ }
395
+
396
+ let tmpHtml = '';
397
+ tmpHtml += '<div class="facto-proj-meta-card"><div class="facto-proj-meta-card-label">Dataset ID</div><div class="facto-proj-meta-card-value">' + (tmpProj.IDDataset || this._CurrentIDDataset) + '</div></div>';
398
+ tmpHtml += '<div class="facto-proj-meta-card"><div class="facto-proj-meta-card-label">Schema Version</div><div class="facto-proj-meta-card-value">v' + (tmpSchema.SchemaVersion || tmpProj.SchemaVersion || 0) + '</div></div>';
399
+ tmpHtml += '<div class="facto-proj-meta-card"><div class="facto-proj-meta-card-label">Columns</div><div class="facto-proj-meta-card-value">' + tmpColumnCount + '</div></div>';
400
+ tmpHtml += '<div class="facto-proj-meta-card"><div class="facto-proj-meta-card-label">Stores</div><div class="facto-proj-meta-card-value">' + this._Stores.length + '</div></div>';
401
+ tmpHtml += '<div class="facto-proj-meta-card"><div class="facto-proj-meta-card-label">Mappings</div><div class="facto-proj-meta-card-value">' + this._Mappings.length + '</div></div>';
402
+ tmpHtml += '<div class="facto-proj-meta-card"><div class="facto-proj-meta-card-label">Records</div><div class="facto-proj-meta-card-value">' + (tmpStats.RecordCount || 0) + '</div></div>';
403
+
404
+ tmpContainer.innerHTML = tmpHtml;
405
+ }
406
+
407
+ // ================================================================
408
+ // Schema Section
409
+ // ================================================================
410
+
411
+ _renderSchemaSection()
412
+ {
413
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Schema');
414
+ if (!tmpContainer) return;
415
+
416
+ let tmpSchema = this._Schema || {};
417
+ let tmpDDL = tmpSchema.SchemaDefinition || '';
418
+
419
+ if (!tmpDDL)
420
+ {
421
+ tmpContainer.innerHTML = '<div class="facto-card" style="text-align:center; padding:1.5em; color:var(--facto-text-tertiary);">No schema defined yet. Click "Edit Schema" to create one.</div>';
422
+ return;
423
+ }
424
+
425
+ let tmpHtml = '';
426
+ tmpHtml += '<div class="facto-proj-ddl-preview">' + this._escapeHtml(tmpDDL) + '</div>';
427
+ tmpContainer.innerHTML = tmpHtml;
428
+ }
429
+
430
+ // ================================================================
431
+ // Stores Section
432
+ // ================================================================
433
+
434
+ _renderStoresSection()
435
+ {
436
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Stores');
437
+ if (!tmpContainer) return;
438
+
439
+ if (this._Stores.length === 0)
440
+ {
441
+ tmpContainer.innerHTML = '<div class="facto-card" style="text-align:center; padding:1.5em; color:var(--facto-text-tertiary);">No stores deployed yet. Click "+ Deploy to Store" to create one.</div>';
442
+ return;
443
+ }
444
+
445
+ // Build connection lookup
446
+ let tmpConnMap = {};
447
+ for (let i = 0; i < this._Connections.length; i++)
448
+ {
449
+ tmpConnMap[this._Connections[i].IDStoreConnection] = this._Connections[i];
450
+ }
451
+
452
+ let tmpHtml = '<table class="facto-table"><thead><tr>';
453
+ tmpHtml += '<th>ID</th><th>Connection</th><th>Target Table</th><th>Status</th><th>Deployed At</th><th>Actions</th>';
454
+ tmpHtml += '</tr></thead><tbody>';
455
+
456
+ for (let i = 0; i < this._Stores.length; i++)
457
+ {
458
+ let tmpStore = this._Stores[i];
459
+ let tmpConn = tmpConnMap[tmpStore.IDStoreConnection] || {};
460
+ let tmpConnName = tmpConn.Name ? (tmpConn.Name + ' (' + (tmpConn.Type || '') + ')') : ('#' + (tmpStore.IDStoreConnection || '?'));
461
+ let tmpStatusClass = (tmpStore.Status === 'Deployed') ? 'deployed' : (tmpStore.Status === 'Failed') ? 'failed' : 'pending';
462
+ let tmpDeployedAt = tmpStore.DeployedAt ? new Date(tmpStore.DeployedAt).toLocaleString() : '\u2014';
463
+
464
+ tmpHtml += '<tr>';
465
+ tmpHtml += '<td>' + (tmpStore.IDProjectionStore || '') + '</td>';
466
+ tmpHtml += '<td>' + this._escapeHtml(tmpConnName) + '</td>';
467
+ tmpHtml += '<td><strong>' + (tmpStore.TargetTableName || '\u2014') + '</strong></td>';
468
+ tmpHtml += '<td><span class="facto-status-badge ' + tmpStatusClass + '">' + (tmpStore.Status || 'Unknown') + '</span></td>';
469
+ tmpHtml += '<td>' + tmpDeployedAt + '</td>';
470
+ tmpHtml += '<td>';
471
+ tmpHtml += '<button class="facto-btn facto-btn-secondary facto-btn-small" onclick="pict.views[\'Facto-Full-ProjectionDetail\'].redeployStore(' + tmpStore.IDProjectionStore + ', ' + tmpStore.IDStoreConnection + ', \'' + (tmpStore.TargetTableName || '').replace(/'/g, "\\'") + '\')">Redeploy</button> ';
472
+ tmpHtml += '<button class="facto-btn facto-btn-danger facto-btn-small" onclick="pict.views[\'Facto-Full-ProjectionDetail\'].confirmDeleteStore(' + tmpStore.IDProjectionStore + ', \'' + (tmpStore.TargetTableName || '').replace(/'/g, "\\'") + '\')">Delete</button>';
473
+ tmpHtml += '</td>';
474
+ tmpHtml += '</tr>';
475
+ }
476
+
477
+ tmpHtml += '</tbody></table>';
478
+ tmpContainer.innerHTML = tmpHtml;
479
+ }
480
+
481
+ async confirmDeleteStore(pIDProjectionStore, pTableName)
482
+ {
483
+ // First confirmation
484
+ let tmpConfirmed = await this.pict.views['Pict-Section-Modal'].confirm('Are you sure you want to delete the deployed store "' + pTableName + '"?\n\nThis will DROP the table and permanently destroy all data in it.', { title: 'Delete Store', confirmLabel: 'Delete', dangerous: true });
485
+ if (!tmpConfirmed)
486
+ {
487
+ return;
488
+ }
489
+
490
+ // Second confirmation — require typing the table name
491
+ let tmpSecondConfirm = await this.pict.views['Pict-Section-Modal'].confirm('This action is IRREVERSIBLE. Are you absolutely sure you want to delete "' + pTableName + '"?', { title: 'Confirm Deletion', confirmLabel: 'Yes, Delete ' + pTableName, dangerous: true });
492
+ if (!tmpSecondConfirm)
493
+ {
494
+ this.pict.views['Pict-Section-Modal'].toast('Deletion cancelled.', {type: 'warning'});
495
+ return;
496
+ }
497
+
498
+ this.deleteStore(pIDProjectionStore, pTableName);
499
+ }
500
+
501
+ deleteStore(pIDProjectionStore, pTableName)
502
+ {
503
+ this.pict.views['Pict-Section-Modal'].toast('Deleting store "' + pTableName + '"...', {type: 'info'});
504
+
505
+ this.pict.providers.Facto.deleteProjectionStore(pIDProjectionStore).then(
506
+ (pResponse) =>
507
+ {
508
+ if (pResponse && pResponse.Error)
509
+ {
510
+ this.pict.views['Pict-Section-Modal'].toast('Error: ' + pResponse.Error, {type: 'error'});
511
+ return;
512
+ }
513
+
514
+ this.pict.views['Pict-Section-Modal'].toast('Store "' + pTableName + '" deleted successfully.', {type: 'success'});
515
+
516
+ // Remove from local stores list and re-render
517
+ this._Stores = this._Stores.filter(
518
+ function(s) { return s.IDProjectionStore !== pIDProjectionStore; });
519
+ this._renderStoresSection();
520
+ this._renderImportSection();
521
+ });
522
+ }
523
+
524
+ // ================================================================
525
+ // Mappings Section
526
+ // ================================================================
527
+
528
+ _renderMappingsSection()
529
+ {
530
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Mappings');
531
+ if (!tmpContainer) return;
532
+
533
+ if (this._Mappings.length === 0)
534
+ {
535
+ tmpContainer.innerHTML = '<div class="facto-card" style="text-align:center; padding:1.5em; color:var(--facto-text-tertiary);">No mappings yet. Create a mapping to link source data to this projection.</div>';
536
+ return;
537
+ }
538
+
539
+ // Build a lookup for source names
540
+ let tmpSourceMap = {};
541
+ for (let i = 0; i < this._Sources.length; i++)
542
+ {
543
+ tmpSourceMap[this._Sources[i].IDSource] = this._Sources[i].Name || this._Sources[i].Hash || ('#' + this._Sources[i].IDSource);
544
+ }
545
+
546
+ // Build a lookup for store names
547
+ let tmpStoreMap = {};
548
+ for (let i = 0; i < this._Stores.length; i++)
549
+ {
550
+ tmpStoreMap[this._Stores[i].IDProjectionStore] = this._Stores[i].TargetTableName || ('Store ' + this._Stores[i].IDProjectionStore);
551
+ }
552
+
553
+ let tmpHtml = '<table class="facto-table"><thead><tr>';
554
+ tmpHtml += '<th>ID</th><th>Name</th><th>Source</th><th>Target Stores</th><th>Active</th>';
555
+ tmpHtml += '</tr></thead><tbody>';
556
+
557
+ for (let i = 0; i < this._Mappings.length; i++)
558
+ {
559
+ let tmpMapping = this._Mappings[i];
560
+ let tmpSourceName = tmpSourceMap[tmpMapping.IDSource] || ('#' + (tmpMapping.IDSource || '?'));
561
+ let tmpActive = tmpMapping.Active
562
+ ? '<span class="facto-badge facto-badge-success">\u2714</span>'
563
+ : '<span class="facto-badge facto-badge-muted">\u2718</span>';
564
+
565
+ // Parse TargetStores from config
566
+ let tmpStoreNames = [];
567
+ try
568
+ {
569
+ let tmpConfig = JSON.parse(tmpMapping.MappingConfiguration || '{}');
570
+ if (Array.isArray(tmpConfig.TargetStores))
571
+ {
572
+ for (let j = 0; j < tmpConfig.TargetStores.length; j++)
573
+ {
574
+ tmpStoreNames.push(tmpStoreMap[tmpConfig.TargetStores[j]] || ('#' + tmpConfig.TargetStores[j]));
575
+ }
576
+ }
577
+ }
578
+ catch (e) { /* ignore */ }
579
+
580
+ // Legacy fallback
581
+ if (tmpStoreNames.length === 0 && tmpMapping.IDProjectionStore)
582
+ {
583
+ tmpStoreNames.push(tmpStoreMap[tmpMapping.IDProjectionStore] || ('#' + tmpMapping.IDProjectionStore));
584
+ }
585
+
586
+ let tmpStoresDisplay = tmpStoreNames.length > 0 ? tmpStoreNames.join(', ') : '\u2014';
587
+
588
+ tmpHtml += '<tr>';
589
+ tmpHtml += '<td>' + (tmpMapping.IDProjectionMapping || '') + '</td>';
590
+ tmpHtml += '<td><strong>' + (tmpMapping.Name || '\u2014') + '</strong></td>';
591
+ tmpHtml += '<td>' + this._escapeHtml(tmpSourceName) + '</td>';
592
+ tmpHtml += '<td>' + this._escapeHtml(tmpStoresDisplay) + '</td>';
593
+ tmpHtml += '<td>' + tmpActive + '</td>';
594
+ tmpHtml += '</tr>';
595
+ }
596
+
597
+ tmpHtml += '</tbody></table>';
598
+ tmpContainer.innerHTML = tmpHtml;
599
+ }
600
+
601
+ // ================================================================
602
+ // Deploy to Store
603
+ // ================================================================
604
+
605
+ showDeployForm()
606
+ {
607
+ let tmpDeployDiv = document.getElementById('Facto-ProjectionDetail-Deploy');
608
+ if (tmpDeployDiv) tmpDeployDiv.style.display = '';
609
+
610
+ // Populate connection dropdown
611
+ let tmpSelect = document.getElementById('Facto-ProjectionDetail-Deploy-Connection');
612
+ if (tmpSelect)
613
+ {
614
+ let tmpHtml = '<option value="">Select a connection...</option>';
615
+ for (let i = 0; i < this._Connections.length; i++)
616
+ {
617
+ let tmpConn = this._Connections[i];
618
+ tmpHtml += '<option value="' + tmpConn.IDStoreConnection + '">' + this._escapeHtml(tmpConn.Name || 'Untitled') + ' (' + (tmpConn.Type || '') + ')</option>';
619
+ }
620
+ tmpSelect.innerHTML = tmpHtml;
621
+ }
622
+
623
+ // Default table name from projection name
624
+ let tmpTableInput = document.getElementById('Facto-ProjectionDetail-Deploy-TableName');
625
+ if (tmpTableInput && !tmpTableInput.value)
626
+ {
627
+ let tmpName = this._ProjectionData ? this._ProjectionData.Name : 'Projection';
628
+ tmpTableInput.value = (tmpName || 'Projection').replace(/[^a-zA-Z0-9_]/g, '_');
629
+ }
630
+
631
+ // Hide old log
632
+ let tmpLog = document.getElementById('Facto-ProjectionDetail-Deploy-Log');
633
+ if (tmpLog) tmpLog.style.display = 'none';
634
+ }
635
+
636
+ hideDeployForm()
637
+ {
638
+ let tmpDeployDiv = document.getElementById('Facto-ProjectionDetail-Deploy');
639
+ if (tmpDeployDiv) tmpDeployDiv.style.display = 'none';
640
+ }
641
+
642
+ deployToStore()
643
+ {
644
+ let tmpSelect = document.getElementById('Facto-ProjectionDetail-Deploy-Connection');
645
+ let tmpTableInput = document.getElementById('Facto-ProjectionDetail-Deploy-TableName');
646
+ let tmpLog = document.getElementById('Facto-ProjectionDetail-Deploy-Log');
647
+
648
+ let tmpIDConn = tmpSelect ? parseInt(tmpSelect.value, 10) : 0;
649
+ let tmpTableName = tmpTableInput ? tmpTableInput.value.trim() : '';
650
+
651
+ if (!tmpIDConn)
652
+ {
653
+ this.pict.views['Pict-Section-Modal'].toast('Select a connection first.', {type: 'warning'});
654
+ return;
655
+ }
656
+ if (!tmpTableName)
657
+ {
658
+ this.pict.views['Pict-Section-Modal'].toast('Enter a table name.', {type: 'warning'});
659
+ return;
660
+ }
661
+
662
+ if (tmpLog)
663
+ {
664
+ tmpLog.style.display = 'block';
665
+ tmpLog.textContent = 'Deploying...';
666
+ }
667
+
668
+ this.pict.providers.Facto.deployProjection(this._CurrentIDDataset, tmpIDConn, tmpTableName).then(
669
+ (pResponse) =>
670
+ {
671
+ if (tmpLog)
672
+ {
673
+ if (pResponse && pResponse.Log)
674
+ {
675
+ tmpLog.textContent = pResponse.Log;
676
+ }
677
+ else if (pResponse && pResponse.Error)
678
+ {
679
+ tmpLog.textContent = 'ERROR: ' + pResponse.Error;
680
+ this.pict.views['Pict-Section-Modal'].toast('Deploy failed: ' + pResponse.Error, {type: 'error'});
681
+ return;
682
+ }
683
+ else
684
+ {
685
+ tmpLog.textContent = 'Deployment complete.';
686
+ }
687
+ }
688
+
689
+ this.pict.views['Pict-Section-Modal'].toast('Store deployed successfully.', {type: 'success'});
690
+
691
+ // Refresh stores list
692
+ this.pict.providers.Facto.loadProjectionStores(this._CurrentIDDataset).then(
693
+ (pResult) =>
694
+ {
695
+ this._Stores = (pResult && pResult.Stores) ? pResult.Stores : [];
696
+ this._renderStoresSection();
697
+ this._renderImportSection();
698
+ this._renderMetaCards();
699
+ });
700
+ });
701
+ }
702
+
703
+ async redeployStore(pIDProjectionStore, pIDStoreConnection, pTableName)
704
+ {
705
+ let tmpConfirmed = await this.pict.views['Pict-Section-Modal'].confirm('Redeploy schema to "' + pTableName + '"? This will update the table structure.', { title: 'Redeploy Schema', confirmLabel: 'Redeploy', dangerous: true });
706
+ if (!tmpConfirmed) return;
707
+
708
+ let tmpLog = document.getElementById('Facto-ProjectionDetail-Deploy-Log');
709
+ let tmpDeployDiv = document.getElementById('Facto-ProjectionDetail-Deploy');
710
+ if (tmpDeployDiv) tmpDeployDiv.style.display = '';
711
+ if (tmpLog)
712
+ {
713
+ tmpLog.style.display = 'block';
714
+ tmpLog.textContent = 'Redeploying...';
715
+ }
716
+
717
+ this.pict.providers.Facto.deployProjection(this._CurrentIDDataset, pIDStoreConnection, pTableName).then(
718
+ (pResponse) =>
719
+ {
720
+ if (tmpLog)
721
+ {
722
+ if (pResponse && pResponse.Log)
723
+ {
724
+ tmpLog.textContent = pResponse.Log;
725
+ }
726
+ else if (pResponse && pResponse.Error)
727
+ {
728
+ tmpLog.textContent = 'ERROR: ' + pResponse.Error;
729
+ this.pict.views['Pict-Section-Modal'].toast('Redeploy failed: ' + pResponse.Error, {type: 'error'});
730
+ return;
731
+ }
732
+ else
733
+ {
734
+ tmpLog.textContent = 'Redeployment complete.';
735
+ }
736
+ }
737
+
738
+ this.pict.views['Pict-Section-Modal'].toast('Store redeployed successfully.', {type: 'success'});
739
+
740
+ // Refresh stores list
741
+ this.pict.providers.Facto.loadProjectionStores(this._CurrentIDDataset).then(
742
+ (pResult) =>
743
+ {
744
+ this._Stores = (pResult && pResult.Stores) ? pResult.Stores : [];
745
+ this._renderStoresSection();
746
+ this._renderImportSection();
747
+ this._renderMetaCards();
748
+ });
749
+ });
750
+ }
751
+
752
+ // ================================================================
753
+ // Import Data
754
+ // ================================================================
755
+
756
+ _renderImportSection()
757
+ {
758
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Import');
759
+ if (!tmpContainer) return;
760
+
761
+ if (this._Mappings.length === 0 || this._Stores.length === 0)
762
+ {
763
+ tmpContainer.innerHTML = '<div class="facto-card" style="text-align:center; padding:1.5em; color:var(--facto-text-tertiary);">Configure at least one mapping and deploy a store before importing data.</div>';
764
+ return;
765
+ }
766
+
767
+ let tmpHtml = '<div class="facto-proj-import-form">';
768
+
769
+ // Mapping dropdown
770
+ tmpHtml += '<div>';
771
+ tmpHtml += '<label>Mapping</label>';
772
+ tmpHtml += '<select id="Facto-ProjectionDetail-Import-Mapping" onchange="pict.views[\'Facto-Full-ProjectionDetail\']._onImportMappingChange()">';
773
+ for (let i = 0; i < this._Mappings.length; i++)
774
+ {
775
+ let tmpMapping = this._Mappings[i];
776
+ tmpHtml += '<option value="' + tmpMapping.IDProjectionMapping + '">' + this._escapeHtml(tmpMapping.Name || ('Mapping #' + tmpMapping.IDProjectionMapping)) + '</option>';
777
+ }
778
+ tmpHtml += '</select>';
779
+ tmpHtml += '</div>';
780
+
781
+ // Store dropdown
782
+ tmpHtml += '<div>';
783
+ tmpHtml += '<label>Target Store</label>';
784
+ tmpHtml += '<select id="Facto-ProjectionDetail-Import-Store">';
785
+ for (let i = 0; i < this._Stores.length; i++)
786
+ {
787
+ let tmpStore = this._Stores[i];
788
+ tmpHtml += '<option value="' + tmpStore.IDProjectionStore + '">' + this._escapeHtml(tmpStore.TargetTableName || ('Store #' + tmpStore.IDProjectionStore)) + '</option>';
789
+ }
790
+ tmpHtml += '</select>';
791
+ tmpHtml += '</div>';
792
+
793
+ // Run Import button
794
+ tmpHtml += '<div>';
795
+ tmpHtml += '<button class="facto-btn facto-btn-primary facto-btn-small" onclick="pict.views[\'Facto-Full-ProjectionDetail\'].runImport()">Run Import</button>';
796
+ tmpHtml += '</div>';
797
+
798
+ // Spacer
799
+ tmpHtml += '<div></div>';
800
+
801
+ // Stage checkbox — full row
802
+ tmpHtml += '<div class="facto-proj-import-stage-row">';
803
+ tmpHtml += '<input type="checkbox" id="Facto-ProjectionDetail-Import-Stage">';
804
+ tmpHtml += '<label for="Facto-ProjectionDetail-Import-Stage">Stage comprehension for inspection</label>';
805
+ tmpHtml += '</div>';
806
+
807
+ tmpHtml += '</div>';
808
+
809
+ // Log area (hidden by default)
810
+ tmpHtml += '<div id="Facto-ProjectionDetail-Import-Log" class="facto-proj-deploy-log" style="display:none;"></div>';
811
+
812
+ // Staging download link area
813
+ tmpHtml += '<div id="Facto-ProjectionDetail-Import-Staging"></div>';
814
+
815
+ tmpContainer.innerHTML = tmpHtml;
816
+
817
+ // If there's a first mapping, filter stores for it
818
+ if (this._Mappings.length > 0)
819
+ {
820
+ this._onImportMappingChange();
821
+ }
822
+ }
823
+
824
+ _onImportMappingChange()
825
+ {
826
+ let tmpMappingSelect = document.getElementById('Facto-ProjectionDetail-Import-Mapping');
827
+ let tmpStoreSelect = document.getElementById('Facto-ProjectionDetail-Import-Store');
828
+ if (!tmpMappingSelect || !tmpStoreSelect) return;
829
+
830
+ let tmpIDMapping = parseInt(tmpMappingSelect.value, 10);
831
+
832
+ // Find the mapping config to see if it has TargetStores
833
+ let tmpTargetStores = null;
834
+ for (let i = 0; i < this._Mappings.length; i++)
835
+ {
836
+ if (this._Mappings[i].IDProjectionMapping == tmpIDMapping)
837
+ {
838
+ try
839
+ {
840
+ let tmpConfig = JSON.parse(this._Mappings[i].MappingConfiguration || '{}');
841
+ if (Array.isArray(tmpConfig.TargetStores) && tmpConfig.TargetStores.length > 0)
842
+ {
843
+ tmpTargetStores = tmpConfig.TargetStores;
844
+ }
845
+ }
846
+ catch (e) { /* ignore */ }
847
+ break;
848
+ }
849
+ }
850
+
851
+ // Rebuild store dropdown, filtering to target stores if specified
852
+ let tmpHtml = '';
853
+ let tmpFilteredHtml = '';
854
+ let tmpAllHtml = '';
855
+ for (let i = 0; i < this._Stores.length; i++)
856
+ {
857
+ let tmpStore = this._Stores[i];
858
+ let tmpOption = '<option value="' + tmpStore.IDProjectionStore + '">' + this._escapeHtml(tmpStore.TargetTableName || ('Store #' + tmpStore.IDProjectionStore)) + '</option>';
859
+ tmpAllHtml += tmpOption;
860
+ if (!tmpTargetStores || tmpTargetStores.indexOf(tmpStore.IDProjectionStore) !== -1)
861
+ {
862
+ tmpFilteredHtml += tmpOption;
863
+ }
864
+ }
865
+ // If filtering left no matches, show all stores instead
866
+ tmpStoreSelect.innerHTML = tmpFilteredHtml || tmpAllHtml;
867
+ }
868
+
869
+ runImport()
870
+ {
871
+ let tmpMappingSelect = document.getElementById('Facto-ProjectionDetail-Import-Mapping');
872
+ let tmpStoreSelect = document.getElementById('Facto-ProjectionDetail-Import-Store');
873
+ let tmpStageCheckbox = document.getElementById('Facto-ProjectionDetail-Import-Stage');
874
+ let tmpLog = document.getElementById('Facto-ProjectionDetail-Import-Log');
875
+ let tmpStagingDiv = document.getElementById('Facto-ProjectionDetail-Import-Staging');
876
+
877
+ let tmpIDMapping = tmpMappingSelect ? parseInt(tmpMappingSelect.value, 10) : 0;
878
+ let tmpIDStore = tmpStoreSelect ? parseInt(tmpStoreSelect.value, 10) : 0;
879
+ let tmpStageComprehension = tmpStageCheckbox ? tmpStageCheckbox.checked : false;
880
+
881
+ if (!tmpIDMapping)
882
+ {
883
+ this.pict.views['Pict-Section-Modal'].toast('Select a mapping first.', {type: 'warning'});
884
+ return;
885
+ }
886
+ if (!tmpIDStore)
887
+ {
888
+ this.pict.views['Pict-Section-Modal'].toast('Select a target store first.', {type: 'warning'});
889
+ return;
890
+ }
891
+
892
+ if (tmpLog)
893
+ {
894
+ tmpLog.style.display = 'block';
895
+ tmpLog.textContent = 'Running import...';
896
+ }
897
+ if (tmpStagingDiv)
898
+ {
899
+ tmpStagingDiv.innerHTML = '';
900
+ }
901
+
902
+ this.pict.providers.Facto.executeImport(this._CurrentIDDataset, tmpIDMapping, tmpIDStore, 100, tmpStageComprehension).then(
903
+ (pResponse) =>
904
+ {
905
+ if (tmpLog)
906
+ {
907
+ if (pResponse && pResponse.Log)
908
+ {
909
+ tmpLog.textContent = pResponse.Log;
910
+ }
911
+ else if (pResponse && pResponse.Error)
912
+ {
913
+ tmpLog.textContent = 'ERROR: ' + pResponse.Error;
914
+ this.pict.views['Pict-Section-Modal'].toast('Import failed: ' + pResponse.Error, {type: 'error'});
915
+ return;
916
+ }
917
+ else
918
+ {
919
+ tmpLog.textContent = 'Import complete.';
920
+ }
921
+ }
922
+
923
+ if (pResponse && pResponse.Success)
924
+ {
925
+ this.pict.views['Pict-Section-Modal'].toast('Import complete: ' + (pResponse.RecordsCreated || 0) + ' records created.', {type: 'success'});
926
+ }
927
+ else if (pResponse && pResponse.RecordsErrored > 0)
928
+ {
929
+ this.pict.views['Pict-Section-Modal'].toast('Import completed with ' + pResponse.RecordsErrored + ' errors.', {type: 'warning'});
930
+ }
931
+
932
+ // Show staging download link if a file was generated
933
+ if (pResponse && pResponse.StagingFile && tmpStagingDiv)
934
+ {
935
+ let tmpDownloadUrl = '/facto/projection/' + this._CurrentIDDataset + '/comprehension/' + pResponse.StagingFile;
936
+ tmpStagingDiv.innerHTML = '<a class="facto-proj-import-staging-link" href="' + tmpDownloadUrl + '" target="_blank">Download staged comprehension: ' + this._escapeHtml(pResponse.StagingFile) + '</a>';
937
+ }
938
+ }).catch(
939
+ (pError) =>
940
+ {
941
+ if (tmpLog)
942
+ {
943
+ tmpLog.style.display = 'block';
944
+ tmpLog.textContent = 'ERROR: ' + (pError.message || pError);
945
+ }
946
+ this.pict.views['Pict-Section-Modal'].toast('Import failed.', {type: 'error'});
947
+ });
948
+ }
949
+
950
+ // ================================================================
951
+ // Schema Editor Delegation
952
+ // ================================================================
953
+
954
+ editSchema()
955
+ {
956
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Container');
957
+ let tmpBack = document.querySelector('.facto-proj-detail-back');
958
+ let tmpSchemaContainer = document.getElementById('Facto-Proj-Schema-Editor-Container');
959
+
960
+ if (tmpContainer) tmpContainer.style.display = 'none';
961
+ if (tmpBack) tmpBack.style.display = 'none';
962
+ if (tmpSchemaContainer) tmpSchemaContainer.style.display = '';
963
+
964
+ let tmpName = this._ProjectionData ? this._ProjectionData.Name : '';
965
+ this.pict.views['Facto-Full-SchemaEditor'].editSchema(this._CurrentIDDataset, tmpName);
966
+ }
967
+
968
+ closeSchemaEditor()
969
+ {
970
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Container');
971
+ let tmpBack = document.querySelector('.facto-proj-detail-back');
972
+ let tmpSchemaContainer = document.getElementById('Facto-Proj-Schema-Editor-Container');
973
+
974
+ if (tmpContainer) tmpContainer.style.display = '';
975
+ if (tmpBack) tmpBack.style.display = '';
976
+ if (tmpSchemaContainer) tmpSchemaContainer.style.display = 'none';
977
+
978
+ // Refresh schema section to pick up changes
979
+ this.pict.providers.Facto.loadProjectionSchema(this._CurrentIDDataset).then(
980
+ (pSchema) =>
981
+ {
982
+ this._Schema = pSchema || {};
983
+ this._renderSchemaSection();
984
+ this._renderMetaCards();
985
+ });
986
+ }
987
+
988
+ onSchemaUpdated(pSchemaVersion)
989
+ {
990
+ // Called by SchemaEditor after a successful save
991
+ if (this._ProjectionData)
992
+ {
993
+ this._ProjectionData.SchemaVersion = pSchemaVersion;
994
+ }
995
+ }
996
+
997
+ // ================================================================
998
+ // Mapping Editor Delegation
999
+ // ================================================================
1000
+
1001
+ editMappings()
1002
+ {
1003
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Container');
1004
+ let tmpBack = document.querySelector('.facto-proj-detail-back');
1005
+ let tmpMappingContainer = document.getElementById('Facto-Proj-Mapping-Editor-Container');
1006
+
1007
+ if (tmpContainer) tmpContainer.style.display = 'none';
1008
+ if (tmpBack) tmpBack.style.display = 'none';
1009
+ if (tmpMappingContainer) tmpMappingContainer.style.display = '';
1010
+
1011
+ let tmpName = this._ProjectionData ? this._ProjectionData.Name : '';
1012
+ this.pict.views['Facto-Full-MappingEditor'].editMappings(this._CurrentIDDataset, tmpName);
1013
+ }
1014
+
1015
+ closeMappingEditor()
1016
+ {
1017
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Container');
1018
+ let tmpBack = document.querySelector('.facto-proj-detail-back');
1019
+ let tmpMappingContainer = document.getElementById('Facto-Proj-Mapping-Editor-Container');
1020
+
1021
+ if (tmpContainer) tmpContainer.style.display = '';
1022
+ if (tmpBack) tmpBack.style.display = '';
1023
+ if (tmpMappingContainer) tmpMappingContainer.style.display = 'none';
1024
+
1025
+ // Refresh mappings section to pick up changes
1026
+ Promise.all(
1027
+ [
1028
+ this.pict.providers.Facto.loadProjectionMappings(this._CurrentIDDataset),
1029
+ this.pict.providers.Facto.loadProjectionStores(this._CurrentIDDataset)
1030
+ ]).then(
1031
+ (pResults) =>
1032
+ {
1033
+ this._Mappings = (pResults[0] && pResults[0].Mappings) ? pResults[0].Mappings : [];
1034
+ this._Stores = (pResults[1] && pResults[1].Stores) ? pResults[1].Stores : [];
1035
+ this._renderMappingsSection();
1036
+ this._renderStoresSection();
1037
+ this._renderMetaCards();
1038
+ });
1039
+ }
1040
+
1041
+ // ================================================================
1042
+ // Query Panel Delegation
1043
+ // ================================================================
1044
+
1045
+ showQuery()
1046
+ {
1047
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Container');
1048
+ let tmpBack = document.querySelector('.facto-proj-detail-back');
1049
+ let tmpQueryContainer = document.getElementById('Facto-Proj-Query-Container');
1050
+
1051
+ if (tmpContainer) tmpContainer.style.display = 'none';
1052
+ if (tmpBack) tmpBack.style.display = 'none';
1053
+ if (tmpQueryContainer) tmpQueryContainer.style.display = '';
1054
+
1055
+ let tmpQueryPanel = this.pict.views['Facto-Full-QueryPanel'];
1056
+ if (tmpQueryPanel)
1057
+ {
1058
+ tmpQueryPanel.render();
1059
+ }
1060
+ }
1061
+
1062
+ closeQuery()
1063
+ {
1064
+ let tmpContainer = document.getElementById('Facto-ProjectionDetail-Container');
1065
+ let tmpBack = document.querySelector('.facto-proj-detail-back');
1066
+ let tmpQueryContainer = document.getElementById('Facto-Proj-Query-Container');
1067
+
1068
+ if (tmpContainer) tmpContainer.style.display = '';
1069
+ if (tmpBack) tmpBack.style.display = '';
1070
+ if (tmpQueryContainer) tmpQueryContainer.style.display = 'none';
1071
+ }
1072
+
1073
+ // ================================================================
1074
+ // Actions
1075
+ // ================================================================
1076
+
1077
+ async deleteProjection()
1078
+ {
1079
+ let tmpName = this._ProjectionData ? this._ProjectionData.Name : this._CurrentIDDataset;
1080
+ let tmpConfirmed = await this.pict.views['Pict-Section-Modal'].confirm('Delete projection "' + tmpName + '"? This will remove the dataset and all associated mappings and stores.', { title: 'Delete Projection', confirmLabel: 'Delete', dangerous: true });
1081
+ if (!tmpConfirmed) return;
1082
+
1083
+ this.pict.providers.Facto.deleteProjection(this._CurrentIDDataset).then(
1084
+ (pResponse) =>
1085
+ {
1086
+ if (pResponse && pResponse.Error)
1087
+ {
1088
+ this.pict.views['Pict-Section-Modal'].toast('Error deleting projection: ' + pResponse.Error, {type: 'error'});
1089
+ return;
1090
+ }
1091
+
1092
+ this.pict.views['Pict-Section-Modal'].toast('Projection deleted.', {type: 'success'});
1093
+ this.pict.PictApplication.navigateTo('/Projections');
1094
+ });
1095
+ }
1096
+
1097
+ refresh()
1098
+ {
1099
+ if (this._CurrentIDDataset)
1100
+ {
1101
+ this._fetchAndDisplayProjection();
1102
+ }
1103
+ }
1104
+
1105
+ goBack()
1106
+ {
1107
+ this.pict.PictApplication.navigateTo('/Projections');
1108
+ }
1109
+
1110
+ _escapeHtml(pText)
1111
+ {
1112
+ let tmpDiv = document.createElement('div');
1113
+ tmpDiv.textContent = pText;
1114
+ return tmpDiv.innerHTML;
1115
+ }
1116
+ }
1117
+
1118
+ module.exports = FactoFullProjectionDetailView;
1119
+
1120
+ module.exports.default_configuration = _ViewConfiguration;