Warehouse project/fix
This commit is contained in:
@@ -16,7 +16,16 @@ class RadiusController extends mfBaseController {
|
|||||||
|
|
||||||
protected function indexAction() {
|
protected function indexAction() {
|
||||||
$this->layout()->set('additionalJS', ["plugins/chart.js/chart.4.4.6.js", "plugins/chart.js/chartjs-adapter-moment.min.js"]);
|
$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"), 'HIDE_PAGE_TITLE' => true]);
|
|
||||||
|
$allowedAcsUserIds = [9, 13, 25, 65, 135, 145, 178];
|
||||||
|
$acsEnabled = in_array($this->me->id, $allowedAcsUserIds);
|
||||||
|
|
||||||
|
Helper::renderVue($this, $this->mod, "Radius", [
|
||||||
|
'CAN_BILLING' => $this->me->can("Billing"),
|
||||||
|
'HIDE_PAGE_TITLE' => true,
|
||||||
|
'USER_ID' => $this->me->id,
|
||||||
|
'ACS_ENABLED' => $acsEnabled
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function proxyUnsecureHTTPRequestToRadiusAction() {
|
protected function proxyUnsecureHTTPRequestToRadiusAction() {
|
||||||
@@ -35,6 +44,49 @@ class RadiusController extends mfBaseController {
|
|||||||
die();
|
die();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run speedtest via ACS proxy
|
||||||
|
*/
|
||||||
|
protected function genieacsRunSpeedtestAction() {
|
||||||
|
try {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$ip = $input['ip'] ?? null;
|
||||||
|
|
||||||
|
if (!$ip) {
|
||||||
|
self::sendError("IP address is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = "http://acs.xinon.at:5000/run-speedtest";
|
||||||
|
$apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ";
|
||||||
|
|
||||||
|
$data = json_encode(['ip' => $ip]);
|
||||||
|
|
||||||
|
$opts = [
|
||||||
|
"http" => [
|
||||||
|
"method" => "POST",
|
||||||
|
"header" => "Content-Type: application/json\r\n" .
|
||||||
|
"X-API-Key: " . $apiKey . "\r\n" .
|
||||||
|
"Content-Length: " . strlen($data) . "\r\n",
|
||||||
|
"content" => $data
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$context = stream_context_create($opts);
|
||||||
|
$response = file_get_contents($url, false, $context);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
self::sendError("Failed to connect to speedtest server");
|
||||||
|
}
|
||||||
|
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
echo $response;
|
||||||
|
die();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("GenieACS runSpeedtest error: " . $e->getMessage());
|
||||||
|
self::sendError("Error running speedtest: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected function sendCustomerEmailAction() {
|
protected function sendCustomerEmailAction() {
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
if (!$input || !isset($input['username'], $input['year'], $input['month'], $input['monthlySummary'], $input['monthlyDetails'], $input['recipient']))
|
if (!$input || !isset($input['username'], $input['year'], $input['month'], $input['monthlySummary'], $input['monthlyDetails'], $input['recipient']))
|
||||||
|
|||||||
@@ -65,6 +65,81 @@ class WorkorderMphBaseController extends TTCrud
|
|||||||
self::returnJson(['docs' => $responseDocs, 'journals' => $journals]);
|
self::returnJson(['docs' => $responseDocs, 'journals' => $journals]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload documentation for the Workorder itself (not Wohneinheit).
|
||||||
|
*/
|
||||||
|
protected function uploadDocumentationAction()
|
||||||
|
{
|
||||||
|
if (empty($_FILES['files']) && empty($_FILES['file'])) self::sendError('Erforderliche Daten fehlen.');
|
||||||
|
if (empty($_POST['workorderMphId'])) self::sendError('Workorder ID fehlt.');
|
||||||
|
|
||||||
|
$workorderMphId = intval($_POST['workorderMphId']);
|
||||||
|
$uploadedCount = 0;
|
||||||
|
|
||||||
|
// Handle multiple files (files[])
|
||||||
|
if (!empty($_FILES['files'])) {
|
||||||
|
foreach ($_FILES['files']['name'] as $index => $name) {
|
||||||
|
if ($_FILES['files']['error'][$index] === UPLOAD_ERR_OK) {
|
||||||
|
// Mock the $_FILES entry for handleFormUpload
|
||||||
|
$_FILES['single_upload_file'] = [
|
||||||
|
'name' => $name,
|
||||||
|
'type' => $_FILES['files']['type'][$index],
|
||||||
|
'tmp_name' => $_FILES['files']['tmp_name'][$index],
|
||||||
|
'error' => $_FILES['files']['error'][$index],
|
||||||
|
'size' => $_FILES['files']['size'][$index]
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
$uploaded = mfUpload::handleFormUpload("single_upload_file", false, "/WorkorderMph");
|
||||||
|
WorkorderMphDocumentationModel::create([
|
||||||
|
'workorderMphId' => $workorderMphId,
|
||||||
|
'fileId' => $uploaded->id,
|
||||||
|
'description' => $_POST['description'] ?? '',
|
||||||
|
'documentType' => $_POST['documentType'] ?? 'other',
|
||||||
|
'create' => time(),
|
||||||
|
'createBy' => $this->user->id
|
||||||
|
]);
|
||||||
|
$uploadedCount++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Log error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle single file (file) - fallback or primary if JS sends single
|
||||||
|
elseif (!empty($_FILES['file'])) {
|
||||||
|
try {
|
||||||
|
$uploaded = mfUpload::handleFormUpload("file", false, "/WorkorderMph");
|
||||||
|
WorkorderMphDocumentationModel::create([
|
||||||
|
'workorderMphId' => $workorderMphId,
|
||||||
|
'fileId' => $uploaded->id,
|
||||||
|
'description' => $_POST['description'] ?? '',
|
||||||
|
'documentType' => $_POST['documentType'] ?? 'other',
|
||||||
|
'create' => time(),
|
||||||
|
'createBy' => $this->user->id
|
||||||
|
]);
|
||||||
|
$uploadedCount++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
self::sendError("Upload fehlgeschlagen: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($uploadedCount > 0) {
|
||||||
|
self::returnJson(['success' => true, 'message' => "$uploadedCount Datei(en) erfolgreich hochgeladen."]);
|
||||||
|
} else {
|
||||||
|
self::sendError("Keine Dateien wurden hochgeladen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete Workorder documentation
|
||||||
|
*/
|
||||||
|
protected function deleteDocumentationAction()
|
||||||
|
{
|
||||||
|
if (empty($this->postData['documentationId'])) self::sendError("Dokumentations-ID fehlt.");
|
||||||
|
WorkorderMphDocumentationModel::delete($this->postData['documentationId']);
|
||||||
|
self::returnJson(['success' => true, 'message' => 'Dokument gelöscht.']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new entry to a workorder's journal.
|
* Adds a new entry to a workorder's journal.
|
||||||
*/
|
*/
|
||||||
@@ -139,14 +214,15 @@ class WorkorderMphBaseController extends TTCrud
|
|||||||
$sql = "SELECT w.id, w.zusatz, w.tuer, w.contact, w.oaid, w.note, w.status_id, w.splice_hak_completed
|
$sql = "SELECT w.id, w.zusatz, w.tuer, w.contact, w.oaid, w.note, w.status_id, w.splice_hak_completed
|
||||||
FROM Wohneinheit w
|
FROM Wohneinheit w
|
||||||
WHERE w.hausnummer_id = $hausnummerId
|
WHERE w.hausnummer_id = $hausnummerId
|
||||||
ORDER BY w.zusatz";
|
ORDER BY w.oaid ASC";
|
||||||
$result = $db->query($sql);
|
$result = $db->query($sql);
|
||||||
$wohneinheiten = $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
|
$wohneinheiten = $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
|
||||||
|
|
||||||
// Get Preorders for this Hausnummer to fallback contact info
|
// Get Preorders for this Hausnummer to fallback contact info
|
||||||
$preorders = [];
|
$preorders = [];
|
||||||
if (class_exists('PreorderModel')) {
|
if (class_exists('PreorderModel')) {
|
||||||
$preorderList = PreorderModel::search(['adb_hausnummer_id' => $workorder->hausnummerId, 'deleted' => 0]);
|
// Use searchActive to filter out canceled preorders (status_code = 20)
|
||||||
|
$preorderList = PreorderModel::searchActive(['adb_hausnummer_id' => $workorder->hausnummerId]);
|
||||||
foreach ($preorderList as $preorder) {
|
foreach ($preorderList as $preorder) {
|
||||||
if ($preorder->adb_wohneinheit_id) {
|
if ($preorder->adb_wohneinheit_id) {
|
||||||
$preorders[$preorder->adb_wohneinheit_id] = $preorder;
|
$preorders[$preorder->adb_wohneinheit_id] = $preorder;
|
||||||
@@ -161,21 +237,30 @@ class WorkorderMphBaseController extends TTCrud
|
|||||||
$contact = $we['contact'];
|
$contact = $we['contact'];
|
||||||
$preorderContact = null;
|
$preorderContact = null;
|
||||||
$preorderUcode = null;
|
$preorderUcode = null;
|
||||||
|
|
||||||
if (isset($preorders[$we['id']])) {
|
if (isset($preorders[$we['id']])) {
|
||||||
$p = $preorders[$we['id']];
|
$p = $preorders[$we['id']];
|
||||||
$preorderUcode = $p->ucode;
|
$preorderUcode = $p->ucode;
|
||||||
$pContact = trim($p->firstname . ' ' . $p->lastname);
|
$pContact = trim($p->firstname . ' ' . $p->lastname);
|
||||||
if ($p->phone) $pContact .= ' (' . $p->phone . ')';
|
if ($p->phone) $pContact .= ' (' . $p->phone . ')';
|
||||||
|
|
||||||
$preorderContact = $pContact;
|
$preorderContact = $pContact;
|
||||||
|
|
||||||
// If address contact is empty, use preorder contact
|
// If address contact is empty, use preorder contact
|
||||||
if (empty($contact)) {
|
if (empty($contact)) {
|
||||||
$contact = $pContact;
|
$contact = $pContact;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get document count for this Wohneinheit
|
||||||
|
$docCountSql = "SELECT COUNT(*) as cnt FROM WohneinheitDocumentation WHERE wohneinheit_id = " . $db->escape($we['id']);
|
||||||
|
$docCountResult = $db->query($docCountSql);
|
||||||
|
$documentCount = 0;
|
||||||
|
if ($docCountResult) {
|
||||||
|
$docCountRow = $docCountResult->fetch_assoc();
|
||||||
|
$documentCount = intval($docCountRow['cnt']);
|
||||||
|
}
|
||||||
|
|
||||||
$response[] = [
|
$response[] = [
|
||||||
'wohneinheitId' => intval($we['id']),
|
'wohneinheitId' => intval($we['id']),
|
||||||
'zusatz' => $we['zusatz'],
|
'zusatz' => $we['zusatz'],
|
||||||
@@ -187,6 +272,7 @@ class WorkorderMphBaseController extends TTCrud
|
|||||||
'status' => intval($we['status_id']),
|
'status' => intval($we['status_id']),
|
||||||
'spliceCompleted' => intval($we['splice_hak_completed'] ?? 0),
|
'spliceCompleted' => intval($we['splice_hak_completed'] ?? 0),
|
||||||
'note' => $we['note'],
|
'note' => $we['note'],
|
||||||
|
'documentCount' => $documentCount,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +300,11 @@ class WorkorderMphBaseController extends TTCrud
|
|||||||
$tuer = $post['tuer'] ?? null;
|
$tuer = $post['tuer'] ?? null;
|
||||||
$zusatz = $post['zusatz'] ?? null;
|
$zusatz = $post['zusatz'] ?? null;
|
||||||
|
|
||||||
|
// Validate that "Tür" field is not empty if it's being set
|
||||||
|
if ($tuer !== null && trim($tuer) === '') {
|
||||||
|
self::sendError("Das Feld 'Tür' darf nicht leer sein.");
|
||||||
|
}
|
||||||
|
|
||||||
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
|
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
|
||||||
$escapedWohneinheitId = $db->escape($wohneinheitId);
|
$escapedWohneinheitId = $db->escape($wohneinheitId);
|
||||||
|
|
||||||
@@ -315,6 +406,93 @@ class WorkorderMphBaseController extends TTCrud
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get documents for a specific Wohneinheit
|
||||||
|
*/
|
||||||
|
protected function getWohneinheitDocumentsAction()
|
||||||
|
{
|
||||||
|
if (empty($this->request->wohneinheitId)) self::sendError("Wohneinheit-ID fehlt.");
|
||||||
|
|
||||||
|
$wohneinheitId = intval($this->request->wohneinheitId);
|
||||||
|
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM WohneinheitDocumentation WHERE wohneinheit_id = " . $db->escape($wohneinheitId) . " ORDER BY `create` ASC";
|
||||||
|
$result = $db->query($sql);
|
||||||
|
$docs = $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
|
||||||
|
|
||||||
|
$responseDocs = [];
|
||||||
|
foreach ($docs as $doc) {
|
||||||
|
$file = new File($doc['fileId']);
|
||||||
|
$responseDocs[] = [
|
||||||
|
'id' => $doc['id'],
|
||||||
|
'fileId' => $doc['fileId'],
|
||||||
|
'fileName' => $file->orig_filename ?? $file->filename,
|
||||||
|
'description' => $doc['description'],
|
||||||
|
'documentType' => $doc['documentType'],
|
||||||
|
'userName' => UserModel::getOne($doc['createBy'])->name ?? 'Unbekannt',
|
||||||
|
'mimetype' => $file->mimetype ?? 'application/octet-stream',
|
||||||
|
'create' => $doc['create']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
self::returnJson(['docs' => $responseDocs]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload document for a specific Wohneinheit
|
||||||
|
*/
|
||||||
|
protected function uploadWohneinheitDocumentAction()
|
||||||
|
{
|
||||||
|
if (empty($_FILES['file']) || empty($_POST['wohneinheitId'])) {
|
||||||
|
self::sendError("Datei und Wohneinheit-ID sind erforderlich.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$wohneinheitId = intval($_POST['wohneinheitId']);
|
||||||
|
$documentType = $_POST['documentType'] ?? 'photo';
|
||||||
|
$description = $_POST['description'] ?? null;
|
||||||
|
|
||||||
|
// Upload file using mfUpload handleFormUpload for proper handling
|
||||||
|
try {
|
||||||
|
$upload = mfUpload::handleFormUpload("file", false, "/WorkorderMph/Wohneinheit");
|
||||||
|
$file = $upload; // handleFormUpload returns the File object
|
||||||
|
} catch (Exception $e) {
|
||||||
|
self::sendError("Datei-Upload fehlgeschlagen: " . $e->getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert into WohneinheitDocumentation table in addressdb
|
||||||
|
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
|
||||||
|
$escapedWohneinheitId = $db->escape($wohneinheitId);
|
||||||
|
$escapedFileId = $db->escape($file->id);
|
||||||
|
$escapedDescription = $description ? "'" . $db->escape($description) . "'" : "NULL";
|
||||||
|
$escapedDocumentType = "'" . $db->escape($documentType) . "'";
|
||||||
|
$escapedCreateBy = $db->escape($this->user->id);
|
||||||
|
$escapedCreate = time();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO WohneinheitDocumentation (wohneinheit_id, fileId, description, documentType, `create`, createBy)
|
||||||
|
VALUES ($escapedWohneinheitId, $escapedFileId, $escapedDescription, $escapedDocumentType, $escapedCreate, $escapedCreateBy)";
|
||||||
|
$db->query($sql);
|
||||||
|
|
||||||
|
self::returnJson(['success' => true, 'message' => 'Dokument erfolgreich hochgeladen.', 'fileId' => $file->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete document for a specific Wohneinheit
|
||||||
|
*/
|
||||||
|
protected function deleteWohneinheitDocumentAction()
|
||||||
|
{
|
||||||
|
if (empty($this->postData['documentationId'])) self::sendError("Dokumentations-ID fehlt.");
|
||||||
|
|
||||||
|
$documentationId = intval($this->postData['documentationId']);
|
||||||
|
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
|
||||||
|
|
||||||
|
$escapedId = $db->escape($documentationId);
|
||||||
|
$sql = "DELETE FROM WohneinheitDocumentation WHERE id = $escapedId";
|
||||||
|
$db->query($sql);
|
||||||
|
|
||||||
|
self::returnJson(['success' => true, 'message' => 'Dokument gelöscht.']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update checkbox documentation fields
|
* Update checkbox documentation fields
|
||||||
*/
|
*/
|
||||||
@@ -327,17 +505,25 @@ class WorkorderMphBaseController extends TTCrud
|
|||||||
if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden.");
|
if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden.");
|
||||||
|
|
||||||
$changes = [];
|
$changes = [];
|
||||||
$checkboxFields = ['easement', 'btb', 'fttxLocationSupplied', 'conduitToHuepLaid', 'huepMounted', 'dropCableAvailable', 'spliceCompleted'];
|
$checkboxFields = [
|
||||||
|
'easement' => 'Leitungsrecht',
|
||||||
|
'btb' => 'Bautechnische Begehung',
|
||||||
|
'fttxLocationSupplied' => 'FTTx Location mit Leerrohr versorgt',
|
||||||
|
'conduitToHuepLaid' => 'Leerrohr bis HÜP/HAK verlegt',
|
||||||
|
'huepMounted' => 'HÜP/HAK montiert',
|
||||||
|
'dropCableAvailable' => 'Dropkabel vorhanden',
|
||||||
|
'spliceCompleted' => 'Spleiß abgeschlossen'
|
||||||
|
];
|
||||||
|
|
||||||
$updateHausnummerStatus = false;
|
$updateHausnummerStatus = false;
|
||||||
|
|
||||||
foreach ($checkboxFields as $field) {
|
foreach ($checkboxFields as $field => $fieldLabel) {
|
||||||
if (array_key_exists($field, $post)) {
|
if (array_key_exists($field, $post)) {
|
||||||
$oldValue = $workorder->$field;
|
$oldValue = $workorder->$field;
|
||||||
$newValue = $post[$field] ? 1 : 0;
|
$newValue = $post[$field] ? 1 : 0;
|
||||||
if ($oldValue !== $newValue) {
|
if ($oldValue !== $newValue) {
|
||||||
$workorder->$field = $newValue;
|
$workorder->$field = $newValue;
|
||||||
$changes[] = "$field: " . ($newValue ? 'ja' : 'nein');
|
$changes[] = "$fieldLabel: " . ($newValue ? 'ja' : 'nein');
|
||||||
|
|
||||||
// Check for FTTx Location mit Leerrohr versorgt
|
// Check for FTTx Location mit Leerrohr versorgt
|
||||||
if ($field === 'fttxLocationSupplied' && $newValue === 1) {
|
if ($field === 'fttxLocationSupplied' && $newValue === 1) {
|
||||||
@@ -374,4 +560,4 @@ class WorkorderMphBaseController extends TTCrud
|
|||||||
self::returnJson(['success' => true, 'message' => 'Dokumentation aktualisiert.']);
|
self::returnJson(['success' => true, 'message' => 'Dokumentation aktualisiert.']);
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddWohneinheitDocumentation extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// AddressDB modifications - add documentation table for Wohneinheit
|
||||||
|
if ($this->getEnvironment() == "addressdb") {
|
||||||
|
$table = $this->table('WohneinheitDocumentation', ['id' => 'id', 'primary_key' => 'id']);
|
||||||
|
if (!$table->exists()) {
|
||||||
|
$table
|
||||||
|
->addColumn('wohneinheit_id', 'integer', ['null' => false])
|
||||||
|
->addColumn('fileId', 'integer', ['null' => false])
|
||||||
|
->addColumn('description', 'text', ['null' => true])
|
||||||
|
->addColumn('documentType', 'string', ['limit' => 100, 'null' => false, 'default' => 'photo'])
|
||||||
|
->addColumn('create', 'integer', ['null' => false])
|
||||||
|
->addColumn('createBy', 'integer', ['null' => false])
|
||||||
|
->addIndex(['wohneinheit_id'], ['name' => 'wohneinheit_id_idx'])
|
||||||
|
->addIndex(['fileId'], ['name' => 'fileId_idx'])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// AddressDB modifications
|
||||||
|
if ($this->getEnvironment() == "addressdb") {
|
||||||
|
$table = $this->table('WohneinheitDocumentation');
|
||||||
|
if ($table->exists()) {
|
||||||
|
$table->drop()->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,10 +16,12 @@
|
|||||||
.radius-scope .inline-copy { display:flex; align-items:center; gap:8px; justify-content: flex-end; }
|
.radius-scope .inline-copy { display:flex; align-items:center; gap:8px; justify-content: flex-end; }
|
||||||
.radius-scope .grid { display:grid; }
|
.radius-scope .grid { display:grid; }
|
||||||
.radius-scope .g-2 { gap: 8px; }
|
.radius-scope .g-2 { gap: 8px; }
|
||||||
|
.radius-scope .g-3 { gap: 12px; }
|
||||||
.radius-scope .g-4 { gap: 16px; }
|
.radius-scope .g-4 { gap: 16px; }
|
||||||
.radius-scope .g-6 { gap: 24px; }
|
.radius-scope .g-6 { gap: 24px; }
|
||||||
.radius-scope .cols-1 { grid-template-columns: 1fr; }
|
.radius-scope .cols-1 { grid-template-columns: 1fr; }
|
||||||
.radius-scope .cols-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
.radius-scope .cols-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||||
|
.radius-scope .cols-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
|
||||||
.radius-scope .cols-4 { grid-template-columns: repeat(4, minmax(0,1fr)); }
|
.radius-scope .cols-4 { grid-template-columns: repeat(4, minmax(0,1fr)); }
|
||||||
@media (max-width: 900px){ .radius-scope .cols-4 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
|
@media (max-width: 900px){ .radius-scope .cols-4 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
|
||||||
@media (max-width: 600px){ .radius-scope .cols-4 { grid-template-columns: 1fr; } }
|
@media (max-width: 600px){ .radius-scope .cols-4 { grid-template-columns: 1fr; } }
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ Vue.component('radius-users', {
|
|||||||
<th style="text-align: center; width: 183px;">Username</th>
|
<th style="text-align: center; width: 183px;">Username</th>
|
||||||
<th style="text-align: center">Info</th>
|
<th style="text-align: center">Info</th>
|
||||||
<th style="text-align: center; width: 190px;">Status</th>
|
<th style="text-align: center; width: 190px;">Status</th>
|
||||||
<th style="text-align: center; width: 115px;">Aktionen</th>
|
<th style="text-align: center; width: 180px;">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</template>
|
</template>
|
||||||
@@ -105,8 +105,8 @@ Vue.component('radius-users', {
|
|||||||
class="fa-duotone fa-circle-info"></i></button>
|
class="fa-duotone fa-circle-info"></i></button>
|
||||||
<button class="ghost-btn" @click="openTransferModal(item.username)" data-tooltip="Transfer Statistik"
|
<button class="ghost-btn" @click="openTransferModal(item.username)" data-tooltip="Transfer Statistik"
|
||||||
data-tooltip-align="left"><i class="fa-duotone fa-chart-line"></i></button>
|
data-tooltip-align="left"><i class="fa-duotone fa-chart-line"></i></button>
|
||||||
<!-- <button class="ghost-btn" @click="openRouterManagement(item)" data-tooltip="Router Management"-->
|
<button v-if="window.TT_CONFIG.ACS_ENABLED" class="ghost-btn" @click="openRouterManagement(item)" data-tooltip="Router Management"
|
||||||
<!-- data-tooltip-align="left"><i class="fa-duotone fa-router"></i></button>-->
|
data-tooltip-align="left"><i class="fa-duotone fa-router"></i></button>
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
<template #observer>
|
<template #observer>
|
||||||
@@ -395,6 +395,7 @@ Vue.component('radius-users', {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</radius-modal>
|
</radius-modal>
|
||||||
|
|
||||||
<radius-modal :show="showRouterModal" :title="'Router Management - ' + (routerData.username || '')" @close="closeRouterModal" modal-class="modal-card-wide">
|
<radius-modal :show="showRouterModal" :title="'Router Management - ' + (routerData.username || '')" @close="closeRouterModal" modal-class="modal-card-wide">
|
||||||
<div class="modal-body-scrollable">
|
<div class="modal-body-scrollable">
|
||||||
<div v-if="routerLoading" class="table-placeholder" style="min-height: 300px;">
|
<div v-if="routerLoading" class="table-placeholder" style="min-height: 300px;">
|
||||||
@@ -406,79 +407,159 @@ Vue.component('radius-users', {
|
|||||||
<div style="margin-top: 16px;">Kein Router mit dieser IP gefunden</div>
|
<div style="margin-top: 16px;">Kein Router mit dieser IP gefunden</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="kv-redesign">
|
<div style="padding: 8px;">
|
||||||
<div class="kv-row"><span class="kv-label">Hersteller</span><code class="kv-value">{{ routerDevice.deviceInfo.manufacturer || '—' }}</code></div>
|
<div class="kv-redesign grid g-2 cols-2">
|
||||||
<div class="kv-row"><span class="kv-label">Modell</span><code class="kv-value">{{ routerDevice.deviceInfo.productClass || '—' }}</code></div>
|
<div class="kv-row"><span class="kv-label">Hardware Version</span><code class="kv-value">{{ routerDevice.deviceInfo.hardwareVersion || '—' }}</code></div>
|
||||||
<div class="kv-row"><span class="kv-label">Hardware Version</span><code class="kv-value">{{ routerDevice.deviceInfo.hardwareVersion || '—' }}</code></div>
|
<div class="kv-row"><span class="kv-label">Software Version</span><code class="kv-value">{{ routerDevice.deviceInfo.softwareVersion || '—' }}</code></div>
|
||||||
<div class="kv-row"><span class="kv-label">Software Version</span><code class="kv-value">{{ routerDevice.deviceInfo.softwareVersion || '—' }}</code></div>
|
<div class="kv-row full-width"><span class="kv-label">Seriennummer</span><code class="kv-value">{{ routerDevice.deviceInfo.serialNumber || '—' }}</code></div>
|
||||||
<div class="kv-row"><span class="kv-label">Seriennummer</span><code class="kv-value">{{ routerDevice.deviceInfo.serialNumber || '—' }}</code></div>
|
<div class="kv-row"><span class="kv-label">Device ID</span>
|
||||||
<div class="kv-row"><span class="kv-label">Device ID</span>
|
<div class="kv-value inline-copy">
|
||||||
<div class="kv-value inline-copy">
|
<code>{{ routerDevice.deviceId || '—' }}</code>
|
||||||
<code>{{ routerDevice.deviceId || '—' }}</code>
|
<button v-if="routerDevice.deviceId" class="icon-btn sm" @click="copy(routerDevice.deviceId, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
||||||
<button v-if="routerDevice.deviceId" class="icon-btn sm" @click="copy(routerDevice.deviceId, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kv-row"><span class="kv-label">Externe IP</span>
|
||||||
|
<div class="kv-value inline-copy">
|
||||||
|
<code>{{ routerDevice.ip || '—' }}</code>
|
||||||
|
<button v-if="routerDevice.ip" class="icon-btn sm" @click="copy(routerDevice.ip, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kv-row"><span class="kv-label">Management IP</span>
|
||||||
|
<div class="kv-value inline-copy">
|
||||||
|
<code>{{ routerDevice.managementIp || '—' }}</code>
|
||||||
|
<button v-if="routerDevice.managementIp" class="icon-btn sm" @click="copy(routerDevice.managementIp, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="kv-row"><span class="kv-label">Externe IP</span>
|
|
||||||
<div class="kv-value inline-copy">
|
|
||||||
<code>{{ routerDevice.ip || '—' }}</code>
|
|
||||||
<button v-if="routerDevice.ip" class="icon-btn sm" @click="copy(routerDevice.ip, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="kv-row"><span class="kv-label">Management IP</span>
|
|
||||||
<div class="kv-value inline-copy">
|
|
||||||
<code>{{ routerDevice.managementIp || '—' }}</code>
|
|
||||||
<button v-if="routerDevice.managementIp" class="icon-btn sm" @click="copy(routerDevice.managementIp, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="kv-row"><span class="kv-label">Management Username</span>
|
|
||||||
<div class="kv-value inline-copy">
|
|
||||||
<code v-if="!loadingUsername">{{ managementUsername || '—' }}</code>
|
|
||||||
<div v-else class="skeleton-line" style="width: 100px; height: 16px;"></div>
|
|
||||||
<button v-if="managementUsername && !loadingUsername" class="icon-btn sm" @click="copy(managementUsername, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
|
||||||
<button class="icon-btn sm" @click="fetchManagementUsername" :disabled="loadingUsername" data-tooltip="Aktualisieren" data-tooltip-align="left">
|
|
||||||
<i class="fa-duotone fa-arrows-rotate" :class="{'fa-spin': loadingUsername}"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<h4 style="margin-bottom: 12px; font-size: 14px; font-weight: 600;">Router Aktionen</h4>
|
<h4 style="margin-bottom: 12px; font-size: 14px; font-weight: 600;">Router Aktionen</h4>
|
||||||
<div class="grid g-3 cols-3">
|
<div class="grid g-2 cols-3">
|
||||||
<button class="ghost-btn" @click="refreshRouter" :disabled="routerActionLoading" style="padding: 12px;">
|
<button class="ghost-btn" @click="runRemoteAccess" :disabled="routerActionLoading || speedtestLoading">
|
||||||
<i class="fa-duotone fa-arrows-rotate"></i> Aktualisieren
|
<i class="fa-duotone fa-key"></i> Remote-Zugriff
|
||||||
</button>
|
</button>
|
||||||
<button class="ghost-btn" @click="rebootRouter" :disabled="routerActionLoading" style="padding: 12px;">
|
<button class="ghost-btn" @click="refreshRouter" :disabled="routerActionLoading || speedtestLoading">
|
||||||
<i class="fa-duotone fa-power-off"></i> Neustart
|
<i class="fa-duotone fa-arrows-rotate"></i> Aktualisieren
|
||||||
</button>
|
</button>
|
||||||
<button class="ghost-btn" @click="pingRouter" :disabled="routerActionLoading" style="padding: 12px;">
|
<button class="ghost-btn" @click="rebootRouter" :disabled="routerActionLoading || speedtestLoading">
|
||||||
<i class="fa-duotone fa-signal-bars"></i> Ping
|
<i class="fa-duotone fa-power-off"></i> Neustart
|
||||||
</button>
|
</button>
|
||||||
<button class="danger-btn" @click="confirmFactoryReset" :disabled="routerActionLoading" style="padding: 12px;">
|
<button class="ghost-btn" @click="pingRouter" :disabled="routerActionLoading || speedtestLoading">
|
||||||
<i class="fa-duotone fa-triangle-exclamation"></i> Factory Reset
|
<i class="fa-duotone fa-signal-bars"></i> Ping
|
||||||
</button>
|
</button>
|
||||||
<button class="ghost-btn" @click="showParameterModal = true" :disabled="routerActionLoading" style="padding: 12px;">
|
<button class="ghost-btn" @click="showParameterModal = true" :disabled="routerActionLoading || speedtestLoading">
|
||||||
<i class="fa-duotone fa-sliders"></i> Parameter lesen
|
<i class="fa-duotone fa-sliders"></i> Parameter lesen
|
||||||
</button>
|
</button>
|
||||||
<button class="ghost-btn" @click="showSetParameterModal = true" :disabled="routerActionLoading" style="padding: 12px;">
|
<button class="ghost-btn" @click="showSetParameterModal = true" :disabled="routerActionLoading || speedtestLoading">
|
||||||
<i class="fa-duotone fa-pen-to-square"></i> Parameter setzen
|
<i class="fa-duotone fa-pen-to-square"></i> Parameter setzen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button class="ghost-btn" @click="runSpeedtest" :disabled="routerActionLoading || speedtestLoading">
|
||||||
</div>
|
<i class="fa-duotone fa-gauge-high"></i> Speedtest
|
||||||
|
</button>
|
||||||
<div v-if="pingResult" class="mt-3">
|
<button class="danger-btn" @click="confirmFactoryReset" :disabled="routerActionLoading || speedtestLoading">
|
||||||
<h4 style="margin-bottom: 12px; font-size: 14px; font-weight: 600;">Ping Ergebnis</h4>
|
<i class="fa-duotone fa-triangle-exclamation"></i> Factory Reset
|
||||||
<div class="kv-redesign">
|
</button>
|
||||||
<div class="kv-row"><span class="kv-label">Pakete gesendet</span><code class="kv-value">{{ pingResult.packetsTransmitted }}</code></div>
|
</div>
|
||||||
<div class="kv-row"><span class="kv-label">Pakete empfangen</span><code class="kv-value">{{ pingResult.packetsReceived }}</code></div>
|
</div>
|
||||||
<div class="kv-row"><span class="kv-label">Paketverlust</span><code class="kv-value">{{ pingResult.packetLoss }}%</code></div>
|
|
||||||
<div class="kv-row"><span class="kv-label">Min / Avg / Max</span><code class="kv-value">{{ pingResult.min }} / {{ pingResult.avg }} / {{ pingResult.max }} ms</code></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</radius-modal>
|
</radius-modal>
|
||||||
|
|
||||||
|
<radius-modal :show="showPingModal" title="Ping Ergebnis" @close="showPingModal = false">
|
||||||
|
<div v-if="routerActionLoading && !pingResult" class="table-placeholder" style="height: 150px;">
|
||||||
|
<div class="btn-loader" style="width: 40px; height: 40px;"></div>
|
||||||
|
<div class="mt-3">Ping läuft...</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="pingResult">
|
||||||
|
<div class="kv-redesign">
|
||||||
|
<div class="kv-row"><span class="kv-label">Gesendet</span><code class="kv-value">{{ pingResult.packetsTransmitted }}</code></div>
|
||||||
|
<div class="kv-row"><span class="kv-label">Empfangen</span><code class="kv-value">{{ pingResult.packetsReceived }}</code></div>
|
||||||
|
<div class="kv-row"><span class="kv-label">Verlust</span><code class="kv-value">{{ pingResult.packetLoss }}%</code></div>
|
||||||
|
<div class="kv-row"><span class="kv-label">Min / Avg / Max</span><code class="kv-value">{{ pingResult.min }} / {{ pingResult.avg }} / {{ pingResult.max }} ms</code></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="table-placeholder" style="height: 150px;">
|
||||||
|
Kein Ergebnis.
|
||||||
|
</div>
|
||||||
|
</radius-modal>
|
||||||
|
|
||||||
|
<radius-modal :show="showSpeedtestModal" title="Speedtest Ergebnis" @close="showSpeedtestModal = false" modal-class="modal-card-wide">
|
||||||
|
<div v-if="speedtestLoading && speedtestHistory.length === 0" class="table-placeholder" style="height: 200px;">
|
||||||
|
<div class="btn-loader" style="width: 40px; height: 40px;"></div>
|
||||||
|
<div class="mt-3">Speedtest wird initialisiert...</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="table-wrap" style="max-height: 500px; overflow-y: auto;">
|
||||||
|
<table class="tt-table compact">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 60px;">#</th>
|
||||||
|
<th style="text-align: right">Bandbreite</th>
|
||||||
|
<th style="text-align: right">Übertragen</th>
|
||||||
|
<th style="text-align: right">Pakete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, idx) in speedtestHistory" :key="idx">
|
||||||
|
<td class="mono small">{{ idx + 1 }}</td>
|
||||||
|
<td class="mono small" style="text-align: right">{{ row.bpsFormatted }}</td>
|
||||||
|
<td class="mono small" style="text-align: right">{{ row.bytesFormatted }}</td>
|
||||||
|
<td class="mono small" style="text-align: right">{{ row.packets }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div ref="speedtestBottom"></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="speedtestLoading" class="center mt-3 muted small"><i class="fa-duotone fa-spinner fa-spin"></i> Aktualisiere...</div>
|
||||||
|
<div v-else class="center mt-3" style="color: var(--ok);"><i class="fa-duotone fa-check-circle"></i> Abgeschlossen</div>
|
||||||
|
</div>
|
||||||
|
</radius-modal>
|
||||||
|
|
||||||
|
<radius-modal :show="showRemoteAccessModal" title="Remote Zugriff Konfiguration" @close="showRemoteAccessModal = false">
|
||||||
|
<div v-if="remoteAccessLoading" class="table-placeholder" style="height: 200px;">
|
||||||
|
<div class="btn-loader" style="width: 40px; height: 40px;"></div>
|
||||||
|
<div class="mt-3">{{ remoteAccessStep }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="remoteAccessResult">
|
||||||
|
<div class="alert ok mb-4" style="background-color: #eaf7ef; border: 1px solid #c9e6d8; color: #206a42; padding: 12px; border-radius: 8px;">
|
||||||
|
<i class="fa-duotone fa-check-circle"></i> Konfiguration erfolgreich abgeschlossen.
|
||||||
|
</div>
|
||||||
|
<div class="kv-redesign">
|
||||||
|
<div class="kv-row">
|
||||||
|
<span class="kv-label">Remote Link</span>
|
||||||
|
<div class="kv-value inline-copy">
|
||||||
|
<a :href="remoteAccessResult.link" target="_blank" class="link">{{ remoteAccessResult.link }}</a>
|
||||||
|
<button class="icon-btn sm" @click="copy(remoteAccessResult.link, $event)" data-tooltip="Kopieren">
|
||||||
|
<i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kv-row">
|
||||||
|
<span class="kv-label">Username</span>
|
||||||
|
<div class="kv-value inline-copy">
|
||||||
|
<code class="mono">{{ remoteAccessResult.username }}</code>
|
||||||
|
<button class="icon-btn sm" @click="copy(remoteAccessResult.username, $event)" data-tooltip="Kopieren">
|
||||||
|
<i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kv-row">
|
||||||
|
<span class="kv-label">Password</span>
|
||||||
|
<div class="kv-value inline-copy">
|
||||||
|
<code class="mono">{{ remoteAccessResult.password }}</code>
|
||||||
|
<button class="icon-btn sm" @click="copy(remoteAccessResult.password, $event)" data-tooltip="Kopieren">
|
||||||
|
<i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="table-placeholder" style="height: 200px;">
|
||||||
|
Ein Fehler ist aufgetreten.
|
||||||
|
</div>
|
||||||
|
</radius-modal>
|
||||||
|
|
||||||
<radius-modal :show="showParameterModal" title="Parameter lesen" @close="showParameterModal = false">
|
<radius-modal :show="showParameterModal" title="Parameter lesen" @close="showParameterModal = false">
|
||||||
<div>
|
<div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -564,6 +645,16 @@ Vue.component('radius-users', {
|
|||||||
routerData: {},
|
routerData: {},
|
||||||
routerDevice: null,
|
routerDevice: null,
|
||||||
pingResult: null,
|
pingResult: null,
|
||||||
|
speedtestLoading: false,
|
||||||
|
speedtestResult: null,
|
||||||
|
speedtestHistory: [],
|
||||||
|
speedtestHasStarted: false,
|
||||||
|
showPingModal: false,
|
||||||
|
showSpeedtestModal: false,
|
||||||
|
showRemoteAccessModal: false,
|
||||||
|
remoteAccessLoading: false,
|
||||||
|
remoteAccessResult: null,
|
||||||
|
remoteAccessStep: '',
|
||||||
showParameterModal: false,
|
showParameterModal: false,
|
||||||
parameterName: '',
|
parameterName: '',
|
||||||
showSetParameterModal: false,
|
showSetParameterModal: false,
|
||||||
@@ -972,6 +1063,8 @@ Vue.component('radius-users', {
|
|||||||
this.routerData = item;
|
this.routerData = item;
|
||||||
this.routerDevice = null;
|
this.routerDevice = null;
|
||||||
this.pingResult = null;
|
this.pingResult = null;
|
||||||
|
this.speedtestResult = null;
|
||||||
|
this.speedtestLoading = false;
|
||||||
this.managementUsername = '';
|
this.managementUsername = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -986,8 +1079,7 @@ Vue.component('radius-users', {
|
|||||||
const deviceData = await deviceResponse.json();
|
const deviceData = await deviceResponse.json();
|
||||||
if (deviceData.success) {
|
if (deviceData.success) {
|
||||||
this.routerDevice = deviceData;
|
this.routerDevice = deviceData;
|
||||||
// Automatically fetch management username
|
// Management Username removed
|
||||||
await this.fetchManagementUsername();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1004,6 +1096,8 @@ Vue.component('radius-users', {
|
|||||||
this.routerData = {};
|
this.routerData = {};
|
||||||
this.routerDevice = null;
|
this.routerDevice = null;
|
||||||
this.pingResult = null;
|
this.pingResult = null;
|
||||||
|
this.speedtestResult = null;
|
||||||
|
this.speedtestLoading = false;
|
||||||
this.showParameterModal = false;
|
this.showParameterModal = false;
|
||||||
this.showSetParameterModal = false;
|
this.showSetParameterModal = false;
|
||||||
this.parameterName = '';
|
this.parameterName = '';
|
||||||
@@ -1070,6 +1164,7 @@ Vue.component('radius-users', {
|
|||||||
const pingIp = this.routerDevice.managementIp || this.routerDevice.ip;
|
const pingIp = this.routerDevice.managementIp || this.routerDevice.ip;
|
||||||
if (!pingIp) return;
|
if (!pingIp) return;
|
||||||
|
|
||||||
|
this.showPingModal = true; // Open modal immediately
|
||||||
this.routerActionLoading = true;
|
this.routerActionLoading = true;
|
||||||
this.pingResult = null;
|
this.pingResult = null;
|
||||||
try {
|
try {
|
||||||
@@ -1151,14 +1246,9 @@ Vue.component('radius-users', {
|
|||||||
}
|
}
|
||||||
this.routerActionLoading = false;
|
this.routerActionLoading = false;
|
||||||
},
|
},
|
||||||
async setParameter() {
|
async setParameterValues(parameters) {
|
||||||
if (!this.routerDevice || !this.routerDevice.deviceId || !this.setParameterName || !this.setParameterValue) return;
|
if (!this.routerDevice || !this.routerDevice.deviceId || !parameters) return false;
|
||||||
|
|
||||||
this.routerActionLoading = true;
|
|
||||||
try {
|
try {
|
||||||
const parameters = {};
|
|
||||||
parameters[this.setParameterName] = this.setParameterValue;
|
|
||||||
|
|
||||||
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsSetParameters`, {
|
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsSetParameters`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
@@ -1167,73 +1257,272 @@ Vue.component('radius-users', {
|
|||||||
parameters: parameters
|
parameters: parameters
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
return response.ok && (await response.json()).success;
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
window.notify('success', 'Parameter erfolgreich gesetzt');
|
|
||||||
this.showSetParameterModal = false;
|
|
||||||
this.setParameterName = '';
|
|
||||||
this.setParameterValue = '';
|
|
||||||
} else {
|
|
||||||
window.notify('error', data.message || 'Fehler beim Setzen des Parameters');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error setting parameter:', e);
|
console.error('Error setting parameters:', e);
|
||||||
window.notify('error', 'Fehler beim Setzen des Parameters');
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setParameter() {
|
||||||
|
if (!this.routerDevice || !this.routerDevice.deviceId || !this.setParameterName || !this.setParameterValue) return;
|
||||||
|
|
||||||
|
this.routerActionLoading = true;
|
||||||
|
const parameters = {};
|
||||||
|
parameters[this.setParameterName] = this.setParameterValue;
|
||||||
|
|
||||||
|
if (await this.setParameterValues(parameters)) {
|
||||||
|
window.notify('success', 'Parameter erfolgreich gesetzt');
|
||||||
|
this.showSetParameterModal = false;
|
||||||
|
this.setParameterName = '';
|
||||||
|
this.setParameterValue = '';
|
||||||
|
} else {
|
||||||
|
window.notify('error', 'Fehler beim Setzen des Parameters');
|
||||||
}
|
}
|
||||||
this.routerActionLoading = false;
|
this.routerActionLoading = false;
|
||||||
},
|
},
|
||||||
async fetchManagementUsername() {
|
async runSpeedtest() {
|
||||||
if (!this.routerDevice || !this.routerDevice.deviceId) return;
|
if (!this.routerDevice || !this.routerDevice.deviceId) return;
|
||||||
|
this.showSpeedtestModal = true; // Open modal immediately
|
||||||
this.loadingUsername = true;
|
this.speedtestLoading = true;
|
||||||
|
this.speedtestResult = null;
|
||||||
|
this.speedtestHistory = [];
|
||||||
|
this.speedtestHasStarted = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetParameters`, {
|
// 1. Set CPE Parameters
|
||||||
method: 'POST',
|
const params = {
|
||||||
headers: {'Content-Type': 'application/json'},
|
'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Start': 1,
|
||||||
body: JSON.stringify({
|
'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.StartBidirect': 1,
|
||||||
deviceId: this.routerDevice.deviceId,
|
'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.WANAccess': true
|
||||||
parameters: ['InternetGatewayDevice.User.1.Username']
|
};
|
||||||
})
|
|
||||||
|
if (!await this.setParameterValues(params)) {
|
||||||
|
throw new Error("Konnte Speedtest-Parameter nicht setzen");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Trigger Speedtest Server
|
||||||
|
const ip = this.routerDevice.ip; // External IP
|
||||||
|
if (!ip) throw new Error("Keine IP-Adresse gefunden");
|
||||||
|
|
||||||
|
const stRes = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsRunSpeedtest`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ ip: ip })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (!stRes.ok) {
|
||||||
const data = await response.json();
|
const err = await stRes.json();
|
||||||
if (data.success) {
|
throw new Error(err.message || "Speedtest-Server Fehler");
|
||||||
window.notify('success', 'Username-Abfrage gestartet. Bitte warten Sie einen Moment und klicken Sie erneut auf Aktualisieren.');
|
}
|
||||||
|
|
||||||
// Wait a bit and then fetch the device data to get the updated value
|
// 3. Poll for result
|
||||||
setTimeout(async () => {
|
this.pollSpeedtestResult();
|
||||||
try {
|
|
||||||
const deviceResponse = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetDeviceInfo?deviceId=${encodeURIComponent(this.routerDevice.deviceId)}`);
|
} catch (e) {
|
||||||
if (deviceResponse.ok) {
|
window.notify('error', e.message);
|
||||||
const deviceInfo = await deviceResponse.json();
|
this.speedtestLoading = false;
|
||||||
if (deviceInfo.success && deviceInfo.fullData) {
|
}
|
||||||
// Extract username from device data
|
},
|
||||||
const usernameData = deviceInfo.fullData['InternetGatewayDevice.User.1.Username'];
|
async pollSpeedtestResult() {
|
||||||
if (usernameData && usernameData.value && usernameData.value[0]) {
|
let attempts = 0;
|
||||||
this.managementUsername = usernameData.value[0];
|
const maxAttempts = 240; // 2 min
|
||||||
|
const resultParam = 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Result';
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
if (!this.showSpeedtestModal) return;
|
||||||
|
if (attempts >= maxAttempts) {
|
||||||
|
this.speedtestLoading = false;
|
||||||
|
window.notify('error', 'Speedtest Zeitüberschreitung');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetParameters`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
deviceId: this.routerDevice.deviceId,
|
||||||
|
parameters: [resultParam]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const val = await this.fetchDeviceParameterValue(resultParam);
|
||||||
|
if (val && typeof val === 'string' && val.includes("BPS")) {
|
||||||
|
const parsed = this.parseSpeedtestResult(val);
|
||||||
|
if (parsed) {
|
||||||
|
this.speedtestHistory.push(parsed);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.speedtestBottom) {
|
||||||
|
this.$refs.speedtestBottom.scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsed.bps > 0) this.speedtestHasStarted = true;
|
||||||
|
|
||||||
|
if (this.speedtestHasStarted && parsed.bps === 0) {
|
||||||
|
this.speedtestLoading = false;
|
||||||
|
window.notify('success', 'Speedtest abgeschlossen');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Error fetching updated device info:', e);
|
|
||||||
} finally {
|
|
||||||
this.loadingUsername = false;
|
|
||||||
}
|
}
|
||||||
}, 3000);
|
} catch(e) { console.error(e); }
|
||||||
} else {
|
|
||||||
this.loadingUsername = false;
|
if (this.speedtestLoading) setTimeout(poll, 500);
|
||||||
window.notify('error', data.message || 'Fehler beim Lesen des Usernames');
|
}, 500);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (this.speedtestLoading) setTimeout(poll, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
},
|
||||||
|
async fetchDeviceParameterValue(paramName) {
|
||||||
|
if (!this.routerDevice || !this.routerDevice.deviceId) return null;
|
||||||
|
try {
|
||||||
|
const deviceResponse = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetDeviceInfo?deviceId=${encodeURIComponent(this.routerDevice.deviceId)}`);
|
||||||
|
if (deviceResponse.ok) {
|
||||||
|
const deviceInfo = await deviceResponse.json();
|
||||||
|
if (deviceInfo.success && deviceInfo.fullData) {
|
||||||
|
const paramData = deviceInfo.fullData[paramName];
|
||||||
|
if (paramData && paramData.value && paramData.value[0]) {
|
||||||
|
return paramData.value[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error fetching management username:', e);
|
console.error('Error fetching parameter value:', e);
|
||||||
window.notify('error', 'Fehler beim Lesen des Usernames');
|
|
||||||
this.loadingUsername = false;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
formatBits(bps) {
|
||||||
|
if (!bps) return '0 Mbit/s';
|
||||||
|
// 1 Mbit = 1,000,000 bits (Standard network speed unit)
|
||||||
|
const mbits = bps / 1000000;
|
||||||
|
return mbits.toFixed(2) + ' Mbit/s';
|
||||||
|
},
|
||||||
|
parseSpeedtestResult(raw) {
|
||||||
|
try {
|
||||||
|
const bpsMatch = raw.match(/BPS\s+(\d+)/);
|
||||||
|
const bytesMatch = raw.match(/Bytes\s+(\d+)/);
|
||||||
|
const packetsMatch = raw.match(/Packets\s+(\d+)/);
|
||||||
|
|
||||||
|
if (bpsMatch) {
|
||||||
|
const bps = parseInt(bpsMatch[1]);
|
||||||
|
const bytes = bytesMatch ? parseInt(bytesMatch[1]) : 0;
|
||||||
|
const packets = packetsMatch ? parseInt(packetsMatch[1]) : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
raw: raw,
|
||||||
|
bps: bps,
|
||||||
|
bpsFormatted: this.formatBits(bps),
|
||||||
|
bytes: bytes,
|
||||||
|
bytesFormatted: window.RadiusUtils.formatBytes(bytes),
|
||||||
|
packets: packets
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing speedtest result", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
generatePassword(length) {
|
||||||
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
let ret = "";
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
async runRemoteAccess() {
|
||||||
|
if (!this.routerDevice || !this.routerDevice.deviceId) return;
|
||||||
|
|
||||||
|
this.showRemoteAccessModal = true;
|
||||||
|
this.remoteAccessLoading = true;
|
||||||
|
this.remoteAccessStep = 'Konfiguriere Parameter...';
|
||||||
|
this.remoteAccessResult = null;
|
||||||
|
|
||||||
|
const password = this.generatePassword(12);
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
'InternetGatewayDevice.User.1.Enable': 1,
|
||||||
|
'InternetGatewayDevice.User.1.Password': password,
|
||||||
|
'InternetGatewayDevice.User.1.RemoteAccessCapable': 1,
|
||||||
|
'InternetGatewayDevice.User.1.Username': timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
const success = await this.setParameterValues(params);
|
||||||
|
if (!success) throw new Error("Fehler beim Setzen der Parameter");
|
||||||
|
|
||||||
|
this.remoteAccessStep = 'Warte auf TR069-User...';
|
||||||
|
this.pollRemoteUsername(password);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
this.remoteAccessLoading = false;
|
||||||
|
window.notify('error', e.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async pollRemoteUsername(password) {
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 60; // 60 * 2s = 120s
|
||||||
|
const userParam = 'InternetGatewayDevice.User.1.Username';
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
if (!this.showRemoteAccessModal) return;
|
||||||
|
if (attempts >= maxAttempts) {
|
||||||
|
this.remoteAccessLoading = false;
|
||||||
|
window.notify('error', 'Remote Access Zeitüberschreitung');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Refresh param
|
||||||
|
await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetParameters`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
deviceId: this.routerDevice.deviceId,
|
||||||
|
parameters: [userParam]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const val = await this.fetchDeviceParameterValue(userParam);
|
||||||
|
if (val && typeof val === 'string' && val.startsWith('TR069-')) {
|
||||||
|
this.remoteAccessResult = {
|
||||||
|
link: `https://${this.routerDevice.ip}:9090`,
|
||||||
|
username: val,
|
||||||
|
password: password
|
||||||
|
};
|
||||||
|
this.remoteAccessLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) { console.error(e); }
|
||||||
|
|
||||||
|
if (this.remoteAccessLoading) setTimeout(poll, 2000);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (this.remoteAccessLoading) setTimeout(poll, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
},
|
||||||
|
async fetchManagementUsername() {
|
||||||
|
// Kept as requested method, but removed call in openRouterManagement if no longer needed
|
||||||
|
// User asked to remove "row", usually implies logic removal too to save resources?
|
||||||
|
// But I will keep the method definition just in case, but I already removed the UI row.
|
||||||
|
// Since the user asked to remove the "Row", I removed the UI part.
|
||||||
|
// The automatic fetching was in openRouterManagement, I removed it there too.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ Vue.component('workorder-mph-admin', {
|
|||||||
<div class="small">
|
<div class="small">
|
||||||
<div><strong>Adresse:</strong> {{ row.street }} {{ row.hausnummer }}<template v-if="row.stiege">/{{ row.stiege }}</template></div>
|
<div><strong>Adresse:</strong> {{ row.street }} {{ row.hausnummer }}<template v-if="row.stiege">/{{ row.stiege }}</template></div>
|
||||||
<div><strong>Ort:</strong> {{ row.plz }} {{ row.city }}</div>
|
<div><strong>Ort:</strong> {{ row.plz }} {{ row.city }}</div>
|
||||||
<div><strong>Wohneinheiten:</strong> {{ row.wohneinheitCount }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -76,18 +75,27 @@ Vue.component('workorder-mph-admin', {
|
|||||||
<template v-slot:appointmentdate="{ row }">{{ formatDate(row.appointmentDate, true) }}</template>
|
<template v-slot:appointmentdate="{ row }">{{ formatDate(row.appointmentDate, true) }}</template>
|
||||||
|
|
||||||
<template v-slot:expandedRow="{ row }">
|
<template v-slot:expandedRow="{ row }">
|
||||||
<div class="row">
|
<workorder-mph-data-provider :workorder-mph-id="row.id" v-slot="{ docs, journals, refresh }">
|
||||||
<div class="col-12">
|
<div class="workorder-mph-expanded-wrapper">
|
||||||
<checkbox-documentation :workorder-mph-id="parseInt(row.id)" :is-admin="true"/>
|
<div class="row g-2">
|
||||||
</div>
|
<!-- Left Column (1/4): Docs Checkbox, Journal, Review -->
|
||||||
<div class="col-12 mt-3">
|
<div class="col-xl-3 col-lg-4">
|
||||||
<wohneinheit-status-manager :workorder-mph-id="parseInt(row.id)" :is-admin="true"/>
|
<div class="mph-details-stack">
|
||||||
</div>
|
<checkbox-documentation :workorder-mph-id="parseInt(row.id)" :is-admin="true"/>
|
||||||
<div class="col-12 mt-3">
|
<workorder-mph-journal :journals="journals" :workorder-mph-id="row.id" :is-admin="true" @refresh="refresh"/>
|
||||||
<workorder-mph-details-manager :workorder-mph-id="row.id" :is-admin="true"
|
<workorder-mph-admin-review :docs="docs" :workorder-mph-id="row.id" @refresh="refresh"/>
|
||||||
@documentation-accepted="$refs.table.$refs.table.refreshTable()"/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Right Column (3/4): Wohneinheiten, Documents -->
|
||||||
|
<div class="col-xl-9 col-lg-8">
|
||||||
|
<div class="mph-details-stack">
|
||||||
|
<wohneinheit-status-manager :workorder-mph-id="parseInt(row.id)" :is-admin="true"/>
|
||||||
|
<workorder-mph-documents :docs="docs" :workorder-mph-id="row.id" :is-admin="true" @refresh="refresh"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</workorder-mph-data-provider>
|
||||||
</template>
|
</template>
|
||||||
</tt-table-crud>
|
</tt-table-crud>
|
||||||
|
|
||||||
@@ -234,4 +242,4 @@ Vue.component('workorder-mph-admin', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -34,10 +34,47 @@
|
|||||||
/*
|
/*
|
||||||
* Wohneinheit Manager - Dense Table Layout
|
* Wohneinheit Manager - Dense Table Layout
|
||||||
*/
|
*/
|
||||||
|
.wohneinheit-manager-container {
|
||||||
|
min-height: 500px;
|
||||||
|
max-height: 800px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wohneinheit-manager .we-splice .we-splice-checkbox .checkmark:after {
|
||||||
|
left: 5px;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.wohneinheit-manager .we-table {
|
.wohneinheit-manager .we-table {
|
||||||
display: table;
|
display: table;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wohneinheit-manager .we-header {
|
||||||
|
display: table-header-group;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wohneinheit-manager .we-header-row {
|
||||||
|
display: table-row;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wohneinheit-manager .we-header .we-cell {
|
||||||
|
display: table-cell;
|
||||||
|
padding: 8px 6px;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
background-color: #f8f9fa; /* Needed for sticky */
|
||||||
}
|
}
|
||||||
|
|
||||||
.wohneinheit-manager .we-row {
|
.wohneinheit-manager .we-row {
|
||||||
@@ -50,60 +87,63 @@
|
|||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wohneinheit-manager .we-cell {
|
.wohneinheit-manager .we-row.locked-row {
|
||||||
display: table-cell;
|
background-color: #e7f5ff;
|
||||||
padding: 8px 12px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wohneinheit-manager .we-bezeichner {
|
.wohneinheit-manager .we-row.locked-row:hover {
|
||||||
width: 20%;
|
background-color: #d0ebff;
|
||||||
font-size: 0.9rem;
|
}
|
||||||
|
|
||||||
|
.wohneinheit-manager .we-cell {
|
||||||
|
display: table-cell;
|
||||||
|
padding: 6px 6px;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wohneinheit-manager .we-zusatz {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wohneinheit-manager .we-tuer {
|
||||||
|
width: 15%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wohneinheit-manager .we-status {
|
.wohneinheit-manager .we-status {
|
||||||
width: 18%;
|
width: 20%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wohneinheit-manager .we-splice {
|
.wohneinheit-manager .we-splice {
|
||||||
width: 12%;
|
width: 12%;
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wohneinheit-manager .we-note {
|
|
||||||
width: 35%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wohneinheit-manager .we-actions {
|
|
||||||
width: 15%;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wohneinheit-manager .we-splice .custom-checkbox-item {
|
|
||||||
padding: 4px;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
height: auto;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wohneinheit-manager .we-splice .custom-checkbox-item .checkmark {
|
.wohneinheit-manager .we-documents {
|
||||||
margin-right: 0;
|
width: 12%;
|
||||||
margin-bottom: 2px;
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wohneinheit-manager .we-actions {
|
||||||
|
width: 8%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Sizing for Dense View */
|
||||||
|
.wohneinheit-manager .form-control-sm {
|
||||||
|
height: calc(1.4em + 0.5rem + 2px);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wohneinheit-manager .we-splice .we-splice-checkbox .checkmark {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wohneinheit-manager .we-splice .custom-checkbox-item .checkbox-label {
|
.wohneinheit-manager .badge {
|
||||||
font-size: 0.7rem;
|
font-size: 0.65rem;
|
||||||
line-height: 1;
|
padding: 2px 4px;
|
||||||
}
|
|
||||||
|
|
||||||
.contact-info {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
margin-top: 4px;
|
|
||||||
padding-left: 4px;
|
|
||||||
border-left: 2px solid #007bff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workorder-mph-button {
|
.workorder-mph-button {
|
||||||
@@ -111,39 +151,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Custom Checkboxes - Compact & Beautiful
|
* Custom Checkboxes - Compact
|
||||||
*/
|
*/
|
||||||
.custom-checkboxes-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 8px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox-item {
|
.custom-checkbox-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 12px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
|
||||||
margin: 0;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox-item:hover:not(.disabled) {
|
|
||||||
background: #e9ecef;
|
|
||||||
border-color: #007bff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox-item.disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox-item input[type="checkbox"] {
|
.custom-checkbox-item input[type="checkbox"] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -157,8 +173,7 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 2px solid #adb5bd;
|
border: 2px solid #adb5bd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-right: 10px;
|
margin-right: 0;
|
||||||
flex-shrink: 0;
|
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,50 +199,255 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox-item .checkbox-label {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #495057;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox-item input[type="checkbox"]:checked ~ .checkbox-label {
|
|
||||||
color: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Required Documents Checklist
|
* Expanded View Layout
|
||||||
*/
|
*/
|
||||||
.required-docs-checklist {
|
.workorder-mph-expanded-wrapper {
|
||||||
background: #f8f9fa;
|
background: #f1f3f5;
|
||||||
border-radius: 6px;
|
padding: 16px;
|
||||||
padding: 8px;
|
border-radius: 8px;
|
||||||
|
margin: -8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-check-item {
|
.workorder-mph-expanded-wrapper .row.g-2 {
|
||||||
display: flex;
|
margin-right: -0.5rem;
|
||||||
align-items: center;
|
margin-left: -0.5rem;
|
||||||
padding: 6px 10px;
|
}
|
||||||
margin-bottom: 4px;
|
|
||||||
|
.workorder-mph-expanded-wrapper .row.g-2 > .col,
|
||||||
|
.workorder-mph-expanded-wrapper .row.g-2 > [class*="col-"] {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Styling */
|
||||||
|
.workorder-mph-card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 4px;
|
border: 1px solid #dee2e6;
|
||||||
font-size: 0.9rem;
|
border-radius: 6px;
|
||||||
gap: 10px;
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-check-item:last-child {
|
.workorder-mph-card:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-check-item i:first-child {
|
.workorder-mph-card-header {
|
||||||
width: 20px;
|
padding: 8px 12px;
|
||||||
text-align: center;
|
background: linear-gradient(to bottom, #ffffff, #f8f9fa);
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #495057;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start; /* Changed from space-between */
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-check-item span {
|
.workorder-mph-card-header > div:last-child,
|
||||||
flex: 1;
|
.workorder-mph-card-header > span:last-child:not(:first-child) {
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-check-item .ml-auto {
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Drag and Drop Zone */
|
||||||
|
.mph-drop-zone {
|
||||||
|
border: 2px dashed #ced4da;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
min-height: 100px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-drop-zone:hover,
|
||||||
|
.mph-drop-zone.dragging {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-drop-zone input[type="file"] {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-drop-zone-icon {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #adb5bd;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-drop-zone:hover .mph-drop-zone-icon {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-drop-zone-text {
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-drop-zone-hint {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-file-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-file-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-file-item .remove-file {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #dc3545;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-file-item .remove-file:hover {
|
||||||
|
background: #ffe3e3;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workorder-mph-card-header i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workorder-mph-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workorder-mph-card-body-compact {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Documentation Checkboxes - Vertical List for Side Panel */
|
||||||
|
.mph-checkbox-vertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-checkbox-item:hover:not(.disabled) {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-color: #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-checkbox-item .checkmark {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-checkbox-item .checkmark:after {
|
||||||
|
left: 4px;
|
||||||
|
top: 0px;
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-checkbox-item .label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Details Stack */
|
||||||
|
.mph-details-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin Review Checklist */
|
||||||
|
.mph-doc-checklist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-doc-check-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-doc-check-item.has-doc {
|
||||||
|
background-color: #f0fff4;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Journal List */
|
||||||
|
.mph-journal-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-journal-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #f1f3f5;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mph-journal-item-header {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #868e96;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Gallery Adjustments */
|
||||||
|
.mph-docs-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -38,9 +38,9 @@ Vue.component('workorder-mph-company', {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:expandedRow="{ row }">
|
<template v-slot:expandedRow="{ row }">
|
||||||
<div class="row">
|
<div class="workorder-mph-expanded-wrapper">
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="col-12 mb-3" v-if="!['completed', 'cancelled', 'documented'].includes(row.status)">
|
<div class="mb-3" v-if="!['completed', 'cancelled', 'documented'].includes(row.status)">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<tt-button v-if="row.status === 'assigned'" text="Termin planen"
|
<tt-button v-if="row.status === 'assigned'" text="Termin planen"
|
||||||
@click="editingAppointmentId = row.id" icon="fas fa-calendar-plus"
|
@click="editingAppointmentId = row.id" icon="fas fa-calendar-plus"
|
||||||
@@ -56,21 +56,18 @@ Vue.component('workorder-mph-company', {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Wohneinheit Manager -->
|
<div class="row g-3">
|
||||||
<div class="col-12">
|
<div class="col-xl-4 col-lg-6">
|
||||||
<wohneinheit-status-manager :workorder-mph-id="parseInt(row.id)" :is-admin="false"
|
<checkbox-documentation :workorder-mph-id="parseInt(row.id)" :is-admin="false"/>
|
||||||
@wohneinheit-updated="checkAllWohneinheitenHaveNotes(row.id)"/>
|
</div>
|
||||||
</div>
|
<div class="col-xl-8 col-lg-6">
|
||||||
|
<workorder-mph-details-manager :workorder-mph-id="row.id" :is-admin="false"
|
||||||
<!-- Checkbox Documentation -->
|
@workorder-completed="$refs.table.$refs.table.refreshTable()"/>
|
||||||
<div class="col-12 mt-3">
|
</div>
|
||||||
<checkbox-documentation :workorder-mph-id="parseInt(row.id)" :is-admin="false"/>
|
<div class="col-12">
|
||||||
</div>
|
<wohneinheit-status-manager :workorder-mph-id="parseInt(row.id)" :is-admin="false"
|
||||||
|
@wohneinheit-updated="checkAllWohneinheitenHaveNotes(row.id)"/>
|
||||||
<!-- Details Manager (Docs & Journal) -->
|
</div>
|
||||||
<div class="col-12 mt-3">
|
|
||||||
<workorder-mph-details-manager :workorder-mph-id="row.id" :is-admin="false"
|
|
||||||
@workorder-completed="$refs.table.$refs.table.refreshTable()"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ Vue.component('tt-fullscreen-viewer', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isImage: file => file?.mimetype?.startsWith('image/'),
|
isImage: file => file?.mimetype?.startsWith('image/') || file?.mimetype === 'application/octet-stream',
|
||||||
isPdf: file => file?.mimetype === 'application/pdf',
|
isPdf: file => file?.mimetype === 'application/pdf',
|
||||||
onContentLoad() {
|
onContentLoad() {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@@ -249,7 +249,7 @@ Vue.component('tt-file-gallery', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isImage: file => file.mimetype?.startsWith('image/'),
|
isImage: file => file.mimetype?.startsWith('image/') || file.mimetype === 'application/octet-stream',
|
||||||
isPdf: file => file.mimetype === 'application/pdf',
|
isPdf: file => file.mimetype === 'application/pdf',
|
||||||
|
|
||||||
getFileIcon(file) {
|
getFileIcon(file) {
|
||||||
|
|||||||
Reference in New Issue
Block a user