Merge branch 'master' into fronkdev

This commit is contained in:
Frank Schubert
2025-04-10 13:27:50 +02:00
74 changed files with 2760 additions and 1027 deletions

View File

@@ -99,6 +99,18 @@ $pagination_entity_name = "Zustimmungserklärungen";
</select>
</div>
<div class="col-2">
<label class="form-label" for="filter_electric_approval">Elektriker freigegeben</label>
<select name="filter[electric_approval]" id="filter_electric_approval" class="form-control">
<option value="">Alle</option>
<option value="!NULL" <?=(array_key_exists("electric_approval", $filter) && $filter["electric_approval"] == "!NULL") ? "selected='selected'" : ""?>>Ja</option>
<option value="NULL" <?=(array_key_exists("electric_approval", $filter) && $filter["electric_approval"] == "NULL") ? "selected='selected'" : ""?>>Nein</option>
</select>
</div>
</div>
<div class="row mt-2">
<div class="col-2">
<label class="form-label" for="filter_conduit_installed_ftu">Leerrohr bis HAK</label>
<select name="filter[conduit_installed_ftu]" id="filter_conduit_installed_ftu" class="form-control">
@@ -108,10 +120,7 @@ $pagination_entity_name = "Zustimmungserklärungen";
</select>
</div>
</div>
<div class="row mt-2">
<div class="col-2">
<div class="col-1">
<label class="form-label" for="filter_inhouse_cabling">Inhouse erledigt</label>
<select name="filter[inhouse_cabling]" id="filter_inhouse_cabling" class="form-control">
<option value="">Alle</option>
@@ -331,6 +340,8 @@ $pagination_entity_name = "Zustimmungserklärungen";
<th>KG</th>
<th>GST-Nr.</th>
<th>Einlagezahl</th>
<th>Wohneinheiten</th>
<th>Bestellungen</th>
<th class="text-center">Anzahl Eigentümer</th>
<th>Anfrageresultat</th>
<th></th>
@@ -354,6 +365,8 @@ $pagination_entity_name = "Zustimmungserklärungen";
<td><?=$item->kg?></td>
<td><?=$item->gst?></td>
<td><?=$item->ez?></td>
<td><?=$item->adb_hausnummer->unit_count ?? ''?></td>
<td><?=$item->preorder_count?></td>
<td class="text-center">
<?php

View File

@@ -144,7 +144,7 @@ $pagination_entity_name = "Adressen";
</td>
</tr>
<tr>
<th>Elektriker</th>
<th>Freigabe Elektriker</th>
<td class="text-monospace"><a data-toggle="modal" data-target="#electricianDateModal" href="#"><i class="fas fa-fw fa-edit"></i></a> <?=($item->inspection_date_electrician) ? date("d.m.Y", $item->inspection_date_electrician) : ""?></td>
<td><input type="checkbox" id="inspection_electrician" class="switchery" data-size="small" data-color="#25b343" data-toggle-param="inspection_electrician" <?=($item->inspection_electrician) ? "checked='checked'" : ""?> /></td>
<td>

View File

@@ -78,7 +78,7 @@
<?php if(array_key_exists("status_id", $filter)): ?>
<?=($filter['status_id'] == $status->id) ? "selected='selected'" : ""?>
<?php else: ?>
<?=($status->id == 3) ? "selected='selected'" : ""?>
<?=(!in_array($me->id, ["145","62","56"]) && $status->id == 3) ? "selected='selected'" : ""?>
<?php endif; ?>
>
<?=$status->code?> - <?=__($status->name."-t")?></option>
@@ -104,6 +104,15 @@
<input type="text" class="form-control" name="filter[building_street]" id="filter_building_street" value="<?=$filter['building_street']?>" />
</div>
<div class="col-2">
<label class="form-label" for="filter_linework_enabled">Baufreigabe</label>
<select name="filter[linework_enabled]" id="filter_linework_enabled" class="form-control">
<option value="">Alle</option>
<option value="1" <?=(array_key_exists("linework_enabled", $filter) && $filter["linework_enabled"] == "1") ? "selected='selected'" : ""?>>Ja</option>
<option value="0" <?=(array_key_exists("linework_enabled", $filter) && $filter["linework_enabled"] == "0") ? "selected='selected'" : ""?>>Nein</option>
</select>
</div>

View File

@@ -1,198 +1,451 @@
<?php
$pagination_baseurl = $this->getUrl($Mod,"Index");
$pagination_baseurl_params = ["filter" => $filter];
$pagination_entity_name = "Patchungen";
if(!is_array($filter)) $filter = [];
// --- Configuration ---
$pagination_baseurl = $this->getUrl($Mod,"Index");
$pagination_baseurl_params = ["filter" => $filter]; // Filters passed via GET
$pagination_entity_name = "Patchungen";
// Ensure $filter is always an array
if(!is_array($filter)) {
$filter = [];
}
// Helper function for safe output (assuming not already handled by framework/template engine)
function e($string) {
return htmlspecialchars($string ?? '', ENT_QUOTES, 'UTF-8');
}
$additionalHead = [
'<link rel="manifest" href="/assets/pwa/patching-manifest.json">'
];
?>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/header.php"); ?>
<!-- start page title -->
<div class="row">
<div class="col-12">
<div class="page-title-box">
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="<?=self::getUrl("Dashboard")?>"><?=MFAPPNAME_SLUG?></a></li>
<li class="breadcrumb-item active">Patchungen</li>
</ol>
</div>
<h4 class="page-title">Patchungen</h4>
<div class="col-12">
<div class="page-title-box">
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="<?=e(self::getUrl("Dashboard"))?>"><?=e(MFAPPNAME_SLUG)?></a></li>
<li class="breadcrumb-item active">Patchungen</li>
</ol>
</div>
<h4 class="page-title">Patchungen</h4>
</div>
</div>
</div>
</div>
<!-- end page title -->
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body mb-3">
<h4 class="header-title mb-3">Filter</h4>
<form method="get" action="<?=self::getUrl("Patching")?>">
<div class="row">
<div class="col-1">
<label class="form-label" for="filter_network_id">Netzgebiet</label>
<select name="filter[network_id]" id="filter_network_id" class="form-control">
<option></option>
<?php foreach($mynetworks as $fnet): ?>
<option value="<?=$fnet->id?>" <?=($filter['network_id'] == $fnet->id) ? "selected='selected'" : ""?>><?=$fnet->name?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-2">
<label class="form-label" for="filter_pop_id">POP</label>
<select name="filter[pop_id]" id="filter_pop_id" class="form-control">
<option></option>
<?php foreach($mynetworks as $fnet): ?>
<?php if(is_array($fnet->pops) && count($fnet->pops)): ?>
<optgroup label="<?=$fnet->name?>">
<?php foreach($fnet->pops as $pop): ?>
<option value="<?=$pop->id?>" <?=($filter['pop_id'] == $pop->id) ? "selected='selected'" : ""?>><?=$pop->name?></option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="col-2">
<label class="form-label" for="filter_patched">Patchstatus</label>
<select name="filter[patched]" id="filter_patched" class="form-control">
<option value="0" <?=($filter['patched'] < 1) ? "selected='selected'" : ""?>>Nicht gepatched</option>
<option value="1" <?=($filter['patched'] == 1) ? "selected='selected'" : ""?>>Gepatched</option>
<option value="2" <?=($filter['patched'] == 2) ? "selected='selected'" : ""?>>Vorpatchen</option>
</select>
</div>
<div class="col-2">
<label class="form-label" for="filter_hide_delayed_finish">Verzögerte Herstellung</label>
<select name="filter[hide_delayed_finish]" id="filter_hide_delayed_finish" class="form-control">
<option value="0" <?=(array_key_exists("hide_delayed_finish", $filter) &&$filter['hide_delayed_finish'] != 1) ? "selected='selected'" : ""?>>Anzeigen</option>
<option value="1" <?=(!array_key_exists("hide_delayed_finish", $filter) || $filter['hide_delayed_finish'] == 1) ? "selected='selected'" : ""?>>Nicht anzeigen</option>
</select>
</div>
<div class="col-1">
<label class="form-label" for="filter_code">Objekt ID</label>
<input type="text" class="form-control" name="filter[code]" id="filter_code" value="<?=$filter['code']?>" />
</div>
<div class="col-2">
<label class="form-label" for="filter_street">Straße</label>
<input type="text" class="form-control" name="filter[street]" id="filter_street" value="<?=$filter['street']?>" />
</div>
</div>
<div class="row mt-2">
<div class="col">
<button type="submit" class="btn btn-primary">Filter anwenden</button>
<a class="btn btn-secondary" href="<?=self::getUrl("Patching")?>">Filter zurücksetzen</a>
</div>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-body mb-3">
<h4 class="header-title">Patchungen</h4>
<div class="col-lg-12">
<?php include(realpath(dirname(__FILE__)."/../")."/tpl/pagination.php"); ?>
<?php include(realpath(dirname(__FILE__)."/../")."/tpl/pagination-summary.php"); ?>
<div class="card">
<div class="card-body mb-3">
<h4 class="header-title mb-3">Filter</h4>
<table class="table table-striped table-hover">
<tr class="table-bordered text-center">
<th colspan="4">Standort</th>
<th colspan="2">ODF</th>
<th colspan="5">Abschluss/Device</th>
<th></th>
</tr>
<tr class="table-bordered">
<th>Netzgebiet</th>
<th>POP</th>
<th>Kunde</th>
<th>Standort</th>
<th>Patchposition ODF</th>
<th>ODF Port</th>
<th>Typ</th>
<th>Splitter / Gerät</th>
<th>Port</th>
<th>Gepatched</th>
<th>Von</th>
<th></th>
</tr>
<?php foreach($terminations as $term): ?>
<tr>
<td><?=$term->building->network->name?></td>
<td>
<?php if($term->getPop()): ?>
<?=$term->getPop()->name?>
<?php else: ?>
<?=$term->building->pop->name?>
<?php endif; ?>
</td>
<td><?=($term->order->owner) ? $term->order->owner->customer_number : ""?><br /> <?=($term->order->owner) ? $term->order->owner->getCompanyOrName() : ""?></td>
<td>
<?=$term->building->street?><br />
<?=$term->building->zip?> <?=$term->building->city?>
</td>
<td class="text-mono text-primary" title="Schrank: <?=($term->workflowitems["ist_schrank"]->value->id) ? $term->workflowitems["ist_schrank"]->value->value_string : $term->workflowitems["schrank"]->value->value_string?> / Einschub: <?=($term->workflowitems["ist_baugruppe"]->value->id) ? $term->workflowitems["ist_baugruppe"]->value->value_string : $term->workflowitems["baugruppe"]->value->value_string?> / Modul: <?=($term->workflowitems["ist_modul"]->value->id) ? $term->workflowitems["ist_modul"]->value->value_string : $term->workflowitems["modul"]->value->value_string?> / Port: <?=($term->workflowitems["ist_ports"]->value->id) ? $term->workflowitems["ist_ports"]->value->value_string : $term->workflowitems["ports"]->value->value_string?>">
<?=($term->workflowitems["ist_schrank"]->value->id) ? $term->workflowitems["ist_schrank"]->value->value_string : $term->workflowitems["schrank"]->value->value_string?> /
<?=($term->workflowitems["ist_baugruppe"]->value->id) ? $term->workflowitems["ist_baugruppe"]->value->value_string : $term->workflowitems["baugruppe"]->value->value_string?> /
<?=($term->workflowitems["ist_modul"]->value->id) ? $term->workflowitems["ist_modul"]->value->value_string : $term->workflowitems["modul"]->value->value_string?> /
<?=($term->workflowitems["ist_ports"]->value->id) ? $term->workflowitems["ist_ports"]->value->value_string : $term->workflowitems["ports"]->value->value_string?>
</td>
<td>
<select name="linework_port" form="term-form-<?=$term->id?>" class="form-control">
<?php foreach($term->getLineworkportPairs() as $ports): ?>
<?php foreach($ports as $p): ?>
<option value="<?=$p?>" <?=($term->patching->linework_ports == $p) ? "selected='selected'" : ""?>><?=$p?></option>
<?php endforeach; ?>
<?php endforeach; ?>
</select>
</td>
<td>
<select name="device_type" form="term-form-<?=$term->id?>" class="form-control">
<option></option>
<option value="splitter" <?=($term->patching->device_type == "splitter") ? "selected='selected'" : ""?>>Splitter</option>
<option value="pon" <?=($term->patching->device_type == "pon") ? "selected='selected'" : ""?>>Shared PON-Port</option>
<option value="switch" <?=($term->patching->device_type == "switch") ? "selected='selected'" : ""?>>Switch</option>
</select>
</td>
<td><input type="text" class="form-control" form="term-form-<?=$term->id?>" name="device_name" value="<?=$term->patching->device_name?>" placeholder="Splitte / Gerät" /></td>
<td><input type="text" class="form-control" form="term-form-<?=$term->id?>" name="device_port" value="<?=$term->patching->device_port?>" placeholder="Port" /></td>
<td><input type="checkbox" class="form-control" form="term-form-<?=$term->id?>" name="patched" value="1" <?=($term->patching->patched == 1) ? "checked='checked'" : ""?> /></td>
<td <?=($term->patching->patched == 1 && $term->patching->patched_by) ? "title='Gepatched: ".date("d.m.Y H:i",$term->patching->patched_date)." von ".$term->patching->patcher->name." (".$term->patching->patcher->address->getCompanyOrName(true).")'" : ""?>>
<?php if($term->patching->patched == 1 && $term->patching->patched_by): ?>
<?=($term->patching->patcher) ? $term->patching->patcher->getAbbrName() : ""?>
<?php endif; ?>
</td>
<td>
<form method="post" id="term-form-<?=$term->id?>" action="<?=self::getUrl("Patching","save", ["s" => $pagination['start'], "filter" => $filter])?>">
<input type="hidden" name="termination_id" value="<?=$term->id?>" />
<input type="submit" class="btn btn-primary" value="Speichern" />
</form>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php include(realpath(dirname(__FILE__)."/../")."/tpl/pagination-summary.php"); ?>
<?php include(realpath(dirname(__FILE__)."/../")."/tpl/pagination.php"); ?>
</div>
<form method="get" action="<?=e(self::getUrl("Patching"))?>">
<div class="row">
<div class="col-12 col-md-6 col-lg-2 mb-3">
<label class="form-label" for="filter_network_id">Netzgebiet</label>
<select name="filter[network_id]" id="filter_network_id" class="form-control">
<option value=""></option>
<?php foreach($mynetworks as $fnet): ?>
<option value="<?=e($fnet->id)?>" <?=($filter['network_id'] ?? null == $fnet->id) ? "selected='selected'" : ""?>><?=e($fnet->name)?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-2 mb-3">
<label class="form-label" for="filter_pop_id">POP</label>
<select name="filter[pop_id]" id="filter_pop_id" class="form-control">
<option value=""></option>
<?php foreach($mynetworks as $fnet): ?>
<?php if(is_array($fnet->pops) && count($fnet->pops)): ?>
<optgroup label="<?=e($fnet->name)?>">
<?php foreach($fnet->pops as $pop): ?>
<option value="<?=e($pop->id)?>" <?=($filter['pop_id'] ?? null == $pop->id) ? "selected='selected'" : ""?>><?=e($pop->name)?></option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-2 mb-3">
<label class="form-label" for="filter_patched">Patchstatus</label>
<select name="filter[patched]" id="filter_patched" class="form-control">
<option value="0" <?=((int)($filter['patched'] ?? 0) < 1) ? "selected='selected'" : ""?>>Nicht gepatched</option>
<option value="1" <?=((int)($filter['patched'] ?? 0) == 1) ? "selected='selected'" : ""?>>Gepatched</option>
<option value="2" <?=((int)($filter['patched'] ?? 0) == 2) ? "selected='selected'" : ""?>>Vorpatchen</option>
</select>
</div>
<div class="col-12 col-md-6 col-lg-2 mb-3">
<label class="form-label" for="filter_hide_delayed_finish">Verzögerte Herstellung</label>
<select name="filter[hide_delayed_finish]" id="filter_hide_delayed_finish" class="form-control">
<option value="0" <?=(isset($filter["hide_delayed_finish"]) && $filter['hide_delayed_finish'] != 1) ? "selected='selected'" : ""?>>Anzeigen</option>
<option value="1" <?=(!isset($filter["hide_delayed_finish"]) || $filter['hide_delayed_finish'] == 1) ? "selected='selected'" : ""?>>Nicht anzeigen</option>
</select>
</div>
<div class="col-12 col-md-6 col-lg-1 mb-3">
<label class="form-label" for="filter_code">Objekt ID</label>
<input type="text" class="form-control" name="filter[code]" id="filter_code" value="<?=e($filter['code'] ?? '')?>" />
</div>
<div class="col-12 col-md-6 col-lg-3 mb-3">
<label class="form-label" for="filter_street">Straße</label>
<input type="text" class="form-control" name="filter[street]" id="filter_street" value="<?=e($filter['street'] ?? '')?>" />
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<button type="submit" class="btn btn-primary">Filter anwenden</button>
<a class="btn btn-secondary" href="<?=e(self::getUrl("Patching"))?>">Filter zurücksetzen</a>
</div>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-body mb-3">
<h4 class="header-title">Patchungen</h4>
<?php // Include pagination controls - Top ?>
<?php include(realpath(dirname(__FILE__)."/../")."/tpl/pagination.php"); ?>
<?php include(realpath(dirname(__FILE__)."/../")."/tpl/pagination-summary.php"); ?>
<div class="table-responsive mt-3"> <?php // Added table-responsive wrapper and margin-top ?>
<table class="table table-striped table-hover">
<thead class="table-bordered"> <?php // Added thead and border class ?>
<tr>
<th colspan="4" class="text-center">Standort</th>
<th colspan="2" class="text-center">ODF</th>
<th colspan="5" class="text-center">Abschluss/Device</th>
<th>Aktion</th> <?php // Renamed last header ?>
</tr>
<tr>
<th>Netzgebiet</th>
<th class="text-nowrap">POP</th> <?php // Added text-nowrap for min-width effect ?>
<th class="text-nowrap">Kunde</th> <?php // Added text-nowrap ?>
<th>Standort</th> <?php // Allow wrapping ?>
<th class="text-nowrap">Patchposition ODF</th> <?php // Added text-nowrap ?>
<th style="min-width: 105px">ODF Port</th> <?php // Select handles resize ok ?>
<th style="min-width: 195px">Typ</th> <?php // Select handles resize ok ?>
<th style="min-width: 190px" class="text-nowrap">Splitter / Gerät</th> <?php // Added text-nowrap ?>
<th style="min-width: 90px" class="text-nowrap">Port</th> <?php // Added text-nowrap ?>
<th>Gepatched</th>
<th class="text-nowrap">Von</th> <?php // Added text-nowrap ?>
<th class="text-nowrap">Speichern</th> <?php // Added text-nowrap ?>
</tr>
</thead>
<tbody>
<?php foreach($terminations as $term): ?>
<?php
// --- Prepare complex data for cleaner output ---
// ODF Patch Position
$wfItems = $term->workflowitems ?? [];
$istSchrankValue = $wfItems["ist_schrank"]->value ?? null;
$schrankValue = $wfItems["schrank"]->value ?? null;
$istBaugruppeValue = $wfItems["ist_baugruppe"]->value ?? null;
$baugruppeValue = $wfItems["baugruppe"]->value ?? null;
$istModulValue = $wfItems["ist_modul"]->value ?? null;
$modulValue = $wfItems["modul"]->value ?? null;
$istPortsValue = $wfItems["ist_ports"]->value ?? null;
$portsValue = $wfItems["ports"]->value ?? null;
$schrank = ($istSchrankValue && $istSchrankValue->id) ? $istSchrankValue->value_string : ($schrankValue ? $schrankValue->value_string : '');
$baugruppe = ($istBaugruppeValue && $istBaugruppeValue->id) ? $istBaugruppeValue->value_string : ($baugruppeValue ? $baugruppeValue->value_string : '');
$modul = ($istModulValue && $istModulValue->id) ? $istModulValue->value_string : ($modulValue ? $modulValue->value_string : '');
$ports = ($istPortsValue && $istPortsValue->id) ? $istPortsValue->value_string : ($portsValue ? $portsValue->value_string : '');
$odfPositionTitle = "Schrank: " . e($schrank) . " / Einschub: " . e($baugruppe) . " / Modul: " . e($modul) . " / Port: " . e($ports);
$odfPositionDisplay = e($schrank) . " / " . e($baugruppe) . " / " . e($modul) . " / " . e($ports);
// Patched By Info
$patching = $term->patching ?? null;
$patcher = $patching ? ($patching->patcher ?? null) : null;
$patchedByTitle = '';
$patchedByDisplay = '';
if ($patching && $patching->patched == 1 && $patching->patched_by && $patcher) {
$patcherName = $patcher->name ?? 'Unbekannt';
$patcherCompany = $patcher->address ? $patcher->address->getCompanyOrName(true) : 'N/A';
$patchedDate = date("d.m.Y H:i", $patching->patched_date);
$patchedByTitle = "Gepatched: {$patchedDate} von " . e($patcherName) . " (" . e($patcherCompany) . ")";
$patchedByDisplay = $patcher->getAbbrName() ?? '';
}
// Customer Info
$customer = $term->order->owner ?? null;
$customerNumber = $customer ? $customer->customer_number : '';
$customerName = $customer ? $customer->getCompanyOrName() : '';
// Location Info
$building = $term->building ?? null;
$networkName = $building ? ($building->network->name ?? '') : '';
$popName = '';
$termPop = $term->getPop(); // Call potentially expensive method once
if ($termPop) {
$popName = $termPop->name ?? '';
} elseif ($building && $building->pop) {
$popName = $building->pop->name ?? '';
}
$street = $building->street ?? '';
$zip = $building->zip ?? '';
$city = $building->city ?? '';
// Linework Ports
$lineworkPorts = $term->getLineworkportPairs() ?? [];
$currentLineworkPort = $patching ? $patching->linework_ports : null;
// Device Info
$deviceType = $patching ? $patching->device_type : '';
$deviceName = $patching ? $patching->device_name : '';
$devicePort = $patching ? $patching->device_port : '';
$isPatched = $patching ? ($patching->patched == 1) : false;
?>
<tr class="align-middle"> <?php // Added align-middle ?>
<td><?=e($networkName)?></td>
<td><?=e($popName)?></td>
<td><?=e($customerNumber)?><br /><?=e($customerName)?></td>
<td>
<?=e($street)?><br />
<?=e($zip)?> <?=e($city)?>
</td>
<td class="text-mono text-primary" title="<?=$odfPositionTitle?>">
<?=$odfPositionDisplay?>
</td>
<td>
<select name="linework_port" form="term-form-<?=e($term->id)?>" class="form-control"> <?php // Removed -sm ?>
<?php foreach($lineworkPorts as $portGroup): ?>
<?php foreach($portGroup as $p): ?>
<option value="<?=e($p)?>" <?=($currentLineworkPort == $p) ? "selected='selected'" : ""?>><?=e($p)?></option>
<?php endforeach; ?>
<?php endforeach; ?>
</select>
</td>
<td>
<select name="device_type" form="term-form-<?=e($term->id)?>" class="form-control"> <?php // Removed -sm ?>
<option value=""></option>
<option value="splitter" <?=($deviceType == "splitter") ? "selected='selected'" : ""?>>Splitter</option>
<option value="pon" <?=($deviceType == "pon") ? "selected='selected'" : ""?>>Shared PON-Port</option>
<option value="switch" <?=($deviceType == "switch") ? "selected='selected'" : ""?>>Switch</option>
</select>
</td>
<td><input type="text" class="form-control device-name-input" form="term-form-<?=e($term->id)?>" name="device_name" value="<?=e($deviceName)?>" placeholder="Splitter / Gerät" /></td> <?php // Removed -sm ?>
<td><input type="text" class="form-control" form="term-form-<?=e($term->id)?>" name="device_port" value="<?=e($devicePort)?>" placeholder="Port" /></td> <?php // Removed -sm ?>
<td class="text-center"> <?php // Added text-center ?>
<input type="hidden" name="patched" value="0" form="term-form-<?=e($term->id)?>" /> <?php // Ensure 0 is sent if checkbox is unchecked ?>
<input type="checkbox" class="form-check-input" style="position:unset !important;height: 2em; width: 2em;" form="term-form-<?=e($term->id)?>" name="patched" value="1" <?=($isPatched) ? "checked='checked'" : ""?> /> <?php // Adjusted checkbox style slightly ?>
</td>
<td <?=!empty($patchedByTitle) ? 'title="' . $patchedByTitle . '"' : ''?>>
<?=e($patchedByDisplay)?>
</td>
<td>
<form method="post" id="term-form-<?=e($term->id)?>" action="<?=e(self::getUrl("Patching","save", ["s" => $pagination['start'] ?? 0, "filter" => $filter]))?>">
<input type="hidden" name="termination_id" value="<?=e($term->id)?>" />
<button type="submit" class="btn btn-primary">Speichern</button> <?php // Changed value to button text, removed -sm ?>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div> <?php // End table-responsive ?>
<?php // Include pagination controls - Bottom ?>
<?php include(realpath(dirname(__FILE__)."/../")."/tpl/pagination-summary.php"); ?>
<?php include(realpath(dirname(__FILE__)."/../")."/tpl/pagination.php"); ?>
</div>
</div>
</div>
</div>
</div>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>
<style>
@media (max-width: 768px) {
.pagination {
overflow-x: auto;
white-space: nowrap;
}
}
.select2-selection__rendered {
line-height: 38px !important;
}
.select2-container .select2-selection--single {
height: 38px !important;
}
.select2-selection__arrow {
height: 37px !important;
}
@media all and (display-mode: standalone) {
#topnav {
display: none !important;
}
.wrapper {
padding-top: 0 !important;
}
}
</style>
<script>
window.DEVICES = <?= json_encode($devices) ?>;
window.DEVICES = window.DEVICES.filter(device => {
return device.name.toLowerCase().includes('olt') || device.name.toLowerCase().includes('pon')
});
</script>
<script>
$(function() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/Patching/sw', { scope: '/' })
.then(registration => {
console.log('Patching PWA Service Worker registered with scope:', registration.scope);
})
.catch(error => {
console.error('Patching PWA Service Worker registration failed:', error);
});
}
if (typeof $().select2 === 'undefined') {
console.error('Select2 library is not loaded.');
// Optionally display an error to the user or fall back gracefully
return; // Stop execution if Select2 is missing
}
const ipNumRegex = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[/-](\d+)$/;
// Helper: Find device object by IP
function findDeviceByIp(ip) {
if (!window.DEVICES || !Array.isArray(window.DEVICES)) {
console.error('DEVICES array not found or invalid.');
return null;
}
return window.DEVICES.find(device => device.ip === ip);
}
// Helper: Update the original hidden input field's value
function updateOriginalInput($originalInput, ip, number) {
const cleanNumber = String(number).trim(); // Ensure number is a string and trimmed
if (ip && cleanNumber) {
$originalInput.val(ip + '/' + cleanNumber).trigger('change'); // Update and trigger change
} else {
$originalInput.val('').trigger('change'); // Clear if incomplete and trigger change
}
}
// --- Initialize for each device name input ---
$('.device-name-input').each(function() {
const $originalInput = $(this);
const $td = $originalInput.closest('td'); // Get the parent cell for structure
let initialValue = $originalInput.val().trim();
// Create wrapper for the Select2 component (Select + Number Input)
// Hide it initially
const $selectWrapper = $('<div class="device-select-wrapper" style="display: none; align-items: center; gap: 5px;"></div>');
const $select = $('<select class="form-control device-select" style="flex-grow: 1; width: auto;"></select>'); // Let flexbox handle width
const $numberInput = $('<input type="number" class="form-control device-number" placeholder="Nr." style="width: 70px; flex-grow: 0; flex-shrink: 0;">'); // Fixed width number input
// Populate Select options
$select.append('<option value="">Gerät auswählen...</option>'); // Placeholder
if (window.DEVICES && Array.isArray(window.DEVICES)) {
window.DEVICES.forEach(device => {
// Display name and IP for clarity in dropdown
const displayText = `${device.name} (${device.ip})`;
$select.append($('<option></option>').val(device.ip).text(displayText));
});
} else {
console.warn("DEVICES array is missing or invalid for row:", $originalInput.attr('form'));
// Optionally add a disabled option indicating an error
$select.append('<option value="" disabled>Device list error</option>');
}
$selectWrapper.append($select).append($numberInput);
$td.append($selectWrapper); // Add the hidden wrapper to the cell
// Function to show Select2 component and hide original input
function showSelectComponent(ip, number) {
$originalInput.hide();
$select.val(ip || '').trigger('change.select2'); // Set value and update Select2 display
$numberInput.val(number || '');
$selectWrapper.css('display', 'flex'); // Use flex to show side-by-side
}
// Function to show original input and hide Select2 component
function showOriginalInput() {
$selectWrapper.hide();
$originalInput.show();
}
// Initial state determination
const match = initialValue.match(ipNumRegex);
if (match) {
const initialIp = match[1];
const initialNumber = match[2];
const foundDevice = findDeviceByIp(initialIp);
if (foundDevice) {
// Valid format and known device -> Show Select2 component
showSelectComponent(initialIp, initialNumber);
} else {
// Valid format but unknown device -> Show original input
showOriginalInput();
}
} else if (!initialValue) {
// Empty -> Show Select2 component (empty)
showSelectComponent(null, null);
} else {
// Invalid format -> Show original input
showOriginalInput();
}
// Initialize Select2 *after* adding options and inserting into DOM
$select.select2({
width: 'style', // Adjust width based on container
// dropdownParent: $td // Optional: Attach dropdown to cell if needed for positioning
});
// --- Event Listeners ---
// Update original input when Select2 or number changes
$select.on('change', function() {
const selectedIp = $(this).val();
const currentNumber = $numberInput.val();
updateOriginalInput($originalInput, selectedIp, currentNumber);
});
$numberInput.on('input change', function() { // Use 'input change' for better responsiveness
const currentIp = $select.val();
const selectedNumber = $(this).val();
updateOriginalInput($originalInput, currentIp, selectedNumber);
});
// Check original input on change/blur to potentially switch view
$originalInput.on('change blur', function() {
const currentValue = $(this).val().trim();
const currentMatch = currentValue.match(ipNumRegex);
if (currentMatch) {
const currentIp = currentMatch[1];
const currentNumber = currentMatch[2];
const device = findDeviceByIp(currentIp);
if (device) {
// Switched to valid/known format -> Show Select2
showSelectComponent(currentIp, currentNumber);
}
// else: Keep showing original input (valid format, but unknown device)
}
// else: Keep showing original input (invalid format)
});
}); // End .each loop
});
</script>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>

