Merge remote-tracking branch 'origin/spidev' into spidev

This commit is contained in:
Daniel Spitzer
2025-12-27 19:37:27 +01:00
212 changed files with 24018 additions and 2711 deletions

View File

@@ -13,7 +13,7 @@
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="<?=self::getUrl("Dashboard")?>"><?=MFAPPNAME_SLUG?></a></li>
<?php if(is_array($filter['addresstype']) && count($filter['addresstype'])): ?>
<?php if(array_key_exists('addresstype', $filter) && is_array($filter['addresstype']) && count($filter['addresstype'])): ?>
<li class="breadcrumb-item"><a href="<?=self::getUrl("Address")?>">Personen & Firmen</a></li>
<li class="breadcrumb-item active">
<?php foreach($filter['addresstype'] as $type) { $types[] = __($type); } ?>
@@ -233,7 +233,7 @@
<td><?=$address->phone?></td>
<td><?=$address->email?></td>
<td style="text-align: left; letter-spacing: 4px; font-size: 1.1em;">
<a href="<?=self::getUrl("User", "Index", ["filter" => ["address_id" => $address->id]])?>" title="Benutzer anzeigen"><i class="fas fa-users"></i></a>
<a href="#" onclick="openPriceTypeModal(<?=$address->id?>, '<?=$address->getCompanyOrName()?>')" title="Kunden-Preistyp auswählen"><i class="fas fa-tag"></i></a>
<a href="<?=self::getUrl("Address", "view", ["id" => $address->id, 's' => $pagination['start']])?>"><i class="far fa-eyes" title="Anzeigen"></i></a>
<a href="<?=self::getUrl("Address", "sendServicePin", ["id" => $address->id])?>" onclick="if(!confirm('Soll der Service-PIN an den Kunden gesendet werden?')) return false;"><i class="fas fa-paper-plane" title="Service PIN als PDF per Email an Kunde"></i></a>
<a href="#" onclick="openCreateTicketModal(`<?=$address->getCompanyOrName()?>`, '<?=$address->customer_number?>', '<?=$address->street . ', ' . $address->zip . ' ' . $address->city?>', '<?=$address->phone?>', '<?=$address->email?>', '<?=$address->spin?>')" title="Störungs-Ticket erstellen" class="text-warning"><i class="fas fa-exclamation-triangle"></i></a>
@@ -254,6 +254,79 @@
</div>
</div>
<!-- PRICE TYPE MODAL START -->
<div class="modal fade" id="priceTypeModal" tabindex="-1" role="dialog" aria-labelledby="priceTypeModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="priceTypeModalLabel">Kunden-Preistyp auswählen</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="priceTypeForm">
<input type="hidden" name="address_id" id="priceType_address_id" />
<div class="form-group">
<label class="form-label" for="priceType_type_id">Preistyp für <strong id="priceType_customer_name"></strong></label>
<select class="form-control" name="priceType_type_id" id="priceType_type_id">
<option value="">Kein Preistyp (Standard)</option>
<?php
require_once(dirname(__FILE__)."/../../../application/WarehouseArticlePriceType/WarehouseArticlePriceTypeModel.php");
$priceTypes = WarehouseArticlePriceTypeModel::getAll();
foreach($priceTypes as $priceType):
?>
<option value="<?=$priceType->id?>"><?=$priceType->title?> (Faktor: <?=$priceType->defaultPriceFactor?>)</option>
<?php endforeach; ?>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="savePriceType()">Speichern</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Schließen</button>
</div>
</div>
</div>
</div>
<script type="text/javascript">
const priceTypeGetUrl = '<?=self::getUrl("AddressPriceType", "get")?>';
const priceTypeSaveUrl = '<?=self::getUrl("AddressPriceType", "save")?>';
function openPriceTypeModal(addressId, customerName) {
$("#priceType_address_id").val(addressId);
$("#priceType_customer_name").text(customerName);
$("#priceType_type_id").val('');
$.get(priceTypeGetUrl, { address_id: addressId }, function(response) {
if(response && response.priceType_id) {
$("#priceType_type_id").val(response.priceType_id);
}
}, 'json');
$("#priceTypeModal").modal("show");
}
function savePriceType() {
const addressId = $("#priceType_address_id").val();
const priceTypeId = $("#priceType_type_id").val();
$.post(priceTypeSaveUrl, {
address_id: addressId,
priceType_id: priceTypeId
}, function(response) {
if(response && response.success) {
$("#priceTypeModal").modal("hide");
window.notify('success', response.message || 'Preistyp erfolgreich gespeichert');
} else {
window.notify('error', response.message || 'Fehler beim Speichern des Preistyps');
}
}, 'json');
}
</script>
<!-- PRICE TYPE MODAL END -->
<!-- CREATE TICKET MODAL START -->
<!--add a bootstrap modal here and below add a new <script> which creates a form for self::getUrl("Address", "createTicket") with post parameters

View File

@@ -189,6 +189,11 @@
<?php if($me->can("ADBExtended") || $me->isAdmin()): ?>
<a class="btn btn-outline-secondary ml-2" href="<?=self::getUrl("ADBWohneinheit", "duplicate")?>"><i class="fas fa-fw fa-copy"></i> Doppelte Homes</a>
<?php endif; ?>
<?php if($me->isAdmin()): ?>
<button type="submit" name="rimoAddressUpdate" value="1" class="btn btn-purple ml-2">
<i class="fas fa-cloud-arrow-up "></i> in Rimo Updaten
</button>
<?php endif; ?>
</div>
</div>
@@ -330,6 +335,10 @@
<a href="<?=self::getUrl("AddressDB", "view", ["id" => $address->id])?>"><i class="far fa-fw fa-eye" title="Adresse Anzeigen"></i></a>
<a href="<?=self::getUrl("AddressDB", "edit", ["id" => $address->id])?>" class="pl-1"><i class="far fa-fw fa-edit" title="Adresse Bearbeiten"></i></a>
<a href="<?=self::getUrl("AddressDB", "delete", ["id" => $address->id])?>" onclick="if(!confirm('Addresse und alle Wohneinheiten wirklich löschen?')) return false;"><i class="far fa-fw fa-trash-alt text-danger" title="Adresse Löschen"></i></a>
<?php if($me->is("Admin")): ?>
<a href="<?=self::getUrl("Building", "createFromAdb", ["adb_hausnummer_id" => $address->id])?>" target="_blank"><i class="far fa-fw fa-person-digging text-success ml-2" title="Adresse in Netzbau anlegen"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>

View File

@@ -180,7 +180,16 @@
<?php foreach($address->wohneinheiten as $unit): ?>
<tr>
<td>
<a href="#" data-home-id="<?=$unit->id?>" data-home-contact title="Kontakte bearbeiten"><i class="fas fa-users-cog text-primary"></i></a>
<?php
$contacts = $unit->contact ? json_decode($unit->contact, true) : [];
$contactCount = is_array($contacts) ? count($contacts) : 0;
?>
<a href="#" data-home-id="<?=$unit->id?>" data-home-contact title="Kontakte bearbeiten<?=$contactCount ? ' (' . $contactCount . ')' : ''?>" style="position: relative; display: inline-block;">
<i class="fas fa-users-cog <?=$contactCount ? 'text-success' : 'text-muted'?>"></i>
<?php if($contactCount): ?>
<span style="position: absolute; top: -8px; right: -8px; background: #28a745; color: white; border-radius: 50%; width: 16px; height: 16px; font-size: 10px; display: flex; align-items: center; justify-content: center; font-weight: bold; padding: 0; box-sizing: border-box;"><?=$contactCount?></span>
<?php endif; ?>
</a>
<a href="<?=self::getUrl("ADBWohneinheit", "edit", ["id" => $unit->id])?>"><i class="fas fa-edit"></i></a>
</td>
<td><?=$unit->id?></td>

View File

@@ -29,6 +29,9 @@
<div class="row col-12">
<div><a href="<?=self::getUrl("Admin", "customerStatistics")?>">Kundenstatistiken</a></div>
</div>
<div class="row col-12">
<div><a href="<?=self::getUrl("Admin", "downloadBusinessCustomers")?>">Businesskunden CSV herunterladen</a></div>
</div>
<div class="row col-12">
<div><a href="<?=self::getUrl("Admin", "RtrReporting")?>">RTR Reporting</a></div>
</div>

View File

@@ -0,0 +1,52 @@
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 0;
}
.label-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.logo-25 {
max-height: 45px;
margin-bottom: 5px;
}
.logo-50 {
max-height: 70px;
margin-bottom: 10px;
}
.address {
font-size: 8px;
line-height: 1.2;
}
.address-size-50 { font-size: 16px }
.inv-number-25 {
font-size: 14px;
font-weight: bold;
margin-top: 5px;
}
.inv-number-50 {
font-size: 28px;
font-weight: bold;
margin-top: 10px;
}
</style>
<div class="label-container">
<img src="<?php echo BASEDIR ?>/public/assets/images/xinon-full.png" class="logo-<?php echo $size; ?>">
<div class="address address-size-<?php echo $size; ?>">
<?php echo $companyAddress; ?><br>
<?php echo $companyPhone; ?>
</div>
<div class="inv-number-<?php echo $size; ?>">
<?php echo $invNumber; ?>
</div>
</div>

View File

@@ -28,6 +28,9 @@
<div class="card-body">
<input type="hidden" name="id" value="<?=$building->id?>" />
<?php if(isset($adb_hausnummer_id)): ?>
<input type="hidden" name="adb_hausnummer_id" value="<?=$adb_hausnummer_id?>" />
<?php endif; ?>
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="network_id">Netzgebiet *</label>
@@ -70,7 +73,11 @@
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="units">Nutzungseinheiten *</label>
<div class="col-lg-10">
<input type="number" class="form-control" name="units" id="units" value="<?=$building->units?>" />
<?php if($building->units == "from_adb"): ?>
<input type="text" class="form-control" name="units" id="units" value="(auto)" disabled="disabled" />
<?php else: ?>
<input type="number" class="form-control" name="units" id="units" value="<?=$building->units?>" />
<?php endif; ?>
</div>
</div>

View File

@@ -304,14 +304,6 @@ $pagination_entity_name = "Zustimmungserklärungen";
</div>
</div>
<?php } ?>
<!-- i have added this to stats and need to show a "traffic light kinda thing round etc" with a grid 2 row 2 col
"status_light_blue" => $status_light_blue,
"status_light_red" => $status_light_red,
"status_light_yellow" => $status_light_yellow,
"status_light_green" => $status_light_green
-->
<div class="card">
<div class="card-body p-0">
<div class="p-2" style="background-color: #f8f9fa">
@@ -344,6 +336,12 @@ $pagination_entity_name = "Zustimmungserklärungen";
<div style="width: 100%; height: 100%; background-color: #5cb85c; border-radius: 50%;"></div>
<span style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-weight: bold;"><?php echo $stats['status_light_green']; ?></span>
</div>
<!-- Gray -->
<div style="position: relative; width: 60px; height: 60px;">
<div style="width: 100%; height: 100%; background-color: #6c757d; border-radius: 50%;"></div>
<span style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-weight: bold;"><?php echo $stats['status_deferred']; ?></span>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,6 @@
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
<?php //var_dump($project);exit; ?>
<?php if (!isset($project)) $project = null; ?>
<?php $prefillAdbNetzgebietId = $_GET['adb_netzgebiet_id'] ?? null; ?>
<!-- start page title -->
<div class="row">
<div class="col-12">
@@ -27,7 +28,7 @@
</h4>
<form class="form-horizontal" method="post" action="<?= self::getUrl("ConstructionConsentProject", "save") ?>">
<input type="hidden" name="id" value="<?=isset($project) ? $project->id : ""?>"/>
<input type="hidden" name="id" value="<?=$project ? $project->id : ""?>"/>
<div class="card">
<div class="card-body">
@@ -36,21 +37,21 @@
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="name">Projektname *</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="name" id="name" value="<?=$project->name?>" />
<input type="text" class="form-control" name="name" id="name" value="<?=$project ? $project->name : ""?>" />
</div>
</div>
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="email">Emailadresse *</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="email" id="email" value="<?=$project->email?>" />
<input type="text" class="form-control" name="email" id="email" value="<?=$project ? $project->email : ""?>" />
</div>
</div>
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="phone">Telefonnummer *</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="phone" id="phone" value="<?=$project->phone?>" />
<input type="text" class="form-control" name="phone" id="phone" value="<?=$project ? $project->phone : ""?>" />
</div>
</div>
@@ -58,8 +59,9 @@
<label class="col-lg-2 col-form-label" for="adb_network_id">Netzgebiete *</label>
<div class="col-lg-10">
<select class="form-control select2" name="adb_netzgebiet_id[]" id="adb_netzgebiet_id" multiple="multiple">
<?php $projectAdbNetworks = ($project && is_array($project->adb_networks)) ? $project->adb_networks : []; ?>
<?php foreach(ADBNetzgebietModel::getAll() as $net): ?>
<option value="<?=$net->id?>" <?=(is_array($project->adb_networks) && array_key_exists($net->id, $project->adb_networks)) ? "selected='selected'" : ""?> ><?=$net->name?></option>
<option value="<?=$net->id?>" <?=(array_key_exists($net->id, $projectAdbNetworks) || $prefillAdbNetzgebietId == $net->id) ? "selected='selected'" : ""?> ><?=$net->name?></option>
<?php endforeach; ?>
</select>
</div>
@@ -70,21 +72,21 @@
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="sender_name">Absendername *</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="sender_name" id="sender_name" value="<?=$project->sender_name?>" />
<input type="text" class="form-control" name="sender_name" id="sender_name" value="<?=$project ? $project->sender_name : ""?>" />
</div>
</div>
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="sender_email">Absender Emailadresse *</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="sender_email" id="sender_email" value="<?=$project->sender_email?>" />
<input type="text" class="form-control" name="sender_email" id="sender_email" value="<?=$project ? $project->sender_email : ""?>" />
</div>
</div>
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="sender_reply_to">Antworten an (Reply To)</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="sender_reply_to" id="sender_reply_toender_email" value="<?=$project->sender_reply_to?>" />
<input type="text" class="form-control" name="sender_reply_to" id="sender_reply_toender_email" value="<?=$project ? $project->sender_reply_to : ""?>" />
</div>
</div>
@@ -96,8 +98,9 @@
<label class="col-lg-2 col-form-label" for="sender_reply_to">Berechtigte Firmen</label>
<div class="col-lg-10">
<select class="form-control select2" name="address_id[]" id="adb_hausnummer_id" multiple="multiple">
<?php $projectAddresses = ($project && is_array($project->addresses)) ? $project->addresses : []; ?>
<?php foreach(AddressModel::search(["addresstype" => TT_NETWORK_ROLES_WITH_OWNER]) as $address): ?>
<option value="<?=$address->id?>" <?=(array_key_exists($address->id, $project->addresses)) ? "selected='selected'" : ""?>><?=$address->getCompanyOrName()?><?=($address->customer_number) ? " (".$address->customer_number.")" : ""?></option>
<option value="<?=$address->id?>" <?=(array_key_exists($address->id, $projectAddresses)) ? "selected='selected'" : ""?>><?=$address->getCompanyOrName()?><?=($address->customer_number) ? " (".$address->customer_number.")" : ""?></option>
<?php endforeach; ?>
</select>
</div>
@@ -108,7 +111,7 @@
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="note">Interne Notiz</label>
<div class="col-lg-10">
<textarea id="note" class="form-control" name="note" rows="5"><?=$project->note?></textarea>
<textarea id="note" class="form-control" name="note" rows="5"><?=$project ? $project->note : ""?></textarea>
</div>
</div>

