improved ipam

This commit is contained in:
2025-08-26 17:18:58 +02:00
parent 68c4e50b20
commit 177a02c8c8
11 changed files with 324 additions and 71 deletions

View File

@@ -93,12 +93,14 @@ class RMLWorkorderModel extends TTCrudBaseModel {
$sql = "
SELECT
w.id, w.status, w.deadlineDate, w.companyId, p.preordercampaign_id, hn.rimo_fcp_name,
n.owner_id as tenantId,
CONCAT_WS(' ', p.firstname, p.lastname) as customerName, p.ucode,
p.company as customerCompany, p.oaid, c.name as companyName,
str.name as street, hn.hausnummer, hn.stiege, we.bezeichner as apartment, plz.plz, ort.name as city
FROM `$fronkDbName`.`RMLWorkorder` w
JOIN `$fronkDbName`.`Preorder` p ON w.preorderId = p.id
LEFT JOIN `$fronkDbName`.`Preordercampaign` pc ON p.preordercampaign_id = pc.id
LEFT JOIN `$fronkDbName`.`Network` n ON pc.network_id = n.id
LEFT JOIN `$fronkDbName`.`RMLWorkorderCompany` c ON w.companyId = c.id
LEFT JOIN `$addressDbName`.`Hausnummer` hn ON p.adb_hausnummer_id = hn.id
LEFT JOIN `$addressDbName`.`Strasse` str ON hn.strasse_id = str.id
@@ -135,6 +137,8 @@ class RMLWorkorderModel extends TTCrudBaseModel {
SELECT COUNT(w.id) as count
FROM `$fronkDbName`.`RMLWorkorder` w
JOIN `$fronkDbName`.`Preorder` p ON w.preorderId = p.id
LEFT JOIN `$fronkDbName`.`Preordercampaign` pc ON p.preordercampaign_id = pc.id
LEFT JOIN `$fronkDbName`.`Network` n ON pc.network_id = n.id
LEFT JOIN `$fronkDbName`.`RMLWorkorderCompany` c ON w.companyId = c.id
LEFT JOIN `$addressDbName`.`Hausnummer` hn ON p.adb_hausnummer_id = hn.id
LEFT JOIN `$addressDbName`.`Strasse` str ON hn.strasse_id = str.id
@@ -153,7 +157,7 @@ class RMLWorkorderModel extends TTCrudBaseModel {
private static function buildCompanyWhereClause(array $filters, int $companyId): string
{
$sql = "c.addressId = " . $companyId;
$sql = "w.companyId = " . $companyId;
if (!empty($filters['id'])) {
$sql .= Helper::generateFilterCondition($filters['id'], 'w.id', true);
@@ -171,9 +175,9 @@ class RMLWorkorderModel extends TTCrudBaseModel {
$searchColumns = "p.firstname|p.lastname|p.company|p.oaid|p.street|p.housenumber|p.zip|p.city|str.name|ort.name|p.phone|p.email";
$sql .= Helper::generateFilterCondition($filters['preorderInfo'], $searchColumns);
} if (!empty($filters['rimo_fcp_name'])) {
$searchColumns = "hn.rimo_fcp_name";
$sql .= Helper::generateFilterCondition($filters['rimo_fcp_name'], $searchColumns);
}
$searchColumns = "hn.rimo_fcp_name";
$sql .= Helper::generateFilterCondition($filters['rimo_fcp_name'], $searchColumns);
}
return "WHERE " . $sql;
}
@@ -192,7 +196,6 @@ class RMLWorkorderModel extends TTCrudBaseModel {
str.name as street, hn.hausnummer, hn.stiege, we.bezeichner as apartment, plz.plz, ort.name as city
FROM `$fronkDbName`.`RMLWorkorder` w
JOIN `$fronkDbName`.`Preorder` p ON w.preorderId = p.id
LEFT JOIN `$fronkDbName`.`RMLWorkorderCompany` c ON w.companyId = c.id
LEFT JOIN `$addressDbName`.`Hausnummer` hn ON p.adb_hausnummer_id = hn.id
LEFT JOIN `$addressDbName`.`Strasse` str ON hn.strasse_id = str.id
LEFT JOIN `$addressDbName`.`Plz` plz ON hn.plz_id = plz.id
@@ -229,7 +232,6 @@ class RMLWorkorderModel extends TTCrudBaseModel {
SELECT COUNT(w.id) as count
FROM `$fronkDbName`.`RMLWorkorder` w
JOIN `$fronkDbName`.`Preorder` p ON w.preorderId = p.id
LEFT JOIN `$fronkDbName`.`RMLWorkorderCompany` c ON w.companyId = c.id
LEFT JOIN `$addressDbName`.`Hausnummer` hn ON p.adb_hausnummer_id = hn.id
LEFT JOIN `$addressDbName`.`Strasse` str ON hn.strasse_id = str.id
LEFT JOIN `$addressDbName`.`Plz` plz ON hn.plz_id = plz.id

View File

@@ -8,7 +8,7 @@ class RMLWorkorderAdminController extends TTCrud
protected array $permissionCheck = ['RMLAdmin'];
protected array $columns = [
// ['key' => 'id', 'text' => 'Auftrag-Nr.', 'table' => ['sortable' => true]],
['key' => 'id', 'text' => 'Auftrag-Nr.', 'table' => ['sortable' => false]],
['key' => 'preordercampaign_id', 'text' => 'Cluster', 'modal' => false, 'table' => ['filter' => 'select']],
['key' => 'preorderInfo', 'text' => 'Kunde / Projekt', 'modal' => false, 'table' => ['sortable' => false]],
['key' => 'rimo_fcp_name', 'text' => 'FCP', 'modal' => false, 'table' => ['sortable' => false]],
@@ -23,7 +23,7 @@ class RMLWorkorderAdminController extends TTCrud
['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'],
['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'],
]]],
['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date']],
['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => false]],
];
private function getStatusText(string $statusKey): string {
@@ -105,18 +105,32 @@ class RMLWorkorderAdminController extends TTCrud
}
private function createWorkordersFromPreorders() {
$newPreorders = PreorderModel::searchActive(['status_code' => 220, 'preorder_status_flags_all' => [3,5]]);
if (empty($newPreorders)) return;
$configs = RMLWorkorderTenantConfigModel::getAll();
foreach ($configs as $config) {
$filters = json_decode($config->workorderCreationFilters, true);
if (empty($filters)) continue;
foreach ($newPreorders as $preorder) {
if (!RMLWorkorderModel::getFirst(['preorderId' => $preorder->id])) {
RMLWorkorderModel::create([
'preorderId' => $preorder->id,
'clusterId' => $preorder->preordercampaign_id,
'status' => 'new',
'create' => time(),
'createBy' => $this->user->id
]);
$networks = NetworkModel::getAll(['owner_id' => $config->addressId]);
if(empty($networks)) continue;
$tenantCampaigns = array_map(fn($n) => $n->id, PreordercampaignModel::getAll(['network_id' => array_map(fn($n) => $n->id, $networks)]));
if(empty($tenantCampaigns)) continue;
$filters['preordercampaign_id'] = $tenantCampaigns;
$newPreorders = PreorderModel::searchActive($filters);
if (empty($newPreorders)) continue;
foreach ($newPreorders as $preorder) {
if (!RMLWorkorderModel::getFirst(['preorderId' => $preorder->id])) {
RMLWorkorderModel::create([
'preorderId' => $preorder->id,
'clusterId' => $preorder->preordercampaign_id,
'status' => 'new',
'create' => time(),
'createBy' => 0 // System User
]);
}
}
}
}
@@ -262,9 +276,22 @@ class RMLWorkorderAdminController extends TTCrud
}
protected function getCompaniesAction() {
$tenantId = $this->request->tenantId ?? null;
$companies = RMLWorkorderCompanyModel::getAll([], null, 0, ['key' => 'name', 'order' => 'ASC']);
if ($tenantId) {
$companies = array_filter($companies, function($company) use ($tenantId) {
// RML Infrastruktur GmbH is always available as a fallback/default
if ($company->addressId == 4807 && empty($company->visibleForAddressId)) {
return true;
}
$visibleFor = !empty($company->visibleForAddressId) ? json_decode($company->visibleForAddressId, true) : [];
return in_array($tenantId, $visibleFor);
});
}
$items = array_map(fn($c) => ['value' => $c->id, 'text' => $c->name], $companies);
self::returnJson($items);
self::returnJson(array_values($items)); // re-index
}
protected function addJournalAction() {
@@ -329,7 +356,7 @@ class RMLWorkorderAdminController extends TTCrud
$preorder = new Preorder($workorder->preorderId);
if ($preorder) {
$preorder->status_id = 15; // Assuming 11 is the status for "fiber in building"
$preorder->status_id = 15; // Assuming 15 is the status for "completed"
$preorder->edit_by = $this->user->id;
$preorder->save();
}

View File

@@ -71,11 +71,12 @@ class RMLWorkorderCompanyController extends TTCrud
$filters = $json['filters'] ?? [];
$order = $json['order'] ?? ['key' => 'deadlineDate', 'order' => 'ASC'];
$companyId = $this->user->address_id;
if ($companyId === 0) {
$company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]);
if (!$company) {
self::returnJson(['rows' => [], 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0])]);
return;
}
$companyId = $company->id;
$workorders = RMLWorkorderModel::getCompanyWorkorders($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order, $companyId);
$totalCount = RMLWorkorderModel::countCompanyWorkorders($filters, $companyId);
@@ -265,7 +266,7 @@ class RMLWorkorderCompanyController extends TTCrud
}
$workorder = RMLWorkorderModel::get($workorderId);
if ($workorder->status === 'correction_requested') {
if ($workorder->status === 'correction_requested' || $workorder->status === 'problem_solved') {
$workorder->status = 'assigned';
RMLWorkorderModel::update((array)$workorder);
$workorder = RMLWorkorderModel::get($workorderId);
@@ -420,4 +421,34 @@ class RMLWorkorderCompanyController extends TTCrud
'journals' => $journals
]);
}
protected function getTenantConfigAction() {
if(empty($this->request->workorderId)) self::sendError("Workorder ID missing.");
$workorder = RMLWorkorderModel::get($this->request->workorderId);
if (!$workorder) self::sendError("Workorder not found.");
$preorder = new Preorder($workorder->preorderId);
if (!$preorder->id) self::sendError("Preorder not found.");
$campaign = new Preordercampaign($preorder->preordercampaign_id);
if (!$campaign->id) self::sendError("Campaign not found.");
$network = NetworkModel::getOne($campaign->network_id);
if (!$network) self::sendError("Network not found.");
$tenantId = $network->owner_id;
$tenantConfig = RMLWorkorderTenantConfigModel::getFirst(['addressId' => $tenantId]);
if (!$tenantConfig) {
$tenantConfig = RMLWorkorderTenantConfigModel::getFirst(['addressId' => 4807]); // RML Default
}
if (!$tenantConfig) {
self::returnJson(['success' => false, 'message' => 'No tenant config found.']);
return;
}
self::returnJson(['success' => true, 'documentationTypes' => json_decode($tenantConfig->documentationTypes, true)]);
}
}