View File

@@ -74,7 +74,7 @@
<?php if(array_key_exists("status_id", $filter)): ?>
<?=($filter['status_id'] == $status->id) ? "selected='selected'" : ""?>
<?php else: ?>
<?=($status->id == 3) ? "selected='selected'" : ""?>
<?=(!in_array($me->id, ["145","62","56"]) && $status->id == 3) ? "selected='selected'" : ""?>
<?php endif; ?>
>
<?=$status->code?> - <?=__($status->name."-b")?></option>
@@ -91,6 +91,15 @@
<label class="form-label" for="filter_street">Straße</label>
<input type="text" class="form-control" name="filter[street]" id="filter_street" value="<?=$filter['street']?>" />
</div>
<div class="col-2">
<label class="form-label" for="filter_pipework_enabled">Baufreigabe</label>
<select name="filter[pipework_enabled]" id="filter_pipework_enabled" class="form-control">
<option value="">Alle</option>
<option value="1" <?=(array_key_exists("pipework_enabled", $filter) && $filter["pipework_enabled"] == "1") ? "selected='selected'" : ""?>>Ja</option>
<option value="0" <?=(array_key_exists("pipework_enabled", $filter) && $filter["pipework_enabled"] == "0") ? "selected='selected'" : ""?>>Nein</option>
</select>
</div>
@@ -99,7 +108,10 @@
<div class="row mt-2">
<div>
<button type="submit" class="btn btn-primary">Filter anwenden</button>
<a class="btn btn-secondary" href="<?=self::getUrl("Pipework")?>">Filter zurücksetzen</a>
<a class="btn btn-secondary" href="<?=self::getUrl("Pipework")?>">Filter zurücksetzen</a>
<?php if ($me->is(["Admin"])) :?>
<a class="btn btn-outline-secondary" href="<?=self::getUrl("Pipework", "History")?>">Zur Historie</a>
<?php endif; ?>
</div>
<div style="width: 512px;">
<div class="row">

View File

@@ -322,8 +322,8 @@ $pagination_entity_name = "Vorbestellungen";
</select>
</div>
<div class="col-sm-12 col-md-2">
<label class="form-label" for="filter_rimo_workorder">Rimo Workorder Status</label>
<div class="col-sm-12 col-md-1">
<label class="form-label" for="filter_rimo_workorder">Workorder Status</label>
<select name="filter[rimo_workorder_status][]" id="filter_rimo_workorder_status" multiple class="form-control">
<option value="Clarify" <?=(isset($filter) && array_key_exists("rimo_workorder_status", $filter) && is_array($filter['rimo_workorder_status']) && in_array("Clarify", $filter['rimo_workorder_status'])) ? "selected='selected'" : ""?>>Clarify</option>
<option value="Accepted" <?=(isset($filter) && array_key_exists("rimo_workorder_status", $filter) && is_array($filter['rimo_workorder_status'])&& in_array("Accepted", $filter['rimo_workorder_status'])) ? "selected='selected'" : ""?>>Accepted</option>
@@ -335,6 +335,13 @@ $pagination_entity_name = "Vorbestellungen";
</select>
</div>
<div class="col-sm-12 col-md-1">
<label class="form-label" for="filter_fcp">FCP</label>
<select name="filter[fcp][]" id="filter_fcp" multiple class="form-control">
<option value="">Kein FCP gefunden</option>
</select>
</div>
<div class="col-sm-12 col-md-2">
<label class="form-label" for="filter_rimo_workorder_team_id">Rimo Workorder Assigned Team</label>
<select name="filter[rimo_workorder_team_id]" id="filter_rimo_workorder_team_id" class="form-control">
@@ -848,6 +855,8 @@ $pagination_entity_name = "Vorbestellungen";
});
//fetch fcps and show on map
getFCPs(preorderMap);
// calculate center position
mapCenterPos = GetCenterFromDegrees(all_coords);
@@ -856,6 +865,23 @@ $pagination_entity_name = "Vorbestellungen";
return true;
}
async function getFCPs(map) {
var fcp = await $.get("<?=self::getUrl("Preorder", "Api")?>", {
do: "getFCPsForCampaign",
campaign_id: "<?=$campaign->id?>"
});
if(fcp.status == "OK") {
fcp.result.forEach((fcp) => {
var icon = L.MakiMarkers.icon({icon: "viewpoint", color: "yellow", size: "m"});
var marker = L.marker([fcp.lat, fcp.lng], {icon: icon}).addTo(map);
var google_maps_link = "https://www.google.com/maps/search/?api=1&query=" + fcp.lat + "," + fcp.lng;
var popup_content = "<a href='" + google_maps_link + "' target='_blank'>Google Maps</a><br />" + fcp.text;
marker.bindPopup(popup_content);
});
}
}
function centerMap() {
preorderMap.setView(mapCenterPos, 12);
}
@@ -1483,4 +1509,45 @@ $pagination_entity_name = "Vorbestellungen";
}
</script>
<script>
$(document).ready(function() {
const fcpSelect = $("#filter_fcp");
const campaignSelect = $("#filter_preordercampaign_id");
const apiUrl = "<?=self::getUrl("Preorder", "Api")?>";
fcpSelect.select2({ data: [], placeholder: "Bitte Kampagne auswählen", allowClear: true });
campaignSelect.on("change", function() {
const campaign_id = $(this).val();
if (!campaign_id) {
fcpSelect.empty().select2({ data: [], placeholder: "Bitte Kampagne auswählen", allowClear: true });
return;
}
$.get(apiUrl, { do: "getFCPsForCampaign", campaign_id: campaign_id }, (success) => {
let fcpData = [];
let opts = { data: [], placeholder: "Bitte Kampagne auswählen", allowClear: true };
if (success?.status === "OK" && Array.isArray(success.result)) {
fcpData = success.result;
fcpData.unshift({ id: "", text: "" });
fcpData.sort((a, b) => {
const aN = a.text.replace(/\D/g, ""), bN = b.text.replace(/\D/g, "");
return aN && bN ? parseInt(aN, 10) - parseInt(bN, 10) : a.text.localeCompare(b.text);
});
opts = { data: fcpData, placeholder: "", allowClear: true };
fcpSelect.empty().select2(opts);
const searchParams = new URLSearchParams(window.location.search);
const fcpValues = searchParams.getAll("filter[fcp][]");
if (fcpValues && fcpValues.length > 0) {
fcpSelect.val(fcpValues).trigger("change");
}
} else {
fcpSelect.empty().select2(opts);
}
}, "json").fail(() => {
fcpSelect.empty().select2({ data: [], placeholder: "Fehler", allowClear: true });
});
});
campaignSelect.trigger("change");
});
</script>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>

View File