View File

@@ -2,7 +2,7 @@
$maxLength = max(mb_strlen($firstline ?? ''), mb_strlen($secondline ?? ''), mb_strlen($thirdline ?? ''));
$fontSize = '13px';
if ($maxLength <= 11) $fontSize = '28px';
if ($maxLength <= 11) $fontSize = '20px';
elseif ($maxLength <= 20) $fontSize = '18px';
elseif ($maxLength <= 45) $fontSize = '16px';

View File

@@ -342,7 +342,8 @@ $pagination_entity_name = "Rechnungen";
<td><?=($invoice->billing_type == "sepa") ? "SEPA" : "Überweisung"?></td>
<td><?=($invoice->billing_delivery == "email") ? "Email" : "Papier"?></td>
<td>
<a href="<?=self::getUrl("Invoice", "downloadInvoiceCsv", ["id" => $invoice->id])?>" title="CSV-Download"><i class="fas fa-file-csv fa-fw"></i></a>
<a href="<?=self::getUrl("Invoice", "downloadInvoiceCsv", ["id" => $invoice->id])?>" title="CSV-Download"><i class="far fa-file-csv fa-fw"></i></a>
<a href="<?=self::getUrl("Invoice", "downloadInvoiceVoiceDetails", ["id" => $invoice->id])?>" title="Download EGN"><i class="far fa-square-phone-flip fa-fw ml-2"></i></a>
</td>
</tr>
<?php endforeach; ?>

View File

@@ -105,8 +105,8 @@
</select>
</div>
<div class="col-2">
<label class="form-label" for="filter_linework_doku_delay">Dokuaufschub</label>
<div class="col-1">
<label class="form-label" for="filter_linework_doku_delay">Dokuaufsch.</label>
<select name="filter[linework_doku_delay]" id="filter_linework_doku_delay" class="form-control">
<option value="0">Ausblenden</option>
<option value="1" <?=($filter['linework_doku_delay'] == 1) ? "selected='selected'" : ""?>>Anzeigen</option>
@@ -115,12 +115,17 @@
<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']?>" />
<input type="text" class="form-control" name="filter[code]" id="filter_code" value="<?=$filter['code'] ?? ''?>" />
</div>
<div class="col-2">
<div class="col-1">
<label class="form-label" for="filter_building_street">Straße</label>
<input type="text" class="form-control" name="filter[building_street]" id="filter_building_street" value="<?=$filter['building_street']?>" />
<input type="text" class="form-control" name="filter[building_street]" id="filter_building_street" value="<?=$filter['building_street'] ?? ''?>" />
</div>
<div class="col-1">
<label class="form-label" for="filter_ap_name">AP-Name</label>
<input type="text" class="form-control" name="filter[ap_name]" id="filter_ap_name" value="<?=$filter['ap_name'] ?? ''?>" />
</div>
<div class="col-2">

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>Xinon Rechnung</title>
<meta charset="utf-8" />
</head>
<body style="border:0; margin: 0;font-family: sans-serif, Verdana;font-size: 11px;" onload="subst()">
<script>
function subst() {
var vars = {};
var query_strings_from_url = document.location.search.substring(1).split('&');
for (var query_string in query_strings_from_url) {
if (query_strings_from_url.hasOwnProperty(query_string)) {
var temp_var = query_strings_from_url[query_string].split('=', 2);
vars[temp_var[0]] = decodeURI(temp_var[1]);
}
}
var css_selector_classes = ['page', 'frompage', 'topage', 'webpage', 'section', 'subsection', 'date', 'isodate', 'time', 'title', 'doctitle', 'sitepage', 'sitepages'];
for (var css_class in css_selector_classes) {
if (css_selector_classes.hasOwnProperty(css_class)) {
var element = document.getElementsByClassName(css_selector_classes[css_class]);
for (var j = 0; j < element.length; ++j) {
element[j].textContent = vars[css_selector_classes[css_class]];
}
}
}
}
</script>
<div style="margin-bottom: 16px;height: 1px"></div>
<div style="color:grey;text-align: center;margin-bottom: 0">
<span>XINON GmbH | Fladnitz 150 | 8322 Studenzen</span><br>
<span>Tel.: +43 3115 40800 | E-Mail: office@xinon.at</span><br>
<span>UID: ATU68711968 | FN: 416556h | LG: Feldbach</span><br>
<span>IBAN: {{ bank_iban }} | BIC: {{ bank_bic }}</span><br>
</div>
<div style="text-align: right">Seite <span class="page"></span> von <span class="topage"></span></div>
<div style="margin-top: 16px;height: 1px"></div>
</body>
</html>

View File

@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html>
<head>
<title>XINON Invoice Header</title>
<meta charset="utf-8" />
<style>
body {
border: 0;
margin: 0;
font-family: sans-serif, Verdana;
font-size: 12px;
}
.info-table {
border-collapse: collapse;
width: 100%;
}
.customer-details {
vertical-align: bottom;
font-size: 14px;
padding-left: 30pt;
width: 35%;
}
.invoice-details {
border: 2px solid #e1e1e1;
padding: 6px;
}
.invoice-details td {
text-align: left;
}
.invoice-details td:first-child {
text-align: right;
}
.separator {
margin-top: 24px;
height: 1px;
}
#topSpacer {
margin-bottom: 32px;
height: 20px;
}
</style>
</head>
<body>
<div id="topSpacer"></div>
<div style="height: 50px; margin-bottom: 8px">
<img alt="Xinon Logo" src="{{ basedir }}/public/assets/images/xinon-full.png" style="text-align:left;height: 85px;">
</div>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td class="customer-details">
<div>{{ addressLine_1 }}</div>
<div>{{ addressLine_2 }}</div>
<div>{{ addressLine_3 }}</div>
<div>{{ addressLine_4 }}</div>
<div>{{ addressLine_5 }}</div>
</td>
<td style="vertical-align: top; text-align: right;">
<table style="display: inline-table; vertical-align: top;">
<tr>
<td style="vertical-align: top; padding-right: 10px;">
<img alt="QR-Code" src="{{ qrCodeSrc }}" style="display: block; height: 100%; max-height: 3.5cm; width: auto;">
</td>
<td>
<table class="invoice-details">
<tr>
<td>Kundennummer:</td>
<td>{{ customerNumber }}</td>
</tr>
<tr>
<td>Verrechnungskonto:</td>
<td>{{ billingAccount }}</td>
</tr>
<tr>
<td>Rechnungsnummer:</td>
<td>{{ invoiceNumber }}</td>
</tr>
<tr>
<td>Belegdatum:</td>
<td>{{ invoiceDate }}</td>
</tr>
{{ leistungszeitraumHtml }}
{{ externeReferenzHtml }}
{{ vatHtml }}
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div class="separator"></div>
</body>
</html>

View File

@@ -0,0 +1,257 @@
<?php
/**
* @var string $ressourcePathPrefix
* @var ManualInvoice $invoice
* @var array $vat
*/
$net_total = $invoice->total;
$gross_total = $invoice->total_gross;
$is_credit = $net_total < 0;
// Check if any position has a discount to conditionally show the discount column
$hasDiscount = false;
foreach($invoice->positions as $p) {
if (($p->discount ?? 0) > 0) {
$hasDiscount = true;
break;
}
}
$gesamtrabatt = $invoice->gesamtrabatt ?? 0;
$subtotal = 0;
foreach($invoice->positions as $p) {
$subtotal += $p->price_total ?? 0;
}
// Group positions by position_group
$groupedPositions = [];
$hasGroups = false;
foreach($invoice->positions as $p) {
if (!empty($p->position_group)) {
$hasGroups = true;
}
}
// If no positions have groups, put all in default (no group header will be shown)
if (!$hasGroups) {
$groupedPositions['_default'] = $invoice->positions;
} else {
foreach($invoice->positions as $p) {
$group = $p->position_group ?? 'Sonstige';
if (!isset($groupedPositions[$group])) {
$groupedPositions[$group] = [];
}
$groupedPositions[$group][] = $p;
}
}
$this->setReturnValue(['filename' => $invoice->invoice_number . ".pdf"]);
?>
<!DOCTYPE html>
<html>
<head>
<title>Rechnung</title>
<meta charset="utf-8" />
<style>
body {
margin-top: 0;
/*padding-top: 20pt;*/
font-family: "Open Sans", sans-serif, Verdana;
font-size: 12px;
}
tr {
page-break-inside: avoid;
}
.uneven {
background-color: #ebebeb;
}
table tr td:last-child {
text-align: right;
}
.additionalRow td:first-child {
text-align: left;
padding-left: 20pt;
}
th {
height: 28px;
}
#invoiceTable tr *:nth-child(2),
#invoiceTable tr *:nth-child(4),
#invoiceTable tr *:nth-child(5),
#invoiceTable tr *:nth-child(6),
#invoiceTable tr *:nth-child(7) {
text-align: right;
}
#invoiceTable tr *:nth-child(3) {
text-align: center;
}
#invoiceTable tr *:not(:first-child) {
padding: 4px 0;
}
#invoiceTable tr td {
font-size: 11px;
}
tr.position td {
vertical-align: top;
}
tr.position td:first-child {
vertical-align: middle !important;
padding-left: 4pt;
}
#invoiceTable tr td:first-child {
max-width: 280pt;
}
</style>
</head>
<body>
<div>
<h2 style="text-align: center;color: #005384">Ihre Xinon <?=($is_credit) ? "Gutschrift" : "Rechnung"?> vom <?=date("d.m.Y",$invoice->invoice_date)?></h2>
<?php if($invoice->einleitender_text ?? ''): ?>
<p style="margin-top: 10pt; margin-bottom: 20pt; text-align: center; font-weight: bold;"><?=nl2br(htmlspecialchars($invoice->einleitender_text))?></p>
<?php endif; ?>
<table style="border-collapse: collapse; width: 100%;" id="invoiceTable">
<tr style="font-weight: bold; border-bottom: 1px solid black;" class="uneven">
<th style="text-align: center">Leistung / Produkt</th>
<th style="text-align: right">Preis</th>
<th style="text-align: center">Menge</th>
<?php if($hasDiscount): ?><th style="text-align: right">Rabatt %</th><?php endif; ?>
<th style="text-align: right">Netto €</th>
<th style="text-align: right">Ust. %</th>
<th style="text-align: right; padding-right: 4pt">Brutto €</th>
</tr>
<?php
$i = 0;
foreach($groupedPositions as $groupName => $positions):
?>
<!-- Group Header (only show if not default) -->
<?php if ($groupName !== '_default'): ?>
<tr style="background-color: #d9d9d9; font-weight: bold;">
<td colspan="<?=$hasDiscount ? '7' : '6'?>" style="padding: 6px 4pt; border-top: 1px solid black; text-align: left;">
<?=htmlspecialchars($groupName)?>
</td>
</tr>
<?php endif; ?>
<?php
foreach($positions as $p):
$amount = (float) number_format($p->amount ?? 0, 3, ",", ".");
$unit = htmlspecialchars($p->unit ?? 'Stk.');
$price = number_format($p->price ?? 0, 2, ",",".");
$discount = $p->discount ?? 0;
$price_total = number_format($p->price_total ?? 0, 2, ",",".");
$price_gross = number_format($p->price_gross ?? 0, 2, ",",".");
$vatrate = number_format($p->vatrate ?? 0, 0, ",",".");
?>
<tr class="position <?=($i%2 == 0) ? "even" : "uneven" ?>">
<td style="padding-left: 4pt; vertical-align: top;">
<?=htmlspecialchars($p->product_name ?? '')?>
<?php if(isset($p->product_info) && $p->product_info): ?>
<div style="padding-left: 12pt; font-size: 10px; color: #666;"><?=htmlspecialchars($p->product_info)?></div>
<?php endif; ?>
<?php if(isset($p->matchcode) && $p->matchcode): ?>
<div style="padding-left: 12pt; font-size: 10px; color: #666;"><?=htmlspecialchars($p->matchcode)?></div>
<?php endif; ?>
</td>
<td style="text-align: right; padding: 4px 0;"><?=$price?> €</td>
<td style="text-align: center; padding: 4px 0;"><?=$amount?> <?=$unit?></td>
<?php if($hasDiscount): ?><td style="text-align: right; padding: 4px 0;"><?=number_format($discount, 2, ",", ".")?>%</td><?php endif; ?>
<td style="text-align: right; padding: 4px 0;"><?=$price_total?> €</td>
<td style="text-align: right; padding: 4px 0;"><?=$vatrate?>%</td>
<td style="text-align: right; padding: 4px 0; padding-right: 4pt;"><?=$price_gross?> €</td>
</tr>
<?php
$i++;
endforeach;
endforeach;
?>
<?php if($gesamtrabatt > 0): ?>
<tr style="background-color: #ebebeb; border-top: 2px solid black;">
<td colspan="<?=$hasDiscount ? '5' : '4'?>" style="padding: 4px 0;">Zwischensumme:</td>
<td colspan="2" style="text-align: right; padding-right: 4pt;"><?=number_format($subtotal, 2, ",","."). " €"?></td>
</tr>
<tr style="background-color: #ebebeb; border-bottom: 1px solid #ccc;">
<td colspan="<?=$hasDiscount ? '5' : '4'?>" style="padding: 4px 0;">Gesamtrabatt <?=number_format($gesamtrabatt, 2, ",", ".")?>%:</td>
<td colspan="2" style="text-align: right; padding-right: 4pt; color: #d32f2f;">-<?=number_format($subtotal * ($gesamtrabatt / 100), 2, ",","."). " €"?></td>
</tr>
<?php endif; ?>
<tr style="font-weight: bold; background-color: #ebebeb; border-bottom: 1px solid black;<?=($gesamtrabatt > 0) ? '' : 'border-top: 2px solid black;'?>">
<td colspan="<?=$hasDiscount ? '5' : '4'?>" style="padding: 4px 0;">Gesamt Netto:</td>
<td colspan="2" style="text-align: right; padding-right: 4pt;"><?=number_format($net_total, 2, ",","."). " €"?></td>
</tr>
<?php foreach ($vat as $rate => $vat_total): ?>
<?php if($rate > 0): ?>
<tr style="font-size: 11px;border-bottom: 1px solid black;">
<td colspan="<?=$hasDiscount ? '5' : '4'?>" style="padding: 4px 0;">USt. <?=number_format($rate, 0, ",", ".")?>%:</td>
<td colspan="2" style="text-align: right; padding-right: 4pt;"><?=number_format($vat_total, 2, ",","."). " €"?></td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
<!-- double underline border on bottom -->
<tr style="font-weight: bold; border-bottom: 3px double black; background-color: #ebebeb;">
<td colspan="<?=$hasDiscount ? '5' : '4'?>" style="padding: 4px 0;">Gesamt Brutto:</td>
<td colspan="2" style="text-align: right; padding-right: 4pt;"><?=number_format($gross_total, 2, ",","."). " €"?></td>
</tr>
</table>
<div style="margin-top: 20pt;">
<?php if($invoice->tax_text): ?>
<p style="font-weight: bold;"><?=$invoice->tax_text?></p>
<?php endif; ?>
<?php if($is_credit): ?>
<p style="color: #FF0000; font-weight: bold; text-align: center;">Gutschrift! Bitte nicht überweisen.</p>
<?php elseif($invoice->billing_type == "sepa"): ?>
<p style="color: #FF0000; font-weight: bold; text-align: center;">BITTE NICHT EINZAHLEN DER BETRAG WIRD AUTOMATISCH VON IHREM KONTO ABGEBUCHT!</p>
<?php else: ?>
<div style="border-top: 1px solid #ccc; padding-top: 10pt; margin-top: 10pt;">
<p style="margin-bottom: 8pt;">
<strong>Zahlungsinformationen:</strong>
</p>
<p style="margin-bottom: 4pt;">
Bitte überweisen Sie den Rechnungsbetrag bis zum <strong><?=(new DateTime("@".$invoice->invoice_date))->modify("+14 days")->format("d.m.Y")?></strong> auf folgendes Konto:
</p>
<table style="margin-left: 20pt; margin-bottom: 12pt;">
<tr><td style="width: 100pt;"><strong>IBAN:</strong></td><td><?=$bank_iban?></td></tr>
<tr><td><strong>BIC:</strong></td><td><?=$bank_bic?></td></tr>
<tr><td><strong>Bank:</strong></td><td><?=$bank_bank?></td></tr>
</table>
<div style="background-color: #f5f5f5; padding: 10pt; border-left: 3px solid #005384; margin-top: 12pt;">
<p style="margin: 0; margin-bottom: 4pt; font-size: 14px;">
<strong>Verwendungszweck: <?=$invoice->invoice_number ?? "VORSCHAU"?></strong>
</p>
<p style="margin: 0; font-size: 10px; color: #666;">
Wichtig: Bitte geben Sie den oben angeführten Verwendungszweck bei der Überweisung an, damit wir Ihre Zahlung eindeutig zuordnen können.
</p>
</div>
</div>
<?php endif; ?>
</div>
</body>
</html>

