@smartnet360/svelte-components 0.0.143 → 0.0.145

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.
@@ -110,11 +110,14 @@
110
110
  if (externalAntenna1) {
111
111
  const found = findAntennaBySpec(antennas, externalAntenna1);
112
112
  if (found) {
113
+ // Compute available tilts INLINE to avoid stale state issue
114
+ const tiltsForAntenna1 = getAvailableTiltsForAntenna(found);
115
+
113
116
  // Use untrack to prevent infinite loop from state updates
114
117
  untrack(() => {
115
118
  selectedAntenna = found;
116
- updateAvailableTilts(found);
117
- ant1ElectricalTilt = findTiltIndex(availableElectricalTilts, externalAntenna1.electricalTilt ?? 0);
119
+ availableElectricalTilts = tiltsForAntenna1;
120
+ ant1ElectricalTilt = findTiltIndex(tiltsForAntenna1, externalAntenna1.electricalTilt ?? 0);
118
121
  ant1MechanicalTilt = externalAntenna1.mechanicalTilt ?? 0;
119
122
  });
120
123
  } else {
@@ -126,9 +129,10 @@
126
129
  if (externalAntenna2) {
127
130
  const found2 = findAntennaBySpec(antennas, externalAntenna2);
128
131
  if (found2) {
132
+ const tiltsForAntenna2 = getAvailableTiltsForAntenna(found2);
129
133
  untrack(() => {
130
134
  selectedAntenna2 = found2;
131
- ant2ElectricalTilt = findTiltIndex(getAvailableTiltsForAntenna(found2), externalAntenna2.electricalTilt ?? 0);
135
+ ant2ElectricalTilt = findTiltIndex(tiltsForAntenna2, externalAntenna2.electricalTilt ?? 0);
132
136
  ant2MechanicalTilt = externalAntenna2.mechanicalTilt ?? 0;
133
137
  if (viewMode === 'single') {
134
138
  viewMode = 'compare';
@@ -17,6 +17,14 @@
17
17
  import { buildCustomCellTree } from '../logic/tree-adapter';
18
18
  import type { CustomSetsApiClient } from '../db/custom-sets-api';
19
19
  import ServerSetBrowser from './ServerSetBrowser.svelte';
20
+ import { extractCsvHeaders } from '../logic/csv-parser';
21
+ import {
22
+ ColumnMapper,
23
+ autoDetectImport,
24
+ getFieldsForMode,
25
+ type ColumnMapping,
26
+ type ImportMode
27
+ } from '../../../../shared/csv-import';
20
28
 
21
29
  interface Props {
22
30
  /** The custom cell sets store */
@@ -57,6 +65,15 @@
57
65
  let importError = $state('');
58
66
  let fileInput: HTMLInputElement;
59
67
 
68
+ // Column mapping state
69
+ let showMappingModal = $state(false);
70
+ let pendingCsvContent = $state<string | null>(null);
71
+ let csvHeaders = $state<string[]>([]);
72
+ let columnMapping = $state<ColumnMapping>({});
73
+ let importMode = $state<ImportMode>('cell');
74
+ let includeAllExtras = $state(true);
75
+ let selectedExtras = $state<number[]>([]);
76
+
60
77
  // Quick Add state
61
78
  let showQuickAdd = $state(false);
62
79
  let quickAddText = $state('');
@@ -154,8 +171,23 @@
154
171
  reader.onload = (e) => {
155
172
  try {
156
173
  const content = e.target?.result as string;
157
- importResult = setsStore.importFromCsv(content, file.name);
158
- showImportModal = true;
174
+ // Extract headers and show mapping modal
175
+ const headers = extractCsvHeaders(content);
176
+ if (headers.length === 0) {
177
+ throw new Error('CSV file appears to be empty');
178
+ }
179
+
180
+ // Store content for later parsing
181
+ pendingCsvContent = content;
182
+ csvHeaders = headers;
183
+
184
+ // Auto-detect import mode and column mapping
185
+ const detected = autoDetectImport(headers);
186
+ importMode = detected.mode;
187
+ columnMapping = detected.mapping;
188
+
189
+ // Show mapping modal
190
+ showMappingModal = true;
159
191
  } catch (err) {
160
192
  importError = err instanceof Error ? err.message : 'Failed to parse CSV';
161
193
  importResult = null;
@@ -168,6 +200,34 @@
168
200
  reader.readAsText(file);
169
201
  input.value = '';
170
202
  }
203
+
204
+ function confirmMapping() {
205
+ if (!pendingCsvContent) return;
206
+
207
+ try {
208
+ importResult = setsStore.importFromCsv(pendingCsvContent, importFileName, {
209
+ columnMapping,
210
+ includeAllExtras,
211
+ selectedExtraIndices: selectedExtras
212
+ });
213
+ showMappingModal = false;
214
+ showImportModal = true;
215
+ } catch (err) {
216
+ importError = err instanceof Error ? err.message : 'Failed to parse CSV';
217
+ showMappingModal = false;
218
+ importResult = null;
219
+ }
220
+ }
221
+
222
+ function closeMappingModal() {
223
+ showMappingModal = false;
224
+ pendingCsvContent = null;
225
+ csvHeaders = [];
226
+ columnMapping = {};
227
+ importMode = 'cell';
228
+ includeAllExtras = true;
229
+ selectedExtras = [];
230
+ }
171
231
 
172
232
  function confirmImport() {
173
233
  if (!importResult) return;
@@ -390,8 +450,8 @@
390
450
  <input
391
451
  type="range"
392
452
  class="form-range"
393
- min="10"
394
- max="200"
453
+ min="5"
454
+ max="100"
395
455
  step="5"
396
456
  value={globalSectorSize}
397
457
  oninput={handleGlobalSectorSizeChange}
@@ -523,6 +583,56 @@
523
583
  </div>
524
584
  </MapControl>
525
585
 
586
+ <!-- Column Mapping Modal -->
587
+ {#if showMappingModal && csvHeaders.length > 0}
588
+ {@const fieldsConfig = getFieldsForMode(importMode)}
589
+ {@const isValid = fieldsConfig.required.every(f =>
590
+ columnMapping[f.type] !== null && columnMapping[f.type] !== undefined
591
+ )}
592
+ <div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
593
+ <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
594
+ <div class="modal-content">
595
+ <div class="modal-header py-2">
596
+ <h6 class="modal-title">
597
+ <i class="bi bi-grid-3x3 me-2"></i>
598
+ Import: {importFileName}
599
+ </h6>
600
+ <button type="button" class="btn-close" aria-label="Close" onclick={closeMappingModal}></button>
601
+ </div>
602
+ <div class="modal-body py-2">
603
+ <ColumnMapper
604
+ headers={csvHeaders}
605
+ mode={importMode}
606
+ mapping={columnMapping}
607
+ {includeAllExtras}
608
+ {selectedExtras}
609
+ onmodechange={(m) => importMode = m}
610
+ onchange={(m) => columnMapping = m}
611
+ onextraschange={(all, selected) => {
612
+ includeAllExtras = all;
613
+ selectedExtras = selected;
614
+ }}
615
+ />
616
+ </div>
617
+ <div class="modal-footer py-2">
618
+ <button type="button" class="btn btn-secondary btn-sm" onclick={closeMappingModal}>
619
+ Cancel
620
+ </button>
621
+ <button
622
+ type="button"
623
+ class="btn btn-primary btn-sm"
624
+ onclick={confirmMapping}
625
+ disabled={!isValid}
626
+ >
627
+ <i class="bi bi-arrow-right me-1"></i>
628
+ Continue
629
+ </button>
630
+ </div>
631
+ </div>
632
+ </div>
633
+ </div>
634
+ {/if}
635
+
526
636
  <!-- Import Result Modal -->
527
637
  {#if showImportModal && importResult}
528
638
  <div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
@@ -203,9 +203,7 @@
203
203
  customGroup: item.customGroup,
204
204
  sizeFactor: item.sizeFactor,
205
205
  setName: set.name,
206
- ...Object.fromEntries(
207
- Object.entries(item.extraFields).map(([k, v]) => [`extra_${k}`, v])
208
- )
206
+ popupInfo: { ...item.extraFields }
209
207
  }
210
208
  });
211
209
  } else if (item.geometry === 'cell' && item.resolvedCell) {
@@ -226,11 +224,7 @@
226
224
  feature.properties.customGroup = item.customGroup;
227
225
  feature.properties.sizeFactor = item.sizeFactor;
228
226
  feature.properties.setName = set.name;
229
-
230
- // Add extra fields
231
- for (const [key, value] of Object.entries(item.extraFields)) {
232
- feature.properties[`extra_${key}`] = value;
233
- }
227
+ feature.properties.popupInfo = { ...item.extraFields };
234
228
  }
235
229
 
236
230
  cellFeatures.push(feature);
@@ -9,21 +9,44 @@
9
9
  * Required column: id (or cellName/txId for backwards compat)
10
10
  * Optional columns: customGroup, sizeFactor, lat, lon, + any extras for tooltips
11
11
  * Supports both comma (,) and semicolon (;) delimiters
12
+ *
13
+ * Supports user-defined column mapping via ColumnMapping from csv-import module.
12
14
  */
13
15
  import type { CustomCellImportResult } from '../types';
14
16
  import type { Cell } from '../../../../shared/demo';
17
+ import type { ColumnMapping } from '../../../../shared/csv-import';
15
18
  /**
16
19
  * Supported delimiters
17
20
  */
18
21
  export type CsvDelimiter = ',' | ';' | 'auto';
22
+ /**
23
+ * Extract headers from CSV content
24
+ * @param csvContent Raw CSV content
25
+ * @param delimiter Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect)
26
+ * @returns Array of header strings
27
+ */
28
+ export declare function extractCsvHeaders(csvContent: string, delimiter?: CsvDelimiter): string[];
29
+ /**
30
+ * Options for CSV parsing
31
+ */
32
+ export interface CsvParseOptions {
33
+ /** Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect) */
34
+ delimiter?: CsvDelimiter;
35
+ /** User-defined column mapping (overrides auto-detection) */
36
+ columnMapping?: ColumnMapping;
37
+ /** Whether to include all extra columns (default: true) */
38
+ includeAllExtras?: boolean;
39
+ /** Specific extra column indices to include (when includeAllExtras is false) */
40
+ selectedExtraIndices?: number[];
41
+ }
19
42
  /**
20
43
  * Parse a CSV string into custom items (cells or points)
21
44
  * @param csvContent Raw CSV content
22
45
  * @param cellLookup Map of cellName -> Cell for resolving cell data
23
- * @param delimiter Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect)
46
+ * @param options Parsing options
24
47
  * @returns Import result with items, unmatched IDs, groups, and extra columns
25
48
  */
26
- export declare function parseCustomCellsCsv(csvContent: string, cellLookup: Map<string, Cell>, delimiter?: CsvDelimiter): CustomCellImportResult;
49
+ export declare function parseCustomCellsCsv(csvContent: string, cellLookup: Map<string, Cell>, options?: CsvParseOptions): CustomCellImportResult;
27
50
  /**
28
51
  * Build a cell lookup map from an array of cells
29
52
  * Creates lookups for both cellName and txId for flexible matching
@@ -9,6 +9,8 @@
9
9
  * Required column: id (or cellName/txId for backwards compat)
10
10
  * Optional columns: customGroup, sizeFactor, lat, lon, + any extras for tooltips
11
11
  * Supports both comma (,) and semicolon (;) delimiters
12
+ *
13
+ * Supports user-defined column mapping via ColumnMapping from csv-import module.
12
14
  */
13
15
  /**
14
16
  * Known/reserved column names
@@ -29,14 +31,29 @@ function detectDelimiter(headerLine) {
29
31
  const semicolonCount = (headerLine.match(/;/g) || []).length;
30
32
  return semicolonCount > commaCount ? ';' : ',';
31
33
  }
34
+ /**
35
+ * Extract headers from CSV content
36
+ * @param csvContent Raw CSV content
37
+ * @param delimiter Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect)
38
+ * @returns Array of header strings
39
+ */
40
+ export function extractCsvHeaders(csvContent, delimiter = 'auto') {
41
+ const lines = csvContent.trim().split('\n');
42
+ if (lines.length === 0)
43
+ return [];
44
+ const headerLine = lines[0];
45
+ const actualDelimiter = delimiter === 'auto' ? detectDelimiter(headerLine) : delimiter;
46
+ return parseCSVLine(headerLine, actualDelimiter);
47
+ }
32
48
  /**
33
49
  * Parse a CSV string into custom items (cells or points)
34
50
  * @param csvContent Raw CSV content
35
51
  * @param cellLookup Map of cellName -> Cell for resolving cell data
36
- * @param delimiter Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect)
52
+ * @param options Parsing options
37
53
  * @returns Import result with items, unmatched IDs, groups, and extra columns
38
54
  */
39
- export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto') {
55
+ export function parseCustomCellsCsv(csvContent, cellLookup, options = {}) {
56
+ const { delimiter = 'auto', columnMapping, includeAllExtras = true, selectedExtraIndices = [] } = options;
40
57
  const lines = csvContent.trim().split('\n');
41
58
  if (lines.length < 2) {
42
59
  return {
@@ -55,34 +72,66 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto')
55
72
  // Parse header
56
73
  const headers = parseCSVLine(headerLine, actualDelimiter);
57
74
  const normalizedHeaders = headers.map(normalizeColumnName);
58
- // Find ID column - prefer 'id', then 'cellname', then 'txid'
59
- let idIndex = normalizedHeaders.findIndex(h => h === 'id');
60
- if (idIndex === -1) {
61
- idIndex = normalizedHeaders.findIndex(h => h === 'cellname');
75
+ // Use column mapping if provided, otherwise auto-detect
76
+ let idIndex;
77
+ let groupIndex;
78
+ let sizeFactorIndex;
79
+ let latIndex;
80
+ let lonIndex;
81
+ if (columnMapping) {
82
+ // Use user-defined mapping
83
+ idIndex = columnMapping.id ?? -1;
84
+ groupIndex = columnMapping.group ?? -1;
85
+ sizeFactorIndex = columnMapping.sizeFactor ?? -1;
86
+ latIndex = columnMapping.lat ?? -1;
87
+ lonIndex = columnMapping.lon ?? -1;
62
88
  }
63
- if (idIndex === -1) {
64
- idIndex = normalizedHeaders.findIndex(h => h === 'txid');
89
+ else {
90
+ // Auto-detect: Find ID column - prefer 'id', then 'cellname', then 'txid'
91
+ idIndex = normalizedHeaders.findIndex(h => h === 'id');
92
+ if (idIndex === -1) {
93
+ idIndex = normalizedHeaders.findIndex(h => h === 'cellname');
94
+ }
95
+ if (idIndex === -1) {
96
+ idIndex = normalizedHeaders.findIndex(h => h === 'txid');
97
+ }
98
+ // Find optional columns
99
+ groupIndex = normalizedHeaders.findIndex(h => h === 'customgroup');
100
+ sizeFactorIndex = normalizedHeaders.findIndex(h => h === 'sizefactor');
101
+ // Find lat/lon columns for point geometry
102
+ latIndex = normalizedHeaders.findIndex(h => h === 'lat' || h === 'latitude');
103
+ lonIndex = normalizedHeaders.findIndex(h => h === 'lon' || h === 'lng' || h === 'longitude');
65
104
  }
66
105
  if (idIndex === -1) {
67
106
  throw new Error('CSV must contain an "id", "cellName", or "txId" column');
68
107
  }
69
- // Find optional columns
70
- const groupIndex = normalizedHeaders.findIndex(h => h === 'customgroup');
71
- const sizeFactorIndex = normalizedHeaders.findIndex(h => h === 'sizefactor');
72
- // Find lat/lon columns for point geometry
73
- let latIndex = normalizedHeaders.findIndex(h => h === 'lat' || h === 'latitude');
74
- let lonIndex = normalizedHeaders.findIndex(h => h === 'lon' || h === 'lng' || h === 'longitude');
75
108
  const hasLatLon = latIndex !== -1 && lonIndex !== -1;
76
- // Find extra columns (not reserved)
109
+ // Find extra columns based on user selection
77
110
  const extraColumns = [];
78
111
  const extraIndices = [];
79
- headers.forEach((header, idx) => {
80
- const normalized = normalizeColumnName(header);
81
- if (!RESERVED_COLUMNS.includes(normalized)) {
82
- extraColumns.push(header.trim());
83
- extraIndices.push(idx);
112
+ // Get indices that are already mapped to known fields
113
+ const mappedIndices = new Set([idIndex, groupIndex, sizeFactorIndex, latIndex, lonIndex].filter(i => i >= 0));
114
+ if (includeAllExtras) {
115
+ // Include all non-reserved, non-mapped columns
116
+ headers.forEach((header, idx) => {
117
+ if (mappedIndices.has(idx))
118
+ return;
119
+ const normalized = normalizeColumnName(header);
120
+ if (!RESERVED_COLUMNS.includes(normalized)) {
121
+ extraColumns.push(header.trim());
122
+ extraIndices.push(idx);
123
+ }
124
+ });
125
+ }
126
+ else {
127
+ // Include only specifically selected columns
128
+ for (const idx of selectedExtraIndices) {
129
+ if (idx >= 0 && idx < headers.length && !mappedIndices.has(idx)) {
130
+ extraColumns.push(headers[idx].trim());
131
+ extraIndices.push(idx);
132
+ }
84
133
  }
85
- });
134
+ }
86
135
  // Parse data rows
87
136
  const cells = [];
88
137
  const unmatchedTxIds = [];
@@ -10,8 +10,9 @@ export function buildCustomCellTree(set) {
10
10
  // Group cells by customGroup
11
11
  const groupMap = new Map();
12
12
  for (const cell of set.cells) {
13
- if (!cell.resolvedCell)
14
- continue; // Skip unresolved cells
13
+ // Skip cells that failed to resolve, but always include points
14
+ if (!cell.resolvedCell && cell.geometry !== 'point')
15
+ continue;
15
16
  if (!groupMap.has(cell.customGroup)) {
16
17
  groupMap.set(cell.customGroup, []);
17
18
  }
@@ -58,7 +59,8 @@ export function buildCustomCellTree(set) {
58
59
  export function getGroupCounts(set) {
59
60
  const counts = new Map();
60
61
  for (const cell of set.cells) {
61
- if (!cell.resolvedCell)
62
+ // Skip cells that failed to resolve, but always include points
63
+ if (!cell.resolvedCell && cell.geometry !== 'point')
62
64
  continue;
63
65
  const current = counts.get(cell.customGroup) || 0;
64
66
  counts.set(cell.customGroup, current + 1);
@@ -7,6 +7,7 @@
7
7
  import type { Cell } from '../../../../shared/demo';
8
8
  import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
9
9
  import type { CustomCellSet, CustomCellImportResult } from '../types';
10
+ import { type CsvParseOptions } from '../logic/csv-parser';
10
11
  /** Function that returns the current cells array */
11
12
  type CellsGetter = () => Cell[];
12
13
  /**
@@ -29,8 +30,11 @@ export declare class CustomCellSetsStore {
29
30
  constructor(cells: Cell[] | CellsGetter, namespace?: string);
30
31
  /**
31
32
  * Import a CSV file and create a new custom cell set
33
+ * @param csvContent Raw CSV content
34
+ * @param fileName Name of the file being imported
35
+ * @param options Parsing options (column mapping, extra fields selection)
32
36
  */
33
- importFromCsv(csvContent: string, fileName: string): CustomCellImportResult;
37
+ importFromCsv(csvContent: string, fileName: string, options?: CsvParseOptions): CustomCellImportResult;
34
38
  /**
35
39
  * Create a new set from import result
36
40
  */
@@ -67,12 +67,15 @@ export class CustomCellSetsStore {
67
67
  }
68
68
  /**
69
69
  * Import a CSV file and create a new custom cell set
70
+ * @param csvContent Raw CSV content
71
+ * @param fileName Name of the file being imported
72
+ * @param options Parsing options (column mapping, extra fields selection)
70
73
  */
71
- importFromCsv(csvContent, fileName) {
74
+ importFromCsv(csvContent, fileName, options) {
72
75
  // Build lookup from all cells
73
76
  const cellLookup = buildCellLookup(this.getCells());
74
- // Parse CSV
75
- const result = parseCustomCellsCsv(csvContent, cellLookup);
77
+ // Parse CSV with options
78
+ const result = parseCustomCellsCsv(csvContent, cellLookup, options || {});
76
79
  return result;
77
80
  }
78
81
  /**
@@ -0,0 +1,288 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Column Mapper Component
4
+ *
5
+ * Compact UI for mapping CSV columns to expected fields.
6
+ * Includes import mode selection (Cell/Point/Site) with auto-detection.
7
+ * Supports extra field selection for tooltips.
8
+ */
9
+ import type { ColumnMapping, ImportFieldsConfig, ImportMode } from './types';
10
+ import { getFieldsForMode } from './types';
11
+ import { detectColumnMapping } from './column-detector';
12
+
13
+ interface Props {
14
+ /** CSV column headers */
15
+ headers: string[];
16
+ /** Current import mode */
17
+ mode: ImportMode;
18
+ /** Current mapping (can be auto-detected or user-modified) */
19
+ mapping: ColumnMapping;
20
+ /** Whether to include all extra columns */
21
+ includeAllExtras?: boolean;
22
+ /** Selected extra column indices (when not including all) */
23
+ selectedExtras?: number[];
24
+ /** Called when mode changes */
25
+ onmodechange?: (mode: ImportMode) => void;
26
+ /** Called when mapping changes */
27
+ onchange?: (mapping: ColumnMapping) => void;
28
+ /** Called when extra fields settings change */
29
+ onextraschange?: (includeAll: boolean, selected: number[]) => void;
30
+ }
31
+
32
+ let {
33
+ headers,
34
+ mode,
35
+ mapping,
36
+ includeAllExtras = true,
37
+ selectedExtras = [],
38
+ onmodechange,
39
+ onchange,
40
+ onextraschange
41
+ }: Props = $props();
42
+
43
+ // Get fields config based on current mode
44
+ let fieldsConfig = $derived(getFieldsForMode(mode));
45
+
46
+ // All fields to display
47
+ let allFields = $derived([...fieldsConfig.required, ...fieldsConfig.optional]);
48
+
49
+ // Get mapped column indices (to exclude from extra fields options)
50
+ let mappedIndices = $derived(new Set(
51
+ Object.values(mapping).filter((v): v is number => v !== null && v !== undefined)
52
+ ));
53
+
54
+ // Available columns for extra fields (not already mapped)
55
+ let availableExtras = $derived(
56
+ headers
57
+ .map((header, idx) => ({ header, idx }))
58
+ .filter(({ idx }) => !mappedIndices.has(idx))
59
+ );
60
+
61
+ // Validation state
62
+ let missingRequired = $derived(
63
+ fieldsConfig.required.filter(f =>
64
+ mapping[f.type] === null || mapping[f.type] === undefined
65
+ )
66
+ );
67
+
68
+ function handleModeChange(newMode: ImportMode) {
69
+ if (newMode === mode) return;
70
+
71
+ // Re-detect mapping for new mode
72
+ const newFieldsConfig = getFieldsForMode(newMode);
73
+ const newMapping = detectColumnMapping(headers, newFieldsConfig);
74
+
75
+ onmodechange?.(newMode);
76
+ onchange?.(newMapping);
77
+ }
78
+
79
+ function handleFieldChange(fieldType: string, event: Event) {
80
+ const select = event.target as HTMLSelectElement;
81
+ const value = select.value;
82
+
83
+ const newMapping = { ...mapping };
84
+
85
+ if (value === '') {
86
+ newMapping[fieldType] = null;
87
+ } else {
88
+ newMapping[fieldType] = parseInt(value, 10);
89
+ }
90
+
91
+ onchange?.(newMapping);
92
+ }
93
+
94
+ function getFieldValue(fieldType: string): string {
95
+ const value = mapping[fieldType];
96
+ return value === null || value === undefined ? '' : String(value);
97
+ }
98
+
99
+ function getMappedHeader(fieldType: string): string | null {
100
+ const idx = mapping[fieldType];
101
+ return idx !== null && idx !== undefined ? headers[idx] : null;
102
+ }
103
+
104
+ // Extra fields handlers
105
+ function handleIncludeAllChange(event: Event) {
106
+ const checked = (event.target as HTMLInputElement).checked;
107
+ onextraschange?.(checked, selectedExtras);
108
+ }
109
+
110
+ function handleExtraFieldChange(slotIndex: number, event: Event) {
111
+ const select = event.target as HTMLSelectElement;
112
+ const value = select.value;
113
+
114
+ const newSelected = [...selectedExtras];
115
+
116
+ if (value === '') {
117
+ // Remove this slot
118
+ newSelected[slotIndex] = -1;
119
+ } else {
120
+ newSelected[slotIndex] = parseInt(value, 10);
121
+ }
122
+
123
+ // Filter out -1 values and duplicates
124
+ const filtered = newSelected.filter((v, i) => v >= 0 && newSelected.indexOf(v) === i);
125
+ onextraschange?.(includeAllExtras, filtered);
126
+ }
127
+
128
+ function getExtraSlotValue(slotIndex: number): string {
129
+ const value = selectedExtras[slotIndex];
130
+ return value === undefined || value < 0 ? '' : String(value);
131
+ }
132
+ </script>
133
+
134
+ <div class="column-mapper">
135
+ <!-- Import Mode Selection -->
136
+ <div class="mode-selection mb-3 pb-2 border-bottom">
137
+ <div class="d-flex align-items-center gap-3">
138
+ <span class="small text-muted">Import as:</span>
139
+ <div class="btn-group btn-group-sm" role="group" aria-label="Import mode">
140
+ <input
141
+ type="radio"
142
+ class="btn-check"
143
+ name="importMode"
144
+ id="mode-cell"
145
+ checked={mode === 'cell'}
146
+ onchange={() => handleModeChange('cell')}
147
+ />
148
+ <label class="btn btn-outline-secondary" for="mode-cell">
149
+ <i class="bi bi-broadcast me-1"></i>Cells
150
+ </label>
151
+
152
+ <input
153
+ type="radio"
154
+ class="btn-check"
155
+ name="importMode"
156
+ id="mode-point"
157
+ checked={mode === 'point'}
158
+ onchange={() => handleModeChange('point')}
159
+ />
160
+ <label class="btn btn-outline-secondary" for="mode-point">
161
+ <i class="bi bi-geo-alt me-1"></i>Points
162
+ </label>
163
+
164
+ <input
165
+ type="radio"
166
+ class="btn-check"
167
+ name="importMode"
168
+ id="mode-site"
169
+ checked={mode === 'site'}
170
+ onchange={() => handleModeChange('site')}
171
+ />
172
+ <label class="btn btn-outline-secondary" for="mode-site">
173
+ <i class="bi bi-pin-map me-1"></i>Sites
174
+ </label>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Column Mapping Grid -->
180
+ <div class="mapping-grid">
181
+ {#each allFields as field (field.type)}
182
+ {@const isRequired = field.required}
183
+ {@const isMapped = mapping[field.type] !== null && mapping[field.type] !== undefined}
184
+ {@const fieldId = `column-map-${field.type}`}
185
+
186
+ <div class="mapping-row d-flex align-items-center gap-2 mb-2">
187
+ <label
188
+ class="field-label mb-0"
189
+ for={fieldId}
190
+ title={field.description || field.label}
191
+ >
192
+ {field.label}{#if isRequired}<span class="text-danger">*</span>{/if}
193
+ </label>
194
+ <select
195
+ id={fieldId}
196
+ class="form-select form-select-sm flex-grow-1"
197
+ class:is-invalid={isRequired && !isMapped}
198
+ class:is-valid={isMapped}
199
+ value={getFieldValue(field.type)}
200
+ onchange={(e) => handleFieldChange(field.type, e)}
201
+ >
202
+ <option value="">{field.description || '-- Select --'}</option>
203
+ {#each headers as header, idx}
204
+ <option value={String(idx)}>{header}</option>
205
+ {/each}
206
+ </select>
207
+ </div>
208
+ {/each}
209
+ </div>
210
+
211
+ <!-- Extra Fields Section -->
212
+ {#if availableExtras.length > 0}
213
+ <div class="extra-fields mt-3 pt-2 border-top">
214
+ <div class="d-flex align-items-center justify-content-between mb-2">
215
+ <span class="small fw-medium">Extra columns for popups</span>
216
+ <div class="form-check form-check-inline mb-0">
217
+ <input
218
+ class="form-check-input"
219
+ type="checkbox"
220
+ id="include-all-extras"
221
+ checked={includeAllExtras}
222
+ onchange={handleIncludeAllChange}
223
+ />
224
+ <label class="form-check-label small" for="include-all-extras">
225
+ Include all ({availableExtras.length})
226
+ </label>
227
+ </div>
228
+ </div>
229
+
230
+ {#if !includeAllExtras}
231
+ <div class="extra-slots">
232
+ {#each [0, 1, 2] as slotIndex}
233
+ {@const slotId = `extra-slot-${slotIndex}`}
234
+ <div class="d-flex align-items-center gap-2 mb-1">
235
+ <select
236
+ id={slotId}
237
+ class="form-select form-select-sm"
238
+ value={getExtraSlotValue(slotIndex)}
239
+ onchange={(e) => handleExtraFieldChange(slotIndex, e)}
240
+ aria-label={`Extra field ${slotIndex + 1}`}
241
+ >
242
+ <option value="">-- Extra {slotIndex + 1} --</option>
243
+ {#each availableExtras as { header, idx }}
244
+ <option value={String(idx)}>{header}</option>
245
+ {/each}
246
+ </select>
247
+ </div>
248
+ {/each}
249
+ </div>
250
+ {/if}
251
+ </div>
252
+ {/if}
253
+
254
+ <!-- Validation Warning -->
255
+ {#if missingRequired.length > 0}
256
+ <div class="alert alert-warning py-1 px-2 mt-2 mb-0 small">
257
+ <i class="bi bi-exclamation-triangle me-1"></i>
258
+ Required: {missingRequired.map(f => f.label).join(', ')}
259
+ </div>
260
+ {/if}
261
+ </div>
262
+
263
+ <style>
264
+ .column-mapper {
265
+ font-size: 0.875rem;
266
+ }
267
+
268
+ .field-label {
269
+ min-width: 80px;
270
+ font-weight: 500;
271
+ font-size: 0.8125rem;
272
+ }
273
+
274
+ .form-select-sm {
275
+ font-size: 0.8125rem;
276
+ padding: 0.25rem 0.5rem;
277
+ }
278
+
279
+ .mapping-row .form-select.is-valid {
280
+ border-color: var(--bs-success);
281
+ background-image: none; /* Remove check icon */
282
+ }
283
+
284
+ .btn-group-sm .btn {
285
+ font-size: 0.75rem;
286
+ padding: 0.25rem 0.5rem;
287
+ }
288
+ </style>
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Column Mapper Component
3
+ *
4
+ * Compact UI for mapping CSV columns to expected fields.
5
+ * Includes import mode selection (Cell/Point/Site) with auto-detection.
6
+ * Supports extra field selection for tooltips.
7
+ */
8
+ import type { ColumnMapping, ImportMode } from './types';
9
+ interface Props {
10
+ /** CSV column headers */
11
+ headers: string[];
12
+ /** Current import mode */
13
+ mode: ImportMode;
14
+ /** Current mapping (can be auto-detected or user-modified) */
15
+ mapping: ColumnMapping;
16
+ /** Whether to include all extra columns */
17
+ includeAllExtras?: boolean;
18
+ /** Selected extra column indices (when not including all) */
19
+ selectedExtras?: number[];
20
+ /** Called when mode changes */
21
+ onmodechange?: (mode: ImportMode) => void;
22
+ /** Called when mapping changes */
23
+ onchange?: (mapping: ColumnMapping) => void;
24
+ /** Called when extra fields settings change */
25
+ onextraschange?: (includeAll: boolean, selected: number[]) => void;
26
+ }
27
+ declare const ColumnMapper: import("svelte").Component<Props, {}, "">;
28
+ type ColumnMapper = ReturnType<typeof ColumnMapper>;
29
+ export default ColumnMapper;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared CSV Import - Column Detector
3
+ *
4
+ * Auto-detects column mappings based on header names.
5
+ * Uses expandable alias maps for flexible matching.
6
+ */
7
+ import type { ColumnMapping, StandardFieldType, ImportFieldsConfig, ImportMode } from './types';
8
+ /**
9
+ * Alias map for auto-detecting column types
10
+ * Add new aliases here to expand recognition
11
+ */
12
+ export declare const COLUMN_ALIASES: Record<StandardFieldType, string[]>;
13
+ /**
14
+ * Find the best matching field type for a column header
15
+ */
16
+ export declare function detectFieldType(header: string): StandardFieldType | null;
17
+ /**
18
+ * Auto-detect column mapping from headers
19
+ * Returns suggested mapping based on alias matching
20
+ */
21
+ export declare function detectColumnMapping(headers: string[], fieldsConfig: ImportFieldsConfig): ColumnMapping;
22
+ /**
23
+ * Validate that all required fields are mapped
24
+ */
25
+ export declare function validateMapping(mapping: ColumnMapping, fieldsConfig: ImportFieldsConfig): {
26
+ valid: boolean;
27
+ missing: string[];
28
+ };
29
+ /**
30
+ * Detect the most likely import mode based on headers
31
+ * Returns 'point' if lat/lon found, 'site' if site-related, otherwise 'cell'
32
+ */
33
+ export declare function detectImportMode(headers: string[]): ImportMode;
34
+ /**
35
+ * Auto-detect import mode and get initial mapping
36
+ */
37
+ export declare function autoDetectImport(headers: string[]): {
38
+ mode: ImportMode;
39
+ mapping: ColumnMapping;
40
+ fieldsConfig: ImportFieldsConfig;
41
+ };
42
+ /**
43
+ * Detect delimiter from CSV content
44
+ */
45
+ export declare function detectDelimiter(headerLine: string): ',' | ';';
46
+ /**
47
+ * Parse CSV header line and return headers array
48
+ */
49
+ export declare function parseHeaderLine(line: string, delimiter: ',' | ';'): string[];
50
+ /**
51
+ * Get CSV headers and suggested mapping
52
+ */
53
+ export declare function analyzeCSVHeaders(csvContent: string, fieldsConfig: ImportFieldsConfig): {
54
+ headers: string[];
55
+ delimiter: ',' | ';';
56
+ suggestedMapping: ColumnMapping;
57
+ rowCount: number;
58
+ };
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Shared CSV Import - Column Detector
3
+ *
4
+ * Auto-detects column mappings based on header names.
5
+ * Uses expandable alias maps for flexible matching.
6
+ */
7
+ import { getFieldsForMode } from './types';
8
+ /**
9
+ * Alias map for auto-detecting column types
10
+ * Add new aliases here to expand recognition
11
+ */
12
+ export const COLUMN_ALIASES = {
13
+ // Universal identifiers
14
+ id: [
15
+ 'id', 'cellname', 'cell_name', 'txid', 'tx_id',
16
+ 'site_id', 'siteid', 'site_code', 'sitecode',
17
+ 'name', 'code', 'identifier', 'key', 'cell_id', 'cellid'
18
+ ],
19
+ // Coordinates
20
+ lat: [
21
+ 'lat', 'latitude', 'y', 'y_coord', 'ycoord',
22
+ 'northing', 'north', 'lat_deg', 'latitude_deg'
23
+ ],
24
+ lon: [
25
+ 'lon', 'lng', 'long', 'longitude', 'x', 'x_coord', 'xcoord',
26
+ 'easting', 'east', 'lon_deg', 'longitude_deg'
27
+ ],
28
+ // Grouping
29
+ group: [
30
+ 'customgroup', 'custom_group', 'group', 'category',
31
+ 'type', 'region', 'area', 'zone', 'cluster', 'segment'
32
+ ],
33
+ // Sizing
34
+ sizeFactor: [
35
+ 'sizefactor', 'size_factor', 'size', 'scale', 'weight', 'factor'
36
+ ],
37
+ // Cell-specific
38
+ tech: [
39
+ 'tech', 'technology', 'rat', 'radio', 'system',
40
+ 'network_type', 'networktype', 'access_technology'
41
+ ],
42
+ band: [
43
+ 'band', 'fband', 'frequency_band', 'frequencyband',
44
+ 'freq', 'frequency', 'carrier', 'earfcn', 'arfcn'
45
+ ],
46
+ azimuth: [
47
+ 'azimuth', 'az', 'bearing', 'direction', 'heading',
48
+ 'antenna_azimuth', 'ant_azimuth', 'ant_az'
49
+ ],
50
+ tilt: [
51
+ 'tilt', 'downtilt', 'etilt', 'mtilt', 'electrical_tilt',
52
+ 'mechanical_tilt', 'antenna_tilt', 'ant_tilt'
53
+ ],
54
+ siteId: [
55
+ 'siteid', 'site_id', 'site', 'location_id', 'locationid',
56
+ 'enodeb_id', 'enodebid', 'gnodeb_id', 'gnodebid', 'bts_id'
57
+ ],
58
+ siteName: [
59
+ 'sitename', 'site_name', 'location_name', 'locationname',
60
+ 'site_label', 'sitelabel', 'station_name'
61
+ ],
62
+ status: [
63
+ 'status', 'state', 'cell_status', 'cellstatus',
64
+ 'operational_status', 'op_status', 'active'
65
+ ],
66
+ height: [
67
+ 'height', 'antenna_height', 'ant_height', 'agl',
68
+ 'elevation', 'tower_height', 'mast_height'
69
+ ],
70
+ power: [
71
+ 'power', 'tx_power', 'txpower', 'eirp',
72
+ 'transmit_power', 'output_power', 'pwr'
73
+ ]
74
+ };
75
+ /**
76
+ * Normalize a column header for comparison
77
+ */
78
+ function normalizeHeader(header) {
79
+ return header
80
+ .trim()
81
+ .toLowerCase()
82
+ .replace(/[\s\-\.]/g, '_'); // Normalize separators to underscore
83
+ }
84
+ /**
85
+ * Find the best matching field type for a column header
86
+ */
87
+ export function detectFieldType(header) {
88
+ const normalized = normalizeHeader(header);
89
+ for (const [fieldType, aliases] of Object.entries(COLUMN_ALIASES)) {
90
+ if (aliases.some(alias => normalized === alias || normalized.includes(alias))) {
91
+ return fieldType;
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ /**
97
+ * Auto-detect column mapping from headers
98
+ * Returns suggested mapping based on alias matching
99
+ */
100
+ export function detectColumnMapping(headers, fieldsConfig) {
101
+ const mapping = {};
102
+ const usedIndices = new Set();
103
+ // Get all field types we're looking for
104
+ const allFields = [...fieldsConfig.required, ...fieldsConfig.optional];
105
+ // First pass: exact matches and strong matches
106
+ for (const field of allFields) {
107
+ const aliases = COLUMN_ALIASES[field.type] || [];
108
+ for (let i = 0; i < headers.length; i++) {
109
+ if (usedIndices.has(i))
110
+ continue;
111
+ const normalized = normalizeHeader(headers[i]);
112
+ // Check for exact match first
113
+ if (aliases.includes(normalized)) {
114
+ mapping[field.type] = i;
115
+ usedIndices.add(i);
116
+ break;
117
+ }
118
+ }
119
+ }
120
+ // Second pass: partial matches for unmapped fields
121
+ for (const field of allFields) {
122
+ if (mapping[field.type] !== undefined)
123
+ continue;
124
+ const aliases = COLUMN_ALIASES[field.type] || [];
125
+ for (let i = 0; i < headers.length; i++) {
126
+ if (usedIndices.has(i))
127
+ continue;
128
+ const normalized = normalizeHeader(headers[i]);
129
+ // Check for partial match
130
+ if (aliases.some(alias => normalized.includes(alias) || alias.includes(normalized))) {
131
+ mapping[field.type] = i;
132
+ usedIndices.add(i);
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ // Set null for any fields not found
138
+ for (const field of allFields) {
139
+ if (mapping[field.type] === undefined) {
140
+ mapping[field.type] = null;
141
+ }
142
+ }
143
+ return mapping;
144
+ }
145
+ /**
146
+ * Validate that all required fields are mapped
147
+ */
148
+ export function validateMapping(mapping, fieldsConfig) {
149
+ const missing = [];
150
+ for (const field of fieldsConfig.required) {
151
+ if (mapping[field.type] === null || mapping[field.type] === undefined) {
152
+ missing.push(field.label);
153
+ }
154
+ }
155
+ return {
156
+ valid: missing.length === 0,
157
+ missing
158
+ };
159
+ }
160
+ /**
161
+ * Detect the most likely import mode based on headers
162
+ * Returns 'point' if lat/lon found, 'site' if site-related, otherwise 'cell'
163
+ */
164
+ export function detectImportMode(headers) {
165
+ const normalizedHeaders = headers.map(h => normalizeHeader(h));
166
+ // Check for lat/lon presence
167
+ const hasLat = normalizedHeaders.some(h => COLUMN_ALIASES.lat.some(alias => h === alias || h.includes(alias)));
168
+ const hasLon = normalizedHeaders.some(h => COLUMN_ALIASES.lon.some(alias => h === alias || h.includes(alias)));
169
+ const hasLatLon = hasLat && hasLon;
170
+ // Check for site-specific headers
171
+ const hasSiteId = normalizedHeaders.some(h => COLUMN_ALIASES.siteId.some(alias => h === alias || h.includes(alias)));
172
+ const hasSiteName = normalizedHeaders.some(h => COLUMN_ALIASES.siteName.some(alias => h === alias || h.includes(alias)));
173
+ // Check for cell-specific headers
174
+ const hasCellId = normalizedHeaders.some(h => ['cellname', 'cell_name', 'cellid', 'cell_id', 'txid', 'tx_id'].some(alias => h === alias || h.includes(alias)));
175
+ // Decision logic:
176
+ // 1. If has lat/lon AND site-specific headers → site mode
177
+ // 2. If has lat/lon AND no cell-specific headers → point mode
178
+ // 3. Otherwise → cell mode (ID resolves against existing cells)
179
+ if (hasLatLon && (hasSiteId || hasSiteName) && !hasCellId) {
180
+ return 'site';
181
+ }
182
+ if (hasLatLon && !hasCellId) {
183
+ return 'point';
184
+ }
185
+ return 'cell';
186
+ }
187
+ /**
188
+ * Auto-detect import mode and get initial mapping
189
+ */
190
+ export function autoDetectImport(headers) {
191
+ const mode = detectImportMode(headers);
192
+ const fieldsConfig = getFieldsForMode(mode);
193
+ const mapping = detectColumnMapping(headers, fieldsConfig);
194
+ return { mode, mapping, fieldsConfig };
195
+ }
196
+ /**
197
+ * Detect delimiter from CSV content
198
+ */
199
+ export function detectDelimiter(headerLine) {
200
+ const commaCount = (headerLine.match(/,/g) || []).length;
201
+ const semicolonCount = (headerLine.match(/;/g) || []).length;
202
+ return semicolonCount > commaCount ? ';' : ',';
203
+ }
204
+ /**
205
+ * Parse CSV header line and return headers array
206
+ */
207
+ export function parseHeaderLine(line, delimiter) {
208
+ // Simple parse (doesn't handle quoted fields with delimiters inside)
209
+ return line.split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, ''));
210
+ }
211
+ /**
212
+ * Get CSV headers and suggested mapping
213
+ */
214
+ export function analyzeCSVHeaders(csvContent, fieldsConfig) {
215
+ const lines = csvContent.trim().split('\n');
216
+ if (lines.length === 0) {
217
+ return { headers: [], delimiter: ',', suggestedMapping: {}, rowCount: 0 };
218
+ }
219
+ const delimiter = detectDelimiter(lines[0]);
220
+ const headers = parseHeaderLine(lines[0], delimiter);
221
+ const suggestedMapping = detectColumnMapping(headers, fieldsConfig);
222
+ return {
223
+ headers,
224
+ delimiter,
225
+ suggestedMapping,
226
+ rowCount: lines.length - 1
227
+ };
228
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * CSV Import Module
3
+ *
4
+ * Shared utilities for flexible CSV column mapping.
5
+ * Supports auto-detection of columns via aliases and manual mapping UI.
6
+ */
7
+ export type { StandardFieldType, FieldDefinition, ColumnMapping, ImportFieldsConfig, ImportMode } from './types';
8
+ export { CUSTOM_POINTS_FIELDS, CELL_IMPORT_FIELDS, CELL_MODE_FIELDS, POINT_MODE_FIELDS, SITE_MODE_FIELDS, getFieldsForMode } from './types';
9
+ export { COLUMN_ALIASES, detectColumnMapping, detectImportMode, autoDetectImport, validateMapping, analyzeCSVHeaders } from './column-detector';
10
+ export { default as ColumnMapper } from './ColumnMapper.svelte';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * CSV Import Module
3
+ *
4
+ * Shared utilities for flexible CSV column mapping.
5
+ * Supports auto-detection of columns via aliases and manual mapping UI.
6
+ */
7
+ // Field configurations
8
+ export { CUSTOM_POINTS_FIELDS, CELL_IMPORT_FIELDS, CELL_MODE_FIELDS, POINT_MODE_FIELDS, SITE_MODE_FIELDS, getFieldsForMode } from './types';
9
+ // Column detection utilities
10
+ export { COLUMN_ALIASES, detectColumnMapping, detectImportMode, autoDetectImport, validateMapping, analyzeCSVHeaders } from './column-detector';
11
+ // UI Component
12
+ export { default as ColumnMapper } from './ColumnMapper.svelte';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Shared CSV Import - Types
3
+ *
4
+ * Reusable types for CSV column mapping across different import features.
5
+ */
6
+ /**
7
+ * Import mode - determines which fields are shown and required
8
+ */
9
+ export type ImportMode = 'cell' | 'point' | 'site';
10
+ /**
11
+ * Standard field types that can be mapped
12
+ */
13
+ export type StandardFieldType = 'id' | 'lat' | 'lon' | 'group' | 'sizeFactor' | 'tech' | 'band' | 'azimuth' | 'tilt' | 'siteId' | 'siteName' | 'status' | 'height' | 'power';
14
+ /**
15
+ * Definition of a field for mapping
16
+ */
17
+ export interface FieldDefinition {
18
+ /** Field type identifier */
19
+ type: StandardFieldType;
20
+ /** Display label for UI */
21
+ label: string;
22
+ /** Whether this field is required */
23
+ required: boolean;
24
+ /** Description/help text */
25
+ description?: string;
26
+ }
27
+ /**
28
+ * Mapping of field types to CSV column indices
29
+ */
30
+ export interface ColumnMapping {
31
+ [fieldType: string]: number | null;
32
+ }
33
+ /**
34
+ * Result of parsing CSV headers
35
+ */
36
+ export interface CsvHeadersResult {
37
+ /** All column headers from CSV */
38
+ headers: string[];
39
+ /** Detected delimiter */
40
+ delimiter: ',' | ';';
41
+ /** Auto-detected column mapping (best guesses) */
42
+ suggestedMapping: ColumnMapping;
43
+ /** Total row count (excluding header) */
44
+ rowCount: number;
45
+ }
46
+ /**
47
+ * Configuration for a specific import context
48
+ */
49
+ export interface ImportFieldsConfig {
50
+ /** Fields that must be mapped */
51
+ required: FieldDefinition[];
52
+ /** Fields that can optionally be mapped */
53
+ optional: FieldDefinition[];
54
+ }
55
+ /**
56
+ * Predefined field configurations for import modes
57
+ */
58
+ /** Cell mode - ID resolves against existing cell data */
59
+ export declare const CELL_MODE_FIELDS: ImportFieldsConfig;
60
+ /** Point mode - creates markers at lat/lon coordinates */
61
+ export declare const POINT_MODE_FIELDS: ImportFieldsConfig;
62
+ /** Site mode - creates site markers at lat/lon */
63
+ export declare const SITE_MODE_FIELDS: ImportFieldsConfig;
64
+ /** Get field config for a given import mode */
65
+ export declare function getFieldsForMode(mode: ImportMode): ImportFieldsConfig;
66
+ export declare const CUSTOM_POINTS_FIELDS: ImportFieldsConfig;
67
+ export declare const CELL_IMPORT_FIELDS: ImportFieldsConfig;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Shared CSV Import - Types
3
+ *
4
+ * Reusable types for CSV column mapping across different import features.
5
+ */
6
+ /**
7
+ * Predefined field configurations for import modes
8
+ */
9
+ /** Cell mode - ID resolves against existing cell data */
10
+ export const CELL_MODE_FIELDS = {
11
+ required: [
12
+ { type: 'id', label: 'Cell ID', required: true, description: 'e.g. cellName, txId' }
13
+ ],
14
+ optional: [
15
+ { type: 'group', label: 'Group', required: false, description: 'e.g. customGroup, category' },
16
+ { type: 'sizeFactor', label: 'Size', required: false, description: 'e.g. sizeFactor, weight' }
17
+ ]
18
+ };
19
+ /** Point mode - creates markers at lat/lon coordinates */
20
+ export const POINT_MODE_FIELDS = {
21
+ required: [
22
+ { type: 'id', label: 'ID / Name', required: true, description: 'e.g. name, label, id' },
23
+ { type: 'lat', label: 'Latitude', required: true, description: 'e.g. lat, latitude, y' },
24
+ { type: 'lon', label: 'Longitude', required: true, description: 'e.g. lon, lng, longitude, x' }
25
+ ],
26
+ optional: [
27
+ { type: 'group', label: 'Group', required: false, description: 'e.g. category, type' },
28
+ { type: 'sizeFactor', label: 'Size', required: false, description: 'e.g. size, weight' }
29
+ ]
30
+ };
31
+ /** Site mode - creates site markers at lat/lon */
32
+ export const SITE_MODE_FIELDS = {
33
+ required: [
34
+ { type: 'id', label: 'Site ID', required: true, description: 'e.g. siteId, site_id, enodeb_id' },
35
+ { type: 'lat', label: 'Latitude', required: true, description: 'e.g. lat, latitude' },
36
+ { type: 'lon', label: 'Longitude', required: true, description: 'e.g. lon, longitude' }
37
+ ],
38
+ optional: [
39
+ { type: 'siteName', label: 'Site Name', required: false, description: 'e.g. site_name, location' },
40
+ { type: 'group', label: 'Group', required: false, description: 'e.g. region, area' }
41
+ ]
42
+ };
43
+ /** Get field config for a given import mode */
44
+ export function getFieldsForMode(mode) {
45
+ switch (mode) {
46
+ case 'cell': return CELL_MODE_FIELDS;
47
+ case 'point': return POINT_MODE_FIELDS;
48
+ case 'site': return SITE_MODE_FIELDS;
49
+ }
50
+ }
51
+ // Legacy exports for backward compatibility
52
+ export const CUSTOM_POINTS_FIELDS = CELL_MODE_FIELDS;
53
+ export const CELL_IMPORT_FIELDS = {
54
+ required: [
55
+ { type: 'id', label: 'Cell ID', required: true, description: 'Unique cell identifier' },
56
+ { type: 'lat', label: 'Latitude', required: true },
57
+ { type: 'lon', label: 'Longitude', required: true },
58
+ { type: 'azimuth', label: 'Azimuth', required: true, description: 'Antenna direction (0-360°)' }
59
+ ],
60
+ optional: [
61
+ { type: 'siteId', label: 'Site ID', required: false },
62
+ { type: 'siteName', label: 'Site Name', required: false },
63
+ { type: 'tech', label: 'Technology', required: false, description: 'e.g., LTE, NR, GSM' },
64
+ { type: 'band', label: 'Band', required: false, description: 'Frequency band' },
65
+ { type: 'tilt', label: 'Tilt', required: false },
66
+ { type: 'height', label: 'Height', required: false },
67
+ { type: 'power', label: 'Power', required: false },
68
+ { type: 'status', label: 'Status', required: false }
69
+ ]
70
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.143",
3
+ "version": "0.0.145",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",