@@ -12,6 +12,7 @@ if(!$no_filename) {
$status_flags_header = [];
foreach(PreorderStatusflagModel::getAll() as $sflag) {
$status_flags_header[$sflag->code] = $sflag->code;
$status_flags_header[$sflag->code . " Datum"] = $sflag->code . " Datum";
}
?>
@@ -35,6 +36,16 @@ while($data = mysqli_fetch_object($res)):
$statusflags = [];
foreach($preorder->statusflags as $sflag) {
$statusflags[$sflag->code] = $sflag->value->value ? 1 : 0;
if($sflag->value->value) {
$historyEntry = PreorderHistoryModel::getFirst([
"preorder_id" => $preorder->id,
"key" => "preorderstatusflag-".$sflag->id."-value",
"new_value" => 1
]);
$statusflags[$sflag->code . " Datum"] = ($historyEntry) ? date("Y-m-d H:i:s", $historyEntry->create) : "";
} else {
$statusflags[$sflag->code . " Datum"] = "";
}
}
$discounts = [];

View File

@@ -416,6 +416,33 @@
<td class="text-monospace"><?=$preorder->adb_wohneinheit->ftu_data["id"]?>
</tr>
</table>
<h3>FCP</h3>
<?php
if($preorder->fcp): ?>
<table class="table table-sm table-striped">
<tr>
<th>FCP Name:</th>
<td class="text-monospace"><?=$preorder->fcp->name?>
</tr><tr>
<th>FCP External ID:</th>
<td class="text-monospace"><?=$preorder->fcp->rimo_id?>
</tr>
<tr>
<th>FCP Execution State:</th>
<td class="text-monospace"><?=$preorder->fcp->rimo_ex_state?>
</tr><tr>
<th>FCP Operational State:</th>
<td class="text-monospace"><?=($preorder->fcp->rimo_op_state != "Undefined") ? $preorder->fcp->rimo_op_state : ""?>
</tr><tr>
<th>FCP Building Type:</th>
<td class="text-monospace"><?=$preorder->fcp->building_type?>
</tr>
</table>
<?php else: ?>
<p>Kein FCP zugewiesen</p>
<?php endif; ?>
</div>
</div>

View File

@@ -73,6 +73,10 @@
<?php else: ?>
<a class="btn btn-secondary" href="<?=self::getUrl("Preorderlogistics")?>">Filter zurücksetzen</a>
<?php endif; ?>
<button id="printAllInView" type="button" class="btn btn-success">Alle in der Liste drucken</button>
<button id="csvExportAddressesAndMarkAsSent" type="button" class="btn btn-success">CSV Export für Versand</button>
</div>
</div>
</form>
@@ -106,7 +110,16 @@
<th></th>
</tr>
<?php foreach($preorders as $preorder): ?>
<tr class="preorder-list-tr" id="preorder-<?=$preorder->id?>">
<tr class="preorder-list-tr" id="preorder-<?=$preorder->id?>"
data-ucode="<?=$preorder->ucode?>"
data-oaid="<?=$preorder->oaid?>"
data-addr-name="<?=$preorder->company ? $preorder->company : $preorder->firstname." ".$preorder->lastname?>"
data-addr-street="<?=$preorder->street?><?=($preorder->housenumber) ? " ".$preorder->housenumber : ""?>"
data-addr-zip="<?=$preorder->zip?>"
data-addr-city="<?=$preorder->city?>"
data-phone="<?=$preorder->phone?>"
data-email="<?=$preorder->email?>"
>
<td class="text-right align-middle"><button type="button" class="btn btn-sm btn-success font-weight-bold" onclick="printShippingSlip(<?=$preorder->id?>)"><i class="fas fa-fw fa-print"></i> DRUCKEN</button> </td>
<td class="text-center align-middle">
<label>Versandt <i class="fas fa-check text-success hidden" id="sent-label-<?=$preorder->id?>"></i>
@@ -197,4 +210,108 @@
</script>
<script>
function printAllInView() {
console.log("printAllInView");
$("#printAllInView").html('<i class="fas fa-spinner fa-spin"></i> Bitte warten...');
$("#printAllInView").prop("disabled", true);
var preorderIds = [];
$(".preorder-list-tr").each(function() {
var id = $(this).attr("id").replace("preorder-", "");
preorderIds.push(id);
});
var printUrls = preorderIds.map(function(id) {
return "<?=self::getUrl("Preorderlogistics", "print")?>?id=" + id;
});
var printWindows = [];
var loadedCount = 0;
var combinedHtml = "";
function loadAndCombine(url, index) {
$.get(url, function(html) {
combinedHtml += html;
loadedCount++;
if (loadedCount === printUrls.length) {
var combinedWindow = window.open("", "_blank");
combinedWindow.document.write(combinedHtml);
combinedWindow.document.close();
// sleep for 1 second to allow the window to load
setTimeout(function() {
combinedWindow.focus();
combinedWindow.print();
}, 1000);
$("#printAllInView").html('Alle in der Liste drucken');
$("#printAllInView").prop("disabled", false);
}
});
}
printUrls.forEach(loadAndCombine);
}
$("#printAllInView").on("click", printAllInView);
async function csvExportAddressesAndMarkAsSent(){
//show confirmation dialog to the user browser api
if(!confirm("Möchten Sie die Adressen als CSV exportieren und als versendet markieren?")) {
return;
}
$("#csvExportAddressesAndMarkAsSent").html('<i class="fas fa-spinner fa-spin"></i> Bitte warten...');
// show a confirmation dialog to the user
// use .preorder-list-tr and data-ucode, data-oaid, data-addr-name, data-addr-street, data-addr-zip, data-addr-city to export to csv
// after exporting mark as sent for each preorder
var preorderIds = [];
$(".preorder-list-tr").each(function() {
var id = $(this).attr("id").replace("preorder-", "");
preorderIds.push(id);
});
var csvData = "ucode,oaid,addr_name,addr_street,addr_zip,addr_city,phone,email\n";
$(".preorder-list-tr").each(function() {
var ucode = $(this).data("ucode");
var oaid = $(this).data("oaid");
var addr_name = $(this).data("addr-name");
var addr_street = $(this).data("addr-street");
var addr_zip = $(this).data("addr-zip");
var addr_city = $(this).data("addr-city");
var phone = $(this).data("phone");
var email = $(this).data("email");
csvData += ucode + "," + oaid + "," + addr_name + "," + addr_street + "," + addr_zip + "," + addr_city + "," + phone + "," + email + "\n";
});
// create a blob from the csv data
var blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" });
var link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.setAttribute("download", "addresses.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// mark all as sent
for (const preorderId of preorderIds) {
await $.post("<?=self::getUrl("Preorderlogistics", "Api")?>", {
do: "saveSent",
id: preorderId,
sent: 1
});
$("#sent-label-" + preorderId).show();
$("#sent-" + preorderId).prop("checked", true);
}
window.notify('success', 'Adressen als CSV exportiert und als versendet markiert');
$("#csvExportAddressesAndMarkAsSent").html('CSV Export für Versand');
}
$("#csvExportAddressesAndMarkAsSent").on("click", csvExportAddressesAndMarkAsSent);
</script>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>

View File

@@ -17,17 +17,18 @@
<style>
body {
margin-top: 0;
padding-top: 20pt;
font-family: "Calibri", "Open Sans", "Dejavu Sans", dejavusans, sans-serif;
font-size: 12pt;
font-size: 11pt;
}
.container {
padding-top: 20pt;
margin-left: 64pt;
margin-right: 64pt;
}
</style>
</head>
<body>
<div>
<div class="container">
<div style="text-align: right;">
<img src="<?=self::getResourcePath()?>assets/images/rml-logo-header.png" style="height:80pt;" />
@@ -42,8 +43,9 @@
<?php endif; ?>
<?php if($preorder->lastname): ?>
<?=$preorder->firstname?> <?=$preorder->lastname?><br />
<?php endif; ?>
<?=$preorder->street?><br />
<?=$preorder->street?> <?=$preorder->housenumber?><br />
<?=$preorder->zip?> <?=$preorder->city?>
</p>
<p style="text-align: right; padding-top: 4pt;">Liezen, <?=date("d.m.Y")?></p>
@@ -98,11 +100,10 @@
<div>
<p style="font-size: 8pt">PS: Für die Region, bleibt in der Region, gehört der Region. Dieser Mehrwert zeichnet unsere regionale Glasfaser-Offensive in den 29 Gemeinden des Bezirks Liezen aus. Sie sind mit Ihrem Glasfaseranschluss jetzt ein Teil davon.</p>
</div>
<br><br>
</div>
<img src="<?=self::getResourcePath()?>assets/images/rml-preorderlogistic-footer.png" style="width:100%;margin-top: 12pt;margin-left:4pt;margin-right:4pt" />
</div>
<div style="position: absolute; bottom:0;">
<img src="<?=self::getResourcePath()?>assets/images/rml-preorderlogistic-footer.png" style="width:100%;" />
</div>
</body>
</html>
</html>

View File

@@ -21,6 +21,7 @@ $additionalCSS = [
...$additionalCSS,
'plugins/daterangepicker/daterangepicker.css',
'plugins/vue/tt-components/css/tt-table.css',
'plugins/vue/tt-components/css/tt-tooltip.css',
'plugins/vue/tt-components/css/tt-loader.css',
'plugins/vue/tt-components/css/tt-position-manager.css',
];

View File

@@ -75,6 +75,11 @@
<?php endforeach; ?>
<?php endif; ?>
<?php if(isset($additionalHead) && is_array($additionalHead) && count($additionalHead)):
foreach($additionalHead as $head): ?>
<?=$head?>
<?php endforeach; endif;?>
<?php if(MFAPPNAME == "devthetool"): ?>
<style type="text/css">
body {

View File

@@ -141,17 +141,16 @@
<?php endif; ?>
<?php if($me->is(["Admin"]) || $me->can(["Cpeprovisioning", "Cpeshipping"])): ?>
<li class="has-submenu mobile-hide">
<li class="has-submenu">
<a href="#">
<i class="fad fa-fw fa-running"></i>Netzbetrieb <div class="arrow-down"></div>
</a>
<ul class="submenu">
<?php if($me->isAdmin() || $me->can("Cpeprovisioning")): ?><li><a href="<?=self::getUrl("Cpeprovisioning")?>"><i class="fad fa-fw fa-hdd text-info"></i> CPE Provisioning</a></li><?php endif; ?>
<?php if($me->isAdmin() || $me->can("Cpeshipping")): ?><li><a href="<?=self::getUrl("Cpeshipping")?>"><i class="fad fa-fw fa-shipping-fast text-info"></i> CPE Versand</a></li><?php endif; ?>
<?php if($me->isAdmin() || $me->can("Cpeprovisioning")): ?><li class="mobile-hide"><a href="<?=self::getUrl("Cpeprovisioning")?>"><i class="fad fa-fw fa-hdd text-info"></i> CPE Provisioning</a></li><?php endif; ?>
<?php if($me->isAdmin() || $me->can("Cpeshipping")): ?><li class="mobile-hide"><a href="<?=self::getUrl("Cpeshipping")?>"><i class="fad fa-fw fa-shipping-fast text-info"></i> CPE Versand</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("Domain")?>"><i class="fad fa-fw fa-globe text-info"></i> Domains</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("IpNetwork")?>"><i class="fa-solid fa-network-wired text-info"></i> IPAM</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("DeviceMonitoring/congestion")?>"><i class="fa-solid fa-block-brick-fire text-info"></i> Device Congestion</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("MaintenanceNotification")?>"><i class="fa-solid fa-envelope-badge text-info"></i> Wartungsmeldungen</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li class="mobile-hide"><a href="<?=self::getUrl("MaintenanceNotification")?>"><i class="fa-solid fa-envelope-badge text-info"></i> Wartungsmeldungen</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("Radius")?>"><i class="fas fa-broadcast-tower text-info"></i> Radius</a></li><?php endif; ?>
</ul>
</li>
@@ -169,9 +168,10 @@
<ul class="submenu">
<?php if($me->can("WarehouseAdmin") || $me->can("WarehouseUser")): ?><li class="has-sub-submenu font-weight-bold"><a>XINON</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin") || $me->can("WarehouseUser")): ?><li><a href="<?=self::getUrl("WarehouseArticle")?>"><i class="far fa-fw fa-box text-info"></i> Artikel</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseItem")?>"><i class="far fa-fw fa-boxes text-info"></i> Lagerbestand (WIP)</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseOrderRecommendation")?>"><i class="far fa-fw fa-box-full text-info"></i> Bestellvorschläge (WIP)</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseOrder")?>"><i class="far fa-fw fa-shopping-bag text-info"></i> Bestellungen (WIP)</a></li><?php endif; ?>
<!-- --><?php //if($me->can("WarehouseAdmin")): ?><!--<li><a href="--><?php //=self::getUrl("WarehouseItem")?><!--"><i class="far fa-fw fa-boxes text-info"></i> Lagerbestand (WIP)</a></li>--><?php //endif; ?>
<!-- --><?php //if($me->can("WarehouseAdmin")): ?><!--<li><a href="--><?php //=self::getUrl("WarehouseOrderRecommendation")?><!--"><i class="far fa-fw fa-box-full text-info"></i> Bestellvorschläge (WIP)</a></li>--><?php //endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseOrder")?>"><i class="far fa-fw fa-shopping-bag text-info"></i> Bestellungen</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseOffer")?>"><i class="far fa-fw fa-file-signature text-info"></i> Angebote</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin") || $me->can("WarehouseUser")): ?><li><a href="<?=self::getUrl("WarehouseOrderRequest")?>"><i class="far fa-fw fa-shopping-cart text-info"></i> Bestellwünsche</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin") || $me->can("WarehouseUser")): ?><li><a href="<?=self::getUrl("WarehouseShippingNote")?>"><i class="far fa-fw fa-shipping-fast text-info"></i> Lieferscheine</a></li><?php endif; ?>

View File

@@ -50,13 +50,21 @@
<?php endforeach; ?>
<?php endif; ?>
<?php if(MFAPPNAME == "devthetool"): ?>
<style type="text/css">
<?php if(isset($additionalHead) && is_array($additionalHead) && count($additionalHead)):
foreach($additionalHead as $head): ?>
<?=$head?>
<?php endforeach; endif;?>
<style>
<?php if(MFAPPNAME == "devthetool"): ?>
body {
border-left: 8px dashed #f672a7;
}
<?php endif; ?>
body {
min-height: 100vh;
}
</style>
<?php endif; ?>
</head>

View File

@@ -1,195 +1,16 @@
<?php
class ADBRimoFcp extends mfBaseModel {
protected function init() {
$this->db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
$this->table = "RimoFcp";
}
public function getProperty($name) {
if($this->$name == null) {
$classname = ucfirst($name);
$idfield = $name."_id";
$this->$name = new $classname($this->$idfield);
if($this->$name->id) {
return $this->$name;
} else {
return null;
}
}
return $this->$name;
}
/********************************
* Begin static Model functions
*/
public static function create(Array $data) {
$model = new ADBRimoFcp();
$table_fields = [
"netzgebiet_id", "name", "rimo_id", "label", "building_type", "rimo_ex_state", "rimo_op_state", "gps_lat", "gps_long",
"create","edit"
];
foreach($data as $field => $value) {
if(in_array($field, $table_fields)) {
$model->$field = $value;
}
}
return $model;
}
public static function getFirst($filter) {
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
$where = self::getSqlFilter($filter);
$sql = "SELECT RimoFcp.* FROM RimoFcp
WHERE $where
ORDER BY name
LIMIT 1";
mfLoghandler::singleton()->debug($sql);
$res = $db->query($sql);
if($db->num_rows($res)) {
$data = $db->fetch_object($res);
$item = new ADBRimoFcp($data);
if($item->id) {
return $item;
} else {
return null;
}
}
return null;
}
public static function getAll() {
$items = [];
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
$res = $db->select("RimoFcp", "*", "1=1 ORDER BY name");
if($db->num_rows($res)) {
while($data = $db->fetch_object($res)) {
$items[] = new ADBRimoFcp($data);
}
}
return $items;
}
public static function count($filter) {
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
$where = self::getSqlFilter($filter);
$sql = "SELECT COUNT(*) as cnt FROM RimoFcp
WHERE $where
";
$res = $db->query($sql);
if($db->num_rows($res)) {
$data = $db->fetch_object($res);
return $data->cnt;
}
return 0;
}
public static function search($filter, $limit = false) {
$items = [];
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
$where = self::getSqlFilter($filter);
$sql = "SELECT RimoFcp.* FROM RimoFcp
WHERE $where
ORDER BY name";
mfLoghandler::singleton()->debug($sql);
if(is_array($limit) && count($limit)) {
if(is_numeric($limit['start']) && is_numeric($limit['count'])) {
$sql .= " LIMIT ".$limit['start'].", ".$limit['count'];
} elseif(is_numeric($limit['count'])) {
$sql .= " LIMIT ".$limit['count'];
}
}
$res = $db->query($sql);
if($db->num_rows($res)) {
while($data = $db->fetch_object($res)) {
$items[] = new ADBRimoFcp($data);
}
}
return $items;
}
private static function getSqlFilter($filter) {
$where = "1=1 ";
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
if(array_key_exists("netzgebiet_id", $filter)) {
$netzgebiet_id = $filter['netzgebiet_id'];
if(is_numeric($netzgebiet_id)) {
$where .= " AND netzgebiet_id=$netzgebiet_id";
} elseif(is_array($netzgebiet_id) && count($netzgebiet_id)) {
$where .= " AND netzgebiet_id IN (". implode(",", $netzgebiet_id).")";
} elseif($netzgebiet_id === null) {
$where .= " AND netzgebiet_id IS NULL";
}
}
if(array_key_exists("name", $filter)) {
$name = $db->escape($filter['name']);
if($name) {
$where .= " AND RimoFcp.name='$name'";
}
}
if(array_key_exists("label", $filter)) {
$label = $db->escape($filter['label']);
if($label) {
$where .= " AND RimoFcp.label='$label'";
}
}
if(array_key_exists("building_type", $filter)) {
$building_type = $db->escape($filter['building_type']);
if($building_type) {
$where .= " AND RimoFcp.building_type='$building_type'";
}
}
if(array_key_exists("rimo_ex_state", $filter)) {
$rimo_ex_state = $db->escape($filter['rimo_ex_state']);
if($rimo_ex_state) {
$where .= " AND RimoFcp.rimo_ex_state='$rimo_ex_state'";
}
}
if(array_key_exists("rimo_op_state", $filter)) {
$rimo_op_state = $db->escape($filter['rimo_op_state']);
if($rimo_op_state) {
$where .= " AND RimoFcp.rimo_op_state='$rimo_op_state'";
}
}
if(array_key_exists("rimo_id", $filter)) {
$rimo_id = $db->escape($filter['rimo_id']);
if($rimo_id) {
$where .= " AND RimoFcp.rimo_id='$rimo_id'";
}
}
//var_dump($filter, $where);exit;
return $where;
}
}
class ADBRimoFcp extends TTCrudBaseModel {
public int $id;
public int $netzgebiet_id;
public ?string $name;
public string $rimo_id;
public ?string $label;
public ?string $building_type;
public ?string $rimo_ex_state;
public ?string $rimo_op_state;
public ?float $gps_lat;
public ?float $gps_long;
public int $create;
public int $edit;
}

View File

@@ -0,0 +1,128 @@
<?php
class ADBRimoFcpController extends TTCrud {
protected string $headerTitle = 'Rimo FCPs';
protected string $singleText = 'Rimo FCP';
// @formatter:off
protected array $columns = [
['key' => 'netzgebiet_id', 'text' => 'Netzgebiet', 'required' => true, 'modal' => ['type' => 'select', 'items' => []], 'table' => ['filter' => 'select']],
['key' => 'name', 'text' => 'Name', 'required' => true],
['key' => 'rimo_id', 'text' => 'Rimo ID', 'required' => true],
['key' => 'label', 'text' => 'Label', 'required' => false],
['key' => 'building_type', 'text' => 'Gebäudetyp', 'required' => false],
['key' => 'rimo_ex_state', 'text' => 'Rimo Ex State', 'required' => false],
['key' => 'rimo_op_state', 'text' => 'Rimo Op State', 'required' => false],
['key' => 'gps_lat', 'text' => 'GPS Lat', 'required' => false],
['key' => 'gps_long', 'text' => 'GPS Long', 'required' => false],
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false],
['key' => 'edit', 'text' => 'Bearbeitet', 'required' => true, 'modal' => false, 'table' => false],
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]],
];
public function prepareCrudConfig() {
$netzgebiete = array_map(function ($netzgebiet) {
return ['value' => $netzgebiet->id, 'text' => $netzgebiet->name];
}, ADBNetzgebietModel::getAll());
$this->columns[0]['modal']['items'] = $netzgebiete;
}
public function ImportFCPsAction() {
$input = json_decode(file_get_contents('php://input'), true);
$fcpList = $input['data'] ?? [];
$networkAreaId = $input['networkAreaId'];
$counts = ['new' => 0, 'upd' => 0];
$now = date('U');
foreach ($fcpList as $fcpIn) {
$rimoId = $fcpIn['ExternalID'] ?? null;
if ($rimoId === null) $rimoId = $fcpIn['External ID'] ?? null;
if ($rimoId === null) continue;
$data = [
'netzgebiet_id' => $networkAreaId,
'name' => $fcpIn['Name'] ?? null,
'rimo_id' => $rimoId,
'label' => $fcpIn['User label'] ?? null,
'building_type' => $fcpIn['Building type'] ?? null,
'rimo_ex_state' => $fcpIn['Execution state'] ?? null,
'rimo_op_state' => $fcpIn['Operational state'] ?? null,
'gps_lat' => floatval(str_replace(',', '.', $fcpIn['Latitude'] ?? '0')),
'gps_long' => floatval(str_replace(',', '.', $fcpIn['Longitude'] ?? '0')),
'edit' => $now
];
$existing = ADBRimoFcp::getAll(['rimo_id' => $rimoId]);
if (count($existing) > 0 && $existing = $existing[0]) {
$data['id'] = $existing->id;
$data['create'] = $existing->create;
ADBRimoFcp::update($data);
$counts['upd']++;
} else {
$data['create'] = $now;
ADBRimoFcp::create($data);
$counts['new']++;
}
}
$msg = sprintf('%d new, %d updated FCPs.', $counts['new'], $counts['upd']);
self::returnJson(['success' => true, 'message' => $msg]);
}
public function ImportLocationsAction() {
$input = json_decode(file_get_contents('php://input'), true);
$fcpsByName = array_column(ADBRimoFcp::getAll(['netzgebiet_id' => $input['networkAreaId']]), null, 'name');
$counts = ['upd' => 0, 'fcpNF' => 0, 'noFCP' => 0, 'noExtId' => 0];
foreach ($input['data'] as $loc) {
$fcpName = trim($loc['FCP cluster name'] ?? '');
$extId = $loc['ExternalID'] ?? null;
if ($extId === null) $extId = $loc['External ID'] ?? null;
if ($fcpName === '') { $counts['noFCP']++; continue; }
if (!isset($fcpsByName[$fcpName])) { $counts['fcpNF']++; continue; }
if ($extId === null) { $counts['noExtId']++; continue; }
$fcp = $fcpsByName[$fcpName];
if ($hn = ADBHausnummerModel::getFirst(['rimo_id' => $extId])) {
$hn->fcp_id = $fcp->id;
$hn->rimo_fcp_name = $fcp->name;
$hn->save();
$counts['upd']++;
}
}
$msg = sprintf('Updated: %d, FCP not Found: %d, No FCP in the CSV: %d, No Rimo ID: %d',
$counts['upd'], $counts['fcpNF'], $counts['noFCP'], $counts['noExtId']);
self::returnJson(['success' => true, 'message' => $msg]);
}
public function MapAction() {
Helper::renderVue($this, "ADBRimoFcpMap", "ADBRimoFcpMap", [
"MAPBOX_KEY" => TT_MAPBOX_TILE_API_TOKEN,
]);
}
public function getAllFCPsAction() {
$input = json_decode(file_get_contents('php://input'), true);
$fcpList = ADBRimoFcp::getAll();
$fcpData = array_map(function ($fcp) {
return [
'id' => $fcp->id,
// 'rimo_ex_state' => $fcp->rimo_ex_state,
// 'rimo_op_state' => $fcp->rimo_op_state,
'gps_lat' => $fcp->gps_lat,
'gps_long' => $fcp->gps_long
];
}, $fcpList);
self::returnJson(['success' => true, 'data' => $fcpData]);
}
}

View File

@@ -167,6 +167,13 @@ class BuildingModel {
$where .= " AND Building.pipeworker_id=$pipeworker_id";
}
}
if(array_key_exists("pipework_enabled", $filter)) {
$pipework_enabled = $filter['pipework_enabled'];
if(!empty($pipework_enabled) || $pipework_enabled === "0") {
$where .= " AND Building.pipework_enabled=$pipework_enabled";
}
}
if(array_key_exists("type", $filter) && is_array($filter['type']) && count($filter['type'])) {
$ot = $filter['type'];
@@ -267,5 +274,52 @@ class BuildingModel {
//var_dump($filter, $where);exit;
return $where;
}
public static function getHistory($from, $to, $network_id, $street_filter): array {
$sql = "SELECT b.id AS building_id,
b.street AS building_street,
b.zip AS building_zip,
b.city AS building_city,
wi.id AS item_id,
wi.name AS item_name,
wi.label AS item_label,
wi.type AS item_type,
wv.id AS value_id,
wv.value_string,
wv.value_int,
wv.value_text,
wv.changed AS last_edited_at,
wv.changed_by AS last_edited_by_user_id
FROM Workflowvalue wv
JOIN Workflowitem wi ON wv.item_id = wi.id
JOIN Building b ON wv.object_id = b.id";
$where = ["wi.object_type = 'Building'", "wi.num < 150"];
if ($from !== null && $to !== null) {
$where[] = "(wv.changed >= " . intval($from) . " AND wv.changed <= " . intval($to) . ")";
}
if ($network_id !== null) {
$where[] = "b.network_id = " . intval($network_id);
}
if (!empty($street_filter)) {
$escaped_street = addslashes($street_filter);
$where[] = "b.street LIKE '%" . $escaped_street . "%'";
}
$sql .= " WHERE " . implode(" AND ", $where);
$sql .= " ORDER BY wv.changed DESC;";
$db = FronkDB::singleton();
$res = $db->query($sql);
if ($db->num_rows($res)) {
$items = [];
while ($data = $db->fetch_object($res)) {
$items[] = $data;
}
return $items;
} else {
return [];
}
}
}

View File

@@ -607,6 +607,15 @@ FROM ConstructionConsent
}
}
if(array_key_exists("electric_approval", $filter)) {
$inhouse_cabling = $filter["electric_approval"];
if($inhouse_cabling == "!NULL") {
$where .= " AND (inspection_electrician IS NOT NULL AND inspection_electrician != 0)";
} elseif($inhouse_cabling == "NULL") {
$where .= " AND (inspection_electrician IS NULL OR inspection_electrician = 0)";
}
}
if (array_key_exists("cwo", $filter) && !empty($filter['cwo'])) {
$where .= "
AND EXISTS (

View File

@@ -507,9 +507,6 @@ class ConstructionConsentController extends mfBaseController {
}
protected function apiAction() {
if(!$this->me->is(["Admin","netowner"]) && !$this->me->can("Preorder")) {
$this->redirect("Dashboard");
}
$do = $this->request->do;
$data = [];

View File

@@ -393,7 +393,7 @@ class ContractController extends mfBaseController {
$this->redirect("Dashboard");
}
var_dump($_FILES);exit;
//var_dump($_FILES);exit;
$r = $this->request;
@@ -1223,4 +1223,4 @@ class ContractController extends mfBaseController {
}
$this->returnJson($results);
}
}
}

View File

@@ -104,7 +104,7 @@ class LineworkController extends mfBaseController {
if(!array_key_exists("status_id", $filter)) {
if(!!in_array($this->me->id, ["145","62","56"]) && !array_key_exists("status_id", $filter)) {
$termination_search["status_id"] = 3;
}

View File

@@ -106,7 +106,15 @@ class PatchingController extends mfBaseController {
$this->layout()->set("terminations", $terminations);
$devices = DeviceModel::getAll();
$this->layout()->set("devices", array_map(function($device) {
return [
"name" => $device->name,
"ip" => $device->ip,
];
}, $devices));
}
private function getPreparedFilter($filter) {
@@ -232,6 +240,31 @@ class PatchingController extends mfBaseController {
$this->redirect("Patching","Index", $qs);
}
protected function swAction() {
$javascript = "self.addEventListener('install', event => {
console.log('Patching PWA Service Worker: Installing...');
});
self.addEventListener('activate', event => {
console.log('Patching PWA Service Worker: Activating...');
});
self.addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});
console.log('Patching PWA Service Worker: Script loaded.');";
header("Content-Type: application/javascript");
header("Service-Worker-Allowed: /");
header("Cache-Control: no-cache");
header("Pragma: no-cache");
header("Expires: 0");
echo $javascript;
exit;
}
}

