Added new WarehouseOrder Module

This commit is contained in:
Luca Haid
2025-01-31 16:09:03 +01:00
parent 91647e952a
commit 30694202da
17 changed files with 623 additions and 281 deletions

View File

@@ -1,64 +1,47 @@
<?php
if (!isset($vueViewName)) {
die("vueViewName is not set");
}
if (!isset($mfLayoutPackage)) {
die("mfLayoutPackage is not set");
}
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";
$additionalJS = [
"bundler.php",
...$additionalJS,
];
$additionalJS = ["bundler.php", ...$additionalJS];
if (is_dir($vueViewPath)) {
$files = scandir($vueViewPath);
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
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";
}
if ($fileExtension === 'css') $additionalCSS[] = "js/pages/$vueViewName/$file";
else if ($fileExtension === 'js') $additionalJS[] = "js/pages/$vueViewName/$file";
}
}
$additionalCSS = [
...$additionalCSS,
...$additionalCSS,
'plugins/daterangepicker/daterangepicker.css',
'plugins/vue/tt-components/css/tt-table.css',
'plugins/vue/tt-components/css/tt-loader.css',
'plugins/vue/tt-components/css/tt-position-manager.css',
];
/**
* Convert PascalCase to snake_case (e.g. PascalCase to pascal-case)
* @param $str string PascalCase string
* @param string $str PascalCase string
* @return string snake-case string
*/
function pascalToSnakeCase(string $str): string {
$snakeCase = preg_replace('/(?<!^)([A-Z])/', '-$1', $str);
return strtolower($snakeCase);
return strtolower(preg_replace('/(?<!^)([A-Z])/', '-$1', $str));
}
$vueTagName = pascalToSnakeCase($vueViewName);
$vueHeaderPath = realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/vueHeader.php";
if(!file_exists($vueHeaderPath)) {
if (!file_exists($vueHeaderPath))
$vueHeaderPath = realpath(dirname(__FILE__) . "/../../default") . "/vueHeader.php";
}
include($vueHeaderPath); ?>
<div id="app">
<tt-page-title
v-if="window['TT_CONFIG'] && window['TT_CONFIG']['PAGE_TITLE'] && window['TT_CONFIG']['PATH']"
@@ -66,11 +49,11 @@ include($vueHeaderPath); ?>
:path="window['TT_CONFIG']['PATH']">
</tt-page-title>
<<?php echo $vueTagName; ?>></<?php echo $vueTagName; ?>>
<<?php echo $vueTagName; ?>>
</<?php echo $vueTagName; ?>>
</div>
<script>
const view = new Vue({el: '#app', data: {window: window}});
</script>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>

View File

@@ -4,7 +4,6 @@
// Hide Articles
class WarehouseEShopController extends TTCrud {
protected string $headerTitle = 'Energie Steiermark Shop';
protected bool $createText = false;
@@ -12,11 +11,13 @@ class WarehouseEShopController extends TTCrud {
protected array $columns = [
['key' => 'title', 'text' => 'Artikel', 'priority' => 11],
['key' => 'category', 'text' => 'Kategorie', 'table' => false],
['key' => 'price', 'text' => 'Preis', 'table' => ['filter' => false,'sortable' => false,'class' => 'text-right']],
['key' => 'amount', 'text' => 'Menge', 'table' => ['filter' => false,'sortable' => false,'class' => 'p-0 width-80'], 'priority' => 9],
['key' => 'add', 'text' => 'Hinzufügen', 'table' => ['filter' => false,'sortable' => false, 'class' => 'width-120 text-center'], 'priority' => 5000]
['key' => 'price', 'text' => 'Preis', 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-right']],
['key' => 'amount', 'text' => 'Menge', 'table' => ['filter' => false, 'sortable' => false, 'class' => 'p-0 width-80'], 'priority' => 9],
['key' => 'add', 'text' => 'Hinzufügen', 'table' => ['filter' => false, 'sortable' => false, 'class' => 'width-120 text-center'], 'priority' => 5000]
];
protected array $permissionCheck = ['WarehouseEShop'];
protected array $infoMessages = [
'create' => 'Not possible',
'update' => 'Not possible',
@@ -24,10 +25,6 @@ class WarehouseEShopController extends TTCrud {
'noChanges' => 'Keine Änderungen',
];
public function permissionCheck(): bool {
return $this->user->can(["WarehouseEShop"]);
}
protected function prepareCrudConfig() {
if (!$this->user->can('WarehouseAdmin')) {
$this->columns[2]['table'] = false;
@@ -62,5 +59,6 @@ class WarehouseEShopController extends TTCrud {
"total_pages" => ceil($filteredAvailable / $perPage),
"per_page" => $perPage,
"filtered_available" => $filteredAvailable,
"total_rows" => $totalRows]]); }
"total_rows" => $totalRows]]);
}
}

View File

