diff --git a/Layout/default/VueViews/Vue.php b/Layout/default/VueViews/Vue.php index c6bab7c16..52a0f5a7d 100644 --- a/Layout/default/VueViews/Vue.php +++ b/Layout/default/VueViews/Vue.php @@ -40,7 +40,7 @@ include($vueHeaderPath); ?>
diff --git a/application/Radius/RadiusController.php b/application/Radius/RadiusController.php index 7e129785d..2685b1130 100644 --- a/application/Radius/RadiusController.php +++ b/application/Radius/RadiusController.php @@ -15,10 +15,9 @@ class RadiusController extends mfBaseController { protected function indexAction() { $this->layout()->set('additionalJS', ["plugins/chart.js/chart.4.4.6.js", "plugins/chart.js/chartjs-adapter-moment.min.js"]); - Helper::renderVue($this, $this->mod, "Radius", ['CAN_BILLING' => $this->me->can("Billing")]); + Helper::renderVue($this, $this->mod, "Radius", ['CAN_BILLING' => $this->me->can("Billing"), 'HIDE_PAGE_TITLE' => true]); } - protected function proxyUnsecureHTTPRequestToRadiusAction() { $url = "http://radius.xinon.at/api.php?" . http_build_query($_GET); $url = str_replace("proxyUnsecureHTTPRequestToRadius", "", $url); diff --git a/public/js/pages/Radius/Radius.css b/public/js/pages/Radius/Radius.css index ac8f00c41..285cbd69e 100644 --- a/public/js/pages/Radius/Radius.css +++ b/public/js/pages/Radius/Radius.css @@ -1,81 +1,434 @@ -.radius-view-selector { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, max-content)); - grid-gap: 10px; - justify-content: start; - margin-bottom: 20px; -} - -.radius-view-selector > *, -.radius-view-selector > * > * { - width: 100%; -} - -@media (max-width: 576px) { - .radius-view-selector { - grid-template-columns: 1fr; - } -} - -.filters { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - grid-gap: 15px; - margin-bottom: 20px; - justify-content: center; - align-items: center; -} - -.status-dot { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 50%; -} - -.status-dot.online { - background-color: green; -} - -.status-dot.offline { - background-color: grey; -} - -.free-users-container { - display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 20px; -} - -@media (max-width: 768px) { - .free-users-container { - grid-template-columns: 1fr; - } -} - -.free-users-column { - background-color: #f8f9fa; - padding: 15px; - border-radius: 5px; -} - - - -/* -RADIUS ONT PARSER +/* ===== Radius.css ===== + Light theme, brand colors (blue), green online accent, compact density. + Scoped with .radius-scope to avoid page-wide resets. */ -.loading-overlay { + +:root{ + --brand-blue: #005384; + --bg: #ffffff; + --card: #ffffff; + --card-2: #f8fafc; + --muted: #667085; + --text: #0b1320; + --accent: var(--brand-blue); + --accent-2: #1e88c9; + --ok: #0f9d58; /* ✅ green accent */ + --bad: #e03131; + --ring: rgba(0,83,132,.20); + --border: #e6e9ef; + --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --radius: 10px; + --radius-pill: 999px; + --shadow: 0 8px 24px rgba(0, 83, 132, .08); +} + +/* container & basic colors */ +.radius-scope a.link { + color: var(--accent); + text-decoration: none; + font-weight: 500; + transition: color .2s ease; +} +.radius-scope a.link:hover { + color: var(--accent-2); + text-decoration: underline; +} +.radius-scope .muted { color: var(--muted); } +.radius-scope .small { font-size: 12px; } +.radius-scope .mini { font-size: 11px; } +.radius-scope .mono { font-family: var(--mono); } +.radius-scope .center { text-align: center; } +.radius-scope .p-sm { padding: .5rem; } +.radius-scope .p-lg { padding: 1.25rem; } +.radius-scope .mt-2 { margin-top: .5rem; } +.radius-scope .mt-3 { margin-top: .75rem; } +.radius-scope .mt-between { margin-top: 12px; } +.radius-scope .nowrap { white-space: nowrap; } +.radius-scope .inline-copy { display:flex; align-items:center; gap:8px; justify-content: flex-end; } + +/* simple grid utilities (used by Free Users + ONT views) */ +.radius-scope .grid { display:grid; } +.radius-scope .g-4 { gap: 16px; } +.radius-scope .g-6 { gap: 24px; } +.radius-scope .cols-2 { grid-template-columns: repeat(2, minmax(0,1fr)); } +.radius-scope .cols-1 { grid-template-columns: 1fr; } +@media (min-width: 900px){ .radius-scope .cols-2@lg { grid-template-columns: repeat(2, minmax(0,1fr)); } } +@media (min-width: 1200px){ .radius-scope .cols-2-xl { grid-template-columns: repeat(2, minmax(0,1fr)); } } +@media (max-width: 899.98px){ .radius-scope .cols-1@sm { grid-template-columns: 1fr; } } + + +/* badges / headings / clusters */ +.radius-scope .badge { display:inline-block; padding:2px 8px; border-radius:999px; background:#eef6fb; color:#0b3a57; font-size:12px; border:1px solid #d6e8f5; } +.radius-scope .h4 { font-size:18px; font-weight:800; letter-spacing:.2px; } +.radius-scope .h5 { font-size:16px; font-weight:800; letter-spacing:.2px; } +.radius-scope .cluster { display:flex; gap:10px; flex-wrap:wrap; align-items: center; } + +/* container */ +.radius-scope.radius-container { + background: transparent; color: var(--text); + display: grid; gap: 16px; + max-width: 90vw; margin: 0 auto; + padding: 20px 0; +} + +.radius-scope .card, .radius-scope .subcard, .radius-scope .progress-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); +} +.radius-scope .card { padding: 14px; } +.radius-scope .subcard { padding: 12px; } + +/* main header inside card */ +.radius-scope .pane-header { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom: 10px; flex-wrap: wrap;} +.radius-scope .pane-header .title { display:flex; align-items:center; gap:10px; font-weight:800; letter-spacing:.2px; font-size: 18px; } +/* ✅ green dot (single branding only in the main header) */ +.radius-scope .logo-dot { + width:14px; height:14px; border-radius:50%; + background: radial-gradient(circle at 30% 30%, #37d26b, #0f9d58 70%); + box-shadow: 0 0 0 3px rgba(15,157,88,.15); + display:inline-block; +} +.radius-scope .view-tabs { display:flex; gap:8px; flex-wrap:wrap; margin-bottom: 6px; } +.radius-scope .view-select-wrap { display: none; } + +@media (max-width: 800px) { + .radius-scope .view-tabs { display: none; } + .radius-scope .view-select-wrap { display: block; } +} + + +/* buttons */ +.radius-scope .tab-btn, .radius-scope .primary-btn, .radius-scope .ghost-btn, .radius-scope .icon-btn, .radius-scope .link-btn, .radius-scope .danger-btn { + appearance:none; outline:none; border:none; cursor:pointer; font-weight:700; letter-spacing:.2px; + transition: transform .12s ease, background .2s ease, border-color .2s ease, box-shadow .2s ease; +} +.radius-scope .tab-btn { + padding: 8px 12px; border-radius: var(--radius-pill); + background: #f4f7fb; color: var(--text); + border: 1px solid var(--border); +} +.radius-scope .tab-btn.active, .radius-scope .tab-btn:hover { + background: #eef6fb; border-color: #d6e8f5; box-shadow: 0 0 0 4px var(--ring); + transform: scale(0.98); +} +.radius-scope .primary-btn { + padding: 8px 14px; border-radius: var(--radius); + color:#fff; background: linear-gradient(135deg, var(--accent), var(--accent-2)); + box-shadow: 0 6px 18px rgba(0,83,132,.25); + height: 38px; + display: inline-flex; + align-items: center; + justify-content: center; +} +.radius-scope .primary-btn:disabled { opacity:.6; cursor:not-allowed; } +.radius-scope .ghost-btn { + padding: 8px 12px; border-radius: var(--radius); + color: var(--accent); background: #f8fbff; border:1px dashed #cfe4f3; +} +.radius-scope .danger-btn { + padding: 8px 12px; border-radius: var(--radius); + color: #c92a2a; background: #fff5f5; border: 1px dashed #ffc9c9; + opacity: .9; + transition: opacity .2s ease-in-out, transform .1s ease-in-out; +} +.radius-scope .danger-btn:hover { opacity: 1; } +.radius-scope .danger-btn:active { transform: scale(0.97); } + +.radius-scope .primary-btn:not(:disabled):hover, .radius-scope .ghost-btn:not(:disabled):hover, .radius-scope .danger-btn:not(:disabled):hover { + transform: translateY(-2px); +} +.radius-scope .primary-btn:not(:disabled):hover { + box-shadow: 0 8px 22px rgba(0,83,132,.3); +} + +.radius-scope .icon-btn { background: transparent; color: var(--muted); padding: 6px 8px; border-radius:8px; } +.radius-scope .icon-btn.sm { padding: 4px 6px; } +.radius-scope .icon-btn:hover { color: var(--text); background:#f2f6fa; } +.radius-scope .link-btn { background: transparent; color: var(--accent); text-decoration: underline; } + +/* inputs */ +.radius-scope .input-wrap { position: relative; } +.radius-scope .ri { + box-sizing: border-box; /* Add this line */ + width: 100%; + padding: 8px 38px 8px 36px; + border-radius: var(--radius); + border: 1px solid var(--border); + background: #fff; + color: var(--text); + transition: box-shadow .15s ease, border-color .15s ease, background .15s ease; +} +.radius-scope .ri:hover:not(:focus) { border-color: #c4d1de; } +.radius-scope .ri:focus { border-color: #bcd9ee; box-shadow: 0 0 0 5px var(--ring); outline: none; background: #fbfeff; } +.radius-scope .ri::placeholder{ color:#9aa6b2; } +.radius-scope .input-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color:#7997ad; font-size: 14px; pointer-events: none; } +.radius-scope .btn-clear { + position: absolute; right: 8px; top: 15%; transform: translateY(-50%); + width: 28px; height: 28px; border-radius: 8px; border: none; + background: #f3f7fa; color:#5a7891; /* Changed color from #7a8fa1 */ cursor: pointer; + transition: all .2s ease; opacity: 1; transform: scale(1); +} +.radius-scope .btn-clear:not(:disabled):hover { background:#e8f2f9; color:#2b5c7e; } +.radius-scope .btn-clear:disabled { background: transparent; color: #c1cbd5; cursor: not-allowed; transform: scale(0.9); opacity: 0.5; } +.radius-scope .btn-clear:disabled:hover { background: transparent; color: #c1cbd5; } + +.radius-scope .select select { width:100%; padding:10px 12px; border-radius: var(--radius); border:1px solid var(--border); background:#fff; -webkit-appearance: none; appearance: none; background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); background-position: right .5rem center; background-repeat: no-repeat; background-size: 1.5em 1.5em; padding-right: 2.5rem; } + + +/* iOS-like switch */ +.radius-scope .switch-field { display:flex; flex-direction:column; gap:6px;align-items: center } +.radius-scope .switch { display:inline-flex; align-items:center; cursor:pointer; user-select:none; } +.radius-scope .switch input { display:none; } +.radius-scope .switch .switch-track { + position: relative; width: 58px; height: 32px; border-radius: 999px; + background: #e8edf3; border: 1px solid #d7e1ea; display:inline-flex; align-items:center; justify-content:space-between; + padding: 0 8px; color:#7b8a98; transition: background .18s ease, border-color .18s ease, box-shadow .18s ease; +} +.radius-scope .switch .on { opacity: 0; transition: opacity .18s ease; } +.radius-scope .switch .off { opacity: 1; transition: opacity .18s ease; } +.radius-scope .switch .switch-track::after { + content: ""; position: absolute; top: 3px; left: 3px; width: 26px; height: 26px; background:#fff; border-radius: 50%; + box-shadow: 0 2px 6px rgba(0,0,0,.08); transition: transform .18s ease, box-shadow .18s ease; +} +.radius-scope .switch input:checked + .switch-track { + background: linear-gradient(135deg, var(--accent), var(--accent-2)); border-color: #a9d0ea; color:#fff; +} +.radius-scope .switch input:focus-visible + .switch-track { + box-shadow: 0 0 0 5px var(--ring); +} +.radius-scope .switch input:checked + .switch-track::after { transform: translateX(26px); } +.radius-scope .switch input:checked + .switch-track .on { opacity: 1; } +.radius-scope .switch input:checked + .switch-track .off { opacity: 0; } + +/* Autocomplete */ +.radius-scope .ac-root { position: relative; } +.radius-scope .ac-panel { + position: absolute; + left: 0; + min-width: 100%; + width: auto; + margin-top: 6px; + z-index: 20; + background: #fff; + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: var(--shadow); + padding: 8px; +} +.radius-scope .ac-panel.wide, +.radius-scope [data-wide="1"] .ac-panel { + left: -6px; + right: auto; /* Change this from -6px to auto */ +} +.radius-scope .ac-skel .skeleton-line { height: 12px; margin: 8px 0; } +.radius-scope .ac-empty { padding: 10px; } +.radius-scope .ac-list { list-style: none; margin: 0; padding: 0; max-height: 260px; overflow: auto; } +.radius-scope .ac-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: 8px; + cursor: pointer; + transition: transform .1s ease, background-color .1s ease; + white-space: nowrap; +} +.radius-scope .ac-item:hover, .radius-scope .ac-item.is-active { background:#f3f8fc; transform: scale(0.99); } +.radius-scope .ac-pop-enter-active, .radius-scope .ac-pop-leave-active { transition: opacity .12s ease, transform .12s ease; transform-origin: top center; } +.radius-scope .ac-pop-enter, .radius-scope .ac-pop-leave-to { opacity:0; transform: translateY(-4px) scale(.98); } + +/* Filters grid (labels optional, works fine without) */ +.radius-scope .filters { display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:12px; align-items:flex-end; } +.radius-scope .field label { display:block; margin: 0 0 6px; color: var(--muted); font-size: 12px; } + +/* tables */ +.radius-scope .table-wrap { overflow:auto; border-radius: 12px; border:1px solid var(--border); background: var(--card-2); max-height: 65vh; } +.radius-scope .table-wrap::-webkit-scrollbar { width: 8px; height: 8px; } +.radius-scope .table-wrap::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 10px; } +.radius-scope .table-wrap::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; border: 2px solid #f1f5f9; } +.radius-scope .table-wrap::-webkit-scrollbar-thumb:hover { background: #94a3b8; } +.radius-scope .tt-table { + width:100%; + min-width: 1000px; /* ADDED THIS LINE */ + border-collapse: collapse; + background: #fff; + table-layout: fixed; +} +.radius-scope .tt-table th, .radius-scope .tt-table td { padding: 10px 12px; border-bottom:1px solid #eef1f5; vertical-align: middle; } +.radius-scope .tt-table thead th { position:sticky; top:0; background:#f6f9fc; z-index:1; font-size:12px; color:#344054; text-transform:uppercase; letter-spacing:.04em; } +.radius-scope .tt-table.compact th, .radius-scope .tt-table.compact td { padding:8px 10px; } +.radius-scope .tt-table.ultra-compact th, .radius-scope .tt-table.ultra-compact td { padding:6px 8px; font-size:12px; } +.radius-scope .rows-enter-active, .radius-scope .rows-leave-active { transition: opacity .12s ease, transform .12s ease; } +.radius-scope .rows-enter, .radius-scope .rows-leave-to { opacity:0; transform: translateY(2px); } + +.radius-scope .results-container .table-wrap { border-radius: 12px 12px 0 0; border-bottom: none; } +.radius-scope .results-summary { padding: 8px 12px; border: 1px solid var(--border); border-top: none; background: #f6f9fc; font-size: 13px; color: var(--muted); border-radius: 0 0 12px 12px; min-height: 38px; display: flex; align-items: center; } + +/* table placeholder for no-input or no-results */ +.radius-scope .table-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 48px 24px; border: 1px solid var(--border); border-radius: 12px; background: var(--card-2); text-align: center; color: var(--muted); font-size: 16px; } +.radius-scope .table-placeholder i { font-size: 32px; color: #bdc8d8; } + + +/* row entrance */ +.radius-scope .row-fade-in { animation: rowIn .22s ease; } +@keyframes rowIn { from { opacity:0; transform: translateY(2px);} to {opacity:1; transform: none;} } + +/* skeletons */ +.radius-scope .skeleton-line { + --h: 12px; + height: var(--h); + border-radius: 8px; + background: linear-gradient(90deg, #eaeef3, #f3f6fa, #eaeef3); + background-size: 300% 100%; + animation: shimmer 1.1s infinite linear; +} +@keyframes shimmer { 0%{background-position:0% 0} 100%{background-position:100% 0} } + +/* button inline loader */ +.radius-scope .btn-loader { width: 18px; height: 18px; border: 2px solid #d5e7f4; border-top-color: var(--brand-blue); border-radius:50%; display:inline-block; animation: spin .9s linear infinite; } +@keyframes spin { to { transform: rotate(360deg);} } + +/* modal */ +.radius-scope .modal-overlay { position: fixed; inset:0; background: rgba(0,0,0,.25); display:flex; align-items:center; justify-content:center; padding: 20px; z-index: 1000; } +.radius-scope .modal-card { width:min(780px, 92vw); max-height: 88vh; overflow:auto; border-radius: 16px; border:1px solid var(--border); background: #fff; } +.radius-scope .modal-head { display:flex; align-items:center; justify-content:space-between; padding: 14px 16px; border-bottom:1px solid var(--border); position:sticky; top:0; background: #fff; z-index: 10; } +.radius-scope .modal-title { font-weight:800; } +.radius-scope .modal-body { padding: 14px 16px; } +.radius-scope .fade-enter-active, .radius-scope .fade-leave-active { transition: opacity .14s ease; } +.radius-scope .fade-enter, .radius-scope .fade-leave-to { opacity:0; } +.radius-scope .pop { animation: pop .16s ease; } +@keyframes pop { from { transform: scale(.98);} to { transform: none;} } + +/* key-value in modal (OLD) */ +.radius-scope .kv { display:grid; grid-template-columns: 180px 1fr; gap: 10px 16px; } +.radius-scope .kv > div { display: contents; } +.radius-scope .kv > div > span { color: var(--muted); } + +/* Redesigned Modal Key-Value */ +.radius-scope .kv-redesign { display: flex; flex-direction: column; } +.radius-scope .kv-redesign .kv-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 12px 4px; + border-bottom: 1px solid var(--border); + gap: 16px; +} +.radius-scope .kv-redesign .kv-row:last-child { border-bottom: none; } +.radius-scope .kv-redesign .kv-label { + color: var(--muted); + flex-shrink: 0; + width: 140px; +} +.radius-scope .kv-redesign .kv-value { + flex-grow: 1; + text-align: right; + word-break: break-all; +} +.radius-scope .kv-redesign .chip { display:inline-flex; align-items:center; gap:6px; padding:3px 10px; border-radius:999px; font-size:12px; border:1px solid var(--border); } +.radius-scope .kv-redesign .chip.ok { background: #eaf7ef; color:#206a42; border-color: #c9e6d8; } +.radius-scope .kv-redesign .chip.bad { background: #fdecec; color:#8a1d1d; border-color: #f6d2d2; } + +/* online state chip */ +.radius-scope .ros-wrap { min-height: 28px; display:flex; align-items:center; justify-content:flex-start; width: 170px; } +.radius-scope .ros-chip { display:flex; align-items:center; gap:8px; padding:4px 8px; border-radius: var(--radius); font-size:12px; font-family: var(--mono); border:1px solid var(--border); background:#fff; width: 100%; } +.radius-scope .ros-chip.is-clickable { cursor: pointer; transition: background-color .15s ease; } +.radius-scope .ros-chip.is-clickable:hover { background-color: #f3f8fc; } +.radius-scope .ros-chip.on { box-shadow: 0 0 0 3px rgba(15,157,88,.08); } +.radius-scope .ros-chip.off { box-shadow: 0 0 0 3px rgba(224,49,49,.08); } +.radius-scope .ros-chip .dot { width:8px; height:8px; border-radius:50%; background: currentColor; color: inherit; flex-shrink: 0; } +.radius-scope .ros-chip.on .dot { background: var(--ok); } +.radius-scope .ros-chip.off .dot { background: var(--bad); } +.radius-scope .ros-chip .ip { flex-grow: 1; text-align: center; } +.radius-scope .ros-chip.skeleton { background: #f8fafc; color: #d1d9e4; } + +/* ONT views shared styles */ +.radius-scope .ont-card .block { background: #fff; border:1px solid var(--border); border-radius: var(--radius); padding: 14px; box-shadow: var(--shadow); } +.radius-scope .ont-card .block + .block { margin-top: 12px; } +.radius-scope .block-head { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom: 10px; flex-wrap: wrap; } +.radius-scope .file-drop { display:block; border:1px dashed #cfe4f3; border-radius: var(--radius); padding: 30px; text-align:center; background:#f8fbff; cursor:pointer; } +.radius-scope .file-cta { display:flex; flex-direction:column; gap:8px; align-items:center; justify-content:center; color:#365972; } + +/* ONT Parser Loading Overlay */ +.radius-scope .overlay { + position:fixed; inset:0; background:rgba(255,255,255,.8); backdrop-filter: blur(4px); + display:flex; flex-direction:column; align-items:center; justify-content:center; + z-index: 50; text-align: center; +} +.radius-scope .ont-loading-card { + background: transparent; border: none; box-shadow: none; + width: min(520px, 90vw); padding: 16px; + color: var(--text); +} +.radius-scope .spinner-lg { width: 42px; height: 42px; border: 4px solid #dbe9f5; border-top-color: var(--brand-blue); border-radius: 50%; animation: spin .9s linear infinite; margin: 0 auto 10px; } +.radius-scope .progress-bar { height: 8px; background:#eef4f8; border-radius:999px; overflow:hidden; border:1px solid #e2ebf3; } +.radius-scope .progress-bar .bar { height:100%; width:0; background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width .2s ease; } +.radius-scope .progress-bar.is-yellow .bar { background: #f7c423; } + +.radius-scope .alert.error { padding:10px 12px; border-radius:10px; border:1px solid #ffd6d6; background:#fff3f3; color:#8a1d1d; } + +/* section entrance */ +.radius-scope .card-in { animation: cardIn .18s ease; } +@keyframes cardIn { from{ opacity:0; transform: translateY(4px);} to { opacity:1; transform: none;} } + +/* ===== NEW STYLES ===== */ + +/* Custom Animated Tooltip */ +[data-tooltip] { position: relative; - padding: 20px; - background: white; - border-radius: 4px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} +[data-tooltip]::before, [data-tooltip]::after { + position: absolute; + left: 50%; + transform: translateX(-50%); + opacity: 0; + pointer-events: none; + transition: all .18s ease-in-out; + z-index: 10; +} +[data-tooltip]::before { + content: ''; + bottom: calc(100% + 0px); + border: 5px solid transparent; + border-top-color: #0b1320; +} +[data-tooltip]::after { + content: attr(data-tooltip); + bottom: calc(100% + 5px); + padding: 4px 8px; + border-radius: 6px; + background: #0b1320; + color: #fff; + font-size: 12px; + font-weight: 500; + white-space: nowrap; +} +[data-tooltip]:hover::before, [data-tooltip]:hover::after { + opacity: 1; + transform: translateX(-50%) translateY(-4px); } -.progress { - height: 30px; +/* IP Input Focus Tooltip */ +.radius-scope .ip-field-wrapper { position: relative; } +.radius-scope .ip-focus-tooltip { + position: absolute; + bottom: calc(100% + 4px); + left: 0; + background: #f8fbff; + border: 1px solid #cfe4f3; + padding: 4px 8px; + border-radius: 6px; + font-size: 11px; + color: var(--accent); + white-space: nowrap; + opacity: 0; + transform: translateY(4px); + pointer-events: none; + transition: all .18s ease-in-out; } - -.progress-bar { - transition: width 0.3s ease; +.radius-scope .ip-field-wrapper:focus-within .ip-focus-tooltip { + opacity: 1; + transform: translateY(0); } \ No newline at end of file diff --git a/public/js/pages/Radius/Radius.js b/public/js/pages/Radius/Radius.js index 3dc6126a1..ee5f6bb15 100644 --- a/public/js/pages/Radius/Radius.js +++ b/public/js/pages/Radius/Radius.js @@ -1,823 +1,242 @@ -// Function to calculate similarity percentage between two strings +/* ===== Radius.js ===== + * Main entry point for the Radius module (light theme). + * Navigation jetzt innerhalb der Hauptkarte; Free-Tab lädt erst bei erstem Klick. + */ + +/* ---------- Shared utils (global) ---------- */ function calculateSimilarity(str1, str2) { - // Normalize strings by converting them to lowercase - str1 = str1.toLowerCase(); - str2 = str2.toLowerCase(); - - let matchCount = 0; - - // Check how many characters in str1 exist in str2 - for (let char of str1) { - if (str2.includes(char)) { - matchCount++; - } - } - - // Calculate similarity percentage - return (matchCount / str1.length) * 100; + if (!str1 || !str2) return 0; + str1 = ('' + str1).toLowerCase(); + str2 = ('' + str2).toLowerCase(); + let match = 0; + for (let c of str1) if (str2.includes(c)) match++; + return (match / str1.length) * 100; } - function validateData(strasse, plz, stadt, info) { - const thresholds = 90; // Similarity threshold in percentage - - // Validate each field against the info string - return !(calculateSimilarity(strasse, info) < thresholds || + const thresholds = 90; + return !( + calculateSimilarity(strasse, info) < thresholds || calculateSimilarity(plz, info) < thresholds || - calculateSimilarity(stadt, info) < thresholds); + calculateSimilarity(stadt, info) < thresholds + ); } - - -Vue.component('radius-ont-parser', { - template: ` -
-
-
-

Schritt 1: Excel (XLSX) Upload

- -
-
- -
-
-

Schritt 2: Spaltenzuordnung

-
-
-
- - -
-
-
- -
-
- - -
-
-

Schritt 3: Ergebnisse

- - - - - - - - - - - - - - - - - - -
ONT SN
{{ row[selectedColumns.kundennummer] }}{{ row[selectedColumns.anschlussstrasse] }}{{ row[selectedColumns.anschlussplz] }}{{ row[selectedColumns.anschlusscity] }}{{ row.ont_sn }}
- - -
-
- -
-
-
- {{ Math.round(progress) }}% -
-
-
Processing {{ currentRow + 1 }} of {{ totalRows }}
-
-
- `, - - data() { - return { - step: 1, - headers: [], - parsedData: [], - processedData: [], - selectedColumns: { - kundennummer: 'crmPartner', - anschlussstrasse: 'AnlStrasse', - anschlussplz: 'AnlPlz', - anschlusscity: 'AnlOrt' - }, - requiredFields: [ - { key: 'kundennummer', label: 'Kundennummer' }, - { key: 'anschlussstrasse', label: 'Anschlussstraße' }, - { key: 'anschlussplz', label: 'Anschluss PLZ' }, - { key: 'anschlusscity', label: 'Anschluss City' } - ], - loading: false, - progress: 0, - currentRow: 0, - totalRows: 0 - }; - }, - - methods: { - async handleFileUpload(event) { - const file = event.target.files[0]; - if (!file) return; - - // Load XLSX library dynamically - await this.loadXLSX(); - - const reader = new FileReader(); - reader.onload = (e) => { - const data = new Uint8Array(e.target.result); - const workbook = XLSX.read(data, { type: "array" }); - const worksheet = workbook.Sheets[workbook.SheetNames[0]]; - - // Read entire sheet as rows of arrays (no header detection yet) - const allRows = XLSX.utils.sheet_to_json(worksheet, { - header: 1, // Return rows as arrays - blankrows: false // Skip blank rows - }); - - // If there's only one row or something unexpected, do a basic parse - if (allRows.length < 2) { - const fallbackData = XLSX.utils.sheet_to_json(worksheet); - this.parsedData = fallbackData; - this.headers = Object.keys(fallbackData[0] || {}); - this.step = 2; - return; - } - - const firstRow = allRows[0] || []; - const secondRow = allRows[1] || []; - - // Count how many cells in each row are empty - const firstRowEmptyCount = firstRow.length - firstRow.filter(Boolean).length; - const secondRowEmptyCount = secondRow.length - secondRow.filter(Boolean).length; - - // If the difference in empty cells is more than 25% of the number of columns in the second row, - // assume the first row is mostly empty and use the second row as the header - const useSecondRowAsHeader = (firstRowEmptyCount - secondRowEmptyCount) > 0.25 * secondRow.length; - - // Now parse again with the correct header row - if (useSecondRowAsHeader) { - this.parsedData = XLSX.utils.sheet_to_json(worksheet, { - range: 1, // Start reading data after the first row - header: secondRow, // Use the second row as the header - defval: "" // Optional: fill empty cells with empty string - }).slice(1); // Skip the first row - this.headers = secondRow; - } else { - this.parsedData = XLSX.utils.sheet_to_json(worksheet, { - range: 0, // Start at the first row - header: firstRow, // Use the first row as the header - defval: "" - }); - this.headers = firstRow; - } - - this.step = 2; - }; - reader.readAsArrayBuffer(file); - }, - - - - async loadXLSX() { - if (!window.XLSX) { - await new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js'; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); - } - }, - - async startProcessing() { - this.loading = true; - this.totalRows = this.parsedData.length; - const processedRows = []; - - mainLoop: - for (let i = 0; i < this.parsedData.length; i++) { - this.currentRow = i; - this.progress = ((i + 1) / this.parsedData.length) * 100; - - // Simulate processing - await this.sleep(100); - - // Process row here - const row = this.parsedData[i]; - - const findUserResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?custnume=' + row[this.selectedColumns.kundennummer]); - const findUserData = await findUserResponse.json(); - - if (findUserData.length === 0) { - row.ont_sn = 'N/A - Kein Benutzer mit dieser Kundennummer gefunden'; - processedRows.push(row); - } else if (findUserData.length === 1) { - const username = findUserData[0].username; - const radacctResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?skipAdditional=true&action2=fetchRadacct&username=' + username); - const radacctData = await radacctResponse.json(); - - row.ont_sn = radacctData.ont_sn || 'N/A - Keine ONT SN gefunden'; - processedRows.push(row); - - } else if (findUserData.length > 1) { - // check string simulairty of strasse, plz, stadt and atleast of 90% of each should be inside findUserData[].info - // if not, ont_sn = N/A - Anschluss konnte nicht zugeordnet werden - - const strasse = row[this.selectedColumns.anschlussstrasse]; - const plz = row[this.selectedColumns.anschlussplz]; - const stadt = row[this.selectedColumns.anschlusscity]; - const info = findUserData[0].info; - - for (let user of findUserData) { - if (validateData(strasse, plz, stadt, info)) { - const username = user.username; - const radacctResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?skipAdditional=true&action2=fetchRadacct&username=' + username); - const radacctData = await radacctResponse.json(); - - row.ont_sn = radacctData.ont_sn || 'N/A - Keine ONT SN gefunden'; - processedRows.push(row); - continue mainLoop; - } - } - - row.ont_sn = 'N/A - Anschluss konnte nicht zugeordnet werden'; - processedRows.push(row); - } - } - - this.loading = false; - this.processedData = processedRows; - this.step = 3; - }, - downloadResults() { - const ws = XLSX.utils.json_to_sheet(this.processedData); - const wb = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(wb, ws, "Results"); - XLSX.writeFile(wb, "results.xlsx"); - }, - sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } +async function copyToClipboard(text) { + try { + await navigator.clipboard.writeText(text || ''); + return true; + } catch { + const ta = document.createElement('textarea'); + ta.value = text || ''; + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); ta.select(); + try { document.execCommand('copy'); } catch {} + document.body.removeChild(ta); + return false; } -}); +} +window.RadiusUtils = { calculateSimilarity, validateData, copyToClipboard }; +/* ---------- Online state chip (fetches radacct when visible) ---------- */ Vue.component('radius-online-state', { - props: ['username'], + props: { username: String }, + data: () => ({ data: null, observed: false, ob: null }), template: ` -
-