View File

@@ -1,4 +1,6 @@
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/header.php"); ?>
<?php if (!isset($network)) $network = null; ?>
<?php $prefillAdbNetzgebietId = $_GET['adb_netzgebiet_id'] ?? null; ?>
<!-- start page title -->
<div class="row">
@@ -8,7 +10,7 @@
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="<?=self::getUrl("Dashboard")?>"><?=MFAPPNAME_SLUG?></a></li>
<li class="breadcrumb-item"><a href="<?=self::getUrl("Network")?>">Netzgebiete</a></li>
<li class="breadcrumb-item active"><?=($network->id) ? "bearbeiten" : "Neu" ?></li>
<li class="breadcrumb-item active"><?=($network && $network->id) ? "bearbeiten" : "Neu" ?></li>
</ol>
</div>
<h4 class="page-title">Netzgebiete</h4>
@@ -22,54 +24,54 @@
<div class="card">
<div class="card-body bg-">
<h4 class="header-title mb-2"><?=($network->id) ? "Netzbereich bearbeiten" : "Neuer Netzbereich"?></h4>
<h4 class="header-title mb-2"><?=($network && $network->id) ? "Netzbereich bearbeiten" : "Neuer Netzbereich"?></h4>
<form class="form-horizontal" method="post" action="<?=self::getUrl("Network", "save")?>">
<div class="card">
<div class="card-body">
<input type="hidden" name="id" value="<?=$network->id?>" />
<input type="hidden" name="id" value="<?=$network ? $network->id : ""?>" />
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="name">Name</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="name" id="name" value="<?=$network->name?>">
<input type="text" class="form-control" name="name" id="name" value="<?=$network ? $network->name : ""?>">
</div>
</div>
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="owner_id">Besitzer</label>
<div class="col-lg-10">
<select class="select2 form-control " name="owner_id" id="owner_id">
<option></option>
<?php foreach($owners as $owner): ?>
<option value="<?=$owner->id?>" <?=($network->owner_id == $owner->id) ? "selected='selected'" : ""?>><?=($owner->getCompanyOrName())?></option>
<option value="<?=$owner->id?>" <?=($network && $network->owner_id == $owner->id) ? "selected='selected'" : ""?>><?=($owner->getCompanyOrName())?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="sytemowner_action_status">Workorder Filter (Admins)</label>
<div class="col-lg-10">
<select class="form-control" name="sytemowner_action_status" id="sytemowner_action_status">
<option></option>
<option value="pipework_needed" <?=($network->sytemowner_action_status == "pipework_needed") ? "selected='selected'" : ""?>>Tiefbau ausständig</option>
<option value="building_connected" <?=($network->sytemowner_action_status == "building_connected") ? "selected='selected'" : ""?>>Tiefbau erledigt</option>
<option value="term_connected" <?=($network->sytemowner_action_status == "term_connected") ? "selected='selected'" : ""?>>Anschluss passiv erschlossen</option>
<option value="pipework_needed" <?=($network && $network->sytemowner_action_status == "pipework_needed") ? "selected='selected'" : ""?>>Tiefbau ausständig</option>
<option value="building_connected" <?=($network && $network->sytemowner_action_status == "building_connected") ? "selected='selected'" : ""?>>Tiefbau erledigt</option>
<option value="term_connected" <?=($network && $network->sytemowner_action_status == "term_connected") ? "selected='selected'" : ""?>>Anschluss passiv erschlossen</option>
</select>
</div>
</div>
<hr />
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="adb_netzgebiet_id">ADB Netzgebiet</label>
<div class="col-lg-10">
<select class="select2 form-control " name="adb_netzgebiet_id" id="adb_netzgebiet_id">
<option></option>
<?php foreach(ADBNetzgebietModel::getAll() as $adbn): ?>
<option value="<?=$adbn->id?>" <?=($network->adb_netzgebiet_id == $adbn->id) ? "selected='selected'" : ""?>><?=$adbn->name?></option>
<option value="<?=$adbn->id?>" <?=(($network && $network->adb_netzgebiet_id == $adbn->id) || $prefillAdbNetzgebietId == $adbn->id) ? "selected='selected'" : ""?>><?=$adbn->name?></option>
<?php endforeach; ?>
</select>
</div>
@@ -81,22 +83,22 @@
<label class="col-lg-2 col-form-label" for="opsystem"></label>
<div class="col-lg-10">
<label class="form-check-label">
<input type="checkbox" name="opsystem" class="form-check-input" value="snopp" id="opsystem" <?=($network->opsystem == "snopp") ? "checked='checked'" : ""?> />
<input type="checkbox" name="opsystem" class="form-check-input" value="snopp" id="opsystem" <?=($network && $network->opsystem == "snopp") ? "checked='checked'" : ""?> />
Für Betrieb in SNOPP freischalten
</label>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="note">Interne Notiz</label>
<div class="col-lg-10">
<textarea id="note" class="form-control" name="note" rows="5"><?=$network->note?></textarea>
<textarea id="note" class="form-control" name="note" rows="5"><?=$network ? $network->note : ""?></textarea>
</div>
</div>
</div>

View File

@@ -1860,7 +1860,7 @@
}
reader.readAsText(selectedFile);
reader.readAsArrayBuffer(selectedFile);
});

View File

@@ -1,9 +1,13 @@
<?php
$filter = $filter ?? [];
$voice_orders = $voice_orders ?? null;
$special_orders = $special_orders ?? null;
$showSpecial = $showSpecial ?? false;
$showVoice = $showVoice ?? false;
$pagination_baseurl = $this->getUrl($Mod,"Index");
$pagination_baseurl_params = ["filter" => $filter];
$pagination_entity_name = "Bestellungen";
//var_dump($mynetworks);
$sorted_networks = [];
if(is_array($mynetworks) && count($mynetworks)) {
@@ -63,7 +67,7 @@
<?php if(is_array($fnet->sections) && count($fnet->sections)): ?>
<optgroup label="<?=$fnet->name?>">
<?php foreach($fnet->sections as $section): ?>
<option value="<?=$section->id?>" <?=($filter['building_networksection_id'] == $section->id) ? "selected='selected'" : ""?>><?=$section->name?></option>
<option value="<?=$section->id?>" <?=(($filter['building_networksection_id'] ?? null) == $section->id) ? "selected='selected'" : ""?>><?=$section->name?></option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
@@ -75,57 +79,65 @@
<label class="form-label" for="filter_status_id">Anschlussstatus</label>
<select name="filter[termination_status]" id="filter_building_status_id" class="form-control">
<option></option>
<option value="pipework_needed" <?=($filter['termination_status'] == "pipework_needed") ? "selected='selected'" : ""?>>Tiefbau ausständig</option>
<option value="building_connected" <?=($filter['termination_status'] == "building_connected") ? "selected='selected'" : ""?>>Tiefbau erledigt</option>
<option value="term_connected" <?=($filter['termination_status'] == "term_connected") ? "selected='selected'" : ""?>>Anschluss passiv erschlossen</option>
<?php if($me->is("Admin")): ?><option value="systemowner_action_status" <?=($filter['termination_status'] == "systemowner_action_status") ? "selected='selected'" : ""?>>Admin Workorder</option><?php endif; ?>
<option value="pipework_needed" <?=(($filter['termination_status'] ?? null) == "pipework_needed") ? "selected='selected'" : ""?>>Tiefbau ausständig</option>
<option value="building_connected" <?=(($filter['termination_status'] ?? null) == "building_connected") ? "selected='selected'" : ""?>>Tiefbau erledigt</option>
<option value="term_connected" <?=(($filter['termination_status'] ?? null) == "term_connected") ? "selected='selected'" : ""?>>Anschluss passiv erschlossen</option>
<?php if($me->is("Admin")): ?><option value="systemowner_action_status" <?=(($filter['termination_status'] ?? null) == "systemowner_action_status") ? "selected='selected'" : ""?>>Admin Workorder</option><?php endif; ?>
</select>
</div>
<div class="col-1">
<label class="form-label" for="filter_building_code">Objekt ID</label>
<input type="text" class="form-control" name="filter[building_code]" id="filter_building_code" value="<?=$filter['building_code']?>" />
<input type="text" class="form-control" name="filter[building_code]" id="filter_building_code" value="<?=$filter['building_code'] ?? ''?>" />
</div>
<div class="col-2">
<label class="form-label" for="filter_building_street">Straße (Anschluss)</label>
<input type="text" class="form-control" name="filter[building_street]" id="filter_building_street" value="<?=$filter['building_street']?>" />
<input type="text" class="form-control" name="filter[building_street]" id="filter_building_street" value="<?=$filter['building_street'] ?? ''?>" />
</div>
<div class="col-1">
<label class="form-label" for="filter_owner">Kunde</label>
<input type="text" class="form-control" name="filter[owner]" id="filter_owner" value="<?=$filter['owner']?>" />
<input type="text" class="form-control" name="filter[owner]" id="filter_owner" value="<?=$filter['owner'] ?? ''?>" />
</div>
<div class="col-2">
<label class="form-label" for="filter_owner">Straße (Kunde)</label>
<input type="text" class="form-control" name="filter[owner_address]" id="filter_owner_address" value="<?=$filter['owner_address']?>" />
<input type="text" class="form-control" name="filter[owner_address]" id="filter_owner_address" value="<?=$filter['owner_address'] ?? ''?>" />
</div>
<div class="col-1">
<label class="form-label" for="filter_partner_number">Partnernummer</label>
<input type="text" class="form-control" name="filter[partner_number]" id="filter_partner_number" value="<?=$filter['partner_number']?>" />
<input type="text" class="form-control" name="filter[partner_number]" id="filter_partner_number" value="<?=$filter['partner_number'] ?? ''?>" />
</div>
</div>
<div class="row mt-1">
<div class="col-2">
<label class="form-label" for="filter_finish_date">Bestellstatus</label>
<select name="filter[finish_date]" id="filter_finish_date" class="form-control">
<option></option>
<option value="0" <?=( (!is_array($filter) || (!array_key_exists("finish_date", $filter) || $filter["finish_date"] != "1")) ? 'selected="selected"' : "")?>>Offen</option>
<option value="1" <?=($filter["finish_date"] == "1" ? 'selected="selected"' : "")?>>Fertiggestellt</option>
<option value="waiting" <?=($filter["finish_date"] == "waiting") ? 'selected="selected"' : ""?>>Wartend / ausgeblendet</option>
<option value="0" <?=(($filter["finish_date"] ?? "0") != "1" ? 'selected="selected"' : "")?>>Offen</option>
<option value="1" <?=(($filter["finish_date"] ?? null) == "1" ? 'selected="selected"' : "")?>>Fertiggestellt</option>
<option value="waiting" <?=(($filter["finish_date"] ?? null) == "waiting") ? 'selected="selected"' : ""?>>Wartend / ausgeblendet</option>
</select>
</div>
<div class="col-1">
<label class="form-label" for="filter_customer_type">Kundentyp</label>
<select name="filter[customer_type]" id="filter_customer_type" class="form-control">
<option></option>
<option value="residential" <?=($filter['customer_type'] == "residential") ? 'selected="selected"' : ""?>>Residential</option>
<option value="business" <?=($filter['customer_type'] == "business") ? 'selected="selected"' : ""?>>Business</option>
<option value="residential" <?=(($filter['customer_type'] ?? null) == "residential") ? 'selected="selected"' : ""?>>Residential</option>
<option value="business" <?=(($filter['customer_type'] ?? null) == "business") ? 'selected="selected"' : ""?>>Business</option>
</select>
</div>
<div class="col-1">
<label class="form-label" for="filter_product_name">Produktname</label>
<input type="text" class="form-control" name="filter[product_name]" id="filter_product_name" value="<?=$filter['product_name'] ?? ''?>" />
</div>
</div>
<div class="row mt-2">
@@ -245,7 +257,7 @@
$cpe_config_finished = true;
}
}
if($hw && $voip_chan && $patched && $cpe_config_finished) {
if($hw && $voip && $patched && $cpe_config_finished) {
break;
}
}
@@ -697,7 +709,7 @@
$cpe_config_finished = true;
}
}
if($hw && $voip_chan && $patched && $cpe_config_finished) {
if($hw && $voip && $patched && $cpe_config_finished) {
break;
}
}