@@ -23,6 +23,8 @@ class WarehouseEShopOrderController extends TTCrud {
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']]
];
protected array $permissionCheck = ['WarehouseEShop'];
protected array $additionalActions = [
['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'],
['key' => 'showTrackingHistory', 'title' => 'Tracking Historie', 'class' => 'fas fa-truck text-primary'],
@@ -38,10 +40,6 @@ class WarehouseEShopOrderController extends TTCrud {
];
//@formatter:on
public function permissionCheck(): bool {
return $this->user->can(["WarehouseEShop"]);
}
protected function customRowsHandler($rows): array {
$statusToText = [
'new' => 'Neu',

View File

@@ -19,7 +19,7 @@ class WarehouseHistoryController {
WarehouseHistoryModel::create(['table' => $mod,
'row_id' => $postData['id'],
'key' => $key,
'old_value' => $currentData->$key,
'old_value' => $currentData->$key ?? '',
'new_value' => $value,
'note' => '',
'user_id' => $me->id,

View File

@@ -7,23 +7,26 @@ class WarehouseOrderController extends TTCrud {
protected array $columns = [
['key' => 'id', 'text' => 'ID', 'modal' => false],
['key' => 'distributorId', 'text' => 'Lieferant', 'required' => true, 'type' => 'autocomplete','table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'],'modal' => [
'apiUrl' => 'WarehouseDistributor/autocomplete','items' => 'WarehouseDistributor/autocomplete', 'type' => 'autocomplete']],
['key' => 'extRef', 'text' => 'Externe Referenz', 'required' => false],
['key' => 'intRef', 'text' => 'Interne Referenz', 'required' => false],
['key' => 'status', 'text' => 'Status', 'required' => true, 'modal' => ['type' => 'select', 'items' => [
['value' => 'new', 'text' => 'Neu'],
['value' => 'accepted', 'text' => 'An Lieferant übergeben'],
['value' => 'sent', 'text' => 'Gesendet'],
['value' => 'done', 'text' => 'Erledigt'],
]]],
['key' => 'trackingNumber', 'text' => 'Trackingnummer', 'required' => false],
['key' => 'sum', 'text' => 'Summe', 'required' => true, 'modal' => false, 'table' => ['filter' => 'numberRange']],
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false, 'filter' => 'datetime'],
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'table' => ['filter' => 'select'], 'modal' => ['type' => 'select', 'items' => []]],
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
['key' => 'orderNumber', 'text' => 'Bestellnummer', 'required' => true, 'modal' => false],
['key' => 'delAddrCity', 'text' => 'Stadt', 'required' => true, 'modal' => false],
['key' => 'delAddrEMail', 'text' => 'E-Mail', 'required' => true, 'modal' => false],
['key' => 'delAddrLine', 'text' => 'Adresse', 'required' => true, 'modal' => false],
['key' => 'delAddrName', 'text' => 'Name', 'required' => true, 'modal' => false],
['key' => 'delAddrPLZ', 'text' => 'PLZ', 'required' => true, 'modal' => false],
['key' => 'editor', 'text' => 'Bearbeiter', 'required' => true, 'modal' => false],
['key' => 'note', 'text' => 'Notiz', 'required' => true, 'modal' => false],
['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'modal' => false],
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false],
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select']],
['key' => 'actions',
'text' => 'Aktionen',
'required' => false,
'modal' => false,
'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
];
protected array $permissionCheck = ['WarehouseAdmin'];
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']];
protected array $infoMessages = ['create' => 'Bestellung wurde erfolgreich erstellt.',
@@ -31,72 +34,36 @@ class WarehouseOrderController extends TTCrud {
'delete' => 'Bestellung wurde gelöscht',
'noChanges' => 'Keine Änderungen',];
public function permissionCheck(): bool {
return $this->user->can(["WarehouseEShop"]);
protected function beforeCreate(): bool {
$currentCount = WarehouseOrderModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]);
$this->postData['orderNumber'] = 'PO' . date('Y') . '-' . str_pad($currentCount + 1, 4, '0', STR_PAD_LEFT);
return true;
}
protected function prepareCrudConfig() {
// Fill Users in createBy column
$column = array_search('createBy', array_column($this->columns, 'key'));
$this->columns[$column]['modal']['items'] = array_map(function ($user) {
return ['value' => intval($user->id), 'text' => $user->name];
}, UserModel::search());
protected function getArticleDistributorDataAction() {
$data = [];
$article = $this->request->articleId;
}
protected function createOrderAction() {
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$json = json_decode(file_get_contents('php://input'), true);
$orders = $json;
$orderIds = [];
foreach ($orders as $order) {
$distributor = $order['distributor'][0];
$orderAmount = $order['orderAmount'];
$orders = $order['orders'];
$order = [
'distributorId' => $distributor['id'],
'extRef' => null,
'status' => 'new',
'trackingNumber' => null,
'sum' => $orderAmount,
'create' => time(),
'createBy' => $this->user->id,
];
$orderId = WarehouseOrderModel::create($order);
$orderIds[] = $orderId;
foreach ($orders as $orderItem) {
$article = WarehouseArticleModel::get($orderItem['articleId']);
WarehouseEShopOrderItemModel::create([
'orderId' => $orderId,
'articleId' => $orderItem['articleId'],
'quantity' => $orderItem['amount'],
'price' => $article->cheapestPurchasePrice,
]);
if ($this->request->allDistributor === 'true') {
foreach (WarehouseDistributorModel::getAll() as $distributor) {
$data[] = [
'id' => $distributor->id,
'name' => $distributor->name,
];
}
} elseif (!empty($article)) {
foreach (WarehouseArticleDistributorModel::getAll(['articleId' => $this->request->articleId]) as $distributor) {
$data[] = [
'id' => $distributor->distributorId,
'name' => WarehouseDistributorModel::get($distributor->distributorId)->name,
'purchasePrice' => $distributor->purchasePrice,
'externalArticleNumber' => $distributor->externalArticleNumber,
];
}
}
self::returnJson(['success' => true, 'message' => $this->infoMessages['create'], 'ids' => $orderIds]);
}
protected function getOrderItemsAction() {
$orderItems = WarehouseEShopOrderItemModel::getAll(['orderId' => $this->request->id]);
// also get the article name of the order items
foreach ($orderItems as $key => $orderItem) {
$article = WarehouseArticleModel::get($orderItem->articleId);
$orderItem->articleName = $article->title;
}
self::returnJson($orderItems);
self::returnJson($data);
}
protected function beforeUpdate($postData): bool {

View File

@@ -1,28 +1,36 @@
<?php
//TODO: fix phpdoc
/**
* @property int $id
* @property 'new'|'accepted'|'sent'|'done' $status
* @property 'singleAddress'|'multipleAddresses' $deliveryMode
* @property string $deliveryAddressName
* @property string $deliveryAddressLine
* @property string $deliveryAddressPLZ
* @property string $deliveryAddressCity
* @property int $create
* @property int $createBy
*/
// id, distributorId, intRef, extRef, status, trackingNumber, create, createBy
/**
* Class WarehouseOrderModel
*
* Represents a warehouse order with delivery details and related metadata.
*
* @property int $id Unique identifier for the warehouse order
* @property string $orderNumber Unique order number
* @property string $delAddrCity City of the delivery address
* @property string $delAddrEMail Email associated with the delivery address
* @property string $delAddrLine Line of the delivery address
* @property string $delAddrName Name associated with the delivery address
* @property string $delAddrPLZ Postal code of the delivery address
* @property int $editor ID of the editor who last modified the order
* @property string $note Additional notes for the order
* @property string $positions Details about positions in the order
* @property int $create Timestamp of the order creation
* @property int $createBy ID of the user who created the order
*/
class WarehouseOrderModel extends TTCrudBaseModel {
public int $id;
public int $distributorId;
public ?string $intRef;
public ?string $extRef;
public float $sum;
public string $status;
public ?string $trackingNumber;
public string $orderNumber;
public string $delAddrCity;
public string $delAddrEMail;
public string $delAddrLine;
public string $delAddrName;
public string $delAddrPLZ;
public int $editor;
public string $note;
public string $positions;
public int $create;
public int $createBy;
}
?>

View File

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

View File

@@ -1,16 +0,0 @@
<?php
/**
* @property int $id
* @property int $orderId
* @property int $articleId
* @property int $quantity
* @property int $price
*/
class WarehouseOrderItemModel extends TTCrudBaseModel {
public int $id;
public int $orderId;
public int $articleId;
public int $quantity;
public float $price;
}

View File

@@ -41,7 +41,11 @@ class WarehouseOrderRequestController extends TTCrud {
'table' => ['filter' => 'select'],
'modal' => ['type' => 'select', 'items' => []]],
['key' => 'warehouseLocation', 'text' => 'Lagerort', 'required' => false, 'type' => 'varchar'],
['key' => 'canceled', 'text' => 'Storniert', 'required' => false, 'modal' => ['visible' => false, 'type' => 'select', 'items' => [['value' => 0, 'text' => 'Nein'], ['value' => 1, 'text' => 'Ja']]], 'table' => ['filter' => 'select']],
['key' => 'canceled',
'text' => 'Storniert',
'required' => false,
'modal' => ['visible' => false, 'type' => 'select', 'items' => [['value' => 0, 'text' => 'Nein'], ['value' => 1, 'text' => 'Ja']]],
'table' => ['filter' => 'select']],
['key' => 'note', 'text' => 'Notiz', 'required' => false, 'type' => 'textarea'],
['key' => 'actions',
'text' => 'Aktionen',
@@ -50,6 +54,8 @@ class WarehouseOrderRequestController extends TTCrud {
'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
];
protected array $permissionCheck = ['WarehouseUser'];
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']];
protected array $infoMessages = ['create' => 'Bestellwunsch wurde erstellt.',
@@ -59,10 +65,6 @@ class WarehouseOrderRequestController extends TTCrud {
protected array $additionalJSVariables = ['BASE_URL' => '/WarehouseOrderRequest', 'WAREHOUSE_ADMIN' => true];
public function permissionCheck(): bool {
return $this->user->can(["WarehouseUser"]);
}
protected function prepareCrudConfig() {
// Fill Users in createBy column
$userArray = array_map(function ($user) {
@@ -96,10 +98,10 @@ class WarehouseOrderRequestController extends TTCrud {
}
protected function customAutoCompleteWare($value) {
if (!is_numeric($value)) return [ 'id' => $value, 'title' => $value];
if (!is_numeric($value)) return ['id' => $value, 'title' => $value];
$article = WarehouseArticleModel::get(intval($value));
return [ 'id' => $article->id, 'title' => $article->title ];
return ['id' => $article->id, 'title' => $article->title];
}
protected function beforeUpdate($postData): bool {

View File

@@ -0,0 +1,57 @@
<?php declare(strict_types = 1);
use Phinx\Migration\AbstractMigration;
final class WarehouseModify10 extends AbstractMigration {
public function up(): void {
if ($this->getEnvironment() == "thetool") {
// Drop the existing tables
$this->table("WarehouseOrder")->drop()->save();
$this->table("WarehouseOrderItem")->drop()->save();
// Create the new WarehouseOrder table
$table = $this->table("WarehouseOrder", ["id" => false, "primary_key" => ["id"]]);
$table->addColumn("id", "integer", ["identity" => true])
->addColumn("orderNumber", "string", ["null" => false, "limit" => 255])
->addColumn("delAddrCity", "string", ["null" => false, "limit" => 255])
->addColumn("delAddrEMail", "string", ["null" => false, "limit" => 255])
->addColumn("delAddrLine", "string", ["null" => false, "limit" => 255])
->addColumn("delAddrName", "string", ["null" => false, "limit" => 255])
->addColumn("delAddrPLZ", "string", ["null" => false, "limit" => 255])
->addColumn("editor", "integer", ["null" => false])
->addColumn("note", "text", ["null" => false])
->addColumn("positions", "text", ["null" => false])
->addColumn("create", "integer", ["null" => false])
->addColumn("createBy", "integer", ["null" => false])
->create();
}
}
public function down(): void {
if ($this->getEnvironment() == "thetool") {
$this->table("WarehouseOrder")->drop()->save();
$WarehouseOrder = $this->table("WarehouseOrder", ["signed" => true]);
$WarehouseOrder
->addColumn('distributorId', 'integer', ['null' => false])
->addColumn('intRef', 'string', ['null' => true])
->addColumn('extRef', 'string', ['null' => true])
->addColumn('status', 'enum', ['values' => ['new', 'accepted', 'sent', 'done'], 'null' => false, 'default' => 'new'])
->addColumn('sum', 'float', ['null' => true])
->addColumn('trackingNumber', 'string', ['null' => true])
->addColumn('create', 'integer', ['null' => false, 'default' => 1728541890])
->addColumn('createBy', 'integer', ['null' => false, 'default' => 1])
->create();
$WarehouseOrderItem = $this->table("WarehouseOrderItem", ["signed" => true]);
$WarehouseOrderItem
->addColumn('orderId', 'integer', ['null' => false])
->addColumn('articleId', 'integer', ['null' => false])
->addColumn('quantity', 'integer', ['null' => false])
->addColumn('price', 'float', ['null' => false])
->addForeignKey('orderId', 'WarehouseOrder', 'id')
->addForeignKey('articleId', 'WarehouseArticle', 'id')
->create();
}
}
}

View File

@@ -25,11 +25,8 @@ class TTCrud extends mfBaseController {
$this->user = $me;
$this->layout()->set('me', $me);
if (method_exists($this, 'permissionCheck')) {
$allowed = $this->permissionCheck();
if (!$allowed) {
$this->redirect("Dashboard");
}
if (isset($this->permissionCheck) && !$me->can($this->permissionCheck)) {
$this->redirect("Dashboard");
} else if (!$me->is(["Admin"])) {
$this->redirect("Dashboard");
}
@@ -45,42 +42,36 @@ class TTCrud extends mfBaseController {
* @return array
*/
protected function getCheckArray(): array {
$checkArray = [];
foreach ($this->columns as $column) {
$checkArray[$column['key']] = ['required' => $column['required'] ?? false,
'required_length' => $column['required_length'] ?? 0,
'title' => $column['text'] ?? $column['key'],
'regex' => $column['regex'] ?? false];
}
return $checkArray;
return array_map(fn($column) => [
'required' => $column['required'] ?? false,
'required_length' => $column['required_length'] ?? 0,
'title' => $column['text'] ?? $column['key'],
'regex' => $column['regex'] ?? false
], $this->columns);
}
protected function indexAction() {
$this->layout()->set('additionalJS', ['js/pages/WarehouseHistory/WarehouseHistoryModal.js']);
$customJsFile = defined('BASEDIR') ? BASEDIR . "/public/js/pages/{$this->mod}/{$this->mod}.js" : null;
$pageName = (defined('BASEDIR') && file_exists(BASEDIR . "/public/js/pages/{$this->mod}/{$this->mod}.js"))
? $this->mod
: "DefaultCrudView";
if ($customJsFile && file_exists($customJsFile)) {
$pageName = $this->mod;
} else {
$pageName = "DefaultCrudView";
}
$JS_VARIABLES = [
"CRUD_CONFIG" => $this->getCrudConfig(),
"CREATE_URL" => $this::getUrl("{$this->mod}/create"),
"TABLE_URL" => $this::getUrl("{$this->mod}/get"),
"UPDATE_URL" => $this::getUrl("{$this->mod}/update"),
"DELETE_URL" => $this::getUrl("{$this->mod}/delete"),
"USER_ID" => $this->user->id
];
$JS_VARIABLES = ["CRUD_CONFIG" => $this->getCrudConfig(),
"CREATE_URL" => $this::getUrl($this->mod . "/create"),
"TABLE_URL" => $this::getUrl($this->mod . "/get"),
"UPDATE_URL" => $this::getUrl($this->mod . "/update"),
"DELETE_URL" => $this::getUrl($this->mod . "/delete"),
"USER_ID" => $this->user->id];
if ($this->additionalJSVariables && is_array($this->additionalJSVariables)) {
$JS_VARIABLES = array_merge($JS_VARIABLES, $this->additionalJSVariables);
}
if (!empty($this->additionalJSVariables) && is_array($this->additionalJSVariables)) $JS_VARIABLES = array_merge($JS_VARIABLES, $this->additionalJSVariables);
Helper::renderVue($this, $pageName, $this->headerTitle, $JS_VARIABLES);
}
/**
* Returns the configuration for the CRUD component for the Vue component.
* @return array
@@ -90,6 +81,13 @@ class TTCrud extends mfBaseController {
$this->prepareCrudConfig();
}
$column = array_search('createBy', array_column($this->columns, 'key'));
if ($column !== false) {
$this->columns[$column]['modal']['items'] = array_map(function ($user) {
return ['value' => intval($user->id), 'text' => $user->name];
}, UserModel::search(['employee' => true]));
}
$columns = array_map(function ($column) {
if (isset($column['type']) && (!isset($column['modal']) || !isset($column['modal']['type']))) {
@@ -186,7 +184,6 @@ class TTCrud extends mfBaseController {
}
protected function createAction() {
// if this->model has property createBy, set it to the current user id and create to current epoch time
if (property_exists($this->model, 'createBy')) {
$this->postData['createBy'] = $this->user->id;
}
@@ -194,12 +191,12 @@ class TTCrud extends mfBaseController {
$this->postData['create'] = time();
}
Helper::validateArray($this->postData, $this->checkArray);
if (method_exists($this, 'beforeCreate') && !$this->beforeCreate($this->postData)) {
self::returnJson(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']);
}
Helper::validateArray($this->postData, $this->checkArray);
$id = $this->model::create($this->postData);
if (method_exists($this, 'afterCreate')) {
@@ -212,12 +209,12 @@ class TTCrud extends mfBaseController {
}
protected function updateAction() {
// if (property_exists($this->model, 'createBy') && !isset($this->postData['createBy'])) {
// $this->postData['createBy'] = $this->user->id;
// }
// if (property_exists($this->model, 'create') && !isset($this->postData['create'])) {
// $this->postData['create'] = time();
// }
if (property_exists($this->model, 'create') && isset($this->postData['create'])) {
$this->postData['create'] = $this->model::get($this->postData['id'])->create;
}
if (property_exists($this->model, 'createBy') && isset($this->postData['createBy'])) {
$this->postData['createBy'] = $this->model::get($this->postData['id'])->createBy;
}
Helper::validateArray($this->postData, array_merge($this->checkArray, ['id' => ['required' => true]]));

View File

@@ -47,6 +47,7 @@ $jsFiles = [
"plugins/vue/tt-components/tt-number-range.js",
"plugins/vue/tt-components/tt-checkbox.js",
"plugins/vue/tt-components/tt-textarea.js",
"plugins/vue/tt-components/tt-position-manager.js",
];

View File

@@ -0,0 +1,27 @@
.warehouse-order-modal-positions-entry-container {
display: grid;
grid-template-columns: 2fr 1fr 1fr 0.5fr 1fr 1fr 0.5fr;
grid-gap: 10px;
}
.warehouse-order-modal-positions-entry-actions {
display: flex;
flex-direction: column;
justify-content: center;
padding-top: 13px;
}
@media (min-width: 992px) {
.modal-lg, .modal-xl {
/*max width either 90% or 1120px*/
max-width: min(90vw, 1120px) !important;
}
}
@media (max-width: 992px) {
.warehouse-order-modal-positions-entry-container {
display: grid;
grid-template-columns: 1fr 1fr !important;
grid-gap: 10px;
}
}

View File

@@ -1,70 +1,169 @@
// noinspection JSUnusedLocalSymbols
Vue.component('warehouse-order-modal', {
props: {
id: {type: [String, Number], required: true},
mode: {type: String, default: 'sign'}
},
template: `
<tt-modal :show="true"
@submit="submit"
:delete="id !== 'create'"
:title="id === 'create' ? 'Bestellung erstellen' : \`Bestellung #\${id} bearbeiten\`"
@update:show="$emit('close')">
<div style="width: 99%">
<h4 class="text-center">Bestelldetails</h4>
<tt-select label="Bearbeiter (XINON)"
:options="window.TT_CONFIG.CRUD_CONFIG.columns.find(column => column.key === 'createBy')?.modal.items"
sm
row
v-model="order.editor"/>
<hr>
<h4 class="text-center">Positionen</h4>
<tt-positions-manager
ref="positionsManager"
v-model="order.positions"
:config="positionsConfig"
@updateField-article="fetchDistributors"
@updateField-distributorId="fetchDistributorData"
/>
<hr>
<h4 class="text-center">Lieferadresse</h4>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 2fr 2fr 1fr 1fr 2fr;">
<tt-input label="Name" v-model="order.delAddrName" sm/>
<tt-input label="Straße" v-model="order.delAddrLine" sm/>
<tt-input label="PLZ" v-model="order.delAddrPLZ" sm/>
<tt-input label="Ort" v-model="order.delAddrCity" sm/>
<tt-input label="E-Mail" v-model="order.delAddrEMail" sm/>
</div>
<hr>
<tt-textarea label="Notiz" v-model="order.note" sm row/>
</div>
</tt-modal>
`,
data() {
return {
window: window,
positionsConfig: {
customOrdering: 'distributorId',
fields: {
article: {
type: 'autocomplete',
label: 'Artikel',
apiUrl: '/WarehouseArticle/autoComplete',
customFieldReference: 'WarehouseArticle',
},
distributorId: {type: 'select', label: 'Lieferant', options: [], customFieldReference: 'WarehouseDistributor'},
distributorArticleNumber: {type: 'input', label: 'Lieferant Art-Nr.'},
amount: {type: 'input', label: 'Menge', inputType: 'number'},
buyPrice: {type: 'input', label: 'Einkaufspreis', inputType: 'number'},
verwendung: {type: 'input', label: 'Verwendung'},
},
validateForm: (formData) => {
const fields = [
{key: 'amount', message: 'Bitte füllen Sie die Menge aus'},
{key: 'distributorId', message: 'Bitte füllen Sie den Lieferanten aus'},
{key: 'article', message: 'Bitte füllen Sie den Artikel aus'},
{key: 'buyPrice', message: 'Bitte füllen Sie den Einkaufspreis aus'}
];
for (const field of fields) {
if (!formData[field.key]) {
window.notify('error', field.message);
return false;
}
}
return true;
},
},
order: {
delAddrName: 'XINON GmbH',
delAddrLine: 'Fladnitz im Raabtal 150',
delAddrPLZ: '8322',
delAddrCity: 'Studenzen',
delAddrEMail: 'einkauf@xinon.at',
note: '',
editor: window.TT_CONFIG['USER_ID'],
positions: [],
}
}
},
async mounted() {
if (this.id !== 'create') {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.id}});
response.data.positions = JSON.parse(response.data.positions);
this.order = response.data;
}
},
methods: {
async submit() {
if (this.order.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
if (this.id === 'create') {
const distributorIds = [...new Set(this.order.positions.map(position => position.distributorId))];
for (const distributorId of distributorIds) {
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/create`, {
...this.order,
distributorId,
positions: this.order.positions.filter(position => position.distributorId === distributorId)
}
);
if (response.data.success) window.notify('success', response.data.message ?? 'Bestellung erfolgreich erstellt');
else window.notify('error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
}
} else {
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/update`, this.order);
if (response.data.success) window.notify('success', response.data.message ?? 'Bestellung erfolgreich aktualisiert');
else window.notify('error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
}
this.$emit('close');
},
async fetchDistributors(article) {
const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getArticleDistributorData`;
const params = typeof article === 'string' ? {allDistributor: true} : {articleId: article};
const response = await axios.get(url, {params});
this.positionsConfig.fields.distributorId.options = response.data.map(distributor => ({
value: distributor.id,
text: distributor.name,
externalArticleNumber: distributor.externalArticleNumber || null,
purchasePrice: distributor.purchasePrice || null,
}));
},
async fetchDistributorData(distributorId) {
if (distributorId && typeof this.$refs.positionsManager.formData.article === 'number') {
const distributor = this.positionsConfig.fields.distributorId.options.find(distributor => parseInt(distributor.value) ===
parseInt(distributorId));
this.$refs.positionsManager.updateField('distributorArticleNumber', distributor.externalArticleNumber);
this.$refs.positionsManager.updateField('buyPrice', distributor.purchasePrice);
}
},
},
});
Vue.component('warehouse-order', {
//language=Vue
template: `
<tt-card>
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"
ref="table">
<template v-slot:create="{ row }">
{{ window.moment(row.create * 1000).format('DD.MM.YYYY HH:mm:ss') }}
</template>
<template v-slot:sum="{ row }">
<div style="text-align: right">{{ row.sum.toFixed(2) }} €</div>
</template>
<template v-slot:expandedRow="{ row }">
<div class="lazy-loading" :data-row-id="row.id">
<tt-loader v-if="orderLazyLoad[row.id] === true"/>
<div v-else>
<ul class="list-group">
<li class="list-group-item" v-for="item in orderLazyLoad[row.id]">
{{ item.quantity }}x {{ item.articleName }} - {{ item.price.toFixed(2) }} €
</li>
</ul>
</div>
</div>
</template>
<warehouse-order-modal v-if="orderModalId" :id="orderModalId" @close="orderModalId = null;$refs.table.$refs.table.refreshTable()"/>
<button @click="orderModalId = 'create'" class="btn btn-primary">Bestellung erstellen</button>
<tt-table-crud emit-edit @edit="orderModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }"><span>Work in Progress</span></template>
</tt-table-crud>
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
</tt-card>
`, data() {
return {
window: window, historyModal: false, historyModalId: null, observer: null, orderLazyLoad: {},
}
}, mounted() {
this.observer = new MutationObserver((mutations) => {
const lazyLoadingElements = document.querySelectorAll('.lazy-loading');
console.log(lazyLoadingElements);
// check row id and check if it is already defined in orderLazyLoad else alert('loading')
// if it is defined do nothing
for (const element of lazyLoadingElements) {
if (element.dataset.rowId in this.orderLazyLoad) {
continue;
}
this.loadOrder(element.dataset.rowId);
}
})
this.observer.observe(document.querySelector('.tt-table-container'), {childList: true, subtree: true,});
}, methods: {
async loadOrder(rowId) {
this.orderLazyLoad[rowId] = true;
// use BASE_PATH . /WarehouseOrder/getOrderItems?id= + rowId
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getOrderItems?id=${rowId}`);
console.log(response.data);
this.orderLazyLoad[rowId] = response.data;
// force re-render of the table
this.$refs.table.$forceUpdate();
window: window,
orderModalId: null,
}
}, beforeDestroy() {
this.observer.disconnect();
}
})
},
})

View File

@@ -0,0 +1,64 @@
.positions-manager {
padding: 1rem;
font-family: sans-serif;
}
.positions-manager .form-group {
margin-bottom: 0;
}
.positions-manager .form-container {
display: flex;
align-items: center; /* Vertically center */
justify-content: flex-start;
gap: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #ddd;
}
.positions-manager .form-container .button-wrapper {
align-self: flex-end; /* Align button container at the bottom */
}
.positions-manager .form-container [class*="tt-input"],
.positions-manager .form-container [class*="tt-checkbox"] {
flex: 1 1 200px; /* Flexible width with a minimum of 200px */
min-width: 200px;
}
.positions-manager .form-container .btn {
align-self: flex-end; /* Align button at the bottom of the form area */
}
.positions-manager table {
width: 100%;
margin-top: 1rem;
border-collapse: collapse;
}
.positions-manager table thead th {
background-color: #f4f4f4;
border-bottom: 2px solid #ddd;
padding: 8px;
text-align: left;
}
.positions-manager table th,
.positions-manager table td {
padding: 8px;
vertical-align: middle; /* Vertically center text */
}
.positions-manager table tbody tr:nth-child(even) {
background-color: #f9f9f9; /* Alternate row color */
}
.positions-manager table .btn {
margin-right: 0.5rem;
}
.positions-manager .form-container .btn-sm,
.positions-manager table .btn.btn-sm {
padding: 0.3rem 0.6rem; /* Small button padding */
font-size: 0.875rem;
}

View File

@@ -42,7 +42,7 @@ Vue.component('tt-autocomplete', {
Einträge werden geladen...
</li>
<template v-show="showSuggestions && displayingItems.length > 0 && isLoading !== true">
<template v-show="showSuggestions && displayingItems.length && isLoading !== true">
<li
v-for="(item) in displayingItems.slice(0, 10)"
:key="item.value"
@@ -104,9 +104,15 @@ Vue.component('tt-autocomplete', {
this.disableIDFetch = false;
return;
}
if (newValue) {
this.$emit('input', newValue);
this.value = newValue;
}
if (oldValue && !newValue) {
this.$emit('input', '');
this.displayValue = '';
}
if (this.value && this.apiUrl) {
@@ -120,14 +126,13 @@ Vue.component('tt-autocomplete', {
const selectedItem = this.items.find(item => item.value === this.value);
this.displayValue = selectedItem ? selectedItem.text : '';
} else {
this.$emit('input', '');
if (this.returnText === false && !(typeof this.value === 'undefined' || this.value === '')) this.$emit('input', '');
this.displayValue = this.displayValue.replace(this.oldDisplayValue, '');
}
},
onInput(event) {
this.displayValue = event.target.value;
this.$emit('input', '');
this.$emit('input', this.returnText ? this.displayValue : '');
if (this.returnText) this.$emit('input', this.displayValue);
this.fetchSuggestions();
}, onFocus() {
this.showSuggestions = true;
@@ -136,10 +141,8 @@ Vue.component('tt-autocomplete', {
this.showSuggestions = false;
}, 200);
}, fetchSuggestions() {
if (this.displayValue.length < 3) {
this.$set(this, 'displayingItems', []);
return this.displayingItems = [];
}
this.$set(this, 'displayingItems', []);
if (this.displayValue.length < 3) return;
if (!this.apiUrl) {

View File

@@ -0,0 +1,163 @@
Vue.component('tt-positions-manager', {
props: {
value: {type: Array, required: false},
config: {type: Object, required: true},
},
data() {
return {
window: window,
positions: this.value,
formData: {},
selectedIndex: null,
resolvingFields: {},
}
},
template: `
<div class="positions-manager">
<div class="form-container">
<template v-for="(field, key) in config.fields">
<slot :name="key" v-bind:field="field" v-bind:value="formData[key]">
<tt-input
v-if="field.type === 'input'"
:label="field.label"
v-model="formData[key]"
@input="$emit('updateField-' + key, $event)"
sm
:type="field.inputType || 'text'"
/>
<tt-autocomplete
v-else-if="field.type === 'autocomplete'"
:label="field.label"
v-model="formData[key]"
@input="$emit('updateField-' + key, $event); window.console.log($event)"
:api-url="window.TT_CONFIG['BASE_PATH'] + field.apiUrl"
sm
/>
<tt-checkbox
v-else-if="field.type === 'checkbox'"
:label="field.label"
@input="$emit('updateField-' + key, $event)"
sm
v-model="formData[key]"
/>
<tt-select
v-else-if="field.type === 'select'"
:label="field.label"
@input="$emit('updateField-' + key, $event); window.console.log('updatefield-' + key, $event)"
sm
v-model="formData[key]"
:options="field.options"
/>
</slot>
</template>
<div class="button-wrapper">
<tt-button @click="saveEntry" sm :additional-class="selectedIndex === null ? 'btn-primary' : 'btn-success'"
:text="selectedIndex === null ? 'Hinzufügen' : 'Aktualisieren'"/>
</div>
</div>
<table class="table table-striped table-sm">
<thead>
<tr>
<th v-for="field in config.fields">{{ field.label }}</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(position, index) in positions" :key="index">
<td v-for="(field, key) in config.fields">
<template v-if="resolvingFields[index + key] === true">
<div class="d-flex justify-content-center align-items-center">
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</template>
<span v-else-if="resolvingFields[index + key]">{{ resolvingFields[index + key] }}</span>
<span v-else>{{ formatFieldValue(position[key], field) }}</span>
</td>
<td>
<button @click="editEntry(index)" class="btn btn-sm btn-primary">Editieren</button>
<button @click="deleteEntry(index)" class="btn btn-sm btn-danger">Löschen</button>
</td>
</tr>
</tbody>
</table>
</div>
`,
methods: {
updateField(key, value) {
this.$set(this.formData, key, value);
},
async saveEntry() {
if (this.config.validateForm && !await this.config.validateForm(this.formData)) return;
if (this.selectedIndex === null) this.positions.push(this.formData);
else this.$set(this.positions, this.selectedIndex, this.formData);
if (this.config.customOrdering) {
this.positions.sort((a, b) => a[this.config.customOrdering] - b[this.config.customOrdering]);
}
this.$emit('input', this.positions);
this.resetForm();
},
editEntry(index) {
this.selectedIndex = index;
this.formData = {...this.positions[index]};
},
deleteEntry(index) {
this.positions.splice(index, 1);
this.$emit('input', this.positions);
},
resetForm() {
this.formData = {};
this.selectedIndex = null;
},
formatFieldValue(value, field) {
if (field.formatter) return field.formatter(value);
return value;
},
async resolveFields() {
for (let i = 0; i < this.positions.length; i++) {
for (let key in this.config.fields) {
if (this.config.fields[key].customFieldResolver) {
this.$set(this.resolvingFields, i + key, true);
const textValue = await this.config.fields[key].customFieldResolver(this.positions[i][key]);
this.$set(this.resolvingFields, i + key, textValue);
} else if (this.config.fields[key].customFieldReference) {
this.$set(this.resolvingFields, i + key, true);
if (this.config.fields[key].customFieldReference) {
const entry = await axios.get(window.TT_CONFIG['BASE_PATH'] +
'/' +
this.config.fields[key].customFieldReference +
'/getById?id=' +
this.positions[i][key]);
const textValue = entry.data.name ?? entry.data.title ?? entry.data.text ?? '[E] Key not found';
console.log(textValue);
this.$set(this.resolvingFields, i + key, textValue);
} else this.$set(this.resolvingFields, i + key, '');
}
}
}
}
},
created() {
if (this.config.customMethods) Object.assign(this, this.config.customMethods);
},
watch: {
positions: {
handler() {
this.resolveFields().then();
},
deep: true
},
value: {
handler() {
this.positions = this.value;
},
deep: true
}
}
});