improved ipam
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
60
db/migrations/20250826160000_rmlworkorder_multi_tenant.php
Normal file
60
db/migrations/20250826160000_rmlworkorder_multi_tenant.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user