@smartnet360/svelte-components 0.0.106 → 0.0.107
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/dist/core/CellTable/CellTable.svelte +87 -15
- package/dist/core/CellTable/CellTable.svelte.d.ts +6 -0
- package/dist/core/CellTable/CellTablePanel.svelte +85 -2
- package/dist/core/CellTable/CellTableToolbar.svelte +71 -1
- package/dist/core/CellTable/CellTableToolbar.svelte.d.ts +17 -0
- package/dist/core/CellTable/ColumnPicker.svelte +214 -0
- package/dist/core/CellTable/ColumnPicker.svelte.d.ts +26 -0
- package/dist/core/CellTable/column-config.d.ts +16 -0
- package/dist/core/CellTable/column-config.js +72 -0
- package/dist/core/CellTable/index.d.ts +2 -1
- package/dist/core/CellTable/index.js +2 -1
- package/dist/core/CoverageMap/data/SiteStore.js +2 -2
- package/dist/shared/demo/cell-generator.js +11 -7
- package/dist/shared/demo/cell-types.d.ts +11 -11
- package/package.json +1 -1
|
@@ -57,8 +57,11 @@
|
|
|
57
57
|
let table: Tabulator | null = null;
|
|
58
58
|
let isInitialized = $state(false);
|
|
59
59
|
|
|
60
|
-
// Reactive column configuration
|
|
61
|
-
let columns = $derived(
|
|
60
|
+
// Reactive column configuration - only changes when preset changes
|
|
61
|
+
let columns = $derived.by(() => {
|
|
62
|
+
// Only depend on columnPreset to avoid unnecessary recalculations
|
|
63
|
+
return getColumnsForPreset(columnPreset, techColors, statusColors, headerFilters);
|
|
64
|
+
});
|
|
62
65
|
|
|
63
66
|
// Build Tabulator options
|
|
64
67
|
function buildOptions(): Options {
|
|
@@ -171,13 +174,7 @@
|
|
|
171
174
|
// Mark as initialized after table is ready
|
|
172
175
|
table.on('tableBuilt', () => {
|
|
173
176
|
isInitialized = true;
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Update table data when cells change
|
|
178
|
-
$effect(() => {
|
|
179
|
-
if (isInitialized && table && cells) {
|
|
180
|
-
table.replaceData(cells);
|
|
177
|
+
// Fire initial data change event
|
|
181
178
|
if (ondatachange) {
|
|
182
179
|
ondatachange({
|
|
183
180
|
type: 'load',
|
|
@@ -185,28 +182,52 @@
|
|
|
185
182
|
filteredCount: cells.length
|
|
186
183
|
});
|
|
187
184
|
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Track previous values to avoid unnecessary updates
|
|
189
|
+
let prevCellsLength = 0;
|
|
190
|
+
let prevCellsFirstId: string | null = null;
|
|
191
|
+
let prevGroupBy: string | null = null;
|
|
192
|
+
let prevColumnPreset: string | null = null;
|
|
193
|
+
|
|
194
|
+
// Update table data when cells actually change (not just reference)
|
|
195
|
+
$effect(() => {
|
|
196
|
+
const currentLength = cells?.length ?? 0;
|
|
197
|
+
const currentFirstId = cells?.[0]?.id ?? null;
|
|
198
|
+
|
|
199
|
+
// Only update if length or first item changed (rough equality check)
|
|
200
|
+
if (isInitialized && table &&
|
|
201
|
+
(currentLength !== prevCellsLength || currentFirstId !== prevCellsFirstId)) {
|
|
202
|
+
prevCellsLength = currentLength;
|
|
203
|
+
prevCellsFirstId = currentFirstId;
|
|
204
|
+
table.replaceData(cells);
|
|
205
|
+
ondatachange?.({
|
|
206
|
+
type: 'load',
|
|
207
|
+
rowCount: cells.length,
|
|
208
|
+
filteredCount: cells.length
|
|
209
|
+
});
|
|
188
210
|
}
|
|
189
211
|
});
|
|
190
212
|
|
|
191
213
|
// Update grouping when groupBy changes
|
|
192
214
|
$effect(() => {
|
|
193
|
-
if (isInitialized && table) {
|
|
215
|
+
if (isInitialized && table && groupBy !== prevGroupBy) {
|
|
216
|
+
prevGroupBy = groupBy;
|
|
194
217
|
if (groupBy === 'none') {
|
|
195
218
|
table.setGroupBy(false);
|
|
196
219
|
} else {
|
|
197
220
|
table.setGroupBy(groupBy);
|
|
198
221
|
table.setGroupHeader(getGroupHeaderFormatter(groupBy));
|
|
199
222
|
}
|
|
200
|
-
// Force redraw after grouping change
|
|
201
|
-
table.redraw(true);
|
|
202
223
|
}
|
|
203
224
|
});
|
|
204
225
|
|
|
205
226
|
// Update columns when preset changes
|
|
206
227
|
$effect(() => {
|
|
207
|
-
if (isInitialized && table &&
|
|
228
|
+
if (isInitialized && table && columnPreset !== prevColumnPreset) {
|
|
229
|
+
prevColumnPreset = columnPreset;
|
|
208
230
|
table.setColumns(columns);
|
|
209
|
-
table.redraw(true);
|
|
210
231
|
}
|
|
211
232
|
});
|
|
212
233
|
|
|
@@ -256,12 +277,63 @@
|
|
|
256
277
|
}
|
|
257
278
|
|
|
258
279
|
export function clearFilters(): void {
|
|
259
|
-
table
|
|
280
|
+
if (!table) return;
|
|
281
|
+
// Clear programmatic filters
|
|
282
|
+
table.clearFilter();
|
|
283
|
+
// Clear header filter inputs
|
|
284
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
285
|
+
(table as any).clearHeaderFilter();
|
|
260
286
|
}
|
|
261
287
|
|
|
262
288
|
export function redraw(): void {
|
|
263
289
|
table?.redraw(true);
|
|
264
290
|
}
|
|
291
|
+
|
|
292
|
+
export function collapseAll(): void {
|
|
293
|
+
if (!table) return;
|
|
294
|
+
// Use setGroupStartOpen to collapse all groups, then refresh data to apply
|
|
295
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
296
|
+
(table as any).setGroupStartOpen(false);
|
|
297
|
+
table.setData(table.getData());
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function expandAll(): void {
|
|
301
|
+
if (!table) return;
|
|
302
|
+
// Use setGroupStartOpen to expand all groups, then refresh data to apply
|
|
303
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
304
|
+
(table as any).setGroupStartOpen(true);
|
|
305
|
+
table.setData(table.getData());
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function toggleHeaderFilters(visible: boolean): void {
|
|
309
|
+
if (!table) return;
|
|
310
|
+
const headerFiltersElement = tableContainer.querySelector('.tabulator-header-filter');
|
|
311
|
+
if (headerFiltersElement) {
|
|
312
|
+
// Toggle all header filter rows
|
|
313
|
+
const filterRows = tableContainer.querySelectorAll('.tabulator-col .tabulator-header-filter');
|
|
314
|
+
filterRows.forEach(el => {
|
|
315
|
+
(el as HTMLElement).style.display = visible ? '' : 'none';
|
|
316
|
+
});
|
|
317
|
+
table.redraw();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function showColumn(field: string): void {
|
|
322
|
+
table?.showColumn(field);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function hideColumn(field: string): void {
|
|
326
|
+
table?.hideColumn(field);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function getVisibleColumns(): string[] {
|
|
330
|
+
if (!table) return [];
|
|
331
|
+
const columns = table.getColumns();
|
|
332
|
+
return columns
|
|
333
|
+
.filter(col => col.isVisible())
|
|
334
|
+
.map(col => col.getField())
|
|
335
|
+
.filter((field): field is string => !!field);
|
|
336
|
+
}
|
|
265
337
|
</script>
|
|
266
338
|
|
|
267
339
|
<div class="cell-table-container">
|
|
@@ -22,6 +22,12 @@ declare const CellTable: import("svelte").Component<Props, {
|
|
|
22
22
|
setFilter: (field: string, type: string, value: unknown) => void;
|
|
23
23
|
clearFilters: () => void;
|
|
24
24
|
redraw: () => void;
|
|
25
|
+
collapseAll: () => void;
|
|
26
|
+
expandAll: () => void;
|
|
27
|
+
toggleHeaderFilters: (visible: boolean) => void;
|
|
28
|
+
showColumn: (field: string) => void;
|
|
29
|
+
hideColumn: (field: string) => void;
|
|
30
|
+
getVisibleColumns: () => string[];
|
|
25
31
|
}, "">;
|
|
26
32
|
type CellTable = ReturnType<typeof CellTable>;
|
|
27
33
|
export default CellTable;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import CellTable from './CellTable.svelte';
|
|
4
4
|
import CellTableToolbar from './CellTableToolbar.svelte';
|
|
5
|
+
import { getColumnMetadata, getPresetVisibleFields } from './column-config';
|
|
5
6
|
import type {
|
|
6
7
|
CellData,
|
|
7
8
|
CellTableGroupField,
|
|
@@ -89,10 +90,22 @@
|
|
|
89
90
|
let filteredCount = $state(cells.length);
|
|
90
91
|
let sidebarOpen = $state(false);
|
|
91
92
|
let clickedCell: CellData | null = $state(null);
|
|
93
|
+
let tableRefSet = false;
|
|
94
|
+
let filtersVisible = $state(true);
|
|
92
95
|
|
|
93
|
-
//
|
|
96
|
+
// Column visibility management
|
|
97
|
+
const columnMeta = getColumnMetadata();
|
|
98
|
+
let visibleColumns = $state<string[]>(getPresetVisibleFields(columnPreset));
|
|
99
|
+
|
|
100
|
+
// Update visible columns when preset changes
|
|
94
101
|
$effect(() => {
|
|
95
|
-
|
|
102
|
+
visibleColumns = getPresetVisibleFields(columnPreset);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Expose table methods via tableRef - only set once
|
|
106
|
+
$effect(() => {
|
|
107
|
+
if (cellTable && !tableRefSet) {
|
|
108
|
+
tableRefSet = true;
|
|
96
109
|
tableRef = {
|
|
97
110
|
redraw: () => cellTable?.redraw()
|
|
98
111
|
};
|
|
@@ -138,6 +151,44 @@
|
|
|
138
151
|
cellTable?.clearFilters();
|
|
139
152
|
}
|
|
140
153
|
|
|
154
|
+
function handleCollapseAll() {
|
|
155
|
+
cellTable?.collapseAll();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function handleExpandAll() {
|
|
159
|
+
cellTable?.expandAll();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleToggleFilters() {
|
|
163
|
+
filtersVisible = !filtersVisible;
|
|
164
|
+
cellTable?.toggleHeaderFilters(filtersVisible);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleColumnVisibilityChange(field: string, visible: boolean) {
|
|
168
|
+
if (visible) {
|
|
169
|
+
if (!visibleColumns.includes(field)) {
|
|
170
|
+
visibleColumns = [...visibleColumns, field];
|
|
171
|
+
}
|
|
172
|
+
cellTable?.showColumn(field);
|
|
173
|
+
} else {
|
|
174
|
+
visibleColumns = visibleColumns.filter(f => f !== field);
|
|
175
|
+
cellTable?.hideColumn(field);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function handleResetColumns() {
|
|
180
|
+
const defaultFields = getPresetVisibleFields(columnPreset);
|
|
181
|
+
visibleColumns = defaultFields;
|
|
182
|
+
// Show/hide columns to match preset
|
|
183
|
+
columnMeta.forEach(col => {
|
|
184
|
+
if (defaultFields.includes(col.field)) {
|
|
185
|
+
cellTable?.showColumn(col.field);
|
|
186
|
+
} else {
|
|
187
|
+
cellTable?.hideColumn(col.field);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
141
192
|
function toggleSidebar() {
|
|
142
193
|
sidebarOpen = !sidebarOpen;
|
|
143
194
|
setTimeout(() => cellTable?.redraw(), 320);
|
|
@@ -212,6 +263,14 @@
|
|
|
212
263
|
onexportcsv={handleExportCSV}
|
|
213
264
|
onexportjson={handleExportJSON}
|
|
214
265
|
onclearfilters={handleClearFilters}
|
|
266
|
+
oncollapseall={handleCollapseAll}
|
|
267
|
+
onexpandall={handleExpandAll}
|
|
268
|
+
{filtersVisible}
|
|
269
|
+
ontogglefilters={handleToggleFilters}
|
|
270
|
+
{columnMeta}
|
|
271
|
+
{visibleColumns}
|
|
272
|
+
oncolumnvisibilitychange={handleColumnVisibilityChange}
|
|
273
|
+
onresetcolumns={handleResetColumns}
|
|
215
274
|
/>
|
|
216
275
|
{/if}
|
|
217
276
|
|
|
@@ -330,6 +389,30 @@
|
|
|
330
389
|
<dt class="col-5 text-muted">Comment</dt>
|
|
331
390
|
<dd class="col-7 fst-italic">{clickedCell.comment}</dd>
|
|
332
391
|
{/if}
|
|
392
|
+
|
|
393
|
+
<!-- Dynamic Other Properties -->
|
|
394
|
+
{#if clickedCell.other && Object.keys(clickedCell.other).length > 0}
|
|
395
|
+
<dt class="col-12 text-muted mt-2 mb-1 border-top pt-2">Other</dt>
|
|
396
|
+
|
|
397
|
+
{#each Object.entries(clickedCell.other) as [key, value]}
|
|
398
|
+
<dt class="col-5 text-muted text-capitalize">{key.replace(/_/g, ' ')}</dt>
|
|
399
|
+
<dd class="col-7">
|
|
400
|
+
{#if value === null || value === undefined}
|
|
401
|
+
<span class="text-muted fst-italic">—</span>
|
|
402
|
+
{:else if typeof value === 'boolean'}
|
|
403
|
+
<span class="badge" class:bg-success={value} class:bg-secondary={!value}>
|
|
404
|
+
{value ? 'Yes' : 'No'}
|
|
405
|
+
</span>
|
|
406
|
+
{:else if typeof value === 'number'}
|
|
407
|
+
<code>{value}</code>
|
|
408
|
+
{:else if typeof value === 'object'}
|
|
409
|
+
<code class="small text-break">{JSON.stringify(value)}</code>
|
|
410
|
+
{:else}
|
|
411
|
+
{String(value)}
|
|
412
|
+
{/if}
|
|
413
|
+
</dd>
|
|
414
|
+
{/each}
|
|
415
|
+
{/if}
|
|
333
416
|
</dl>
|
|
334
417
|
{:else}
|
|
335
418
|
<div class="text-center text-muted py-5">
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { CellTableGroupField, ColumnPreset } from './types';
|
|
3
|
+
import type { ColumnMeta } from './column-config';
|
|
4
|
+
import ColumnPicker from './ColumnPicker.svelte';
|
|
3
5
|
|
|
4
6
|
interface Props {
|
|
5
7
|
/** Current grouping field */
|
|
@@ -28,6 +30,22 @@
|
|
|
28
30
|
onexportjson?: () => void;
|
|
29
31
|
/** Clear filters event */
|
|
30
32
|
onclearfilters?: () => void;
|
|
33
|
+
/** Collapse all groups event */
|
|
34
|
+
oncollapseall?: () => void;
|
|
35
|
+
/** Expand all groups event */
|
|
36
|
+
onexpandall?: () => void;
|
|
37
|
+
/** Whether header filters are visible */
|
|
38
|
+
filtersVisible?: boolean;
|
|
39
|
+
/** Toggle filters event */
|
|
40
|
+
ontogglefilters?: () => void;
|
|
41
|
+
/** All available columns metadata */
|
|
42
|
+
columnMeta?: ColumnMeta[];
|
|
43
|
+
/** Currently visible column fields */
|
|
44
|
+
visibleColumns?: string[];
|
|
45
|
+
/** Column visibility change event */
|
|
46
|
+
oncolumnvisibilitychange?: (field: string, visible: boolean) => void;
|
|
47
|
+
/** Reset columns to preset default */
|
|
48
|
+
onresetcolumns?: () => void;
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
let {
|
|
@@ -43,7 +61,15 @@
|
|
|
43
61
|
onpresetchange,
|
|
44
62
|
onexportcsv,
|
|
45
63
|
onexportjson,
|
|
46
|
-
onclearfilters
|
|
64
|
+
onclearfilters,
|
|
65
|
+
oncollapseall,
|
|
66
|
+
onexpandall,
|
|
67
|
+
filtersVisible = true,
|
|
68
|
+
ontogglefilters,
|
|
69
|
+
columnMeta = [],
|
|
70
|
+
visibleColumns = [],
|
|
71
|
+
oncolumnvisibilitychange,
|
|
72
|
+
onresetcolumns
|
|
47
73
|
}: Props = $props();
|
|
48
74
|
|
|
49
75
|
const groupOptions: { value: CellTableGroupField; label: string }[] = [
|
|
@@ -107,6 +133,28 @@
|
|
|
107
133
|
<option value={option.value}>{option.label}</option>
|
|
108
134
|
{/each}
|
|
109
135
|
</select>
|
|
136
|
+
{#if groupBy !== 'none'}
|
|
137
|
+
<div class="btn-group ms-2">
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
class="btn btn-sm btn-outline-secondary"
|
|
141
|
+
onclick={oncollapseall}
|
|
142
|
+
title="Collapse all groups"
|
|
143
|
+
aria-label="Collapse all groups"
|
|
144
|
+
>
|
|
145
|
+
<i class="bi bi-chevron-contract"></i>
|
|
146
|
+
</button>
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
class="btn btn-sm btn-outline-secondary"
|
|
150
|
+
onclick={onexpandall}
|
|
151
|
+
title="Expand all groups"
|
|
152
|
+
aria-label="Expand all groups"
|
|
153
|
+
>
|
|
154
|
+
<i class="bi bi-chevron-expand"></i>
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
{/if}
|
|
110
158
|
</div>
|
|
111
159
|
{/if}
|
|
112
160
|
|
|
@@ -127,6 +175,15 @@
|
|
|
127
175
|
<option value={option.value}>{option.label}</option>
|
|
128
176
|
{/each}
|
|
129
177
|
</select>
|
|
178
|
+
{#if columnMeta.length > 0}
|
|
179
|
+
<ColumnPicker
|
|
180
|
+
columns={columnMeta}
|
|
181
|
+
{visibleColumns}
|
|
182
|
+
presetName={presetOptions.find(p => p.value === columnPreset)?.label ?? 'Default'}
|
|
183
|
+
onchange={oncolumnvisibilitychange}
|
|
184
|
+
onreset={onresetcolumns}
|
|
185
|
+
/>
|
|
186
|
+
{/if}
|
|
130
187
|
</div>
|
|
131
188
|
{/if}
|
|
132
189
|
|
|
@@ -134,6 +191,19 @@
|
|
|
134
191
|
|
|
135
192
|
<!-- Actions -->
|
|
136
193
|
<div class="toolbar-actions d-flex align-items-center gap-2">
|
|
194
|
+
{#if ontogglefilters}
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
class="btn btn-sm"
|
|
198
|
+
class:btn-outline-secondary={!filtersVisible}
|
|
199
|
+
class:btn-secondary={filtersVisible}
|
|
200
|
+
onclick={ontogglefilters}
|
|
201
|
+
title={filtersVisible ? 'Hide filters' : 'Show filters'}
|
|
202
|
+
aria-label={filtersVisible ? 'Hide filters' : 'Show filters'}
|
|
203
|
+
>
|
|
204
|
+
<i class="bi bi-funnel"></i>
|
|
205
|
+
</button>
|
|
206
|
+
{/if}
|
|
137
207
|
{#if onclearfilters}
|
|
138
208
|
<button
|
|
139
209
|
type="button"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CellTableGroupField, ColumnPreset } from './types';
|
|
2
|
+
import type { ColumnMeta } from './column-config';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** Current grouping field */
|
|
4
5
|
groupBy?: CellTableGroupField;
|
|
@@ -26,6 +27,22 @@ interface Props {
|
|
|
26
27
|
onexportjson?: () => void;
|
|
27
28
|
/** Clear filters event */
|
|
28
29
|
onclearfilters?: () => void;
|
|
30
|
+
/** Collapse all groups event */
|
|
31
|
+
oncollapseall?: () => void;
|
|
32
|
+
/** Expand all groups event */
|
|
33
|
+
onexpandall?: () => void;
|
|
34
|
+
/** Whether header filters are visible */
|
|
35
|
+
filtersVisible?: boolean;
|
|
36
|
+
/** Toggle filters event */
|
|
37
|
+
ontogglefilters?: () => void;
|
|
38
|
+
/** All available columns metadata */
|
|
39
|
+
columnMeta?: ColumnMeta[];
|
|
40
|
+
/** Currently visible column fields */
|
|
41
|
+
visibleColumns?: string[];
|
|
42
|
+
/** Column visibility change event */
|
|
43
|
+
oncolumnvisibilitychange?: (field: string, visible: boolean) => void;
|
|
44
|
+
/** Reset columns to preset default */
|
|
45
|
+
onresetcolumns?: () => void;
|
|
29
46
|
}
|
|
30
47
|
declare const CellTableToolbar: import("svelte").Component<Props, {}, "">;
|
|
31
48
|
type CellTableToolbar = ReturnType<typeof CellTableToolbar>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ColumnPicker - Column visibility customization dropdown
|
|
4
|
+
*
|
|
5
|
+
* Shows a dropdown panel with checkboxes to toggle individual column visibility.
|
|
6
|
+
* Works alongside presets - user can customize which columns are visible.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface ColumnInfo {
|
|
10
|
+
field: string;
|
|
11
|
+
title: string;
|
|
12
|
+
group: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** All available columns with their metadata */
|
|
17
|
+
columns: ColumnInfo[];
|
|
18
|
+
/** Currently visible column fields */
|
|
19
|
+
visibleColumns: string[];
|
|
20
|
+
/** Callback when column visibility changes */
|
|
21
|
+
onchange?: (field: string, visible: boolean) => void;
|
|
22
|
+
/** Callback to reset to preset defaults */
|
|
23
|
+
onreset?: () => void;
|
|
24
|
+
/** Current preset name for display */
|
|
25
|
+
presetName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
columns = [],
|
|
30
|
+
visibleColumns = [],
|
|
31
|
+
onchange,
|
|
32
|
+
onreset,
|
|
33
|
+
presetName = 'Default'
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
let isOpen = $state(false);
|
|
37
|
+
let dropdownRef: HTMLDivElement;
|
|
38
|
+
|
|
39
|
+
// Group columns by category
|
|
40
|
+
const groupedColumns = $derived.by(() => {
|
|
41
|
+
const groups: Record<string, ColumnInfo[]> = {};
|
|
42
|
+
for (const col of columns) {
|
|
43
|
+
if (!groups[col.group]) {
|
|
44
|
+
groups[col.group] = [];
|
|
45
|
+
}
|
|
46
|
+
groups[col.group].push(col);
|
|
47
|
+
}
|
|
48
|
+
return groups;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const groupOrder = ['Core', 'Physical', 'Network', 'Atoll', 'Position', 'Planning'];
|
|
52
|
+
|
|
53
|
+
function toggleDropdown() {
|
|
54
|
+
isOpen = !isOpen;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handleCheckboxChange(field: string, event: Event) {
|
|
58
|
+
const checked = (event.target as HTMLInputElement).checked;
|
|
59
|
+
onchange?.(field, checked);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function handleReset() {
|
|
63
|
+
onreset?.();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleSelectAll() {
|
|
67
|
+
columns.forEach(col => onchange?.(col.field, true));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function handleDeselectAll() {
|
|
71
|
+
// Keep at least core columns visible
|
|
72
|
+
columns.forEach(col => {
|
|
73
|
+
const isCore = col.group === 'Core';
|
|
74
|
+
onchange?.(col.field, isCore);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Close dropdown when clicking outside
|
|
79
|
+
function handleClickOutside(event: MouseEvent) {
|
|
80
|
+
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
|
|
81
|
+
isOpen = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
$effect(() => {
|
|
86
|
+
if (isOpen) {
|
|
87
|
+
document.addEventListener('click', handleClickOutside);
|
|
88
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<div class="column-picker position-relative" bind:this={dropdownRef}>
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
class="btn btn-sm btn-outline-secondary"
|
|
97
|
+
onclick={toggleDropdown}
|
|
98
|
+
title="Customize columns"
|
|
99
|
+
aria-label="Customize columns"
|
|
100
|
+
aria-expanded={isOpen}
|
|
101
|
+
>
|
|
102
|
+
<i class="bi bi-pencil"></i>
|
|
103
|
+
</button>
|
|
104
|
+
|
|
105
|
+
{#if isOpen}
|
|
106
|
+
<div class="column-picker-dropdown position-absolute end-0 mt-1 bg-white border rounded shadow-sm"
|
|
107
|
+
style="z-index: 1050; width: 280px; max-height: 400px;">
|
|
108
|
+
|
|
109
|
+
<!-- Header -->
|
|
110
|
+
<div class="d-flex align-items-center justify-content-between p-2 border-bottom bg-light">
|
|
111
|
+
<span class="small fw-medium">
|
|
112
|
+
<i class="bi bi-layout-three-columns text-primary"></i>
|
|
113
|
+
Customize "{presetName}"
|
|
114
|
+
</span>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
class="btn-close btn-close-sm"
|
|
118
|
+
onclick={() => isOpen = false}
|
|
119
|
+
aria-label="Close"
|
|
120
|
+
></button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Quick actions -->
|
|
124
|
+
<div class="d-flex gap-2 p-2 border-bottom">
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
class="btn btn-sm btn-outline-secondary flex-grow-1"
|
|
128
|
+
onclick={handleSelectAll}
|
|
129
|
+
>
|
|
130
|
+
Select All
|
|
131
|
+
</button>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
class="btn btn-sm btn-outline-secondary flex-grow-1"
|
|
135
|
+
onclick={handleDeselectAll}
|
|
136
|
+
>
|
|
137
|
+
Core Only
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- Scrollable column list -->
|
|
142
|
+
<div class="column-list overflow-auto" style="max-height: 280px;">
|
|
143
|
+
{#each groupOrder as groupName}
|
|
144
|
+
{#if groupedColumns[groupName]}
|
|
145
|
+
<div class="column-group">
|
|
146
|
+
<div class="px-2 py-1 bg-body-tertiary small fw-medium text-muted border-bottom">
|
|
147
|
+
{groupName}
|
|
148
|
+
</div>
|
|
149
|
+
{#each groupedColumns[groupName] as col}
|
|
150
|
+
<label class="d-flex align-items-center px-2 py-1 column-item">
|
|
151
|
+
<input
|
|
152
|
+
type="checkbox"
|
|
153
|
+
class="form-check-input me-2 mt-0"
|
|
154
|
+
checked={visibleColumns.includes(col.field)}
|
|
155
|
+
onchange={(e) => handleCheckboxChange(col.field, e)}
|
|
156
|
+
/>
|
|
157
|
+
<span class="small">{col.title}</span>
|
|
158
|
+
<code class="ms-auto small text-muted">{col.field}</code>
|
|
159
|
+
</label>
|
|
160
|
+
{/each}
|
|
161
|
+
</div>
|
|
162
|
+
{/if}
|
|
163
|
+
{/each}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- Footer with reset -->
|
|
167
|
+
<div class="p-2 border-top bg-light">
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
class="btn btn-sm btn-outline-primary w-100"
|
|
171
|
+
onclick={handleReset}
|
|
172
|
+
>
|
|
173
|
+
<i class="bi bi-arrow-counterclockwise"></i>
|
|
174
|
+
Reset to "{presetName}" Default
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
{/if}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<style>
|
|
182
|
+
.column-picker-dropdown {
|
|
183
|
+
animation: fadeIn 0.15s ease-out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@keyframes fadeIn {
|
|
187
|
+
from {
|
|
188
|
+
opacity: 0;
|
|
189
|
+
transform: translateY(-4px);
|
|
190
|
+
}
|
|
191
|
+
to {
|
|
192
|
+
opacity: 1;
|
|
193
|
+
transform: translateY(0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.column-item {
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
transition: background-color 0.1s;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.column-item:hover {
|
|
203
|
+
background-color: var(--bs-tertiary-bg, #f8f9fa);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.form-check-input {
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.btn-close-sm {
|
|
211
|
+
font-size: 0.65rem;
|
|
212
|
+
padding: 0.25rem;
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColumnPicker - Column visibility customization dropdown
|
|
3
|
+
*
|
|
4
|
+
* Shows a dropdown panel with checkboxes to toggle individual column visibility.
|
|
5
|
+
* Works alongside presets - user can customize which columns are visible.
|
|
6
|
+
*/
|
|
7
|
+
interface ColumnInfo {
|
|
8
|
+
field: string;
|
|
9
|
+
title: string;
|
|
10
|
+
group: string;
|
|
11
|
+
}
|
|
12
|
+
interface Props {
|
|
13
|
+
/** All available columns with their metadata */
|
|
14
|
+
columns: ColumnInfo[];
|
|
15
|
+
/** Currently visible column fields */
|
|
16
|
+
visibleColumns: string[];
|
|
17
|
+
/** Callback when column visibility changes */
|
|
18
|
+
onchange?: (field: string, visible: boolean) => void;
|
|
19
|
+
/** Callback to reset to preset defaults */
|
|
20
|
+
onreset?: () => void;
|
|
21
|
+
/** Current preset name for display */
|
|
22
|
+
presetName?: string;
|
|
23
|
+
}
|
|
24
|
+
declare const ColumnPicker: import("svelte").Component<Props, {}, "">;
|
|
25
|
+
type ColumnPicker = ReturnType<typeof ColumnPicker>;
|
|
26
|
+
export default ColumnPicker;
|
|
@@ -61,3 +61,19 @@ export declare function getColumnsForPreset(preset: ColumnPreset, techColors?: T
|
|
|
61
61
|
* Get group header formatter for a specific field
|
|
62
62
|
*/
|
|
63
63
|
export declare function getGroupHeaderFormatter(groupField: string): (value: unknown, count: number) => string;
|
|
64
|
+
/**
|
|
65
|
+
* Column metadata for the column picker
|
|
66
|
+
*/
|
|
67
|
+
export interface ColumnMeta {
|
|
68
|
+
field: string;
|
|
69
|
+
title: string;
|
|
70
|
+
group: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get column metadata for the column picker UI
|
|
74
|
+
*/
|
|
75
|
+
export declare function getColumnMetadata(): ColumnMeta[];
|
|
76
|
+
/**
|
|
77
|
+
* Get default visible columns for a preset
|
|
78
|
+
*/
|
|
79
|
+
export declare function getPresetVisibleFields(preset: ColumnPreset): string[];
|
|
@@ -463,3 +463,75 @@ export function getGroupHeaderFormatter(groupField) {
|
|
|
463
463
|
<span class="text-muted">(${count} cell${count !== 1 ? 's' : ''})</span>`;
|
|
464
464
|
};
|
|
465
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Get column metadata for the column picker UI
|
|
468
|
+
*/
|
|
469
|
+
export function getColumnMetadata() {
|
|
470
|
+
return [
|
|
471
|
+
// Core
|
|
472
|
+
{ field: 'id', title: 'ID', group: 'Core' },
|
|
473
|
+
{ field: 'cellName', title: 'Cell Name', group: 'Core' },
|
|
474
|
+
{ field: 'siteId', title: 'Site ID', group: 'Core' },
|
|
475
|
+
{ field: 'tech', title: 'Technology', group: 'Core' },
|
|
476
|
+
{ field: 'fband', title: 'Band', group: 'Core' },
|
|
477
|
+
{ field: 'frq', title: 'Frequency', group: 'Core' },
|
|
478
|
+
{ field: 'status', title: 'Status', group: 'Core' },
|
|
479
|
+
{ field: 'type', title: 'Type', group: 'Core' },
|
|
480
|
+
{ field: 'onAirDate', title: 'On Air Date', group: 'Core' },
|
|
481
|
+
// Physical
|
|
482
|
+
{ field: 'antenna', title: 'Antenna', group: 'Physical' },
|
|
483
|
+
{ field: 'azimuth', title: 'Azimuth', group: 'Physical' },
|
|
484
|
+
{ field: 'height', title: 'Height', group: 'Physical' },
|
|
485
|
+
{ field: 'electricalTilt', title: 'E-Tilt', group: 'Physical' },
|
|
486
|
+
{ field: 'beamwidth', title: 'Beamwidth', group: 'Physical' },
|
|
487
|
+
// Network
|
|
488
|
+
{ field: 'dlEarfn', title: 'DL EARFCN', group: 'Network' },
|
|
489
|
+
{ field: 'bcch', title: 'BCCH', group: 'Network' },
|
|
490
|
+
{ field: 'pci', title: 'PCI', group: 'Network' },
|
|
491
|
+
{ field: 'rru', title: 'RRU', group: 'Network' },
|
|
492
|
+
{ field: 'cellID', title: 'Cell ID', group: 'Network' },
|
|
493
|
+
{ field: 'cellId2G', title: 'Cell ID 2G', group: 'Network' },
|
|
494
|
+
{ field: 'txId', title: 'TX ID', group: 'Network' },
|
|
495
|
+
{ field: 'ctrlid', title: 'Ctrl ID', group: 'Network' },
|
|
496
|
+
{ field: 'nwtET', title: 'NWT ET', group: 'Network' },
|
|
497
|
+
{ field: 'nwtPW', title: 'NWT PW', group: 'Network' },
|
|
498
|
+
{ field: 'nwtRS', title: 'NWT RS', group: 'Network' },
|
|
499
|
+
{ field: 'nwtBW', title: 'NWT BW', group: 'Network' },
|
|
500
|
+
// Atoll
|
|
501
|
+
{ field: 'atollET', title: 'Atoll ET', group: 'Atoll' },
|
|
502
|
+
{ field: 'atollPW', title: 'Atoll PW', group: 'Atoll' },
|
|
503
|
+
{ field: 'atollRS', title: 'Atoll RS', group: 'Atoll' },
|
|
504
|
+
{ field: 'atollBW', title: 'Atoll BW', group: 'Atoll' },
|
|
505
|
+
// Position
|
|
506
|
+
{ field: 'latitude', title: 'Latitude', group: 'Position' },
|
|
507
|
+
{ field: 'longitude', title: 'Longitude', group: 'Position' },
|
|
508
|
+
{ field: 'siteLatitude', title: 'Site Latitude', group: 'Position' },
|
|
509
|
+
{ field: 'siteLongitude', title: 'Site Longitude', group: 'Position' },
|
|
510
|
+
{ field: 'dx', title: 'DX', group: 'Position' },
|
|
511
|
+
{ field: 'dy', title: 'DY', group: 'Position' },
|
|
512
|
+
// Planning
|
|
513
|
+
{ field: 'planner', title: 'Planner', group: 'Planning' },
|
|
514
|
+
{ field: 'comment', title: 'Comment', group: 'Planning' },
|
|
515
|
+
{ field: 'customSubgroup', title: 'Subgroup', group: 'Planning' },
|
|
516
|
+
];
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Get default visible columns for a preset
|
|
520
|
+
*/
|
|
521
|
+
export function getPresetVisibleFields(preset) {
|
|
522
|
+
switch (preset) {
|
|
523
|
+
case 'compact':
|
|
524
|
+
return ['cellName', 'siteId', 'tech', 'fband', 'status'];
|
|
525
|
+
case 'full':
|
|
526
|
+
return getColumnMetadata().map(c => c.field);
|
|
527
|
+
case 'physical':
|
|
528
|
+
return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.physical];
|
|
529
|
+
case 'network':
|
|
530
|
+
return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.network];
|
|
531
|
+
case 'planning':
|
|
532
|
+
return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.planning];
|
|
533
|
+
case 'default':
|
|
534
|
+
default:
|
|
535
|
+
return ['id', 'cellName', 'siteId', 'tech', 'fband', 'frq', 'status', 'azimuth', 'height', 'antenna'];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
@@ -7,6 +7,7 @@ export { default as CellTable } from './CellTable.svelte';
|
|
|
7
7
|
export { default as CellTableToolbar } from './CellTableToolbar.svelte';
|
|
8
8
|
export { default as CellTablePanel } from './CellTablePanel.svelte';
|
|
9
9
|
export { default as CellTableDemo } from './CellTableDemo.svelte';
|
|
10
|
+
export { default as ColumnPicker } from './ColumnPicker.svelte';
|
|
10
11
|
export { generateCells, generateCellsFromPreset, getGeneratorInfo, GENERATOR_PRESETS, type CellGeneratorConfig, type GeneratorPreset } from '../../shared/demo';
|
|
11
12
|
export type { CellData, CellTableGroupField, ColumnPreset, ColumnVisibility, TechColorMap, StatusColorMap, CellTableProps, RowSelectionEvent, RowClickEvent, RowDblClickEvent, DataChangeEvent, CellTableColumn, ColumnGroups } from './types';
|
|
12
|
-
export { DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS, COLUMN_GROUPS, getAllColumns, getColumnsForPreset, getGroupHeaderFormatter, createTechFormatter, createStatusFormatter, createFbandFormatter, numberFormatter, coordinateFormatter, azimuthFormatter, heightFormatter } from './column-config';
|
|
13
|
+
export { DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS, COLUMN_GROUPS, getAllColumns, getColumnsForPreset, getGroupHeaderFormatter, getColumnMetadata, getPresetVisibleFields, createTechFormatter, createStatusFormatter, createFbandFormatter, numberFormatter, coordinateFormatter, azimuthFormatter, heightFormatter, type ColumnMeta } from './column-config';
|
|
@@ -8,9 +8,10 @@ export { default as CellTable } from './CellTable.svelte';
|
|
|
8
8
|
export { default as CellTableToolbar } from './CellTableToolbar.svelte';
|
|
9
9
|
export { default as CellTablePanel } from './CellTablePanel.svelte';
|
|
10
10
|
export { default as CellTableDemo } from './CellTableDemo.svelte';
|
|
11
|
+
export { default as ColumnPicker } from './ColumnPicker.svelte';
|
|
11
12
|
// Re-export shared demo data utilities for convenience
|
|
12
13
|
// Note: Cell type is NOT re-exported here to avoid conflicts with map-v2
|
|
13
14
|
// Import Cell from '$lib/shared/demo' or '@smartnet360/svelte-components' directly
|
|
14
15
|
export { generateCells, generateCellsFromPreset, getGeneratorInfo, GENERATOR_PRESETS } from '../../shared/demo';
|
|
15
16
|
// Configuration utilities
|
|
16
|
-
export { DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS, COLUMN_GROUPS, getAllColumns, getColumnsForPreset, getGroupHeaderFormatter, createTechFormatter, createStatusFormatter, createFbandFormatter, numberFormatter, coordinateFormatter, azimuthFormatter, heightFormatter } from './column-config';
|
|
17
|
+
export { DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS, COLUMN_GROUPS, getAllColumns, getColumnsForPreset, getGroupHeaderFormatter, getColumnMetadata, getPresetVisibleFields, createTechFormatter, createStatusFormatter, createFbandFormatter, numberFormatter, coordinateFormatter, azimuthFormatter, heightFormatter } from './column-config';
|
|
@@ -116,8 +116,8 @@ export class SiteStore {
|
|
|
116
116
|
availableTilts: ['0']
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
// Determine TX power (priority: atollPW >
|
|
120
|
-
const txPower = cell.atollPW || cell.
|
|
119
|
+
// Determine TX power (priority: atollPW > nwtPW > default)
|
|
120
|
+
const txPower = cell.atollPW || cell.nwtPW || 43; // Default to 43 dBm (20W)
|
|
121
121
|
// Determine frequency from band
|
|
122
122
|
const frequency = antennaPattern.frequency || this.parseFrequency(cell.fband);
|
|
123
123
|
// Get mechanical tilt (might need to parse from string)
|
|
@@ -176,7 +176,7 @@ export function generateCells(config) {
|
|
|
176
176
|
id: cellName,
|
|
177
177
|
txId: cellName,
|
|
178
178
|
cellID: cellName,
|
|
179
|
-
|
|
179
|
+
cellId2G: techBand.tech === '2G' ? cellName : '',
|
|
180
180
|
cellName: cellName,
|
|
181
181
|
siteId: siteId,
|
|
182
182
|
tech: techBand.tech,
|
|
@@ -201,22 +201,26 @@ export function generateCells(config) {
|
|
|
201
201
|
siteLongitude: siteLng,
|
|
202
202
|
comment: `Demo ${techBand.tech} ${techBand.band} cell at azimuth ${sector.azimuth}°`,
|
|
203
203
|
planner: 'Demo User',
|
|
204
|
-
|
|
204
|
+
atollET: 43.0,
|
|
205
205
|
atollPW: 20.0,
|
|
206
206
|
atollRS: 500.0 + (techBand.band === '700' ? 200 : 0),
|
|
207
207
|
atollBW: parseFloat(techBand.band) / 100,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
rru: `RRU-${siteId}-${sector.sectorNum}`,
|
|
209
|
+
nwtET: 40.0,
|
|
210
|
+
nwtPW: 20,
|
|
211
|
+
pci: cellCounter % 504,
|
|
212
212
|
nwtRS: 450.0,
|
|
213
213
|
nwtBW: 10.0,
|
|
214
214
|
other: {
|
|
215
|
+
city: ['Tehran', 'Shiraz', 'Isfahan', 'Mashhad', 'Tabriz'][siteNum % 5],
|
|
216
|
+
bcc: bandIndex % 8,
|
|
217
|
+
ncc: Math.floor(bandIndex / 8) % 8,
|
|
218
|
+
mall: random() < 0.1 ? 'Shopping Center' : undefined,
|
|
219
|
+
hsn: techBand.tech === '2G' ? Math.floor(random() * 64) : undefined,
|
|
215
220
|
demoCell: true,
|
|
216
221
|
siteNumber: actualSiteIndex,
|
|
217
222
|
sector: sector.sectorNum,
|
|
218
223
|
techBandKey: `${techBand.tech}_${techBand.band}`,
|
|
219
|
-
radius: normalizedRadius,
|
|
220
224
|
densityZone: zone.name
|
|
221
225
|
},
|
|
222
226
|
customSubgroup: `Sector-${sector.sectorNum}`
|
|
@@ -10,7 +10,7 @@ export interface Cell {
|
|
|
10
10
|
id: string;
|
|
11
11
|
txId: string;
|
|
12
12
|
cellID: string;
|
|
13
|
-
|
|
13
|
+
cellId2G: string;
|
|
14
14
|
cellName: string;
|
|
15
15
|
siteId: string;
|
|
16
16
|
tech: string;
|
|
@@ -35,16 +35,16 @@ export interface Cell {
|
|
|
35
35
|
siteLongitude: number;
|
|
36
36
|
comment: string;
|
|
37
37
|
planner: string;
|
|
38
|
-
|
|
39
|
-
atollPW
|
|
40
|
-
atollRS
|
|
41
|
-
atollBW
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
nwtRS
|
|
47
|
-
nwtBW
|
|
38
|
+
atollET?: number;
|
|
39
|
+
atollPW?: number;
|
|
40
|
+
atollRS?: number;
|
|
41
|
+
atollBW?: number;
|
|
42
|
+
rru: string;
|
|
43
|
+
nwtET?: number;
|
|
44
|
+
nwtPW?: number;
|
|
45
|
+
pci?: number;
|
|
46
|
+
nwtRS?: number;
|
|
47
|
+
nwtBW?: number;
|
|
48
48
|
other?: Record<string, unknown>;
|
|
49
49
|
customSubgroup: string;
|
|
50
50
|
}
|