Merge branch 'master' into fronkdev

This commit is contained in:
Frank Schubert
2024-11-27 13:10:18 +01:00
25 changed files with 772 additions and 199 deletions
+19 -9
View File
@@ -136,15 +136,25 @@
<div class="row">
<div class="col-sm-12 col-md-2">
<label class="form-label" for="filter_preordercampaign_id">Kampagne</label>
<select name="filter[preordercampaign_id]" id="filter_preordercampaign_id" class="form-control">
<option value="">Alle</option>
<?php foreach($my_campaigns as $c): ?>
<option value="<?=$c->id?>" <?=(isset($campaign) && $c->id == $campaign->id) ? "selected='selected'" : ""?>><?=$c->name?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-12 col-md-2">
<label class="form-label" for="filter_preordercampaign_id">Kampagne</label>
<select name="filter[preordercampaign_id]" id="filter_preordercampaign_id" class="form-control">
<option value="">Alle</option>
<?php foreach($my_campaigns as $c): ?>
<option value="<?=$c->id?>" <?=(isset($campaign) && $c->id == $campaign->id) ? "selected='selected'" : ""?>><?=$c->name?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-12 col-md-2">
<label class="form-label" for="partner_id">Partner</label>
<select name="filter[partner_id]" id="filter_partner_id" class="form-control">
<option value="">Alle</option>
<?php foreach($partners as $partner): ?>
<option value="<?=$partner['partner_id']?>" <?=(isset($filter) && array_key_exists("partner_id", $filter) && $filter["partner_id"] == $partner['partner_id']) ? "selected='selected'" : ""?>><?=$partner['name']?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-12 col-md-2">
<label class="form-label" for="filter_status">Status</label>
@@ -148,5 +148,9 @@ TODO: enable option for showing prices
</div>
<?php endif; ?>
<div style="padding-top: 16pt">
Die Ware bleibt bis zur vollständigen Bezahlung Eigentum der XINON GmbH.
</div>
</body>
</html>
+13 -4
View File
@@ -562,10 +562,19 @@ class AddressController extends mfBaseController {
}
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "mergedName" => $search]));
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "company" => $search]));
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "firstname" => $search]));
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "lastname" => $search]));
if (isset($_GET['fibu_primary_account'])) {
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "mergedName" => $search, "fibu_primary_account" => true]));
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "company" => $search, "fibu_primary_account" => true]));
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "firstname" => $search, "fibu_primary_account" => true]));
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "lastname" => $search, "fibu_primary_account" => true]));
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "street" => $search, "fibu_primary_account" => true]));
} else {
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "mergedName" => $search]));
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "company" => $search]));
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "firstname" => $search]));
$addresses = array_merge($addresses, AddressModel::search(["parents_only" => $po, "addresstype" => [$role], "lastname" => $search]));
}
if(!is_array($addresses) || !count($addresses)) {
return false;
+11 -2
View File
@@ -157,12 +157,17 @@ class CalendarModel
$where .= " AND calendar_id IN (" . implode(",", $visibleCalendars) . ")";
$whereTimeRecording = " AND `Calendar`.`go_calendar_id` IN (" . implode(",", $visibleCalendars) . ")";
}
$sql = "SELECT `cal_events`.id, uuid, calendar_id, `cal_events`.user_id, start_time, end_time, timezone, all_day_event, `cal_events`.name,`cal_calendars`.name calendar_name, description, location, repeat_end_time, reminder, ctime,cname, mtime,mname, muser_id, busy, status, resource_event_id, private, rrule, `cal_events`.background, `cal_events`.files_folder_id, read_only, category_id, exception_for_event_id, recurrence_id, is_organizer,event_type,busy,recurrence FROM cal_events INNER JOIN `cal_calendars` ON (`cal_calendars`.`id`=`cal_events`.`calendar_id`) WHERE 1=1 $where ";
$sql = "SELECT `cal_events`.id,`cal_events`.categories, uuid, calendar_id, `cal_events`.user_id, start_time, end_time, timezone, all_day_event, `cal_events`.name,`cal_calendars`.name calendar_name, description, location, repeat_end_time, reminder, ctime,cname, mtime,mname, muser_id, busy, status, resource_event_id, private, rrule, `cal_events`.background, `cal_events`.files_folder_id, read_only, category_id, exception_for_event_id, recurrence_id, is_organizer,event_type,busy,recurrence,rrule_events FROM cal_events INNER JOIN `cal_calendars` ON (`cal_calendars`.`id`=`cal_events`.`calendar_id`) WHERE 1=1 $where ";
$res = $dbcal->query($sql);
if ($dbcal->num_rows($res)) {
while ($data = $dbcal->fetch_array($res)) {
if ($data['categories']) {
$categories = json_decode($data['categories'], true);
} else {
$categories = [];
}
unset($byweekday);
$rrule = false;
if ($attachments[$data['uuid']]) {
@@ -173,7 +178,7 @@ class CalendarModel
$attachmentLinks = "";
}
if ($data['all_day_event'] == 1) {
if (strpos($data['name'], "Bereitschaft") === false && strpos($data['name'], "Blocker") === false) {
if (in_array("Feiertag", $categories)) {
continue;
}
$starttime = date("Y-m-d", $data['start_time']);
@@ -190,6 +195,7 @@ class CalendarModel
if ($data['recurrence']) {
$recurrence = json_decode($data['recurrence'], true);
$rrule_events= json_decode($data['rrule_events'], true);
if ($rrulefreq[$recurrence['pattern']['type']]) {
unset ($byweekday);
$freq = $rrulefreq[$recurrence['pattern']['type']];
@@ -220,6 +226,7 @@ class CalendarModel
}
} else {
$rrule = false;
$rrule_events = false;
}
if ($calendarColors[$data['calendar_id']]['bgcolor']) {
@@ -245,7 +252,9 @@ class CalendarModel
'rights' => array('rights' => $rights, 'order' => $rights),
'location' => array('location' => $data['location']),
'busy' => array('busy' => $data['busy']),
'allDay' => array('allDay' => $data['all_day_event']),
'rrule' => array('rrule' => $rrule),
'rrule_events' => array('rrule_events' => $rrule_events),
'duration' => array('duration' => $duration),
'event_type' => array('event_type' => $data['event_type']),
'description' => array('description' => ($data['description'])),
+9
View File
@@ -0,0 +1,9 @@
<?php
/**
* @property mixed|null $name
*/
class Graphing extends mfBaseModel
{
}
@@ -0,0 +1,64 @@
<?php
class GraphingController extends mfBaseController{
private string $ZABBIX_API_URL = ZABBIX_API_URL;
private string $ZABBIX_API_KEY = ZABBIX_API_KEY;
private Zabbix $zabbix;
protected function init(): void {
$me = new User();
$me->loadMe();
$this->layout()->set("me", $me);
$this->me = $me;
if (!$this->me->isAdmin()) {
$this->redirect("dashboard");
}
$this->zabbix = new Zabbix($this->ZABBIX_API_URL, $this->ZABBIX_API_KEY);
}
protected function indexAction() {
$this->layout()->set('additionalJS', ["plugins/chart.js/chart.4.4.6.js", "plugins/chart.js/chartjs-adapter-moment.min.js"]);
Helper::renderVue($this, "DeviceGraphing", $this->mod, []);
}
protected function dataAction() {
header('Content-Type: application/json');
$hostId = $this->request->hostId;
$hostInterfaceItems = $this->zabbix->getHostInterfaceItems($hostId, '');
// limit to 25 items
$hostInterfaceItems = array_slice($hostInterfaceItems, 0, 25);
$itemIds = array_map(function($item) {
return $item['itemid'];
}, $hostInterfaceItems);
$itemValues = $this->zabbix->getItemValues($itemIds, 1000);
$out = [];
foreach ($hostInterfaceItems as $item) {
$out[$item['itemid']] = [
'name' => str_replace('Bits', 'Mbps', $item['name']),
'units' => $item['units'],
'values' => []
];
}
foreach ($itemValues as $itemValue) {
$out[$itemValue['itemid']]['values'][] = [
'clock' => $itemValue['clock'],
'value' => $itemValue['value'] / 1000000
];
}
// sort by name
uasort($out, function($a, $b) {
return strcmp($a['name'], $b['name']);
});
die(json_encode($out));
}
}
+1 -1
View File
@@ -164,7 +164,7 @@ class PreorderController extends mfBaseController {
}
$this->layout()->set("pagination", $pagination);
$this->layout()->set("preorders", $preorders);
$this->layout()->set("partners", PreorderModel::getAllPartners());
}
private function getPreparedFilter($filter) {
+18
View File
@@ -178,6 +178,24 @@ class PreorderModel {
return null;
}
public static function getAllPartners(): array {
$items = [];
$db = FronkDB::singleton();
$res = $db->select("Preorder", "partner_id", "partner_id IS NOT NULL AND partner_id > 0 GROUP BY partner_id");
if($db->num_rows($res)) {
while($data = $db->fetch_object($res)) {
$partner = AddressModel::getOne($data->partner_id);
$items[] = [
"partner_id" => $data->partner_id,
"name" => $partner->getCompanyOrName()
];
}
}
return $items;
}
public static function countWithLogistics($filter) {
$db = FronkDB::singleton();
@@ -4,50 +4,35 @@ class WarehouseShippingNoteController extends TTCrud {
protected string $headerTitle = 'Lieferscheine';
protected bool $createText = false;
protected array $columns = [['key' => 'id', 'text' => 'LS-Nr.', 'required' => false, 'modal' => false, 'table' => ['class' => 'text-nowrap']],
['key' => 'billingAddressId',
'text' => 'Rechnungsadresse',
'required' => true,
'type' => 'autocomplete',
'table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'],
'modal' => ['apiUrl' => 'Address/api?do=findAddress', 'items' => '/Address/Api?do=findAddress', 'type' => 'autocomplete']],
['key' => 'deliveryAddressName', 'text' => 'L.-Adr. Name', 'required' => true],
['key' => 'deliveryAddressLine', 'text' => 'L.-Adr.', 'required' => true],
['key' => 'deliveryAddressPLZ', 'text' => 'L.-Adr. PLZ', 'required' => true],
['key' => 'deliveryAddressEMail', 'text' => 'L.-Adr. EMail', 'required' => true, 'table' => false],
['key' => 'note', 'text' => 'Notiz', 'required' => true, 'table' => false],
['key' => 'status',
'text' => 'Status',
'required' => true,
'table' => ['filter' => 'select'],
'modal' => ['type' => 'select',
'items' => [['value' => 'new', 'text' => 'Neu'],
['value' => 'accepted', 'text' => 'Akzeptiert'],
['value' => 'invoiced', 'text' => 'In Rechnung gestellt'],]]],
['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'table' => false, 'modal' => false],
['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['filter' => 'date']],
['key' => 'createBy',
'text' => 'Erstellt von',
'required' => true,
'type' => 'autocomplete',
'table' => ['class' => 'text-nowrap', 'filter' => 'select'],
'modal' => ['items' => [], 'type' => 'select',]],
//@formatter:off
protected array $columns = [
['key' => 'id', 'text' => 'LS-Nr.', 'required' => false, 'modal' => false, 'table' => ['class' => 'text-nowrap']],
['key' => 'billingAddressId', 'text' => 'Rechnungsadresse', 'required' => true, 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'], 'modal' => ['apiUrl' => 'Address/api?do=findAddress', 'items' => '/Address/Api?do=findAddress', 'type' => 'autocomplete']],
['key' => 'deliveryAddressName', 'text' => 'L.-Adr. Name', 'required' => true],
['key' => 'deliveryAddressLine', 'text' => 'L.-Adr.', 'required' => true],
['key' => 'deliveryAddressPLZ', 'text' => 'L.-Adr. PLZ', 'required' => true],
['key' => 'deliveryAddressEMail', 'text' => 'L.-Adr. EMail', 'required' => false, 'table' => false],
['key' => 'note', 'text' => 'Art der Arbeit', 'required' => true, 'table' => false],
['key' => 'status', 'text' => 'Status', 'required' => true, 'table' => ['filter' => 'select'], 'modal' => ['type' => 'select', 'items' => [['value' => 'new', 'text' => 'Neu'], ['value' => 'inProgress', 'text' => 'In Bearbeitung'], ['value' => 'accepted', 'text' => 'Akzeptiert'], ['value' => 'invoiced', 'text' => 'In Rechnung gestellt'],]]],
['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'table' => false, 'modal' => false],
['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['filter' => 'date']],
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => 'select'], 'modal' => ['items' => [], 'type' => 'select',]],
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],];
['key' => 'actions',
'text' => 'Aktionen',
'required' => false,
'modal' => false,
'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],];
protected array $defaultOrder = ['key' => 'create', 'order' => 'DESC'];
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'],
['key' => 'print', 'title' => 'Drucken', 'class' => 'fas fa-print text-primary'],
['key' => 'printWithPrice', 'title' => 'Drucken mit Preis', 'class' => 'fas fa-print text-success'],
];
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true];
protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.',
'update' => 'Lieferschein wurde aktualisiert',
'delete' => 'Lieferschein wurde gelöscht',
'noChanges' => 'Keine Änderungen vorgenommen'];
//@formatter:on
protected function prepareCrudConfig() {
$users = array_map(function ($user) {
@@ -55,6 +40,10 @@ class WarehouseShippingNoteController extends TTCrud {
}, UserModel::search());
$this->columns[array_search('createBy', array_column($this->columns, 'key'))]['modal']['items'] = $users;
if (!$this->user->can('WarehouseAdmin')) {
$this->additionalJSVariables['WAREHOUSE_ADMIN'] = false;
}
}
protected function beforeCreate($postData): bool {
@@ -81,6 +70,14 @@ class WarehouseShippingNoteController extends TTCrud {
}
protected function beforeUpdate($postData): bool {
$shippingNote = WarehouseShippingNoteModel::get($postData['id']);
if ($shippingNote->status === 'accepted' || $shippingNote->status === 'invoiced') {
http_response_code(500);
self::returnJson(['success' => false, 'message' => 'Änderungen nicht mehr möglich']);
die();
}
$postData['positions'] = json_encode($postData['positions']);
(new WarehouseHistoryController)->create($postData, $this->mod);
return true;
@@ -242,9 +239,10 @@ class WarehouseShippingNoteController extends TTCrud {
// json decode hoursEntries and add to positions
$hoursEntries = json_decode($shippingNote->hoursEntries, true);
foreach ($hoursEntries as $hoursEntry) {
// die(json_encode($hoursEntry));
$positions[] = [
'articleTitle' => "Arbeitsstunden",
'articleDescription' => "Mitarbeiter: " . UserModel::getOne($hoursEntry['userId'])->name,
'articleDescription' => "Datum: ". date("d.m.Y", strtotime($hoursEntry['date'])) . " | Mitarbeiter: " . UserModel::getOne($hoursEntry['userId'])->name,
'articleUnit' => 'Std.',
'amount' => $hoursEntry['hourCount'],
'price' => $hoursEntry['hourlyPrice'] * $hoursEntry['hourCount'] ?? 0,
@@ -277,8 +275,8 @@ class WarehouseShippingNoteController extends TTCrud {
}
$pdf_vars = ["shippingNote" => $shippingNote,
"positions" => $positions,
"textElements" => $textElements,
"positions" => $positions,
"textElements" => $textElements,
"showPrices" => isset($_GET['price']) && $_GET['price'] == "true",
"bank_iban" => TT_INVOICE_BANK_IBAN,
"bank_bic" => TT_INVOICE_BANK_BIC,
@@ -328,7 +326,7 @@ class WarehouseShippingNoteController extends TTCrud {
// TODO: either move this to UserController or make it better
protected function userAutoCompleteAction() {
$users = array_map(function($user) {
$users = array_map(function ($user) {
return ['value' => $user->id, 'text' => $user->name];
}, UserModel::search(['employee' => true]));
@@ -336,11 +334,11 @@ class WarehouseShippingNoteController extends TTCrud {
$searchedID = $this->request->searchedID;
if (strlen($searchedID) > 0) {
// find user with value searchedID
$out = array_filter($users, function($user) use ($searchedID) {
$out = array_filter($users, function ($user) use ($searchedID) {
return $user['value'] == $searchedID;
});
} else {
$out = array_filter($users, function($user) {
$out = array_filter($users, function ($user) {
;
return strpos(strtolower($user['text']), strtolower($this->request->q)) !== false;
});
@@ -353,19 +351,20 @@ class WarehouseShippingNoteController extends TTCrud {
//TODO: either move this to TimerecordingCarController or make it better
protected function timerecordingCarAutoCompleteAction() {
$timerecordingCars = array_map(function($timerecordingCar) {
return ['value' => $timerecordingCar->id, 'text' => $timerecordingCar->number_plate . " " . $timerecordingCar->brand . " " . $timerecordingCar->model];
$timerecordingCars = array_map(function ($timerecordingCar) {
return ['value' => $timerecordingCar->id,
'text' => $timerecordingCar->number_plate . " " . $timerecordingCar->brand . " " . $timerecordingCar->model];
}, TimerecordingCarModel::getAll());
$out = null;
$searchedID = $this->request->searchedID;
if (strlen($searchedID) > 0) {
// find user with value searchedID
$out = array_filter($timerecordingCars, function($timerecordingCar) use ($searchedID) {
$out = array_filter($timerecordingCars, function ($timerecordingCar) use ($searchedID) {
return $timerecordingCar['value'] == $searchedID;
});
} else {
$out = array_filter($timerecordingCars, function($timerecordingCar) {
$out = array_filter($timerecordingCars, function ($timerecordingCar) {
return strpos(strtolower($timerecordingCar['text']), strtolower($this->request->q)) !== false;
});
@@ -387,6 +386,36 @@ class WarehouseShippingNoteController extends TTCrud {
die(json_encode(['success' => true, 'status' => 'USER_NO_CAR']));
}
protected function geoAutocompleteAction() {
$search = $this->request->q;
$search = urlencode($search);
$url = "https://nominatim.haid.in/search?q=$search&format=json";
$data = json_decode(file_get_contents($url), true);
$out = [];
foreach ($data as $entry) {
$parsedDisplayNameParts = [];
foreach (explode(',', $entry['display_name']) as $part) {
// if str_includes Bezirk remove it
if (strpos($part, 'Bezirk') !== false) {
continue;
}
$parsedDisplayNameParts[] = $part;
}
$out[] = ['value' => $entry['lat'] . "," . $entry['lon'], 'text' => implode(',', $parsedDisplayNameParts)];
}
self::returnJson($out);
}
protected function geoReverseAction() {
$lat = $this->request->lat;
$lon = $this->request->lon;
$url = "https://nominatim.haid.in/reverse?lat=$lat&lon=$lon&format=json";
$data = json_decode(file_get_contents($url), true);
self::returnJson($data);
}
//TODO: export this to an api class for openstreetmap
protected function getDistanceAction() {
@@ -413,7 +442,7 @@ class WarehouseShippingNoteController extends TTCrud {
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://nominatim.openstreetmap.org/search?q=$address&format=json",
CURLOPT_URL => "https://nominatim.haid.in/search?q=$address&format=json",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => "",
@@ -9,7 +9,7 @@ class WarehouseShippingNoteModel extends TTCrudBaseModel {
public string $deliveryAddressCity;
public string $deliveryAddressEMail;
public string $note;
public string $status; // 'new'|'accepted'|'invoiced'
public string $status; // 'new'|'in_progress'|'accepted'|'invoiced' TODO: add to enum / migration
public string $positions;
public string $textElements;
public string $hoursEntries;
@@ -0,0 +1,26 @@
<?php /** @noinspection ALL */
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class WarehouseModify3 extends AbstractMigration {
public function up(): void {
if ($this->getEnvironment() == "thetool") {
$WarehouseShippingNote = $this->table("WarehouseShippingNote", ["signed" => true]);
$WarehouseShippingNote->changeColumn("status", "enum", ["values" => ["new", "accepted", "invoiced", "in_progress"], "null" => false]);
$WarehouseShippingNote->save();
}
if ($this->getEnvironment() == "addressdb") {
}
}
public function down(): void {
if ($this->getEnvironment() == "thetool") {
$WarehouseShippingNote = $this->table("WarehouseShippingNote");
$WarehouseShippingNote->changeColumn("status", "enum", ["values" => ["new", "accepted", "invoiced"], "null" => false]);
$WarehouseShippingNote->save();
}
}
}
@@ -0,0 +1,27 @@
<?php /** @noinspection ALL */
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class WarehouseModify4 extends AbstractMigration {
public function up(): void {
if ($this->getEnvironment() == "thetool") {
$WarehouseShippingNote = $this->table("WarehouseShippingNote", ["signed" => true]);
// change column hoursEntries to text without limit
$WarehouseShippingNote->changeColumn("hoursEntries", "text", ["null" => true]);
$WarehouseShippingNote->save();
}
if ($this->getEnvironment() == "addressdb") {
}
}
public function down(): void {
if ($this->getEnvironment() == "thetool") {
$WarehouseShippingNote = $this->table("WarehouseShippingNote");
$WarehouseShippingNote->changeColumn("hoursEntries", "text", ["limit" => 255, "null" => true]);
$WarehouseShippingNote->save();
}
}
}
+4
View File
@@ -28,6 +28,10 @@ class Helper {
} else if (!empty($filterValue)) {
if ($exactMatch) {
$sql .= " AND `$columnName` = '" . $filterValue . "'";
} else if ($filterValue[0] === "%") {
$sql .= " AND `$columnName` LIKE '" . $filterValue . "'";
} else if ($filterValue[strlen($filterValue) - 1] === "%") {
$sql .= " AND `$columnName` LIKE '" . $filterValue . "'";
} else {
$filterItems = explode(" ", $filterValue);
foreach ($filterItems as $item) {
+14 -2
View File
@@ -124,6 +124,10 @@ class TTCrud extends mfBaseController {
$page = $this->postData['pagination']['page'] ?? 1;
$perPage = $this->postData['pagination']['per_page'] ?? 10;
if ($order['key'] === null && isset($this->defaultOrder)) {
$order = $this->defaultOrder;
}
$rows = $this->model::getAll($filter, $perPage, ($page - 1) * $perPage, $order);
$filteredAvailable = $this->model::count($filter);
$totalRows = $this->model::count();
@@ -240,11 +244,19 @@ class TTCrud extends mfBaseController {
if (strlen($searchedID) > 0) {
$filter = ['id' => $searchedID];
$data = $this->model::getAll($filter, 10);
} else {
$filter = [$textKey => $this->request->q];
$filter = [$textKey => $this->request->q . '%'];
$data = $this->model::getAll($filter, 10);
if (count($data) < 11) {
$filter = [$textKey => $this->request->q];
$lazyData = $this->model::getAll($filter, 10);
$data = array_merge($data, $lazyData);
$data = array_unique($data, SORT_REGULAR);
$data = array_slice($data, 0, 10);
}
}
$data = $this->model::getAll($filter, 10);
self::returnJson(array_map(function ($item) use ($textKey) {
return ['value' => $item->id, 'text' => $item->$textKey];
+7 -4
View File
@@ -46,13 +46,13 @@ class Zabbix {
return $response['result'];
}
public function getItemValues($itemIds) {
public function getItemValues($itemIds, $limit = 15) {
$response = $this->zabbixRequest('history.get', array(
'itemids' => $itemIds,
'output' => 'extend',
'sortfield' => 'clock',
'sortorder' => 'DESC',
'limit' => 15
'limit' => $limit
));
return $response['result'];
}
@@ -64,10 +64,13 @@ class Zabbix {
return $response['result'];
}
public function getInterfaceItems($hostId, $interfaceName) {
public function getHostInterfaceItems($hostId) {
$response = $this->zabbixRequest('item.get', array(
'hostids' => $hostId,
'search' => array('name' => array($interfaceName, "Bits"))
'output' => ['itemid','name_resolved', 'key_', 'units'],
'search' => ['name' => ["Bits received", "Bits sent"]],
'searchByAny' => true,
'sortfield' => 'name'
));
return $response['result'];
}
+172 -64
View File
@@ -85,6 +85,7 @@ document.addEventListener('DOMContentLoaded', function () {
var rrule = null;
var duration = null;
var rruleflag = false;
let allDAy;
$.each($('.calendar-check'), function (index, value) {
if ($(this).prop('checked')) {
rights = true;
@@ -102,16 +103,12 @@ document.addEventListener('DOMContentLoaded', function () {
$.each(json.data, function (index, value) {
if (!value.timerecording.timerecording) {
allDAy = false;
rrule = null;
duration = null;
rruleflag = false;
category = value.ccategory.ccategory;
if (value.rrule.rrule) {
rrule = value.rrule.rrule;
duration = value.duration.duration;
rruleflag = true;
}
if (value.calendar_id.calendar_id in calendarRights) {
if (calendarRights[value.calendar_id.calendar_id] == 'all') {
@@ -124,38 +121,90 @@ document.addEventListener('DOMContentLoaded', function () {
} else {
movable = false;
}
let event = {
id: value.id.id,
start: value.cstart.cstart,
end: value.cend.cend,
title: category,
description: value.description.description,
location: value.location.location,
attachment: value.attachment.attachment,
attachments: value.attachments.attachments,
calendar_id: value.calendar_id,
event_type: value.event_type.event_type,
classNames: ['cal-class-group-' + value.calendar_id.calendar_id, 'cal-class-id-' + value.id.id],
textColor: value.txtColor.txtColor,
backgroundColor: value.bgColor.bgColor,
editable: rights,
rruleflag: rruleflag,
rrule: rrule,
duration: duration,
droppable: movable,
startEditable: movable,
durationEditable: movable,
resizableFromStart: movable,
resourceId: value.calendar_id.calendar_id,
calendar_name: value.calendar_name.calendar_name,
clickable: rights,
mtime: value.mtime.mtime,
mname: value.mname.mname,
ctime: value.ctime.ctime,
cname: value.cname.cname,
busy: value.busy.busy
};
userevents.push(event);
if (value.allDay.allDay == "1") {
allDAy = true;
}
if (value.rrule.rrule) {
$.each(value.rrule_events.rrule_events, function (index, rrule_event) {
rruleflag = true;
let busy;
if (rrule_event.showAs == 'busy') {
busy = "1";
} else if (rrule_event.showAs == 'tentative') {
busy = '2';
} else if (rrule_event.showAs == 'free') {
busy = '0';
}
let event = {
id: value.id.id,
start: rrule_event.start,
end: rrule_event.end,
title: rrule_event.subject,
description: value.description.description,
location: value.location.location,
allDay: allDAy,
attachment: value.attachment.attachment,
attachments: value.attachments.attachments,
calendar_id: value.calendar_id,
event_type: value.event_type.event_type,
classNames: ['cal-class-group-' + value.calendar_id.calendar_id, 'cal-class-id-' + value.id.id],
textColor: value.txtColor.txtColor,
backgroundColor: value.bgColor.bgColor,
editable: rights,
rruleflag: rruleflag,
dates: rrule,
duration: duration,
droppable: movable,
startEditable: movable,
durationEditable: movable,
resizableFromStart: movable,
resourceId: value.calendar_id.calendar_id,
calendar_name: value.calendar_name.calendar_name,
clickable: rights,
mtime: value.mtime.mtime,
mname: value.mname.mname,
ctime: value.ctime.ctime,
cname: value.cname.cname,
busy: busy
};
userevents.push(event);
});
} else {
let event = {
id: value.id.id,
start: value.cstart.cstart,
end: value.cend.cend,
title: category,
description: value.description.description,
location: value.location.location,
allDay: allDAy,
attachment: value.attachment.attachment,
attachments: value.attachments.attachments,
calendar_id: value.calendar_id,
event_type: value.event_type.event_type,
classNames: ['cal-class-group-' + value.calendar_id.calendar_id, 'cal-class-id-' + value.id.id],
textColor: value.txtColor.txtColor,
backgroundColor: value.bgColor.bgColor,
editable: rights,
rruleflag: rruleflag,
dates: rrule,
duration: duration,
droppable: movable,
startEditable: movable,
durationEditable: movable,
resizableFromStart: movable,
resourceId: value.calendar_id.calendar_id,
calendar_name: value.calendar_name.calendar_name,
clickable: rights,
mtime: value.mtime.mtime,
mname: value.mname.mname,
ctime: value.ctime.ctime,
cname: value.cname.cname,
busy: value.busy.busy
};
userevents.push(event);
}
if (value.rrule.rrule) {
}
} else {
@@ -1767,6 +1816,7 @@ Xinon GMbH`;
var rrule = null;
var duration = null;
var rruleflag = false;
let allDAy = false;
$.each($('.calendar-check'), function (index, value) {
if ($(this).prop('checked')) {
rights = true;
@@ -1787,6 +1837,7 @@ Xinon GMbH`;
rrule = null;
duration = null;
rruleflag = false;
allDAy = false;
category = value.ccategory.ccategory;
if (value.rrule.rrule) {
rrule = value.rrule.rrule;
@@ -1805,33 +1856,90 @@ Xinon GMbH`;
} else {
movable = false;
}
userevents.push({
id: value.id.id,
start: value.cstart.cstart,
end: value.cend.cend,
title: category,
description: value.description.description,
location: value.location.location,
attachment: value.attachment.attachment,
attachments: value.attachments.attachments,
calendar_id: value.calendar_id,
event_type: value.event_type.event_type,
classNames: ['cal-class-group-' + value.calendar_id.calendar_id, 'cal-class-id-' + value.id.id],
textColor: value.txtColor.txtColor,
backgroundColor: value.bgColor.bgColor,
editable: movable,
rruleflag: rruleflag,
rrule: rrule,
duration: duration,
resourceId: value.calendar_id.calendar_id,
calendar_name: value.calendar_name.calendar_name,
clickable: rights,
mtime: value.mtime.mtime,
mname: value.mname.mname,
ctime: value.ctime.ctime,
cname: value.cname.cname,
busy: value.busy.busy
});
if (value.allDay.allDay == "1") {
allDAy = true;
}
if (value.rrule.rrule) {
$.each(value.rrule_events.rrule_events, function (index, rrule_event) {
rruleflag = true;
let busy;
if (rrule_event.showAs == 'busy') {
busy = "1";
} else if (rrule_event.showAs == 'tentative') {
busy = '2';
} else if (rrule_event.showAs == 'free') {
busy = '0';
}
let event = {
id: value.id.id,
start: rrule_event.start,
end: rrule_event.end,
title: rrule_event.subject,
description: value.description.description,
location: value.location.location,
allDay: allDAy,
attachment: value.attachment.attachment,
attachments: value.attachments.attachments,
calendar_id: value.calendar_id,
event_type: value.event_type.event_type,
classNames: ['cal-class-group-' + value.calendar_id.calendar_id, 'cal-class-id-' + value.id.id],
textColor: value.txtColor.txtColor,
backgroundColor: value.bgColor.bgColor,
editable: rights,
rruleflag: rruleflag,
dates: rrule,
duration: duration,
droppable: movable,
startEditable: movable,
durationEditable: movable,
resizableFromStart: movable,
resourceId: value.calendar_id.calendar_id,
calendar_name: value.calendar_name.calendar_name,
clickable: rights,
mtime: value.mtime.mtime,
mname: value.mname.mname,
ctime: value.ctime.ctime,
cname: value.cname.cname,
busy: busy
};
userevents.push(event);
});
} else {
let event = {
id: value.id.id,
start: value.cstart.cstart,
end: value.cend.cend,
title: category,
description: value.description.description,
location: value.location.location,
allDay: allDAy,
attachment: value.attachment.attachment,
attachments: value.attachments.attachments,
calendar_id: value.calendar_id,
event_type: value.event_type.event_type,
classNames: ['cal-class-group-' + value.calendar_id.calendar_id, 'cal-class-id-' + value.id.id],
textColor: value.txtColor.txtColor,
backgroundColor: value.bgColor.bgColor,
editable: rights,
rruleflag: rruleflag,
dates: rrule,
duration: duration,
droppable: movable,
startEditable: movable,
durationEditable: movable,
resizableFromStart: movable,
resourceId: value.calendar_id.calendar_id,
calendar_name: value.calendar_name.calendar_name,
clickable: rights,
mtime: value.mtime.mtime,
mname: value.mname.mname,
ctime: value.ctime.ctime,
cname: value.cname.cname,
busy: value.busy.busy
};
userevents.push(event);
}
} else {
otherevents.push({
id: value.id.id,
+1 -1
View File
@@ -77,7 +77,7 @@ Vue.component('DeviceTable', {
<template v-if="row.zabbix_host_id !== '0' && row.zabbix_host_id !== null">
<a :href="window['TT_CONFIG']['ZABBIX_URL'] + '/zabbix.php?action=latest.view&hostids%5B%5D=' + row.zabbix_host_id" target="_blank" class="text-info" title="Zabbix"><i class="fas fa-server"></i></a>
<a :href="window['TT_CONFIG']['GRAFANA_URL'] + '/d/Ta3PtRWZk/mikrotik-dashboard?orgId=1&var-host=' + row.name" target="_blank" class="text-info" title="Grafana"><i class="fas fa-chart-line"></i></a>
<a :href="window['TT_CONFIG']['BASE_URL'] + '/Graphing?id=' + row.zabbix_host_id + '&hostname=' + row.name" target="_blank" class="text-info" title="Graphen"><i class="fas fa-chart-line"></i></a>
</template>
</template>
@@ -0,0 +1,133 @@
Vue.component('tt-graph', {
template: `
<!-- use chart js with datasets etc everything needed for chart.js here to be usable width and height aswell -->
<tt-card>
<template v-slot:header>
<h3 style="text-align: center;user-select: none">{{ header }}</h3>
</template>
<div ref="container">
<canvas ref="chart" :style="{width: width + 'px', height: height + 'px'}"></canvas></div>
</tt-card>
`,
props: ['data', 'labels', 'header'],
data() {
return {
chart: null,
width: 400,
height: 220
}
},
mounted() {
const ctx = this.$refs.chart.getContext('2d');
this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: this.labels,
datasets: this.data
},
options: {
scales: {
y: {
beginAtZero: true
},
x: {
type: 'time',
time: {
// time is epoch
parser: 'X',
// unit: 'hour',
displayFormats: {
minute: 'DD.M. HH:mm',
}
},
// min: '00:00:00',
// max: '24:00:00',
ticks: {
autoSkipPadding: 25,
autoSkip: true,
maxRotation: 0
}
}
},
responsive: true,
},
});
// set width and height to the canvas element actual width and height
this.width = this.$refs.container.width;
this.height = this.$refs.container.height;
}
})
Vue.component('device-graphing', {
template: `
<div style="display: grid; grid-template-columns: 45vw 45vw; gap: 1rem;">
<h3 style="text-align: center;user-select: none;grid-column: 1 / span 2;">{{ hostname }}</h3>
<tt-loader v-if="graphs.length === 0"></tt-loader>
<template v-for="graph in graphs">
<tt-graph :data="graph.data" :labels="graph.labels" :header="graph.name"></tt-graph>
</template>
</div>
`,
data() {
return {
graphs: [],
hostname: ''
}
},
async mounted() {
// get hostname from url params
this.hostname = new URLSearchParams(window.location.search).get('hostname');
console.log(this.hostname);
// get id from url params
const id = new URLSearchParams(window.location.search).get('id');
const response = await axios.get('/Graphing/data?id=' + id);
const data = response.data;
const graphs = {};
for (const item in data) {
// Create graphs.[item.name.split(':')[0]] if it doesn't exist
if (!graphs[data[item].name.split(':')[0]]) {
graphs[data[item].name.split(':')[0]] = {
name: data[item].name.split(':')[0],
data: [],
labels: []
}
}
// Add the data to the graph but check if it's received or sent and already exists
if (data[item].name.split(':')[1].includes('received') && graphs[data[item].name.split(':')[0]].data.find(data => data.label.includes('received'))) {
continue;
}
if (data[item].name.split(':')[1].includes('sent') && graphs[data[item].name.split(':')[0]].data.find(data => data.label.includes('sent'))) {
continue;
}
graphs[data[item].name.split(':')[0]].data.push({
label: data[item].name.split(':')[1],
data: data[item].values.map(value => value.value),
fill: false,
borderColor: data[item].name.includes('received') ? 'rgb(75, 192, 192)' : 'rgb(192, 75, 75)',
tension: 0.1
});
}
// Add the labels to the this.graphs
for (const graph in graphs) {
graphs[graph].labels = data[Object.keys(data).find(key => data[key].name.includes(graph))].values.map(value => value.clock);
this.graphs.push(graphs[graph]);
}
}
});
@@ -246,6 +246,7 @@ Vue.component('warehouse-article-price-modal', {
})
// noinspection EqualityComparisonWithCoercionJS
Vue.component('warehouse-article', {
//language=Vue
template: `
@@ -261,7 +262,7 @@ Vue.component('warehouse-article', {
<template v-slot:cheapestsellprice="{ row }">
<template v-for="price in JSON.parse(row.cheapestSellPrice)">
<span v-if="price && window.TT_CONFIG['WAREHOUSE_ADMIN'] === true">{{price.title}}: {{(price.price)}} <br></span>
<span v-if="price && window.TT_CONFIG['WAREHOUSE_ADMIN'] == true">{{price.title}}: {{(price.price)}} <br></span>
<span v-if="price && price.title === 'Verkauf'">{{(price.price)}} </span>
</template>
</template>
@@ -36,6 +36,10 @@
grid-gap: 10px;
}
.warehouse-shipping-note-modal-positions-entry-actions, .warehouse-shipping-note-modal-hours-entry-actions {
grid-column: 2;
}
.signModal > div {
margin: 0;
width: 100vw;
@@ -57,7 +57,7 @@ Vue.component('warehouse-shipping-note-modal-hours-entry', {
<tt-input v-model="hourCount" label="Stunden" sm/>
<tt-autocomplete v-model="carId" :api-url="carApiUrl" label="Fahrzeug" sm/>
<tt-input v-model="hourlyPrice" label="Stundenlohn" type="number" sm v-if="showHourlyPrice"/>
<tt-input v-model="kilometerCount" label="Kilometer" sm/>
<tt-input :disabled="carId === ''" v-model="kilometerCount" label="Kilometer" sm/>
<div class="warehouse-shipping-note-modal-hours-entry-actions">
<button @click="createOrUpdate" class="btn btn-sm btn-primary">Speichern</button>
</div>
@@ -87,6 +87,10 @@ Vue.component('warehouse-shipping-note-modal-hours-entry', {
this.updateCarId().then();
},
async updateKilometerCount() {
if (!this.carId) {
this.kilometerCount = '';
return;
}
const delAddr = this.$parent.$parent.$parent.delAddrLine +
' ' +
this.$parent.$parent.$parent.delAddrCity +
@@ -96,9 +100,11 @@ Vue.component('warehouse-shipping-note-modal-hours-entry', {
this.kilometerCount = response.data.distance
},
async updateCarId() {
if (!this.userId || this.carId) return;
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/timerecordingCarForUser?userId=' + this.userId);
if (response.data.status === 'USER_NO_CAR') {
this.window.notify('info', 'Kein zugewiesenes Fahrzeug gefunden');
this.carId = '';
return;
}
this.carId = response.data.id;
@@ -114,12 +120,14 @@ Vue.component('warehouse-shipping-note-modal-hours-entry', {
}
},
async mounted() {
if (!this.carId) this.updateCarId().then();
if (!this.userId) this.userId = this.window.TT_CONFIG['USER_ID'];
if (!this.carId) this.updateCarId().then();
if (!this.date) this.updateDate();
if (!this.kilometerCount) this.updateKilometerCount().then();
this.$parent.$parent.$parent.$watch('delAddrLine', this.updateKilometerCount);
this.$watch('carId', this.updateKilometerCount);
}
})
@@ -270,10 +278,9 @@ Vue.component('warehouse-shipping-note-modal-positions-entry', {
// TODO: if articlePacket is needed we need to implement this
async createOrUpdate() {
if (!this.amount) return this.window.notify('error', 'Bitte füllen sie die Menge aus');
if (!this.price) return this.window.notify('error', 'Bitte füllen sie den Preis aus');
const data = {
amount: this.amount,
price: parseFloat(this.price)
price: parseFloat(this.price) ?? ''
}
if (!this.articleId && this.$refs.article.displayValue) {
data.articleText = this.$refs.article.displayValue;
@@ -331,7 +338,9 @@ Vue.component('warehouse-shipping-note-modal-positions-view', {
<td colspan="4" class="text-center">Keine Einträge</td>
</tr>
<tr v-for="position in positions">
<td>{{ position.article ? articleNames[position.article] : position.articlePacket ? articlePacketNames[position.articlePacket] : position.articleText }}</td>
<td>{{ position.article ? articleNames[position.article] : position.articlePacket ? articlePacketNames[position.articlePacket] :
position.articleText }}
</td>
<td>{{ position.amount }}</td>
<td>{{ (position.price?.toFixed(2)) }} </td>
<td>
@@ -415,7 +424,7 @@ Vue.component('warehouse-shipping-note-modal-positions', {
},
editEntry(entry) {
this.selectedUpdateIndex = this.positions.indexOf(entry);
if (entry.article)this.$refs.entry.articleId = entry.article;
if (entry.article) this.$refs.entry.articleId = entry.article;
if (entry.articlePacket) this.$refs.entry.articlePacketId = entry.articlePacket;
if (entry.articleText) this.$refs.entry.$refs.article.displayValue = entry.articleText;
this.$refs.entry.amount = entry.amount;
@@ -425,6 +434,7 @@ Vue.component('warehouse-shipping-note-modal-positions', {
})
// noinspection EqualityComparisonWithCoercionJS
Vue.component('warehouse-shipping-note-modal', {
props: {
id: {type: [String, Number], required: true},
@@ -434,7 +444,7 @@ Vue.component('warehouse-shipping-note-modal', {
data() {
return {
window: window,
billAddrAutoCompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress',
billAddrAutoCompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1',
billAddrId: '',
delAddrName: '',
delAddrLine: '',
@@ -451,7 +461,7 @@ Vue.component('warehouse-shipping-note-modal', {
//language=Vue
template: `
<tt-modal :show="true" @submit="submit" :delete="false" :title="title" @update:show="$emit('close')">
<tt-modal :show="true" @submit="submit" @delete="reqDelete" :delete="id !== 'create'" :title="title" @update:show="$emit('close')">
<div style="width: 99%">
<h4 class="text-center">Liefer- und Rechnungsadresse</h4>
<tt-autocomplete v-model="billAddrId" :api-url="billAddrAutoCompleteUrl" label="Rechnungsadresse" sm row/>
@@ -460,14 +470,16 @@ Vue.component('warehouse-shipping-note-modal', {
:del-addr-e-mail.sync="delAddrEMail"/>
<template v-if="billAddrId && delAddrName && delAddrLine && delAddrPLZ && delAddrCity">
<hr>
<h4 class="text-center">Textelemente</h4>
<warehouse-shipping-note-modal-text-elements :text-elements="textElements"/>
<div v-show="delAddrFilled === true">
<template v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && 1 < 0">
<hr>
<h4 class="text-center">Textelemente</h4>
<warehouse-shipping-note-modal-text-elements :text-elements="textElements"/>
</template>
<hr>
<tt-textarea label="Einleitender Text" v-model="note" sm row/>
<tt-textarea label="Art der Arbeit" v-model="note" sm row/>
<hr>
@@ -478,16 +490,23 @@ Vue.component('warehouse-shipping-note-modal', {
<hr>
<h4 class="text-center">Positionen</h4>
<warehouse-shipping-note-modal-positions :positions.sync="positions" :bill-addr-id="billAddrId"/>
</template>
</div>
<div v-else class="text-center">Bitte füllen Sie die Rechnungs- und Lieferadresse aus</div>
<div v-show="delAddrFilled === false" class="text-center">Bitte füllen Sie die Rechnungs- und Lieferadresse aus</div>
</div>
<!-- TODO: fix these buttons-->
<template v-slot:footer-prepend v-if="id !== 'create'">
<button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && status === 'new'" class="btn btn-warning" @click="alert('In Bearbeitung')">In
Bearbeitung
</button>
<button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && (status === 'new' || status === 'in_progress')" class="btn btn-success"
@click="alert('Accepted')">Akzeptieren
</button>
<button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && status === 'accepted'" class="btn btn-info" @click="alert('Invoiced')">
Verrechnet
</button>
<button class="btn btn-info" @click="$emit('open-signing-modal', id)">Unterschreiben</button>
<!-- <button class="btn btn-success" @click="alert('Accept')">Akzeptieren</button>-->
<!-- <button class="btn btn-warning" @click="alert('Invoiced')">Verrechnet</button>-->
</template>
</tt-modal>
`,
@@ -516,8 +535,14 @@ Vue.component('warehouse-shipping-note-modal', {
}
},
methods: {
openSigningModal() {
async reqDelete() {
const response = await axios.post(window.TT_CONFIG['DELETE_URL'], {id: this.id});
if (response.data.success) {
this.window.notify('success', response.data.message || 'Erfolgreich gelöscht');
this.$emit('close');
} else {
this.window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
}
},
async submit() {
const data = {
@@ -553,13 +578,16 @@ Vue.component('warehouse-shipping-note-modal', {
computed: {
title() {
return this.id === 'create' ? 'Lieferschein erstellen' : `Lieferschein #${this.id} bearbeiten`;
},
delAddrFilled() {
if (this.id !== 'create') return true;
return !!this.delAddrName && !!this.delAddrLine && !!this.delAddrPLZ && !!this.delAddrCity;
}
}
})
Vue.component('warehouse-shipping-note-modal-address', {
// also add props for delAddrName, delAddrLine, delAddrPLZ, delAddrCity which we will sync with the parent component
props: {
billAddrId: {type: [String, Number], required: true},
delAddrName: {type: String, required: true},
@@ -570,14 +598,15 @@ Vue.component('warehouse-shipping-note-modal-address', {
},
data() {
return {
window: window,
addressModes: [{text: 'Wie Rechnungsadresse', value: 'billing'},
{text: 'Bestehende Lieferadresse', value: 'existing'},
{text: 'Andere Lieferadresse', value: 'new'}],
addressMode: 'existing',
addresses: [],
fetchedBillAddr: null,
selectedAddr: '',
window: window,
addressModes: [{text: 'Wie Rechnungsadresse', value: 'billing'},
{text: 'Bestehende Lieferadresse', value: 'existing'},
{text: 'Andere Lieferadresse', value: 'new'}],
addressMode: 'existing',
addresses: [],
fetchedBillAddr: null,
selectedAddr: '',
newAddrGeoLatLon: '',
}
},
//language=Vue
@@ -590,36 +619,66 @@ Vue.component('warehouse-shipping-note-modal-address', {
</template>
<template v-else-if="addressMode === 'new'">
<tt-input v-model="delAddrName" label="Lieferadresse Name" sm row/>
<tt-input v-model="delAddrLine" label="Lieferadresse" sm row/>
<tt-input v-model="delAddrPLZ" label="Lieferadresse PLZ" sm row/>
<tt-input v-model="delAddrCity" label="Lieferadresse Ort" sm row/>
<tt-input v-model="delAddrEMail" label="Lieferadresse E-Mail" sm row/>
<tt-input :value="delAddrName" @input="$emit('update:delAddrName', $event)" label="Lieferadresse Name*" sm row/>
<tt-input :value="delAddrEMail" @input="$emit('update:delAddrEMail', $event)" label="Lieferadresse E-Mail" sm row/>
<tt-autocomplete :api-url="window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/geoAutocomplete'" @input="newAddrGeoLatLon = $event"
label="Adresse*" sm row/>
<span v-if="delAddrLine && delAddrPLZ && delAddrCity">Adresse: {{ delAddrLine }}, {{ delAddrPLZ }} {{ delAddrCity }}</span>
<!-- <tt-input :value="delAddrLine" @input="$emit('update:delAddrLine', $event)" label="Lieferadresse" sm row/>-->
<!-- <tt-input :value="delAddrPLZ" @input="$emit('update:delAddrPLZ', $event)" label="Lieferadresse PLZ" sm row/>-->
<!-- <tt-input :value="delAddrCity" @input="$emit('update:delAddrCity', $event)" label="Lieferadresse Ort" sm row/>-->
</template>
</div>
`,
watch: {
billAddrId: {handler: 'updateBillingMode', immediate: false},
addressMode: {handler: 'fetchDeliveryAddresses', immediate: false},
selectedAddr: {handler: 'setSelectedAddrValues', immediate: false},
billAddrId: {handler: 'updateBillingMode', immediate: false},
addressMode: {handler: 'fetchDeliveryAddresses', immediate: false},
selectedAddr: {handler: 'setSelectedAddrValues', immediate: false},
newAddrGeoLatLon: {handler: 'fetchGeoAddress', immediate: false},
},
methods: {
async fetchGeoAddress() {
if (!this.newAddrGeoLatLon) {
this.$emit('update:delAddrLine', '');
this.$emit('update:delAddrPLZ', '');
this.$emit('update:delAddrCity', '');
return;
}
const [lat, lon] = this.newAddrGeoLatLon.split(',');
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/geoReverse?lat=' + lat + '&lon=' + lon);
if (response.data.address.road) {
this.$emit('update:delAddrLine',
`${response.data.address.road}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
} else if(response.data.address.village) {
this.$emit('update:delAddrLine',
`${response.data.address.village}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
} else if(response.data.address.hamlet) {
this.$emit('update:delAddrLine',
`${response.data.address.hamlet}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
} else if(response.data.address.residential) {
this.$emit('update:delAddrLine',
`${response.data.address.residential}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
} else if(response.data.address.city) {
this.$emit('update:delAddrLine',
`${response.data.address.city}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
}
this.$emit('update:delAddrPLZ', response.data.address.postcode);
this.$emit('update:delAddrCity', response.data.address.village || response.data.address.city || response.data.address.town);
},
async updateBillingMode() {
await this.fetchDeliveryAddresses();
// this.addressMode = 'billing';
console.log('updateBillingMode');
// Here we check if the address is already in the list of addresses, if not we will set the addressMode to billing and fetch the billing address
if (this.delAddrName && this.delAddrLine && this.delAddrPLZ && this.delAddrCity) {
const foundAddress = this.addresses.find(address => address.deliveryAddressName ===
this.delAddrName &&
address.deliveryAddressLine ===
this.delAddrLine &&
address.deliveryAddressPLZ ===
this.delAddrPLZ &&
address.deliveryAddressCity ===
this.delAddrCity && address.deliveryAddressEMail === this.delAddrEMail);
const foundAddress = this.addresses.find(address =>
address.deliveryAddressName === this.delAddrName &&
address.deliveryAddressLine === this.delAddrLine &&
address.deliveryAddressPLZ === this.delAddrPLZ &&
address.deliveryAddressCity === this.delAddrCity &&
address.deliveryAddressEMail === this.delAddrEMail);
if (foundAddress) {
this.addressMode = 'existing';
this.selectedAddr = foundAddress.id;
@@ -631,11 +690,21 @@ Vue.component('warehouse-shipping-note-modal-address', {
await this.fetchBillingAddress();
}
},
async fetchDeliveryAddresses() {
async fetchDeliveryAddresses(newVal, oldVal) {
if ((oldVal === 'billing' || oldVal === 'existing') && newVal === 'new') {
this.$emit('update:delAddrName', '');
this.$emit('update:delAddrLine', '');
this.$emit('update:delAddrPLZ', '');
this.$emit('update:delAddrCity', '');
this.$emit('update:delAddrEMail', '');
return;
}
if (this.addressMode === 'billing' && this.billAddrId) {
await this.fetchBillingAddress();
return;
}
if (!this.billAddrId || this.addressMode !== 'existing' || this.fetchedBillAddr === this.billAddrId) return;
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getDeliveryAddresses?billingAddressId=' + this.billAddrId);
@@ -667,7 +736,8 @@ Vue.component('warehouse-shipping-note-modal-address', {
this.window.notify('error', 'Rechnungsadresse konnte nicht gefunden werden');
return;
}
this.window.notify('success', 'Rechnungsadresse gefunden');
// TODO: here is still a bug that we fetch the billing address twice
// this.window.notify('success', 'Rechnungsadresse gefunden');
this.$emit('update:delAddrName',
response.data.result.address.company || response.data.result.address.firstname + ' ' + response.data.result.address.lastname);
@@ -690,9 +760,9 @@ Vue.component('warehouse-shipping-note-signature-pad', {
},
data() {
return {
window: window,
signaturePad: null,
shippingNote: null,
window: window,
signaturePad: null,
shippingNote: null,
signatureName: '',
}
},
@@ -700,19 +770,24 @@ Vue.component('warehouse-shipping-note-signature-pad', {
template: `
<tt-modal class="signModal" :show="true" :delete="false" :submit="false" @update:show="$emit('close')" :title="'Unterschrift'">
<div style="max-width: 520px;display: flex; flex-direction: column; align-items: center;">
<div style="width: 480px"><tt-input v-model="signatureName" label="Name" row/></div>
<div><canvas id="signature-pad" width="500" height="200" style="border: 1px solid black"></canvas></div>
<div>
<button class="btn btn-primary" @click="submit()">Speichern</button>
<button class="btn btn-primary" @click="signaturePad.clear()">Leeren</button>
</div>
<div style="width: 480px">
<tt-input v-model="signatureName" label="Name" row/>
</div>
<div>
<canvas id="signature-pad" width="500" height="200" style="border: 1px solid black"></canvas>
</div>
<div>
<button class="btn btn-primary" @click="submit()">Speichern</button>
<button class="btn btn-primary" @click="signaturePad.clear()">Leeren</button>
</div>
</div>
</tt-modal>
`,
methods: {
async submit() {
const data = this.signaturePad.toDataURL();
const response = await axios.post(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/sign?id=' + this.shippingNoteId, {signature: data, signatureName: this.signatureName});
const response = await axios.post(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/sign?id=' + this.shippingNoteId,
{signature: data, signatureName: this.signatureName});
if (response.data.success) {
this.window.notify('success', response.data.message || 'Erfolgreich unterschrieben');
this.$emit('close');
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
/*!
* chartjs-adapter-moment v1.0.1
* https://www.chartjs.org
* (c) 2022 chartjs-adapter-moment Contributors
* Released under the MIT license
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("moment"),require("chart.js")):"function"==typeof define&&define.amd?define(["moment","chart.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).moment,e.Chart)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var f=n(e);const a={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};t._adapters._date.override("function"==typeof f.default?{_id:"moment",formats:function(){return a},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=f.default(e,t):e instanceof f.default||(e=f.default(e)),e.isValid()?e.valueOf():null},format:function(e,t){return f.default(e).format(t)},add:function(e,t,n){return f.default(e).add(t,n).valueOf()},diff:function(e,t,n){return f.default(e).diff(f.default(t),n)},startOf:function(e,t,n){return e=f.default(e),"isoWeek"===t?(n=Math.trunc(Math.min(Math.max(0,n),6)),e.isoWeekday(n).startOf("day").valueOf()):e.startOf(t).valueOf()},endOf:function(e,t){return f.default(e).endOf(t).valueOf()}}:{})}));
@@ -99,7 +99,7 @@ Vue.component('tt-autocomplete', {
if (this.value && this.apiUrl) {
const response = await axios.get(`${this.apiUrl}&autocomplete=1&searchedID=${this.value}`);
const response = await axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&searchedID=${this.value}`);
this.displayValue = response.data[0].text;
} else if (this.value) {
const selectedItem = this.items.find(item => item.value === this.value);
@@ -145,7 +145,6 @@ Vue.component('tt-autocomplete', {
this.isLoading = true;
clearTimeout(this.fetchSuggestionsDebounceTimer);
console.log(this.displayValue);
this.fetchSuggestionsDebounceTimer = setTimeout(() => {
setTimeout(async () => {
@@ -155,7 +154,7 @@ Vue.component('tt-autocomplete', {
return;
}
const response = await axios.get(`${this.apiUrl}&autocomplete=1&q=${encodeURIComponent(this.displayValue)}`);
const response = await axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&q=${encodeURIComponent(this.displayValue)}`);
if (response.data?.status === 'error') {
this.displayingItems = [];
} else {
+3 -1
View File
@@ -38,7 +38,9 @@ Vue.component('tt-modal', {
this.$emit('update:show', false)
}
if (event.key === 'Enter' && this.save) {
// only submit
if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT') {
return
}
this.$emit('submit')
}
}