Merge remote-tracking branch 'origin/spidev' into spidev
This commit is contained in:
@@ -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">×</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
|
||||
|
||||
@@ -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; ?>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
52
Layout/default/AssetManagement/LABEL.php
Normal file
52
Layout/default/AssetManagement/LABEL.php
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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; ?>
|
||||
|
||||
@@ -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">
|
||||
|
||||
43
Layout/default/ManualInvoice/PDF_FOOTER.html
Normal file
43
Layout/default/ManualInvoice/PDF_FOOTER.html
Normal 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>
|
||||
107
Layout/default/ManualInvoice/PDF_HEADER.html
Normal file
107
Layout/default/ManualInvoice/PDF_HEADER.html
Normal 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>
|
||||
257
Layout/default/ManualInvoice/PDF_MAIN.php
Normal file
257
Layout/default/ManualInvoice/PDF_MAIN.php
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -1860,7 +1860,7 @@
|
||||
|
||||
|
||||
}
|
||||
reader.readAsText(selectedFile);
|
||||
reader.readAsArrayBuffer(selectedFile);
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
133
Layout/default/VueViews/Vue3.php
Normal file
133
Layout/default/VueViews/Vue3.php
Normal 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"); ?>
|
||||
935
Layout/default/VueViews/WarehouseStocktakePWA.php
Normal file
935
Layout/default/VueViews/WarehouseStocktakePWA.php
Normal 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>
|
||||
40
Layout/default/WarehouseArticle/LABEL.php
Normal file
40
Layout/default/WarehouseArticle/LABEL.php
Normal 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>
|
||||
@@ -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; ?>
|
||||
|
||||
@@ -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)?>
|
||||
|
||||
@@ -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; ?>
|
||||
|
||||
83
Layout/default/vueHeader3.php
Normal file
83
Layout/default/vueHeader3.php
Normal 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">
|
||||
Reference in New Issue
Block a user