Files

651 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ADBNetzgebiet - Netzgebietverwaltung (Vue 3 + TT-Core)
*/
const ADBNetzgebiet = {
name: 'ADBNetzgebiet',
template: `
<div class="tt-scope netzgebiet-container">
<section class="card card-in">
<!-- Header -->
<div class="pane-header">
<div class="title">
<span class="logo-dot"></span>
<span>Netzgebietverwaltung</span>
</div>
<button class="primary-btn" @click="openCreateModal">
<i class="fa-duotone fa-plus"></i> Neues Netzgebiet
</button>
</div>
<hr class="content-divider" />
<!-- Filter Bar -->
<div class="filter-bar">
<div class="filter-center">
<div class="input-wrap filter-main">
<i class="fa-duotone fa-magnifying-glass input-icon"></i>
<input class="ri" v-model.trim="filters.name" placeholder="Name suchen..." @input="debouncedFilter">
</div>
<div class="input-wrap filter-md">
<i class="fa-duotone fa-key input-icon"></i>
<input class="ri" v-model.trim="filters.extref" placeholder="ExtRef..." @input="debouncedFilter">
</div>
<div class="select filter-sm">
<select v-model="filters.source" @change="applyFilter">
<option value="">Alle Quellen</option>
<option v-for="source in availableSources" :key="source" :value="source">{{ source }}</option>
</select>
</div>
<div class="select filter-sm">
<select v-model="filters.hasNetwork" @change="applyFilter">
<option value="">Netzwerk</option>
<option value="yes">Mit Netzwerk</option>
<option value="no">Ohne Netzwerk</option>
</select>
</div>
<div class="select filter-sm">
<select v-model="filters.hasCampaign" @change="applyFilter">
<option value="">Kampagne</option>
<option value="yes">Mit Kampagne</option>
<option value="no">Ohne Kampagne</option>
</select>
</div>
<div class="select filter-sm">
<select v-model="filters.hasConsent" @change="applyFilter">
<option value="">Zustimmung</option>
<option value="yes">Mit Zustimmung</option>
<option value="no">Ohne Zustimmung</option>
</select>
</div>
<button v-if="hasActiveFilters" class="icon-btn" @click="clearFilters" title="Filter zurücksetzen">
<i class="fa-duotone fa-xmark"></i>
</button>
</div>
</div>
<!-- Data Table -->
<div class="table-container">
<table class="tt-table netzgebiet-table" v-if="!isLoading && paginatedItems.length">
<thead>
<tr>
<th class="col-name">Name / ExtRef</th>
<th class="col-source">Quelle</th>
<th class="col-freigabe">Freigaben</th>
<th class="col-network">Netzwerk</th>
<th class="col-campaign">Kampagne</th>
<th class="col-consent">Zustimmung</th>
<th class="col-actions"></th>
</tr>
</thead>
<tbody>
<tr v-for="item in paginatedItems" :key="item.netzgebiet.id">
<td class="col-name">
<a class="link name-link" href="#" @click.prevent="openEditModal(item)">{{ item.netzgebiet.name || '(Ohne Name)' }}</a>
<div v-if="item.netzgebiet.extref" class="sub-text mono">{{ item.netzgebiet.extref }}</div>
</td>
<td class="col-source">
<span class="source-badge">{{ item.netzgebiet.source || '—' }}</span>
<div v-if="item.netzgebiet.source_id" class="sub-text mono truncate" :title="item.netzgebiet.source_id">{{ item.netzgebiet.source_id }}</div>
</td>
<td class="col-freigabe">
<div class="freigabe-badges">
<span v-for="f in parsedFreigabe(item.netzgebiet.freigabe)" :key="f" class="freigabe-badge" :class="'f-' + f" :title="freigabeLabels[f]">{{ f.charAt(0).toUpperCase() }}</span>
<span v-if="!parsedFreigabe(item.netzgebiet.freigabe).length" class="muted">—</span>
</div>
</td>
<td class="col-network">
<template v-if="item.related.networks.length">
<a v-for="net in item.related.networks.slice(0, 2)" :key="net.id"
:href="window.TT_CONFIG.NETWORK_URL + '?id=' + net.id"
target="_blank" class="related-link">
{{ net.name }}
</a>
<span v-if="item.related.networks.length > 2" class="more-badge">+{{ item.related.networks.length - 2 }}</span>
</template>
<a v-else :href="window.TT_CONFIG.NETWORK_CREATE_URL + '?adb_netzgebiet_id=' + item.netzgebiet.id" class="create-link" title="Netzwerk erstellen">
<i class="fa-duotone fa-plus-circle"></i> Erstellen
</a>
</td>
<td class="col-campaign">
<template v-if="item.related.campaigns.length">
<a v-for="camp in item.related.campaigns.slice(0, 1)" :key="camp.id"
:href="window.TT_CONFIG.CAMPAIGN_URL + '?id=' + camp.id"
target="_blank" class="related-link">
{{ camp.name }}
</a>
<span v-if="item.related.campaigns.length > 1" class="more-badge">+{{ item.related.campaigns.length - 1 }}</span>
</template>
<a v-else-if="item.related.networks.length" :href="window.TT_CONFIG.CAMPAIGN_CREATE_URL + '?network_id=' + item.related.networks[0].id" class="create-link" title="Kampagne erstellen">
<i class="fa-duotone fa-plus-circle"></i> Erstellen
</a>
<span v-else class="muted">—</span>
</td>
<td class="col-consent">
<template v-if="item.related.consent_projects.length">
<a v-for="cons in item.related.consent_projects.slice(0, 1)" :key="cons.id"
:href="window.TT_CONFIG.CONSENT_URL + '?id=' + cons.id"
target="_blank" class="related-link" v-html="formatConsentName(cons.name)">
</a>
<span v-if="item.related.consent_projects.length > 1" class="more-badge">+{{ item.related.consent_projects.length - 1 }}</span>
</template>
<a v-else :href="window.TT_CONFIG.CONSENT_CREATE_URL + '?adb_netzgebiet_id=' + item.netzgebiet.id" class="create-link" title="Zustimmungsprojekt erstellen">
<i class="fa-duotone fa-plus-circle"></i> Erstellen
</a>
</td>
<td class="col-actions">
<button
v-if="item.netzgebiet.source && item.netzgebiet.source.startsWith('rimo-')"
class="icon-btn"
@click.prevent="handleRimoImportClick(item)"
:title="getImportButtonTitle(item.netzgebiet.id)"
:disabled="getImportButtonDisabled(item.netzgebiet.id)">
<i class="fa-duotone" :class="getImportButtonIcon(item.netzgebiet.id)"></i>
</button>
<button class="icon-btn" @click.prevent="openEditModal(item)" title="Bearbeiten"><i class="fa-duotone fa-pen"></i></button>
<button class="icon-btn" @click.prevent="copyNetzgebiet(item)" title="Kopieren"><i class="fa-duotone fa-copy"></i></button>
<button class="icon-btn" @click.prevent="openHistoryModal(item)" title="Verlauf"><i class="fa-duotone fa-clock-rotate-left"></i></button>
</td>
</tr>
</tbody>
</table>
<!-- Loading State -->
<div v-if="isLoading" class="table-placeholder">
<i class="fa-duotone fa-spinner fa-spin"></i>
<span>Lade Netzgebiete...</span>
</div>
<!-- Empty State -->
<div v-if="!isLoading && !filteredNetzgebiete.length" class="table-placeholder">
<i class="fa-duotone fa-database"></i>
<span>Keine Netzgebiete gefunden.</span>
</div>
</div>
<!-- Pagination -->
<div class="pagination-bar" v-if="!isLoading && filteredNetzgebiete.length">
<div class="pagination-info">
{{ paginationStart }}{{ paginationEnd }} von {{ filteredNetzgebiete.length }} Netzgebieten
</div>
<div class="pagination-controls">
<select v-model.number="pageSize" @change="currentPage = 1" class="page-size-select">
<option :value="25">25</option>
<option :value="50">50</option>
<option :value="100">100</option>
</select>
<button class="icon-btn" :disabled="currentPage <= 1" @click="currentPage--"><i class="fa-duotone fa-chevron-left"></i></button>
<span class="page-indicator">{{ currentPage }} / {{ totalPages }}</span>
<button class="icon-btn" :disabled="currentPage >= totalPages" @click="currentPage++"><i class="fa-duotone fa-chevron-right"></i></button>
</div>
</div>
</section>
<!-- Edit/Create Modal -->
<tt-dialog :show="showEditModal" :title="editItem && editItem.id ? 'Netzgebiet bearbeiten' : 'Neues Netzgebiet'" size="wide" @close="showEditModal = false">
<div v-if="editItem" class="modal-form">
<div class="form-grid">
<div class="field span-2">
<label>Name *</label>
<input class="ri" v-model="editItem.name" placeholder="Name des Netzgebiets">
</div>
<div class="field">
<label>Externe Referenz</label>
<input class="ri" v-model="editItem.extref" placeholder="ExtRef">
</div>
<div class="field">
<label>Quelle</label>
<div class="select">
<select v-model="editItem.source">
<option value="">Bitte wählen...</option>
<option value="rimo-rest-api">rimo-rest-api</option>
<option value="csv">csv</option>
<option value="csv-rimo">csv-rimo</option>
<option value="manual">manual</option>
<option value="xinon_qgis">xinon_qgis</option>
<option value="citycom-oan-api">citycom-oan-api</option>
<option value="test">test</option>
</select>
</div>
</div>
<div class="field">
<label>Source ID</label>
<input class="ri" v-model="editItem.source_id" placeholder="Source ID">
</div>
</div>
<div class="form-section">
<label class="section-label">Freigaben</label>
<div class="checkbox-row">
<label v-for="f in freigabeOptions" :key="f.key" class="checkbox-field">
<input type="checkbox" v-model="editItem.freigabe[f.key]">
<span>{{ f.label }}</span>
</label>
</div>
</div>
<div class="form-section">
<label class="section-label">Optionen</label>
<div class="options-grid">
<label v-for="opt in optionsConfig" :key="opt.key" class="checkbox-field" :title="opt.tooltip">
<input type="checkbox" v-model="editItem.options[opt.key]" :true-value="1" :false-value="0">
<span>{{ opt.label }}</span>
</label>
</div>
<div class="form-grid mt-3">
<div class="field">
<label>MPH Min Homes (Auto-Zählung)</label>
<input class="ri" type="number" v-model.number="editItem.options.mph_min_homes_tool_automatic_count" min="0">
</div>
</div>
</div>
</div>
<template #footer>
<button class="ghost-btn" @click="showEditModal = false" :disabled="isSaving">Abbrechen</button>
<button class="primary-btn" @click="saveNetzgebiet" :disabled="isSaving || !editItem?.name">
<span v-if="!isSaving">Speichern</span>
<span v-else class="btn-loader"></span>
</button>
</template>
</tt-dialog>
<!-- History Modal -->
<tt-dialog :show="showHistoryModal" :title="historyTitle" size="wide" @close="showHistoryModal = false">
<div class="history-container">
<div v-if="historyLoading" class="table-placeholder compact">
<i class="fa-duotone fa-spinner fa-spin"></i>
</div>
<div v-else-if="!filteredHistory.length" class="table-placeholder compact">
<i class="fa-duotone fa-clock-rotate-left"></i>
<span>Kein Verlauf vorhanden.</span>
</div>
<div v-else class="history-list">
<div v-for="entry in filteredHistory" :key="entry.id" class="history-entry" :class="'action-' + entry.action">
<div class="history-icon">
<i v-if="entry.action === 'update'" class="fa-duotone fa-pen-to-square"></i>
<i v-else-if="entry.action === 'create'" class="fa-duotone fa-plus-circle"></i>
<i v-else-if="entry.action === 'delete'" class="fa-duotone fa-trash-can"></i>
</div>
<div class="history-content">
<div class="history-header">
<strong>{{ translateAction(entry.action) }}</strong>
<span v-if="entry.action === 'update'" class="field-label">{{ translateField(entry.field) }}</span>
<span class="history-meta">{{ entry.user_name || 'System' }} · {{ formatTimestamp(entry.timestamp) }}</span>
</div>
<div v-if="entry.action === 'update'" class="history-diff">
<span class="diff-old" :class="{ expandable: isLongValue(entry.field, entry.old_value), expanded: expandedIds[entry.id + '_old'] }" @click="toggleExpand(entry.id + '_old')">{{ formatValue(entry.field, entry.old_value) }}</span>
<i class="fa-duotone fa-arrow-right"></i>
<span class="diff-new" :class="{ expandable: isLongValue(entry.field, entry.new_value), expanded: expandedIds[entry.id + '_new'] }" @click="toggleExpand(entry.id + '_new')">{{ formatValue(entry.field, entry.new_value) }}</span>
</div>
</div>
</div>
</div>
</div>
</tt-dialog>
<!-- RIMO Import Log Modal -->
<tt-dialog :show="showRimoLogModal" :title="rimoLogTitle" size="large" @close="closeRimoLogModal">
<div v-if="!rimoLogContent && rimoLogStatus === 'running'" class="table-placeholder compact">
<i class="fa-duotone fa-spinner fa-spin"></i>
<span>Lade Log...</span>
</div>
<div v-else-if="!rimoLogContent" class="table-placeholder compact">
<i class="fa-duotone fa-file-lines"></i>
<span>Kein Log vorhanden.</span>
</div>
<pre v-else class="log-view" style="white-space: pre-wrap; word-break: break-all; max-height: 60vh; overflow-y: auto; background: #f5f5f5; padding: 10px; border-radius: 5px;">{{ rimoLogContent }}</pre>
<template #footer>
<div class="footer-status" style="margin-right: auto; font-size: 12px; color: #666;">
Status: <strong :style="{color: rimoLogStatus === 'running' ? 'blue' : (rimoLogStatus === 'cooldown' ? 'orange' : 'green')}">{{ rimoLogStatus }}</strong>
</div>
<button class="ghost-btn" @click="closeRimoLogModal">Schließen</button>
</template>
</tt-dialog>
</div>
`,
data() {
return {
window: window,
isLoading: true,
isSaving: false,
netzgebiete: [],
currentPage: 1,
pageSize: 50,
filters: { name: '', extref: '', source: '', hasNetwork: '', hasCampaign: '', hasConsent: '' },
filterDebounce: null,
showEditModal: false,
editItem: null,
showHistoryModal: false,
historyLoading: false,
historyItems: [],
historyTitle: 'Verlauf',
expandedIds: {},
// RIMO Import
importStatus: {},
showRimoLogModal: false,
rimoLogContent: '',
rimoLogTitle: '',
rimoLogStatus: 'idle',
rimoLogInterval: null,
statusInterval: null,
freigabeLabels: { interest: 'Interest', provision: 'Provision', order: 'Order', reorder: 'Reorder' },
freigabeOptions: [
{ key: 'interest', label: 'Interest' },
{ key: 'provision', label: 'Provision' },
{ key: 'order', label: 'Order' },
{ key: 'reorder', label: 'Reorder' }
],
optionsConfig: [
{ key: 'create_address_parts', label: 'create_address_parts', tooltip: 'Neue Straßen/PLZ/Ort anlegen' },
{ key: 'update_freigabe', label: 'update_freigabe', tooltip: 'Setzt Freigabe auf Basis Netzgebiet' },
{ key: 'update_address', label: 'update_address', tooltip: 'Straßennamen ändern' },
{ key: 'hausnummer_dont_overwrite_netzgebiet', label: 'dont_overwrite_netzgebiet', tooltip: 'Netzgebiete nicht überschreiben' },
{ key: 'create_preorder', label: 'create_preorder', tooltip: 'Bestellungen erstellen (SBIDI)' },
{ key: 'preorder_only_oaid', label: 'preorder_only_oaid', tooltip: 'SBIDI OAID aus RIMO' },
{ key: 'wo_ignore_status', label: 'wo_ignore_status', tooltip: 'Status ignorieren' },
{ key: 'delete_units', label: 'delete_units', tooltip: 'Homes löschen die nicht in RIMO sind' },
{ key: 'unit_create_oaid', label: 'unit_create_oaid', tooltip: 'OAID bei Unit erstellen' }
],
defaultOptions: {
create_address_parts: 0, update_freigabe: 1, update_address: 1,
hausnummer_dont_overwrite_netzgebiet: 0, create_preorder: 0,
preorder_only_oaid: 0, wo_ignore_status: 0, delete_units: 0,
mph_min_homes_tool_automatic_count: 3, unit_create_oaid: 0
}
};
},
computed: {
availableSources() {
const sources = new Set();
this.netzgebiete.forEach(item => {
if (item.netzgebiet?.source) sources.add(item.netzgebiet.source);
});
return Array.from(sources).sort();
},
hasActiveFilters() {
return Object.values(this.filters).some(v => v);
},
filteredNetzgebiete() {
return this.netzgebiete.filter(item => {
const n = item.netzgebiet;
if (!n) return false;
if (this.filters.name && !n.name?.toLowerCase().includes(this.filters.name.toLowerCase())) return false;
if (this.filters.extref && !n.extref?.toLowerCase().includes(this.filters.extref.toLowerCase())) return false;
if (this.filters.source && n.source !== this.filters.source) return false;
const hasNetwork = item.related?.networks?.length > 0;
const hasCampaign = item.related?.campaigns?.length > 0;
const hasConsent = item.related?.consent_projects?.length > 0;
if (this.filters.hasNetwork === 'yes' && !hasNetwork) return false;
if (this.filters.hasNetwork === 'no' && hasNetwork) return false;
if (this.filters.hasCampaign === 'yes' && !hasCampaign) return false;
if (this.filters.hasCampaign === 'no' && hasCampaign) return false;
if (this.filters.hasConsent === 'yes' && !hasConsent) return false;
if (this.filters.hasConsent === 'no' && hasConsent) return false;
return true;
});
},
totalPages() { return Math.ceil(this.filteredNetzgebiete.length / this.pageSize) || 1; },
paginatedItems() {
const start = (this.currentPage - 1) * this.pageSize;
return this.filteredNetzgebiete.slice(start, start + this.pageSize);
},
paginationStart() { return this.filteredNetzgebiete.length ? (this.currentPage - 1) * this.pageSize + 1 : 0; },
paginationEnd() { return Math.min(this.currentPage * this.pageSize, this.filteredNetzgebiete.length); },
filteredHistory() {
return this.historyItems.filter(e => !['edit', 'create'].includes(e.field));
}
},
watch: {
filteredNetzgebiete() { if (this.currentPage > this.totalPages) this.currentPage = 1; }
},
async mounted() {
await this.fetchNetzgebiete();
this.fetchImportStatus();
this.statusInterval = setInterval(this.fetchImportStatus, 15000); // Poll every 15s
},
beforeDestroy() {
clearInterval(this.statusInterval);
clearInterval(this.rimoLogInterval);
},
methods: {
formatConsentName(name) {
if (name && name.startsWith('Glasfaserprojekt')) {
return name.replace('Glasfaserprojekt', 'Glasfaserprojekt<br />');
}
return name;
},
debouncedFilter() {
clearTimeout(this.filterDebounce);
this.filterDebounce = setTimeout(() => this.currentPage = 1, 300);
},
applyFilter() { this.currentPage = 1; },
clearFilters() {
this.filters = { name: '', extref: '', source: '', hasNetwork: '', hasCampaign: '', hasConsent: '' };
this.currentPage = 1;
},
async fetchNetzgebiete() {
this.isLoading = true;
try {
const response = await axios.get(window.TT_CONFIG.GET_URL);
this.netzgebiete = response.data.success ? (response.data.data || []) : (response.data || []);
} catch (error) {
console.error('Fehler:', error);
window.notify?.('error', 'Netzgebiete konnten nicht geladen werden.');
} finally {
this.isLoading = false;
}
},
parsedFreigabe(json) {
try { return JSON.parse(json || '[]') || []; }
catch { return []; }
},
openCreateModal() {
this.editItem = {
id: null, name: '', extref: '', source: '', source_id: '',
freigabe: { interest: true, provision: true, order: true, reorder: true },
options: { ...this.defaultOptions }
};
this.showEditModal = true;
},
openEditModal(item) {
const n = item.netzgebiet;
let options = {};
try { options = JSON.parse(n.options || '{}'); } catch {}
let freigabeArr = [];
try { freigabeArr = JSON.parse(n.freigabe || '[]') || []; } catch {}
const freigabeObj = {};
['interest', 'provision', 'order', 'reorder'].forEach(f => freigabeObj[f] = freigabeArr.includes(f));
this.editItem = {
id: n.id, name: n.name || '', extref: n.extref || '',
source: n.source || '', source_id: n.source_id || '',
freigabe: freigabeObj,
options: { ...this.defaultOptions, ...options }
};
this.showEditModal = true;
},
async copyNetzgebiet(item) {
const n = item.netzgebiet;
let options = {};
try { options = JSON.parse(n.options || '{}'); } catch {}
let freigabeArr = [];
try { freigabeArr = JSON.parse(n.freigabe || '[]') || []; } catch {}
const freigabeObj = {};
['interest', 'provision', 'order', 'reorder'].forEach(f => freigabeObj[f] = freigabeArr.includes(f));
this.editItem = {
id: null,
name: '',
extref: '',
source: n.source || '',
source_id: '',
freigabe: freigabeObj,
options: { ...this.defaultOptions, ...options }
};
this.showEditModal = true;
},
async saveNetzgebiet() {
if (!this.editItem?.name) return;
this.isSaving = true;
const freigabeArray = Object.keys(this.editItem.freigabe).filter(k => this.editItem.freigabe[k]);
const payload = {
id: this.editItem.id, name: this.editItem.name, extref: this.editItem.extref,
source: this.editItem.source, source_id: this.editItem.source_id,
freigabe: freigabeArray, options: this.editItem.options
};
try {
const response = await axios.post(window.TT_CONFIG.SAVE_URL, payload);
if (response.data.success) {
window.notify?.('success', response.data.message);
this.showEditModal = false;
await this.fetchNetzgebiete();
} else {
window.notify?.('error', response.data.message || 'Fehler beim Speichern.');
}
} catch { window.notify?.('error', 'Netzwerkfehler.'); }
finally { this.isSaving = false; }
},
async openHistoryModal(item) {
this.historyTitle = `Verlauf: ${item.netzgebiet.name}`;
this.showHistoryModal = true;
this.historyLoading = true;
this.historyItems = [];
try {
const response = await axios.get(window.TT_CONFIG.HISTORY_URL + '?id=' + item.netzgebiet.id);
this.historyItems = response.data.success ? (response.data.data || []) : (response.data || []);
} catch { window.notify?.('error', 'Verlauf konnte nicht geladen werden.'); }
finally { this.historyLoading = false; }
},
// RIMO Import Methods
async fetchImportStatus() {
const rimoIds = this.netzgebiete
.filter(item => item.netzgebiet.source && item.netzgebiet.source.startsWith('rimo-'))
.map(item => item.netzgebiet.id);
if (!rimoIds.length) return;
try {
const response = await axios.post(window.TT_CONFIG.GET_RIMO_IMPORT_STATUS_URL, { ids: rimoIds });
if (response.data.success) {
this.importStatus = response.data.data;
}
} catch (error) {
console.error("Could not fetch RIMO import statuses.", error);
}
},
handleRimoImportClick(item) {
const status = this.importStatus[item.netzgebiet.id]?.status || 'idle';
if (status === 'running') {
this.openRimoLogModal(item);
} else if (status === 'cooldown') {
const remaining = this.importStatus[item.netzgebiet.id]?.remaining || 0;
window.notify?.('info', `Bitte warten Sie noch ${Math.ceil(remaining / 60)} Minuten.`);
this.openRimoLogModal(item);
} else {
this.startRimoImport(item);
}
},
getImportButtonTitle(id) {
const status = this.importStatus[id]?.status || 'idle';
if (status === 'running') return 'Import-Log anzeigen';
if (status === 'cooldown') return 'Manueller RIMO-Import (Abkühlphase)';
return 'Manuellen RIMO-Import starten';
},
getImportButtonDisabled(id) {
const status = this.importStatus[id]?.status || 'idle';
return false; // Never truly disabled, just changes action
},
getImportButtonIcon(id) {
const status = this.importStatus[id]?.status || 'idle';
if (status === 'running') return 'fa-spinner fa-spin';
if (status === 'cooldown') return 'fa-hourglass-half';
return 'fa-rocket';
},
async startRimoImport(item) {
try {
const response = await axios.get(window.TT_CONFIG.START_RIMO_IMPORT_URL + '?id=' + item.netzgebiet.id);
if (response.data.success) {
window.notify?.('success', 'RIMO Import gestartet.');
this.importStatus[item.netzgebiet.id] = { status: 'running' };
this.openRimoLogModal(item);
} else {
window.notify?.('error', response.data.message || 'Import konnte nicht gestartet werden.');
}
} catch (error) {
window.notify?.('error', 'Fehler beim Starten des Imports.');
console.error(error);
}
},
openRimoLogModal(item) {
this.rimoLogTitle = `RIMO Import: ${item.netzgebiet.name}`;
this.showRimoLogModal = true;
this.fetchRimoLog(item); // initial fetch
this.rimoLogInterval = setInterval(() => this.fetchRimoLog(item), 3000);
},
closeRimoLogModal() {
this.showRimoLogModal = false;
clearInterval(this.rimoLogInterval);
this.rimoLogContent = '';
this.rimoLogTitle = '';
this.rimoLogStatus = 'idle';
},
async fetchRimoLog(item) {
try {
const response = await axios.get(window.TT_CONFIG.GET_RIMO_IMPORT_LOG_URL + '?id=' + item.netzgebiet.id);
if (response.data.success) {
this.rimoLogContent = response.data.data.log;
this.rimoLogStatus = response.data.data.status;
// If no longer running, stop polling
if (this.rimoLogStatus !== 'running') {
clearInterval(this.rimoLogInterval);
this.fetchImportStatus(); // refresh overall status
}
}
} catch (error) {
console.error('Could not fetch RIMO log.', error);
clearInterval(this.rimoLogInterval);
}
},
translateAction(action) { return { create: 'Erstellt', update: 'Geändert', delete: 'Gelöscht' }[action] || action; },
translateField(field) {
return { name: 'Name', extref: 'ExtRef', source: 'Quelle', source_id: 'Source ID',
freigabe: 'Freigaben', options: 'Optionen', unit_counts: 'Einheiten' }[field] || field;
},
formatTimestamp(ts) {
if (!ts) return '—';
try { return new Date(ts.replace(' ', 'T')).toLocaleString('de-AT'); }
catch { return ts; }
},
formatValue(field, value) {
if (value === null || value === undefined || value === '') return '—';
if (['freigabe', 'options'].includes(field)) {
try {
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
if (field === 'freigabe' && Array.isArray(parsed)) return parsed.join(', ') || '—';
if (field === 'options' && typeof parsed === 'object') {
const entries = Object.entries(parsed).filter(([,v]) => v !== 0 && v !== '0');
return entries.map(([k,v]) => `${k}: ${v}`).join(', ') || '—';
}
} catch {}
}
return String(value);
},
isLongValue(field, value) {
return this.formatValue(field, value).length > 40;
},
toggleExpand(id) {
this.expandedIds[id] = !this.expandedIds[id];
}
}
};
if (window.VueApp) {
window.VueApp.component('a-d-b-netzgebiet', ADBNetzgebiet);
}