improved cpe provisioning
This commit is contained in:
@@ -1,20 +1,33 @@
|
|||||||
/* Cpeprovisioning.css */
|
/* Cpeprovisioning.css */
|
||||||
body {
|
body {
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Page & Filter Layout --- */
|
/* --- Page & Filter Layout --- */
|
||||||
|
.cpe-provisioning-page {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpe-provisioning-page .filter-wrapper {
|
||||||
|
background: #fff;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.cpe-provisioning-page .filter-grid {
|
.cpe-provisioning-page .filter-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cpe-provisioning-page .filter-actions {
|
.cpe-provisioning-page .filter-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-indicator, .no-results-indicator {
|
.loading-indicator, .no-results-indicator {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4rem 2rem;
|
padding: 4rem 2rem;
|
||||||
@@ -23,138 +36,264 @@ body {
|
|||||||
|
|
||||||
/* --- Cards Container --- */
|
/* --- Cards Container --- */
|
||||||
.cpe-cards-container {
|
.cpe-cards-container {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Single Card Styling --- */
|
/* --- Single Card Styling --- */
|
||||||
.cpe-card {
|
.cpe-card {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid #dee2e6;
|
border: 1px solid #dee2e6;
|
||||||
border-left: 5px solid transparent;
|
border-left: 4px solid #adb5bd;
|
||||||
border-radius: 0.5rem;
|
border-radius: 4px;
|
||||||
transition: box-shadow 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
.cpe-card.is-dirty {
|
.cpe-card.is-dirty {
|
||||||
border-left-color: #f7c423; /* Yellow accent for dirty */
|
border-left-color: #fcc419; /* Yellow accent for dirty */
|
||||||
}
|
}
|
||||||
.cpe-card:hover {
|
.cpe-card:hover {
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Card Header --- */
|
/* --- Card Header --- */
|
||||||
.cpe-card-header {
|
.cpe-card-header {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: minmax(200px, 1.5fr) 2fr auto;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
padding: 0.5rem 1rem;
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
border-bottom: 1px solid #e9ecef;
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.cpe-card-header .customer-info strong {
|
|
||||||
color: #005384;
|
.customer-info {
|
||||||
font-size: 1.1rem;
|
flex: 1;
|
||||||
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
.cpe-card-header .customer-info small {
|
.customer-info .name {
|
||||||
display: block;
|
color: #1864ab;
|
||||||
font-size: 0.8rem;
|
font-weight: 600;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
.location-contact-header {
|
.customer-info .meta {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 0.1rem 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.location-info {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem 1.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
.location-info div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.location-info i {
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1rem;
|
||||||
color: #005384;
|
|
||||||
}
|
}
|
||||||
.header-actions a { color: inherit; transition: color 0.2s; }
|
.header-actions a, .header-actions span {
|
||||||
.header-actions a:hover { color: #f7c423; }
|
color: #868e96;
|
||||||
|
transition: color 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.header-actions a:hover, .header-actions span:hover { color: #228be6; }
|
||||||
|
.header-actions .text-purple { color: #be4bdb; }
|
||||||
|
|
||||||
/* --- Card Content Grid --- */
|
/* --- Card Content Grid --- */
|
||||||
.cpe-card-content {
|
.cpe-card-content {
|
||||||
padding: 0.75rem 1rem;
|
padding: 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(280px, 1fr)); /* Changed to 4 columns */
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 1rem 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-column {
|
.content-column {
|
||||||
padding-top: 0.5rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
}
|
min-width: 0; /* Prevent overflow */
|
||||||
.content-column.action-column {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-column h5 {
|
.content-column h5 {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #005384;
|
color: #868e96;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
|
border-bottom: 1px solid #f1f3f5;
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
border-bottom: 1px solid #e9ecef;
|
|
||||||
}
|
}
|
||||||
.content-column p {
|
|
||||||
font-size: 0.9rem;
|
.content-column .form-group {
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.5rem;
|
||||||
line-height: 1.4;
|
|
||||||
}
|
}
|
||||||
.content-column .form-group, .content-column .tt-select-modern {
|
.content-column label {
|
||||||
margin-bottom: 0;
|
font-size: 0.75rem;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.action-column .btn {
|
.content-column .form-control, .content-column .custom-select {
|
||||||
width: 100%;
|
font-size: 0.85rem;
|
||||||
margin-top: 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Specific Column Tweaks */
|
||||||
.shipping-dims {
|
.shipping-dims {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 0.5rem 0.75rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
.finish-wrapper {
|
|
||||||
|
.product-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.product-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #343a40;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.product-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vlans-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.vlan-chip {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: #e7f5ff;
|
||||||
|
color: #1971c2;
|
||||||
|
border: 1px solid #d0ebff;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: all 0.1s;
|
||||||
|
}
|
||||||
|
.vlan-chip:hover {
|
||||||
|
background: #d0ebff;
|
||||||
|
}
|
||||||
|
.vlan-chip input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Column */
|
||||||
|
.action-column {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.finish-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.25rem;
|
background: #f8f9fa;
|
||||||
background-color: #f1f3f5;
|
padding: 0.5rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 4px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
.mt-auto {
|
.finish-toggle label { margin: 0; }
|
||||||
margin-top: auto !important;
|
|
||||||
|
/* --- Custom Modal --- */
|
||||||
|
.custom-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
z-index: 1050;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.custom-modal-container {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: modalSlideIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes modalSlideIn {
|
||||||
|
from { opacity: 0; transform: translateY(-20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.custom-modal-header {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.custom-modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
.custom-modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: #adb5bd;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.custom-modal-close:hover { color: #495057; }
|
||||||
|
.custom-modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
.custom-modal-footer {
|
||||||
|
padding: 1rem;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
text-align: right;
|
||||||
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- VLAN Chip Styling --- */
|
/* Responsive */
|
||||||
.vlans-container { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
||||||
.tt-chip { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 16px; background-color: #f1f3f5; border: 1px solid #dee2e6; font-size: 0.8em; white-space: nowrap; }
|
|
||||||
.tt-chip.is-checked { background-color: #e7f5ff; border-color: #a5d8ff; color: #1c7ed6; font-weight: 500; }
|
|
||||||
.tt-chip > * + * { margin-left: 6px; }
|
|
||||||
.tt-chip input[type="checkbox"] { margin: 0; cursor: pointer; }
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 1400px) {
|
@media (max-width: 1400px) {
|
||||||
.cpe-card-content {
|
.cpe-card-content {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 768px) {
|
||||||
.cpe-card-header {
|
.cpe-card-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
}
|
||||||
}
|
.cpe-card-header {
|
||||||
|
flex-direction: column;
|
||||||
.tt-switch {
|
align-items: flex-start;
|
||||||
margin-bottom: 0 !important;
|
}
|
||||||
}
|
}
|
||||||
@@ -8,18 +8,18 @@ Vue.component('tt-chip', {
|
|||||||
Vue.component('Cpeprovisioning', {
|
Vue.component('Cpeprovisioning', {
|
||||||
template: `
|
template: `
|
||||||
<div class="cpe-provisioning-page">
|
<div class="cpe-provisioning-page">
|
||||||
<tt-card>
|
<div class="filter-wrapper">
|
||||||
<div class="filter-grid">
|
<div class="filter-grid">
|
||||||
<tt-select label="Netzgebiet" :options="networkOptions" v-model="filters.network_id" @input="debouncedFetchData" sm/>
|
<tt-select label="Netzgebiet" :options="networkOptions" v-model="filters.network_id" @input="fetchData(true)" sm/>
|
||||||
<tt-select label="Provisioningstatus" :options="statusOptions" v-model="filters.routerconfig_finished" @input="debouncedFetchData" sm/>
|
<tt-select label="Provisioningstatus" :options="statusOptions" v-model="filters.routerconfig_finished" @input="fetchData(true)" sm/>
|
||||||
<tt-select label="Verzögerte Herstellung" :options="delayOptions" v-model="filters.hide_delayed_finish" @input="debouncedFetchData" sm/>
|
<tt-select label="Verzögerte Herstellung" :options="delayOptions" v-model="filters.hide_delayed_finish" @input="fetchData(true)" sm/>
|
||||||
<tt-input label="Suche" v-model="filters.owner" sm placeholder="Kunde, SPIN, Adresse..." @input="debouncedFetchData"/>
|
<tt-input label="Suche" v-model="filters.owner" sm placeholder="Kunde, SPIN, Adresse..." @input="handleSearchInput" @keyup.native.enter="fetchData(true)"/>
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
<tt-button text="Anwenden" @click="fetchData(true)" additional-class="btn-primary" sm/>
|
<tt-button text="Anwenden" @click="fetchData(true)" additional-class="btn-primary" sm/>
|
||||||
<tt-button text="Zurücksetzen" @click="resetFilters" additional-class="btn-secondary" sm/>
|
<tt-button text="Zurücksetzen" @click="resetFilters" additional-class="btn-secondary" sm/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</tt-card>
|
|
||||||
|
|
||||||
<div v-if="loading && items.length === 0" class="loading-indicator">
|
<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>
|
<div class="spinner-border text-primary" role="status"><span class="sr-only">Loading...</span></div>
|
||||||
@@ -35,17 +35,20 @@ Vue.component('Cpeprovisioning', {
|
|||||||
<div v-for="item in filteredItems" :key="item.orderproduct_id" class="cpe-card" :class="{ 'is-dirty': item.isDirty }">
|
<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="cpe-card-header">
|
||||||
<div class="customer-info">
|
<div class="customer-info">
|
||||||
<span style="display: ruby;">
|
<div>
|
||||||
<strong>{{ item.customer }}<small v-if="item.owner_customer_number" class="text-muted ml-2">#{{ item.owner_customer_number }}</small></strong>
|
<span class="name">{{ item.customer }}</span>
|
||||||
</span>
|
<span v-if="item.owner_customer_number" class="badge badge-light text-muted">#{{ item.owner_customer_number }}</span>
|
||||||
<small v-if="item.spin" class="text-muted">SPIN: <span class="text-pink">{{ item.spin }}</span></small>
|
</div>
|
||||||
|
<small v-if="item.spin" class="meta">SPIN: <span class="text-primary">{{ item.spin }}</span></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="location-contact-header">
|
|
||||||
<div><strong>Netzgebiet:</strong> {{ item.network || 'N/A' }}</div>
|
<div class="location-info">
|
||||||
<div v-if="item.owner_phone"><i class="fas fa-phone mr-2 text-muted"></i>{{ item.owner_phone }}</div>
|
<div><i class="fas fa-network-wired"></i> {{ item.network || 'N/A' }}</div>
|
||||||
<div><strong>Adresse:</strong> {{ item.owner_full_address || 'N/A' }}</div>
|
<div><i class="fas fa-map-marker-alt"></i> {{ 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 v-if="item.owner_phone"><i class="fas fa-phone"></i> {{ item.owner_phone }}</div>
|
||||||
|
<div v-if="item.owner_email"><i class="fas fa-envelope"></i> {{ item.owner_email }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<a target="_blank" :href="window.TT_CONFIG.ORDER_URL + '/Index/?id=' + item.order_id + '&addJournal=1'">
|
<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>
|
<tt-tooltip text="Bestelljournal" position="top"><i class="fas fa-scroll"></i></tt-tooltip>
|
||||||
@@ -66,93 +69,131 @@ Vue.component('Cpeprovisioning', {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cpe-card-content">
|
<div class="cpe-card-content">
|
||||||
|
<!-- Column 1: Configuration -->
|
||||||
<div class="content-column">
|
<div class="content-column">
|
||||||
<h5>Router Konfiguration</h5>
|
<h5>Router Konfiguration</h5>
|
||||||
<tt-select label="Router" :options="routerOptions" v-model="item.cpe_data.routertype" @input="markDirty(item); checkShipping(item); handleRouterTypeChange(item)" sm no-form-group/>
|
<tt-select label="Router Modell" :options="routerOptions" v-model="item.cpe_data.routertype"
|
||||||
<tt-input label="WLAN SSID" v-model="item.cpe_data.wifi_ssid" @input="markDirty(item)" sm no-form-group/>
|
@input="markDirty(item); checkShipping(item); handleRouterTypeChange(item)" sm no-form-group/>
|
||||||
<tt-input label="WPA Key" v-model="item.cpe_data.wifi_pass" @input="markDirty(item)" sm no-form-group/>
|
|
||||||
<div class="d-flex align-items-center" style="gap: 5px;">
|
<div class="d-flex align-items-end" style="gap: 5px;">
|
||||||
<tt-input label="Router MAC" v-model="item.cpe_data.mac" @input="val => handleMacInput(item, val)" sm no-form-group style="flex-grow: 1;"/>
|
<tt-input label="Router MAC (Scan)" v-model="item.cpe_data.mac"
|
||||||
<button @click="copyToClipboard(item.cpe_data.mac)" :disabled="!item.cpe_data.mac" class="btn btn-sm btn-primary" style="height: 28px; margin-top:28px; width: 38px; padding: 0;">
|
@input="val => handleMacInput(item, val)" sm no-form-group style="flex-grow: 1;"/>
|
||||||
|
<button @click="copyToClipboard(item.cpe_data.mac)" :disabled="!item.cpe_data.mac"
|
||||||
|
class="btn btn-sm btn-light border" style="height: 31px;" title="MAC kopieren">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<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' }"/>
|
|
||||||
|
<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 v-if="item.termination_id" label="ONT Seriennummer" 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>
|
||||||
|
|
||||||
|
<!-- Column 2: Shipping -->
|
||||||
<div class="content-column">
|
<div class="content-column">
|
||||||
<h5>Versand & Abschluss</h5>
|
<h5>Versand & Logistik</h5>
|
||||||
<tt-checkbox label="Versandauftrag" v-model="item.cpe_data.shipping" @input="markDirty(item);checkShipping(item)" sm/>
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
<div class="shipping-dims">
|
<label class="mb-0">Versandauftrag</label>
|
||||||
<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-switch v-model="item.cpe_data.shipping" @input="markDirty(item);checkShipping(item)"/>
|
||||||
<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>
|
</div>
|
||||||
<tt-textarea label="Kommentar" v-model="item.cpe_data.note" @input="markDirty(item)" sm no-form-group/>
|
<div class="shipping-dims">
|
||||||
|
<tt-input label="Gewicht (kg)" v-model="item.cpe_data.ship_weight" @input="markDirty(item)" sm type="number" :disabled="!item.cpe_data.shipping"/>
|
||||||
|
<tt-input label="Länge (cm)" v-model="item.cpe_data.ship_length" @input="markDirty(item)" sm type="number" :disabled="!item.cpe_data.shipping"/>
|
||||||
|
<tt-input label="Breite (cm)" v-model="item.cpe_data.ship_width" @input="markDirty(item)" sm type="number" :disabled="!item.cpe_data.shipping"/>
|
||||||
|
<tt-input label="Höhe (cm)" v-model="item.cpe_data.ship_height" @input="markDirty(item)" sm type="number" :disabled="!item.cpe_data.shipping"/>
|
||||||
|
</div>
|
||||||
|
<tt-textarea label="Interner Kommentar" v-model="item.cpe_data.note" @input="markDirty(item)" sm no-form-group rows="2"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 3: Product Info -->
|
||||||
<div class="content-column">
|
<div class="content-column">
|
||||||
<h5>Produkt & VLANs</h5>
|
<h5>Produkt & Services</h5>
|
||||||
<p><strong>{{ item.product_name }}</strong> <small class="text-muted">{{ item.product_code }}</small></p>
|
<div class="product-info">
|
||||||
<p>
|
<span class="product-name">{{ item.product_name }}</span>
|
||||||
<span class="badge badge-info">{{ item.access_type }}</span>
|
<div class="product-badges">
|
||||||
<span class="ml-2"><i class="fas fa-arrow-down"></i> {{ item.access_type_down }}</span>
|
<span class="badge badge-secondary">{{ item.product_code }}</span>
|
||||||
<span class="ml-2"><i class="fas fa-arrow-up"></i> {{ item.access_type_up }}</span>
|
<span class="badge badge-info">{{ item.access_type }}</span>
|
||||||
</p>
|
</div>
|
||||||
<div class="vlans-container mt-2">
|
<div class="mt-1 text-muted">
|
||||||
|
<i class="fas fa-arrow-down"></i> {{ item.access_type_down }} | <i class="fas fa-arrow-up"></i> {{ item.access_type_up }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vlans-container">
|
||||||
<template v-for="(vlan, key) in item.vlans">
|
<template v-for="(vlan, key) in item.vlans">
|
||||||
<tt-chip v-if="vlan.tag" class="vlan-chip" :key="key">
|
<label v-if="vlan.tag" class="vlan-chip" :key="key">
|
||||||
<input type="checkbox" :checked="vlan.checked" @change="markDirty(item); vlan.checked = !vlan.checked"/>
|
<input type="checkbox" :checked="vlan.checked" @change="markDirty(item); vlan.checked = !vlan.checked"/>
|
||||||
<span>{{ key.charAt(0).toUpperCase() + key.slice(1) }}: {{ vlan.tag }}</span>
|
<span>{{ key.charAt(0).toUpperCase() + key.slice(1) }}: {{ vlan.tag }}</span>
|
||||||
</tt-chip>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 4: Actions -->
|
||||||
<div class="content-column action-column">
|
<div class="content-column action-column">
|
||||||
<h5>Aktionen</h5>
|
<h5>Aktionen</h5>
|
||||||
<div class="action-buttons">
|
<div>
|
||||||
<tt-button text="In Radius anlegen"
|
<tt-button text="Radius User anlegen"
|
||||||
@click="createRadiusUser(item)"
|
@click="createRadiusUser(item)"
|
||||||
:disabled="!isValidMac(item.cpe_data.mac)"
|
:disabled="!isValidMac(item.cpe_data.mac)"
|
||||||
sm
|
sm block
|
||||||
additional-class="btn-primary"
|
additional-class="btn-outline-primary mb-2"
|
||||||
title="Sendet Kundendaten an Chrome Extension" />
|
icon="fas fa-user-plus"
|
||||||
|
title="An Chrome Extension senden" />
|
||||||
</div>
|
</div>
|
||||||
<div class="finish-wrapper mt-auto">
|
|
||||||
<label class="col-form-label col-form-label-sm">Konfig abgeschlossen</label>
|
<div class="mt-auto">
|
||||||
<tt-switch v-model="item.cpe_data.routerconfig_finished" @input="markDirty(item)"/>
|
<div class="finish-toggle">
|
||||||
|
<label><strong>Konfiguration fertig</strong></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'"
|
||||||
|
block icon="fas fa-save"/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loading && items.length > 0" class="text-center mt-4 mb-4">
|
<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>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="text-center mt-4 mb-4" v-if="!loading && pagination.total_pages && page <= pagination.total_pages">
|
<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" />
|
<tt-button text="Weitere laden" @click="fetchData(false)" additional-class="btn-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tt-modal :show="showExtensionIdModal" title="Extension ID Konfigurieren" @close="showExtensionIdModal=false" @submit="saveExtensionId">
|
<!-- Custom Extension ID Modal -->
|
||||||
<div>
|
<div class="custom-modal-overlay" v-if="showExtensionIdModal" @click.self="showExtensionIdModal=false">
|
||||||
<div class="field"><label style="margin-bottom: 8px; font-size: 14px;">Chrome Extension ID</label>
|
<div class="custom-modal-container">
|
||||||
<div class="input-wrap"><i class="fa-duotone fa-puzzle-piece input-icon"></i><input class="ri" type="text"
|
<div class="custom-modal-header">
|
||||||
v-model.trim="extensionId"
|
<h3>Extension ID Konfigurieren</h3>
|
||||||
placeholder="z.B. jglijfiddilckddlmbnlojmmlahboffh">
|
<button class="custom-modal-close" @click="showExtensionIdModal=false">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="custom-modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Chrome Extension ID</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fas fa-puzzle-piece"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" v-model.trim="extensionId" placeholder="z.B. jglijfiddilckddlmbnlojmmlahboffh">
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">Die ID der 'TheTool Helper' Chrome Extension.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-modal-footer">
|
||||||
|
<button class="btn btn-secondary mr-2" @click="showExtensionIdModal=false">Abbrechen</button>
|
||||||
|
<button class="btn btn-primary" @click="saveExtensionId">Speichern</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</tt-modal>
|
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
data() {
|
data() {
|
||||||
@@ -171,11 +212,11 @@ Vue.component('Cpeprovisioning', {
|
|||||||
delayOptions: [ { value: '1', text: 'Nicht anzeigen' }, { value: '0', text: 'Anzeigen' } ],
|
delayOptions: [ { value: '1', text: 'Nicht anzeigen' }, { value: '0', text: 'Anzeigen' } ],
|
||||||
page: 1,
|
page: 1,
|
||||||
pagination: {},
|
pagination: {},
|
||||||
debouncedFetchData: null,
|
searchDebounceTimer: null,
|
||||||
macInputTimers: {}, // Store timers for debouncing MAC input
|
macInputTimers: {},
|
||||||
extensionId: 'jglijfiddilckddlmbnlojmmlahboffh',
|
extensionId: 'jglijfiddilckddlmbnlojmmlahboffh',
|
||||||
showExtensionIdModal: false,
|
showExtensionIdModal: false,
|
||||||
processingMacItems: new Set() // Track items currently being processed
|
processingMacItems: new Set()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -188,7 +229,8 @@ Vue.component('Cpeprovisioning', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.debouncedFetchData = _.debounce(this.fetchData.bind(this, true), 400);
|
// Removed usage of _.debounce
|
||||||
|
// Load Extension ID from local storage
|
||||||
const savedExtensionId = localStorage.getItem('radiusExtensionId');
|
const savedExtensionId = localStorage.getItem('radiusExtensionId');
|
||||||
if (savedExtensionId) {
|
if (savedExtensionId) {
|
||||||
this.extensionId = savedExtensionId;
|
this.extensionId = savedExtensionId;
|
||||||
@@ -197,23 +239,22 @@ Vue.component('Cpeprovisioning', {
|
|||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
window.removeEventListener('keydown', this.handleKeydown);
|
window.removeEventListener('keydown', this.handleKeydown);
|
||||||
// Clear all pending MAC input timers
|
|
||||||
Object.keys(this.macInputTimers).forEach(key => {
|
Object.keys(this.macInputTimers).forEach(key => {
|
||||||
clearTimeout(this.macInputTimers[key]);
|
clearTimeout(this.macInputTimers[key]);
|
||||||
});
|
});
|
||||||
|
if (this.searchDebounceTimer) {
|
||||||
|
clearTimeout(this.searchDebounceTimer);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
copyToClipboard(text) {
|
copyToClipboard(text) {
|
||||||
|
if(!text) return;
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text)
|
||||||
.then(() => {
|
.then(() => window.notify('success', 'Kopiert!'))
|
||||||
window.notify('success', 'Text erfolgreich in die Zwischenablage kopiert!');
|
.catch(() => window.notify('error', 'Fehler beim Kopieren'));
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Fehler beim Kopieren des Textes: ', err);
|
|
||||||
window.notify('error', 'Fehler beim Kopieren des Textes.');
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
handleKeydown(e) {
|
handleKeydown(e) {
|
||||||
|
// CTRL + ALT + E to open Extension Config
|
||||||
if (e.code === 'KeyE' && e.ctrlKey && e.altKey) {
|
if (e.code === 'KeyE' && e.ctrlKey && e.altKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.openExtensionIdModal();
|
this.openExtensionIdModal();
|
||||||
@@ -223,15 +264,25 @@ Vue.component('Cpeprovisioning', {
|
|||||||
this.showExtensionIdModal = true;
|
this.showExtensionIdModal = true;
|
||||||
},
|
},
|
||||||
saveExtensionId() {
|
saveExtensionId() {
|
||||||
localStorage.setItem('radiusExtensionId', this.extensionId);
|
if(this.extensionId) {
|
||||||
this.showExtensionIdModal = false;
|
localStorage.setItem('radiusExtensionId', this.extensionId);
|
||||||
window.notify('success', 'Extension ID gespeichert.');
|
this.showExtensionIdModal = false;
|
||||||
|
window.notify('success', 'Extension ID gespeichert.');
|
||||||
|
} else {
|
||||||
|
window.notify('error', 'Bitte eine ID eingeben.');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isValidMac(mac) {
|
isValidMac(mac) {
|
||||||
if (!mac) return false;
|
if (!mac) return false;
|
||||||
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||||
return macRegex.test(mac);
|
return macRegex.test(mac);
|
||||||
},
|
},
|
||||||
|
handleSearchInput() {
|
||||||
|
if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer);
|
||||||
|
this.searchDebounceTimer = setTimeout(() => {
|
||||||
|
this.fetchData(true);
|
||||||
|
}, 400);
|
||||||
|
},
|
||||||
async fetchData(isNewSearch = false) {
|
async fetchData(isNewSearch = false) {
|
||||||
if (isNewSearch) {
|
if (isNewSearch) {
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
@@ -288,278 +339,175 @@ Vue.component('Cpeprovisioning', {
|
|||||||
markDirty(item) {
|
markDirty(item) {
|
||||||
this.$set(item, 'isDirty', true);
|
this.$set(item, 'isDirty', true);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- MAC & QR Logic ---
|
||||||
|
|
||||||
parseMacFromQrCode(qrCode) {
|
parseMacFromQrCode(qrCode) {
|
||||||
console.log('[MAC Parser] Raw QR Code input:', qrCode);
|
if (!qrCode) return null;
|
||||||
|
// Remove whitespace and newlines
|
||||||
// Remove any whitespace/newlines (barcode scanner input doesn't support newlines, but just in case)
|
const cleaned = qrCode.replace(/[\s\n\r]+/g, '');
|
||||||
const cleaned = qrCode.replace(/\s+/g, '');
|
|
||||||
console.log('[MAC Parser] Cleaned QR Code:', cleaned);
|
// Strict Pattern: Must contain a dash separating 6 hex and 12 hex
|
||||||
|
// Example: "CWMP-ID=00040E-802395709D7C" or just "00040E-802395709D7C"
|
||||||
// Extract MAC address from QR format: 00040E-802395709D7C
|
const matchDash = cleaned.match(/([0-9A-Fa-f]{6})-([0-9A-Fa-f]{12})/);
|
||||||
// The MAC address is the 12 hex characters AFTER the dash
|
|
||||||
const match = cleaned.match(/-([0-9A-Fa-f]{12})/);
|
if (matchDash) {
|
||||||
console.log('[MAC Parser] Regex match result:', match);
|
console.log('[MAC Parser] Found Pattern:', matchDash[0]);
|
||||||
|
// RETURN ONLY THE PART AFTER THE DASH (Group 2)
|
||||||
if (match) {
|
return matchDash[2];
|
||||||
const macAddress = match[1]; // Get only the part after the dash: 802395709D7C
|
|
||||||
console.log('[MAC Parser] Extracted MAC:', macAddress);
|
|
||||||
return macAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[MAC Parser] No match found, returning null');
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
calculateMacOffset(macAddress, offset) {
|
calculateMacOffset(macAddress, offset) {
|
||||||
console.log('[MAC Offset] Input MAC:', macAddress, 'Offset:', offset);
|
// Convert to BigInt to handle 48-bit integer math safely
|
||||||
|
|
||||||
// Convert MAC to decimal using BigInt for large numbers
|
|
||||||
const macDecimal = BigInt('0x' + macAddress);
|
const macDecimal = BigInt('0x' + macAddress);
|
||||||
console.log('[MAC Offset] MAC as decimal:', macDecimal.toString());
|
|
||||||
|
|
||||||
// Apply offset (add or subtract)
|
|
||||||
const newMacDecimal = macDecimal + BigInt(offset);
|
const newMacDecimal = macDecimal + BigInt(offset);
|
||||||
console.log('[MAC Offset] New MAC as decimal:', newMacDecimal.toString());
|
|
||||||
|
|
||||||
// Convert back to hex (12 chars, padded with zeros)
|
|
||||||
let newMacHex = newMacDecimal.toString(16).toUpperCase();
|
let newMacHex = newMacDecimal.toString(16).toUpperCase();
|
||||||
newMacHex = newMacHex.padStart(12, '0');
|
// Ensure 12 chars padding
|
||||||
console.log('[MAC Offset] Result MAC:', newMacHex);
|
return newMacHex.padStart(12, '0');
|
||||||
|
|
||||||
return newMacHex;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
formatMacAddress(macAddress) {
|
formatMacAddress(macAddress) {
|
||||||
console.log('[MAC Format] Input MAC:', macAddress);
|
// Strip existing delimiters
|
||||||
|
|
||||||
// Remove any existing formatting
|
|
||||||
const cleaned = macAddress.replace(/[:-]/g, '');
|
const cleaned = macAddress.replace(/[:-]/g, '');
|
||||||
console.log('[MAC Format] Cleaned MAC:', cleaned);
|
// Add colons
|
||||||
|
return cleaned.match(/.{1,2}/g).join(':').toUpperCase();
|
||||||
// Format as AA:BB:CC:DD:EE:FF (uppercase)
|
|
||||||
const formatted = cleaned.match(/.{1,2}/g).join(':').toUpperCase();
|
|
||||||
console.log('[MAC Format] Formatted MAC:', formatted);
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMacInput(item, val) {
|
handleMacInput(item, val) {
|
||||||
if (val !== undefined) {
|
if (val !== undefined) {
|
||||||
item.cpe_data.mac = val;
|
item.cpe_data.mac = val;
|
||||||
}
|
}
|
||||||
const currentValue = item.cpe_data.mac;
|
|
||||||
|
|
||||||
console.log('[MAC Input] === START handleMacInput ===');
|
|
||||||
console.log('[MAC Input] Item:', item);
|
|
||||||
console.log('[MAC Input] Current MAC value:', currentValue);
|
|
||||||
console.log('[MAC Input] Router type:', item.cpe_data.routertype);
|
|
||||||
|
|
||||||
const itemKey = item.orderproduct_id;
|
const itemKey = item.orderproduct_id;
|
||||||
|
|
||||||
// Check if we're already processing this item to prevent recursion
|
|
||||||
if (this.processingMacItems.has(itemKey)) {
|
|
||||||
console.log('[MAC Input] Already processing this item, returning to prevent recursion');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.markDirty(item);
|
this.markDirty(item);
|
||||||
|
|
||||||
// Clear existing timer for this item
|
|
||||||
if (this.macInputTimers[itemKey]) {
|
if (this.macInputTimers[itemKey]) {
|
||||||
console.log('[MAC Input] Clearing existing timer for item:', itemKey);
|
|
||||||
clearTimeout(this.macInputTimers[itemKey]);
|
clearTimeout(this.macInputTimers[itemKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for immediate QR code match (CWMP ID format)
|
// Immediate check if it LOOKS like it contains our pattern
|
||||||
// Format: 00040E-802395709D7C (approx 19 chars)
|
const isPotentialScan = item.cpe_data.mac && item.cpe_data.mac.includes('-');
|
||||||
if (currentValue && currentValue.includes('-') && currentValue.length >= 18) {
|
|
||||||
const quickMatch = /-([0-9A-Fa-f]{12})/.test(currentValue);
|
if (isPotentialScan) {
|
||||||
if (quickMatch) {
|
// If it has a dash, we process immediately to see if it matches our strict pattern
|
||||||
console.log('[MAC Input] Quick match found, processing immediately');
|
this.processMacAddress(item);
|
||||||
this.processMacAddress(item);
|
} else {
|
||||||
return;
|
// Otherwise debounce (typing manual address)
|
||||||
}
|
this.macInputTimers[itemKey] = setTimeout(() => {
|
||||||
|
this.processMacAddress(item);
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new timer (debounce with 300ms delay)
|
|
||||||
console.log('[MAC Input] Creating new debounce timer for item:', itemKey);
|
|
||||||
this.macInputTimers[itemKey] = setTimeout(() => {
|
|
||||||
console.log('[MAC Input] Debounce timer fired for item:', itemKey);
|
|
||||||
this.processMacAddress(item);
|
|
||||||
}, 300); // 300ms delay to wait for barcode scanner to finish
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleRouterTypeChange(item) {
|
handleRouterTypeChange(item) {
|
||||||
console.log('[Router Type Change] Router type changed, checking if MAC needs processing');
|
// Re-process MAC if router type changes (offset might change)
|
||||||
this.processMacAddress(item);
|
if (item.cpe_data.mac) {
|
||||||
|
this.processMacAddress(item);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
processMacAddress(item) {
|
processMacAddress(item) {
|
||||||
const inputValue = item.cpe_data.mac;
|
const inputValue = item.cpe_data.mac;
|
||||||
const routerType = item.cpe_data.routertype;
|
const routerType = item.cpe_data.routertype;
|
||||||
const itemKey = item.orderproduct_id;
|
|
||||||
|
|
||||||
console.log('[MAC Process] === START processMacAddress ===');
|
if (!inputValue) return;
|
||||||
console.log('[MAC Process] Input value:', inputValue);
|
|
||||||
console.log('[MAC Process] Router type:', routerType);
|
|
||||||
|
|
||||||
// Only process if it's a QR code format (contains dash, no colons)
|
// 1. Try to detect and parse QR code format
|
||||||
// This prevents reprocessing already formatted MAC addresses
|
// This will ONLY return a value if the XXXXXX-XXXXXXXXXXXX pattern exists
|
||||||
if (!inputValue) {
|
const parsedMac = this.parseMacFromQrCode(inputValue);
|
||||||
console.log('[MAC Process] No input value, returning');
|
|
||||||
return;
|
if (parsedMac) {
|
||||||
}
|
// A QR-like pattern (XXXXXX-XXXXXXXXXXXX) was found
|
||||||
|
if (routerType === 'FritzBox 4050' || routerType === 'FritzBox 7690') {
|
||||||
|
// Perform calculation and full formatting for specific routers
|
||||||
|
let offset = 0;
|
||||||
|
if (routerType === 'FritzBox 4050') offset = -3;
|
||||||
|
else if (routerType === 'FritzBox 7690') offset = 2;
|
||||||
|
|
||||||
if (!inputValue.includes('-')) {
|
try {
|
||||||
console.log('[MAC Process] No dash found in input, returning');
|
const newMac = (offset !== 0) ? this.calculateMacOffset(parsedMac, offset) : parsedMac;
|
||||||
return;
|
const formatted = this.formatMacAddress(newMac);
|
||||||
}
|
|
||||||
|
|
||||||
if (inputValue.includes(':')) {
|
if (item.cpe_data.mac !== formatted) {
|
||||||
console.log('[MAC Process] Already formatted (contains colons), returning');
|
this.$set(item.cpe_data, 'mac', formatted);
|
||||||
return;
|
const offsetStr = offset > 0 ? `+${offset}` : `${offset}`;
|
||||||
}
|
window.notify('success', `MAC berechnet (${offsetStr}): ${formatted}`);
|
||||||
|
}
|
||||||
console.log('[MAC Process] Input contains dash and no colons, proceeding...');
|
} catch (e) {
|
||||||
|
console.error('MAC Calculation error', e);
|
||||||
// Check if this is a 4050 or 7690 router
|
}
|
||||||
if (routerType !== 'FritzBox 4050' && routerType !== 'FritzBox 7690') {
|
} else {
|
||||||
console.log('[MAC Process] Router type is not FritzBox 4050 or FritzBox 7690, returning. Current type:', routerType);
|
// For other routers, just extract the 12-char MAC from QR, but don't apply offset.
|
||||||
return;
|
// The 'formatting' part (AA:BB:CC...) should only happen for 4050/7690
|
||||||
}
|
// So for other types, we'll just put the raw 12-char MAC from the QR.
|
||||||
|
if (item.cpe_data.mac !== parsedMac) {
|
||||||
console.log('[MAC Process] Router type is valid:', routerType);
|
this.$set(item.cpe_data, 'mac', parsedMac); // Store raw 12-char MAC
|
||||||
|
window.notify('info', 'MAC aus QR extrahiert (keine Berechnung für diesen Routertyp).');
|
||||||
// Mark this item as being processed
|
}
|
||||||
this.processingMacItems.add(itemKey);
|
}
|
||||||
console.log('[MAC Process] Added item to processing set:', itemKey);
|
} else {
|
||||||
|
// No QR pattern found, apply general manual entry formatting if it's a valid 12-char hex string
|
||||||
// Safety timeout to ensure we don't get stuck in processing state
|
const cleanInput = inputValue.replace(/[:-]/g, '').replace(/\s/g, '');
|
||||||
const safetyTimeout = setTimeout(() => {
|
if (cleanInput.length === 12 && /^[0-9A-Fa-f]{12}$/.test(cleanInput)) {
|
||||||
console.log('[MAC Process] Safety timeout - removing item from processing set:', itemKey);
|
const formatted = this.formatMacAddress(cleanInput);
|
||||||
this.processingMacItems.delete(itemKey);
|
if (item.cpe_data.mac !== formatted) {
|
||||||
}, 2000);
|
this.$set(item.cpe_data, 'mac', formatted);
|
||||||
|
window.notify('info', 'MAC formatiert.');
|
||||||
try {
|
}
|
||||||
// Parse MAC from QR code
|
|
||||||
const parsedMac = this.parseMacFromQrCode(inputValue);
|
|
||||||
|
|
||||||
if (!parsedMac) {
|
|
||||||
console.log('[MAC Process] Failed to parse MAC from QR code');
|
|
||||||
window.notify('error', 'Konnte MAC-Adresse nicht aus QR-Code parsen');
|
|
||||||
clearTimeout(safetyTimeout);
|
|
||||||
this.processingMacItems.delete(itemKey);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[MAC Process] Successfully parsed MAC:', parsedMac);
|
|
||||||
|
|
||||||
// Calculate offset based on router type
|
|
||||||
const offset = routerType === 'FritzBox 4050' ? -3 : 2;
|
|
||||||
console.log('[MAC Process] Applying offset:', offset);
|
|
||||||
|
|
||||||
const newMac = this.calculateMacOffset(parsedMac, offset);
|
|
||||||
console.log('[MAC Process] Calculated new MAC:', newMac);
|
|
||||||
|
|
||||||
// Format MAC address
|
|
||||||
const formattedMac = this.formatMacAddress(newMac);
|
|
||||||
console.log('[MAC Process] Formatted MAC:', formattedMac);
|
|
||||||
|
|
||||||
// Update the field using Vue.set for proper reactivity
|
|
||||||
console.log('[MAC Process] Updating item.cpe_data.mac to:', formattedMac);
|
|
||||||
this.$set(item.cpe_data, 'mac', formattedMac);
|
|
||||||
|
|
||||||
// Force Vue to update the DOM
|
|
||||||
this.$nextTick(() => {
|
|
||||||
console.log('[MAC Process] After nextTick, MAC value is:', item.cpe_data.mac);
|
|
||||||
// Clear safety timeout and remove from processing set
|
|
||||||
clearTimeout(safetyTimeout);
|
|
||||||
this.processingMacItems.delete(itemKey);
|
|
||||||
console.log('[MAC Process] Removed item from processing set after nextTick:', itemKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show notification
|
|
||||||
const offsetStr = offset > 0 ? `+${offset}` : `${offset}`;
|
|
||||||
window.notify('success', `MAC-Adresse aus QR-Code geparst (${offsetStr}): ${formattedMac}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[MAC Process] Error processing MAC:', error);
|
|
||||||
clearTimeout(safetyTimeout);
|
|
||||||
this.processingMacItems.delete(itemKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[MAC Process] === END processMacAddress ===');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
checkShipping(item) {
|
checkShipping(item) {
|
||||||
if (item.cpe_data.shipping && item.cpe_data.routertype) {
|
if (item.cpe_data.shipping && item.cpe_data.routertype) {
|
||||||
const shippingData = this.window.TT_CONFIG.ROUTER_SHIPPING_DATA[item.cpe_data.routertype];
|
const shippingData = this.window.TT_CONFIG.ROUTER_SHIPPING_DATA ? this.window.TT_CONFIG.ROUTER_SHIPPING_DATA[item.cpe_data.routertype] : null;
|
||||||
if (shippingData) {
|
if (shippingData) {
|
||||||
item.cpe_data.ship_weight = shippingData.weight;
|
item.cpe_data.ship_weight = shippingData.weight;
|
||||||
item.cpe_data.ship_length = shippingData.length;
|
item.cpe_data.ship_length = shippingData.length;
|
||||||
item.cpe_data.ship_width = shippingData.width;
|
item.cpe_data.ship_width = shippingData.width;
|
||||||
item.cpe_data.ship_height = shippingData.height;
|
item.cpe_data.ship_height = shippingData.height;
|
||||||
item.cpe_data = { ...item.cpe_data }; // Trigger reactivity
|
item.cpe_data = { ...item.cpe_data }; // Trigger reactivity
|
||||||
this.window.notify('success', 'Versanddaten wurden automatisch ausgefüllt.');
|
this.window.notify('success', 'Versanddaten übernommen.');
|
||||||
}
|
}
|
||||||
} 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
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
createRadiusUser(item) {
|
createRadiusUser(item) {
|
||||||
console.log('[Create Radius User] === START ===');
|
|
||||||
console.log('[Create Radius User] Item:', item);
|
|
||||||
|
|
||||||
// Prepare the data to send to the Chrome extension
|
|
||||||
const customerNumber = item.owner_customer_number || 'N/A';
|
|
||||||
const macAddress = item.cpe_data.mac;
|
|
||||||
const address = item.owner_full_address || 'N/A';
|
|
||||||
const customerName = item.customer || 'N/A';
|
|
||||||
const productName = item.product_name || 'N/A';
|
|
||||||
|
|
||||||
console.log('[Create Radius User] Customer Number:', customerNumber);
|
|
||||||
console.log('[Create Radius User] MAC Address:', macAddress);
|
|
||||||
console.log('[Create Radius User] Address:', address);
|
|
||||||
console.log('[Create Radius User] Customer Name:', customerName);
|
|
||||||
console.log('[Create Radius User] Product Name:', productName);
|
|
||||||
|
|
||||||
window.notify('info', 'Sende Daten an Chrome Extension...');
|
window.notify('info', 'Sende Daten an Chrome Extension...');
|
||||||
|
|
||||||
const extensionId = this.extensionId;
|
|
||||||
const message = {
|
const message = {
|
||||||
type: "CREATE_RADIUS_USER",
|
type: "CREATE_RADIUS_USER",
|
||||||
payload: {
|
payload: {
|
||||||
customerNumber: customerNumber,
|
customerNumber: item.owner_customer_number || 'N/A',
|
||||||
macAddress: macAddress,
|
macAddress: item.cpe_data.mac,
|
||||||
address: address,
|
address: item.owner_full_address || 'N/A',
|
||||||
customerName: customerName,
|
customerName: item.customer || 'N/A',
|
||||||
productName: productName
|
productName: item.product_name || 'N/A'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[Create Radius User] Extension ID:', extensionId);
|
|
||||||
console.log('[Create Radius User] Message:', message);
|
|
||||||
|
|
||||||
if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) {
|
if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) {
|
||||||
try {
|
try {
|
||||||
chrome.runtime.sendMessage(extensionId, message, (response) => {
|
chrome.runtime.sendMessage(this.extensionId, message, (response) => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
console.warn('[Create Radius User] Senden an Erweiterung fehlgeschlagen:', chrome.runtime.lastError.message);
|
console.warn(chrome.runtime.lastError.message);
|
||||||
window.notify('warning', 'Daten konnten nicht an die Erweiterung gesendet werden. (Drücke STRG + ALT + E zum Konfigurieren)');
|
window.notify('warning', 'Kommunikation fehlgeschlagen. Extension installiert?');
|
||||||
} else {
|
} else {
|
||||||
console.log('[Create Radius User] Erweiterung hat geantwortet:', response);
|
window.notify('success', 'Daten gesendet!');
|
||||||
window.notify('success', 'Daten erfolgreich an Chrome Extension gesendet!');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Create Radius User] Fehler beim Senden an die Erweiterung:', e);
|
window.notify('error', 'Fehler: ' + e.message);
|
||||||
window.notify('error', 'Fehler beim Senden an die Erweiterung.');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('[Create Radius User] Chrome Extension Messaging API nicht verfügbar.');
|
window.notify('warning', 'Chrome Messaging API nicht verfügbar.');
|
||||||
window.notify('warning', 'Chrome Messaging API nicht gefunden.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Create Radius User] === END ===');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_buildSavePayload(item) {
|
_buildSavePayload(item) {
|
||||||
return {
|
return {
|
||||||
id: item.cpe_id,
|
id: item.cpe_id,
|
||||||
@@ -573,6 +521,7 @@ Vue.component('Cpeprovisioning', {
|
|||||||
routerconfig_finished: item.cpe_data.routerconfig_finished ? 1 : 0,
|
routerconfig_finished: item.cpe_data.routerconfig_finished ? 1 : 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveCpe(item) {
|
async saveCpe(item) {
|
||||||
this.$set(item, 'isSaving', true);
|
this.$set(item, 'isSaving', true);
|
||||||
const payload = this._buildSavePayload(item);
|
const payload = this._buildSavePayload(item);
|
||||||
@@ -602,6 +551,5 @@ Vue.component('Cpeprovisioning', {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchData(true);
|
this.fetchData(true);
|
||||||
window.addEventListener('keydown', this.handleKeydown);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user