View File

@@ -5,6 +5,7 @@ class RMLWorkorderCompanyModel extends TTCrudBaseModel {
public int $id;
public int $addressId;
public string $name;
public ?string $visibleForAddressId;
public int $create;
public int $createBy;
}

View File

@@ -0,0 +1,12 @@
<?php
// RMLWorkorderTenantConfigModel.php
class RMLWorkorderTenantConfigModel extends TTCrudBaseModel {
public int $id;
public int $addressId;
public string $name;
public string $documentationTypes; // JSON
public string $workorderCreationFilters; // JSON
public int $create;
public int $createBy;
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class RmlworkorderMultiTenant extends AbstractMigration
{
public function up(): void
{
if ($this->getEnvironment() == "thetool") {
$companyTable = $this->table('RMLWorkorderCompany');
if (!$companyTable->hasColumn('visibleForAddressId')) {
$companyTable->addColumn('visibleForAddressId', 'text', ['null' => true, 'default' => null, 'after' => 'name'])
->save();
}
if (!$this->hasTable('RMLWorkorderTenantConfig')) {
$tenantConfigTable = $this->table('RMLWorkorderTenantConfig', ['id' => false, 'primary_key' => ['id']]);
$tenantConfigTable->addColumn('id', 'integer', ['identity' => true, 'signed' => false])
->addColumn('addressId', 'integer')
->addColumn('name', 'string', ['limit' => 255])
->addColumn('documentationTypes', 'json')
->addColumn('workorderCreationFilters', 'json')
->addColumn('create', 'integer')
->addColumn('createBy', 'integer')
->addIndex(['addressId'], ['name' => 'addressId_idx'])
->create();
}
$this->table('RMLWorkorderTenantConfig')->insert([
[
'id' => 1,
'addressId' => 4807,
'name' => 'RML Standard',
'documentationTypes' => '[{\"value\": \"photo_hup_mounted\", \"text\": \"Foto vom montierten HÜP\"}, {\"value\": \"photo_hup_open\", \"text\": \"Foto von dem offenen HÜP\"}, {\"value\": \"photo_splice_cassette_hup\", \"text\": \"Foto der Spleißkassette HÜP\"}, {\"value\": \"photo_splice_cassette_fcp\", \"text\": \"Foto der Spleißkassette - FCP\"}, {\"value\": \"photo_hup_closed_stickers\", \"text\": \"Foto vom geschlossenen HÜP mit allen Aufklebern\"}, {\"value\": \"photo_fcp_labeled\", \"text\": \"Foto vom FCP beschriftet\"}, {\"value\": \"photo_patch_position_osp\", \"text\": \"Foto der Patch-Position - OSP-Seite\"}, {\"value\": \"photo_patch_position_anb\", \"text\": \"Foto der Patch-Position - ANB-Seite\"}, {\"value\": \"measurement_protocol_otdr\", \"text\": \"ODTR Messung (1310nm & 1550nm)\"}]',
'workorderCreationFilters' => '{\"status_code\": 220, \"preorder_status_flags_all\": [3, 5]}',
'create' => 1724704509,
'createBy' => 1
]
])->save();
$this->execute("UPDATE RMLWorkorderCompany SET visibleForAddressId = '[4807]' WHERE visibleForAddressId IS NULL;");
}
}
public function down(): void
{
if ($this->getEnvironment() == "thetool") {
if ($this->hasTable('RMLWorkorderTenantConfig')) {
$this->table('RMLWorkorderTenantConfig')->drop()->save();
}
$companyTable = $this->table('RMLWorkorderCompany');
if ($companyTable->hasColumn('visibleForAddressId')) {
$companyTable->removeColumn('visibleForAddressId')
->save();
}
}
}
}

View File

@@ -205,7 +205,7 @@ class Helper {
if ($user->isAdmin()) $campaigns = PreordercampaignModel::getAll();
else {
$networkIDs = array_unique(array_merge(
array_column($user->myNetworks(["netowner", "salespartner"]), 'id'),
array_column($user->myNetworks(["netowner"]), 'id'),
json_decode($user->getFlag("preorder_networks")->value() ?: '[]')
));
$campaigns = PreordercampaignModel::search(['network_id' => $networkIDs]);

View File

@@ -7,7 +7,7 @@ Vue.component('r-m-l-workorder-admin', {
<div style="width: 300px;">
<tt-select
class="mb-0"
:options="companies"
:options="companiesForMassAssign"
v-model="massAssignCompanyId"
@input="massAssignCompanies"
placeholder="Firma auswählen..."
@@ -62,32 +62,35 @@ Vue.component('r-m-l-workorder-admin', {
<div class="d-flex justify-content-between align-items-center">
<div class="flex-grow-1">
<div v-if="editingWorkorderId === row.id">
<tt-select
:options="companies"
:value="row.companyId"
@input="assignCompany(row, $event)"
@blur="editingWorkorderId = null"
@keydown.esc.native="editingWorkorderId = null"
placeholder="Firma zuweisen..."
sm
no-form-group
<div v-if="companiesLoading" class="spinner-border spinner-border-sm"></div>
<tt-select v-else
:options="companiesByTenant[row.tenantId] || []"
:value="row.companyId"
@input="assignCompany(row, $event)"
@blur="editingWorkorderId = null"
@keydown.esc.native="editingWorkorderId = null"
placeholder="Firma zuweisen..."
sm
no-form-group
/>
</div>
<div v-else-if="row.status === 'new'">
<tt-select
:options="companies"
:value="row.companyId"
@input="assignCompany(row, $event)"
placeholder="Firma zuweisen..."
sm
no-form-group
<div v-if="companiesLoading" class="spinner-border spinner-border-sm"></div>
<tt-select v-else
:options="companiesByTenant[row.tenantId] || []"
:value="row.companyId"
@input="assignCompany(row, $event)"
@focus="getCompaniesForWorkorder(row)"
placeholder="Firma zuweisen..."
sm
no-form-group
/>
</div>
<div v-else class="d-flex align-items-center">
<span>{{ row.companyName || 'N/A' }}</span>
<tt-button
icon="fas fa-edit"
@click="editingWorkorderId = row.id"
@click="startCompanyEdit(row)"
additional-class="btn-link btn-sm p-0 ml-2"
title="Zuweisung ändern"
/>
@@ -156,9 +159,11 @@ Vue.component('r-m-l-workorder-admin', {
workordersToAssign: [],
editingWorkorderId: null,
editingDeadlineId: null,
companies: [],
companiesByTenant: {},
companiesLoading: false,
massAssignCompanyId: null,
massAssignLoading: false,
companiesForMassAssign: [],
crudConfig: {
...window.TT_CONFIG.CRUD_CONFIG,
selectable: false,
@@ -184,10 +189,6 @@ Vue.component('r-m-l-workorder-admin', {
}
}
},
async mounted() {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getCompanies`);
this.companies = response.data;
},
methods: {
addToAssignList(row) {
if (!this.workordersToAssign.includes(row.id)) {
@@ -205,6 +206,24 @@ Vue.component('r-m-l-workorder-admin', {
if (!timestamp) return '';
return window.moment.unix(timestamp).format('DD.MM.YYYY');
},
async getCompaniesForWorkorder(workorder) {
if (!workorder.tenantId || this.companiesByTenant[workorder.tenantId]) {
return;
}
this.companiesLoading = true;
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getCompanies`, { params: { tenantId: workorder.tenantId } });
this.$set(this.companiesByTenant, workorder.tenantId, response.data);
} catch (e) {
window.notify('error', 'Firmenliste konnte nicht geladen werden.');
} finally {
this.companiesLoading = false;
}
},
async startCompanyEdit(row) {
await this.getCompaniesForWorkorder(row);
this.editingWorkorderId = row.id;
},
async assignCompany(workorder, companyId) {
if (!companyId) {
this.editingWorkorderId = null;
@@ -320,6 +339,36 @@ Vue.component('r-m-l-workorder-admin', {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
}
},
watch: {
workordersToAssign: {
async handler(newVal) {
if (newVal.length === 0) {
this.companiesForMassAssign = [];
return;
}
const firstWorkorderId = newVal[0];
const firstWorkorder = this.$refs.table.$refs.table.rows.find(r => r.id === firstWorkorderId);
if (!firstWorkorder) return;
const firstTenantId = firstWorkorder.tenantId;
const allSameTenant = newVal.every(id => {
const wo = this.$refs.table.$refs.table.rows.find(r => r.id === id);
return wo && wo.tenantId === firstTenantId;
});
if (!allSameTenant) {
window.notify('error', 'Massen-Zuweisung nur für Aufträge des gleichen Mandanten möglich.');
this.workordersToAssign.pop(); // remove last added
return;
}
await this.getCompaniesForWorkorder(firstWorkorder);
this.companiesForMassAssign = this.companiesByTenant[firstTenantId] || [];
},
deep: true
}
}
});
@@ -365,7 +414,8 @@ Vue.component('rml-documentation-viewer-admin', {
<div class="card-header"><h5><i class="fas fa-check-circle text-success mr-2"></i>Dokumentation akzeptieren</h5></div>
<div class="card-body">
<p class="small text-muted">Prüfen Sie, ob alle erforderlichen Dokumente vorhanden und korrekt sind.</p>
<ul class="list-unstyled">
<div v-if="loadingConfig" class="text-center"><i class="fas fa-spinner fa-spin"></i></div>
<ul v-else class="list-unstyled">
<li v-for="docType in requiredDocTypes" :key="docType.value" class="mb-2 d-flex align-items-center small">
<i :class="isUploaded(docType.value) ? 'fas fa-check-circle text-success' : 'far fa-circle text-muted'" class="fa-fw mr-2"></i>
<span>{{ docType.text }}</span>
@@ -409,6 +459,7 @@ Vue.component('rml-documentation-viewer-admin', {
data() {
return {
loading: true,
loadingConfig: true,
correctionLoading: false,
docs: [],
journals: [],
@@ -416,7 +467,16 @@ Vue.component('rml-documentation-viewer-admin', {
correctionText: '',
newJournalMessage: '',
addingJournalEntry: false,
requiredDocTypes: [
tenantDocTypes: null,
}
},
computed: {
requiredDocTypes() {
if (this.tenantDocTypes) {
return this.tenantDocTypes;
}
// Fallback for RML default
return [
{ value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP' },
{ value: 'photo_hup_open', text: 'Foto von dem offenen HÜP' },
{ value: 'photo_splice_cassette_hup', text: 'Foto der Spleißkassette HÜP' },
@@ -426,7 +486,7 @@ Vue.component('rml-documentation-viewer-admin', {
{ value: 'photo_patch_position_osp', text: 'Foto der Patch-Position - OSP-Seite' },
{ value: 'photo_patch_position_anb', text: 'Foto der Patch-Position - ANB-Seite' },
{ value: 'measurement_protocol_otdr', text: 'ODTR Messung (1310nm & 1550nm)' },
]
];
}
},
methods: {
@@ -445,6 +505,21 @@ Vue.component('rml-documentation-viewer-admin', {
this.loading = false;
}
},
async loadTenantConfig() {
this.loadingConfig = true;
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getTenantConfig`, {
params: { workorderId: this.workorderId }
});
if (response.data.success) {
this.tenantDocTypes = response.data.documentationTypes;
}
} catch (e) {
console.error("Could not load tenant documentation config", e);
} finally {
this.loadingConfig = false;
}
},
async requestCorrection() {
if (!this.correctionText) return window.notify('error', 'Bitte geben Sie einen Grund für die Korrektur an.');
if (this.selectedDocs.length === 0) return window.notify('error', 'Bitte wählen Sie mindestens ein Dokument für die Korrektur aus.');
@@ -498,7 +573,8 @@ Vue.component('rml-documentation-viewer-admin', {
return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm');
},
},
mounted() {
this.fetchData();
async mounted() {
await this.loadTenantConfig();
await this.fetchData();
}
});

View File

@@ -22,7 +22,7 @@ Vue.component('r-m-l-workorder-company', {
</template>
<template v-slot:appointmentdate="{ row }">
<div v-if="!row.appointmentDate && ['assigned', 'correction_requested'].includes(row.status)">
<div v-if="!row.appointmentDate && ['assigned', 'correction_requested', 'problem_solved'].includes(row.status)">
<tt-date-picker
placeholder="Termin festlegen..."
:date-range="false"
@@ -193,7 +193,7 @@ Vue.component('documentation-manager', {
props: ['workorderId'],
template: `
<div class="p-3 bg-light" style="width: 100%;">
<div v-if="loadingWorkorder" class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
<div v-if="loadingWorkorder || loadingConfig" class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
<div v-else class="row">
<div class="col-lg-4 mb-3 mb-lg-0">
<div>
@@ -327,6 +327,8 @@ Vue.component('documentation-manager', {
data() {
return {
loadingWorkorder: true,
loadingConfig: true,
tenantDocTypes: null,
workorder: null,
uploading: false,
completing: false,
@@ -346,8 +348,16 @@ Vue.component('documentation-manager', {
files: [],
documentType: 'photo_hup_mounted',
description: ''
},
requiredDocTypes: [
}
}
},
computed: {
requiredDocTypes() {
if (this.tenantDocTypes) {
return this.tenantDocTypes;
}
// Fallback for RML default
return [
{ value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP' },
{ value: 'photo_hup_open', text: 'Foto von dem offenen HÜP' },
{ value: 'photo_splice_cassette_hup', text: 'Foto der Spleißkassette HÜP' },
@@ -357,10 +367,8 @@ Vue.component('documentation-manager', {
{ value: 'photo_patch_position_osp', text: 'Foto der Patch-Position - OSP-Seite' },
{ value: 'photo_patch_position_anb', text: 'Foto der Patch-Position - ANB-Seite' },
{ value: 'measurement_protocol_otdr', text: 'ODTR Messung (1310nm & 1550nm)' },
]
}
},
computed: {
];
},
allDocTypes() {
return [
...this.requiredDocTypes,
@@ -403,6 +411,21 @@ Vue.component('documentation-manager', {
if (!timestamp) return '';
return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm');
},
async loadTenantConfig() {
this.loadingConfig = true;
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getTenantConfig`, {
params: { workorderId: this.workorderId }
});
if (response.data.success) {
this.tenantDocTypes = response.data.documentationTypes;
}
} catch (e) {
console.error("Could not load tenant documentation config", e);
} finally {
this.loadingConfig = false;
}
},
async loadWorkorder() {
this.loadingWorkorder = true;
try {
@@ -545,7 +568,6 @@ Vue.component('documentation-manager', {
}
let journalParts = [];
// Sort types to have a consistent order in the journal message
types.sort();
for (const type of types) {
@@ -599,6 +621,7 @@ Vue.component('documentation-manager', {
},
async mounted() {
await this.loadWorkorder();
await this.loadTenantConfig();
await this.fetchDocs();
}
});

View File

@@ -267,4 +267,13 @@
100% {
transform: rotate(360deg);
}
}
.tt-file-gallery-item.is-missing {
cursor: not-allowed;
opacity: 0.6;
}
.tt-file-gallery-item.is-missing .tt-file-gallery-overlay {
display: none; /* Hide the zoom icon overlay */
}

View File

@@ -241,6 +241,7 @@ Vue.component('tt-file-gallery', {
fullscreenItem: null,
editingFile: null,
selectedFiles: [],
missingFileIds: new Set(),
}),
computed: {
imageFiles() {
@@ -267,7 +268,6 @@ Vue.component('tt-file-gallery', {
return 'fas fa-file text-secondary';
}
},
toggleSelection(fileId) {
if (!this.selectable) return;
const index = this.selectedFiles.indexOf(fileId);
@@ -275,9 +275,16 @@ Vue.component('tt-file-gallery', {
else this.selectedFiles.push(fileId);
this.$emit('selection-changed', this.selectedFiles);
},
handleImageError(file) {
if (!file || !file.id) return;
this.missingFileIds.add(file.id);
this.$forceUpdate(); // Force a re-render as Vue might not detect the Set change
},
isMissing(file) {
return this.missingFileIds.has(file.id);
},
openViewer(file) {
if (this.editingFile) return;
if (this.isMissing(file) || this.editingFile) return;
this.fullscreenItem = file;
},
closeViewer() {
@@ -310,21 +317,26 @@ Vue.component('tt-file-gallery', {
<div v-else class="card-body">
<div class="tt-file-gallery-grid">
<div v-for="file in files" :key="file.id" class="tt-file-gallery-item"
:class="[{ 'selected': selectable && selectedFiles.includes(file.id) }, file.class]"
:class="[{ 'selected': selectable && selectedFiles.includes(file.id), 'is-missing': isMissing(file) }, file.class]"
@click="openViewer(file)">
<div v-if="selectable" class="selection-indicator" @click.stop="toggleSelection(file.id)">
<i :class="selectedFiles.includes(file.id) ? 'fas fa-check-circle text-primary' : 'far fa-circle text-muted'"></i>
</div>
<div class="tt-file-gallery-thumbnail-wrapper">
<img v-if="isImage(file)" :src="'/File/show?id=' + file.fileId + '&size=small'"
class="tt-file-gallery-thumbnail" :alt="file.fileName">
<div v-else-if="isPdf(file)" class="tt-file-gallery-icon-container"><i
class="fas fa-file-pdf fa-3x text-danger"></i></div>
<div v-if="isMissing(file)" class="tt-file-gallery-icon-container">
<i class="fas fa-exclamation-triangle fa-3x text-warning" title="File not found"></i>
</div>
<img v-else-if="isImage(file)" :src="'/File/show?id=' + file.fileId + '&size=small'"
class="tt-file-gallery-thumbnail" :alt="file.fileName" @error="handleImageError(file)">
<div v-else-if="isPdf(file)" class="tt-file-gallery-icon-container">
<i class="fas fa-file-pdf fa-3x text-danger"></i>
</div>
<a v-else :href="'/File/download?id=' + file.fileId" target="_blank" @click.stop
class="tt-file-gallery-icon-container">
<i :class="getFileIcon(file)" class="fa-3x"></i>
</a>
<div class="tt-file-gallery-overlay" @click.stop="openViewer(file)"><i class="fas fa-search-plus"></i>
<div class="tt-file-gallery-overlay" @click.stop="openViewer(file)">
<i class="fas fa-search-plus"></i>
</div>
</div>
<div class="tt-file-gallery-filename" :title="file.fileName">