Merge branch 'CpeProvisioning/v3' into 'master'
new v3 layout See merge request fronk/thetool!1787
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
class CpeprovisioningController extends mfBaseController
|
||||
{
|
||||
protected function init() {
|
||||
// disable error display for all requests to avoid information leakage
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
if (defined('TT_CPE_PROV_ALLOW_COUNTS_IP') &&
|
||||
in_array($_SERVER['REMOTE_ADDR'], TT_CPE_PROV_ALLOW_COUNTS_IP) &&
|
||||
!mfLoginController::isLoggedIn()) {
|
||||
@@ -482,6 +486,8 @@ class CpeprovisioningController extends mfBaseController
|
||||
'id' => $product->id, 'order_id' => $product->order_id, 'termination_id' => $product->termination_id, 'orderproduct_id' => $product->id,
|
||||
'network' => $term->building->network->name ?? "{$order->owner->zip} {$order->owner->city}",
|
||||
'spin' => $order->owner->spin, 'customer' => $order->owner->getCompanyOrName(),
|
||||
'owner_email' => $order->owner->email,
|
||||
'owner_phone' => $order->owner->phone,
|
||||
'product_name' => $product->product->name, 'product_code' => $term->code ?? '',
|
||||
'access_type' => $attrs['bras_type']->value,
|
||||
'access_type_down' => $attrs["bw_down"]->value,
|
||||
@@ -557,9 +563,12 @@ class CpeprovisioningController extends mfBaseController
|
||||
],
|
||||
"ROUTER_SHIPPING_DATA" => [
|
||||
"TP-Link Archer C80" => ["weight" => 1, "length" => 35, "width" => 24, "height" => 8],
|
||||
"FritzBox 5530" => ["weight" => 1, "length" => 27, "width" => 19, "height" => 7],
|
||||
"FritzBox 4050" => ["weight" => 1, "length" => 27, "width" => 19, "height" => 7],
|
||||
"FritzBox 7690" => ["weight" => 1, "length" => 27, "width" => 19, "height" => 7],
|
||||
"FritzBox 4040" => ["weight" => 1, "length" => 30, "width" => 24, "height" => 7],
|
||||
"FritzBox 7530" => ["weight" => 1, "length" => 26, "width" => 19, "height" => 7],
|
||||
"FritzBox 7590" => ["weight" => 1, "length" => 30, "width" => 24, "height" => 7],
|
||||
"FritzBox 7530" => ["weight" => 1, "length" => 27, "width" => 19, "height" => 7],
|
||||
"FritzBox 7590" => ["weight" => 1, "length" => 27, "width" => 19, "height" => 7],
|
||||
"FritzBox 6490 Cable" => ["weight" => 1, "length" => 30, "width" => 26, "height" => 8]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,159 +1,156 @@
|
||||
/* Cpeprovisioning.css */
|
||||
|
||||
.cpe-provisioning-page .filter-card {
|
||||
margin-bottom: 1rem;
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- Page & Filter Layout --- */
|
||||
.cpe-provisioning-page .filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.cpe-provisioning-page .filter-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding-top: 1.5rem; /* Align with form labels */
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.loading-indicator, .no-results-indicator {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.cpe-provisioning-page .cpe-details-grid {
|
||||
/* --- Cards Container --- */
|
||||
.cpe-cards-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #dee2e6;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.cpe-provisioning-page .section-title {
|
||||
grid-column: 1 / -1;
|
||||
/* --- Single Card Styling --- */
|
||||
.cpe-card {
|
||||
background-color: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-left: 5px solid transparent;
|
||||
border-radius: 0.5rem;
|
||||
transition: box-shadow 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
||||
}
|
||||
.cpe-card.is-dirty {
|
||||
border-left-color: #f7c423; /* Yellow accent for dirty */
|
||||
}
|
||||
.cpe-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* --- Card Header --- */
|
||||
.cpe-card-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(200px, 1.5fr) 2fr auto;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
.cpe-card-header .customer-info strong {
|
||||
color: #005384;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.cpe-card-header .customer-info small {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.location-contact-header {
|
||||
font-size: 0.85rem;
|
||||
color: #495057;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.1rem 1rem;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
font-size: 1.1rem;
|
||||
color: #005384;
|
||||
}
|
||||
.header-actions a { color: inherit; transition: color 0.2s; }
|
||||
.header-actions a:hover { color: #f7c423; }
|
||||
|
||||
/* --- Card Content Grid --- */
|
||||
.cpe-card-content {
|
||||
padding: 0.75rem 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 1.25fr) minmax(280px, 1.25fr) minmax(280px, 1.5fr);
|
||||
gap: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.content-column {
|
||||
padding-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.content-column.action-column {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.content-column h5 {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #005384;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
.content-column p {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.content-column .form-group, .content-column .tt-select-modern {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.action-column .btn {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid #f7c423;
|
||||
}
|
||||
|
||||
.cpe-provisioning-page .info-pills {
|
||||
.shipping-dims {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem 0.75rem;
|
||||
}
|
||||
.finish-wrapper {
|
||||
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;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem;
|
||||
background-color: #f1f3f5;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.mt-auto {
|
||||
margin-top: auto !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
/* --- VLAN Chip Styling --- */
|
||||
.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: 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%;
|
||||
@media (max-width: 1400px) {
|
||||
.cpe-card-content {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
.cpe-card-header {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
@@ -10,147 +10,123 @@ 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 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="Suche" v-model="filters.owner" sm placeholder="Kunde, SPIN, Adresse..." @input="applyClientSideFilter"/>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<div v-if="loading" 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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 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">
|
||||
<strong>{{ item.customer }}</strong>
|
||||
<small class="text-muted">SPIN: <span class="text-pink">{{ item.spin }}</span></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>
|
||||
<div class="location-contact-header">
|
||||
<div><strong>Netzgebiet:</strong> {{ item.network }}</div>
|
||||
<div><strong>POP:</strong> {{ item.pop_name || 'N/A' }}</div>
|
||||
<div v-if="item.owner_phone"><i class="fas fa-phone mr-2 text-muted"></i>{{ item.owner_phone }}</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=' + row.order_id" class="mr-2">
|
||||
<i class="fas fa-print"></i>
|
||||
<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>
|
||||
<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 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>
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<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 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>
|
||||
</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 v-if="row.termination_id && row.ont_deployed !== 0" 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);checkShipping(row)" 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 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>
|
||||
</template>
|
||||
</tt-table>
|
||||
</tt-card>
|
||||
|
||||
<div class="content-column action-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 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>
|
||||
`,
|
||||
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' }
|
||||
],
|
||||
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 },
|
||||
]
|
||||
}
|
||||
statusOptions: [ { value: '0', text: 'Offen' }, { value: '1', text: 'Abgeschlossen' } ],
|
||||
delayOptions: [ { value: '1', text: 'Nicht anzeigen' }, { value: '0', text: 'Anzeigen' } ],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -163,47 +139,100 @@ Vue.component('Cpeprovisioning', {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
applyFilters(fetch = true) {
|
||||
const table = this.$refs.cpeTable;
|
||||
if (table) {
|
||||
table.filters = JSON.parse(JSON.stringify(this.filters));
|
||||
if (fetch) table.fetchData(1);
|
||||
async fetchData() {
|
||||
this.loading = true;
|
||||
this.items = [];
|
||||
this.filteredItems = [];
|
||||
|
||||
const payload = {
|
||||
pagination: { page: 1, per_page: 100 },
|
||||
filters: {
|
||||
network_id: this.filters.network_id,
|
||||
routerconfig_finished: this.filters.routerconfig_finished,
|
||||
hide_delayed_finish: this.filters.hide_delayed_finish,
|
||||
},
|
||||
order: { key: 'order_id', order: 'desc' }
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(window.TT_CONFIG.CPE_PROV_API_GET_URL, payload);
|
||||
this.items = (data.rows || []).map(item => ({
|
||||
...item,
|
||||
isDirty: false,
|
||||
isSaving: 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 || '',
|
||||
}));
|
||||
this.applyClientSideFilter();
|
||||
} catch (error) {
|
||||
console.error("Error fetching CPE data:", error);
|
||||
window.notify('error', 'Fehler beim Laden der Daten.');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
applyClientSideFilter() {
|
||||
if (!this.filters.owner) {
|
||||
this.filteredItems = this.items;
|
||||
return;
|
||||
}
|
||||
const search = this.filters.owner.toLowerCase();
|
||||
this.filteredItems = this.items.filter(item => {
|
||||
return (item.customer && item.customer.toLowerCase().includes(search)) ||
|
||||
(item.spin && item.spin.toLowerCase().includes(search)) ||
|
||||
(item.owner_address && item.owner_address.toLowerCase().includes(search)) ||
|
||||
(item.product_name && item.product_name.toLowerCase().includes(search)) ||
|
||||
(item.network && item.network.toLowerCase().includes(search));
|
||||
});
|
||||
},
|
||||
resetFilters() {
|
||||
this.filters = {
|
||||
network_id: '',
|
||||
routerconfig_finished: '0',
|
||||
hide_delayed_finish: '1',
|
||||
owner: ''
|
||||
};
|
||||
this.applyFilters();
|
||||
this.filters = { network_id: '', routerconfig_finished: '0', hide_delayed_finish: '1', owner: '' };
|
||||
this.fetchData();
|
||||
},
|
||||
markDirty(row, field) {
|
||||
console.log(`Marking row as dirty for field ${field}`);
|
||||
this.$set(row, 'isDirty', true);
|
||||
markDirty(item) {
|
||||
this.$set(item, 'isDirty', true);
|
||||
},
|
||||
async checkShipping (row) {
|
||||
async checkShipping(row) {
|
||||
await this.$nextTick();
|
||||
let wasPrefilled = false;
|
||||
|
||||
if (row.cpe_data.shipping && row.cpe_data.routertype) {
|
||||
const shippingData = this.window.TT_CONFIG.ROUTER_SHIPPING_DATA[row.cpe_data.routertype];
|
||||
if (shippingData) {
|
||||
if (!row.cpe_data.ship_weight) row.cpe_data.ship_weight = shippingData.weight;
|
||||
if (!row.cpe_data.ship_length) row.cpe_data.ship_length = shippingData.length;
|
||||
if (!row.cpe_data.ship_width) row.cpe_data.ship_width = shippingData.width;
|
||||
if (!row.cpe_data.ship_height) row.cpe_data.ship_height = shippingData.height;
|
||||
if (!row.cpe_data.ship_weight) {
|
||||
this.$set(row.cpe_data, 'ship_weight', shippingData.weight);
|
||||
wasPrefilled = true;
|
||||
}
|
||||
if (!row.cpe_data.ship_length) {
|
||||
this.$set(row.cpe_data, 'ship_length', shippingData.length);
|
||||
wasPrefilled = true;
|
||||
}
|
||||
if (!row.cpe_data.ship_width) {
|
||||
this.$set(row.cpe_data, 'ship_width', shippingData.width);
|
||||
wasPrefilled = true;
|
||||
}
|
||||
if (!row.cpe_data.ship_height) {
|
||||
this.$set(row.cpe_data, 'ship_height', shippingData.height);
|
||||
wasPrefilled = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if (!row.cpe_data.shipping) {
|
||||
// Clear the fields if shipping is unchecked
|
||||
row.cpe_data.ship_weight = '';
|
||||
row.cpe_data.ship_length = '';
|
||||
row.cpe_data.ship_width = '';
|
||||
row.cpe_data.ship_height = '';
|
||||
}
|
||||
|
||||
if (wasPrefilled) {
|
||||
this.markDirty(row);
|
||||
this.window.notify('success', 'Versanddaten wurden automatisch ausgefüllt.');
|
||||
}
|
||||
},
|
||||
async saveCpe(row) {
|
||||
this.$set(row, 'isSaving', true);
|
||||
|
||||
const payload = {
|
||||
id: row.cpe_id,
|
||||
order_id: row.order_id,
|
||||
@@ -220,8 +249,15 @@ Vue.component('Cpeprovisioning', {
|
||||
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();
|
||||
if (this.filters.routerconfig_finished === '0' && payload.routerconfig_finished) {
|
||||
this.items = this.items.filter(item => item.orderproduct_id !== row.orderproduct_id);
|
||||
this.applyClientSideFilter();
|
||||
} else {
|
||||
const index = this.items.findIndex(item => item.orderproduct_id === row.orderproduct_id);
|
||||
if (index !== -1) {
|
||||
this.items[index].isDirty = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.window.notify('error', data.message || 'Fehler beim Speichern.');
|
||||
}
|
||||
@@ -233,6 +269,6 @@ Vue.component('Cpeprovisioning', {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.applyFilters(false);
|
||||
this.fetchData();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user