View File

@@ -47,7 +47,7 @@
<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>
<option value="<?=$fnet->id?>" <?=(($filter['network_id'] ?? null) == $fnet->id) ? "selected='selected'" : ""?>><?=$fnet->name?></option>
<?php endforeach; ?>
</select>
</div>
@@ -60,7 +60,7 @@
<?php if(is_array($fnet->sections) && count($fnet->sections)): ?>
<optgroup label="<?=$fnet->name?>">
<?php foreach($fnet->sections as $section): ?>
<option value="<?=$section->id?>" <?=($filter['networksection_id'] == $section->id) ? "selected='selected'" : ""?>><?=$section->name?></option>
<option value="<?=$section->id?>" <?=(($filter['networksection_id'] ?? null) == $section->id) ? "selected='selected'" : ""?>><?=$section->name?></option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
@@ -102,12 +102,17 @@
<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']?>" />
<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']?>" />
<input type="text" class="form-control" name="filter[street]" id="filter_street" value="<?=$filter['street'] ?? ''?>" />
</div>
<div class="col-1">
<label class="form-label" for="filter_ap_name">AP-Name</label>
<input type="text" class="form-control" name="filter[ap_name]" id="filter_ap_name" value="<?=$filter['ap_name'] ?? ''?>" />
</div>
<div class="col-2">

View File

