@smartnet360/svelte-components 0.0.115 → 0.0.117
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/apps/antenna-pattern/components/AntennaDiagrams.svelte +63 -11
- package/dist/apps/antenna-pattern/components/AntennaDiagrams.svelte.d.ts +21 -1
- package/dist/core/CellTable/CellHistoryDemo.svelte +182 -0
- package/dist/core/CellTable/CellHistoryDemo.svelte.d.ts +3 -0
- package/dist/core/CellTable/CellTable.svelte +33 -2
- package/dist/core/CellTable/CellTable.svelte.d.ts +3 -1
- package/dist/core/CellTable/CellTableDemo.svelte +80 -1
- package/dist/core/CellTable/CellTablePanel.svelte +60 -1
- package/dist/core/CellTable/CellTablePanel.svelte.d.ts +5 -0
- package/dist/core/CellTable/CellTableToolbar.svelte +2 -1
- package/dist/core/CellTable/column-config.d.ts +17 -0
- package/dist/core/CellTable/column-config.js +141 -0
- package/dist/core/CellTable/index.d.ts +2 -1
- package/dist/core/CellTable/index.js +1 -0
- package/dist/core/CellTable/types.d.ts +10 -1
- package/dist/shared/demo/cell-generator.js +20 -0
- package/dist/shared/demo/cell-types.d.ts +19 -0
- package/dist/shared/demo/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -11,18 +11,52 @@
|
|
|
11
11
|
// Chart engine types
|
|
12
12
|
type ChartEngineType = 'polar-line' | 'polar-bar' | 'polar-area';
|
|
13
13
|
|
|
14
|
+
// Optional props for external initialization (non-breaking - all have defaults)
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Initial antenna 1 name to pre-select */
|
|
17
|
+
initialAntenna1Name?: string;
|
|
18
|
+
/** Initial antenna 2 name to pre-select */
|
|
19
|
+
initialAntenna2Name?: string;
|
|
20
|
+
/** Initial electrical tilt for antenna 1 */
|
|
21
|
+
initialEtilt1?: number;
|
|
22
|
+
/** Initial electrical tilt for antenna 2 */
|
|
23
|
+
initialEtilt2?: number;
|
|
24
|
+
/** Initial mechanical tilt for antenna 1 */
|
|
25
|
+
initialMtilt1?: number;
|
|
26
|
+
/** Initial mechanical tilt for antenna 2 */
|
|
27
|
+
initialMtilt2?: number;
|
|
28
|
+
/** Initial view mode */
|
|
29
|
+
initialViewMode?: 'single' | 'compare';
|
|
30
|
+
/** Label for antenna 1 (e.g., cell name) */
|
|
31
|
+
antenna1Label?: string;
|
|
32
|
+
/** Label for antenna 2 (e.g., cell name) */
|
|
33
|
+
antenna2Label?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
initialAntenna1Name = undefined,
|
|
38
|
+
initialAntenna2Name = undefined,
|
|
39
|
+
initialEtilt1 = 0,
|
|
40
|
+
initialEtilt2 = 0,
|
|
41
|
+
initialMtilt1 = 0,
|
|
42
|
+
initialMtilt2 = 0,
|
|
43
|
+
initialViewMode = 'single',
|
|
44
|
+
antenna1Label = undefined,
|
|
45
|
+
antenna2Label = undefined,
|
|
46
|
+
}: Props = $props();
|
|
47
|
+
|
|
14
48
|
let antennas = $state<Antenna[]>([]);
|
|
15
49
|
let selectedAntenna = $state<Antenna | null>(null);
|
|
16
50
|
let selectedAntenna2 = $state<Antenna | null>(null);
|
|
17
51
|
|
|
18
52
|
// External Bootstrap slider values
|
|
19
|
-
let ant1ElectricalTilt = $state(
|
|
20
|
-
let ant2ElectricalTilt = $state(
|
|
21
|
-
let ant1MechanicalTilt = $state(
|
|
22
|
-
let ant2MechanicalTilt = $state(
|
|
53
|
+
let ant1ElectricalTilt = $state(initialEtilt1);
|
|
54
|
+
let ant2ElectricalTilt = $state(initialEtilt2);
|
|
55
|
+
let ant1MechanicalTilt = $state(initialMtilt1);
|
|
56
|
+
let ant2MechanicalTilt = $state(initialMtilt2);
|
|
23
57
|
|
|
24
58
|
// Viewing mode and pattern visibility
|
|
25
|
-
let viewMode = $state<'single' | 'compare'>(
|
|
59
|
+
let viewMode = $state<'single' | 'compare'>(initialViewMode);
|
|
26
60
|
let patternType = $state<'horizontal' | 'vertical'>('vertical');
|
|
27
61
|
|
|
28
62
|
// Chart engine selection
|
|
@@ -117,14 +151,28 @@
|
|
|
117
151
|
antennas = await loadAntennas();
|
|
118
152
|
|
|
119
153
|
if (antennas.length > 0) {
|
|
120
|
-
//
|
|
121
|
-
|
|
154
|
+
// Check if initial antenna names were provided (for external initialization)
|
|
155
|
+
if (initialAntenna1Name) {
|
|
156
|
+
// Find antenna by name (case-insensitive partial match)
|
|
157
|
+
const found = antennas.find(a =>
|
|
158
|
+
a.name.toLowerCase().includes(initialAntenna1Name.toLowerCase())
|
|
159
|
+
);
|
|
160
|
+
selectedAntenna = found || antennas[0];
|
|
161
|
+
} else {
|
|
162
|
+
selectedAntenna = antennas[0];
|
|
163
|
+
}
|
|
164
|
+
|
|
122
165
|
if (selectedAntenna.tilt) {
|
|
123
166
|
availableElectricalTilts = selectedAntenna.tilt.split(',').map(t => t.trim());
|
|
124
167
|
}
|
|
125
168
|
|
|
126
|
-
// Set
|
|
127
|
-
if (
|
|
169
|
+
// Set antenna 2 selection (for compare mode)
|
|
170
|
+
if (initialAntenna2Name) {
|
|
171
|
+
const found = antennas.find(a =>
|
|
172
|
+
a.name.toLowerCase().includes(initialAntenna2Name.toLowerCase())
|
|
173
|
+
);
|
|
174
|
+
selectedAntenna2 = found || (antennas.length > 1 ? antennas[1] : antennas[0]);
|
|
175
|
+
} else if (antennas.length > 1) {
|
|
128
176
|
selectedAntenna2 = antennas[1];
|
|
129
177
|
} else {
|
|
130
178
|
selectedAntenna2 = antennas[0]; // Use same antenna if only one available
|
|
@@ -138,9 +186,13 @@
|
|
|
138
186
|
// Generate chart title based on current selection
|
|
139
187
|
function generateChartTitle(): string {
|
|
140
188
|
if (viewMode === 'compare' && selectedAntenna && selectedAntenna2) {
|
|
141
|
-
|
|
189
|
+
// Use custom labels if provided, otherwise use antenna names
|
|
190
|
+
const label1 = antenna1Label || selectedAntenna.name;
|
|
191
|
+
const label2 = antenna2Label || selectedAntenna2.name;
|
|
192
|
+
return `${label1} vs ${label2}`;
|
|
142
193
|
} else if (selectedAntenna) {
|
|
143
|
-
|
|
194
|
+
const label = antenna1Label || selectedAntenna.name;
|
|
195
|
+
return `${label} - Pattern Analysis`;
|
|
144
196
|
}
|
|
145
197
|
return 'Antenna Pattern Analysis';
|
|
146
198
|
}
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Initial antenna 1 name to pre-select */
|
|
3
|
+
initialAntenna1Name?: string;
|
|
4
|
+
/** Initial antenna 2 name to pre-select */
|
|
5
|
+
initialAntenna2Name?: string;
|
|
6
|
+
/** Initial electrical tilt for antenna 1 */
|
|
7
|
+
initialEtilt1?: number;
|
|
8
|
+
/** Initial electrical tilt for antenna 2 */
|
|
9
|
+
initialEtilt2?: number;
|
|
10
|
+
/** Initial mechanical tilt for antenna 1 */
|
|
11
|
+
initialMtilt1?: number;
|
|
12
|
+
/** Initial mechanical tilt for antenna 2 */
|
|
13
|
+
initialMtilt2?: number;
|
|
14
|
+
/** Initial view mode */
|
|
15
|
+
initialViewMode?: 'single' | 'compare';
|
|
16
|
+
/** Label for antenna 1 (e.g., cell name) */
|
|
17
|
+
antenna1Label?: string;
|
|
18
|
+
/** Label for antenna 2 (e.g., cell name) */
|
|
19
|
+
antenna2Label?: string;
|
|
20
|
+
}
|
|
21
|
+
declare const AntennaDiagrams: import("svelte").Component<Props, {}, "">;
|
|
2
22
|
type AntennaDiagrams = ReturnType<typeof AntennaDiagrams>;
|
|
3
23
|
export default AntennaDiagrams;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import CellTablePanel from './CellTablePanel.svelte';
|
|
3
|
+
import type { RowSelectionEvent, CellData } from './types';
|
|
4
|
+
|
|
5
|
+
// Generate mock history data for a single cell
|
|
6
|
+
function generateMockHistory(cellName: string, count: number = 20): Partial<CellData>[] {
|
|
7
|
+
const antennas = [
|
|
8
|
+
'ADU451R76V06',
|
|
9
|
+
'ADU451R79V06',
|
|
10
|
+
'AAHF4518R3V06',
|
|
11
|
+
'AAHF4516R1V06',
|
|
12
|
+
'742215'
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const history: Partial<CellData>[] = [];
|
|
16
|
+
const baseDate = new Date('2025-10-20');
|
|
17
|
+
|
|
18
|
+
let currentAntenna = antennas[0];
|
|
19
|
+
let currentET = 4;
|
|
20
|
+
let currentMT = 2;
|
|
21
|
+
let currentPW = 20;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < count; i++) {
|
|
24
|
+
// Random chance to change values
|
|
25
|
+
if (Math.random() > 0.7) {
|
|
26
|
+
currentAntenna = antennas[Math.floor(Math.random() * antennas.length)];
|
|
27
|
+
}
|
|
28
|
+
if (Math.random() > 0.6) {
|
|
29
|
+
currentET = Math.floor(Math.random() * 10);
|
|
30
|
+
}
|
|
31
|
+
if (Math.random() > 0.8) {
|
|
32
|
+
currentMT = Math.floor(Math.random() * 6);
|
|
33
|
+
}
|
|
34
|
+
if (Math.random() > 0.7) {
|
|
35
|
+
currentPW = 15 + Math.floor(Math.random() * 15);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const date = new Date(baseDate);
|
|
39
|
+
date.setDate(date.getDate() - i * 7); // Weekly snapshots going back
|
|
40
|
+
|
|
41
|
+
history.push({
|
|
42
|
+
id: `hist-${i}`,
|
|
43
|
+
cellName: cellName,
|
|
44
|
+
configDate: date.toLocaleDateString('en-GB', {
|
|
45
|
+
day: '2-digit',
|
|
46
|
+
month: '2-digit',
|
|
47
|
+
year: 'numeric',
|
|
48
|
+
hour: '2-digit',
|
|
49
|
+
minute: '2-digit'
|
|
50
|
+
}),
|
|
51
|
+
antenna: currentAntenna,
|
|
52
|
+
atollET: currentET,
|
|
53
|
+
atollMT: currentMT,
|
|
54
|
+
atollPW: currentPW,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return history;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let cellName = $state('2918141');
|
|
62
|
+
let historyData = $state(generateMockHistory('2918141'));
|
|
63
|
+
|
|
64
|
+
function loadHistory() {
|
|
65
|
+
historyData = generateMockHistory(cellName);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleSelectionChange(event: RowSelectionEvent) {
|
|
69
|
+
console.log('Selection changed:', event.ids);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compare two selected history records
|
|
74
|
+
*/
|
|
75
|
+
function handleCompareTwoRecords(record1: CellData, record2: CellData) {
|
|
76
|
+
const params = new URLSearchParams();
|
|
77
|
+
|
|
78
|
+
// Use antenna from both records
|
|
79
|
+
if (record1.antenna) params.set('ant1', record1.antenna);
|
|
80
|
+
if (record2.antenna) params.set('ant2', record2.antenna);
|
|
81
|
+
|
|
82
|
+
// Use electrical tilt values
|
|
83
|
+
params.set('etilt1', String(record1.atollET || 0));
|
|
84
|
+
params.set('etilt2', String(record2.atollET || 0));
|
|
85
|
+
|
|
86
|
+
// Use mechanical tilt values
|
|
87
|
+
params.set('mtilt1', String(record1.atollMT || 0));
|
|
88
|
+
params.set('mtilt2', String(record2.atollMT || 0));
|
|
89
|
+
|
|
90
|
+
// Add config date as labels
|
|
91
|
+
params.set('label1', `${record1.cellName} (${record1.configDate})`);
|
|
92
|
+
params.set('label2', `${record2.cellName} (${record2.configDate})`);
|
|
93
|
+
|
|
94
|
+
const url = `/apps/antenna-compare?${params.toString()}`;
|
|
95
|
+
window.open(url, '_blank');
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<div class="cell-history-page vh-100 d-flex flex-column">
|
|
100
|
+
<!-- Header -->
|
|
101
|
+
<div class="demo-controls bg-light border-bottom px-3 py-2 d-flex align-items-center gap-3 flex-wrap">
|
|
102
|
+
<div class="input-group input-group-sm" style="width: auto;">
|
|
103
|
+
<span class="input-group-text">Cell Name</span>
|
|
104
|
+
<input
|
|
105
|
+
type="text"
|
|
106
|
+
class="form-control"
|
|
107
|
+
bind:value={cellName}
|
|
108
|
+
style="width: 120px;"
|
|
109
|
+
/>
|
|
110
|
+
<button class="btn btn-outline-primary" onclick={loadHistory}>
|
|
111
|
+
<i class="bi bi-arrow-clockwise"></i> Load History
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
<span class="badge bg-info">
|
|
115
|
+
{historyData.length} records
|
|
116
|
+
</span>
|
|
117
|
+
<span class="text-muted small">
|
|
118
|
+
Select 2 rows to compare antenna configurations
|
|
119
|
+
</span>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<!-- CellTablePanel with history preset -->
|
|
123
|
+
<div class="flex-grow-1 overflow-hidden">
|
|
124
|
+
<CellTablePanel
|
|
125
|
+
cells={historyData as CellData[]}
|
|
126
|
+
groupBy="none"
|
|
127
|
+
columnPreset="history"
|
|
128
|
+
selectable={true}
|
|
129
|
+
multiSelect={true}
|
|
130
|
+
showToolbar={true}
|
|
131
|
+
showExport={true}
|
|
132
|
+
headerFilters={true}
|
|
133
|
+
showDetailsSidebar={false}
|
|
134
|
+
showScrollSpy={false}
|
|
135
|
+
title="Cell Configuration History"
|
|
136
|
+
onselectionchange={handleSelectionChange}
|
|
137
|
+
>
|
|
138
|
+
{#snippet footer({ selectedRows, selectedCount })}
|
|
139
|
+
<div class="d-flex align-items-center justify-content-between">
|
|
140
|
+
<span class="text-muted small">
|
|
141
|
+
{#if selectedCount > 0}
|
|
142
|
+
{selectedCount} record(s) selected
|
|
143
|
+
{:else}
|
|
144
|
+
Select rows to compare configurations
|
|
145
|
+
{/if}
|
|
146
|
+
</span>
|
|
147
|
+
<div class="btn-group">
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
class="btn btn-sm btn-outline-success"
|
|
151
|
+
disabled={selectedCount !== 2}
|
|
152
|
+
title={selectedCount === 2 ? 'Compare antenna patterns for selected dates' : 'Select exactly 2 records to compare'}
|
|
153
|
+
onclick={() => handleCompareTwoRecords(selectedRows[0], selectedRows[1])}
|
|
154
|
+
>
|
|
155
|
+
<i class="bi bi-broadcast-pin"></i>
|
|
156
|
+
<span class="d-none d-sm-inline ms-1">Compare Antennas</span>
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class="btn btn-sm btn-outline-secondary"
|
|
161
|
+
disabled={selectedCount === 0}
|
|
162
|
+
onclick={() => console.log('Export:', selectedRows.map(r => r.configDate))}
|
|
163
|
+
>
|
|
164
|
+
<i class="bi bi-download"></i>
|
|
165
|
+
<span class="d-none d-sm-inline ms-1">Export</span>
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
{/snippet}
|
|
170
|
+
</CellTablePanel>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<style>
|
|
175
|
+
.cell-history-page {
|
|
176
|
+
background: var(--bs-body-bg, #f8f9fa);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.demo-controls {
|
|
180
|
+
flex-shrink: 0;
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
RowSelectionEvent,
|
|
11
11
|
RowClickEvent,
|
|
12
12
|
RowDblClickEvent,
|
|
13
|
+
RowContextMenuEvent,
|
|
13
14
|
DataChangeEvent
|
|
14
15
|
} from './types';
|
|
15
16
|
import {
|
|
@@ -34,6 +35,8 @@
|
|
|
34
35
|
onrowclick?: (event: RowClickEvent) => void;
|
|
35
36
|
/** Row double-click event */
|
|
36
37
|
onrowdblclick?: (event: RowDblClickEvent) => void;
|
|
38
|
+
/** Row context menu (right-click) event */
|
|
39
|
+
onrowcontextmenu?: (event: RowContextMenuEvent) => void;
|
|
37
40
|
/** Data change event (filter, sort, etc.) */
|
|
38
41
|
ondatachange?: (event: DataChangeEvent) => void;
|
|
39
42
|
}
|
|
@@ -58,6 +61,7 @@
|
|
|
58
61
|
onselectionchange,
|
|
59
62
|
onrowclick,
|
|
60
63
|
onrowdblclick,
|
|
64
|
+
onrowcontextmenu,
|
|
61
65
|
ondatachange
|
|
62
66
|
}: Props = $props();
|
|
63
67
|
|
|
@@ -67,8 +71,25 @@
|
|
|
67
71
|
|
|
68
72
|
// Reactive column configuration - only changes when preset changes
|
|
69
73
|
let columns = $derived.by(() => {
|
|
70
|
-
//
|
|
71
|
-
|
|
74
|
+
// Get base columns from preset
|
|
75
|
+
const baseColumns = getColumnsForPreset(columnPreset, techColors, statusColors, headerFilters);
|
|
76
|
+
|
|
77
|
+
// Add row selection checkbox column if selectable
|
|
78
|
+
if (selectable) {
|
|
79
|
+
const selectColumn = {
|
|
80
|
+
title: '',
|
|
81
|
+
formatter: 'rowSelection',
|
|
82
|
+
titleFormatter: multiSelect ? 'rowSelection' : undefined,
|
|
83
|
+
hozAlign: 'center' as const,
|
|
84
|
+
headerSort: false,
|
|
85
|
+
width: 40,
|
|
86
|
+
frozen: true,
|
|
87
|
+
cssClass: 'cell-table-select-column',
|
|
88
|
+
};
|
|
89
|
+
return [selectColumn, ...baseColumns];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return baseColumns;
|
|
72
93
|
});
|
|
73
94
|
|
|
74
95
|
// Pre-sort data using our custom multi-level sorter
|
|
@@ -152,6 +173,16 @@
|
|
|
152
173
|
}
|
|
153
174
|
});
|
|
154
175
|
|
|
176
|
+
table.on('rowContext', (e, row) => {
|
|
177
|
+
if (onrowcontextmenu) {
|
|
178
|
+
(e as MouseEvent).preventDefault();
|
|
179
|
+
onrowcontextmenu({
|
|
180
|
+
row: (row as RowComponent).getData() as CellData,
|
|
181
|
+
event: e as MouseEvent
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
155
186
|
table.on('rowSelectionChanged', (data, rows) => {
|
|
156
187
|
if (onselectionchange) {
|
|
157
188
|
const cellData = data as CellData[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
|
2
2
|
import 'tabulator-tables/dist/css/tabulator_bootstrap5.min.css';
|
|
3
|
-
import type { CellTableProps, CellData, RowSelectionEvent, RowClickEvent, RowDblClickEvent, DataChangeEvent } from './types';
|
|
3
|
+
import type { CellTableProps, CellData, RowSelectionEvent, RowClickEvent, RowDblClickEvent, RowContextMenuEvent, DataChangeEvent } from './types';
|
|
4
4
|
interface Props extends CellTableProps {
|
|
5
5
|
/** Row selection change event */
|
|
6
6
|
onselectionchange?: (event: RowSelectionEvent) => void;
|
|
@@ -8,6 +8,8 @@ interface Props extends CellTableProps {
|
|
|
8
8
|
onrowclick?: (event: RowClickEvent) => void;
|
|
9
9
|
/** Row double-click event */
|
|
10
10
|
onrowdblclick?: (event: RowDblClickEvent) => void;
|
|
11
|
+
/** Row context menu (right-click) event */
|
|
12
|
+
onrowcontextmenu?: (event: RowContextMenuEvent) => void;
|
|
11
13
|
/** Data change event (filter, sort, etc.) */
|
|
12
14
|
ondatachange?: (event: DataChangeEvent) => void;
|
|
13
15
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import CellTablePanel from './CellTablePanel.svelte';
|
|
3
|
-
import type { CellTableGroupField, ColumnPreset, RowSelectionEvent } from './types';
|
|
3
|
+
import type { CellTableGroupField, ColumnPreset, RowSelectionEvent, CellData } from './types';
|
|
4
4
|
import { generateCellsFromPreset, getGeneratorInfo, type GeneratorPreset } from '../../shared/demo';
|
|
5
5
|
|
|
6
6
|
let datasetSize: GeneratorPreset = $state('small');
|
|
@@ -42,6 +42,53 @@
|
|
|
42
42
|
console.log('Selection changed:', event.ids);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Context menu action handlers
|
|
46
|
+
function handleViewCell(cell: CellData) {
|
|
47
|
+
console.log('View cell:', cell.id, cell.cellName);
|
|
48
|
+
alert(`Viewing cell: ${cell.cellName}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleEditCell(cell: CellData) {
|
|
52
|
+
console.log('Edit cell:', cell.id, cell.cellName);
|
|
53
|
+
alert(`Edit cell: ${cell.cellName}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleDeleteCell(cell: CellData) {
|
|
57
|
+
console.log('Delete cell:', cell.id, cell.cellName);
|
|
58
|
+
if (confirm(`Delete cell ${cell.cellName}?`)) {
|
|
59
|
+
demoCells = demoCells.filter(c => c.id !== cell.id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleShowOnMap(cell: CellData) {
|
|
64
|
+
console.log('Show on map:', cell.latitude, cell.longitude);
|
|
65
|
+
alert(`Show on map: ${cell.cellName}\nLat: ${cell.latitude}\nLon: ${cell.longitude}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compare two selected cells' antenna configurations
|
|
70
|
+
*/
|
|
71
|
+
function handleCompareTwoCells(cell1: CellData, cell2: CellData) {
|
|
72
|
+
const params = new URLSearchParams();
|
|
73
|
+
|
|
74
|
+
// Use antenna from both cells
|
|
75
|
+
if (cell1.antenna) params.set('ant1', cell1.antenna);
|
|
76
|
+
if (cell2.antenna) params.set('ant2', cell2.antenna);
|
|
77
|
+
|
|
78
|
+
// Use electrical tilt values (parse first value if comma-separated)
|
|
79
|
+
const etilt1 = cell1.electricalTilt ? parseInt(cell1.electricalTilt.split(',')[0], 10) : 0;
|
|
80
|
+
const etilt2 = cell2.electricalTilt ? parseInt(cell2.electricalTilt.split(',')[0], 10) : 0;
|
|
81
|
+
params.set('etilt1', String(etilt1 || 0));
|
|
82
|
+
params.set('etilt2', String(etilt2 || 0));
|
|
83
|
+
|
|
84
|
+
// Add cell name + status as labels
|
|
85
|
+
params.set('label1', `${cell1.cellName} (${cell1.status})`);
|
|
86
|
+
params.set('label2', `${cell2.cellName} (${cell2.status})`);
|
|
87
|
+
|
|
88
|
+
const url = `/apps/antenna-compare?${params.toString()}`;
|
|
89
|
+
window.open(url, '_blank');
|
|
90
|
+
}
|
|
91
|
+
|
|
45
92
|
function regenerateData() {
|
|
46
93
|
demoCells = generateCellsFromPreset(datasetSize);
|
|
47
94
|
}
|
|
@@ -221,6 +268,16 @@
|
|
|
221
268
|
{/if}
|
|
222
269
|
</span>
|
|
223
270
|
<div class="btn-group">
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
class="btn btn-sm btn-outline-success"
|
|
274
|
+
disabled={selectedCount !== 2}
|
|
275
|
+
title={selectedCount === 2 ? 'Compare Atoll antennas of selected cells' : 'Select exactly 2 cells to compare'}
|
|
276
|
+
onclick={() => handleCompareTwoCells(selectedRows[0], selectedRows[1])}
|
|
277
|
+
>
|
|
278
|
+
<i class="bi bi-broadcast-pin"></i>
|
|
279
|
+
<span class="d-none d-sm-inline ms-1">Compare Antennas</span>
|
|
280
|
+
</button>
|
|
224
281
|
<button
|
|
225
282
|
type="button"
|
|
226
283
|
class="btn btn-sm btn-outline-primary"
|
|
@@ -242,6 +299,28 @@
|
|
|
242
299
|
</div>
|
|
243
300
|
</div>
|
|
244
301
|
{/snippet}
|
|
302
|
+
|
|
303
|
+
{#snippet contextMenu({ row, closeMenu })}
|
|
304
|
+
<div class="dropdown-menu show shadow-lg" style="min-width: 180px;">
|
|
305
|
+
<h6 class="dropdown-header text-truncate" style="max-width: 200px;">
|
|
306
|
+
<i class="bi bi-broadcast me-1"></i>{row.cellName}
|
|
307
|
+
</h6>
|
|
308
|
+
<div class="dropdown-divider"></div>
|
|
309
|
+
<button class="dropdown-item" onclick={() => { handleViewCell(row); closeMenu(); }}>
|
|
310
|
+
<i class="bi bi-eye me-2 text-primary"></i>View Details
|
|
311
|
+
</button>
|
|
312
|
+
<button class="dropdown-item" onclick={() => { handleEditCell(row); closeMenu(); }}>
|
|
313
|
+
<i class="bi bi-pencil me-2 text-warning"></i>Edit Cell
|
|
314
|
+
</button>
|
|
315
|
+
<button class="dropdown-item" onclick={() => { handleShowOnMap(row); closeMenu(); }}>
|
|
316
|
+
<i class="bi bi-geo-alt me-2 text-info"></i>Show on Map
|
|
317
|
+
</button>
|
|
318
|
+
<div class="dropdown-divider"></div>
|
|
319
|
+
<button class="dropdown-item text-danger" onclick={() => { handleDeleteCell(row); closeMenu(); }}>
|
|
320
|
+
<i class="bi bi-trash me-2"></i>Delete Cell
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
{/snippet}
|
|
245
324
|
</CellTablePanel>
|
|
246
325
|
</div>
|
|
247
326
|
</div>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
RowSelectionEvent,
|
|
11
11
|
RowClickEvent,
|
|
12
12
|
RowDblClickEvent,
|
|
13
|
+
RowContextMenuEvent,
|
|
13
14
|
DataChangeEvent,
|
|
14
15
|
TechColorMap,
|
|
15
16
|
StatusColorMap
|
|
@@ -68,6 +69,8 @@
|
|
|
68
69
|
footer?: Snippet<[{ selectedRows: CellData[]; selectedCount: number }]>;
|
|
69
70
|
/** Custom details sidebar content */
|
|
70
71
|
detailsContent?: Snippet<[{ cell: CellData | null; closeSidebar: () => void }]>;
|
|
72
|
+
/** Custom context menu content (right-click on row) */
|
|
73
|
+
contextMenu?: Snippet<[{ row: CellData; closeMenu: () => void }]>;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
let {
|
|
@@ -96,9 +99,15 @@
|
|
|
96
99
|
headerSearch,
|
|
97
100
|
headerActions,
|
|
98
101
|
footer,
|
|
99
|
-
detailsContent
|
|
102
|
+
detailsContent,
|
|
103
|
+
contextMenu
|
|
100
104
|
}: Props = $props();
|
|
101
105
|
|
|
106
|
+
// Context menu state
|
|
107
|
+
let contextMenuVisible = $state(false);
|
|
108
|
+
let contextMenuRow: CellData | null = $state(null);
|
|
109
|
+
let contextMenuPosition = $state({ x: 0, y: 0 });
|
|
110
|
+
|
|
102
111
|
// Storage keys
|
|
103
112
|
const STORAGE_KEY_GROUP = `${storageKey}-groupBy`;
|
|
104
113
|
const STORAGE_KEY_COLUMNS = `${storageKey}-visibleColumns`;
|
|
@@ -239,6 +248,26 @@
|
|
|
239
248
|
updateScrollSpyGroups();
|
|
240
249
|
}
|
|
241
250
|
|
|
251
|
+
function handleRowContextMenu(event: RowContextMenuEvent) {
|
|
252
|
+
if (contextMenu) {
|
|
253
|
+
contextMenuRow = event.row;
|
|
254
|
+
contextMenuPosition = { x: event.event.clientX, y: event.event.clientY };
|
|
255
|
+
contextMenuVisible = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function closeContextMenu() {
|
|
260
|
+
contextMenuVisible = false;
|
|
261
|
+
contextMenuRow = null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function handleClickOutside(event: MouseEvent) {
|
|
265
|
+
const target = event.target as HTMLElement;
|
|
266
|
+
if (!target.closest('.context-menu-container')) {
|
|
267
|
+
closeContextMenu();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
242
271
|
function updateScrollSpyGroups() {
|
|
243
272
|
if (scrollSpyEnabled && groupBy !== 'none') {
|
|
244
273
|
// Small delay to ensure table has updated
|
|
@@ -477,6 +506,7 @@
|
|
|
477
506
|
onselectionchange={handleSelectionChange}
|
|
478
507
|
ondatachange={handleDataChange}
|
|
479
508
|
onrowclick={handleRowClick}
|
|
509
|
+
onrowcontextmenu={handleRowContextMenu}
|
|
480
510
|
{onrowdblclick}
|
|
481
511
|
/>
|
|
482
512
|
</div>
|
|
@@ -617,8 +647,21 @@
|
|
|
617
647
|
{@render footer({ selectedRows, selectedCount })}
|
|
618
648
|
</div>
|
|
619
649
|
{/if}
|
|
650
|
+
|
|
651
|
+
<!-- Context Menu (portal-like, fixed position) -->
|
|
652
|
+
{#if contextMenuVisible && contextMenuRow && contextMenu}
|
|
653
|
+
<div
|
|
654
|
+
class="context-menu-container"
|
|
655
|
+
style="position: fixed; left: {contextMenuPosition.x}px; top: {contextMenuPosition.y}px; z-index: 1050;"
|
|
656
|
+
>
|
|
657
|
+
{@render contextMenu({ row: contextMenuRow, closeMenu: closeContextMenu })}
|
|
658
|
+
</div>
|
|
659
|
+
{/if}
|
|
620
660
|
</div>
|
|
621
661
|
|
|
662
|
+
<!-- Click outside handler -->
|
|
663
|
+
<svelte:window onclick={contextMenuVisible ? handleClickOutside : undefined} />
|
|
664
|
+
|
|
622
665
|
<style>
|
|
623
666
|
.cell-table-panel {
|
|
624
667
|
background-color: var(--bs-body-bg, #fff);
|
|
@@ -694,4 +737,20 @@
|
|
|
694
737
|
.scrollspy-badge:active {
|
|
695
738
|
transform: translateY(0);
|
|
696
739
|
}
|
|
740
|
+
|
|
741
|
+
/* Context menu styling */
|
|
742
|
+
.context-menu-container {
|
|
743
|
+
animation: contextMenuFadeIn 0.1s ease-out;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
@keyframes contextMenuFadeIn {
|
|
747
|
+
from {
|
|
748
|
+
opacity: 0;
|
|
749
|
+
transform: scale(0.95);
|
|
750
|
+
}
|
|
751
|
+
to {
|
|
752
|
+
opacity: 1;
|
|
753
|
+
transform: scale(1);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
697
756
|
</style>
|
|
@@ -61,6 +61,11 @@ interface Props {
|
|
|
61
61
|
cell: CellData | null;
|
|
62
62
|
closeSidebar: () => void;
|
|
63
63
|
}]>;
|
|
64
|
+
/** Custom context menu content (right-click on row) */
|
|
65
|
+
contextMenu?: Snippet<[{
|
|
66
|
+
row: CellData;
|
|
67
|
+
closeMenu: () => void;
|
|
68
|
+
}]>;
|
|
64
69
|
}
|
|
65
70
|
declare const CellTablePanel: import("svelte").Component<Props, {
|
|
66
71
|
getSelectedRows: () => CellData[];
|
|
@@ -100,7 +100,8 @@
|
|
|
100
100
|
{ value: 'physical', label: 'Physical' },
|
|
101
101
|
{ value: 'network', label: 'Network' },
|
|
102
102
|
{ value: 'planning', label: 'Planning' },
|
|
103
|
-
{ value: 'compare', label: 'Compare (Atoll/
|
|
103
|
+
{ value: 'compare', label: 'Compare (Atoll/NW)' },
|
|
104
|
+
{ value: 'kpi', label: 'KPI Trends' }
|
|
104
105
|
];
|
|
105
106
|
|
|
106
107
|
function handleGroupChange(e: Event) {
|
|
@@ -61,6 +61,23 @@ export declare function heightFormatter(cell: CellComponent): string;
|
|
|
61
61
|
* - Gray: one or both values missing
|
|
62
62
|
*/
|
|
63
63
|
export declare function createCompareFormatter(atollField: string, nwtField: string): (cell: CellComponent) => string;
|
|
64
|
+
/**
|
|
65
|
+
* Creates a sparkline SVG formatter for KPI time-series data
|
|
66
|
+
* Renders a mini line chart showing trend over time
|
|
67
|
+
*
|
|
68
|
+
* @param options Configuration for the sparkline
|
|
69
|
+
*/
|
|
70
|
+
export interface SparklineOptions {
|
|
71
|
+
width?: number;
|
|
72
|
+
height?: number;
|
|
73
|
+
lineColor?: string;
|
|
74
|
+
fillColor?: string;
|
|
75
|
+
showDots?: boolean;
|
|
76
|
+
showLastValue?: boolean;
|
|
77
|
+
unit?: string;
|
|
78
|
+
decimals?: number;
|
|
79
|
+
}
|
|
80
|
+
export declare function createSparklineFormatter(options?: SparklineOptions): (cell: CellComponent) => string;
|
|
64
81
|
/**
|
|
65
82
|
* Custom sorter for fband - extracts numeric portion and sorts numerically
|
|
66
83
|
* Examples: LTE700 → 700, GSM900 → 900, LTE1800 → 1800, 5G-3500 → 3500
|
|
@@ -60,6 +60,8 @@ export const COLUMN_GROUPS = {
|
|
|
60
60
|
atoll: ['atollET', 'atollPW', 'atollRS', 'atollBW'],
|
|
61
61
|
position: ['latitude', 'longitude', 'siteLatitude', 'siteLongitude', 'dx', 'dy'],
|
|
62
62
|
compare: ['compareET', 'comparePW', 'compareRS', 'compareBW'],
|
|
63
|
+
kpi: ['kpiTraffic', 'kpiThroughput', 'kpiAvailability', 'kpiSuccessRate'],
|
|
64
|
+
history: ['configDate', 'antenna', 'atollET', 'atollMT', 'atollPW'],
|
|
63
65
|
};
|
|
64
66
|
/**
|
|
65
67
|
* Create a technology badge formatter
|
|
@@ -166,6 +168,55 @@ export function createCompareFormatter(atollField, nwtField) {
|
|
|
166
168
|
return `<span class="badge" style="background-color: ${bgColor}; color: ${textColor}; font-size: 0.75rem; font-weight: normal;">${atollStr} | ${nwtStr}</span>`;
|
|
167
169
|
};
|
|
168
170
|
}
|
|
171
|
+
export function createSparklineFormatter(options = {}) {
|
|
172
|
+
const { width = 80, height = 24, lineColor = '#0d6efd', fillColor = 'rgba(13,110,253,0.2)', showDots = false, showLastValue = true, unit = '', decimals = 1 } = options;
|
|
173
|
+
return (cell) => {
|
|
174
|
+
const data = cell.getValue();
|
|
175
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
176
|
+
return '<span class="text-muted">—</span>';
|
|
177
|
+
}
|
|
178
|
+
const values = data.filter(v => typeof v === 'number' && !isNaN(v));
|
|
179
|
+
if (values.length === 0) {
|
|
180
|
+
return '<span class="text-muted">—</span>';
|
|
181
|
+
}
|
|
182
|
+
const min = Math.min(...values);
|
|
183
|
+
const max = Math.max(...values);
|
|
184
|
+
const range = max - min || 1;
|
|
185
|
+
const padding = 2;
|
|
186
|
+
const chartWidth = showLastValue ? width - 35 : width - 4;
|
|
187
|
+
const chartHeight = height - 4;
|
|
188
|
+
// Generate SVG path
|
|
189
|
+
const points = values.map((v, i) => {
|
|
190
|
+
const x = padding + (i / (values.length - 1 || 1)) * (chartWidth - padding * 2);
|
|
191
|
+
const y = padding + (1 - (v - min) / range) * (chartHeight - padding * 2);
|
|
192
|
+
return { x, y, v };
|
|
193
|
+
});
|
|
194
|
+
// Line path
|
|
195
|
+
const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' ');
|
|
196
|
+
// Fill path (closed polygon)
|
|
197
|
+
const fillPath = `${linePath} L ${points[points.length - 1].x.toFixed(1)} ${chartHeight - padding} L ${padding} ${chartHeight - padding} Z`;
|
|
198
|
+
// Dots
|
|
199
|
+
const dots = showDots
|
|
200
|
+
? points.map(p => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="1.5" fill="${lineColor}"/>`).join('')
|
|
201
|
+
: '';
|
|
202
|
+
// Last value text
|
|
203
|
+
const lastValue = values[values.length - 1];
|
|
204
|
+
const valueText = showLastValue
|
|
205
|
+
? `<text x="${width - 2}" y="${height / 2 + 4}" text-anchor="end" font-size="10" fill="#333">${lastValue.toFixed(decimals)}${unit}</text>`
|
|
206
|
+
: '';
|
|
207
|
+
// Trend indicator (comparing last vs first)
|
|
208
|
+
const trend = values.length > 1 ? values[values.length - 1] - values[0] : 0;
|
|
209
|
+
const trendColor = trend >= 0 ? '#198754' : '#dc3545';
|
|
210
|
+
return `
|
|
211
|
+
<svg width="${width}" height="${height}" style="vertical-align: middle;">
|
|
212
|
+
<path d="${fillPath}" fill="${fillColor}" />
|
|
213
|
+
<path d="${linePath}" fill="none" stroke="${lineColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
214
|
+
${dots}
|
|
215
|
+
${valueText}
|
|
216
|
+
</svg>
|
|
217
|
+
`.trim().replace(/\s+/g, ' ');
|
|
218
|
+
};
|
|
219
|
+
}
|
|
169
220
|
/**
|
|
170
221
|
* Custom sorter for fband - extracts numeric portion and sorts numerically
|
|
171
222
|
* Examples: LTE700 → 700, GSM900 → 900, LTE1800 → 1800, 5G-3500 → 3500
|
|
@@ -277,6 +328,12 @@ export function getAllColumns(techColors = DEFAULT_TECH_COLORS, statusColors = D
|
|
|
277
328
|
width: 120,
|
|
278
329
|
...headerFilterParams,
|
|
279
330
|
},
|
|
331
|
+
{
|
|
332
|
+
title: 'Config Date',
|
|
333
|
+
field: 'configDate',
|
|
334
|
+
width: 150,
|
|
335
|
+
...headerFilterParams,
|
|
336
|
+
},
|
|
280
337
|
// Physical columns
|
|
281
338
|
{
|
|
282
339
|
title: 'Antenna',
|
|
@@ -398,6 +455,14 @@ export function getAllColumns(techColors = DEFAULT_TECH_COLORS, statusColors = D
|
|
|
398
455
|
formatter: numberFormatter(1),
|
|
399
456
|
...headerFilterParams,
|
|
400
457
|
},
|
|
458
|
+
{
|
|
459
|
+
title: 'Atoll MT',
|
|
460
|
+
field: 'atollMT',
|
|
461
|
+
width: 90,
|
|
462
|
+
hozAlign: 'right',
|
|
463
|
+
formatter: numberFormatter(1),
|
|
464
|
+
...headerFilterParams,
|
|
465
|
+
},
|
|
401
466
|
{
|
|
402
467
|
title: 'Atoll PW',
|
|
403
468
|
field: 'atollPW',
|
|
@@ -455,6 +520,63 @@ export function getAllColumns(techColors = DEFAULT_TECH_COLORS, statusColors = D
|
|
|
455
520
|
formatter: createCompareFormatter('atollBW', 'nwBW'),
|
|
456
521
|
headerTooltip: 'Atoll BW | Network BW',
|
|
457
522
|
},
|
|
523
|
+
// KPI Sparkline columns
|
|
524
|
+
{
|
|
525
|
+
title: 'Traffic',
|
|
526
|
+
field: 'kpiTraffic',
|
|
527
|
+
width: 120,
|
|
528
|
+
hozAlign: 'center',
|
|
529
|
+
formatter: createSparklineFormatter({
|
|
530
|
+
unit: '',
|
|
531
|
+
lineColor: '#0d6efd',
|
|
532
|
+
fillColor: 'rgba(13,110,253,0.15)',
|
|
533
|
+
decimals: 0
|
|
534
|
+
}),
|
|
535
|
+
headerTooltip: 'Traffic volume trend (GB)',
|
|
536
|
+
headerSort: false,
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
title: 'Throughput',
|
|
540
|
+
field: 'kpiThroughput',
|
|
541
|
+
width: 120,
|
|
542
|
+
hozAlign: 'center',
|
|
543
|
+
formatter: createSparklineFormatter({
|
|
544
|
+
unit: '',
|
|
545
|
+
lineColor: '#6f42c1',
|
|
546
|
+
fillColor: 'rgba(111,66,193,0.15)',
|
|
547
|
+
decimals: 1
|
|
548
|
+
}),
|
|
549
|
+
headerTooltip: 'Throughput trend (Mbps)',
|
|
550
|
+
headerSort: false,
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
title: 'Avail %',
|
|
554
|
+
field: 'kpiAvailability',
|
|
555
|
+
width: 120,
|
|
556
|
+
hozAlign: 'center',
|
|
557
|
+
formatter: createSparklineFormatter({
|
|
558
|
+
unit: '%',
|
|
559
|
+
lineColor: '#198754',
|
|
560
|
+
fillColor: 'rgba(25,135,84,0.15)',
|
|
561
|
+
decimals: 1
|
|
562
|
+
}),
|
|
563
|
+
headerTooltip: 'Availability trend (%)',
|
|
564
|
+
headerSort: false,
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
title: 'Success %',
|
|
568
|
+
field: 'kpiSuccessRate',
|
|
569
|
+
width: 120,
|
|
570
|
+
hozAlign: 'center',
|
|
571
|
+
formatter: createSparklineFormatter({
|
|
572
|
+
unit: '%',
|
|
573
|
+
lineColor: '#fd7e14',
|
|
574
|
+
fillColor: 'rgba(253,126,20,0.15)',
|
|
575
|
+
decimals: 1
|
|
576
|
+
}),
|
|
577
|
+
headerTooltip: 'Success rate trend (%)',
|
|
578
|
+
headerSort: false,
|
|
579
|
+
},
|
|
458
580
|
// Position columns
|
|
459
581
|
{
|
|
460
582
|
title: 'Latitude',
|
|
@@ -549,6 +671,13 @@ export function getColumnsForPreset(preset, techColors = DEFAULT_TECH_COLORS, st
|
|
|
549
671
|
case 'compare':
|
|
550
672
|
visibleFields = [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.compare];
|
|
551
673
|
break;
|
|
674
|
+
case 'kpi':
|
|
675
|
+
visibleFields = [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.kpi];
|
|
676
|
+
break;
|
|
677
|
+
case 'history':
|
|
678
|
+
// Simplified view for config history - just cellName, date, and config fields
|
|
679
|
+
visibleFields = ['cellName', ...COLUMN_GROUPS.history];
|
|
680
|
+
break;
|
|
552
681
|
case 'default':
|
|
553
682
|
default:
|
|
554
683
|
visibleFields = [
|
|
@@ -613,14 +742,22 @@ export function getColumnMetadata() {
|
|
|
613
742
|
{ field: 'nwBW', title: 'NW BW', group: 'Network' },
|
|
614
743
|
// Atoll
|
|
615
744
|
{ field: 'atollET', title: 'Atoll ET', group: 'Atoll' },
|
|
745
|
+
{ field: 'atollMT', title: 'Atoll MT', group: 'Atoll' },
|
|
616
746
|
{ field: 'atollPW', title: 'Atoll PW', group: 'Atoll' },
|
|
617
747
|
{ field: 'atollRS', title: 'Atoll RS', group: 'Atoll' },
|
|
618
748
|
{ field: 'atollBW', title: 'Atoll BW', group: 'Atoll' },
|
|
749
|
+
// History
|
|
750
|
+
{ field: 'configDate', title: 'Config Date', group: 'History' },
|
|
619
751
|
// Compare (Atoll vs Network)
|
|
620
752
|
{ field: 'compareET', title: 'Δ ET', group: 'Compare' },
|
|
621
753
|
{ field: 'comparePW', title: 'Δ PW', group: 'Compare' },
|
|
622
754
|
{ field: 'compareRS', title: 'Δ RS', group: 'Compare' },
|
|
623
755
|
{ field: 'compareBW', title: 'Δ BW', group: 'Compare' },
|
|
756
|
+
// KPI Trends
|
|
757
|
+
{ field: 'kpiTraffic', title: 'Traffic', group: 'KPI' },
|
|
758
|
+
{ field: 'kpiThroughput', title: 'Throughput', group: 'KPI' },
|
|
759
|
+
{ field: 'kpiAvailability', title: 'Availability', group: 'KPI' },
|
|
760
|
+
{ field: 'kpiSuccessRate', title: 'Success Rate', group: 'KPI' },
|
|
624
761
|
// Position
|
|
625
762
|
{ field: 'latitude', title: 'Latitude', group: 'Position' },
|
|
626
763
|
{ field: 'longitude', title: 'Longitude', group: 'Position' },
|
|
@@ -651,6 +788,10 @@ export function getPresetVisibleFields(preset) {
|
|
|
651
788
|
return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.planning];
|
|
652
789
|
case 'compare':
|
|
653
790
|
return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.compare];
|
|
791
|
+
case 'kpi':
|
|
792
|
+
return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.kpi];
|
|
793
|
+
case 'history':
|
|
794
|
+
return ['cellName', ...COLUMN_GROUPS.history];
|
|
654
795
|
case 'default':
|
|
655
796
|
default:
|
|
656
797
|
return ['siteId', 'txId', 'cellName', 'tech', 'fband', 'frq', 'status', 'azimuth', 'height', 'antenna'];
|
|
@@ -7,7 +7,8 @@ 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 CellHistoryDemo } from './CellHistoryDemo.svelte';
|
|
10
11
|
export { default as ColumnPicker } from './ColumnPicker.svelte';
|
|
11
12
|
export { generateCells, generateCellsFromPreset, getGeneratorInfo, GENERATOR_PRESETS, type CellGeneratorConfig, type GeneratorPreset } from '../../shared/demo';
|
|
12
|
-
export type { CellData, CellTableGroupField, ColumnPreset, ColumnVisibility, TechColorMap, StatusColorMap, CellTableProps, RowSelectionEvent, RowClickEvent, RowDblClickEvent, DataChangeEvent, CellTableColumn, ColumnGroups } from './types';
|
|
13
|
+
export type { CellData, CellTableGroupField, ColumnPreset, ColumnVisibility, TechColorMap, StatusColorMap, CellTableProps, RowSelectionEvent, RowClickEvent, RowDblClickEvent, RowContextMenuEvent, DataChangeEvent, CellTableColumn, ColumnGroups } from './types';
|
|
13
14
|
export { DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS, COLUMN_GROUPS, getAllColumns, getColumnsForPreset, getGroupHeaderFormatter, getColumnMetadata, getPresetVisibleFields, fbandSorter, cellNameSectorSorter, cellDataSorter, createTechFormatter, createStatusFormatter, createFbandFormatter, numberFormatter, coordinateFormatter, azimuthFormatter, heightFormatter, type ColumnMeta } from './column-config';
|
|
@@ -8,6 +8,7 @@ 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 CellHistoryDemo } from './CellHistoryDemo.svelte';
|
|
11
12
|
export { default as ColumnPicker } from './ColumnPicker.svelte';
|
|
12
13
|
// Re-export shared demo data utilities for convenience
|
|
13
14
|
// Note: Cell type is NOT re-exported here to avoid conflicts with map-v2
|
|
@@ -16,7 +16,7 @@ export type CellTableGroupField = CellGroupingField | 'none';
|
|
|
16
16
|
/**
|
|
17
17
|
* Column preset configurations
|
|
18
18
|
*/
|
|
19
|
-
export type ColumnPreset = 'default' | 'compact' | 'full' | 'physical' | 'network' | 'planning' | 'compare';
|
|
19
|
+
export type ColumnPreset = 'default' | 'compact' | 'full' | 'physical' | 'network' | 'planning' | 'compare' | 'kpi' | 'history';
|
|
20
20
|
/**
|
|
21
21
|
* Column visibility configuration
|
|
22
22
|
*/
|
|
@@ -93,6 +93,13 @@ export interface RowDblClickEvent {
|
|
|
93
93
|
row: CellData;
|
|
94
94
|
event: MouseEvent;
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Row context menu (right-click) event data
|
|
98
|
+
*/
|
|
99
|
+
export interface RowContextMenuEvent {
|
|
100
|
+
row: CellData;
|
|
101
|
+
event: MouseEvent;
|
|
102
|
+
}
|
|
96
103
|
/**
|
|
97
104
|
* Data change event data
|
|
98
105
|
*/
|
|
@@ -123,4 +130,6 @@ export interface ColumnGroups {
|
|
|
123
130
|
atoll: string[];
|
|
124
131
|
position: string[];
|
|
125
132
|
compare: string[];
|
|
133
|
+
kpi: string[];
|
|
134
|
+
history: string[];
|
|
126
135
|
}
|
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
* Generates realistic cell network data with configurable parameters.
|
|
5
5
|
* Supports density zones, multiple technologies, and various site configurations.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Generate mock KPI time-series data
|
|
9
|
+
* Creates an array of values with realistic variation
|
|
10
|
+
*/
|
|
11
|
+
function generateMockKpiData(days, min, max, random) {
|
|
12
|
+
const data = [];
|
|
13
|
+
let current = min + random() * (max - min);
|
|
14
|
+
for (let i = 0; i < days; i++) {
|
|
15
|
+
// Add some realistic variation (±10%)
|
|
16
|
+
const variation = (random() - 0.5) * 0.2 * (max - min);
|
|
17
|
+
current = Math.max(min, Math.min(max, current + variation));
|
|
18
|
+
data.push(Math.round(current * 10) / 10);
|
|
19
|
+
}
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
7
22
|
/**
|
|
8
23
|
* Preset configurations
|
|
9
24
|
*/
|
|
@@ -211,6 +226,11 @@ export function generateCells(config) {
|
|
|
211
226
|
pci: cellCounter % 504,
|
|
212
227
|
nwRS: 450.0,
|
|
213
228
|
nwBW: 10.0,
|
|
229
|
+
// KPI time-series mock data (7 days)
|
|
230
|
+
kpiTraffic: generateMockKpiData(7, 50, 200, random),
|
|
231
|
+
kpiThroughput: generateMockKpiData(7, 20, 80, random),
|
|
232
|
+
kpiAvailability: generateMockKpiData(7, 95, 100, random),
|
|
233
|
+
kpiSuccessRate: generateMockKpiData(7, 90, 100, random),
|
|
214
234
|
other: {
|
|
215
235
|
city: ['Tehran', 'Shiraz', 'Isfahan', 'Mashhad', 'Tabriz'][siteNum % 5],
|
|
216
236
|
bcc: bandIndex % 8,
|
|
@@ -35,7 +35,9 @@ export interface Cell {
|
|
|
35
35
|
siteLongitude: number;
|
|
36
36
|
comment: string;
|
|
37
37
|
planner: string;
|
|
38
|
+
configDate?: string;
|
|
38
39
|
atollET?: number;
|
|
40
|
+
atollMT?: number;
|
|
39
41
|
atollPW?: number;
|
|
40
42
|
atollRS?: number;
|
|
41
43
|
atollBW?: number;
|
|
@@ -45,6 +47,10 @@ export interface Cell {
|
|
|
45
47
|
nwBW?: number;
|
|
46
48
|
pci?: number;
|
|
47
49
|
rru: string;
|
|
50
|
+
kpiTraffic?: number[];
|
|
51
|
+
kpiThroughput?: number[];
|
|
52
|
+
kpiAvailability?: number[];
|
|
53
|
+
kpiSuccessRate?: number[];
|
|
48
54
|
other?: Record<string, unknown>;
|
|
49
55
|
customSubgroup: string;
|
|
50
56
|
}
|
|
@@ -60,3 +66,16 @@ export type TechnologyBandKey = string;
|
|
|
60
66
|
* Grouping fields for views
|
|
61
67
|
*/
|
|
62
68
|
export type CellGroupingField = 'tech' | 'fband' | 'frq' | 'status' | 'siteId' | 'none';
|
|
69
|
+
/**
|
|
70
|
+
* Cell configuration history record
|
|
71
|
+
* Represents a snapshot of cell configuration at a point in time
|
|
72
|
+
*/
|
|
73
|
+
export interface CellConfigHistory {
|
|
74
|
+
id: string;
|
|
75
|
+
cellName: string;
|
|
76
|
+
configDate: string;
|
|
77
|
+
antenna: string;
|
|
78
|
+
antennaPower: number;
|
|
79
|
+
mechanicalTilt: number;
|
|
80
|
+
electricalTilt: number;
|
|
81
|
+
}
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides common demo data generators and types for use across components.
|
|
5
5
|
*/
|
|
6
|
-
export type { Cell, CellStatus, CellGroupingField } from './cell-types';
|
|
6
|
+
export type { Cell, CellStatus, CellGroupingField, CellConfigHistory } from './cell-types';
|
|
7
7
|
export { generateCells, generateCellsFromPreset, getGeneratorInfo, GENERATOR_PRESETS, type CellGeneratorConfig, type GeneratorPreset } from './cell-generator';
|