View File

@@ -102,7 +102,7 @@ class PipeworkController extends mfBaseController {
}
if(!array_key_exists("status_id", $filter)) {
if(!in_array($this->me->id, ["145","62","56"]) && !array_key_exists("status_id", $filter)) {
$building_search["status_id"] = 3;
}
@@ -461,5 +461,51 @@ class PipeworkController extends mfBaseController {
}
protected function historyAction() {
if (!$this->me->isAdmin()) {
throw new Exception("Forbidden", 403);
}
Helper::renderVue($this, "PipeworkHistory", "PipeworkHistory", [
"IS_ADMIN" => $this->me->isAdmin(),
"NETWORKS" => array_map(function ($network) {
return [
"value" => $network->id,
"text" => $network->name,
];
}, NetworkModel::getAll()),
"USERS" => array_map(function ($user) {
return [
"value" => $user->id,
"text" => $user->name,
];
}, UserModel::search(['employee' => true])),
]);
}
protected function historyAPIAction() {
if (!$this->me->isAdmin()) self::sendError("Keine Berechtigung");
$from = $this->request->from;
$to = $this->request->to;
$network_id = $this->request->network_id;
$street_filter = $this->request->street_filter;
// from and to is unix timestamp
if ($from && $to) {
$from = (int)$from;
$to = (int)$to;
if ($from > $to) self::sendError('Von kann nicht nach dem Bis-Datum liegen');
$fourWeeksInSeconds = 2419200;
// if (($to - $from) > $fourWeeksInSeconds) self::sendError('Der Zeitraum darf maximal 4 Wochen betragen');
}
if ($from && $to && $network_id) {
self::returnJson(["status" => "OK","data" => BuildingModel::getHistory($from,$to,$network_id,$street_filter)]);
} else {
self::sendError('Fehlerhafte Parameter');
}
}
}

View File