@@ -856,6 +856,16 @@ $pagination_entity_name = "Vorbestellungen";
<?php endif; ?>
<div class="col-sm-12 col-md-1">
<label class="form-label" for="filter_tool_building_type">Gebäude Typ</label>
<select name="filter[tool_building_type]" id="filter_tool_building_type" class="form-control">
<option value="">Alle</option>
<option value="0" <?=(isset($filter) && array_key_exists("tool_building_type", $filter) && $filter['tool_building_type'] === "0") ? "selected='selected'" : ""?>>Unbekannt</option>
<option value="1" <?=(isset($filter) && array_key_exists("tool_building_type", $filter) && $filter['tool_building_type'] == "1") ? "selected='selected'" : ""?>>EFH</option>
<option value="2" <?=(isset($filter) && array_key_exists("tool_building_type", $filter) && $filter['tool_building_type'] == "2") ? "selected='selected'" : ""?>>MPH</option>
</select>
</div>
</div>
<div class="row mt-2">
@@ -1073,8 +1083,19 @@ $pagination_entity_name = "Vorbestellungen";
<td style="text-align: left; letter-spacing: 4px; font-size: 1.1em;">
<div class="preorder-campaign-table-actions">
<?php if(!$me->is(["preorderfront"]) && !$me->is("preorderreadonly")): ?>
<a href="#" data-home-id="<?=$preorder->adb_wohneinheit_id?>" data-home-contact title="Kontakte bearbeiten"><i class="fas fa-users-cog text-primary"></i></a>
<?php
$contacts = ($preorder->adb_wohneinheit_id && $preorder->adb_wohneinheit && $preorder->adb_wohneinheit->contact) ? json_decode($preorder->adb_wohneinheit->contact, true) : [];
$contactCount = is_array($contacts) ? count($contacts) : 0;
?>
<a href="#" data-home-id="<?=$preorder->adb_wohneinheit_id?>" data-home-contact title="Kontakte bearbeiten<?=$contactCount ? ' (' . $contactCount . ')' : ''?>" style="position: relative; display: inline-block;">
<i class="fas fa-users-cog <?=$contactCount ? 'text-success' : 'text-muted'?>"></i>
<?php if($contactCount): ?>
<span style="position: absolute; top: -8px; right: -8px; background: #28a745; color: white; border-radius: 50%; width: 16px; height: 16px; font-size: 10px; display: flex; align-items: center; justify-content: center; font-weight: bold; padding: 0; box-sizing: border-box;"><?=$contactCount?></span>
<?php endif; ?>
</a>
<a href="<?=self::getUrl("Preorder", "edit", ["id" => $preorder->id])?>"><i class="far fa-edit" title="Vorbestellung Bearbeiten"></i></a>
<?php endif; ?>
<?php if($me->isAdmin()): ?>
<a href="<?=self::getUrl("Preorder", "delete", ["id" => $preorder->id, "filter" => $filter])?>" class="text-danger" onclick="if(!confirm('Vorbestellung wirklich löschen?')) return false;" title="Vorbestellung Löschen"><i class="fas fa-trash"></i></a>
<?php endif; ?>
</div>
@@ -1337,18 +1358,30 @@ $pagination_entity_name = "Vorbestellungen";
/*
* Globals for map display
*/
var borderpoly = [];
<?php if(isset($campaign) && $campaign && $campaign->adb_netzgebiet): ?>
borderpoly = <?=($campaign->adb_netzgebiet->borderpoly) ? $campaign->adb_netzgebiet->borderpoly : "[]"?>;
<?php elseif($me->is("Admin")): ?>
borderpoly = [];
<?php foreach(ADBNetzgebietModel::search(["borderpoly" => true]) as $bp_netz): ?>
borderpoly.push(<?=$bp_netz->borderpoly?>);
<?php endforeach; ?>
var borderpolies = [];
<?php if($me->is("Admin")): ?>
<?php foreach(ADBNetzgebietModel::search(["borderpoly" => true]) as $bp_netz): ?>
borderpolies.push([<?=$bp_netz->borderpoly?>]);
<?php endforeach; ?>
<?php elseif(isset($campaign) && $campaign):
$adb_networks = [];
if(is_array($campaign->salesclusters) && count($campaign->salesclusters)) {
$adb_networks = $campaign->salesclusters;
} else {
$adb_networks = [$campaign->adb_netzgebiet];
}
if(count($adb_networks)): ?>
<?php foreach($adb_networks as $network): ?>
borderpoly = <?=($network->borderpoly) ?: "[]"?>;
borderpolies.push(borderpoly);
<?php endforeach; ?>
<?php endif; ?>
<?php endif; ?>
var preorderMap;
var preorders = [];
var fttxlocations = [];
var markers = [];
var markerState = true;
var mapCenterPos = [<?=TT_PLACEHOLDER_GPS_LAT?>, <?=TT_PLACEHOLDER_GPS_LONG?>];
@@ -1419,17 +1452,20 @@ $pagination_entity_name = "Vorbestellungen";
function addMarkers() {
if(borderpoly) {
var border = L.polygon(borderpoly, {
fillColor: 'blue',
weight: 8,
opacity: 0.5,
color: 'violet', //Outline color
fillOpacity: 0.05
}).addTo(preorderMap);
if(borderpolies) {
borderpolies.forEach(function(borderpoly) {
var border = L.polygon(borderpoly, {
fillColor: 'blue',
weight: 8,
opacity: 0.5,
color: 'violet', //Outline color
fillOpacity: 0.05
}).addTo(preorderMap);
});
}
if(!Array.isArray(preorders) | !preorders.length) {
if(!Array.isArray(preorders) || !preorders.length) {
return false;
}
// draw markers and calculate center position
@@ -1458,10 +1494,12 @@ $pagination_entity_name = "Vorbestellungen";
icon_name = "industry";
}
var marker_popup_content = `<?php include(realpath(dirname(__FILE__))."/include/preorder_popup.php");?>`;
// popup fields
const preorder_view_url = `<?=self::getUrl("Preorder")?>/Index?filter[ucode]=${preorder.ucode}#preorder=${preorder.id}`;
var marker_popup_content = `<?php include(realpath(dirname(__FILE__))."/include/preorder_popup.php"); ?>`;
[
["PREORDER_URL", preorder_view_url],
["street", preorder.adb_strasse],
@@ -1482,8 +1520,10 @@ $pagination_entity_name = "Vorbestellungen";
marker_popup_content = marker_popup_content.replaceAll("{{" + item[0].toUpperCase() + "}}", item[1]);
});
var tooltip_content = preorder.adb_strasse + " " + preorder.adb_hausnummer + "<br />" + preorder.adb_plz + " " + preorder.adb_ort + "<br /><br />Execution State: " + preorder.adb_ex_state
console.log(tooltip_content);
var icon = L.MakiMarkers.icon({icon: icon_name, color: icon_color, size: "l"});
var marker = L.marker(gps, {icon: icon}).addTo(preorderMap).bindPopup(marker_popup_content);
var marker = L.marker(gps, {icon: icon}).bindPopup(marker_popup_content).bindTooltip(tooltip_content).addTo(preorderMap);
markers[preorder.id] = marker;
<?php if($me->is("Admin")): ?>
@@ -1515,6 +1555,7 @@ $pagination_entity_name = "Vorbestellungen";
//fetch fcps and show on map
getFCPs(preorderMap);
addFttxLocations(preorderMap);
// calculate center position
mapCenterPos = GetCenterFromDegrees(all_coords);
@@ -1593,6 +1634,29 @@ $pagination_entity_name = "Vorbestellungen";
});
}
function addFttxLocations(preorderMap) {
fttx_c = {
"gross planning": "grey",
"detailed planning": "yellow",
"plan released": "tomato",
"assigned": "aqua",
"executed": "darkblue",
"documented": "lime",
"canceled": "darkred",
};
fttxlocations.forEach(loc => {
if(!loc.gps_lat || !loc.gps_long || !loc.ex_state) return;
var circle = L.circleMarker([loc.gps_lat, loc.gps_long], {
color: fttx_c[loc.ex_state.toLowerCase()],
fillColor: fttx_c[loc.ex_state.toLowerCase()],
fillOpacity: .8,
radius: 6
}).bindTooltip(loc.street + "<br />" + loc.zip + " " + loc.city + "<br /><br />Execution State: " + loc.ex_state).addTo(preorderMap);
})
}
function centerMap() {
preorderMap.setView(mapCenterPos, 12);
}
@@ -1603,19 +1667,25 @@ $pagination_entity_name = "Vorbestellungen";
$.post('<?=self::getUrl("Preorder", "Api")?>', {
'do': "getFilteredPreorders",
filter: filter
filter: filter,
},function(success) {
if(success.status == "OK") {
changes = false;
if(Array.isArray(success.result.preorders)) {
preorders = success.result.preorders;
changes = true;
}
if(Array.isArray(success.result.fttxlocations))
fttxlocations = success.result.fttxlocations;
changes = true;
}
if(changes) {
renderMap();
}
}
},
},
'json'
);
}
function getFilter() {

View File

@@ -75,8 +75,8 @@ while($data = mysqli_fetch_object($res)):
if($data->attributes) {
$attribs = json_decode($data->attributes, true);
if($attribs['bep_specified']) $bep = true;
if($attribs['inhouse_cabling_supplied']) $inhouse = true;
if(isset($attribs['bep_specified']) && $attribs['bep_specified']) $bep = true;
if(isset($attribs['inhouse_cabling_supplied']) && $attribs['inhouse_cabling_supplied']) $inhouse = true;
}
$addon_property = 0;

View File

@@ -1,4 +1,6 @@
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
<?php if (!isset($campaign)) $campaign = null; ?>
<?php $prefillNetworkId = $_GET['network_id'] ?? null; ?>
<!-- start page title -->
<div class="row">
<div class="col-12">
@@ -28,7 +30,7 @@
<form class="form-horizontal" method="post"
action="<?= self::getUrl("Preordercampaign", "save") ?>">
<input type="hidden" name="id" value="<?= $campaign->id ?>"/>
<input type="hidden" name="id" value="<?= $campaign ? $campaign->id : "" ?>"/>
<div class="card">
<div class="card-body">
@@ -39,7 +41,7 @@
<select class="select2 form-control " name="network_id" id="network_id">
<option></option>
<?php foreach ($networks as $network): ?>
<option value="<?= $network->id ?>" <?= ($campaign->network_id == $network->id) ? "selected='selected'" : "" ?>><?= ($network->name) ?></option>
<option value="<?= $network->id ?>" <?= (($campaign && $campaign->network_id == $network->id) || $prefillNetworkId == $network->id) ? "selected='selected'" : "" ?>><?= ($network->name) ?></option>
<?php endforeach; ?>
</select>
</div>
@@ -49,7 +51,7 @@
<label class="col-lg-2 col-form-label" for="name">Name *</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="name" id="name"
value="<?= $campaign->name ?>"/>
value="<?= $campaign ? $campaign->name : "" ?>"/>
</div>
</div>
@@ -57,7 +59,7 @@
<label class="col-lg-2 col-form-label" for="description">Info</label>
<div class="col-lg-10">
<textarea class="form-control" style="height:120px;"
name="description"><?= $campaign->description ?></textarea>
name="description"><?= $campaign ? $campaign->description : "" ?></textarea>
</div>
</div>
@@ -65,7 +67,7 @@
<label class="col-lg-2 col-form-label" for="area">Gebiet *</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="area" id="area"
value="<?= $campaign->area ?>"/>
value="<?= $campaign ? $campaign->area : "" ?>"/>
</div>
</div>
@@ -73,7 +75,7 @@
<label class="col-lg-2 col-form-label" for="homes_total">Homes gesamt *</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="homes_total" id="homes_total"
value="<?= $campaign->homes_total ?>"/>
value="<?= $campaign ? $campaign->homes_total : "" ?>"/>
</div>
</div>
@@ -81,7 +83,7 @@
<label class="col-lg-2 col-form-label" for="from">Von</label>
<div class="col-lg-10">
<input type="text" class="form-control datepicker" name="from" id="from"
value="<?= ($campaign->from) ? date('d.m.Y', $campaign->from) : "" ?>"/>
value="<?= ($campaign && $campaign->from) ? date('d.m.Y', $campaign->from) : "" ?>"/>
</div>
</div>
@@ -89,7 +91,7 @@
<label class="col-lg-2 col-form-label" for="to">Bis</label>
<div class="col-lg-10">
<input type="text" class="form-control datepicker" name="to" id="to"
value="<?= ($campaign->to) ? date('d.m.Y', $campaign->to) : "" ?>"/>
value="<?= ($campaign && $campaign->to) ? date('d.m.Y', $campaign->to) : "" ?>"/>
</div>
</div>
@@ -100,30 +102,31 @@
<div class="col-lg-10">
<select class="form-control" name="product_type" id="product_type"
data-placeholder="Bitte auswählen ...">
<option value="all" <?= ($campaign->product_type == "all") ? "selected='selected'" : "" ?>>
<option value="all" <?= ($campaign && $campaign->product_type == "all") ? "selected='selected'" : "" ?>>
Alle Produkte im Netzgebiet
</option>
<option value="no_setup" <?= ($campaign->product_type == "no_setup") ? "selected='selected'" : "" ?>>
<option value="no_setup" <?= ($campaign && $campaign->product_type == "no_setup") ? "selected='selected'" : "" ?>>
Alle Produkte im Netzgebiet, ohne Herstellungsprodukt
</option>
<option value="setup_only" <?= ($campaign->product_type == "setup_only") ? "selected='selected'" : "" ?>>
<option value="setup_only" <?= ($campaign && $campaign->product_type == "setup_only") ? "selected='selected'" : "" ?>>
Nur Anschlussbestellung, keine Produkte
</option>
</select>
</div>
</div>
<?php $campaignTypes = ($campaign && is_array($campaign->types)) ? $campaign->types : []; ?>
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="types">Erlaubte Vorbestellungstypen
*</label>
<div class="col-lg-10">
<select class="select2 form-control select2-multiple" name="types[]" id="types"
multiple="multiple" data-placeholder="Bitte auswählen ...">
<option value="interest" <?= (is_array($campaign->types) && array_key_exists("interest", $campaign->types)) ? "selected='selected'" : "" ?>><?= __("interest", "preorder") ?></option>
<option value="provision" <?= (is_array($campaign->types) && array_key_exists("provision", $campaign->types)) ? "selected='selected'" : "" ?>><?= __("provision", "preorder") ?></option>
<option value="order" <?= (is_array($campaign->types) && array_key_exists("order", $campaign->types)) ? "selected='selected'" : "" ?>><?= __("order", "preorder") ?></option>
<option value="reorder" <?= (is_array($campaign->types) && array_key_exists("reorder", $campaign->types)) ? "selected='selected'" : "" ?>><?= __("reorder", "preorder") ?></option>
<option value="legacytransfer" <?= (is_array($campaign->types) && array_key_exists("legacytransfer", $campaign->types)) ? "selected='selected'" : "" ?>><?= __("legacytransfer", "preorder") ?></option>
<option value="interest" <?= array_key_exists("interest", $campaignTypes) ? "selected='selected'" : "" ?>><?= __("interest", "preorder") ?></option>
<option value="provision" <?= array_key_exists("provision", $campaignTypes) ? "selected='selected'" : "" ?>><?= __("provision", "preorder") ?></option>
<option value="order" <?= array_key_exists("order", $campaignTypes) ? "selected='selected'" : "" ?>><?= __("order", "preorder") ?></option>
<option value="reorder" <?= array_key_exists("reorder", $campaignTypes) ? "selected='selected'" : "" ?>><?= __("reorder", "preorder") ?></option>
<option value="legacytransfer" <?= array_key_exists("legacytransfer", $campaignTypes) ? "selected='selected'" : "" ?>><?= __("legacytransfer", "preorder") ?></option>
</select>
</div>
</div>
@@ -134,16 +137,16 @@
<div class="col-lg-10">
<select class="form-control" name="fulfillment" id="fulfillment"
data-placeholder="Bitte auswählen ...">
<option value="thetool" <?= ($campaign->fulfillment == "thetool") ? "selected='selected'" : "" ?>>
<option value="thetool" <?= ($campaign && $campaign->fulfillment == "thetool") ? "selected='selected'" : "" ?>>
thetool
</option>
<option value="rimo" <?= ($campaign->fulfillment == "rimo") ? "selected='selected'" : "" ?>>
<option value="rimo" <?= ($campaign && $campaign->fulfillment == "rimo") ? "selected='selected'" : "" ?>>
RIMO
</option>
<option value="citycom_oan" <?= ($campaign->fulfillment == "citycom_oan") ? "selected='selected'" : "" ?>>
<option value="citycom_oan" <?= ($campaign && $campaign->fulfillment == "citycom_oan") ? "selected='selected'" : "" ?>>
Citycom OAN
</option>
<option value="thirdparty" <?= ($campaign->fulfillment == "thirdparty") ? "selected='selected'" : "" ?>>
<option value="thirdparty" <?= ($campaign && $campaign->fulfillment == "thirdparty") ? "selected='selected'" : "" ?>>
Drittsystem
</option>
</select>
@@ -155,13 +158,13 @@
<div class="col-lg-10">
<select class="form-control" name="oaid_origin" id="oaid_origin"
data-placeholder="Bitte auswählen ...">
<option value="thetool" <?= ($campaign->oaid_origin == "thetool") ? "selected='selected'" : "" ?>>
<option value="thetool" <?= ($campaign && $campaign->oaid_origin == "thetool") ? "selected='selected'" : "" ?>>
thetool
</option>
<option value="ofaa" <?= ($campaign->oaid_origin == "ofaa") ? "selected='selected'" : "" ?>>
<option value="ofaa" <?= ($campaign && $campaign->oaid_origin == "ofaa") ? "selected='selected'" : "" ?>>
OFAA
</option>
<option value="other" <?= ($campaign->oaid_origin == "other") ? "selected='selected'" : "" ?>>
<option value="other" <?= ($campaign && $campaign->oaid_origin == "other") ? "selected='selected'" : "" ?>>
Andere (importieren, aber nicht verarbeiten)
</option>
</select>
@@ -171,6 +174,10 @@
</div>
</div>
<?php $campaignSalesclusters = ($campaign && is_array($campaign->salesclusters)) ? $campaign->salesclusters : []; ?>
<?php $campaignAllFcpNames = ($campaign && is_array($campaign->all_fcp_names)) ? $campaign->all_fcp_names : []; ?>
<?php $campaignBannedFcps = ($campaign && is_array($campaign->banned_fcps)) ? $campaign->banned_fcps : []; ?>
<?php $campaignRequiredFields = ($campaign && is_array($campaign->required_fields)) ? $campaign->required_fields : []; ?>
<div class="card">
<div class="card-body">
@@ -182,7 +189,7 @@
name="adb_netzgebiet_ids[]" id="adb_netzgebiet_ids" multiple="multiple"
data-placeholder="Salescluster ...">
<?php foreach (ADBNetzgebietModel::getAll() as $salescluster): ?>
<option value="<?= $salescluster->id ?>" <?= (is_array($campaign->salesclusters) && array_key_exists($salescluster->id, $campaign->salesclusters)) ? "selected='selected'" : "" ?>><?= $salescluster->name ?></option>
<option value="<?= $salescluster->id ?>" <?= array_key_exists($salescluster->id, $campaignSalesclusters) ? "selected='selected'" : "" ?>><?= $salescluster->name ?></option>
<?php endforeach; ?>
</select>
</div>
@@ -195,8 +202,8 @@
<select class="select2 form-control select2-multiple bg-danger"
name="banned_rimo_fcp[]" id="banned_rimo_fcp" multiple="multiple"
data-placeholder="FCPs ...">
<?php foreach ($campaign->all_fcp_names as $fcp_name): ?>
<option value="<?= $fcp_name ?>" <?= (is_array($campaign->banned_fcps) && in_array($fcp_name, $campaign->banned_fcps)) ? "selected='selected'" : "" ?>><?= $fcp_name ?></option>
<?php foreach ($campaignAllFcpNames as $fcp_name): ?>
<option value="<?= $fcp_name ?>" <?= in_array($fcp_name, $campaignBannedFcps) ? "selected='selected'" : "" ?>><?= $fcp_name ?></option>
<?php endforeach; ?>
</select>
</div>
@@ -208,7 +215,7 @@
<div class="col-lg-10">
<select class="select2 form-control select2-multiple" name="required_fields[]"
id="required_fields" multiple="multiple" data-placeholder="Felder ...">
<option value="contact_type" <?= (is_array($campaign->required_fields) && in_array("contact_type", $campaign->required_fields)) ? "selected='selected'" : "" ?>>
<option value="contact_type" <?= in_array("contact_type", $campaignRequiredFields) ? "selected='selected'" : "" ?>>
Kontakttyp (Besitzer/Bewohner)
</option>
</select>
@@ -221,10 +228,10 @@
Ort:</label>
<div class="col-lg-10">
<select class="form-control" name="district_is_city" id="district_is_city">
<option value="0" <?= (!$campaign->district_is_city) ? "selected='selected'" : "" ?>>
<option value="0" <?= (!$campaign || !$campaign->district_is_city) ? "selected='selected'" : "" ?>>
Nein
</option>
<option value="1" <?= ($campaign->district_is_city) ? "selected='selected'" : "" ?>>
<option value="1" <?= ($campaign && $campaign->district_is_city) ? "selected='selected'" : "" ?>>
Ja
</option>
</select>
@@ -238,10 +245,10 @@
<div class="col-lg-10">
<select class="form-control" name="hausnummer_add_zusatz"
id="hausnummer_add_zusatz">
<option value="0" <?= (!$campaign->hausnummer_add_zusatz) ? "selected='selected'" : "" ?>>
<option value="0" <?= (!$campaign || !$campaign->hausnummer_add_zusatz) ? "selected='selected'" : "" ?>>
Nein
</option>
<option value="1" <?= ($campaign->hausnummer_add_zusatz) ? "selected='selected'" : "" ?>>
<option value="1" <?= ($campaign && $campaign->hausnummer_add_zusatz) ? "selected='selected'" : "" ?>>
Ja
</option>
</select>
@@ -253,10 +260,10 @@
pro Wohneinheit (API):</label>
<div class="col-lg-10">
<select class="form-control" name="exist_is_error" id="exist_is_error">
<option value="0" <?= (!$campaign->exist_is_error) ? "selected='selected'" : "" ?>>
<option value="0" <?= (!$campaign || !$campaign->exist_is_error) ? "selected='selected'" : "" ?>>
Mehr als eine
</option>
<option value="1" <?= ($campaign->exist_is_error) ? "selected='selected'" : "" ?>>
<option value="1" <?= ($campaign && $campaign->exist_is_error) ? "selected='selected'" : "" ?>>
Maximal eine
</option>
</select>
@@ -270,7 +277,7 @@
<label class="col-lg-2 col-form-label" for="cifurl">CIF Url</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="cifurl" id="cifurl"
value="<?= $campaign->cifurl ?>"/>
value="<?= $campaign ? $campaign->cifurl : "" ?>"/>
<small>
Customer Installation Feedback (für QR-Code bei Status 145).<br/>
Templatevariable <code>{{CIFTOKEN}}</code> wird mit echtem Cif Token ersetzt<br/>
@@ -284,7 +291,7 @@
for="cifcableurl">Kabelnachbestell-Url</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="cifcableurl" id="cifcableurl"
value="<?= $campaign->cifcableurl ?>"/>
value="<?= $campaign ? $campaign->cifcableurl : "" ?>"/>
<small>Für Begleitschreiben - Status 145</small>
</div>
</div>
@@ -335,13 +342,15 @@
</div>
</div>
<?php $campaignActiveOperators = ($campaign && is_array($campaign->active_operators)) ? $campaign->active_operators : []; ?>
<?php $campaignPassiveOperators = ($campaign && is_array($campaign->passive_operators)) ? $campaign->passive_operators : []; ?>
<div class="card bg-light">
<div class="card-body">
<h4>Netzbetreiber</h4>
<div class="card">
<div class="card-body">
<h4>Aktivnetzbetreiber</h4>
<?php foreach ($campaign->active_operators as $aop): ?>
<?php foreach ($campaignActiveOperators as $aop): ?>
<div class="form-group row">
<label class="col-lg-2 col-form-label"
for="active_operators_<?= $aop->id ?>"></label>
@@ -415,7 +424,7 @@
id="passive_operators" multiple="multiple"
data-placeholder="Netzbetreiber wählen ...">
<?php foreach (AddressModel::search(['addresstype' => ["netowner", "salespartner"]]) as $operator): ?>
<option value="<?= $operator->id ?>" <?= (is_array($campaign->passive_operators) && array_key_exists($operator->id, $campaign->passive_operators)) ? "selected='selected'" : "" ?>><?= $operator->getCompanyOrName() ?></option>
<option value="<?= $operator->id ?>" <?= array_key_exists($operator->id, $campaignPassiveOperators) ? "selected='selected'" : "" ?>><?= $operator->getCompanyOrName() ?></option>
<?php endforeach; ?>
</select>
</div>
@@ -433,7 +442,7 @@
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="">Netzinhaber FIBU Kostenstelle</label>
<div class="col-lg-10">
<input type="text" class="form-control" name="netowner_fibu_cost_code" value="<?=$campaign->netowner_fibu_cost_code?>" />
<input type="text" class="form-control" name="netowner_fibu_cost_code" value="<?=$campaign ? $campaign->netowner_fibu_cost_code : ""?>" />
</div>
@@ -611,8 +620,9 @@
<select class="select2 form-control select2-multiple"
name="apiusers[]" id="apiusers" multiple="multiple"
data-placeholder="Benutzer auswählen ...">
<?php $campaignApiUsers = ($campaign && is_array($campaign->apiusers)) ? $campaign->apiusers : []; ?>
<?php foreach (UserModel::search(['apikey' => true]) as $user): ?>
<option value="<?= $user->id ?>" <?= (is_array($campaign->apiusers) && array_key_exists($user->id, $campaign->apiusers)) ? "selected='selected'" : "" ?>><?= $user->username ?>
<option value="<?= $user->id ?>" <?= array_key_exists($user->id, $campaignApiUsers) ? "selected='selected'" : "" ?>><?= $user->username ?>
(<?= $user->name ?>)
</option>
<?php endforeach; ?>
@@ -626,7 +636,7 @@
Hostnamen</label>
<div class="col-lg-10">
<textarea class="form-control"
name="corsorigins"><?= ($campaign->corsorigins) ? implode("\n", $campaign->corsorigins) : "" ?></textarea>
name="corsorigins"><?= ($campaign && $campaign->corsorigins) ? implode("\n", $campaign->corsorigins) : "" ?></textarea>
<small>Hostname der Website, mit oder ohne Protokoll
(<em>https://</em>); *. als Wildcard erlaubt
(<em>*.domain.com</em>); ein Eintrag pro Zeile</small>
@@ -642,7 +652,7 @@
<label class="col-lg-2 col-form-label" for="note">Interne Notiz</label>
<div class="col-lg-10">
<textarea class="form-control" style="height:120px;" name="note"
id="note"><?= $campaign->note ?></textarea>
id="note"><?= $campaign ? $campaign->note : "" ?></textarea>
</div>
</div>
</div>
@@ -754,8 +764,8 @@
<script>
$(document).ready(function() {
// Initialize with existing data
let iframeOrigins = <?= $campaign->iframe_origins ?? '[]'; ?>;
let iframeConsents = <?= $campaign->iframe_consents ?? '{}'; ?>;
let iframeOrigins = <?= ($campaign && $campaign->iframe_origins) ? $campaign->iframe_origins : '[]'; ?>;
let iframeConsents = <?= ($campaign && $campaign->iframe_consents) ? $campaign->iframe_consents : '{}'; ?>;
console.log(iframeConsents);

View File

@@ -0,0 +1,133 @@
<?php
if (!isset($vueViewName)) die("vueViewName is not set");
if (!isset($mfLayoutPackage)) die("mfLayoutPackage is not set");
$additionalCSS = $additionalCSS ?? [];
$additionalJS = $additionalJS ?? [];
$vueViewPath = BASEDIR . "/public/js/pages/$vueViewName";
// Load page-specific CSS and JS files
if (is_dir($vueViewPath)) {
foreach (scandir($vueViewPath) as $file) {
if ($file === '.' || $file === '..') continue;
$fileExtension = pathinfo($file, PATHINFO_EXTENSION);
if ($fileExtension === 'css') $additionalCSS[] = "js/pages/$vueViewName/$file";
else if ($fileExtension === 'js') $additionalJS[] = "js/pages/$vueViewName/$file";
}
}
// Add TT-Core CSS
$additionalCSS = [
"plugins/vue/tt-core/styles/tt-core.css",
...$additionalCSS,
];
/**
* Convert PascalCase to snake_case (e.g. PascalCase to pascal-case)
* @param string $str PascalCase string
* @return string snake-case string
*/
function pascalToSnakeCase(string $str): string {
return strtolower(preg_replace('/(?<!^)([A-Z])/', '-$1', $str));
}
$vueTagName = pascalToSnakeCase($vueViewName);
$vueHeaderPath = realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/vueHeader3.php";
if (!file_exists($vueHeaderPath))
$vueHeaderPath = realpath(dirname(__FILE__) . "/../../default") . "/vueHeader3.php";
include($vueHeaderPath); ?>
<div id="app">
<<?php echo $vueTagName; ?>>
</<?php echo $vueTagName; ?>>
</div>
<!-- TT-Core Library -->
<script src="<?php echo mfBaseController::getUrl(""); ?>plugins/vue/tt-core/index.js" type="module"></script>
<!-- Vue 3 Initialization -->
<script>
// TT-Core components to load
const ttCoreComponents = [
'plugins/vue/tt-core/components/data-display/TtDataTable.js',
'plugins/vue/tt-core/components/data-display/TtStatusChip.js',
'plugins/vue/tt-core/components/display/TtInfoCard.js',
'plugins/vue/tt-core/components/feedback/TtLoadingIndicator.js',
'plugins/vue/tt-core/components/feedback/TtSkeleton.js',
'plugins/vue/tt-core/components/forms/TtSmartAutocomplete.js',
'plugins/vue/tt-core/components/forms/TtFileDropzone.js',
'plugins/vue/tt-core/components/forms/TtCopyButton.js',
'plugins/vue/tt-core/components/overlays/TtDialog.js',
'plugins/vue/tt-core/components/navigation/TtViewSwitcher.js'
];
// All additional scripts
const allScripts = <?php echo json_encode($additionalJS); ?>;
// Separate Chart.js libraries (need to load first)
const chartLibs = allScripts.filter(s => s.includes('chart.js/chart.'));
const chartAdapters = allScripts.filter(s => s.includes('chartjs-adapter'));
const pageScripts = allScripts.filter(s => !s.includes('chart.js/') && !s.includes('chartjs-adapter'));
// Wait for TT_CORE to be loaded (since index.js is a module and loads async)
function initVueApp() {
if (typeof window.TT_CORE === 'undefined') {
// TT_CORE not loaded yet, wait a bit and try again
setTimeout(initVueApp, 50);
return;
}
const { createApp } = Vue;
const app = createApp({
data() {
return {
window: window
};
}
});
// CRITICAL: Register TT-Core components and set window.VueApp
window.TT_CORE.registerComponents(app);
// Load scripts in order:
// 1. TT-Core components
// 2. Chart.js library (if needed)
// 3. Chart.js adapters (after Chart.js)
// 4. Page-specific components
loadScripts(ttCoreComponents)
.then(() => chartLibs.length ? loadScripts(chartLibs) : Promise.resolve())
.then(() => chartAdapters.length ? loadScripts(chartAdapters) : Promise.resolve())
.then(() => loadScripts(pageScripts))
.then(() => {
// Mount the app after all components are loaded and registered
app.mount('#app');
})
.catch(err => {
console.error('Failed to load components:', err);
});
}
// Dynamically load scripts
function loadScripts(scriptPaths) {
const baseUrl = '<?php echo mfBaseController::getUrl(""); ?>';
const promises = scriptPaths.map(src => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = baseUrl + src;
script.onload = resolve;
script.onerror = () => reject(new Error(`Failed to load ${src}`));
document.body.appendChild(script);
});
});
return Promise.all(promises);
}
// Start initialization
initVueApp();
</script>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>

View File

@@ -0,0 +1,935 @@
<?php
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Inventur Scanner</title>
<link rel="shortcut icon" href="/assets/images/favicon.ico">
<meta name="theme-color" content="#005384">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.27/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.2/dist/axios.min.js"></script>
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<script>
window.TT_CONFIG = <?= json_encode($JSGlobals ?? []) ?>;
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
'primary': '#005384',
'secondary': '#fac41b',
},
}
}
}
</script>
<style>
html, body {
overscroll-behavior: none;
touch-action: manipulation;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease-in-out; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
.slide-up-enter-active, .slide-up-leave-active { transition: transform 0.3s ease-out, opacity 0.3s ease-out; }
.slide-up-enter-from, .slide-up-leave-to { transform: translateY(100%); opacity: 0; }
#qr-reader {
width: 100%;
border: none !important;
}
#qr-reader video {
border-radius: 12px;
}
#qr-reader__scan_region {
background: transparent !important;
}
#qr-reader__dashboard {
display: none !important;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
.success-flash {
animation: successFlash 0.5s ease-out;
}
@keyframes successFlash {
0% { background-color: rgb(34, 197, 94); }
100% { background-color: transparent; }
}
/* Custom numpad styles */
.numpad-btn {
min-height: 52px;
font-size: 1.25rem;
font-weight: 600;
border-radius: 12px;
transition: all 0.15s ease;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.numpad-btn:active {
transform: scale(0.95);
}
/* Warning banner animation - intense without causing overflow */
@keyframes pulse-warning {
0%, 100% {
opacity: 1;
box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.7), inset 0 0 0 0 rgba(251, 191, 36, 0.2);
border-color: rgb(251, 191, 36);
background-color: rgb(254, 243, 199);
}
50% {
opacity: 0.95;
box-shadow: 0 0 20px 5px rgba(251, 191, 36, 0.6), inset 0 0 20px 0 rgba(251, 191, 36, 0.2);
border-color: rgb(245, 158, 11);
background-color: rgb(253, 230, 138);
}
}
.warning-pulse {
animation: pulse-warning 0.8s ease-in-out infinite;
}
.dark .warning-pulse {
animation: pulse-warning-dark 0.8s ease-in-out infinite;
}
@keyframes pulse-warning-dark {
0%, 100% {
box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.5), inset 0 0 0 0 rgba(251, 191, 36, 0.1);
border-color: rgb(217, 119, 6);
}
50% {
box-shadow: 0 0 25px 8px rgba(251, 191, 36, 0.4), inset 0 0 15px 0 rgba(251, 191, 36, 0.15);
border-color: rgb(245, 158, 11);
}
}
</style>
</head>
<body class="bg-slate-100 dark:bg-slate-900 transition-colors duration-300">
<div id="app" class="min-h-screen"></div>
<script>
const { createApp, ref, reactive, computed, onMounted, onUnmounted, watch, nextTick } = Vue;
const app = createApp({
setup() {
// === STATE ===
const currentScreen = ref('stocktake-select'); // stocktake-select, scanner, manual-entry
const stocktakes = ref([]);
const selectedStocktake = ref(null);
const isLoading = ref(true);
const scannerActive = ref(false);
const cameraAvailable = ref(true);
const lastScan = ref(null);
const recentScans = ref([]);
const progress = reactive({ totalScanned: 0, myScanned: 0 });
const theme = ref(localStorage.getItem('theme') || 'system');
// Categories
const categories = ref([]);
const selectedCategory = ref(null);
const showCategoryBrowser = ref(false);
// Already scanned warning
const alreadyScannedWarning = reactive({
show: false,
existingItem: null,
});
// Form state
const manualForm = reactive({
show: false,
article: null,
quantity: '',
rack: '',
shelf: '',
note: '',
searchQuery: '',
searchResults: [],
searching: false,
showNumpad: false,
});
// Scanner instance
let html5QrCode = null;
const API_BASE = window.TT_CONFIG.BASE_PATH || '/WarehouseStocktakePWA';
const api = axios.create({ baseURL: API_BASE });
// === COMPUTED ===
const isDark = computed(() => {
if (theme.value === 'dark') return true;
if (theme.value === 'light') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
});
// Check if mobile device for numpad display
const isMobile = computed(() => {
const ua = navigator.userAgent.toLowerCase();
return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i.test(ua);
});
// === METHODS ===
const applyTheme = () => {
document.documentElement.classList.toggle('dark', isDark.value);
};
const setTheme = (newTheme) => {
theme.value = newTheme;
localStorage.setItem('theme', newTheme);
applyTheme();
};
const fetchStocktakes = async () => {
isLoading.value = true;
try {
const res = await api.get('/getActiveStocktakes');
if (res.data.success) {
stocktakes.value = res.data.stocktakes;
}
} catch (e) {
console.error('Failed to fetch stocktakes:', e);
} finally {
isLoading.value = false;
}
};
const fetchCategories = async () => {
try {
const res = await api.get('/getCategories');
if (res.data.success) {
categories.value = res.data.categories;
}
} catch (e) {
console.error('Failed to fetch categories:', e);
}
};
const selectStocktake = async (stocktake) => {
selectedStocktake.value = stocktake;
currentScreen.value = 'scanner';
await fetchMyScans();
await fetchProgress();
await fetchCategories();
await nextTick();
startScanner();
};
const backToList = () => {
stopScanner();
selectedStocktake.value = null;
currentScreen.value = 'stocktake-select';
fetchStocktakes();
};
const startScanner = async () => {
if (html5QrCode) {
await stopScanner();
}
try {
html5QrCode = new Html5Qrcode("qr-reader");
await html5QrCode.start(
{ facingMode: "environment" },
{
fps: 10,
qrbox: { width: 250, height: 250 },
aspectRatio: 1.0,
},
onScanSuccess,
onScanFailure
);
scannerActive.value = true;
cameraAvailable.value = true;
} catch (err) {
console.error('Scanner start error:', err);
cameraAvailable.value = false;
// Don't show alert - user can use manual search
}
};
const stopScanner = async () => {
if (html5QrCode && scannerActive.value) {
try {
await html5QrCode.stop();
} catch (e) {
console.error('Scanner stop error:', e);
}
}
scannerActive.value = false;
};
const onScanSuccess = async (decodedText) => {
// Prevent rapid duplicate scans
if (lastScan.value && lastScan.value.code === decodedText && Date.now() - lastScan.value.time < 2000) {
return;
}
lastScan.value = { code: decodedText, time: Date.now() };
// Vibrate feedback
if (navigator.vibrate) {
navigator.vibrate(100);
}
// Lookup article
try {
const res = await api.get('/getArticle', { params: { code: decodedText } });
if (res.data.success) {
await handleArticleSelected(res.data.article);
} else {
showToast(res.data.message || 'Artikel nicht gefunden', 'error');
}
} catch (e) {
showToast('Fehler beim Laden des Artikels', 'error');
}
};
const onScanFailure = (error) => {
// Ignore - continuous scanning
};
const handleArticleSelected = async (article) => {
await stopScanner();
// Reset form fields
manualForm.article = article;
manualForm.quantity = '';
manualForm.rack = '';
manualForm.shelf = '';
manualForm.note = '';
manualForm.showNumpad = true;
// Check if already scanned
try {
const checkRes = await api.get('/checkAlreadyScanned', {
params: {
stocktakeId: selectedStocktake.value.id,
articleId: article.id
}
});
if (checkRes.data.success && checkRes.data.alreadyScanned) {
alreadyScannedWarning.show = true;
alreadyScannedWarning.existingItem = checkRes.data.existingItem;
} else {
alreadyScannedWarning.show = false;
alreadyScannedWarning.existingItem = null;
}
} catch (e) {
console.error('Check already scanned error:', e);
}
manualForm.show = true;
};
const submitScan = async (overwrite = false) => {
const qty = parseFloat(manualForm.quantity) || 0;
if (!manualForm.article || qty <= 0) {
showToast('Bitte Menge angeben', 'error');
return;
}
try {
const payload = {
stocktakeId: selectedStocktake.value.id,
articleId: manualForm.article.id,
quantity: qty,
rack: manualForm.rack || null,
shelf: manualForm.shelf || null,
note: manualForm.note || null,
};
if (overwrite && alreadyScannedWarning.existingItem) {
payload.overwrite = true;
payload.overwriteItemId = alreadyScannedWarning.existingItem.id;
}
const res = await api.post('/submitScan', payload);
if (res.data.success) {
showToast(res.data.message, 'success');
// Add to recent scans
recentScans.value.unshift({
...res.data.item,
scannedAt: new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }),
flash: true,
});
if (recentScans.value.length > 20) {
recentScans.value.pop();
}
// Update progress
if (!overwrite) {
progress.totalScanned++;
progress.myScanned++;
}
closeForm();
} else {
showToast(res.data.message || 'Fehler beim Speichern', 'error');
}
} catch (e) {
showToast('Netzwerkfehler', 'error');
}
};
const closeForm = async () => {
manualForm.show = false;
manualForm.article = null;
manualForm.quantity = '';
manualForm.searchQuery = '';
manualForm.searchResults = [];
manualForm.showNumpad = false;
alreadyScannedWarning.show = false;
alreadyScannedWarning.existingItem = null;
selectedCategory.value = null;
showCategoryBrowser.value = false;
await nextTick();
if (cameraAvailable.value) {
startScanner();
}
};
const openManualEntry = async () => {
await stopScanner();
// Reset form fields
manualForm.show = true;
manualForm.article = null;
manualForm.quantity = '';
manualForm.rack = '';
manualForm.shelf = '';
manualForm.note = '';
manualForm.searchQuery = '';
manualForm.searchResults = [];
manualForm.showNumpad = false;
alreadyScannedWarning.show = false;
selectedCategory.value = null;
showCategoryBrowser.value = false;
};
const openCategoryBrowser = () => {
showCategoryBrowser.value = true;
selectedCategory.value = null;
manualForm.searchResults = [];
};
const selectCategory = async (category) => {
selectedCategory.value = category;
showCategoryBrowser.value = false;
manualForm.searching = true;
try {
const res = await api.get('/searchArticles', {
params: { categoryId: category.id, query: manualForm.searchQuery || '' }
});
if (res.data.success) {
manualForm.searchResults = res.data.articles;
}
} catch (e) {
console.error('Category search error:', e);
} finally {
manualForm.searching = false;
}
};
const clearCategoryFilter = () => {
selectedCategory.value = null;
manualForm.searchResults = [];
if (manualForm.searchQuery.length >= 2) {
searchArticles();
}
};
const searchArticles = async () => {
if (manualForm.searchQuery.length < 2 && !selectedCategory.value) {
manualForm.searchResults = [];
return;
}
manualForm.searching = true;
try {
const params = { query: manualForm.searchQuery };
if (selectedCategory.value) {
params.categoryId = selectedCategory.value.id;
}
const res = await api.get('/searchArticles', { params });
if (res.data.success) {
manualForm.searchResults = res.data.articles;
}
} catch (e) {
console.error('Search error:', e);
} finally {
manualForm.searching = false;
}
};
const selectSearchResult = async (article) => {
await handleArticleSelected(article);
};
const fetchMyScans = async () => {
if (!selectedStocktake.value) return;
try {
const res = await api.get('/getMyScans', { params: { stocktakeId: selectedStocktake.value.id } });
if (res.data.success) {
recentScans.value = res.data.items;
}
} catch (e) {
console.error('Failed to fetch scans:', e);
}
};
const fetchProgress = async () => {
if (!selectedStocktake.value) return;
try {
const res = await api.get('/getProgress', { params: { stocktakeId: selectedStocktake.value.id } });
if (res.data.success) {
progress.totalScanned = res.data.progress.totalScanned;
progress.myScanned = res.data.progress.myScanned;
}
} catch (e) {
console.error('Failed to fetch progress:', e);
}
};
// Numpad functions
const numpadInput = (val) => {
if (val === 'clear') {
manualForm.quantity = '';
} else if (val === 'backspace') {
manualForm.quantity = String(manualForm.quantity).slice(0, -1);
} else if (val === '+') {
const current = parseFloat(manualForm.quantity) || 0;
manualForm.quantity = String(current + 1);
} else if (val === '-') {
const current = parseFloat(manualForm.quantity) || 0;
if (current > 1) {
manualForm.quantity = String(current - 1);
}
} else if (val === '.') {
if (!String(manualForm.quantity).includes('.')) {
manualForm.quantity = (manualForm.quantity || '0') + '.';
}
} else {
manualForm.quantity = (manualForm.quantity || '') + val;
}
};
// Toast notification
const toast = reactive({ show: false, message: '', type: 'success' });
let toastTimeout = null;
const showToast = (message, type = 'success') => {
toast.message = message;
toast.type = type;
toast.show = true;
if (toastTimeout) clearTimeout(toastTimeout);
toastTimeout = setTimeout(() => { toast.show = false; }, 3000);
};
const logout = () => {
window.location.href = API_BASE + '/logout';
};
// === LIFECYCLE ===
onMounted(() => {
applyTheme();
fetchStocktakes();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme);
});
onUnmounted(() => {
stopScanner();
});
// Debounced search
let searchTimeout = null;
watch(() => manualForm.searchQuery, (val) => {
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(searchArticles, 300);
});
return {
currentScreen, stocktakes, selectedStocktake, isLoading, scannerActive, cameraAvailable,
recentScans, progress, theme, manualForm, toast, categories, selectedCategory,
showCategoryBrowser, alreadyScannedWarning, isMobile,
selectStocktake, backToList, submitScan, closeForm,
openManualEntry, selectSearchResult, setTheme, logout,
openCategoryBrowser, selectCategory, clearCategoryFilter, numpadInput,
};
},
template: `
<div class="min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-primary text-white px-4 py-3 flex items-center justify-between sticky top-0 z-30 shadow-lg">
<div class="flex items-center">
<button v-if="currentScreen === 'scanner'" @click="backToList" class="mr-3 p-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div>
<h1 class="font-bold text-lg">Inventur Scanner</h1>
<p v-if="selectedStocktake" class="text-xs text-white/80">{{ selectedStocktake.title }}</p>
</div>
</div>
<div class="flex items-center space-x-2">
<button @click="setTheme(theme === 'dark' ? 'light' : 'dark')" class="p-2 rounded-full hover:bg-white/10">
<svg v-if="theme === 'dark'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button>
<button @click="logout" class="p-2 rounded-full hover:bg-white/10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</button>
</div>
</header>
<!-- Main Content -->
<main class="flex-1 overflow-auto">
<!-- Stocktake Selection Screen -->
<div v-if="currentScreen === 'stocktake-select'" class="p-4">
<div v-if="isLoading" class="space-y-4">
<div v-for="i in 3" :key="i" class="bg-white dark:bg-slate-800 rounded-xl p-4 shadow animate-pulse">
<div class="h-5 bg-slate-200 dark:bg-slate-700 rounded w-3/4 mb-2"></div>
<div class="h-4 bg-slate-200 dark:bg-slate-700 rounded w-1/2"></div>
</div>
</div>
<div v-else-if="stocktakes.length === 0" class="text-center py-20">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-slate-300 dark:text-slate-600 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p class="text-slate-500 dark:text-slate-400">Keine aktiven Inventuren</p>
<button @click="fetchStocktakes" class="mt-4 px-4 py-2 bg-primary text-white rounded-lg">
Aktualisieren
</button>
</div>
<div v-else class="space-y-4">
<p class="text-sm text-slate-500 dark:text-slate-400 mb-2">Aktive Inventuren auswählen:</p>
<div v-for="st in stocktakes" :key="st.id"
@click="selectStocktake(st)"
class="bg-white dark:bg-slate-800 rounded-xl p-4 shadow cursor-pointer active:scale-[0.98] transition">
<div class="flex justify-between items-start">
<div>
<h3 class="font-bold text-slate-800 dark:text-white">{{ st.title }}</h3>
<p class="text-sm text-slate-500 dark:text-slate-400">{{ st.locationName }}</p>
<p class="text-xs text-slate-400 dark:text-slate-500 mt-1">{{ st.stocktakeNumber }}</p>
</div>
<div class="text-right">
<span class="inline-block bg-secondary text-primary text-xs font-bold px-2 py-1 rounded-full">
{{ st.totalScannedItems }} Artikel
</span>
<p v-if="st.startedAt" class="text-xs text-slate-400 mt-1">{{ st.startedAt }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- Scanner Screen -->
<div v-if="currentScreen === 'scanner'" class="flex flex-col h-full">
<!-- Progress Bar -->
<div class="bg-white dark:bg-slate-800 px-4 py-2 flex justify-between items-center text-sm border-b dark:border-slate-700">
<span class="text-slate-600 dark:text-slate-300">
<strong class="text-primary dark:text-secondary">{{ progress.totalScanned }}</strong> gesamt
</span>
<span class="text-slate-600 dark:text-slate-300">
<strong class="text-green-600 dark:text-green-400">{{ progress.myScanned }}</strong> von mir
</span>
</div>
<!-- Scanner View -->
<div v-if="!manualForm.show" class="p-4">
<div v-if="cameraAvailable" class="relative bg-black rounded-xl overflow-hidden mb-4">
<div id="qr-reader" class="w-full"></div>
<div v-if="!scannerActive" class="absolute inset-0 flex items-center justify-center bg-slate-900/80">
<div class="text-center text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-2 animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<p>Kamera wird gestartet...</p>
</div>
</div>
</div>
<div v-else class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4 mb-4">
<div class="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-amber-500 mr-2 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<p class="text-amber-800 dark:text-amber-200 font-medium">Kamera nicht verfügbar</p>
<p class="text-amber-700 dark:text-amber-300 text-sm">Verwenden Sie die manuelle Suche unten.</p>
</div>
</div>
</div>
<button @click="openManualEntry"
class="w-full py-3 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-200 rounded-xl font-medium">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
Manuelle Suche
</button>
</div>
<!-- Entry Form -->
<transition name="slide-up">
<div v-if="manualForm.show" class="flex-1 bg-white dark:bg-slate-800 p-4 overflow-auto">
<!-- Search (if no article selected) -->
<div v-if="!manualForm.article" class="space-y-4">
<!-- Category Filter -->
<div v-if="selectedCategory" class="flex items-center bg-primary/10 dark:bg-primary/20 rounded-lg p-2 mb-2">
<span class="text-sm text-primary dark:text-secondary font-medium flex-1">
Kategorie: {{ selectedCategory.name }}
</span>
<button @click="clearCategoryFilter" class="p-1 text-primary dark:text-secondary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Search Input -->
<div class="relative">
<input v-model="manualForm.searchQuery" type="text" inputmode="search"
placeholder="Artikelnummer oder Name..."
class="w-full px-4 py-3 rounded-xl border dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:ring-2 focus:ring-primary">
<div v-if="manualForm.searching" class="absolute right-3 top-3">
<svg class="animate-spin h-5 w-5 text-slate-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</div>
<!-- Category Browser Button -->
<button @click="openCategoryBrowser"
class="w-full py-3 bg-primary/10 dark:bg-primary/20 text-primary dark:text-secondary rounded-xl font-medium flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
Nach Kategorie durchsuchen
</button>
<!-- Category Browser Modal -->
<div v-if="showCategoryBrowser" class="fixed inset-0 bg-black/50 z-50 flex items-end">
<div class="bg-white dark:bg-slate-800 w-full rounded-t-2xl max-h-[70vh] flex flex-col">
<div class="p-4 border-b dark:border-slate-700 flex justify-between items-center">
<h3 class="font-bold text-lg dark:text-white">Kategorie wählen</h3>
<button @click="showCategoryBrowser = false" class="p-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="overflow-y-auto flex-1 p-4">
<div class="grid grid-cols-2 gap-2">
<div v-for="cat in categories" :key="cat.id"
@click="selectCategory(cat)"
class="p-3 bg-slate-50 dark:bg-slate-700 rounded-lg cursor-pointer active:bg-slate-100 dark:active:bg-slate-600 text-center">
<p class="font-medium text-slate-800 dark:text-white text-sm">{{ cat.name }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- Search Results -->
<div v-if="manualForm.searchResults.length" class="space-y-2 max-h-64 overflow-auto">
<div v-for="article in manualForm.searchResults" :key="article.id"
@click="selectSearchResult(article)"
class="p-3 bg-slate-50 dark:bg-slate-700 rounded-lg cursor-pointer active:bg-slate-100 dark:active:bg-slate-600">
<p class="font-medium text-slate-800 dark:text-white">{{ article.title }}</p>
<p class="text-sm text-slate-500 dark:text-slate-400">{{ article.articleNumber }}</p>
</div>
</div>
<button @click="closeForm" class="w-full py-3 bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200 rounded-xl">
Abbrechen
</button>
</div>
<!-- Article Form (if article selected) -->
<div v-else class="space-y-4">
<!-- Already Scanned Warning -->
<div v-if="alreadyScannedWarning.show" class="bg-amber-100 dark:bg-amber-900/30 border-2 border-amber-400 dark:border-amber-600 rounded-xl p-4 warning-pulse">
<div class="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-amber-600 dark:text-amber-400 mr-3 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<p class="font-bold text-amber-800 dark:text-amber-200">Bereits gescannt!</p>
<p class="text-sm text-amber-700 dark:text-amber-300 mt-1">
Dieser Artikel wurde bereits erfasst:
<br><strong>{{ alreadyScannedWarning.existingItem.countedQuantity }} Stk.</strong>
von {{ alreadyScannedWarning.existingItem.scannedBy }}
({{ alreadyScannedWarning.existingItem.scannedAt }})
</p>
<p class="text-xs text-amber-600 dark:text-amber-400 mt-2">
Geben Sie die neue Menge ein und klicken Sie auf "Überschreiben".
</p>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700 rounded-xl p-4">
<p class="font-bold text-lg text-slate-800 dark:text-white">{{ manualForm.article.title }}</p>
<p class="text-sm text-slate-500 dark:text-slate-400">{{ manualForm.article.articleNumber }}</p>
<p v-if="manualForm.article.categoryName" class="text-xs text-slate-400 mt-1">{{ manualForm.article.categoryName }}</p>
</div>
<!-- Quantity with Custom Numpad -->
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Menge ({{ manualForm.article.unit }}) *
</label>
<div class="text-center bg-slate-100 dark:bg-slate-700 rounded-xl p-4 mb-3">
<span class="text-4xl font-bold text-primary dark:text-secondary">
{{ manualForm.quantity || '0' }}
</span>
<span class="text-xl text-slate-500 ml-2">{{ manualForm.article.unit }}</span>
</div>
<!-- Desktop: regular input field -->
<div v-if="!isMobile" class="mt-2">
<input v-model="manualForm.quantity" type="number" inputmode="decimal" min="0.01" step="0.01"
placeholder="Menge eingeben..."
class="w-full px-4 py-3 text-xl font-bold text-center rounded-xl border dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:ring-2 focus:ring-primary">
</div>
<!-- Numpad (only on mobile devices) -->
<div v-if="manualForm.showNumpad && isMobile" class="grid grid-cols-4 gap-2">
<button @click="numpadInput('1')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">1</button>
<button @click="numpadInput('2')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">2</button>
<button @click="numpadInput('3')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">3</button>
<button @click="numpadInput('+')" class="numpad-btn bg-green-500 text-white">+</button>
<button @click="numpadInput('4')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">4</button>
<button @click="numpadInput('5')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">5</button>
<button @click="numpadInput('6')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">6</button>
<button @click="numpadInput('-')" class="numpad-btn bg-red-500 text-white">-</button>
<button @click="numpadInput('7')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">7</button>
<button @click="numpadInput('8')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">8</button>
<button @click="numpadInput('9')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">9</button>
<button @click="numpadInput('backspace')" class="numpad-btn bg-slate-300 dark:bg-slate-500 text-slate-800 dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2M3 12l6.414 6.414a2 2 0 001.414.586H19a2 2 0 002-2V7a2 2 0 00-2-2h-8.172a2 2 0 00-1.414.586L3 12z" />
</svg>
</button>
<button @click="numpadInput('.')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">.</button>
<button @click="numpadInput('0')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">0</button>
<button @click="numpadInput('clear')" class="numpad-btn bg-slate-300 dark:bg-slate-500 text-slate-800 dark:text-white col-span-2">C</button>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Regal</label>
<input v-model="manualForm.rack" type="text"
class="w-full px-4 py-2 rounded-xl border dark:border-slate-600 dark:bg-slate-700 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Fach</label>
<input v-model="manualForm.shelf" type="text"
class="w-full px-4 py-2 rounded-xl border dark:border-slate-600 dark:bg-slate-700 dark:text-white">
</div>
</div>
<!-- Action Buttons -->
<div class="space-y-2 pt-4">
<div class="flex space-x-3">
<button @click="closeForm" class="flex-1 py-3 bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200 rounded-xl font-medium">
Abbrechen
</button>
<!-- Show "Speichern" only when NOT already scanned -->
<button v-if="!alreadyScannedWarning.show"
@click="submitScan(false)"
class="flex-1 py-3 bg-green-600 text-white rounded-xl font-bold">
Speichern
</button>
<!-- Show "Überschreiben" only when already scanned -->
<button v-else
@click="submitScan(true)"
class="flex-1 py-3 bg-amber-500 text-white rounded-xl font-bold">
Überschreiben
</button>
</div>
</div>
</div>
</div>
</transition>
<!-- Recent Scans List -->
<div v-if="!manualForm.show && recentScans.length" class="flex-1 bg-white dark:bg-slate-800 overflow-auto">
<div class="px-4 py-2 bg-slate-50 dark:bg-slate-700/50 sticky top-0">
<p class="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Letzte Scans</p>
</div>
<div class="divide-y dark:divide-slate-700">
<div v-for="(item, index) in recentScans" :key="item.id"
:class="{ 'success-flash': item.flash }"
class="px-4 py-3 flex justify-between items-center">
<div class="min-w-0 flex-1">
<p class="font-medium text-slate-800 dark:text-white truncate">{{ item.articleTitle }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">
{{ item.articleNumber }}
<span v-if="item.rack || item.shelf" class="ml-2">
| {{ item.rack || '-' }} / {{ item.shelf || '-' }}
</span>
</p>
</div>
<div class="text-right ml-4">
<p class="font-bold text-primary dark:text-secondary">{{ item.countedQuantity }} {{ item.unit }}</p>
<p class="text-xs text-slate-400">{{ item.scannedAt }}</p>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Toast Notification -->
<transition name="fade">
<div v-if="toast.show"
:class="toast.type === 'success' ? 'bg-green-600' : 'bg-red-600'"
class="fixed bottom-20 left-4 right-4 p-4 rounded-xl text-white text-center font-medium shadow-lg z-50">
{{ toast.message }}
</div>
</transition>
</div>
`
});
app.mount('#app');
</script>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<?php
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
// QR code options - small padding, high quality
$options = new QROptions([
'outputType' => QRCode::OUTPUT_IMAGE_PNG,
'scale' => 10,
'quietzoneSize' => 1,
]);
// Generate QR code data - encode article ID for Inventur scanning
$qrData = "WA:" . $articleId . ":" . $articleNumber;
$qrCodeBase64 = (new QRCode($options))->render($qrData);
?>
<html>
<head>
<meta charset="utf-8">
<style>
* { margin: 0; padding: 0; }
html, body { height: 25mm; width: 50mm; }
body { font-family: Arial, sans-serif; color: #000; }
table { border-collapse: collapse; }
</style>
</head>
<body>
<table cellpadding="0" cellspacing="0" border="0" style="width: 50mm; height: 25mm;">
<tr>
<td style="width: 22mm; height: 25mm; vertical-align: middle; text-align: center;">
<img src="<?php echo $qrCodeBase64; ?>" style="width: 21mm; height: 21mm;">
</td>
<td style="height: 25mm; vertical-align: middle; padding-left: 1mm; padding-right: 1mm;">
<img src="<?php echo BASEDIR; ?>/public/assets/images/xinon-full-transparent.png" style="width: 24mm; height: auto; display: block; margin-bottom: 1mm;">
<div style="font-size: 10px; font-weight: bold; color: #000; text-align: center;"><?php echo htmlspecialchars($articleNumber); ?></div>
<div style="font-size: 7px; color: #000; margin-top: 1px; word-wrap: break-word; overflow: hidden; text-align: center;"><?php echo htmlspecialchars($articleTitle); ?></div>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -61,7 +61,8 @@ if ($includeTax) {
}
$formattedOfferDate = date("d.m.Y", $offerDate);
$formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate));
$validityDays = isset($validity) ? (int)$validity : 14;
$formattedValidUntil = date("d.m.Y", strtotime("+$validityDays days", $offerDate));
?>
<!DOCTYPE html>
@@ -116,7 +117,7 @@ $formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate));
<table>
<tr>
<td class="label" style="text-align: left"><?= $text['offerNumberLabel'] ?></td>
<td><?= htmlspecialchars($offerNumber) ?></td>
<td><?= htmlspecialchars($offerNumber) ?> - v<?= isset($offer->version) ? $offer->version : 1 ?></td>
<td class="label"><?= $text['offerDateLabel'] ?></td>
<td><?= $formattedOfferDate ?></td>
</tr>
@@ -173,7 +174,7 @@ $formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate));
<td class="amount"><?= number_format($p['amount'], 2, ',', '.') ?></td>
<td class="unit"><?= htmlspecialchars($p['articleUnit']) ?></td>
<td class="price"><?= formatPrice($p['price'], '€') ?></td>
<td class="price"><?= htmlspecialchars($p['discount'] . '%') ?></td>
<td class="discount"><?= htmlspecialchars($p['discount'] . '%') ?></td>
<td class="total"><?= formatPrice($p['totalPrice'], '€') ?></td>
</tr>
<?php endforeach; ?>

