Files
thetool/public/js/pages/Cpeprovisioning/Cpeprovisioning.js

392 lines
19 KiB
JavaScript

Vue.component('tt-chip', {
props: {
checked: { type: Boolean, default: false }
},
template: `<div class="tt-chip" :class="{ 'is-checked': checked }"><slot></slot></div>`
});
Vue.component('Cpeprovisioning', {
template: `
<div class="cpe-provisioning-page">
<tt-card>
<div class="filter-grid">
<tt-select label="Netzgebiet" :options="networkOptions" v-model="filters.network_id" @input="debouncedFetchData" sm/>
<tt-select label="Provisioningstatus" :options="statusOptions" v-model="filters.routerconfig_finished" @input="debouncedFetchData" sm/>
<tt-select label="Verzögerte Herstellung" :options="delayOptions" v-model="filters.hide_delayed_finish" @input="debouncedFetchData" sm/>
<tt-input label="Suche" v-model="filters.owner" sm placeholder="Kunde, SPIN, Adresse..." @input="debouncedFetchData"/>
<div class="filter-actions">
<tt-button text="Anwenden" @click="fetchData(true)" additional-class="btn-primary" sm/>
<tt-button text="Zurücksetzen" @click="resetFilters" additional-class="btn-secondary" sm/>
</div>
</div>
</tt-card>
<div v-if="loading && items.length === 0" class="loading-indicator">
<div class="spinner-border text-primary" role="status"><span class="sr-only">Loading...</span></div>
<p class="mt-2">Daten werden geladen...</p>
</div>
<div v-if="!loading && filteredItems.length === 0" class="no-results-indicator">
<i class="fas fa-info-circle fa-2x text-muted mb-2"></i>
<p>Keine Einträge für die aktuellen Filter gefunden.</p>
</div>
<div class="cpe-cards-container">
<div v-for="item in filteredItems" :key="item.orderproduct_id" class="cpe-card" :class="{ 'is-dirty': item.isDirty }">
<div class="cpe-card-header">
<div class="customer-info">
<span style="display: ruby;">
<strong>{{ item.customer }}<small v-if="item.owner_customer_number" class="text-muted ml-2">#{{ item.owner_customer_number }}</small></strong>
</span>
<small v-if="item.spin" class="text-muted">SPIN: <span class="text-pink">{{ item.spin }}</span></small>
</div>
<div class="location-contact-header">
<div><strong>Netzgebiet:</strong> {{ item.network || 'N/A' }}</div>
<div v-if="item.owner_phone"><i class="fas fa-phone mr-2 text-muted"></i>{{ item.owner_phone }}</div>
<div><strong>Adresse:</strong> {{ item.owner_full_address || 'N/A' }}</div>
<div v-if="item.owner_email"><i class="fas fa-envelope mr-2 text-muted"></i>{{ item.owner_email }}</div>
</div>
<div class="header-actions">
<a target="_blank" :href="window.TT_CONFIG.ORDER_URL + '/Index/?id=' + item.order_id + '&addJournal=1'">
<tt-tooltip text="Bestelljournal" position="top"><i class="fas fa-scroll"></i></tt-tooltip>
</a>
<a target="_blank" :href="window.TT_CONFIG.CPE_PROV_PRINT_PDF_URL + '?order_id=' + item.order_id">
<tt-tooltip text="Label drucken" position="top"><i class="fas fa-print"></i></tt-tooltip>
</a>
<tt-tooltip v-if="item.vot" text="Vorortinstallation" position="top"><i class="fas fa-tools text-purple"></i></tt-tooltip>
<tt-tooltip v-if="item.hw" :text="item.hw" position="top"><i class="fas fa-shopping-bag text-purple"></i></tt-tooltip>
<tt-tooltip v-if="item.voip" text="Voice Produkt vorhanden" position="top"><i class="fas fa-phone text-purple"></i></tt-tooltip>
<tt-tooltip v-if="item.note" :text="item.note" position="top" allow-wrapping><i class="fas fa-clipboard-list text-purple"></i></tt-tooltip>
<a target="_blank" :href="item.snopp_url" v-if="item.show_snopp_button">
<tt-tooltip text="SNOPP" position="top" allow-wrapping>
<img style="height: 18px;" src="/img/snop-logo.png" alt="Snop Logo">
</tt-tooltip>
</a>
</div>
</div>
<div class="cpe-card-content">
<div class="content-column">
<h5>Router Konfiguration</h5>
<tt-select label="Router" :options="routerOptions" v-model="item.cpe_data.routertype" @input="markDirty(item); checkShipping(item)" sm no-form-group/>
<tt-input label="WLAN SSID" v-model="item.cpe_data.wifi_ssid" @input="markDirty(item)" sm no-form-group/>
<tt-input label="WPA Key" v-model="item.cpe_data.wifi_pass" @input="markDirty(item)" sm no-form-group/>
<tt-input label="Router MAC" v-model="item.cpe_data.mac" @input="markDirty(item)" sm no-form-group/>
<tt-input v-if="item.termination_id" label="ONT SN" v-model="item.ont_sn" @input="markDirty(item)" sm no-form-group :additional-props="{ placeholder: item.ont_deployed ? 'ONT montiert' : 'ONT nicht montiert' }"/>
</div>
<div class="content-column">
<h5>Versand & Abschluss</h5>
<tt-checkbox label="Versandauftrag" v-model="item.cpe_data.shipping" @input="markDirty(item);checkShipping(item)" sm/>
<div class="shipping-dims">
<tt-input label="Gewicht" v-model="item.cpe_data.ship_weight" @input="markDirty(item)" sm type="number" :disabled="!item.cpe_data.shipping" placeholder="kg"/>
<tt-input label="Länge" v-model="item.cpe_data.ship_length" @input="markDirty(item)" sm type="number" :disabled="!item.cpe_data.shipping" placeholder="cm"/>
<tt-input label="Breite" v-model="item.cpe_data.ship_width" @input="markDirty(item)" sm type="number" :disabled="!item.cpe_data.shipping" placeholder="cm"/>
<tt-input label="Höhe" v-model="item.cpe_data.ship_height" @input="markDirty(item)" sm type="number" :disabled="!item.cpe_data.shipping" placeholder="cm"/>
</div>
<tt-textarea label="Kommentar" v-model="item.cpe_data.note" @input="markDirty(item)" sm no-form-group/>
</div>
<div class="content-column">
<h5>Produkt & VLANs</h5>
<p><strong>{{ item.product_name }}</strong> <small class="text-muted">{{ item.product_code }}</small></p>
<p>
<span class="badge badge-info">{{ item.access_type }}</span>
<span class="ml-2"><i class="fas fa-arrow-down"></i> {{ item.access_type_down }}</span>
<span class="ml-2"><i class="fas fa-arrow-up"></i> {{ item.access_type_up }}</span>
</p>
<div class="vlans-container mt-2">
<template v-for="(vlan, key) in item.vlans">
<tt-chip v-if="vlan.tag" class="vlan-chip" :key="key">
<input type="checkbox" :checked="vlan.checked" @change="markDirty(item); vlan.checked = !vlan.checked"/>
<span>{{ key.charAt(0).toUpperCase() + key.slice(1) }}: {{ vlan.tag }}</span>
</tt-chip>
</template>
</div>
</div>
<div class="content-column action-column">
<h5>Aktionen</h5>
<div class="action-buttons">
<tt-button text="In Radius anlegen"
@click="createRadiusUser(item)"
:disabled="!isValidMac(item.cpe_data.mac)"
:loading="item.isCreatingRadius"
sm
additional-class="btn-primary" />
<tt-button text="ACS Auto VLAN Zuweisung testen"
@click="testAcsVlan(item)"
:disabled="!isVlanSelected(item) || !isValidMac(item.cpe_data.mac)"
sm
additional-class="btn-info mt-2" />
</div>
<div class="finish-wrapper mt-auto">
<label class="col-form-label col-form-label-sm">Konfig abgeschlossen</label>
<tt-switch v-model="item.cpe_data.routerconfig_finished" @input="markDirty(item)"/>
</div>
<tt-button text="Speichern" @click="saveCpe(item)" :loading="item.isSaving" :disabled="!item.isDirty" :additional-class="item.isDirty ? 'btn-success' : 'btn-secondary'"/>
</div>
</div>
</div>
</div>
<div v-if="loading && items.length > 0" class="text-center mt-4 mb-4">
<div class="card d-inline-block p-3 shadow-sm">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
<span class="ml-2">Lade weitere Einträge...</span>
</div>
</div>
<div class="text-center mt-4 mb-4" v-if="!loading && pagination.total_pages && page <= pagination.total_pages">
<tt-button text="Weitere laden" @click="fetchData(false)" additional-class="btn-primary" />
</div>
<tt-modal :show="showExtensionIdModal" title="Extension ID Konfigurieren" @close="showExtensionIdModal=false">
<div>
<div class="field"><label style="margin-bottom: 8px; font-size: 14px;">Chrome Extension ID</label>
<div class="input-wrap"><i class="fa-duotone fa-puzzle-piece input-icon"></i><input class="ri" type="text"
v-model.trim="extensionId"
placeholder="z.B. jglijfiddilckddlmbnlojmmlahboffh">
</div>
</div>
<div class="cluster" style="justify-content: flex-end; margin-top: 24px; gap: 12px;">
<button class="primary-btn" @click="saveExtensionId">Speichern</button>
</div>
</div>
</tt-modal>
</div>
`,
data() {
return {
window,
loading: true,
items: [],
filteredItems: [],
filters: {
network_id: '',
routerconfig_finished: '0',
hide_delayed_finish: '1',
owner: ''
},
statusOptions: [ { value: '0', text: 'Offen' }, { value: '1', text: 'Abgeschlossen' } ],
delayOptions: [ { value: '1', text: 'Nicht anzeigen' }, { value: '0', text: 'Anzeigen' } ],
page: 1,
pagination: {},
debouncedFetchData: null,
extensionId: 'jglijfiddilckddlmbnlojmmlahboffh',
showExtensionIdModal: false
}
},
computed: {
networkOptions() {
const networks = window.TT_CONFIG.NETWORKS || [];
return [{ value: '', text: 'Alle Gebiete' }, ...networks.map(net => ({ value: net.id, text: net.name }))];
},
routerOptions() {
return window.TT_CONFIG.ROUTER_OPTIONS || [];
}
},
created() {
this.debouncedFetchData = _.debounce(this.fetchData.bind(this, true), 400);
const savedExtensionId = localStorage.getItem('radiusExtensionId');
if (savedExtensionId) {
this.extensionId = savedExtensionId;
}
window.addEventListener('keydown', this.handleKeydown);
},
beforeDestroy() {
window.removeEventListener('keydown', this.handleKeydown);
},
methods: {
handleKeydown(e) {
if (e.code === 'KeyE' && e.ctrlKey && e.altKey) {
e.preventDefault();
this.openExtensionIdModal();
}
},
openExtensionIdModal() {
this.showExtensionIdModal = true;
},
saveExtensionId() {
localStorage.setItem('radiusExtensionId', this.extensionId);
this.showExtensionIdModal = false;
window.notify('success', 'Extension ID gespeichert.');
},
isValidMac(mac) {
if (!mac) return false;
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
return macRegex.test(mac);
},
async fetchData(isNewSearch = false) {
if (isNewSearch) {
this.page = 1;
this.items = [];
this.filteredItems = [];
this.pagination = {};
}
if (!isNewSearch && this.pagination.total_pages && this.page > this.pagination.total_pages) {
return;
}
this.loading = true;
const payload = {
pagination: { page: this.page, per_page: 25 },
filters: { ...this.filters },
order: { key: 'order_id', order: 'desc' }
};
try {
const { data } = await axios.post(window.TT_CONFIG.CPE_PROV_API_GET_URL, payload);
const newItems = (data.rows || []).map(item => ({
...item,
isDirty: false,
isSaving: false,
isCreatingRadius: false,
pop_name: item.pop_name || 'N/A',
owner_address: `${item.owner_street || ''} ${item.owner_housenumber || ''}, ${item.owner_zip || ''} ${item.owner_city || ''}`,
owner_phone: item.owner_phone || '',
owner_email: item.owner_email || '',
}));
if (isNewSearch) {
this.items = newItems;
} else {
this.items.push(...newItems);
}
this.pagination = data.pagination;
this.page++;
this.filteredItems = this.items;
} catch (error) {
console.error("Error fetching CPE data:", error);
window.notify('error', 'Fehler beim Laden der Daten.');
} finally {
this.loading = false;
}
},
resetFilters() {
this.filters = { network_id: '', routerconfig_finished: '0', hide_delayed_finish: '1', owner: '' };
this.fetchData(true);
},
markDirty(item) {
this.$set(item, 'isDirty', true);
},
checkShipping(item) {
if (item.cpe_data.shipping && item.cpe_data.routertype) {
const shippingData = this.window.TT_CONFIG.ROUTER_SHIPPING_DATA[item.cpe_data.routertype];
if (shippingData) {
item.cpe_data.ship_weight = shippingData.weight;
item.cpe_data.ship_length = shippingData.length;
item.cpe_data.ship_width = shippingData.width;
item.cpe_data.ship_height = shippingData.height;
item.cpe_data = { ...item.cpe_data }; // Trigger reactivity
this.window.notify('success', 'Versanddaten wurden automatisch ausgefüllt.');
}
} else if (!item.cpe_data.shipping) {
item.cpe_data.ship_weight = '';
item.cpe_data.ship_length = '';
item.cpe_data.ship_width = '';
item.cpe_data.ship_height = '';
item.cpe_data = { ...item.cpe_data }; // Trigger reactivity
}
},
isVlanSelected(item) {
return item.vlans && Object.values(item.vlans).some(v => v.checked);
},
async createRadiusUser(item) {
// Disable button during request
this.$set(item, 'isCreatingRadius', true);
try {
const { data } = await axios.post(window.TT_CONFIG.CPE_PROV_API_CREATE_RADIUS_USER_URL, {
mac: item.cpe_data.mac
});
if (data.success) {
window.notify('success', `RADIUS User erfolgreich angelegt! Kundennr: ${data.data.customer_number}`);
console.log('RADIUS User created:', data.data);
} else {
window.notify('error', data.message || 'Fehler beim Anlegen des RADIUS Users.');
}
} catch (error) {
const errorMsg = error.response?.data?.message || 'Ein unerwarteter Fehler ist aufgetreten.';
window.notify('error', errorMsg);
console.error('Error creating RADIUS user:', error);
} finally {
this.$set(item, 'isCreatingRadius', false);
}
},
async testAcsVlan(item) {
const button = this.$el.querySelector(`[data-orderproduct-id="${item.orderproduct_id}"] .btn-info`);
if (button) {
button.disabled = true;
}
try {
const { data } = await axios.post(window.TT_CONFIG.CPE_PROV_API_TEST_ACS_VLAN_URL, {
mac: item.cpe_data.mac
});
if (data.success) {
window.notify('success', `ACS VLAN Zuweisung erfolgreich: VLAN ${data.vlan_id}`);
} else {
window.notify('error', data.message || 'Fehler bei der ACS VLAN Zuweisung.');
}
} catch (error) {
window.notify('error', 'Ein unerwarteter Fehler ist aufgetreten.');
} finally {
if (button) {
button.disabled = false;
}
}
},
_buildSavePayload(item) {
return {
id: item.cpe_id,
order_id: item.order_id,
orderproduct_id: item.orderproduct_id,
termination_id: item.termination_id,
ont_sn: item.ont_sn,
vlans: item.vlans,
...item.cpe_data,
shipping: item.cpe_data.shipping ? 1 : 0,
routerconfig_finished: item.cpe_data.routerconfig_finished ? 1 : 0,
};
},
async saveCpe(item) {
this.$set(item, 'isSaving', true);
const payload = this._buildSavePayload(item);
try {
const { data } = await axios.post(this.window.TT_CONFIG.CPE_PROV_API_SAVE_URL, payload);
if (data.success) {
this.window.notify('success', data.message);
if (this.filters.routerconfig_finished === '0' && payload.routerconfig_finished) {
this.items = this.items.filter(i => i.orderproduct_id !== item.orderproduct_id);
this.filteredItems = this.items;
} else {
const index = this.items.findIndex(i => i.orderproduct_id === item.orderproduct_id);
if (index !== -1) {
this.$set(this.items[index], 'isDirty', false);
}
}
} else {
this.window.notify('error', data.message || 'Fehler beim Speichern.');
}
} catch (error) {
this.window.notify('error', 'Ein unerwarteter Fehler ist aufgetreten.');
} finally {
this.$set(item, 'isSaving', false);
}
}
},
mounted() {
this.fetchData(true);
window.addEventListener('keydown', this.handleKeydown);
}
});