@@ -13,6 +13,7 @@ class Preorder extends mfBaseModel {
private $building;
private $adb_hausnummer;
private $adb_wohneinheit;
private $fcp;
private $services;
private $ordered_services;
private $creator;
@@ -1396,6 +1397,11 @@ class Preorder extends mfBaseModel {
return $this->creator;
}
if($name === 'fcp') {
if(!$this->adb_hausnummer->fcp_id) return null;
return ADBRimoFcp::get($this->adb_hausnummer->fcp_id);
}
if($name == "editor") {
$this->editor = new User($this->edit_by);
return $this->editor;

View File

@@ -1042,6 +1042,9 @@ class PreorderController extends mfBaseController {
case "saveAttribute":
$return = $this->saveAttributeApi();
break;
case "getFCPsForCampaign":
$return = $this->getFCPsForCampaignApi();
break;
case "getFilteredPreorders":
$return = $this->getFilteredPreordersApi();
break;
@@ -1085,6 +1088,17 @@ class PreorderController extends mfBaseController {
$this->returnJson($data);
}
protected function getFCPsForCampaignApi(): array {
$campaign = new Preordercampaign($this->request->campaign_id);
if (!$campaign->id) return [];
return array_map(
fn($fcp) => ["id" => $fcp->name ?? null, "text" => $fcp->name ?? null, 'lat' => $fcp->gps_lat ?? null, 'lng' => $fcp->gps_long ?? null],
ADBRimoFcp::getAll(["netzgebiet_id" => intval($campaign->network->adb_netzgebiet_id)]) ?? []
);
}
private function setBilledApi() {
$preorder_id = $this->request->id;

View File

@@ -1005,6 +1005,29 @@ class PreorderModel
}
}
}
if (!empty($filter['fcp']) && array_key_exists("preordercampaign_id", $filter)) {
$fcp = $filter['fcp'];
$db = FronkDB::singleton();
$campaign = new Preordercampaign($filter['preordercampaign_id']);
if (is_array($fcp)) {
$items = array_map(fn($i) => ADBRimoFcp::getAll([
'netzgebiet_id' => intval($campaign->network->adb_netzgebiet_id),
'name' => $i])[0], array_filter($fcp));
$items = array_map(fn($i) => $i->id, array_filter($items));
if ($items) $where .= " AND adb_hausnummer.fcp_id IN (" . implode(',', $items) . ")";
} else {
$fcp = ADBRimoFcp::getAll([
'netzgebiet_id' => intval($campaign->network->adb_netzgebiet_id),
'name' => $fcp]);
if ($fcp) $fcp = $fcp[0]->id;
else $fcp = null;
$where .= " AND adb_hausnummer.rimo_fcp_name = '" . $db->escape($fcp) . "'";
}
}
// custom where clause
if (array_key_exists("add-where", $filter)) {

View File

@@ -146,6 +146,13 @@ class TerminationModel {
$where .= " AND Termination.status_id = $status_id";
}
}
if(array_key_exists("linework_enabled", $filter)) {
$linework_enabled = $filter['linework_enabled'];
if(!empty($linework_enabled) || $linework_enabled === '0') {
$where .= " AND Termination.linework_enabled=$linework_enabled";
}
}
if(array_key_exists("lineworker_id", $filter)) {
$lineworker_id = $filter['lineworker_id'];

View File

@@ -18,9 +18,9 @@ class WarehouseArticleController extends TTCrud {
['key' => 'cheapestSellPrice', 'text' => 'Verkauf', 'modal' => false, 'table' => ['class' => 'text-center', 'suffix' => ' €']],
['key' => 'warningAmount', 'text' => 'Warnmenge', 'required' => true,'modal' => ['type' => 'number'], 'table' => ['class' => 'text-center']], // Stock/inventory related
['key' => 'criticalAmount', 'text' => 'Kritische Menge', 'required' => true,'modal' => ['type' => 'number'], 'table' => ['class' => 'text-center']], // Stock/inventory related
['key' => 'isSerialDocumentation', 'text' => 'Seriennummern', 'required' => true,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
['key' => 'isEShop', 'text' => 'Ist E-Shop', 'required' => true,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
['key' => 'isEShopHide', 'text' => 'E-Shop Versteckt', 'required' => true,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
['key' => 'isSerialDocumentation', 'text' => 'Seriennummern', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
['key' => 'isEShop', 'text' => 'Ist E-Shop', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
['key' => 'isEShopHide', 'text' => 'E-Shop Versteckt', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 8]]
];

View File

@@ -10,10 +10,10 @@ class WarehouseArticleModel extends TTCrudBaseModel {
public ?string $cheapestSellPrice;
public int $warningAmount;
public int $criticalAmount;
public int $isEShop;
public int $isEShopHide;
public ?int $isEShop;
public ?int $isEShopHide;
public string $unit;
public int $isSerialDocumentation;
public ?int $isSerialDocumentation;
public int $revenueAccount;

View File

@@ -47,11 +47,13 @@ class WarehouseArticlePacketController extends TTCrud {
foreach ($subItems as $subItem) {
$article = WarehouseArticleModel::get($subItem->id);
$cheapestSellPrices = json_decode($article->cheapestSellPrice);
$cheapestSellPrices = json_decode($article->cheapestSellPrice, true);
// find in array cheapestSellPrices by title === 'Energie Steiermark' and get the price
$articlePrice = array_values(array_filter($cheapestSellPrices, function ($cheapestSellPrice) {
return $cheapestSellPrice->title === 'Energie Steiermark';
}))[0]->price;
return $cheapestSellPrice['title'] === 'Energie Steiermark';
}));
$articlePrice = $articlePrice[0]['price'] ?? 0;
$calculatedSellPrice += $subItem->amount * $articlePrice;
}

View File

@@ -2,20 +2,21 @@
class WarehouseOfferController extends TTCrud {
protected string $headerTitle = 'Angebote';
protected string $singleText = 'Angebot';
protected bool $createText = false;
protected array $columns = [
['key' => 'id', 'text' => 'ID', 'modal' => false],
['key' => 'id', 'text' => 'ID', 'modal' => false, 'table' => false],
['key' => 'offerNumber', 'text' => 'Angebotsnummer', 'required' => true, 'modal' => false],
['key' => 'customerNumber', 'text' => 'Kundennummer', 'required' => true, 'modal' => false],
['key' => 'customerName', 'text' => 'Kundenname', 'required' => true, 'modal' => false],
['key' => 'customerCity', 'text' => 'Stadt', 'required' => true, 'modal' => false],
['key' => 'customerVAT', 'text' => 'UID', 'required' => true, 'modal' => false],
['key' => 'editor', 'text' => 'Sachbearbeiter', 'required' => true, 'modal' => false],
['key' => 'editor', 'text' => 'Sachbearbeiter', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']],
['key' => 'totalAmount', 'text' => 'Gesamtbetrag', 'required' => true, 'modal' => false],
['key' => 'status', 'text' => 'Status', 'required' => true, 'modal' => ['type' => 'select']],
['key' => 'status', 'text' => 'Status', 'required' => true],
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false],
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select']],
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']],
['key' => 'actions',
'text' => 'Aktionen',
'required' => false,
@@ -24,27 +25,19 @@ class WarehouseOfferController extends TTCrud {
];
protected array $permissionCheck = ['WarehouseAdmin'];
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']];
protected array $additionalActions = [
['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'],
['key' => 'sendOffer', 'title' => 'Angebot senden', 'class' => 'fas fa-paper-plane text-success']
];
protected array $additionalJS = ['
https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js
https://cdn.jsdelivr.net/npm/vue-draggable-next@2.1.0'];
protected array $infoMessages = [
'create' => 'Angebot wurde erfolgreich erstellt.',
'update' => 'Angebot wurde aktualisiert.',
'delete' => 'Angebot wurde gelöscht',
'noChanges' => 'Keine Änderungen',
'sent' => 'Angebot wurde erfolgreich gesendet',
];
protected function prepareCrudConfig(): void {
$editorColumnIndex = array_search('editor', array_column($this->columns, 'key'));
$this->columns[$editorColumnIndex]['modal']['items'] = array_map(function ($user) {
return ['value' => intval($user->id), 'text' => $user->name];
}, UserModel::search(['employee' => true]));
}
protected function beforeCreate(): bool {
$currentCount = WarehouseOfferModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]);
$this->postData['offerNumber'] = 'AN' . date('Y') . '-' . str_pad($currentCount + 1, 4, '0', STR_PAD_LEFT);
$this->postData['status'] = 'new';
return true;
}
@@ -57,4 +50,24 @@ class WarehouseOfferController extends TTCrud {
protected function getHistoryAction() {
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
}
protected function createTemplateAction() {
$_POST = json_decode(file_get_contents('php://input'), true);
$templateId = WarehouseOfferTemplateModel::create([
'templateName' => $_POST['name'],
'positions' => $_POST['positions'],
'totalDiscount' => $_POST['totalDiscount'],
'paymentTerms' => $_POST['paymentTerms'],
'deliveryTerms' => $_POST['deliveryTerms'],
'closingText' => $_POST['closingText'],
'notes' => $_POST['notes'],
]);
self::returnJson(['success' => true, 'id' => $templateId]);
}
protected function getTemplatesAction() {
self::returnJson(WarehouseOfferTemplateModel::getAll());
}
}

View File

@@ -7,6 +7,7 @@
*
* @property int $id Unique identifier for the warehouse offer
* @property string $offerNumber Unique offer number
* @property string $reference Reference number for the offer
* @property string $customerNumber Customer number
* @property string $customerName Name of the customer
* @property string $customerStreet Street address of the customer
@@ -30,6 +31,7 @@
class WarehouseOfferModel extends TTCrudBaseModel {
public int $id;
public string $offerNumber;
public string $reference;
public string $customerNumber;
public string $customerName;
public string $customerStreet;
@@ -50,3 +52,31 @@ class WarehouseOfferModel extends TTCrudBaseModel {
public int $create;
public int $createBy;
}
//SQL TO CREATE TABLE
/*
CREATE TABLE `warehouse_offer` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`offerNumber` varchar(255) NOT NULL,
`customerNumber` varchar(255) NOT NULL,
`customerName` varchar(255) NOT NULL,
`customerStreet` varchar(255) NOT NULL,
`customerCity` varchar(255) NOT NULL,
`customerZip` varchar(255) NOT NULL,
`customerVAT` varchar(255) NOT NULL,
`editor` int(11) NOT NULL,
`purpose` varchar(255) NOT NULL,
`positions` text NOT NULL,
`alternativePositions` text NOT NULL,
`totalDiscount` float NOT NULL,
`paymentTerms` varchar(255) NOT NULL,
`deliveryTerms` varchar(255) NOT NULL,
`closingText` varchar(255) NOT NULL,
`notes` varchar(255) NOT NULL,
`status` varchar(255) NOT NULL,
`totalAmount` float NOT NULL,
`create` int(11) NOT NULL,
`createBy` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
*/

View File

@@ -0,0 +1,9 @@
<?php
/**
* @property mixed|null $name
*/
class WarehouseOfferTemplate extends mfBaseModel
{
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Class WarehouseOfferTemplateModel
*
* Represents a warehouse offer template with key details.
*
* @property string $templateName Name of the template
* @property string $positions Details about positions in the offer
* @property float $totalDiscount Total discount applied to the offer
* @property string $paymentTerms Payment terms for the offer
* @property string $deliveryTerms Delivery terms for the offer
* @property string $closingText Closing text for the offer
* @property string $notes Additional notes for the offer
*/
class WarehouseOfferTemplateModel extends TTCrudBaseModel
{
public string $templateName;
public string $positions;
public float $totalDiscount;
public string $paymentTerms;
public string $deliveryTerms;
public string $closingText;
public string $notes;
}

View File

@@ -1,4 +1,6 @@
<?php
<?php /** @noinspection PhpUndefinedClassInspection */
/** @noinspection PhpUndefinedNamespaceInspection */
class WarehouseOrderController extends TTCrud {
protected string $headerTitle = 'Lieferantenbestellungen';
@@ -83,7 +85,7 @@ class WarehouseOrderController extends TTCrud {
foreach ($order['positions'] as &$position) {
$position['distributorName'] = WarehouseDistributorModel::get($position['distributorId'])->name;
$position['articleName'] = WarehouseArticleModel::get($position['article'])->title;
$position['articleName'] = $position['article_text'] ?? WarehouseArticleModel::get($position['article'])->title;
}
return $order;
@@ -227,7 +229,7 @@ class WarehouseOrderController extends TTCrud {
$mail->isHTML(true);
$mail->Subject = "Neue Bestellung #$orderNumber";
$mail->Body = "<!DOCTYPE html>
<html>
<html lang='de'>
<head>
<title>XINON E-Mail Template</title>
<meta charset='utf-8'/>

View File

@@ -1,9 +1,15 @@
<?php
/**
* @property mixed|null $name
*/
class WarehouseOrderRequest extends mfBaseModel
{
class WarehouseOrderRequest extends TTCrudBaseModel {
public int $id;
public ?int $addressId;
public string $purpose;
public string $positions;
public ?string $note;
public ?string $linkedOrderIds;
public ?int $cancelled;
public ?int $done;
public int $create;
public int $createBy;
}
}

View File

@@ -1,4 +1,7 @@
<?php /** @noinspection PhpVoidFunctionResultUsedInspection */
<?php /** @noinspection PhpUndefinedClassInspection */
/** @noinspection PhpUndefinedNamespaceInspection */
/** @noinspection PhpVoidFunctionResultUsedInspection */
class WarehouseOrderRequestController extends TTCrud {
protected string $headerTitle = 'Bestellwünsche';
@@ -10,6 +13,7 @@ class WarehouseOrderRequestController extends TTCrud {
['key' => 'id', 'text' => 'Bestellnummer', 'table' => ['filter' => false], 'modal' => false],
['key' => 'addressId', 'text' => 'Kunde', 'required' => false, 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'], 'modal' => ['apiUrl' => 'Address/api?do=findAddress&fibu_primary_account=1', 'items' => '/Address/Api?do=findAddress&fibu_primary_account=1', 'type' => 'autocomplete']],
['key' => 'purpose', 'text' => 'Verwendungszweck', 'required' => true],
['key' => 'note', 'text' => 'Notiz', 'required' => false],
['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'modal' => ['type' => 'positions-manager', 'config' => [
'header' => 'Positionen',
'fields' => [
@@ -69,10 +73,10 @@ class WarehouseOrderRequestController extends TTCrud {
$cancel = filter_var($this->request->cancel, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 1]]);
if (!$id || $cancel === false) self::returnJson(['error' => 'Ungültige Anfrage']);
if (!(WarehouseOrderRequestModel::get($id))) self::returnJson(['error' => 'Bestellwunsch nicht gefunden']);
if (!(WarehouseOrderRequest::get($id))) self::returnJson(['error' => 'Bestellwunsch nicht gefunden']);
$currentData = (array) WarehouseOrderRequestModel::get($id);
WarehouseOrderRequestModel::update(array_merge($currentData, ['id' => $id, 'cancelled' => $cancel]));
$currentData = (array) WarehouseOrderRequest::get($id);
WarehouseOrderRequest::update(array_merge($currentData, ['id' => $id, 'cancelled' => $cancel]));
self::returnJson(['success' => true]);
}
@@ -86,13 +90,70 @@ class WarehouseOrderRequestController extends TTCrud {
$done = filter_var($this->request->done, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 1]]);
if (!$id || $done === false) self::returnJson(['error' => 'Ungültige Anfrage']);
if (!(WarehouseOrderRequestModel::get($id))) self::returnJson(['error' => 'Bestellwunsch nicht gefunden']);
if (!(WarehouseOrderRequest::get($id))) self::returnJson(['error' => 'Bestellwunsch nicht gefunden']);
$currentData = (array) WarehouseOrderRequestModel::get($id);
WarehouseOrderRequestModel::update(array_merge($currentData, ['id' => $id, 'done' => $done]));
$currentData = (array) WarehouseOrderRequest::get($id);
WarehouseOrderRequest::update(array_merge($currentData, ['id' => $id, 'done' => $done]));
self::returnJson(['success' => true]);
}
private function getPHPMailer() {
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
try {
// Server settings
$mail->isSMTP();
$mail->Host = TT_WAREHOUSE_ORDER_SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = TT_WAREHOUSE_ORDER_SMTP_USER;
$mail->Password = TT_WAREHOUSE_ORDER_SMTP_PASS;
$mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
return $mail;
} catch (Exception $e) {
self::returnJson(['error' => 'Mailer Error: ' . $mail->ErrorInfo]);
exit;
}
}
protected function afterCreate($orderRequest) {
try {
$mail = $this->getPHPMailer();
$mail->setFrom('einkauf@xinon.at', 'XINON Einkauf');
$mail->addAddress('einkauf@xinon.at', 'XINON Einkauf');
$mail->isHTML(true);
$mail->Subject = "Neuer Bestellwunsch #" . $orderRequest['id'] . " von " . $this->user->name . ' eingelangt';
// build html table and fetch articleId if set else use articleId_text if its a text article
$html = '<table style="width: 100%; border-collapse: collapse;">';
$html .= '<tr><th style="border: 1px solid #000; padding: 8px;">Artikel</th><th style="border: 1px solid #000; padding: 8px;">Menge</th><th style="border: 1px solid #000; padding: 8px;">Zweck</th></tr>';
foreach ($orderRequest['positions'] as $position) {
$articleId = isset($position['articleId']) ? WarehouseArticleModel::get($position['articleId'])->title : $position['articleId_text'];
$html .= '<tr>';
$html .= '<td style="border: 1px solid #000; padding: 8px;">' . htmlspecialchars($articleId) . '</td>';
$html .= '<td style="border: 1px solid #000; padding: 8px;">' . htmlspecialchars($position['amount']) . '</td>';
$html .= '<td style="border: 1px solid #000; padding: 8px;">' . htmlspecialchars($position['purpose']) . '</td>';
$html .= '</tr>';
}
$html .= '</table>';
// Set the HTML content
$mail->Body = "Neuer Bestellwunsch #" . $orderRequest['id'] . " von " . $this->user->name . ' eingelangt<br><br>' .
'Notiz: ' . htmlspecialchars($orderRequest['note']) . '<br><br>' . $html;
// Send the email
if (!$mail->send()) {
self::returnJson(['error' => 'Message could not be sent. Mailer Error: ' . $mail->ErrorInfo]);
exit;
}
} catch (Exception $e) {
self::returnJson(['error' => 'Message could not be sent. Mailer Error: ' . $mail->ErrorInfo]);
exit;
}
}
protected function createNewLogAction() {
$postData = json_decode(file_get_contents('php://input'), true);

View File

@@ -1,15 +0,0 @@
<?php
class WarehouseOrderRequestModel extends TTCrudBaseModel {
public int $id;
public ?int $addressId;
public string $purpose;
public string $positions;
public ?string $note;
public ?string $linkedOrderIds;
public ?int $cancelled;
public ?int $done;
public int $create;
public int $createBy;
}

View File

@@ -16,6 +16,7 @@ class WarehouseShippingNoteController extends TTCrud {
['value' => 'cancelled', 'text' => 'Storniert', 'icon' => 'fas fa-ban text-danger'],
['value' => 'on_hold', 'text' => 'In Wartestellung', 'icon' => 'fas fa-pause text-warning'],
]]],
['key' => 'type', 'text' => 'Typ', 'required' => false],
['key' => 'deliveryAddressName', 'text' => 'L.-Adr. Name', 'required' => true],
['key' => 'deliveryAddressLine', 'text' => 'L.-Adr.', 'required' => true],
['key' => 'deliveryAddressPLZ', 'text' => 'L.-Adr. PLZ', 'required' => true],
@@ -29,6 +30,7 @@ class WarehouseShippingNoteController extends TTCrud {
protected array $defaultOrder = ['key' => 'create', 'order' => 'DESC'];
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true];
protected array $additionalHead = ['<link rel="manifest" href="/assets/pwa/shipping-note-manifest.json">'];
protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.',
'update' => 'Lieferschein wurde aktualisiert',
@@ -378,23 +380,13 @@ class WarehouseShippingNoteController extends TTCrud {
}
protected function changeStatusAction() {
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
$json = json_decode(file_get_contents('php://input'), true);
$id = $json['id'];
$status = $json['status'];
if (strlen($id) < 1) {
http_response_code(500);
self::returnJson(['success' => false, 'message' => 'Lieferschein wurde nicht gefunden']);
}
if (empty($json['id'])) self::sendError('Lieferschein wurde nicht gefunden');
$shippingNote = (array) WarehouseShippingNoteModel::get($id);
if ($shippingNote['status'] === 'invoiced') {
http_response_code(500);
self::returnJson(['success' => false, 'message' => 'Status kann nicht geändert werden']);
}
$shippingNote = (array) WarehouseShippingNoteModel::get($json['id']);
if ($shippingNote['status'] === 'invoiced') self::sendError('Status kann nicht geändert werden');
$shippingNote['status'] = $status;
$shippingNote['status'] = $json['status'];
WarehouseShippingNoteModel::update($shippingNote);
$statusNiceText = [
'new' => 'Neu',
@@ -404,7 +396,7 @@ class WarehouseShippingNoteController extends TTCrud {
'cancelled' => 'Storniert',
'on_hold' => 'In Wartestellung',
];
self::returnJson(['success' => true, 'message' => 'Status wurde auf ' . $statusNiceText[$status] . ' geändert']);
self::returnJson(['success' => true, 'message' => 'Status wurde auf ' . $statusNiceText[$json['status']] . ' geändert']);
}
//TODO: either move this to TimerecordingCarController or make it better
@@ -602,4 +594,31 @@ class WarehouseShippingNoteController extends TTCrud {
$logs = WarehouseLogModel::getAll(['table' => 'WarehouseShippingNote','rowId' => $shippingNoteId], null, 0, ['order' => 'DESC', 'key' => 'create']);
self::returnJson($logs);
}
protected function swAction() {
$javascript = "self.addEventListener('install', event => {
console.log('Patching PWA Service Worker: Installing...');
});
self.addEventListener('activate', event => {
console.log('Patching PWA Service Worker: Activating...');
});
self.addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});
console.log('Patching PWA Service Worker: Script loaded.');";
header("Content-Type: application/javascript");
header("Service-Worker-Allowed: /");
header("Cache-Control: no-cache");
header("Pragma: no-cache");
header("Expires: 0");
echo $javascript;
exit;
}
}

View File

@@ -3,6 +3,7 @@
class WarehouseShippingNoteModel extends TTCrudBaseModel {
public int $id;
public ?int $billingAddressId;
public ?string $type;
public string $deliveryAddressName;
public string $deliveryAddressLine;
public string $deliveryAddressPLZ;

View File

@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);
use Phinx\Migration\AbstractMigration;
final class WarehouseModify16 extends AbstractMigration {
public function up(): void {
if ($this->getEnvironment() == "thetool") {
$WarehouseArticleTable = $this->table("WarehouseArticle");
$WarehouseArticleTable
->changeColumn("isEShop", "integer", ["default" => 0])
->changeColumn("isEShopHide", "integer", ["default" => 0])
->changeColumn("isSerialDocumentation", "integer", ["default" => 0])
->save();
$WarehouseOfferTable = $this->table("WarehouseOffer", ["id" => false, "primary_key" => "id"]);
$WarehouseOfferTable
->addColumn("id", "integer", ["identity" => true])
->addColumn("offerNumber", "string", ["limit" => 255, "null" => false])
->addColumn("reference", "string", ["limit" => 255, "null" => false])
->addColumn("customerNumber", "string", ["limit" => 255, "null" => false])
->addColumn("customerName", "string", ["limit" => 255, "null" => false])
->addColumn("customerStreet", "string", ["limit" => 255, "null" => false])
->addColumn("customerCity", "string", ["limit" => 255, "null" => false])
->addColumn("customerZip", "string", ["limit" => 255, "null" => false])
->addColumn("customerVAT", "string", ["limit" => 255, "null" => false])
->addColumn("editor", "integer", ["null" => false])
->addColumn("purpose", "string", ["limit" => 255, "null" => false])
->addColumn("positions", "text", ["null" => false])
->addColumn("alternativePositions", "text", ["null" => false])
->addColumn("totalDiscount", "float", ["null" => false])
->addColumn("paymentTerms", "string", ["limit" => 255, "null" => false])
->addColumn("deliveryTerms", "string", ["limit" => 255, "null" => false])
->addColumn("closingText", "text", ["null" => false])
->addColumn("notes", "string", ["limit" => 255, "null" => false])
->addColumn("status", "string", ["limit" => 255, "null" => false])
->addColumn("totalAmount", "float", ["null" => false])
->addColumn("create", "integer", ["null" => false])
->addColumn("createBy", "integer", ["null" => false])
->save();
$WarehouseOfferTemplateTable = $this->table("WarehouseOfferTemplate", ["id" => false, "primary_key" => "id"]);
$WarehouseOfferTemplateTable
->addColumn("id", "integer", ["identity" => true])
->addColumn("templateName", "string", ["limit" => 255, "null" => false])
->addColumn("positions", "text", ["null" => false])
->addColumn("totalDiscount", "float", ["null" => false])
->addColumn("paymentTerms", "string", ["limit" => 255, "null" => false])
->addColumn("deliveryTerms", "string", ["limit" => 255, "null" => false])
->addColumn("closingText", "text", ["null" => false])
->addColumn("notes", "text", ["null" => false])
->save();
}
}
public function down(): void {
if ($this->getEnvironment() == "thetool") {
// change table "WarehouseArticle" and set isEShop, isEShopHide, isSerialDocumentation to no default
$WarehouseArticleTable = $this->table("WarehouseArticle");
$WarehouseArticleTable
->changeColumn("isEShop", "integer", ["default" => null])
->changeColumn("isEShopHide", "integer", ["default" => null])
->changeColumn("isSerialDocumentation", "integer", ["default" => null])
->save();
$this->table("WarehouseOffer")->drop()->save();
$this->table("WarehouseOfferTemplate")->drop()->save();
}
}
}

View File

@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
use Phinx\Migration\AbstractMigration;
final class WarehouseModify17 extends AbstractMigration {
public function up(): void {
if ($this->getEnvironment() == "thetool") {
$WarehouseShippingNoteTable = $this->table("WarehouseShippingNote");
$WarehouseShippingNoteTable
->addColumn("type", "string", ["limit" => 255, "default" => ""])
->save();
}
}
public function down(): void {
if ($this->getEnvironment() == "thetool") {
$WarehouseShippingNoteTable = $this->table("WarehouseShippingNote");
if ($WarehouseShippingNoteTable->hasColumn("type")) {
$WarehouseShippingNoteTable
->removeColumn("type")
->save();
}
}
}
}

View File

@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
use Phinx\Migration\AbstractMigration;
final class WarehouseModify18 extends AbstractMigration {
public function up(): void {
if ($this->getEnvironment() == "thetool") {
$WarehouseShippingNoteTable = $this->table("WarehouseShippingNote");
$WarehouseShippingNoteTable
->changeColumn("type", "string", ["limit" => 255, "default" => null])
->save();
}
}
public function down(): void {
if ($this->getEnvironment() == "thetool") {
$WarehouseShippingNoteTable = $this->table("WarehouseShippingNote");
if ($WarehouseShippingNoteTable->hasColumn("type")) {
$WarehouseShippingNoteTable
->changeColumn("type", "string", ["limit" => 255])
->save();
}
}
}
}

View File

@@ -4,12 +4,12 @@ FROM debian:bookworm
# Install wkhtmltopdf
RUN apt update
RUN apt install wget libfontenc1 xfonts-75dpi xfonts-base xfonts-encodings xfonts-utils openssl build-essential libssl-dev libxrender-dev git-core libx11-dev libxext-dev libfontconfig1-dev libfreetype6-dev fontconfig -y
# wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.stretch_amd64.deb
# dpkg --force-all -i wkhtmltox_0.12.6-1.stretch_amd64.deb
# wget https://www.mytaxexpress.com/download/libssl1.1_1.1.1f-1ubuntu2.17_amd64.deb
# dpkg -i libssl1.1_1.1.1f-1ubuntu2.17_amd64.deb
# wget https://archive.debian.org/debian/pool/main/libj/libjpeg8/libjpeg8_8b-1_amd64.deb
# dpkg -i libjpeg8_8b-1_amd64.deb
RUN wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.stretch_amd64.deb
RUN dpkg --force-all -i wkhtmltox_0.12.6-1.stretch_amd64.deb
RUN wget https://www.mytaxexpress.com/download/libssl1.1_1.1.1f-1ubuntu2.17_amd64.deb
RUN dpkg -i libssl1.1_1.1.1f-1ubuntu2.17_amd64.deb
RUN wget https://archive.debian.org/debian/pool/main/libj/libjpeg8/libjpeg8_8b-1_amd64.deb
RUN dpkg -i libjpeg8_8b-1_amd64.deb
# Install apache2 and PHP and PHP modules
RUN apt update && \

View File

@@ -32,7 +32,7 @@ class Helper {
$sql .= " AND `$columnName` = '" . $filterValue . "'";
} else if (strpos($columnName, "|") !== false) {
foreach (explode(" ", $filterValue) as $item)
$sql .= " AND CONCAT(" . join(",", explode("|", $columnName)) . ") LIKE '%" . str_replace("%", "", $item) . "%'";
$sql .= " AND CONCAT(" . join(",", explode("|", $columnName)) . ") LIKE '%" . $item . "%'";
} else if ($filterValue[0] === "%") {
$sql .= " AND `$columnName` LIKE '" . $filterValue . "'";
} else if ($filterValue[strlen($filterValue) - 1] === "%") {

View File

@@ -8,6 +8,7 @@
* @property array $columns
* @property array $additionalActions
* @property array $additionalJSVariables
* @property array $additionalHead
* @property array $infoMessages
* @property bool $onlyView
* @property array $defaultOrder
@@ -33,8 +34,13 @@ class TTCrud extends mfBaseController {
$this->redirect("Dashboard");
}
$modelName = str_replace('Controller', 'Model', get_class($this));
$this->model = new $modelName();
$c = get_class($this);
foreach ([str_replace('Controller', 'Model', $c), str_replace('Controller', '', $c)] as $m)
if (class_exists($m)) {
$this->model = new $m();
break;
}
$this->postData = json_decode(file_get_contents('php://input'), true);
$this->checkArray = $this->getCheckArray();
$this->infoMessages = $this->getInfoMessages();
@@ -75,6 +81,7 @@ class TTCrud extends mfBaseController {
];
if (!empty($this->additionalJSVariables) && is_array($this->additionalJSVariables)) $JS_VARIABLES = array_merge($JS_VARIABLES, $this->additionalJSVariables);
if (!empty($this->additionalHead) && is_array($this->additionalHead)) $this->layout()->set('additionalHead', $this->additionalHead);
Helper::renderVue($this, $pageName, $this->headerTitle, $JS_VARIABLES);
}
@@ -273,8 +280,10 @@ class TTCrud extends mfBaseController {
$data = [];
if (count($data) < 11) {
$filter = [$filterKey => '%' . $this->request->q . '%'];
$lazyData = $this->model::getAll($filter, 10);
// $this->request->q replace ? with
$data = $this->model::getAll([$textKey => $this->request->q . '%'], 10);
$lazyData = $this->model::getAll([$filterKey => $this->request->q], 10);
$data = array_merge($data, $lazyData);
$data = array_unique($data, SORT_REGULAR);
$data = array_slice($data, 0, 10);

View File

@@ -10,10 +10,24 @@ class TTCrudBaseModel {
}
}
private static function getFullyQualifiedTable(): string {
$table = str_replace('Model', '', get_called_class());
$tableIncludesADBSuffix = strpos($table, 'ADB') !== false;
$table = str_replace('ADB', '', $table);
return "`" . ($tableIncludesADBSuffix ? ADDRESSDB_DBNAME : FRONKDB_DBNAME) . "`.`" . $table . "`";
}
private static function getDB() {
if (strpos(self::getFullyQualifiedTable(), ADDRESSDB_DBNAME) !== false)
$FronkDB = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
else $FronkDB = FronkDB::singleton();
return $FronkDB->link;
}
public static function create($data) {
$FronkDB = FronkDB::singleton();
$db = $FronkDB->link;
$table = self::getTable();
$db = self::getDB();
$table = self::getFullyQualifiedTable();
self::checkAllFields($data, ['id']);
$sqlColumns = [];
@@ -39,16 +53,12 @@ class TTCrudBaseModel {
$sqlColumns[] = "`$field`";
}
$sql = "INSERT INTO `$table` (" . implode(", ", $sqlColumns) . ") VALUES (" . implode(", ", $sqlValues) . ")";
$sql = "INSERT INTO $table (" . implode(", ", $sqlColumns) . ") VALUES (" . implode(", ", $sqlValues) . ")";
$db->query($sql) or die($db->error);
return $db->insert_id;
}
public static function getTable(): string {
return str_replace('Model', '', get_called_class());
}
/**
* Checks if all required fields of the current class are present in a given data array.
*
@@ -87,11 +97,10 @@ class TTCrudBaseModel {
public static function get($id): TTCrudBaseModel {
$FronkDB = FronkDB::singleton();
$db = $FronkDB->link;
$db = self::getDB();
$id = $db->real_escape_string($id);
$table = self::getTable();
$sql = "SELECT * FROM `$table` WHERE `id` = $id";
$table = self::getFullyQualifiedTable();
$sql = "SELECT * FROM $table WHERE `id` = $id";
$result = $db->query($sql);
// as TTCRudBaseModel is abstract, we need to get the class name of the child class
@@ -100,11 +109,10 @@ class TTCrudBaseModel {
}
public static function count($filter = []): int {
$FronkDB = FronkDB::singleton();
$db = $FronkDB->link;
$table = self::getTable();
$db = self::getDB();
$table = self::getFullyQualifiedTable();
$filter = self::getSQLFilter($filter);
$sql = "SELECT COUNT(*) as count FROM `$table` $filter";
$sql = "SELECT COUNT(*) as count FROM $table $filter";
$result = $db->query($sql);
return $result->fetch_assoc()['count'];
@@ -128,11 +136,10 @@ class TTCrudBaseModel {
}
public static function getAll($filter = [], $limit = null, $offset = 0, $order = ["key" => null]): array {
$FronkDB = FronkDB::singleton();
$db = $FronkDB->link;
$table = self::getTable();
$db = self::getDB();
$table = self::getFullyQualifiedTable();
$filter = self::getSQLFilter($filter);
$sql = "SELECT * FROM `$table` $filter";
$sql = "SELECT * FROM $table $filter";
$sql .= $order['key'] === null ? " ORDER BY `id` ASC" : " ORDER BY `" . $order['key'] . "` " . $order['order'];
$sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset;
@@ -153,9 +160,8 @@ class TTCrudBaseModel {
}
public static function update($data) {
$FronkDB = FronkDB::singleton();
$db = $FronkDB->link;
$table = self::getTable();
$db = self::getDB();
$table = self::getFullyQualifiedTable();
// Check if all fields are set
self::checkAllFields($data);
@@ -183,17 +189,16 @@ class TTCrudBaseModel {
$values[] = $value === null ? "`$field` = NULL" : "`$field` = '" . $db->real_escape_string($value) . "'";
}
$sql = "UPDATE `$table` SET " . implode(", ", $values) . " WHERE `id` = " . $db->real_escape_string($data['id']);
$sql = "UPDATE $table SET " . implode(", ", $values) . " WHERE `id` = " . $db->real_escape_string($data['id']);
$db->query($sql);
return $db->affected_rows;
}
public static function delete($id) {
$FronkDB = FronkDB::singleton();
$db = $FronkDB->link;
$table = self::getTable();
$db = self::getDB();
$table = self::getFullyQualifiedTable();
$id = $db->real_escape_string($id);
$sql = "DELETE FROM `$table` WHERE `id` = $id";
$sql = "DELETE FROM $table WHERE `id` = $id";
$db->query($sql);
return $db->affected_rows;
}

View File

@@ -371,7 +371,7 @@ class mfBaseController
}
public static function sendError(string $message): void {
http_response_code(422); // More appropriate status for validation errors
http_response_code(500);
self::returnJson(['success' => false, 'message' => $message]);
exit;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -0,0 +1,22 @@
{
"name": "thetool Patching",
"short_name": "Patching",
"description": "Patching tool for thetool.",
"start_url": "/Patching/",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/assets/images/pwa-thetool-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/assets/images/pwa-thetool-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -0,0 +1,22 @@
{
"name": "thetool Lieferscheine",
"short_name": "Lieferscheine",
"description": "Lieferscheine für thetool.",
"start_url": "/WarehouseShippingNote/",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/assets/images/pwa-thetool-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/assets/images/pwa-thetool-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -48,6 +48,8 @@ $jsFiles = [
"plugins/vue/tt-components/tt-checkbox.js",
"plugins/vue/tt-components/tt-textarea.js",
"plugins/vue/tt-components/tt-position-manager.js",
"plugins/vue/tt-components/tt-tooltip.js",
"plugins/vue/tt-components/tt-map.js",
];

View File

@@ -0,0 +1,247 @@
Vue.component('csv-import-modal', {
props: {
show: { type: Boolean, required: true },
title: { type: String, required: true },
apiUrl: { type: String, required: true },
showNetworkAreas: { type: Boolean, default: false },
networkAreas: { type: Array, default: () => [] }
},
data() {
return {
selectedFile: null,
loading: false,
errorMessage: null,
fileInputKey: Date.now(),
selectedNetworkArea: null
};
},
methods: {
handleFileChange(event) {
const file = event.target.files[0];
this.resetInputState();
if (!file) return;
const fileNameLower = file.name.toLowerCase();
const allowedTypes = ['text/csv', 'application/vnd.ms-excel'];
if (!fileNameLower.endsWith('.csv') && !allowedTypes.includes(file.type)) {
this.errorMessage = 'Bitte wählen Sie eine gültige CSV-Datei aus (.csv).';
this.resetFileInputVisuals();
return;
}
this.selectedFile = file;
},
readFileAsString() {
return new Promise((resolve, reject) => {
if (!this.selectedFile) {
return reject(new Error("Keine Datei ausgewählt."));
}
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = () => reject(new Error("Fehler beim Lesen der Datei."));
reader.readAsText(this.selectedFile, 'UTF-8');
});
},
parseCSV(csvText) {
if (!csvText || typeof csvText !== 'string') return [];
const lines = csvText.trim().split(/\r?\n/);
if (lines.length < 2) return [];
const headers = lines[0].split(';').map(h => this.cleanValue(h));
const data = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
const values = line.split(';');
if (values.length !== headers.length) continue;
const entry = {};
headers.forEach((header, j) => {
entry[header] = this.cleanValue(values[j]);
});
data.push(entry);
}
return data;
},
cleanValue(value) {
if (typeof value !== 'string') return value;
let cleaned = value.trim();
if (cleaned.startsWith('"') && cleaned.endsWith('"')) {
cleaned = cleaned.slice(1, -1).replace(/""/g, '"');
}
return cleaned;
},
async submit() {
if (!this.selectedFile) {
this.errorMessage = 'Bitte wählen Sie zuerst eine Datei aus.';
return;
}
if (this.showNetworkAreas && !this.selectedNetworkArea) {
this.errorMessage = 'Bitte wählen Sie einen Netzbereich aus.';
return;
}
this.errorMessage = null;
this.loading = true;
try {
const csvString = await this.readFileAsString();
const parsedData = this.parseCSV(csvString);
const payload = { data: parsedData };
if (this.showNetworkAreas) {
payload.networkAreaId = this.selectedNetworkArea;
}
const response = await axios.post(this.apiUrl, payload);
window.notify(response.data.success ? 'success' : 'warning', response.data.message || 'Import erfolgreich.');
this.$emit('close', true);
} catch (error) {
let backendMsg = error.response?.data?.message || '';
let detailsMsg = '';
if (error.response?.data?.errors) {
detailsMsg = Object.values(error.response.data.errors).flat().join(', ');
}
this.errorMessage = `Importfehler: ${error.message || 'Unbekannter Fehler.'}${backendMsg ? ' Server: ' + backendMsg : ''}${detailsMsg ? ' Details: ' + detailsMsg : ''}`;
window.notify('error', this.errorMessage);
} finally {
this.loading = false;
}
},
closeModal() {
if (!this.loading) {
this.$emit('close', false);
}
},
resetFileInputVisuals() {
this.fileInputKey = Date.now();
},
resetInputState() {
this.selectedFile = null;
this.errorMessage = null;
},
resetAll() {
this.resetInputState();
this.resetFileInputVisuals();
this.selectedNetworkArea = null;
}
},
watch: {
show(newVal) {
if (!newVal) {
this.resetAll();
}
}
},
template: `
<tt-modal
:show="show"
@update:show="closeModal"
:title="title"
@submit="submit"
:save-loading="loading"
save-text="Importieren"
:delete="false"
>
<tt-loader :absolute="false" v-if="loading"/>
<template v-else>
<div v-if="showNetworkAreas" class="form-group" style="margin: 10px 0">
<tt-select
label="Netzbereich auswählen"
v-model="selectedNetworkArea"
:options="networkAreas"
:required="true"
:disabled="loading || !networkAreas || networkAreas.length === 0"
sm
row
/>
<small v-if="!networkAreas || networkAreas.length === 0" class="form-text text-danger">
Keine Netzbereiche verfügbar.
</small>
</div>
<div class="form-group" style="margin: 10px 0">
<label>CSV-Datei auswählen (Trennzeichen: Semikolon ';')</label>
<input
type="file"
class="form-control"
accept=".csv, text/csv, application/vnd.ms-excel"
@change="handleFileChange"
:key="fileInputKey"
:disabled="loading"
/>
<small v-if="selectedFile" class="form-text text-muted">
Ausgewählt: {{ selectedFile.name }}
</small>
</div>
<div v-if="errorMessage" class="alert alert-danger mt-2" role="alert">
{{ errorMessage }}
</div>
</template>
</tt-modal>
`
});
Vue.component('a-d-b-rimo-fcp', {
template: `
<tt-card>
<tt-table-crud ref="table">
<template #table-top-buttons>
<div style="display: flex; gap: 10px;">
<tt-button icon="fas fa-upload" text="FCPs Importieren" additional-class="btn-outline-success" @click="showImportFCPModal = true" />
<tt-button icon="fas fa-upload" text="Locations Importieren" additional-class="btn-outline-success" @click="showImportLocationsModal = true" />
<tt-button text="Karte anzeigen" icon="fas fa-map" additional-class="btn-outline-primary" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/ADBRimoFcp/Map'" />
</div>
</template>
</tt-table-crud>
<csv-import-modal
v-if="showImportFCPModal"
:show="showImportFCPModal"
:show-network-areas="true"
:network-areas="networkAreas"
title="FCPs Importieren"
:api-url="fcpApiUrl"
@close="handleModalClose"
/>
<csv-import-modal
v-if="showImportLocationsModal"
:show="showImportLocationsModal"
:show-network-areas="true"
:network-areas="networkAreas"
title="Locations Importieren"
:api-url="locationsApiUrl"
@close="handleModalClose"
/>
</tt-card>
`,
data() {
return {
showImportFCPModal: false,
showImportLocationsModal: false,
networkAreas: window.TT_CONFIG?.CRUD_CONFIG?.columns?.find(col => col.key === 'netzgebiet_id')?.modal?.items || [],
fcpApiUrl: window.TT_CONFIG['BASE_PATH'] + '/ADBRimoFcp/ImportFCPs',
locationsApiUrl: window.TT_CONFIG['BASE_PATH'] + '/ADBRimoFcp/ImportLocations',
window
}
},
methods: {
handleModalClose(importWasSuccessful) {
this.showImportFCPModal = false;
this.showImportLocationsModal = false;
if (importWasSuccessful) this.$refs.table.$refs.table.refreshTable();
}
}
});

View File

@@ -0,0 +1,93 @@
Vue.component('ADBRimoFcpMap', {
data() {
return {
mapMarkers: [],
isLoading: true,
error: null,
window,
fetchUrl: window.TT_CONFIG.BASE_PATH + '/ADBRimoFcp/getAllFCPs',
};
},
async created() {
await this.fetchAndPrepareData();
},
methods: {
async fetchAndPrepareData() {
this.isLoading = true;
this.error = null;
try {
const response = await axios.get(this.fetchUrl);
if (response.data && response.data.success && Array.isArray(response.data.data)) {
this.mapMarkers = response.data.data
.filter(fcp => fcp.gps_lat != null && fcp.gps_long != null)
.map(fcp => ({
lat: fcp.gps_lat,
lng: fcp.gps_long,
options: {
asyncPopupContent: async (markerData) => {
const response = await axios.get(`${this.window.TT_CONFIG.BASE_PATH}/ADBRimoFcp/getById?id=${fcp.id}`);
const fullFcpData = response.data;
return `
<div style="padding: 0px 5px 5px 5px; font-size: 0.85rem;">
<h5 class="mb-3 mt-1">${fullFcpData.name}</h5>
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="font-weight-bold py-1">RIMO ID:</td>
<td class="py-1" style="word-break: break-all;">${fullFcpData.rimo_id}</td>
</tr>
<tr>
<td class="font-weight-bold py-1">RIMO Ex State:</td>
<td class="py-1">${fullFcpData.rimo_ex_state}</td>
</tr>
<tr>
<td class="font-weight-bold py-1">RIMO Op State:</td>
<td class="py-1">${fullFcpData.rimo_op_state}</td>
</tr>
<tr>
<td class="font-weight-bold py-1">Building Type:</td>
<td class="py-1">${fullFcpData.building_type}</td>
</tr>
<tr>
<td class="font-weight-bold py-1">Coordinates:</td>
<td class="py-1">
<a href="https://maps.google.com/?q=${fullFcpData.gps_lat},${fullFcpData.gps_long}" target="_blank" class="text-primary">
<i class="fas fa-map-marker-alt mr-1"></i>${fullFcpData.gps_lat},<br> ${fullFcpData.gps_long}
</a>
</td>
</tr>
</tbody>
</table>
</div>
`; },
}
}));
} else {
console.error("Invalid data format from API:", response.data);
this.error = "Invalid data format received.";
this.mapMarkers = [];
}
} catch (err) {
console.error("Error fetching FCP data:", err);
this.error = "Failed to load FCP locations.";
this.mapMarkers = [];
} finally {
this.isLoading = false;
}
}
},
template: `
<tt-card style="height: 80vh; position: relative;">
<template #header>
<h5>FCP Locations</h5>
</template>
<div v-if="!isLoading && error" class="alert alert-danger">
{{ error }}
</div>
<tt-map :markers-data="mapMarkers" :loading="isLoading"></tt-map>
<div v-if="!isLoading && !error && mapMarkers.length === 0" class="alert alert-info">
No FCP locations found.
</div>
</tt-card>
`
});

View File

@@ -0,0 +1,119 @@
Vue.component('pipework-history', {
//language=Vue
template: `<tt-card>
<div class="filter-row">
<tt-input label="Addresse" v-model="address" placeholder="Adresse" row sm/>
<tt-autocomplete :items="window.TT_CONFIG.NETWORKS" v-model="selectedNetwork" label="Netzgebiet" row/>
<tt-date-picker date-range v-model="dateRange" label="Datum" row sm/>
<tt-button :disabled="checkParameters !== true" @click="getPipeworkHistory" text="Anzeigen" sm icon="fa-solid fa-magnifying-glass" additional-class="btn-primary"/>
<span v-if="checkParameters !== true" class="text-danger">{{ checkParameters }}</span>
</div>
<div>
<tt-table v-if="pipeworkHistory.length > 0" :config="pipeworkHistoryTableConfig" :data="pipeworkHistory" :key="pipeworkHistoryTableIndex">
<template #item_type="{ row }">
<span v-if="row.value_int !== null">{{ row.value_int }}</span>
<span v-else-if="row.value_string !== null">{{ row.value_string }}</span>
<span v-else-if="row.value_text !== null">{{ row.value_text }}</span>
<span v-else>Unbekannt</span>
</template>
<template #last_edited_at="{ row }">
<span v-if="row.last_edited_at">{{ window.moment.unix(row.last_edited_at).format('DD.MM.YYYY HH:mm') }}</span>
<span v-else>Unbekannt</span>
</template>
<template #last_edited_by_user_id="{ row }">
<span v-if="row.last_edited_by_user_id">{{ window.TT_CONFIG.USERS.find(user => user.value === row.last_edited_by_user_id)?.text }}</span>
<span v-else>Unbekannt</span>
</template>
<template #item_id="{ row }">
<tt-button @click="openPipework(row)" text="Zum Tiefbau" icon="fa-solid fa-pen-to-square" additional-class="btn-primary" sm/>
</template>
</tt-table>
</div>
</tt-card>`,
data() {
return {
window: window,
address: '',
dateRange: {
from: window.moment().subtract(4, 'weeks').unix(),
to: window.moment().unix()
},
selectedNetwork: null,
pipeworkHistory: [],
pipeworkHistoryLoading: true,
pipeworkHistoryError: false,
pipeworkHistoryTableIndex: 0,
pipeworkHistoryTableConfig: {
key: 'PipeworkHistoryTable',
tableHeader: 'Tiefbau Historie',
defaultPageSize: 50,
headers: [
{text: 'Straße', key: 'building_street'},
{text: 'Ort', key: 'building_city'},
{text: 'Feld', key: 'item_label'},
{text: 'Wert', key: 'item_type'},
{text: 'Editiert', key: 'last_edited_at'},
{text: 'Von', key: 'last_edited_by_user_id'},
{text: 'Actions', key: 'item_id'},
],
}
}
},
computed: {
checkParameters() {
if (!this.selectedNetwork) {
return 'Bitte Netzgebiet auswählen';
} else if (this.dateRange.from && this.dateRange.to) {
const from = window.moment.unix(this.dateRange.from);
const to = window.moment.unix(this.dateRange.to);
const diff = to.diff(from, 'days');
if (diff > 28) {
return 'Bitte Zeitraum von maximal 4 Wochen auswählen';
}
}
return true;
}
},
methods: {
async getPipeworkHistory() {
this.pipeworkHistoryLoading = true;
this.pipeworkHistoryError = false;
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Pipework/HistoryAPI`, {
params: {
network_id: this.selectedNetwork,
street_filter: this.address,
from: this.dateRange.from,
to: this.dateRange.to
}
})
this.pipeworkHistory = response.data.data;
this.pipeworkHistoryLoading = false;
this.pipeworkHistoryTableIndex++;
} catch (error) {
console.error(error);
this.pipeworkHistoryLoading = false;
this.pipeworkHistoryError = true;
this.window.notify('error', 'Fehler beim Abrufen der Daten');
}
},
openPipework(row) {
const networkId = this.selectedNetwork;
const street = row.building_street;
this.window.open(`${window.TT_CONFIG.BASE_PATH}/Pipework?filter[network_id]=${networkId}&filter[street]=${street}&filter[status_id]=`, '_blank');
}
}
})

View File

@@ -295,7 +295,7 @@ Vue.component('warehouse-article', {
<th>Preis</th>
<th>Summe</th>
</tr>
<tr v-for="(item, index) in order.orders" :key="index">
<tr v-for="(item, index) in order.orders" :key="index + item.id">
<td>{{item.title}}</td>
<td>{{item.amount}}</td>
<td>{{item.purchasePrice}} €</td>
@@ -363,5 +363,17 @@ Vue.component('warehouse-article', {
window.location.href = `${window['TT_CONFIG']['BASE_PATH']}/WarehouseOrder`;
}, 2000);
}
},
mounted() {
const table = this.$refs.table?.$refs?.table;
if (!table) return;
const showId = new URLSearchParams(window.location.search).get('showId');
const currentFilterId = table.filters?.id;
if ((showId && currentFilterId !== showId) || (!showId && currentFilterId)) {
table.filters = showId ? { id: showId } : {};
table.refreshTable();
}
}
})

View File

@@ -1,14 +1,5 @@
@media (min-width: 992px) {
.modal-lg, .modal-xl {
/*max width either 90% or 1120px*/
max-width: min(90vw, 1120px) !important;
}
}
@media (max-width: 992px) {
.warehouse-order-modal-positions-entry-container {
display: grid;
grid-template-columns: 1fr 1fr !important;
grid-gap: 10px;
max-width: min(90vw) !important;
}
}

View File

@@ -1,64 +1,73 @@
Vue.component('warehouse-offer-modal', {
props: {
id: {type: [String, Number], required: true},
props: {
id: {type: [String, Number], required: true},
mode: {type: String, default: 'edit'}
},
template: `
<tt-modal :show="true"
@submit="submit"
:delete="id !== 'create'"
:title="id === 'create' ? 'Angebot erstellen' : \`Angebot #\${id} bearbeiten\`"
@update:show="$emit('close')">
<div style="width: 99%"><h4 class="text-center">Angebotdetails</h4>
<tt-select label="Sachbearbeiter"
:options="window.TT_CONFIG.CRUD_CONFIG.columns.find(column => column.key === 'createBy')?.modal.items"
sm
row
v-model="offer.editor"/>
<tt-autocomplete label="Kunde" v-model="offer.customerNumber" sm row :api-url="billAddrAutoCompleteUrl"/>
<tt-input label="Kundenreferenz" v-model="offer.reference" sm row/>
<tt-textarea label="Angebotszweck" v-model="offer.purpose" sm row/>
<hr>
<h4 class="text-center">Kundenadresse</h4>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 2fr 1fr 2fr 1fr 1fr;">
<tt-input label="Name" v-model="offer.customerName" sm/>
<tt-input label="Kontakt" v-model="offer.contactPerson" sm/>
<tt-input label="Straße" v-model="offer.customerStreet" sm/>
<tt-input label="PLZ" v-model="offer.customerZip" sm/>
<tt-input label="Ort" v-model="offer.customerCity" sm/>
</div>
<hr>
<h4 class="text-center">Positionen</h4>
<tt-positions-manager group-mode ref="positionsManager" v-model="offer.positions" :config="positionsConfig" @updateField-article="fetchArticleData"/>
<hr>
<tt-input label="Gesamtrabatt (%)" v-model="offer.totalDiscount" sm row type="number"/>
<tt-select label="Zahlungskonditionen" :options="paymentTerms" sm row v-model="offer.paymentTerms"/>
<tt-select label="Lieferkonditionen" :options="deliveryTerms" sm row v-model="offer.deliveryTerms"/>
<tt-textarea label="Schlusstext" sm rows="11" row v-model="offer.closingText"/>
<hr>
<tt-textarea label="Notizen" v-model="offer.notes" sm row/>
</div>
</tt-modal>
`,
<tt-modal :show="true"
@submit="submit"
:delete="id !== 'create'"
:title="id === 'create' ? 'Angebot erstellen' : \`Angebot #\${id} bearbeiten\`"
@update:show="$emit('close')">
<div style="width: 99%"><h4 class="text-center">Angebotdetails</h4>
<tt-select label="Sachbearbeiter"
:options="window.TT_CONFIG.CRUD_CONFIG.columns.find(column => column.key === 'createBy')?.modal.items"
sm
row
v-model="offer.editor"/>
<tt-autocomplete label="Kunde" v-model="offer.customerNumber" sm row :api-url="billAddrAutoCompleteUrl"/>
<tt-input label="Kundenreferenz" v-model="offer.reference" sm row/>
<tt-textarea label="Angebotszweck" v-model="offer.purpose" sm row/>
<hr>
<h4 class="text-center">Kundenadresse</h4>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 2fr 1fr 2fr 1fr 1fr 1fr;">
<tt-input label="Name" v-model="offer.customerName" sm/>
<tt-input label="Kontakt" v-model="offer.contactPerson" sm/>
<tt-input label="Straße" v-model="offer.customerStreet" sm/>
<tt-input label="PLZ" v-model="offer.customerZip" sm/>
<tt-input label="Ort" v-model="offer.customerCity" sm/>
<tt-input label="USt-IdNr." v-model="offer.customerVAT" sm/>
</div>
<hr>
<h4 class="text-center">Positionen</h4>
<tt-positions-manager group-mode ref="positionsManager" v-model="offer.positions" :config="positionsConfig"
@updateField-article="fetchArticleData"/>
<hr>
<tt-input label="Gesamtrabatt (%)" v-model="offer.totalDiscount" sm row type="number"/>
<tt-input label="Gesamtsumme" v-model="offerTotalPrice" sm row type="number" disabled/>
<tt-select label="Zahlungskonditionen" :options="paymentTerms" sm row v-model="offer.paymentTerms"/>
<tt-select label="Lieferkonditionen" :options="deliveryTerms" sm row v-model="offer.deliveryTerms"/>
<tt-textarea label="Schlusstext" sm rows="11" row v-model="offer.closingText"/>
<hr>
<tt-textarea label="Notizen" v-model="offer.notes" sm row/>
</div>
<template v-slot:footer-prepend>
<tt-input placeholder="Vorlagenname" no-form-group v-model="templateName"/>
<tt-button text="Als Vorlage speichern" @click="saveTemplate" icon="fas fa-save" additional-class="btn-success"/>
</template>
</tt-modal>
`,
data() {
return {
billAddrAutoCompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1',
window: window,
positionsConfig: {
fields: {
article: {
type: 'autocomplete',
label: 'Artikel',
apiUrl: '/WarehouseArticle/autoComplete',
window: window,
positionsConfig: {
fields: {
article: {
type: 'autocomplete',
label: 'Artikel',
apiUrl: '/WarehouseArticle/autoComplete',
customFieldReference: 'WarehouseArticle',
},
amount: {type: 'input', label: 'Menge', inputType: 'number'},
unit: {type: 'input', label: 'Einheit'},
amount: {type: 'input', label: 'Menge', inputType: 'number'},
unit: {type: 'input', label: 'Einheit'},
articleNumber: {type: 'input', label: 'Artikelnummer'},
isAlternative: {type: 'checkbox', label: 'Alternativposition'},
unitPrice: {type: 'input', label: 'Einzelpreis', inputType: 'number'},
discount: {type: 'input', label: 'Rabatt (%)', inputType: 'number'},
unitPrice: {type: 'input', label: 'Einzelpreis', inputType: 'number'},
discount: {type: 'input', label: 'Rabatt (%)', inputType: 'number'},
},
validateForm: (formData) => {
const requiredFields = ['article', 'amount', 'unitPrice'];
@@ -71,45 +80,46 @@ Vue.component('warehouse-offer-modal', {
return true;
},
},
paymentTerms: [
paymentTerms: [
{value: 'net30', text: '30 Tage netto'},
{value: 'net60', text: '60 Tage netto'},
{value: 'immediate', text: 'Sofort fällig'},
],
deliveryTerms: [
deliveryTerms: [
{value: 'ex_works', text: 'Ab Werk'},
{value: 'free_delivery', text: 'Frei Haus'},
{value: 'fob', text: 'FOB'},
],
offer: {
editor: window.TT_CONFIG['USER_ID'],
customerNumber: '',
reference: '',
purpose: '',
customerName: '',
customerStreet: '',
customerZip: '',
customerCity: '',
customerVAT: '',
positions: [],
offer: {
editor: window.TT_CONFIG['USER_ID'],
customerNumber: '',
reference: '',
purpose: '',
customerName: '',
customerStreet: '',
customerZip: '',
customerCity: '',
customerVAT: '',
positions: [],
alternativePositions: [],
totalDiscount: 0,
paymentTerms: 'net30',
deliveryTerms: 'ex_works',
closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\n' +
'\n' +
'Auftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\n' +
'\n' +
'Diese Angebot hat eine Gültigkeit von 4 Wochen.\n' +
'\n' +
'Verrechnung erfolgt nach tatsächlichem Aufwand.\n' +
'\n' +
'Wir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\n' +
'\n' +
'Sollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.\n' +
' ',
notes: '',
}
totalDiscount: 0,
paymentTerms: 'net30',
deliveryTerms: 'ex_works',
closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\n' +
'\n' +
'Auftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\n' +
'\n' +
'Diese Angebot hat eine Gültigkeit von 4 Wochen.\n' +
'\n' +
'Verrechnung erfolgt nach tatsächlichem Aufwand.\n' +
'\n' +
'Wir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\n' +
'\n' +
'Sollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.\n' +
' ',
notes: '',
},
templateName: '',
}
},
async mounted() {
@@ -122,6 +132,7 @@ Vue.component('warehouse-offer-modal', {
},
methods: {
async submit() {
this.offer.totalAmount = this.offerTotalPrice;
if (this.offer.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
const url = this.id === 'create'
@@ -147,32 +158,107 @@ Vue.component('warehouse-offer-modal', {
this.$refs.positionsManager.updateField('unit', response.data.unit);
}
},
async saveTemplate() {
if (!this.templateName) return window.notify('error', 'Bitte geben Sie einen Namen für die Vorlage ein.');
if (this.offer.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/createTemplate`, {
name: this.templateName,
positions: this.offer.positions,
totalDiscount: this.offer.totalDiscount,
paymentTerms: this.offer.paymentTerms,
deliveryTerms: this.offer.deliveryTerms,
closingText: this.offer.closingText,
notes: this.offer.notes
});
if (response.data.success) {
window.notify('success', response.data.message ?? 'Vorlage erfolgreich gespeichert');
} else {
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
}
}
},
watch: {
'offer.customerNumber': async function () {
if (!this.offer.customerNumber) return;
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/Address/api?do=getAddress&id=${this.offer.customerNumber}`);
if (response.data.status !== 'OK' || !response.data.result.address) {
this.window.notify('error', 'Kundenadresse konnte nicht gefunden werden');
return;
}
const address = response.data.result.address;
this.offer.customerName = address.company || `${address.firstname} ${address.lastname}`;
this.offer.customerStreet = address.street;
this.offer.customerZip = address.zip;
this.offer.customerCity = address.city;
}
},
computed: {
offerTotalPrice() {
const totalPrice = this.offer.positions.reduce((total, position) => {
if (!position.amount) return total;
const discount = position.discount ? (position.unitPrice * position.amount) * position.discount / 100 : 0;
return total + (position.unitPrice * position.amount) - discount;
}, 0);
return totalPrice - (totalPrice * this.offer.totalDiscount / 100);
}
}
});
Vue.component('warehouse-offer', {
template: `
<tt-card>
<warehouse-offer-modal v-if="offerModalId" :id="offerModalId" @close="offerModalId = null;$refs.table.$refs.table.refreshTable()"/>
<button @click="offerModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
<tt-table-crud emit-edit @edit="offerModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }">
<div>
<h5>Notizen</h5>
<p>{{ row.notes }}</p>
<h5>Verlauf</h5>
<ul>
<li v-for="entry in row.journal">{{ entry.date }} - {{ entry.description }}</li>
</ul>
</div>
</template>
</tt-table-crud>
</tt-card>
`,
<tt-card>
<warehouse-offer-modal v-if="offerModalId" :id="offerModalId" ref="modal"
@close="offerModalId = null;$refs.table.$refs.table.refreshTable()"/>
<div style="display: flex; gap: 8px">
<button @click="offerModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
<div class="dropdown">
<button class="btn btn-outline-primary dropdown-toggle" @click="offerTemplatesDropdown = !offerTemplatesDropdown">
Angebot aus Vorlage erstellen <i class="fas fa-caret-down"></i>
</button>
<ul class="dropdown-menu" :class="{'show': offerTemplatesDropdown}">
<li v-for="template in offerTemplates" @click="createOfferFromTemplate(template)">
<a class="dropdown-item">{{ template.templateName }}</a>
</li>
</ul>
</div>
</div>
<tt-table-crud emit-edit @edit="offerModalId = $event.id" ref="table">
</tt-table-crud>
</tt-card>
`,
data() {
return {
window: window,
window: window,
offerModalId: null,
offerTemplates: [],
offerTemplatesDropdown: false,
}
},
async mounted() {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/getTemplates`);
this.offerTemplates = response.data;
},
methods: {
async createOfferFromTemplate(template) {
this.offerModalId = 'create';
await this.$nextTick();
this.$refs.modal.offer.positions = JSON.parse(template.positions);
this.$refs.modal.offer.totalDiscount = template.totalDiscount;
this.$refs.modal.offer.paymentTerms = template.paymentTerms;
this.$refs.modal.offer.deliveryTerms = template.deliveryTerms;
this.$refs.modal.offer.closingText = template.closingText;
this.$refs.modal.offer.notes = template.notes;
this.window.notify('success', 'Angebot aus Vorlage erstellt');
}
}
});

View File

@@ -278,8 +278,19 @@ Vue.component('warehouse-order-modal', {
v-model="order.positions"
:config="positionsConfig"
@updateField-article="fetchDistributors"
@updateField-article_text="fetchDistributors"
@updateField-distributorId="fetchDistributorData"
/>
>
<template #form-actions-append>
<!-- v-if $refs.positionsManager.formData.article parse is int and not NaN we show a <tt-button> with a @click to BASE_PATH /WarehouseArticle?showId-->
<tt-button
v-if="!isNaN(parseInt($refs.positionsManager.formData.article))"
text="Zum Artikel"
sm
additional-class="btn-outline-primary"
@click="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticle?showId=' + $refs.positionsManager.formData.article)"/>
</template>
</tt-positions-manager>
<hr>
<h4 class="text-center">Lieferadresse</h4>
@@ -319,6 +330,7 @@ Vue.component('warehouse-order-modal', {
return {
window: window,
showSendShippingNote: null,
lastDistributorFetch: null,
positionsConfig: {
customOrdering: 'distributorId',
fields: {
@@ -327,6 +339,7 @@ Vue.component('warehouse-order-modal', {
label: 'Artikel',
apiUrl: '/WarehouseArticle/autoComplete',
customFieldReference: 'WarehouseArticle',
emitDisplayValue: true,
},
distributorId: {type: 'select', label: 'Lieferant', options: [], customFieldReference: 'WarehouseDistributor'},
distributorArticleNumber: {type: 'input', label: 'Lieferant Art-Nr.'},
@@ -334,23 +347,12 @@ Vue.component('warehouse-order-modal', {
buyPrice: {type: 'input', label: 'Einkaufspreis', inputType: 'number'},
verwendung: {type: 'input', label: 'Verwendung'},
},
validateForm: (formData) => {
const fields = [
{key: 'amount', message: 'Bitte füllen Sie die Menge aus'},
{key: 'distributorId', message: 'Bitte füllen Sie den Lieferanten aus'},
{key: 'article', message: 'Bitte füllen Sie den Artikel aus'},
{key: 'buyPrice', message: 'Bitte füllen Sie den Einkaufspreis aus'}
];
for (const field of fields) {
if (!formData[field.key]) {
window.notify('error', field.message);
return false;
}
}
return true;
},
validateFormOptions: [
{key: 'amount', message: 'Bitte füllen Sie die Menge aus'},
{key: 'distributorId', message: 'Bitte füllen Sie den Lieferanten aus'},
{key: 'article', message: 'Bitte füllen Sie den Artikel aus'},
{key: 'buyPrice', message: 'Bitte füllen Sie den Einkaufspreis aus'}
],
},
order: {
extReference: '',
@@ -373,23 +375,26 @@ Vue.component('warehouse-order-modal', {
return;
}
const orderRequest = JSON.parse(localStorage.getItem('WarehouseOrder_create'));
if (!orderRequest) return;
const orderRequests = JSON.parse(localStorage.getItem('WarehouseOrder_create'));
if (!orderRequests) return;
const positions = JSON.parse(orderRequest.positions);
this.order.positions = await Promise.all(positions.map(async p => {
const distributor = (await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getArticleDistributorData`,
{params: {articleId: p.articleId}})).data[0];
return {
article: p.articleId,
amount: p.amount,
buyPrice: distributor.purchasePrice,
distributorId: distributor.id,
distributorArticleNumber: distributor.externalArticleNumber,
verwendung: `${p.hasOwnProperty('purpose') ? p.purpose : ''} [Bestellwunsch: #${orderRequest.id}]`,
linkedOrderRequestId: orderRequest.id
};
}));
for (const orderRequest of orderRequests) {
const positions = JSON.parse(orderRequest.positions);
const parsedPositions = await Promise.all(positions.map(async p => {
const distributor = (await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getArticleDistributorData`,
{params: {articleId: p.articleId}})).data[0];
return {
article: p.articleId,
amount: p.amount,
buyPrice: distributor.purchasePrice,
distributorId: distributor.id,
distributorArticleNumber: distributor.externalArticleNumber,
verwendung: `${p.hasOwnProperty('purpose') ? p.purpose : ''} [Bestellwunsch: #${orderRequest.id}]`,
linkedOrderRequestId: orderRequest.id
};
}));
this.order.positions = [...this.order.positions, ...parsedPositions];
}
localStorage.removeItem('WarehouseOrder_create');
},
@@ -433,7 +438,9 @@ Vue.component('warehouse-order-modal', {
async fetchDistributors(article) {
const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getArticleDistributorData`;
const params = typeof article === 'string' ? {allDistributor: true} : {articleId: article};
if (JSON.stringify(params) === JSON.stringify(this.lastDistributorFetch)) return;
this.lastDistributorFetch = params;
const response = await axios.get(url, {params});
this.positionsConfig.fields.distributorId.options = response.data.map(distributor => ({
value: distributor.id,
@@ -570,10 +577,8 @@ Vue.component('warehouse-order', {
},
methods: {
async closeModal() {
console.log("hi");
this.orderModalId = null;
this.changeStatusModalId = null;
console.log("hi");
await new Promise(resolve => setTimeout(resolve, 250));
this.$refs.table.$refs.table.refreshTable();
},

View File

@@ -5,35 +5,35 @@ window['TT_CONFIG']['CRUD_CONFIG']['editCondition'] = (row) => {
window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [
...window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"],
{
key: "cancelRequest",
title: "Bestellwunsch stornieren",
class: "fas fa-ban text-danger", // Instead of fa-times, use a ban icon
key: "cancelRequest",
title: "Bestellwunsch stornieren",
class: "fas fa-ban text-danger", // Instead of fa-times, use a ban icon
condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.cancelled === 0,
},
{
key: "uncancelRequest",
title: "Bestellwunsch wiederherstellen",
class: "fas fa-undo text-warning", // Use an undo icon for restore, with a warning color
key: "uncancelRequest",
title: "Bestellwunsch wiederherstellen",
class: "fas fa-undo text-warning", // Use an undo icon for restore, with a warning color
condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.cancelled === 1,
},
{
key: "createOrder",
title: "Bestellung erstellen",
class: "fas fa-shopping-cart text-primary", // Use shopping-cart to indicate order creation
key: "addOrderToCart",
title: "Bestellung in den Warenkorb",
class: "fas fa-shopping-cart text-primary", // Use shopping-cart to indicate order creation
condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'
&& row.cancelled === 0 && (!row.linkedOrderIds || row.linkedOrderIds.length === 0)
&& JSON.parse(row.positions).filter(position => position.articleId_text).length === 0,
},
{
key: "doneOrder",
title: "Bestellwunsch erledigt",
class: "fas fa-check-circle text-success", // Use check-circle for marking as done
key: "doneOrder",
title: "Bestellwunsch erledigt",
class: "fas fa-check-circle text-success", // Use check-circle for marking as done
condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.done === 0,
},
{
key: "undoneOrder",
title: "Bestellwunsch wieder offen",
class: "fas fa-redo-alt text-info", // Use redo-alt to indicate reopening the order
key: "undoneOrder",
title: "Bestellwunsch wieder offen",
class: "fas fa-redo-alt text-info", // Use redo-alt to indicate reopening the order
condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.done === 1,
},
];
@@ -41,12 +41,12 @@ window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [
Vue.component('add-log-modal', {
props: {
orderRequestId: {type: Number, required: true},
type: {type: String, default: 'accept'}
type: {type: String, default: 'accept'}
},
data() {
return {
orderRequest: null,
note: '',
note: '',
};
},
async mounted() {
@@ -58,11 +58,11 @@ Vue.component('add-log-modal', {
window.notify('error', 'Bestellwunsch wurde storniert');
}
},
methods: {
methods: {
async submit() {
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/createNewLogAction`, {
orderRequestId: this.orderRequestId,
note: this.note,
note: this.note,
});
if (response.data.success) {
@@ -75,85 +75,80 @@ Vue.component('add-log-modal', {
}
},
template: `
<tt-modal :show="true" :delete="false" @submit="submit" @update:show="$emit('close')" title="Status ändern">
<tt-loader :absolute="false" v-if="!orderRequest"/>
<template v-else>
<tt-textarea label="Bemerkung*" v-model="note" sm/>
</template>
</tt-modal>
<tt-modal :show="true" :delete="false" @submit="submit" @update:show="$emit('close')" title="Status ändern">
<tt-loader :absolute="false" v-if="!orderRequest"/>
<template v-else>
<tt-textarea label="Bemerkung*" v-model="note" sm/>
</template>
</tt-modal>
`
`
})
Vue.component('order-request-log', {
props: {orderRequestId: {type: Number, required: true}},
data: () => ({
data: () => ({
logs: []
}),
async mounted() {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/getLogById`, {params: {orderRequestId: this.orderRequestId}});
this.logs = response.data;
const [{data: logs}, {data: order}] = await Promise.all([
axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/getLogById`, {params: {orderRequestId: this.orderRequestId}}),
axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/getById`, {params: {id: this.orderRequestId}})
]);
this.logs = logs;
const response2 = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/getById`, {params: {id: this.orderRequestId}});
// check if linkedOrderIds is set and if set length > 0 and if so, get the linked orders logs
// and add them to the logs array and sort them by create date
// if response2.data.linkedOrderIds is a string try to parse it
if (typeof response2.data.linkedOrderIds === 'string') {
try {
response2.data.linkedOrderIds = JSON.parse(response2.data.linkedOrderIds);
} catch {
}
if (typeof order.linkedOrderIds === 'string') try {
order.linkedOrderIds = JSON.parse(order.linkedOrderIds);
} catch {
order.linkedOrderIds = [];
}
if (response2.data.linkedOrderIds && response2.data.linkedOrderIds.length > 0) {
const linkedOrdersLogs = await Promise.all(
response2.data.linkedOrderIds.map(async (id) => {
const res1 = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id}});
const res2 = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getLogById`, {params: {id}});
if (!order.linkedOrderIds?.length) return;
return res2.data.map(log => {
log.message = `${res1.data.orderNumber} - ${log.message}`;
return log;
})
})
);
this.logs = this.logs.concat(...linkedOrdersLogs).sort((a, b) => b.create - a.create);
}
const linkedLogs = (await Promise.all(
order.linkedOrderIds.map(async id => {
const [{data: order}, {data: orderLogs}] = await Promise.all([
axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id}}),
axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getLogById`, {params: {id}})
]);
return orderLogs.map(log => ({...log, message: `${order.orderNumber} - ${log.message}`}));
})
)).flat();
},
this.logs = [...logs, ...linkedLogs].sort((a, b) => b.create - a.create);
}
,
methods: {
formatDate: date => window.moment(date * 1000).format('DD.MM.YYYY HH:mm'),
formatDate: date => window.moment(date * 1000).format('DD.MM.YYYY HH:mm'),
getUserName: id => window.TT_CONFIG.CRUD_CONFIG.columns.find(col => col.key === 'createBy')?.modal.items.find(u => u.value === id)?.text
},
//language=Vue
template: `
<div>
<template v-if="logs.length > 0">
<hr>
<h3>Log</h3>
<div v-for="log in logs" :key="log.id" class="alert alert-light">
{{ formatDate(log.create) }} ({{ getUserName(log.createBy) }}) | {{ log.message }}
</div>
</template>
</div>
`
<div>
<template v-if="logs.length > 0">
<hr>
<h3>Log</h3>
<div v-for="log in logs" :key="log.id" class="alert alert-light">
{{ formatDate(log.create) }} ({{ getUserName(log.createBy) }}) | {{ log.message }}
</div>
</template>
</div>
`
})
Vue.component('linked-order-status', {
props: ['linkedOrders'],
data: () => ({
orders: [],
data: () => ({
orders: [],
statusTranslations: {
new: 'Neu',
accepted: 'Akzeptiert',
ordered: 'Bestellt',
sent: 'Versendet',
new: 'Neu',
accepted: 'Akzeptiert',
ordered: 'Bestellt',
sent: 'Versendet',
partiallyDelivered: 'Teilweise geliefert',
fullyDelivered: 'Geliefert',
cancelled: 'Storniert',
fullyDelivered: 'Geliefert',
cancelled: 'Storniert',
}
}),
async mounted() {
@@ -163,85 +158,97 @@ Vue.component('linked-order-status', {
},
//language=Vue
template: `
<div>
<div>
<span v-for="(order, index) in orders" :key="order.id" :class="{ 'mt-1': index > 0 }"
class="badge badge-pill badge-primary mr-1">{{ order.orderNumber }} - {{ statusTranslations[order.status] }}</span>
</div>`
</div>`
});
Vue.component('warehouse-order-request-detail', {
props: {
positions: {
type: Array,
type: Array,
required: true
}
},
//language=Vue
template: `
<div style="display: flex; justify-content: center; margin-bottom: 10px">
<div class="WarehouseOrderRequestDetailTable">
<div>ARTIKEL</div>
<div>MENGE</div>
<div>ZWECK</div>
<template v-for="position in positions">
<div>
<tt-resolver v-if="position.articleId" reference="WarehouseArticle" :value="position.articleId"/>
<span v-else>{{ position.articleId_text }}</span>
</div>
<div>{{ position.amount }}</div>
<div>{{ position.purpose }}</div>
</template>
</div>
</div>
`
<div style="display: flex; justify-content: center; margin-bottom: 10px">
<div class="WarehouseOrderRequestDetailTable">
<div>ARTIKEL</div>
<div>MENGE</div>
<div>ZWECK</div>
<template v-for="position in positions">
<div>
<tt-resolver v-if="position.articleId" reference="WarehouseArticle" :value="position.articleId"/>
<span v-else>{{ position.articleId_text }}</span>
</div>
<div>{{ position.amount }}</div>
<div>{{ position.purpose }}</div>
</template>
</div>
</div>
`
});
Vue.component('warehouse-order-request', {
//language=Vue
template: `
<tt-card>
<tt-table-crud @openHistory="openHistory"
@cancelRequest="cancelRequest"
@uncancelRequest="uncancelRequest"
@doneOrder="doneOrder"
@undoneOrder="undoneOrder"
@createLog="createLog"
@createOrder="createOrder"
ref="crud">
<template #linkedorderids="{row}">
<linked-order-status :linkedOrders="row.linkedOrderIds" v-if="row.linkedOrderIds"/>
</template>
<template #expandedRow="{row}">
<warehouse-order-request-detail :positions="JSON.parse(row['positions'])"/>
<order-request-log :orderRequestId="row.id"/>
<hr>
<h4>Notiz</h4>
<span>{{ row.note }}</span>
</template>
</tt-table-crud>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
<add-log-modal v-if="addLogModalId"
:orderRequestId="addLogModalId"
@close="addLogModal = false; addLogModalId = null; $refs.crud.$refs.table.refreshTable()"/>
</tt-card>
`,
data: () => ({
<tt-card>
<tt-table-crud @openHistory="openHistory"
@cancelRequest="cancelRequest"
@uncancelRequest="uncancelRequest"
@doneOrder="doneOrder"
@undoneOrder="undoneOrder"
@createLog="createLog"
@createOrder="createOrder"
@addOrderToCart="addOrderToCart"
ref="crud">
<template #table-top-buttons>
<tt-tooltip :text="createOrdersButtonToolTipText">
<tt-button
:disabled="orderShoppingCart.length === 0"
@click="createOrder"
additional-class="btn-outline-success text-center"
:text="createOrdersButtonText" icon="fas fa-shopping-cart"/>
</tt-tooltip>
</template>
<template #linkedorderids="{row}">
<linked-order-status :linkedOrders="row.linkedOrderIds" v-if="row.linkedOrderIds"/>
</template>
<template #expandedRow="{row}">
<warehouse-order-request-detail :positions="JSON.parse(row['positions'])"/>
<order-request-log :orderRequestId="row.id"/>
<hr>
<h4>Notiz</h4>
<span>{{ row.note }}</span>
</template>
</tt-table-crud>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
<add-log-modal v-if="addLogModalId"
:orderRequestId="addLogModalId"
@close="addLogModal = false; addLogModalId = null; $refs.crud.$refs.table.refreshTable()"/>
</tt-card>
`,
data: () => ({
window,
historyModal: false,
historyModalId: null,
addLogModal: false,
addLogModalId: null,
showHiddenRequests: false,
historyModal: false,
historyModalId: null,
addLogModal: false,
addLogModalId: null,
showHiddenRequests: false,
showCanceledRequests: false,
orderRequestModalId: null
orderRequestModalId: null,
orderShoppingCart: [],
}),
methods: {
methods: {
openHistory(e) {
this.historyModal = true;
this.historyModalId = e.id;
},
async cancelRequest(row, cancel) {
async cancelRequest(row, cancel = '1') {
if (!confirm('Bestellwunsch wirklich stornieren?')) return;
const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrderRequest/cancel?id=${row.id}&cancel=${cancel}`);
window.notify(res.data.success ? 'success' : 'error',
@@ -268,12 +275,26 @@ Vue.component('warehouse-order-request', {
uncancelRequest(row) {
this.cancelRequest(row, '0');
},
async createOrder(row) {
const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrderRequest/getById?id=${row.id}`);
async createOrder() {
if (this.orderShoppingCart.length > 0) {
localStorage.setItem('WarehouseOrder_create', JSON.stringify(this.orderShoppingCart));
window.location.href = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder`;
} else window.notify('warning', 'Warenkorb ist leer');
},
async addOrderToCart(row) {
const res = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/getById?id=${row.id}`);
if (res.data?.positions && typeof res.data.positions === 'string') {
localStorage.setItem('WarehouseOrder_create', JSON.stringify(res.data));
window.location.href = `${window.TT_CONFIG.BASE_PATH}/WarehouseOrder`;
} else window.notify('error', res.data.message || 'Fehler beim erstellen der Bestellung');
this.orderShoppingCart.push(res.data);
window.notify('success', 'Bestellung in den Warenkorb gelegt');
} else window.notify('error', res.data.message || 'Fehler beim hinzufügen der Bestellung');
}
},
computed: {
createOrdersButtonText: function () {
return this.orderShoppingCart.length > 0 ? `${this.orderShoppingCart.length} Bestellung(en) erstellen` : 'Bestellung(en) erstellen';
},
createOrdersButtonToolTipText: function () {
return this.orderShoppingCart.length > 0 ? `Erstellt ${this.orderShoppingCart.length} Bestellung(en)` : 'Keine Bestellung im Warenkorb';
}
}
});

View File

@@ -188,4 +188,18 @@ input:disabled + .ios-switch-slider {
.see-through-test-modal .modal {
position: unset !important;
background: unset !important;
}
.see-through-test-modal .btn-secondary {
display:none;
}
@media all and (display-mode: standalone) {
#topnav {
display: none !important;
}
.wrapper {
padding-top: 0 !important;
}
}

View File

@@ -29,6 +29,12 @@ window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [
"class": "fas fa-ban text-danger",
"condition": (row) => (window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['new', 'in_progress', 'accepted'].includes(row.status)) || (row.status === 'new' && row.signature === null),
},
{
"key": "status_to_new",
"title": "Lieferschein wiedereröffnen",
"class": "fas fa-redo-alt text-success",
"condition": (row) => (window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.status === 'cancelled'),
},
{
"key": "add_log",
"title": "Log Eintrag hinzufügen",
@@ -64,7 +70,7 @@ Vue.component('warehouse-shipping-note-positions', {
<ul v-if="!loading">
<li v-for="position in positions">
<span>{{ position.amount }}x
{{ position.article ? articleData[position.article]?.text : position.articlePacket ? articlePacketData[position.articlePacket]?.text : position.articleText }}</span>
{{ position.article ? articleData[position.article]?.text : position.articlePacket ? articlePacketData[position.articlePacket]?.text : position.articleText ? position.articleText : position.article_text }}</span>
</li>
<template v-for="entry in hoursEntries">
<li><span>{{ entry.hourCount }}h Arbeitszeit</span></li>
@@ -294,8 +300,8 @@ Vue.component('warehouse-shipping-note-see-through', {
<warehouse-shipping-note-modal v-if="currentRow" :id="currentRow.id" @close="fetchData" ref="modal"/>
</div>
<div v-if="activeTab === 'Logs'" style="flex: 1; overflow: auto;" class="see-through-test-modal">
<warehouse-shipping-note-logs :shipping-note-id="currentRow.id"/>
<add-log-modal-sn v-if="currentRow" :shipping-note-id="currentRow.id" @close="fetchData"/>
<warehouse-shipping-note-logs :shipping-note-id="currentRow.id" :key="'logs' + logModalKey"/>
<add-log-modal-sn v-if="currentRow" :shipping-note-id="currentRow.id" @close="logModalKey++" :key="'modal'+logModalKey"/>
</div>
</div>
</template>
@@ -305,6 +311,7 @@ Vue.component('warehouse-shipping-note-see-through', {
`,
data() {
return {
logModalKey: 0,
currentPage: 1,
perPage: 1,
rows: [],
@@ -355,6 +362,10 @@ Vue.component('warehouse-shipping-note-see-through', {
})
this.rows = response.data.rows;
if (this.rows.length === 0) {
this.$emit('close');
this.window.notify('info', 'Keine Lieferscheine mit diesem Status gefunden');
}
this.loading = false;
}
@@ -408,6 +419,7 @@ Vue.component('warehouse-shipping-note', {
@status_to_invoiced="changeStatus($event.id, 'invoiced')"
@status_to_on_hold="changeStatus($event.id, 'on_hold')"
@status_to_cancelled="changeStatus($event.id, 'cancelled')"
@status_to_new="changeStatus($event.id, 'new')"
@add_log="addLogModalId = $event.id"
@edit="shippingNoteModalId = $event.id"
ref="table">
@@ -448,3 +460,15 @@ Vue.component('warehouse-shipping-note', {
},
}
})
window.addEventListener('DOMContentLoaded', () => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/WarehouseShippingNote/sw', { scope: '/' })
.then(registration => {
console.log('Patching PWA Service Worker registered with scope:', registration.scope);
})
.catch(error => {
console.error('Patching PWA Service Worker registration failed:', error);
});
}
})

View File

@@ -21,6 +21,13 @@ Vue.component('warehouse-shipping-note-modal', {
textElements: [],
hoursEntries: [],
},
availableTypes: [
{text: 'Xinon Intern (XI)', value: 'XI'},
{text: 'Xinon Hersteller (XH)', value: 'XH'},
{text: 'Energie Steiermark (ESTMK)', value: 'ESTMK'},
{text: 'Steirische Breitband- und Digitalinfrastrukturgesellschaft (SBIDI)', value: 'SBIDI'},
{text: 'Verrechnen (V)', value: 'V'},
],
hoursLoading: false,
geoAddr: '',
selectedBillingAddress: '',
@@ -108,6 +115,7 @@ Vue.component('warehouse-shipping-note-modal', {
<template v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true">
<hr>
<tt-autocomplete v-model="shippingNote.billingAddressId" :api-url="billAddrAutoCompleteUrl" label="Rechnungsadresse" sm row/>
<tt-autocomplete v-model="shippingNote.type" :items="availableTypes" label="Typ" sm row return-text/>
</template>
<tt-textarea v-model="shippingNote.note" label="Art der Arbeit" sm row/>
@@ -171,8 +179,14 @@ Vue.component('warehouse-shipping-note-modal', {
methods: {
async submit() {
this.loading = true;
if (!this.shippingNote.positions.length && !this.shippingNote.hoursEntries.length)
if (!this.shippingNote.positions.length && !this.shippingNote.hoursEntries.length) {
this.loading = false;
return window.notify('error', 'Mindestens eine Position oder eine Stundenbuchung sind erforderlich');
}
if (this.availableTypes.find(t => t.text === this.shippingNote.type)) {
this.shippingNote.type = this.availableTypes.find(t => t.name === this.shippingNote.type).value;
}
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/${this.id === 'create' ? 'create' : 'update'}`, this.shippingNote);
@@ -253,4 +267,4 @@ Vue.component('warehouse-shipping-note-signature-pad', {
this.shippingNote = response.data;
this.signaturePad = new SignaturePad(document.getElementById('signature-pad'));
}
})
})

View File

@@ -0,0 +1,96 @@
.tt-tooltip-wrapper {
position: relative;
display: inline-block; /* Or 'block' depending on your layout needs */
cursor: pointer;
}
.tt-tooltip-box {
position: absolute;
background-color: #333; /* Dark background */
color: #fff; /* White text */
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 10; /* Ensure it's above other elements */
opacity: 0; /* Hidden by default, fade in */
transition: opacity 0.3s;
pointer-events: none; /* Prevent tooltip from interfering with mouse events */
text-align: center; /* Center text */
}
/* Make tooltip visible when showTooltip is true */
.tt-tooltip-wrapper:hover .tt-tooltip-box {
opacity: 1;
}
/* Positioning */
.tt-tooltip-top {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 5px; /* Space between element and tooltip */
}
.tt-tooltip-bottom {
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 5px;
}
.tt-tooltip-left {
right: 100%;
top: 50%;
transform: translateY(-50%);
margin-right: 5px;
}
.tt-tooltip-right {
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 5px;
}
/* Optional: Add arrows */
.tt-tooltip-box::after {
content: '';
position: absolute;
border-width: 5px;
border-style: solid;
}
.tt-tooltip-top::after {
top: 100%;
left: 50%;
margin-left: -5px;
border-color: #333 transparent transparent transparent;
}
.tt-tooltip-bottom::after {
bottom: 100%;
left: 50%;
margin-left: -5px;
border-color: transparent transparent #333 transparent;
}
.tt-tooltip-left::after {
top: 50%;
left: 100%;
margin-top: -5px;
border-color: transparent transparent transparent #333;
}
.tt-tooltip-right::after {
top: 50%;
right: 100%;
margin-top: -5px;
border-color: transparent #333 transparent transparent;
}
.tt-tooltip-wrapper > * {
display: inline-block; /* Ensure the tooltip wrapper behaves correctly */
width: 100% !important;
}

View File

@@ -126,7 +126,7 @@ Vue.component('tt-autocomplete', {
this.displayValue = response.data[0].text;
} else if (this.value) {
const selectedItem = this.items.find(item => item.value === this.value);
this.displayValue = selectedItem ? selectedItem.text : '';
this.displayValue = selectedItem ? selectedItem.text : this.displayValue;
} else {
if (this.returnText === false && !(typeof this.value === 'undefined' || this.value === '')) this.$emit('input', '');
this.displayValue = this.displayValue.replace(this.oldDisplayValue, '');

View File

@@ -2,9 +2,7 @@
Vue.component('tt-button', {
//language=Vue
template: `
<div>
<template v-if="href">
<a :href="href" class="btn" :class="buttonClasses" @click="handleClick">
<a v-if="href" :href="href" class="btn" :class="buttonClasses" @click="handleClick">
<template v-if="loading">
<span class="spinner"></span>
</template>
@@ -13,9 +11,7 @@ Vue.component('tt-button', {
{{text}}
</template>
</a>
</template>
<template v-else>
<button @click="handleClick" class="btn" :class="buttonClasses" :disabled="loading">
<button v-else @click="handleClick" class="btn" :class="buttonClasses" :disabled="loading">
<template v-if="loading">
<span class="spinner"></span>
</template>
@@ -24,8 +20,6 @@ Vue.component('tt-button', {
{{text}}
</template>
</button>
</template>
</div>
`,
props: {
sm: {type: Boolean, default: false},

View File

@@ -10,6 +10,7 @@ Vue.component('tt-input', {
hint: String,
additionalProps: Object,
sm: {type: Boolean, default: false},
noFormGroup: {type: Boolean, default: false},
},
data() {
return {
@@ -22,7 +23,7 @@ Vue.component('tt-input', {
}
},
template: `
<div class="form-group" :class="{'row': row}">
<div :class="{'row': row, 'form-group' : !noFormGroup}">
<slot name="prepend"></slot>
<label
:class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}"

View File

@@ -0,0 +1,185 @@
Vue.component('tt-map', {
props: {
markersData: {
type: Array,
default: () => [] // Expecting [{ lat: Number, lng: Number, options: { maki?: Object, popup?: String, asyncPopupContent?: Function } }, ...]
},
config: {
type: Object,
default: () => ({}) // User overrides for defaults
},
loading: {
type: Boolean,
default: false
}
},
data() {
return {
map: null,
markerLayer: null,
tileLayers: { streets: null, satellite: null },
mapType: localStorage.getItem('tt-map-type') || 'streets', // Default to 'streets' or stored preference
internalLoading: true,
scriptsLoaded: false,
};
},
computed: {
isLoading() {
return this.internalLoading || this.loading;
},
mapConfig() {
const defaults = {
center: [46.9, 15.4995],
zoom: 11,
mapboxKey: window.TT_CONFIG?.MAPBOX_KEY,
streetsTileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}',
streetsTileId: 'mapbox/streets-v11',
satelliteTileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}',
satelliteTileId: 'mapbox/satellite-streets-v12', // Or 'mapbox/satellite-v9'
tileAttribution: '© <a href="https://www.mapbox.com/about/maps/">Mapbox</a> © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
clusterOptions: {},
makiMarkerOptions: { icon: "marker", color: "#3b82f6", size: "m" }
};
return { ...defaults, ...this.config }; // Merge user config over defaults
}
},
async mounted() {
try {
await this.loadScripts();
this.scriptsLoaded = true;
this.initializeMap();
this.updateMarkers();
this.internalLoading = false;
} catch (error) {
console.error("Map Initialization Error:", error);
this.internalLoading = false;
}
},
methods: {
loadScripts() {
const scripts = [
{ type: 'link', url: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' },
{ type: 'script', url: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js' },
{ type: 'script', url: 'https://unpkg.com/leaflet-makimarkers@3.1.0/Leaflet.MakiMarkers.js' },
{ type: 'link', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css' },
{ type: 'link', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css' },
{ type: 'script', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js' }
];
const promises = scripts.map(s => new Promise((resolve, reject) => {
let el;
if (s.type === 'script') {
el = document.createElement('script');
el.src = s.url; el.async = false; el.onload = resolve; el.onerror = reject;
} else {
el = document.createElement('link');
el.rel = 'stylesheet'; el.href = s.url; resolve();
}
if (el) document.head.appendChild(el); else reject();
}));
return Promise.all(promises);
},
initializeMap() {
if (!this.scriptsLoaded || !L || !L.MarkerClusterGroup || !L.MakiMarkers || !this.mapConfig.mapboxKey) return;
this.map = L.map(this.$refs.mapContainer, { preferCanvas: true }).setView(this.mapConfig.center, this.mapConfig.zoom);
L.MakiMarkers.accessToken = this.mapConfig.mapboxKey;
this.tileLayers.streets = L.tileLayer(this.mapConfig.streetsTileUrl, {
attribution: this.mapConfig.tileAttribution, maxZoom: 18, id: this.mapConfig.streetsTileId,
tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey
});
this.tileLayers.satellite = L.tileLayer(this.mapConfig.satelliteTileUrl, {
attribution: this.mapConfig.tileAttribution, maxZoom: 18, id: this.mapConfig.satelliteTileId,
tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey
});
this.tileLayers[this.mapType].addTo(this.map); // Add initial layer based on preference
this.markerLayer = L.markerClusterGroup(this.mapConfig.clusterOptions);
this.map.addLayer(this.markerLayer);
// Invalidate size after initial load if container might not have been ready
this.$nextTick(() => {
this.map.invalidateSize();
});
// Add resize listener
window.addEventListener('resize', this.handleResize);
},
updateMarkers() {
if (!this.map || !this.markerLayer || !this.scriptsLoaded) return;
this.markerLayer.clearLayers();
const markersToAdd = [];
this.markersData.forEach(data => {
if (data.lat != null && data.lng != null) {
const makiOptions = { ...this.mapConfig.makiMarkerOptions, ...(data.options?.maki || {}) };
const icon = L.MakiMarkers.icon(makiOptions);
const marker = L.marker([data.lat, data.lng], { icon: icon });
if (data.options?.popup) {
marker.bindPopup(data.options.popup);
} else if (data.options?.asyncPopupContent && typeof data.options.asyncPopupContent === 'function') {
marker.bindPopup(() => '<div class="popup-loader">Loading...</div>'); // Initial content
marker.on('popupopen', async (e) => {
const popup = e.popup;
try {
const content = await data.options.asyncPopupContent(data); // Pass marker data to function
popup.setContent(content);
} catch (error) {
console.error("Error loading popup content:", error);
popup.setContent('<div class="text-danger">Failed to load content.</div>');
}
popup.update(); // Adjust size
});
}
if (data.options?.tooltip) marker.bindTooltip(data.options.tooltip);
markersToAdd.push(marker);
}
});
if (markersToAdd.length > 0) this.markerLayer.addLayers(markersToAdd);
},
toggleMapType() {
this.map.removeLayer(this.tileLayers[this.mapType]);
this.mapType = this.mapType === 'streets' ? 'satellite' : 'streets';
this.tileLayers[this.mapType].addTo(this.map);
localStorage.setItem('tt-map-type', this.mapType);
},
handleResize() {
if (this.map) {
// Use debounce if resize events fire too rapidly
this.map.invalidateSize();
}
}
},
watch: {
markersData: { handler() { this.updateMarkers(); }, deep: true },
loading(newVal) {
// Optional: Invalidate map size when loading finishes, in case container size changed
if (!newVal && this.map) {
this.$nextTick(() => this.map.invalidateSize());
}
}
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize);
if (this.map) {
this.map.remove();
this.map = null;
}
},
template: `
<div style="position: relative; width: 100%; height: 100%;">
<div v-if="isLoading" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1001; background: rgba(255,255,255,0.7); display: flex; justify-content: center; align-items: center;">
<tt-loader></tt-loader>
</div>
<div ref="mapContainer" style="width: 100%; height: 100%; z-index: 1;" :style="{ visibility: internalLoading ? 'hidden' : 'visible' }"></div>
<button @click="toggleMapType"
class="btn btn-light btn-sm"
style="position: absolute; top: 10px; right: 10px; z-index: 1000;">
<i :class="mapType === 'streets' ? 'fas fa-globe-americas' : 'fas fa-map'"></i>
{{ mapType === 'streets' ? 'Satellite' : 'Map' }}
</button>
</div>
`
});

View File

@@ -69,6 +69,7 @@ Vue.component('tt-positions-manager',
:emit-display-value="field.emitDisplayValue || false"
v-model="formData[key]"
@input="$emit('updateField-' + key, $event)"
@displayValue="$emit('updateField-' + key + '_text', $event)"
:ref="'autocomplete-' + key"
:api-url="window.TT_CONFIG['BASE_PATH'] + field.apiUrl"
sm
@@ -105,6 +106,8 @@ Vue.component('tt-positions-manager',
:additional-class="selectedIndex === null ? 'btn-primary' : 'btn-success'"
:text="selectedIndex === null ? 'Hinzufügen' : 'Aktualisieren'"/>
</div>
<slot name="form-actions-append"></slot>
</div>
@@ -170,9 +173,7 @@ Vue.component('tt-positions-manager',
},
checkEmitDisplayValueAutocomplete() {
for (const [key, field] of Object.entries(this.config.fields)) {
console.log("HI");
if ((typeof field.showCondition === 'function' && field.showCondition(this.formData) === true || !field.showCondition) && field.type === 'autocomplete' && field.emitDisplayValue && (isNaN(this.formData[key]) || !this.formData[key]) && this.$refs['autocomplete-' + key][0]) {
console.log("hi");
this.$set(this.formData, key + '_text', this.$refs['autocomplete-' + key][0].displayValue);
this.$delete(this.formData, key);
}

View File

@@ -65,7 +65,7 @@ Vue.component('tt-table-crud', {
<tt-input v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'number'" v-model="crudModalData[column.key]" :label="column.text" type="number" sm row/>
<tt-textarea v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'textarea'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
<tt-select v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'select'" v-model="crudModalData[column.key]" :label="column.text" :options="column.items" sm row/>
<tt-autocomplete v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'autocomplete'" v-model="crudModalData[column.key]" :label="column.text" :api-url="column.apiUrl" :items="column.items" sm row :return-text="column.returnText" />
<tt-autocomplete v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'autocomplete'" v-model="crudModalData[column.key]" :label="column.text" :api-url="column.apiUrl" :items="typeof column.items === 'string' ? [] : column.items" sm row :return-text="column.returnText" />
<tt-date-picker v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'datepicker'" v-model="crudModalData[column.key]" :label="column.text" sm row :date-range="false" :ref="column.key.toLowerCase() + '-modal-input'"/>
<tt-icon-select v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'icon-select'" v-model="crudModalData[column.key]" :options="column.items" :label="column.text" sm row/>
<tt-checkbox v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'checkbox'" v-model="crudModalData[column.key]" :label="column.text" sm row/>

View File

@@ -0,0 +1,31 @@
Vue.component('tt-tooltip', {
props: {
text: {
type: String,
required: true,
default: 'Tooltip text'
},
position: {
type: String,
default: 'top', // Options: top, bottom, left, right
validator: function (value) {
// The value must match one of these strings
return ['top', 'bottom', 'left', 'right'].indexOf(value) !== -1;
}
}
},
data() {
return {
showTooltip: true
};
},
template: `
<div class="tt-tooltip-wrapper"
@mouseenter="showTooltip = true"
@mouseleave="showTooltip = false">
<slot></slot> <div v-if="showTooltip" class="tt-tooltip-box" :class="['tt-tooltip-' + position]">
{{ text }}
</div>
</div>
`
});

View File

@@ -1,95 +0,0 @@
#!/usr/bin/php
<?php
//require 'vendor/autoload.php';
require("../../../config/config.php");
define('FRONKDB_SQLDEBUG',false);
error_reporting(E_ALL & ~(E_NOTICE | E_STRICT | E_DEPRECATED));
require_once(LIBDIR."/mvcfronk/mfRouter/mfRouter.php");
require_once(LIBDIR."/mvcfronk/mfBase/mfBaseModel.php");
require_once(LIBDIR."/mvcfronk/mfBase/mfBaseController.php");
$me = new User(1);
define("INTERNAL_USER_ID", $me->id);
define("INTERNAL_USER_USERNAME", $me->username);
define("MFBASE_BYPASS_LOGIN", true);
$filename = __DIR__."/import/SDIBuilding__Locations__FTTx___241204_PremNord.csv";
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
$log = mfLoghandler::singleton();
$input = fopen($filename, "r");
$bom = "\xef\xbb\xbf";
if(fgets($input, 4) !== $bom) {
// BOM not found - rewind pointer to start of file.
rewind($input);
}
$netzgebiet = new ADBNetzgebiet(2);
$default_freigabe = json_encode(["interest", "provision", "order", "reorder"]);
$headers = [];
$u = 0;
$i = 0;
while($csv = fgetcsv($input, 0, ";")) {
$i++;
if($i == 1) {
foreach($csv as $key => $name) {
$headers[$name] = $key;
}
continue;
}
//var_dump($headers);exit;
$fcp = false;
if(!trim($csv[1])) {
continue;
}
$fcp_name = trim($csv[$headers["FCP cluster name"]]);
$rimo_id = trim($csv[$headers["ExternalID"]]);
if(!$rimo_id) {
echo "no rimo id\n";
continue;
}
if(!$fcp_name) {
echo "no fcp name\n";
continue;
}
$fcp = ADBRimoFcp::getFirst(["netzgebiet_id" => $netzgebiet->id, "name" => $fcp_name]);
if(!$fcp) {
echo "FCP nicht gefunden in netzgebiet\n";
continue;
}
$building = ADBHausnummerModel::getFirst(["rimo_id" => $rimo_id]);
if(!$building) {
echo "Hausnummer nicht gefunden\n";
continue;
}
if($building->fcp_id != $fcp->id) {
$building->fcp_id = $fcp->id;
$building->save();
}
$u++;
//echo implode(", ", $csv)."\n";
//$gem_kz = trim($csv[61]);
}
echo "updated $u Hausnummern\n";

View File

@@ -1,102 +0,0 @@
#!/usr/bin/php
<?php
//require 'vendor/autoload.php';
require("../../../config/config.php");
define('FRONKDB_SQLDEBUG',false);
error_reporting(E_ALL & ~(E_NOTICE | E_STRICT | E_DEPRECATED));
require_once(LIBDIR."/mvcfronk/mfRouter/mfRouter.php");
require_once(LIBDIR."/mvcfronk/mfBase/mfBaseModel.php");
require_once(LIBDIR."/mvcfronk/mfBase/mfBaseController.php");
$me = new User(1);
$filename = __DIR__."/import/SDIBuilding__FCPs__241204_PremNord.csv";
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
$log = mfLoghandler::singleton();
$input = fopen($filename, "r");
$bom = "\xef\xbb\xbf";
if(fgets($input, 4) !== $bom) {
// BOM not found - rewind pointer to start of file.
rewind($input);
}
//$gemeinde_id = 1448;
$netzgebiet = new ADBNetzgebiet(2);
$default_freigabe = json_encode(["interest", "provision", "order", "reorder"]);
$headers = [];
$i = 0;
while($csv = fgetcsv($input, 0, ";")) {
$i++;
if($i == 1) {
foreach($csv as $key => $name) {
$headers[$name] = $key;
}
continue;
}
//var_dump($headers);exit;
$fcp = false;
if(!trim($csv[1])) {
continue;
}
$fcp_name = trim($csv[$headers["Name"]]);
$rimo_id = trim($csv[$headers["ExternalID"]]);
$label = trim($csv[$headers["User label"]]);
$ex_state = trim($csv[$headers["Execution state"]]);
$op_state = trim($csv[$headers["Operational state"]]);
$gps_lat = trim($csv[$headers["Latitude"]]);
$gps_long = trim($csv[$headers["Longitude"]]);
$building_type = trim($csv[$headers["Building type"]]);
if(!$rimo_id) {
echo "no rimo id\n";
continue;
}
if(!$fcp_name) {
echo "no fcp name\n";
continue;
}
$data = [
"netzgebiet_id" => $netzgebiet->id,
"name" => $fcp_name,
"rimo_id" => $rimo_id,
"label" => $label,
"building_type" => $building_type,
"rimo_ex_state" => $ex_state,
"rimo_op_state" => $op_state,
"gps_lat" => str_replace(",",".",$gps_lat),
"gps_long" => str_replace(",",".",$gps_long)
];
$fcp = ADBRimoFcp::getFirst(["rimo_id" => $rimo_id]);
if($fcp) {
echo "update\n";
$fcp->update($data);
} else {
echo "create\n";
$fcp = ADBRimoFcp::create($data);
}
if(!$fcp->save()) {
die("Error saving FCP\n");
}
//echo implode(", ", $csv)."\n";
//$gem_kz = trim($csv[61]);
}