View File

@@ -11,7 +11,7 @@ if(preg_match('/^(.+)-1R$/', $color_name, $cmatch)) {
<select class="form-control selectpicker show-tick" name="wfitem_<?=$item->name?>" id="wfitem_<?=$item->name?>_<?=$$wftype->id?>" title="Farbe wählen" data-style="btn-outline-<?=$color_name?>">
<option></option>
<?php foreach(TT_CABLE_COLORS as $name => $color): ?>
<?php if($color['two-color']): ?>
<?php if(!empty($color['two-color'])): ?>
<option
style="color: #<?=$color["hexfg"]?>;
background: rgb(<?=$color["r"]?>,<?=$color["g"]?>,<?=$color["b"]?>);
@@ -20,16 +20,16 @@ if(preg_match('/^(.+)-1R$/', $color_name, $cmatch)) {
"
value="<?=$name?>"
data-bg-color="#<?=$color["hex"]?>" <?=($color['mark']) ? "data-icon='fa-ellipsis-h'" : ""?>
data-bg-color="#<?=$color["hex"]?>" <?=(!empty($color['mark'])) ? "data-icon='fa-ellipsis-h'" : ""?>
<?=($name == $item->value->value_string) ? "selected='selected'" : ""?>
>
<?=ucfirst($name)?>
</option>
<?php else: ?>
<option
style="background-color: rgba(<?=$color["r"]?>,<?=$color["g"]?>,<?=$color["b"]?>, .5); color: #<?=$color["hexfg"]?>"
value="<?=$name?>"
data-bg-color="#<?=$color["hex"]?>" <?=($color['mark']) ? "data-icon='fa-ellipsis-h'" : ""?>
<option
style="background-color: rgba(<?=$color["r"]?>,<?=$color["g"]?>,<?=$color["b"]?>, .5); color: #<?=$color["hexfg"]?>"
value="<?=$name?>"
data-bg-color="#<?=$color["hex"]?>" <?=(!empty($color['mark'])) ? "data-icon='fa-ellipsis-h'" : ""?>
<?=($name == $item->value->value_string) ? "selected='selected'" : ""?>
>
<?=ucfirst($name)?>

View File

@@ -144,6 +144,8 @@
<!-- add a new line Arbeitsaufträge for RMLCompany, add a new line Arbeitsaufträge-Management for RMLAdmin -->
<?php if($me->can("RMLCompany")): ?><li><a href="<?=self::getUrl("WorkorderCompany")?>"><i class="far fa-fw fa-clipboard-question text-info"></i> Arbeitsaufträge</a></li><?php endif; ?>
<?php if($me->can("RMLAdmin")): ?><li><a href="<?=self::getUrl("WorkorderAdmin")?>"><i class="far fa-fw fa-clipboard-question text-info"></i> Arbeitsaufträge-Management</a></li><?php endif; ?>
<?php if($me->can("WorkorderMph")): ?><li><a href="<?=self::getUrl("WorkorderMphCompany")?>"><i class="far fa-fw fa-buildings text-info"></i> MPH Arbeitsaufträge</a></li><?php endif; ?>
<?php if($me->can("WorkorderMphAdmin")): ?><li><a href="<?=self::getUrl("WorkorderMphAdmin")?>"><i class="far fa-fw fa-buildings text-info"></i> MPH Arbeitsaufträge Verwaltung</a></li><?php endif; ?>
</ul>
</li>
<?php endif; ?>
@@ -179,9 +181,10 @@
<!-- --><?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; ?>
<?php if($me->can("WarehouseAdmin") || $me->can("WarehouseUser")): ?><li><a href="<?=self::getUrl("WarehouseProject")?>"><i class="fas fa-fw fa-project-diagram text-info"></i> Projekte</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseStocktake")?>"><i class="far fa-fw fa-clipboard-check text-info"></i> Inventur</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseDistributor")?>"><i class="far fa-fw fa-cogs text-info"></i> Administration</a></li><?php endif; ?>
@@ -209,7 +212,7 @@
</li>
<?php endif; ?>
<?php if($me->is(["Admin","netowner","salespartner"]) || $me->can(["Order", "Preorder"])): ?>
<?php if($me->is(["Admin","netowner","salespartner"]) || $me->can(["Order", "Preorder", "WarehouseAdmin"])): ?>
<li class="has-submenu">
<a href="#">
<i class="fal fa-fw fa-money-bill-wave"></i>Verkauf <div class="arrow-down"></div>
@@ -220,6 +223,8 @@
<li><a href="<?=self::getUrl("Preorder", "RimoTypeMap")?>"><i class="far fa-fw fa-map text-info"></i> RIMO Typen-Karte</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->is(["Admin","salespartner"]) && $me->can("Order")): ?>
<li class="border-top"><a href="<?=self::getUrl("Order")?>"><i class="far fa-fw fa-file-signature text-info"></i> Bestellungen</a></li>
<?php endif; ?>

View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= MFAPPNAME_FULL ?></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="shortcut icon" href="<?= self::getResourcePath() ?>assets/images/favicon.ico">
<link href="<?= self::getResourcePath() ?>cssbundler.php?<?= $git_merge_ts ?>" rel="stylesheet">
<link href="<?= self::getResourcePath() ?>fontawesome/css/fontawesome.min.css?<?= $git_merge_ts ?>"
rel="stylesheet">
<link href="<?= self::getResourcePath() ?>fontawesome/css/solid.min.css?<?= $git_merge_ts ?>" rel="stylesheet">
<link href="<?= self::getResourcePath() ?>fontawesome/css/regular.min.css?<?= $git_merge_ts ?>" rel="stylesheet">
<link href="<?=self::getResourcePath()?>fontawesome/css/duotone.min.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
<link href="<?=self::getResourcePath()?>fontawesome/css/duotone-regular.min.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
<link href="<?= self::getResourcePath() ?>fontawesome/css/sharp-light.min.css?<?= $git_merge_ts ?>"
rel="stylesheet">
<?php if (!empty($additionalCSS)):
foreach ($additionalCSS as $css): ?>
<link rel="stylesheet" href="<?= self::getResourcePath() ?><?= $css ?>?<?= $git_merge_ts ?>">
<?php endforeach;
endif;
if (!empty($additionalHead)):
foreach ($additionalHead as $head):
echo $head;
endforeach;
endif; ?>
<script>
const baseurl = '<?=self::getResourcePath()?>';
window.mfNotify = <?=json_encode($mfNotify ?? null)?>;
window.TT_CONFIG = {};
<?php
if(isset($JSGlobals) && is_array($JSGlobals) && count($JSGlobals)):
foreach($JSGlobals as $key => $value): ?>
window.TT_CONFIG.<?=$key?> = <?=is_array($value) ? json_encode($value) : "'$value'"; ?>;
<?php endforeach; endif;?>
</script>
<script type="text/javascript" src="<?=self::getResourcePath()?>js/jquery.min.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>js/popper.min.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>js/bootstrap.min.js"></script>
<!-- Vue 3 CDN -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Axios for HTTP requests -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- Moment.js for date handling -->
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
<script src="<?= self::getResourcePath() ?>plugins/notification/notify.js" defer></script>
<script src="<?= self::getResourcePath() ?>plugins/bookstack/bookstackIntegration.js" defer></script>
<style>
body {
min-height: 100vh;
}
<?php if (MFAPPNAME === "devthetool"): ?>
body {
border-left: 8px dashed #f672a7;
}
<?php endif; ?>
</style>
</head>
<body>
<header id="topnav">
<?php
include(__DIR__ . "/topbar.php");
include(__DIR__ . "/menu.php");
?>
</header>
<div class="wrapper pl-0 pl-lg-1 pr-0 pr-lg-1">
<div class="container-fluid">