added v2 of cpeprov

This commit is contained in:
Luca Haid
2025-07-21 22:27:40 +02:00
parent 14757f4de0
commit f7218ab144
13 changed files with 786 additions and 57 deletions
@@ -0,0 +1,159 @@
/* Cpeprovisioning.css */
.cpe-provisioning-page .filter-card {
margin-bottom: 1rem;
}
.cpe-provisioning-page .filter-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
align-items: end;
}
.cpe-provisioning-page .filter-actions {
display: flex;
gap: 0.5rem;
padding-top: 1.5rem; /* Align with form labels */
}
.cpe-provisioning-page .cpe-details-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
padding: 1rem;
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
}
.cpe-provisioning-page .section-title {
grid-column: 1 / -1;
font-size: 1.1rem;
font-weight: 600;
color: #005384;
margin-top: 0.5rem;
margin-bottom: 0;
padding-bottom: 0.5rem;
border-bottom: 2px solid #f7c423;
}
.cpe-provisioning-page .info-pills {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.5rem 0;
}
.cpe-provisioning-page .info-pill {
background-color: #e9ecef;
color: #495057;
padding: 0.25rem 0.6rem;
border-radius: 1rem;
font-size: 0.8rem;
display: inline-flex;
align-items: center;
gap: 0.3rem;
white-space: nowrap;
}
.cpe-provisioning-page .info-pill i {
color: #005384;
}
/* Ensure form groups don't have excessive bottom margin in the grid */
.cpe-provisioning-page .cpe-details-grid .form-group {
margin-bottom: 0;
}
/* Save button alignment */
.cpe-provisioning-page .save-button-container {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
margin-top: 1rem;
}
/* For tt-table expanded row content */
.tt-table tbody tr[style*="display: table-row;"] > td {
background-color: #f8f9fa !important;
padding: 0;
}
/* Change tracking */
.cpe-provisioning-page .is-dirty {
background-color: #fff3cd; /* A light yellow to indicate changes */
border-color: #ffeeba;
}
.cpe-provisioning-page .form-group-condensed .col-form-label {
padding-bottom: 0;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.cpe-provisioning-page .filter-grid,
.cpe-provisioning-page .cpe-details-grid {
grid-template-columns: 1fr; /* Stack on smaller screens */
}
.cpe-provisioning-page .filter-actions {
padding-top: 0;
flex-direction: column;
}
.cpe-provisioning-page .filter-actions .btn {
width: 100%;
}
}
.vlans-container {
display: flex;
flex-wrap: wrap;
/*center items in the middle of the full width*/
justify-content: center;
/*flex-direction: column;*/
gap: 0.5rem;
}
/* TODO: MOVE TT-CHIP TO OWN FILE */
.tt-chip {
display: inline-flex;
align-items: center;
padding: 5px 12px;
border-radius: 16px; /* Pill shape */
background-color: #f1f3f5; /* Light grey for unchecked state */
border: 1px solid #dee2e6;
font-size: 0.875em; /* 14px if base is 16px */
margin: 3px;
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
cursor: default;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.tt-chip.is-checked {
background-color: #e7f5ff; /* A pleasant light blue for the checked state */
border-color: #a5d8ff;
color: #1c7ed6;
font-weight: 500;
}
/* Add a subtle shadow on hover for interactivity */
.tt-chip:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Style elements passed into the slot for consistent spacing and alignment */
.tt-chip > * + * {
margin-left: 8px;
}
.tt-chip input[type="checkbox"] {
margin: 0;
width: 15px;
height: 15px;
cursor: pointer;
}
@@ -0,0 +1,217 @@
// Cpeprovisioning.js
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-card">
<div class="filter-grid">
<tt-select label="Netzgebiet" :options="networkOptions" v-model="filters.network_id" sm/>
<tt-select label="Provisioningstatus" :options="statusOptions" v-model="filters.routerconfig_finished" sm/>
<tt-select label="Verzögerte Herstellung" :options="delayOptions" v-model="filters.hide_delayed_finish" sm/>
<tt-input label="Kunde" v-model="filters.owner" sm placeholder="Name, SPIN, ..."/>
<div class="filter-actions">
<tt-button text="Anwenden" @click="applyFilters" additional-class="btn-primary" sm/>
<tt-button text="Zurücksetzen" @click="resetFilters" additional-class="btn-secondary" sm/>
</div>
</div>
</div>
</tt-card>
<tt-card :no-body-padding-top="true">
<tt-table
:fetch-url="window.TT_CONFIG.CPE_PROV_API_GET_URL"
:config="tableConfig"
ref="cpeTable"
key="cpeProvisioningTable"
ssr>
<template v-slot:customer="{ row }">
<div><strong>{{ row.customer }}</strong></div>
<div><small class="text-muted">SPIN: {{ row.spin }}</small></div>
<div><small>{{ row.network }}</small></div>
</template>
<template v-slot:product="{ row }">
<div><strong>{{ row.product_name }}</strong></div>
<div><small class="text-muted">{{ row.product_code }}</small></div>
<div>
<small>{{ row.access_type }}</small>
<small><i class="fas fa-arrow-down"></i> {{ row.access_type_down }} Mbit/s</small>
<small><i class="fas fa-arrow-up"></i> {{ row.access_type_up }} Mbit/s</small>
</div>
<div class="mt-1 d-flex align-items-center">
<a target="_blank" :href="window.TT_CONFIG.ORDER_URL + '/Index/?id=' + row.order_id + '&addJournal=1'" class="mr-2">
<i class="fas fa-scroll"></i>
</a>
<template v-if="row.vot || row.hw || row.voip || row.note">
<tt-tooltip v-if="row.vot" text="Vorortinstallation" position="top">
<i class="fas fa-tools text-purple mr-2"></i>
</tt-tooltip>
<tt-tooltip v-if="row.hw" :text="row.hw" position="top" class="mr-2">
<i class="fas fa-shopping-bag text-purple"></i>
</tt-tooltip>
<tt-tooltip v-if="row.voip" text="Voice Produkt vorhanden" position="top" class="mr-2">
<i class="fas fa-phone text-purple"></i>
</tt-tooltip>
<tt-tooltip v-if="row.note" :text="row.note" position="top" allow-wrapping class="mr-2">
<i class="fas fa-clipboard-list text-purple"></i>
</tt-tooltip>
<a target="_blank" :href="row.snopp_url" class="mr-2">
<tt-tooltip v-if="row.show_snopp_button" text="SNOPP" position="top" allow-wrapping>
<img style="height: 18px; vertical-align: middle;" class="logo-top-search" src="/img/snop-logo.png" alt="Snop Logo">
</tt-tooltip>
</a>
</template>
</div>
</template>
<template v-slot:vlans="{ row }">
<div class="vlans-container">
<template v-for="(vlan, key) in row.vlans">
<tt-chip v-if="vlan.tag" class="vlan-chip" :key="key">
<input type="checkbox" :checked="vlan.checked" @change="markDirty(row, 1); vlan.checked = !vlan.checked"/>
<span>{{ key.charAt(0).toUpperCase() + key.slice(1) }}: {{ vlan.tag }}</span>
</tt-chip>
</template>
</div>
</template>
<template v-slot:expandedRow="{ row }">
<div class="cpe-details-grid">
<h5 class="section-title">Router Konfiguration</h5>
<tt-select label="Router" :options="routerOptions" v-model="row.cpe_data.routertype" @input="markDirty(row, 2)" sm/>
<tt-input label="WLAN SSID" v-model="row.cpe_data.wifi_ssid" @input="markDirty(row, 3)" sm/>
<tt-input label="WPA Key" v-model="row.cpe_data.wifi_pass" @input="markDirty(row, 4)" sm/>
<tt-input label="Router MAC" v-model="row.cpe_data.mac" @input="markDirty(row, 5)" sm/>
<tt-input label="ONT SN" v-model="row.ont_sn" @input="markDirty(row, 6)" sm/>
<h5 class="section-title">Versand</h5>
<tt-checkbox label="Versandauftrag" v-model="row.cpe_data.shipping" @input="markDirty(row, 7)" sm/>
<tt-input label="Gewicht (kg)" v-model="row.cpe_data.ship_weight" @input="markDirty(row, 8)" sm type="number" :disabled="!row.cpe_data.shipping"/>
<tt-input label="Länge (cm)" v-model="row.cpe_data.ship_length" @input="markDirty(row, 9)" sm type="number" :disabled="!row.cpe_data.shipping"/>
<tt-input label="Breite (cm)" v-model="row.cpe_data.ship_width" @input="markDirty(row, 10)" sm type="number" :disabled="!row.cpe_data.shipping"/>
<tt-input label="Höhe (cm)" v-model="row.cpe_data.ship_height" @input="markDirty(row, 11)" sm type="number" :disabled="!row.cpe_data.shipping"/>
<h5 class="section-title">Abschluss</h5>
<div style="grid-column: 1 / -1;">
<tt-textarea label="Kommentar" v-model="row.cpe_data.note" @input="markDirty(row, 12)" sm/>
</div>
<tt-checkbox label="Konfig abgeschlossen" v-model="row.cpe_data.routerconfig_finished" @input="markDirty(row, 13)" sm/>
<div class="save-button-container">
<tt-button
text="Speichern"
@click="saveCpe(row)"
:loading="row.isSaving"
:disabled="!row.isDirty"
:additional-class="row.isDirty ? 'btn-success' : 'btn-secondary'"/>
</div>
</div>
</template>
</tt-table>
</tt-card>
</div>
`,
data() {
return {
window,
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' }
],
tableConfig: {
key: 'cpeProvisioning',
tableHeader: 'CPE Provisioning',
expandCondition: () => true,
customRowClass: row => (row.isDirty ? 'is-dirty' : ''),
headers: [
{ key: 'customer', text: 'Kunde', sortable: false, filter: false, priority: 100 },
{ key: 'product', text: 'Produkt', sortable: false, filter: false, priority: 90 },
{ key: 'vlans', text: 'VLANs', sortable: false, filter: false, priority: 80 },
]
}
}
},
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 || [];
}
},
methods: {
applyFilters(fetch = true) {
const table = this.$refs.cpeTable;
if (table) {
table.filters = JSON.parse(JSON.stringify(this.filters));
if (fetch) table.fetchData(1);
}
},
resetFilters() {
this.filters = {
network_id: '',
routerconfig_finished: '0',
hide_delayed_finish: '1',
owner: ''
};
this.applyFilters();
},
markDirty(row, field) {
console.log(`Marking row as dirty for field ${field}`);
this.$set(row, 'isDirty', true);
},
async saveCpe(row) {
this.$set(row, 'isSaving', true);
const payload = {
id: row.cpe_id,
order_id: row.order_id,
orderproduct_id: row.orderproduct_id,
termination_id: row.termination_id,
ont_sn: row.ont_sn,
vlans: row.vlans,
...row.cpe_data,
shipping: row.cpe_data.shipping ? 1 : 0,
routerconfig_finished: row.cpe_data.routerconfig_finished ? 1 : 0,
};
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);
this.$set(row, 'isDirty', false);
this.$refs.cpeTable.refreshTable();
} else {
this.window.notify('error', data.message || 'Fehler beim Speichern.');
}
} catch (error) {
this.window.notify('error', 'Ein unerwarteter Fehler ist aufgetreten.');
} finally {
this.$set(row, 'isSaving', false);
}
}
},
mounted() {
this.applyFilters(false);
}
});