Merge branch 'Warehouse/improve2' into 'master'
new update See merge request fronk/thetool!1252
This commit is contained in:
@@ -10,22 +10,20 @@ class WarehouseArticleController extends TTCrud {
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true, 'table' => ['priority' => 9]],
|
||||
['key' => 'articleNumber', 'text' => 'Nr.', 'required' => true],
|
||||
['key' => 'description', 'text' => 'Beschreibung', 'required' => true],
|
||||
['key' => 'category', 'text' => 'Kategorie', 'required' => true],
|
||||
['key' => 'unit', 'text' => 'Einheit', 'required' => true,'table' => false], // Boolean value
|
||||
['key' => 'revenueAccount', 'text' => 'Erlöskonto', 'required' => true,'modal' =>
|
||||
['type' => 'select', 'items' => [['value' => 0, 'text' => 'Dienstleistungen'], ['value' => 1, 'text' => 'Handelswaren']]
|
||||
], 'table' => false], // Boolean value
|
||||
['key' => 'category_id', 'text' => 'Kategorie', 'required' => true, 'modal' => ['type' => 'select', 'items' => []], 'table' => ['filter' => 'select']],
|
||||
['key' => 'unit', 'text' => 'Einheit', 'required' => true,'table' => false],
|
||||
['key' => 'revenueAccount', 'text' => 'Erlöskonto', 'required' => true,'modal' => ['type' => 'select', 'items' => [['value' => 0, 'text' => 'Dienstleistungen'], ['value' => 1, 'text' => 'Handelswaren']]], 'table' => false],
|
||||
['key' => 'cheapestPurchasePrice', 'text' => 'Einkauf', 'modal' => false, 'table' => ['class' => 'text-center', 'suffix' => ' €']],
|
||||
['key' => 'cheapestSellPrice', 'text' => 'Verkauf', 'modal' => false, 'table' => ['class' => 'text-center', 'suffix' => ' €']],
|
||||
['key' => 'warningAmount', 'text' => 'Warnmenge', 'required' => true,'modal' => ['type' => 'number'], 'table' => false], // Stock/inventory related
|
||||
['key' => 'criticalAmount', 'text' => 'Kritische Menge', 'required' => true,'modal' => ['type' => 'number'], 'table' => false], // Stock/inventory related
|
||||
['key' => 'isSerialDocumentation', 'text' => 'Seriennummern', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
|
||||
['key' => 'isEShop', 'text' => 'Ist E-Shop', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
|
||||
['key' => 'isEShopHide', 'text' => 'E-Shop Versteckt', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
|
||||
['key' => 'warningAmount', 'text' => 'Warnmenge', 'required' => true,'modal' => ['type' => 'number'], 'table' => false],
|
||||
['key' => 'criticalAmount', 'text' => 'Kritische Menge', 'required' => true,'modal' => ['type' => 'number'], 'table' => false],
|
||||
['key' => 'isSerialDocumentation', 'text' => 'Seriennummern', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false],
|
||||
['key' => 'isEShop', 'text' => 'Ist E-Shop', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false],
|
||||
['key' => 'isEShopHide', 'text' => 'E-Shop Versteckt', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 8]]
|
||||
];
|
||||
|
||||
protected array $autocompleteColumns = ['articleNumber', 'title', 'description', 'category'];
|
||||
protected array $autocompleteColumns = ['articleNumber', 'title', 'description'];
|
||||
|
||||
protected array $additionalActions = [['key' => 'openHistory','title' => 'Historie','class' => 'fas fa-history text-secondary']];
|
||||
// @formatter:on
|
||||
@@ -33,8 +31,12 @@ class WarehouseArticleController extends TTCrud {
|
||||
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true];
|
||||
|
||||
protected function prepareCrudConfig() {
|
||||
$categories = array_map(fn($category) => ['value' => $category->id, 'text' => $category->name], WarehouseCategory::getAll());
|
||||
$this->columns[array_search('category_id', array_column($this->columns, 'key'))]['modal']['items'] = $categories;
|
||||
|
||||
if ($this->user->can('WarehouseAdmin')) return;
|
||||
|
||||
|
||||
array_walk($this->columns, fn(&$col) => in_array($col['key'], ['actions', 'cheapestPurchasePrice', 'warningAmount', 'criticalAmount']) && $col['table'] = false);
|
||||
|
||||
$this->createText = false;
|
||||
|
||||
@@ -5,7 +5,7 @@ class WarehouseArticleModel extends TTCrudBaseModel {
|
||||
public string $title;
|
||||
public string $articleNumber;
|
||||
public string $description;
|
||||
public string $category;
|
||||
public ?int $category_id;
|
||||
public ?float $cheapestPurchasePrice;
|
||||
public ?string $cheapestSellPrice;
|
||||
public int $warningAmount;
|
||||
|
||||
@@ -95,4 +95,8 @@ class WarehouseArticlePriceController extends TTCrud {
|
||||
WarehouseArticleController::updateSellPrices($postData['articleId']);
|
||||
}
|
||||
|
||||
public function afterDelete($postData) {
|
||||
WarehouseArticleController::updateSellPrices($postData['articleId']);
|
||||
}
|
||||
|
||||
}
|
||||
23
application/WarehouseCategory/WarehouseCategory.php
Normal file
23
application/WarehouseCategory/WarehouseCategory.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
class WarehouseCategory extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $name;
|
||||
public string $description;
|
||||
public int $create;
|
||||
public int $create_by;
|
||||
public ?int $edit;
|
||||
public ?int $edit_by;
|
||||
}
|
||||
|
||||
// SQL:
|
||||
// CREATE TABLE `WarehouseCategory` (
|
||||
// `id` INT NOT NULL AUTO_INCREMENT,
|
||||
// `name` VARCHAR(255) NOT NULL,
|
||||
// `description` TEXT NOT NULL,
|
||||
// `create` INT NOT NULL,
|
||||
// `create_by` INT NOT NULL,
|
||||
// `edit` INT DEFAULT NULL,
|
||||
// `edit_by` INT DEFAULT NULL,
|
||||
// PRIMARY KEY (`id`)
|
||||
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
class WarehouseCategoryController extends TTCrud {
|
||||
protected string $headerTitle = 'Kategorien';
|
||||
protected string $createText = 'Kategorie erstellen';
|
||||
protected string $singleText = 'Kategorie';
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true,],
|
||||
['key' => 'description', 'text' => 'Beschreibung', 'required' => true],
|
||||
['key' => 'create', 'text' => 'Erstellt am', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
||||
['key' => 'create_by', 'text' => 'Erstellt von', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
||||
['key' => 'edit', 'text' => 'Bearbeitet am', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
||||
['key' => 'edit_by', 'text' => 'Bearbeitet von', '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', 'priority' => 10]],
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']];
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
|
||||
}
|
||||
}
|
||||
@@ -23,15 +23,16 @@ class WarehouseOfferController extends TTCrud {
|
||||
'modal' => false,
|
||||
'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
||||
];
|
||||
|
||||
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true];
|
||||
protected array $permissionCheck = ['WarehouseAdmin'];
|
||||
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']];
|
||||
protected array $additionalActions = [['key' => 'openpdf', 'title' => 'PDF öffnen', 'class' => 'fas fa-file-pdf', 'color' => 'primary']];
|
||||
|
||||
protected function prepareCrudConfig(): void {
|
||||
$editorColumnIndex = array_search('editor', array_column($this->columns, 'key'));
|
||||
$this->columns[$editorColumnIndex]['modal']['items'] = array_map(function ($user) {
|
||||
return ['value' => intval($user->id), 'text' => $user->name];
|
||||
}, UserModel::search(['employee' => true]));
|
||||
if (!$this->user->can('WarehouseAdmin')) $this->additionalJSVariables['WAREHOUSE_ADMIN'] = false;
|
||||
}
|
||||
|
||||
protected function beforeCreate(): bool {
|
||||
@@ -52,6 +53,7 @@ class WarehouseOfferController extends TTCrud {
|
||||
}
|
||||
|
||||
protected function createTemplateAction() {
|
||||
if (!$this->user->can('WarehouseAdmin')) self::sendError("Keine Berechtigung");
|
||||
$_POST = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$templateId = WarehouseOfferTemplateModel::create([
|
||||
@@ -67,7 +69,68 @@ class WarehouseOfferController extends TTCrud {
|
||||
self::returnJson(['success' => true, 'id' => $templateId]);
|
||||
}
|
||||
|
||||
protected function deleteTemplateAction() {
|
||||
if (!$this->user->can('WarehouseAdmin')) self::sendError("Keine Berechtigung");
|
||||
WarehouseOfferTemplateModel::delete($this->request->id);
|
||||
self::returnJson(['success' => true]);
|
||||
}
|
||||
|
||||
protected function getTemplatesAction() {
|
||||
self::returnJson(WarehouseOfferTemplateModel::getAll());
|
||||
}
|
||||
|
||||
public function createPDFAction($returnFilename = false, $idOverride = null) {
|
||||
//display errors
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$id = $idOverride ?? $this->request->id;
|
||||
if (strlen($id) < 1) self::sendError('ID fehlt');
|
||||
|
||||
$offer = WarehouseOfferModel::get($id);
|
||||
if (!$offer->id) self::sendError('Angebot nicht gefunden');
|
||||
|
||||
$positions = json_decode($offer->positions, true);
|
||||
$entries = [];
|
||||
|
||||
foreach ($positions as $position) {
|
||||
if (!isset($position['article'])) continue;
|
||||
$article = WarehouseArticleModel::get($position['article']);
|
||||
$position['articleText'] = WarehouseArticleModel::get($position['article'])->title;
|
||||
$position['articleDescription'] = $article->description === $article->title ? "" : $article->description;
|
||||
$position['articleUnit'] = $position['unit'] ?? $article->unit ?? 'Stk.';
|
||||
|
||||
if (isset($position['_group'])) {
|
||||
$entries[$position['_group']][] = $position;
|
||||
} else {
|
||||
$entries[''][] = $position;
|
||||
}
|
||||
}
|
||||
|
||||
$pdf_vars = [
|
||||
"offer" => $offer,
|
||||
"entries" => $entries,
|
||||
"bank_iban" => TT_INVOICE_BANK_IBAN,
|
||||
"bank_bic" => TT_INVOICE_BANK_BIC,
|
||||
"bank_bank" => TT_INVOICE_BANK_BANK,
|
||||
"bank_owner" => TT_INVOICE_BANK_OWNER
|
||||
];
|
||||
|
||||
$headerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseOffer/PDF_HEADER.html");
|
||||
$headerHtml = str_replace("{{ addressLine_header }}",
|
||||
// {{ addressLine_1 }} {{ addressLine_2 }} {{ addressLine_3 }} {{ addressLine_4 }} {{ externalReference }}
|
||||
$headerHtml = str_replace("{{ addressLine_1 }}", $offer->customerName, $headerHtml));
|
||||
$headerHtml = str_replace("{{ addressLine_2 }}", $offer->customerStreet, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_3 }}", $offer->customerZip . ' ' . $offer->customerCity, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_5 }}", $offer->customerVAT, $headerHtml);
|
||||
$headerHtml = str_replace("{{ externalReference }}", $offer->customerReference, $headerHtml);
|
||||
|
||||
|
||||
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*
|
||||
* Represents a warehouse offer template with key details.
|
||||
*
|
||||
* @property int $id Unique identifier for the template
|
||||
* @property string $templateName Name of the template
|
||||
* @property string $positions Details about positions in the offer
|
||||
* @property float $totalDiscount Total discount applied to the offer
|
||||
@@ -15,6 +16,7 @@
|
||||
*/
|
||||
class WarehouseOfferTemplateModel extends TTCrudBaseModel
|
||||
{
|
||||
public int $id;
|
||||
public string $templateName;
|
||||
public string $positions;
|
||||
public float $totalDiscount;
|
||||
|
||||
@@ -529,9 +529,6 @@ class WarehouseShippingNoteController extends TTCrud {
|
||||
return $distance * 2;
|
||||
}
|
||||
|
||||
$fromData = geocode($from);
|
||||
$toData = geocode($to);
|
||||
|
||||
$distance = route($from, $to);
|
||||
|
||||
$roundedDistanceKm = round($distance / 1000, 0);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseShippingNoteTextElement extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
class WarehouseShippingNoteTextElementController extends TTCrud {
|
||||
protected string $headerTitle = 'Lieferschein Textelemente';
|
||||
protected string $createText = 'Lieferschein Textelement erstellen';
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true],
|
||||
['key' => 'content', 'text' => 'Text', 'required' => true, 'modal' => []],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['filter' => 'datetime', 'class' => 'text-nowrap']],
|
||||
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => false, 'modal' => [
|
||||
'type' => 'select', 'items' => [],'visible' => false], ],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]],
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $infoMessages = ['create' => 'Lieferschein Textelement wurde erstellt',
|
||||
'update' => 'Lieferschein Textelement wurde aktualisiert',
|
||||
'delete' => 'Lieferschein Textelement wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen'];
|
||||
|
||||
protected function prepareCrudConfig() {
|
||||
// add all users to createBy column
|
||||
$this->columns[array_search('createBy', array_column($this->columns, 'key'))]['modal']['items'] = array_map(function ($user) {
|
||||
return ['value' => $user->id, 'text' => $user->name];
|
||||
}, UserModel::getAll());
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
$history = WarehouseHistoryModel::getByRowId($this->request->id, $this->mod);
|
||||
|
||||
$history = array_map(function ($item) {
|
||||
$item = (array) $item;
|
||||
|
||||
$item['columnHeader'] = $this->columns[array_search($item['key'], array_column($this->columns, 'key'))]['text'];
|
||||
return $item;
|
||||
}, $history);
|
||||
|
||||
self::returnJson($history);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
class WarehouseShippingNoteTextElementModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $title;
|
||||
public string $content;
|
||||
public int $create;
|
||||
public int $createBy;
|
||||
}
|
||||
|
||||
176
db/migrations/20250424100000_warehouse_modify_20.php
Normal file
176
db/migrations/20250424100000_warehouse_modify_20.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php /** @noinspection ALL */
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
use Phinx\Util\Literal; // Import Literal for raw SQL expressions
|
||||
|
||||
final class WarehouseModify20 extends AbstractMigration {
|
||||
public function up(): void {
|
||||
// Only run this migration in the 'thetool' environment
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
|
||||
// --- Start: Original WarehouseShippingNoteTextElement logic ---
|
||||
$WarehouseShippingNoteTextElementTable = $this->table("WarehouseShippingNoteTextElement");
|
||||
if ($WarehouseShippingNoteTextElementTable->exists()) {
|
||||
$WarehouseShippingNoteTextElementTable->drop()->save();
|
||||
}
|
||||
|
||||
$WarehouseShippingNoteTextElementTable = $this->table("WarehouseShippingNoteTextElement", ['id' => 'id', 'signed' => false]);
|
||||
if (!$WarehouseShippingNoteTextElementTable->exists()) {
|
||||
$WarehouseShippingNoteTextElementTable
|
||||
->addColumn("title", "string", ["limit" => 255])
|
||||
->addColumn("content", "text")
|
||||
// Use Literal::from('CURRENT_TIMESTAMP') or similar if your DB supports it for default timestamps
|
||||
// Using a fixed default like this might not be ideal
|
||||
->addColumn("create", "integer", ["default" => 1728541890])
|
||||
->addColumn("createBy", "integer", ["default" => 1])
|
||||
->create(); // Use create() instead of save() for new tables
|
||||
}
|
||||
// --- End: Original WarehouseShippingNoteTextElement logic ---
|
||||
|
||||
|
||||
// --- Start: New WarehouseArticle Category Migration Logic ---
|
||||
|
||||
// 1. Populate WarehouseCategory with distinct categories from WarehouseArticle
|
||||
// Using execute() for complex INSERT with SELECT, TRIM, UNIX_TIMESTAMP, and ON DUPLICATE KEY UPDATE
|
||||
// Assumes WarehouseCategory table exists with columns: name, description, create, create_by
|
||||
$populateCategorySql = <<<SQL
|
||||
INSERT INTO `WarehouseCategory` (`name`, `description`, `create`, `create_by`)
|
||||
SELECT DISTINCT
|
||||
TRIM(`category`), -- name
|
||||
TRIM(`category`), -- description
|
||||
UNIX_TIMESTAMP(), -- create (current Unix time)
|
||||
1 -- create_by
|
||||
FROM `WarehouseArticle`
|
||||
WHERE `category` IS NOT NULL AND TRIM(`category`) != ''
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name` = VALUES(`name`), -- Keep existing name
|
||||
`description` = VALUES(`description`), -- Update description if needed
|
||||
`create` = VALUES(`create`), -- Update timestamp if needed
|
||||
`create_by` = VALUES(`create_by`); -- Update creator if needed
|
||||
SQL;
|
||||
$this->execute($populateCategorySql);
|
||||
|
||||
// Get the WarehouseArticle table object
|
||||
$warehouseArticleTable = $this->table('WarehouseArticle');
|
||||
|
||||
// 2. Add the new category_id column to WarehouseArticle
|
||||
$warehouseArticleTable
|
||||
->addColumn('category_id', 'integer', [
|
||||
'null' => true, // Allow NULL values
|
||||
'signed' => false, // Typically IDs are unsigned
|
||||
'default' => null,
|
||||
'after' => 'description', // Position the column
|
||||
'comment' => 'References the WarehouseCategory table ID'
|
||||
])
|
||||
->update(); // Apply the column addition
|
||||
|
||||
// 3. Update WarehouseArticle.category_id based on the old category name
|
||||
// Using execute() for the UPDATE with JOIN and COLLATE
|
||||
// *** IMPORTANT: Adjust 'utf8mb4_unicode_ci' if your columns use a different collation ***
|
||||
$updateCategoryIdSql = <<<SQL
|
||||
UPDATE `WarehouseArticle` wa
|
||||
LEFT JOIN `WarehouseCategory` wc ON TRIM(wa.`category`) = wc.`name` COLLATE utf8mb4_unicode_ci
|
||||
SET wa.`category_id` = wc.`id`
|
||||
WHERE wa.`category` IS NOT NULL AND TRIM(wa.`category`) != '';
|
||||
SQL;
|
||||
$this->execute($updateCategoryIdSql);
|
||||
|
||||
// 4. Drop the old category column and its index
|
||||
// Phinx automatically handles index removal when removing the column if it was defined via Phinx.
|
||||
// However, the original index 'category' might have been created manually.
|
||||
// Best practice: Explicitly remove the index first if unsure.
|
||||
if ($warehouseArticleTable->hasIndex('category')) {
|
||||
$warehouseArticleTable->removeIndexByName('category')->update();
|
||||
}
|
||||
// Now remove the column itself
|
||||
$warehouseArticleTable->removeColumn('category')->update();
|
||||
|
||||
// 5. Add an index to the new category_id column
|
||||
// Check if index already exists before adding
|
||||
if (!$warehouseArticleTable->hasIndex('category_id')) {
|
||||
$warehouseArticleTable->addIndex('category_id', ['name' => 'idx_category_id'])->update();
|
||||
}
|
||||
|
||||
// --- End: New WarehouseArticle Category Migration Logic ---
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
// Only run this migration in the 'thetool' environment
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
|
||||
// --- Start: Reversal of WarehouseArticle Category Migration Logic ---
|
||||
|
||||
$warehouseArticleTable = $this->table('WarehouseArticle');
|
||||
|
||||
// 1. Remove the index from category_id
|
||||
if ($warehouseArticleTable->hasIndexByName('idx_category_id')) {
|
||||
$warehouseArticleTable->removeIndexByName('idx_category_id')->update();
|
||||
}
|
||||
|
||||
// 2. Add the old 'category' column back
|
||||
// Ensure the definition matches the original state as closely as possible.
|
||||
$warehouseArticleTable
|
||||
->addColumn('category', 'string', [
|
||||
'limit' => 255,
|
||||
'null' => false, // Assuming it was NOT NULL originally based on CREATE TABLE statement
|
||||
'collation' => 'utf8mb4_unicode_ci', // Match original collation
|
||||
'encoding' => 'utf8mb4',
|
||||
'after' => 'description' // Place it back roughly where it was
|
||||
])
|
||||
->update();
|
||||
|
||||
// 3. Add the index back to the 'category' column
|
||||
// Check if index already exists before adding
|
||||
if (!$warehouseArticleTable->hasIndex('category')) {
|
||||
$warehouseArticleTable->addIndex('category')->update();
|
||||
}
|
||||
|
||||
// 4. Populate the old 'category' column based on 'category_id'
|
||||
// Using execute() for the UPDATE with JOIN
|
||||
// *** IMPORTANT: Adjust 'utf8mb4_unicode_ci' if your columns use a different collation ***
|
||||
$repopulateCategorySql = <<<SQL
|
||||
UPDATE `WarehouseArticle` wa
|
||||
LEFT JOIN `WarehouseCategory` wc ON wa.`category_id` = wc.`id`
|
||||
SET wa.`category` = wc.`name` COLLATE utf8mb4_unicode_ci -- Ensure collation match for assignment
|
||||
WHERE wa.`category_id` IS NOT NULL;
|
||||
SQL;
|
||||
$this->execute($repopulateCategorySql);
|
||||
|
||||
// 5. Remove the 'category_id' column
|
||||
$warehouseArticleTable->removeColumn('category_id')->update();
|
||||
|
||||
// --- End: Reversal of WarehouseArticle Category Migration Logic ---
|
||||
|
||||
|
||||
// --- Start: Original Down Logic (potentially destructive/incomplete) ---
|
||||
// Note: This part only recreates WarehouseShippingNoteTextElement and drops WarehouseCategory.
|
||||
// It does NOT fully revert the state before the *entire* 'up' method ran if WarehouseCategory existed before.
|
||||
// Consider if this is the desired behavior.
|
||||
|
||||
$WarehouseShippingNoteTextElementTable = $this->table("WarehouseShippingNoteTextElement", ['id' => 'id', 'signed' => false]);
|
||||
// This check seems redundant if the goal is to ensure it exists after 'down'
|
||||
// if (!$WarehouseShippingNoteTextElementTable->exists()) {
|
||||
// Drop if exists, then recreate to ensure clean state matching original 'down' intent
|
||||
if ($WarehouseShippingNoteTextElementTable->exists()) {
|
||||
$WarehouseShippingNoteTextElementTable->drop()->save();
|
||||
}
|
||||
$WarehouseShippingNoteTextElementTable = $this->table("WarehouseShippingNoteTextElement", ['id' => 'id', 'signed' => false]); // Re-initialize after drop
|
||||
$WarehouseShippingNoteTextElementTable
|
||||
->addColumn("title", "string", ["limit" => 255])
|
||||
->addColumn("content", "text")
|
||||
->addColumn("create", "integer", ["default" => 1728541890])
|
||||
->addColumn("createBy", "integer", ["default" => 1])
|
||||
->create(); // Use create()
|
||||
// }
|
||||
|
||||
$WarehouseCategoryTable = $this->table("WarehouseCategory", ['id' => 'id', 'signed' => false]);
|
||||
if ($WarehouseCategoryTable->exists()) {
|
||||
// This drops the category table entirely, including data potentially added outside this migration
|
||||
$WarehouseCategoryTable->drop()->save();
|
||||
}
|
||||
// --- End: Original Down Logic ---
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,10 @@ class TTCrud extends mfBaseController {
|
||||
}, $data));
|
||||
}
|
||||
|
||||
protected function getAllAction() {
|
||||
self::returnJson($this->model::getAll($this->postData['filters'] ?? []));
|
||||
}
|
||||
|
||||
protected function getByIdAction() {
|
||||
$id = $_GET['id'] ?? null;
|
||||
if (!$id || !is_numeric($id)) {
|
||||
|
||||
@@ -1,5 +1,38 @@
|
||||
.warehouse-article-prices > div {
|
||||
/* style.css */
|
||||
.warehouse-article-prices > div,
|
||||
.warehouse-article-distributor > div {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(120px, 1fr)) 72px;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr)) 120px; /* Defines 4 columns */
|
||||
gap: 12px; /* Use gap for spacing */
|
||||
margin-bottom: 8px; /* Add small gap between rows */
|
||||
}
|
||||
|
||||
.warehouse-article-prices .form-group,
|
||||
.warehouse-article-distributor .form-group {
|
||||
margin-bottom: 0; /* Keep inputs tight in grid cells */
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.warehouse-article-prices > div,
|
||||
.warehouse-article-distributor > div {
|
||||
display: block; /* Stack items vertically */
|
||||
border: 1px solid #eee; /* Lighter border */
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.warehouse-article-prices > div > *,
|
||||
.warehouse-article-distributor > div > * {
|
||||
display: block; /* Ensure children are block */
|
||||
margin-bottom: 10px !important; /* Add space between stacked items */
|
||||
}
|
||||
.warehouse-article-prices > div > div:last-child, /* Target button container */
|
||||
.warehouse-article-distributor > div > div:last-child {
|
||||
text-align: right; /* Keep buttons aligned right */
|
||||
margin-bottom: 0 !important; /* Remove extra margin below buttons */
|
||||
}
|
||||
.warehouse-article-prices > div > div:last-child > *, /* Target buttons inside */
|
||||
.warehouse-article-distributor > div > div:last-child > * {
|
||||
margin-left: 5px; /* Add small space between buttons */
|
||||
}
|
||||
}
|
||||
@@ -1,97 +1,175 @@
|
||||
async function handleApiResponse(responsePromise) {
|
||||
const res = await responsePromise;
|
||||
if (res.data.success === false) return window.notify('error', `Fehler: ${res.data.errors.join(', ')}`);
|
||||
window.notify('success', res.data.message || 'Erfolgreich');
|
||||
}
|
||||
|
||||
Vue.component('warehouse-article-prices', {
|
||||
props: {id: {type: Number, required: true}},
|
||||
template: `
|
||||
<div>
|
||||
|
||||
<tt-card>
|
||||
<h4>Artikelpreise</h4>
|
||||
|
||||
<div class="warehouse-article-prices">
|
||||
<div v-for="(price, index) in articlePrices" :key="index" class="article-price">
|
||||
<div class="article-price-type">
|
||||
<span v-if="price.isRobot" class="robot-icon"><i class="fa-solid fa-robot"></i></span>
|
||||
{{ index }}
|
||||
</div>
|
||||
<tt-input sm v-model="price.priceMultiplier" label="Preisfaktor"/>
|
||||
<tt-input sm v-model="price.priceOverride" label="Preisoverride"/>
|
||||
|
||||
<div>
|
||||
<tt-button sm icon="fa-solid fa-trash" class="remove-price" additional-class="btn-danger"></tt-button>
|
||||
<tt-button sm icon="fa-solid fa-save" class="save-price" additional-class="btn-primary"></tt-button>
|
||||
</div>
|
||||
|
||||
<tt-card>
|
||||
<h4 style="text-align: center">Artikelpreise überschreiben</h4>
|
||||
<div class="warehouse-article-prices">
|
||||
<div v-for="(price, typeTitle) in articlePrices" :key="typeTitle">
|
||||
<div style="align-self: center;">
|
||||
<i v-if="price.isRobot" class="fa-solid fa-robot"></i> {{ typeTitle }}
|
||||
</div>
|
||||
<tt-input sm v-model="price.priceMultiplier" label="Preisfaktor" @input="price.priceOverride = null"/>
|
||||
<tt-input sm v-model="price.priceOverride" label="Preis" @input="price.priceMultiplier = null"/>
|
||||
<div style="align-self: end;">
|
||||
<tt-button sm icon="fa-solid fa-save" additional-class="btn-primary" @click="savePrice(price)"/>
|
||||
<tt-button sm icon="fa-solid fa-trash" additional-class="btn-danger" v-if="!price.isRobot"
|
||||
@click="deletePrice(price)"/>
|
||||
</div>
|
||||
</div>
|
||||
</tt-card>
|
||||
|
||||
</div>`,
|
||||
data: () => ({
|
||||
window,
|
||||
articlePrices: [],
|
||||
}),
|
||||
</div>
|
||||
</tt-card>
|
||||
`,
|
||||
data: () => ({window, articlePrices: {}}),
|
||||
async mounted() {
|
||||
const [res1, res2] = await Promise.all([
|
||||
axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/get', {filters: {articleId: this.id}}),
|
||||
axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePriceType/get')
|
||||
]);
|
||||
const prices = {};
|
||||
res2.data.rows.forEach(t => prices[t.title] = {
|
||||
isRobot: true,
|
||||
articlePriceTypeId: t.id,
|
||||
priceMultiplier: t.defaultPriceFactor,
|
||||
priceOverride: null
|
||||
});
|
||||
res1.data.rows.forEach(p => {
|
||||
const t = res2.data.rows.find(t => t.id === p.articlePriceTypeId);
|
||||
if (t) prices[t.title] = {
|
||||
isRobot: false,
|
||||
articlePriceTypeId: p.articlePriceTypeId,
|
||||
priceMultiplier: p.priceMultiplier || t.defaultPriceFactor,
|
||||
priceOverride: p.priceOverride
|
||||
await this.fetchArticlePrices();
|
||||
},
|
||||
methods: {
|
||||
async fetchArticlePrices() {
|
||||
const [pricesRes, typesRes] = await Promise.all([
|
||||
axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/get`, {filters: {articleId: this.id}}),
|
||||
axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePriceType/get`)
|
||||
]);
|
||||
const prices = {};
|
||||
typesRes.data.rows.forEach(type => prices[type.title] = {
|
||||
isRobot: true,
|
||||
articlePriceTypeId: type.id,
|
||||
priceMultiplier: type.defaultPriceFactor,
|
||||
priceOverride: null
|
||||
});
|
||||
pricesRes.data.rows.forEach(pData => {
|
||||
const type = typesRes.data.rows.find(t => t.id === pData.articlePriceTypeId);
|
||||
if (type) prices[type.title] = {
|
||||
id: pData.id,
|
||||
isRobot: false,
|
||||
articlePriceTypeId: pData.articlePriceTypeId,
|
||||
priceMultiplier: pData.priceMultiplier,
|
||||
priceOverride: pData.priceOverride
|
||||
};
|
||||
});
|
||||
this.articlePrices = prices;
|
||||
},
|
||||
async savePrice(price) {
|
||||
const payload = {
|
||||
articleId: this.id,
|
||||
articlePriceTypeId: price.articlePriceTypeId,
|
||||
priceMultiplier: price.priceMultiplier,
|
||||
priceOverride: price.priceOverride
|
||||
};
|
||||
});
|
||||
this.articlePrices = prices;
|
||||
if (price.isRobot) {
|
||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/create`, payload));
|
||||
await this.fetchArticlePrices();
|
||||
} else {
|
||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/update`, {id: price.id, ...payload}));
|
||||
await this.fetchArticlePrices();
|
||||
}
|
||||
},
|
||||
async deletePrice(price) {
|
||||
const payload = {id: price.id, articleId: this.id, articlePriceTypeId: price.articlePriceTypeId}
|
||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/delete`, payload));
|
||||
await this.fetchArticlePrices();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Vue.component('warehouse-article', {
|
||||
//language=Vue
|
||||
// warehouse-article-distributor.vue.js
|
||||
Vue.component('warehouse-article-distributor', {
|
||||
props: {id: {type: Number, required: true}},
|
||||
template: `
|
||||
<tt-card>
|
||||
<h4 style="text-align: center">Lieferanten für diesen Artikel</h4>
|
||||
<tt-autocomplete :api-url="window['TT_CONFIG']['BASE_PATH'] + '/WarehouseDistributor/autocomplete'"
|
||||
v-model="newDistributorId" label="Neuen Lieferant hinzufügen"/>
|
||||
<div class="warehouse-article-distributor">
|
||||
<div v-for="(distributor, index) in articleDistributors" :key="distributor.id || ('new-' + index)">
|
||||
<tt-resolver style="align-self: center;" reference="WarehouseDistributor"
|
||||
:value="distributor.distributorId"></tt-resolver>
|
||||
<tt-input sm v-model="distributor.externalArticleNumber" label="Externe Artikelnummer"/>
|
||||
<tt-input sm v-model="distributor.purchasePrice" label="Einkaufspreis"/>
|
||||
<div style="align-self: end;">
|
||||
<tt-button sm icon="fa-solid fa-save" additional-class="btn-primary"
|
||||
@click="saveDistributor(distributor)"/>
|
||||
<tt-button sm icon="fa-solid fa-trash" additional-class="btn-danger" v-if="distributor.id"
|
||||
@click="deleteDistributor(distributor)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</tt-card>
|
||||
`,
|
||||
data: () => ({window, articleDistributors: [], newDistributorId: null}),
|
||||
async mounted() {
|
||||
await this.fetchArticleDistributors();
|
||||
},
|
||||
methods: {
|
||||
async fetchArticleDistributors() {
|
||||
const res = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/get`, {filters: {articleId: this.id}});
|
||||
this.articleDistributors = res.data.rows;
|
||||
},
|
||||
async saveDistributor(distributor) {
|
||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/${distributor.id ? 'update' : 'create'}`, distributor));
|
||||
await this.fetchArticleDistributors();
|
||||
},
|
||||
async deleteDistributor(distributor) {
|
||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/delete`, distributor));
|
||||
await this.fetchArticleDistributors();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
newDistributorId(newId) {
|
||||
if (newId) {
|
||||
if (!this.articleDistributors.some(d => d.distributorId === newId)) {
|
||||
this.articleDistributors.push({
|
||||
articleId: this.id,
|
||||
distributorId: newId,
|
||||
externalArticleNumber: null,
|
||||
purchasePrice: null
|
||||
});
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.newDistributorId = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
<tt-table-crud ref="table" @openHistory="historyModal = true; historyModalId = $event.id">
|
||||
|
||||
// warehouse-article.vue.js
|
||||
Vue.component('warehouse-article', {
|
||||
template: `
|
||||
<tt-card>
|
||||
<tt-table-crud ref="table" @openHistory="historyModalId = $event.id; historyModal = true">
|
||||
<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}}: <span
|
||||
style="white-space:nowrap;"> {{(price.price)}} €</span><br></span>
|
||||
<span v-else-if="price && price.title === 'Verkauf'">{{(price.price)}} €</span>
|
||||
<template v-for="price in JSON.parse(row.cheapestSellPrice || '[]')">
|
||||
<span v-if="price && window.TT_CONFIG['WAREHOUSE_ADMIN']">{{ price.title }}: <span
|
||||
style="white-space:nowrap;">{{ price.price }} €</span><br></span>
|
||||
<span v-else-if="price && price.title === 'Verkauf'">{{ price.price }} €</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-slot:modal-prepend="{ crudModalData }">
|
||||
<warehouse-article-prices v-if="crudModalData.id" :id="crudModalData.id"/>
|
||||
<warehouse-article-distributor v-if="crudModalData.id" :id="crudModalData.id"/>
|
||||
</template>
|
||||
|
||||
</tt-table-crud>
|
||||
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
</tt-card>
|
||||
`, data: () => ({
|
||||
window,
|
||||
historyModal: false,
|
||||
historyModalId: null,
|
||||
}),
|
||||
`,
|
||||
data: () => ({window, historyModal: false, historyModalId: null}),
|
||||
mounted() {
|
||||
const table = this.$refs.table?.$refs?.table;
|
||||
if (!table) return;
|
||||
|
||||
const showId = new URLSearchParams(window.location.search).get('showId');
|
||||
const currentFilterId = table.filters?.id;
|
||||
|
||||
if ((showId && currentFilterId !== showId) || (!showId && currentFilterId)) {
|
||||
table.filters = showId ? {id: showId} : {};
|
||||
if (showId && (!table.filters || table.filters.id !== showId)) {
|
||||
table.filters = {...table.filters, id: showId};
|
||||
table.refreshTable();
|
||||
} else if (!showId && table.filters?.id) {
|
||||
delete table.filters.id;
|
||||
if (Object.keys(table.filters).length === 0) table.filters = {};
|
||||
table.refreshTable();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -99,7 +99,7 @@ Vue.component('warehouse-administration-switch', {
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseDistributor';" :class="{ 'active': window.location.href.includes('WarehouseDistributor') }" class="btn btn-primary">Lieferanten</button>
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseLocation';" :class="{ 'active': window.location.href.includes('WarehouseLocation') }" class="btn btn-primary">Lagerorte</button>
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticlePriceType';" :class="{ 'active': window.location.href.includes('WarehouseArticlePriceType') }" class="btn btn-primary">Preistypen</button>
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNoteTextElement';" :class="{ 'active': window.location.href.includes('Device') }" class="btn btn-primary">LS Texte</button>
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseCategory';" :class="{ 'active': window.location.href.includes('Device') }" class="btn btn-primary">Kategorie</button>
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseAdministration';" :class="{ 'active': window.location.href.includes('Device') }" class="btn btn-primary">Admin-Tools</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
@@ -110,8 +110,9 @@ Vue.component('warehouse-administration-switch', {
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseDistributor'; showDropdown = false" class="dropdown-item">Lieferanten</a>
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticlePriceType'; showDropdown = false" class="dropdown-item">Lagerorte</a>
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseLocation'; showDropdown = false" class="dropdown-item">Preistypen</a>
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNoteTextElement'; showDropdown = false" class="dropdown-item">LS Texte</a>
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseCategory'; showDropdown = false" class="dropdown-item">Kategorie</a>
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseAdministration'; showDropdown = false" class="dropdown-item">Admin-Tools</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ Vue.component('warehouse-offer-modal', {
|
||||
</div>
|
||||
|
||||
|
||||
<template v-slot:footer-prepend>
|
||||
<template v-slot:footer-prepend v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'">
|
||||
<tt-input placeholder="Vorlagenname" no-form-group v-model="templateName"/>
|
||||
<tt-button text="Als Vorlage speichern" @click="saveTemplate" icon="fas fa-save" additional-class="btn-success"/>
|
||||
</template>
|
||||
@@ -217,12 +217,15 @@ Vue.component('warehouse-offer', {
|
||||
<div style="display: flex; gap: 8px">
|
||||
<button @click="offerModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-primary dropdown-toggle" @click="offerTemplatesDropdown = !offerTemplatesDropdown">
|
||||
<button class="btn btn-outline-primary dropdown-toggle" @click.stop="offerTemplatesDropdown = !offerTemplatesDropdown" >
|
||||
Angebot aus Vorlage erstellen <i class="fas fa-caret-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu" :class="{'show': offerTemplatesDropdown}">
|
||||
<li v-for="template in offerTemplates" @click="createOfferFromTemplate(template)">
|
||||
<ul class="dropdown-menu" :class="{'show': offerTemplatesDropdown}" >
|
||||
<li v-for="template in offerTemplates" @click="createOfferFromTemplate(template)" style="display: flex; gap: 2px;cursor: pointer;margin-bottom: 4px;margin-right: 4px">
|
||||
<a class="dropdown-item">{{ template.templateName }}</a>
|
||||
<tt-button
|
||||
v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'"
|
||||
icon="fas fa-trash" sm additional-class="btn-danger" @click.stop="deleteTemplate(template.id)"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -259,6 +262,15 @@ Vue.component('warehouse-offer', {
|
||||
this.$refs.modal.offer.notes = template.notes;
|
||||
|
||||
this.window.notify('success', 'Angebot aus Vorlage erstellt');
|
||||
},
|
||||
async deleteTemplate(id) {
|
||||
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/deleteTemplate?id=${id}`);
|
||||
if (response.data.success) {
|
||||
this.offerTemplates = this.offerTemplates.filter(template => template.id !== id);
|
||||
this.window.notify('success', 'Vorlage erfolgreich gelöscht');
|
||||
} else {
|
||||
this.window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -282,7 +282,6 @@ Vue.component('warehouse-order-modal', {
|
||||
@updateField-distributorId="fetchDistributorData"
|
||||
>
|
||||
<template #form-actions-append>
|
||||
<!-- v-if $refs.positionsManager.formData.article parse is int and not NaN we show a <tt-button> with a @click to BASE_PATH /WarehouseArticle?showId-->
|
||||
<tt-button
|
||||
v-if="!isNaN(parseInt($refs.positionsManager.formData.article))"
|
||||
text="Zum Artikel"
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [
|
||||
{
|
||||
"key": "status_to_progress",
|
||||
"key": "status_to_progress",
|
||||
"title": "In Bearbeitung",
|
||||
"class": "fas fa-cog text-warning",
|
||||
"condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['new', 'on_hold'].includes(row.status),
|
||||
},
|
||||
{
|
||||
"key": "status_to_accepted",
|
||||
"key": "status_to_accepted",
|
||||
"title": "Akzeptieren",
|
||||
"class": "fas fa-check text-success",
|
||||
"condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['new', 'in_progress', 'on_hold'].includes(row.status),
|
||||
},
|
||||
{
|
||||
"key": "status_to_invoiced",
|
||||
"key": "status_to_invoiced",
|
||||
"title": "Verrechnet",
|
||||
"class": "fas fa-file-invoice text-info",
|
||||
"condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['in_progress', 'accepted', 'on_hold'].includes(row.status),
|
||||
},
|
||||
{
|
||||
"key": "status_to_on_hold",
|
||||
"key": "status_to_on_hold",
|
||||
"title": "On Hold",
|
||||
"class": "fas fa-pause text-warning",
|
||||
"condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['accepted', 'new', 'in_progress'].includes(row.status),
|
||||
},
|
||||
{
|
||||
"key": "status_to_cancelled",
|
||||
"key": "status_to_cancelled",
|
||||
"title": "Storniert",
|
||||
"class": "fas fa-ban text-danger",
|
||||
"condition": (row) => (window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['new', 'in_progress', 'accepted'].includes(row.status)) || (row.status === 'new' && row.signature === null),
|
||||
},
|
||||
{
|
||||
"key": "status_to_new",
|
||||
"key": "status_to_new",
|
||||
"title": "Lieferschein wiedereröffnen",
|
||||
"class": "fas fa-redo-alt text-success",
|
||||
"condition": (row) => (window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.status === 'cancelled'),
|
||||
@@ -53,32 +53,32 @@ window.TT_CONFIG["CRUD_CONFIG"]["editCondition"] = (row) => row.status === 'new'
|
||||
|
||||
Vue.component('warehouse-shipping-note-positions', {
|
||||
//language=Vue
|
||||
props: {
|
||||
positions: Array,
|
||||
props: {
|
||||
positions: Array,
|
||||
hoursEntries: Array
|
||||
}, data() {
|
||||
return {
|
||||
articleData: {}, loading: false, articlePacketData: {}, userData: {}
|
||||
}
|
||||
}, template: `
|
||||
<div>
|
||||
<div v-if="loading" class="text-center">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="loading" class="text-center">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
|
||||
<ul v-if="!loading">
|
||||
<li v-for="position in positions">
|
||||
<ul v-if="!loading">
|
||||
<li v-for="position in positions">
|
||||
<span>{{ position.amount }}x
|
||||
{{ position.article ? articleData[position.article]?.text : position.articlePacket ? articlePacketData[position.articlePacket]?.text : position.articleText ? position.articleText : position.article_text }}</span>
|
||||
</li>
|
||||
<template v-for="entry in hoursEntries">
|
||||
<li><span>{{ entry.hourCount }}h Arbeitszeit</span></li>
|
||||
<li v-if="entry.carId">{{ entry.kilometerCount }}km Anfahrt</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
`, async mounted() {
|
||||
</li>
|
||||
<template v-for="entry in hoursEntries">
|
||||
<li><span>{{ entry.hourCount }}h Arbeitszeit</span></li>
|
||||
<li v-if="entry.carId">{{ entry.kilometerCount }}km Anfahrt</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
`, async mounted() {
|
||||
this.loading = true;
|
||||
for (const position of this.positions) {
|
||||
if (position.article) {
|
||||
@@ -105,14 +105,14 @@ Vue.component('add-log-modal-sn', {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
note: '',
|
||||
file: null,
|
||||
uploadedFiles: [],
|
||||
submitLoading: false
|
||||
window: window,
|
||||
note: '',
|
||||
file: null,
|
||||
uploadedFiles: [],
|
||||
submitLoading: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
methods: {
|
||||
async handleFileUpload(event) {
|
||||
const files = event.target.files;
|
||||
if (!files.length) return;
|
||||
@@ -131,7 +131,7 @@ Vue.component('add-log-modal-sn', {
|
||||
|
||||
if (response.data.success) {
|
||||
this.uploadedFiles.push({
|
||||
id: response.data.fileId,
|
||||
id: response.data.fileId,
|
||||
name: file.name
|
||||
});
|
||||
window.notify('success', `File "${file.name}" uploaded successfully`);
|
||||
@@ -153,7 +153,7 @@ Vue.component('add-log-modal-sn', {
|
||||
const fileIds = this.uploadedFiles.map(file => file.id);
|
||||
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/createNewLogAction`, {
|
||||
shippingNoteId: this.shippingNoteId,
|
||||
note: this.note,
|
||||
note: this.note,
|
||||
fileIds: JSON.stringify(fileIds),
|
||||
});
|
||||
|
||||
@@ -168,53 +168,55 @@ Vue.component('add-log-modal-sn', {
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<tt-modal :show="true" @submit="submit" :delete="false" @update:show="$emit('close')" title="Logeintrag hinzufügen" :save-loading="submitLoading">
|
||||
<template>
|
||||
<tt-modal :show="true" @submit="submit" :delete="false" @update:show="$emit('close')"
|
||||
title="Logeintrag hinzufügen" :save-loading="submitLoading">
|
||||
<template>
|
||||
|
||||
<div class="form-group" style="margin: 10px 0">
|
||||
<label>Dateiupload (Mehrere)</label>
|
||||
<input type="file" class="form-control" @change="handleFileUpload" multiple/>
|
||||
</div>
|
||||
<div class="form-group" style="margin: 10px 0">
|
||||
<label>Dateiupload (Mehrere)</label>
|
||||
<input type="file" class="form-control" @change="handleFileUpload" multiple/>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadedFiles.length" class="upload-success-alert">
|
||||
<div class="alert-header">
|
||||
<i class="fa fa-check-circle" aria-hidden="true"></i>
|
||||
<span v-if="uploadedFiles.length === 1">Datei erfolgreich hochgeladen</span>
|
||||
<span v-else>Dateien erfolgreich hochgeladen</span>
|
||||
</div>
|
||||
<ul class="file-list">
|
||||
<li v-for="(file, index) in uploadedFiles" :key="file.id" class="file-item">
|
||||
<i class="fa fa-file" aria-hidden="true"></i>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<button type="button" class="remove-btn" @click="removeFile(index)">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="uploadedFiles.length" class="upload-success-alert">
|
||||
<div class="alert-header">
|
||||
<i class="fa fa-check-circle" aria-hidden="true"></i>
|
||||
<span v-if="uploadedFiles.length === 1">Datei erfolgreich hochgeladen</span>
|
||||
<span v-else>Dateien erfolgreich hochgeladen</span>
|
||||
</div>
|
||||
<ul class="file-list">
|
||||
<li v-for="(file, index) in uploadedFiles" :key="file.id" class="file-item">
|
||||
<i class="fa fa-file" aria-hidden="true"></i>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<button type="button" class="remove-btn" @click="removeFile(index)">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<tt-textarea label="Bemerkung" v-model="note" sm/>
|
||||
</template>
|
||||
</tt-modal>
|
||||
`
|
||||
<tt-textarea label="Bemerkung" v-model="note" sm/>
|
||||
</template>
|
||||
</tt-modal>
|
||||
`
|
||||
})
|
||||
|
||||
Vue.component('tt-file', {
|
||||
props: ['id'],
|
||||
data: () => ({file: null}),
|
||||
data: () => ({file: null}),
|
||||
async mounted() {
|
||||
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/File/getById`, {params: {id: this.id}});
|
||||
this.file = response.data;
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<a :href="'/File/download?id=' + id" target="_blank" v-if="file">{{ file.filename }}</a>
|
||||
<template v-else>
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="sr-only">Loading...</span></div>
|
||||
</template>
|
||||
</div>
|
||||
`
|
||||
<div>
|
||||
<a :href="'/File/download?id=' + id" target="_blank" v-if="file">{{ file.filename }}</a>
|
||||
<template v-else>
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"><span
|
||||
class="sr-only">Loading...</span></div>
|
||||
</template>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
|
||||
@@ -229,23 +231,23 @@ Vue.component('warehouse-shipping-note-logs', {
|
||||
},
|
||||
//language=Vue
|
||||
template: `
|
||||
<div>
|
||||
<div v-if="loading" class="text-center">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="loading" class="text-center">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading">
|
||||
<div v-for="log in logs">
|
||||
{{ formatDate(log.create) }} ({{ getUserName(log.createBy) }}) | {{ log.message }}
|
||||
<template v-if="log.fileIds">
|
||||
<div v-for="file in JSON.parse(log.fileIds)">
|
||||
<tt-file :id="file"/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
<div v-if="!loading">
|
||||
<div v-for="log in logs">
|
||||
{{ formatDate(log.create) }} ({{ getUserName(log.createBy) }}) | {{ log.message }}
|
||||
<template v-if="log.fileIds">
|
||||
<div v-for="file in JSON.parse(log.fileIds)">
|
||||
<tt-file :id="file"/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
const response = await axios.get(window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/getLog', {params: {shippingNoteId: this.shippingNoteId}});
|
||||
@@ -253,7 +255,7 @@ Vue.component('warehouse-shipping-note-logs', {
|
||||
this.loading = false;
|
||||
},
|
||||
methods: {
|
||||
formatDate: date => window.moment(date * 1000).format('DD.MM.YYYY HH:mm'),
|
||||
formatDate: date => window.moment(date * 1000).format('DD.MM.YYYY HH:mm'),
|
||||
getUserName: id => window.TT_CONFIG.CRUD_CONFIG.columns.find(col => col.key === 'createBy')?.modal.items.find(u => u.value === id)?.text
|
||||
}
|
||||
})
|
||||
@@ -262,52 +264,65 @@ Vue.component('warehouse-shipping-note-see-through', {
|
||||
props: ['wantedState'],
|
||||
//language=Vue
|
||||
template: `
|
||||
<div class="modal" style="position: fixed; z-index: 9999; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center;">
|
||||
<div class="modal-content" style="background-color: #fff; border-radius: 8px; width: 95%; height: 95%; max-width: none; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column;">
|
||||
<div class="modal-top-bar" style="background: linear-gradient(135deg, #f7c423, #005384); color: white; padding: 15px 20px; border-top-left-radius: 8px; border-top-right-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h2 style="margin: 0; font-family: Arial, sans-serif; font-size: 20px; flex: 1;">Lieferschein Durchschau Modus</h2>
|
||||
|
||||
<div class="modal"
|
||||
style="position: fixed; z-index: 9999; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center;">
|
||||
<div class="modal-content"
|
||||
style="background-color: #fff; border-radius: 8px; width: 95%; height: 95%; max-width: none; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column;">
|
||||
<div class="modal-top-bar"
|
||||
style="background: linear-gradient(135deg, #f7c423, #005384); color: white; padding: 15px 20px; border-top-left-radius: 8px; border-top-right-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h2 style="margin: 0; font-family: Arial, sans-serif; font-size: 20px; flex: 1;">Lieferschein Durchschau
|
||||
Modus</h2>
|
||||
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<button @click="previousNote" style="background: none; border: none; color: white; font-family: Arial, sans-serif; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 5px;">
|
||||
<button @click="previousNote"
|
||||
style="background: none; border: none; color: white; font-family: Arial, sans-serif; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 5px;">
|
||||
<i class="fas fa-chevron-left" style="font-size: 12px;"></i> Vorheriger
|
||||
</button>
|
||||
<button @click="nextNote" style="background: none; border: none; color: white; font-family: Arial, sans-serif; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 5px;">
|
||||
<button @click="nextNote"
|
||||
style="background: none; border: none; color: white; font-family: Arial, sans-serif; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 5px;">
|
||||
Nächster <i class="fas fa-chevron-right" style="font-size: 12px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
<i @click="$emit('close')" class="fas fa-times" style="font-size: 20px; cursor: pointer; margin-left: 15px;"></i>
|
||||
<i @click="$emit('close')" class="fas fa-times"
|
||||
style="font-size: 20px; cursor: pointer; margin-left: 15px;"></i>
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 20px; display: flex; flex: 1; overflow: hidden;">
|
||||
<template>
|
||||
<div style="width: 50%; height: 100%; overflow: auto;">
|
||||
<iframe ref="iframe" v-if="currentRow" :src="'/WarehouseShippingNote/createPDF?id=' + currentRow.id" style="width: 100%; height: 100%; border: none;"></iframe>
|
||||
</div>
|
||||
<div style="width: 50%; height: 100%; padding-left: 20px; display: flex; flex-direction: column;">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<button @click="activeTab = 'Editieren'" :style="tabStyle('Editieren')">Editieren</button>
|
||||
<button @click="activeTab = 'Logs'" :style="tabStyle('Logs')">Logs</button>
|
||||
|
||||
<div style="margin-top: 20px;" v-if="currentRow">
|
||||
<template v-for="action in window.TT_CONFIG.CRUD_CONFIG.additionalActions">
|
||||
<template v-if="typeof action.condition === 'function' ? action.condition(currentRow) : true && action.key !== 'add_log' && action.key !== 'print'">
|
||||
<i @click="changeStatus(action.key, currentRow)" :class="action.class" style="font-size: 20px; cursor: pointer; margin-right: 10px;"></i>
|
||||
<div style="width: 50%; height: 100%; overflow: auto;">
|
||||
<iframe ref="iframe" v-if="currentRow" :src="'/WarehouseShippingNote/createPDF?id=' + currentRow.id"
|
||||
style="width: 100%; height: 100%; border: none;"></iframe>
|
||||
</div>
|
||||
<div style="width: 50%; height: 100%; padding-left: 20px; display: flex; flex-direction: column;">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<button @click="activeTab = 'Editieren'" :style="tabStyle('Editieren')">Editieren</button>
|
||||
<button @click="activeTab = 'Logs'" :style="tabStyle('Logs')">Logs</button>
|
||||
|
||||
<div style="margin-top: 20px;" v-if="currentRow">
|
||||
<template v-for="action in window.TT_CONFIG.CRUD_CONFIG.additionalActions">
|
||||
<template
|
||||
v-if="typeof action.condition === 'function' ? action.condition(currentRow) : true && action.key !== 'add_log' && action.key !== 'print'">
|
||||
<i @click="changeStatus(action.key, currentRow)" :class="action.class"
|
||||
style="font-size: 20px; cursor: pointer; margin-right: 10px;"></i>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-if="activeTab === 'Editieren'" style="flex: 1; overflow: auto;" class="see-through-test-modal">
|
||||
<warehouse-shipping-note-modal v-if="currentRow" :id="currentRow.id" @close="fetchData" ref="modal">
|
||||
<template v-slot:footer-prepend v-if="wantedState === 'new'">
|
||||
<button class="btn btn-warning" @click="saveAndSetToProgress">Speichern und in Bearbeitung
|
||||
setzen
|
||||
</button>
|
||||
</template>
|
||||
</warehouse-shipping-note-modal>
|
||||
</div>
|
||||
<div v-if="activeTab === 'Logs'" style="flex: 1; overflow: auto;" class="see-through-test-modal">
|
||||
<warehouse-shipping-note-logs :shipping-note-id="currentRow.id" :key="'logs' + logModalKey"/>
|
||||
<add-log-modal-sn v-if="currentRow" :shipping-note-id="currentRow.id" @close="logModalKey++"
|
||||
:key="'modal'+logModalKey"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-if="activeTab === 'Editieren'" style="flex: 1; overflow: auto;" class="see-through-test-modal">
|
||||
<warehouse-shipping-note-modal v-if="currentRow" :id="currentRow.id" @close="fetchData" ref="modal">
|
||||
<template v-slot:footer-prepend v-if="wantedState === 'new'">
|
||||
<button class="btn btn-warning" @click="saveAndSetToProgress">Speichern und in Bearbeitung setzen</button>
|
||||
</template>
|
||||
</warehouse-shipping-note-modal>
|
||||
</div>
|
||||
<div v-if="activeTab === 'Logs'" style="flex: 1; overflow: auto;" class="see-through-test-modal">
|
||||
<warehouse-shipping-note-logs :shipping-note-id="currentRow.id" :key="'logs' + logModalKey"/>
|
||||
<add-log-modal-sn v-if="currentRow" :shipping-note-id="currentRow.id" @close="logModalKey++" :key="'modal'+logModalKey"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,71 +399,339 @@ Vue.component('warehouse-shipping-note-see-through', {
|
||||
}
|
||||
});
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
Vue.component('warehouse-article-modal', {
|
||||
template: `
|
||||
<tt-modal :show="true" :save="false" :delete="false" @update:show="$emit('close')"
|
||||
title="Artikel Suchen">
|
||||
<tt-card>
|
||||
<div v-if="!isLoadingCategories" :style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
|
||||
gap: '10px',
|
||||
padding: '10px',
|
||||
marginBottom: '15px',
|
||||
borderBottom: '1px solid #eee'
|
||||
}">
|
||||
<div v-for="category in categories"
|
||||
:key="category.id"
|
||||
@click="selectCategory(category)"
|
||||
:style="getCategoryStyle(category)"
|
||||
style="
|
||||
padding: '10px 15px';
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
background-color: #f9f9f9;
|
||||
font-size: 0.9em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
"
|
||||
:title="category.name"
|
||||
@mouseover="hoverCategory($event, true)"
|
||||
@mouseleave="hoverCategory($event, false)">
|
||||
{{ category.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="text-align: center; padding: 20px; color: #777;">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
Lade Kategorien...
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="selectedCategory" style="padding: 0 10px 15px 10px;">
|
||||
<tt-input
|
||||
label="Artikel suchen"
|
||||
placeholder="Titel, Artikelnummer, Beschreibung..."
|
||||
v-model="searchTerm"
|
||||
:sm="true"
|
||||
:row="false"
|
||||
type="search"
|
||||
hint="Sucht in Titel, Artikelnummer und Beschreibung."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedCategory" style="padding: 0 10px 10px 10px; min-height: 150px; position: relative;">
|
||||
<div v-if="isLoadingArticles" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.7); display: flex; justify-content: center; align-items: center; z-index: 10;">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span style="margin-left: 10px;">Lade Artikel...</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!isLoadingArticles && filteredArticles.length > 0" :style="{ display: 'grid', gridTemplateColumns: '1fr', gap: '8px' }">
|
||||
<div v-for="article in filteredArticles"
|
||||
:key="article.id"
|
||||
@click="selectArticle(article)"
|
||||
style="
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
transition: background-color 0.15s ease;
|
||||
"
|
||||
@mouseover="event => event.currentTarget.style.backgroundColor='#f5f5f5'"
|
||||
@mouseleave="event => event.currentTarget.style.backgroundColor='#fff'">
|
||||
<div>
|
||||
<strong style="font-size: 0.95em;">{{ article.title }}</strong>
|
||||
<span style="font-size: 0.85em; color: #666; margin-left: 10px;">({{ article.articleNumber }})</span>
|
||||
</div>
|
||||
<div v-if="article.description" style="font-size: 0.85em; color: #555; margin-top: 3px;">
|
||||
{{ article.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isLoadingArticles && articles.length > 0 && filteredArticles.length === 0" style="color: #777; text-align: center; padding: 20px;">
|
||||
Keine Artikel entsprechen Ihrer Suche nach "{{ searchTerm }}".
|
||||
</div>
|
||||
|
||||
<div v-if="!isLoadingArticles && articles.length === 0 && !isLoadingArticles" style="color: #777; text-align: center; padding: 20px;">
|
||||
Keine Artikel in der ausgewählten Kategorie gefunden.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</tt-card>
|
||||
</tt-modal>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
isLoadingCategories: false, // Added loading state for categories
|
||||
categories: [],
|
||||
selectedCategory: null,
|
||||
articles: [],
|
||||
searchTerm: '',
|
||||
isLoadingArticles: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredArticles() {
|
||||
if (!this.searchTerm) {
|
||||
return this.articles;
|
||||
}
|
||||
const lowerSearchTerm = this.searchTerm.toLowerCase();
|
||||
// Ensure articles is an array before filtering
|
||||
if (!Array.isArray(this.articles)) {
|
||||
console.warn('Attempted to filter non-array articles:', this.articles);
|
||||
return [];
|
||||
}
|
||||
return this.articles.filter(article => {
|
||||
const titleMatch = article.title?.toLowerCase().includes(lowerSearchTerm);
|
||||
const articleNumberMatch = article.articleNumber?.toLowerCase().includes(lowerSearchTerm);
|
||||
const descriptionMatch = article.description?.toLowerCase().includes(lowerSearchTerm);
|
||||
return titleMatch || articleNumberMatch || descriptionMatch;
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async selectCategory(category) {
|
||||
if (this.selectedCategory && this.selectedCategory.id === category.id) {
|
||||
return;
|
||||
}
|
||||
this.selectedCategory = category;
|
||||
this.searchTerm = '';
|
||||
this.articles = [];
|
||||
console.log('Selected Category:', this.selectedCategory);
|
||||
await this.fetchArticles(category.id);
|
||||
},
|
||||
|
||||
async fetchArticles(categoryId) {
|
||||
if (!categoryId) return;
|
||||
this.isLoadingArticles = true;
|
||||
try {
|
||||
const response = await axios.post(window.TT_CONFIG.BASE_PATH + '/WarehouseArticle/getAll', {
|
||||
filters: {
|
||||
category_id: categoryId
|
||||
}
|
||||
});
|
||||
// Robust check: Ensure response.data is an array
|
||||
if (Array.isArray(response.data)) {
|
||||
this.articles = response.data;
|
||||
} else {
|
||||
console.warn('Fetched articles data is not an array:', response.data);
|
||||
this.articles = []; // Set to empty array if not valid
|
||||
}
|
||||
console.log('Fetched Articles:', this.articles);
|
||||
} catch (error) {
|
||||
console.error("Error fetching articles:", error);
|
||||
this.articles = [];
|
||||
} finally {
|
||||
this.isLoadingArticles = false;
|
||||
}
|
||||
},
|
||||
|
||||
selectArticle(article) {
|
||||
console.log('Selected Article:', article);
|
||||
this.$emit('article-selected', article);
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
getCategoryStyle(category) {
|
||||
const style = {};
|
||||
if (this.selectedCategory && this.selectedCategory.id === category.id) {
|
||||
style.backgroundColor = '#d0e0ff';
|
||||
style.borderColor = '#a0c0ff';
|
||||
style.fontWeight = 'bold';
|
||||
style.boxShadow = '0 0 5px rgba(0, 100, 255, 0.3)';
|
||||
}
|
||||
return style;
|
||||
},
|
||||
|
||||
hoverCategory(event, isHovering) {
|
||||
// Call findCategoryId safely
|
||||
const categoryId = this.findCategoryId(event.target.innerText);
|
||||
|
||||
// Guard: Only proceed if categoryId was found
|
||||
if (categoryId === null) {
|
||||
// console.warn('Could not find category ID for:', event.target.innerText);
|
||||
return; // Stop if ID couldn't be found (e.g., categories not loaded yet)
|
||||
}
|
||||
|
||||
// Rest of the hover logic...
|
||||
if (!event.target.dataset.categoryId) {
|
||||
event.target.dataset.categoryId = categoryId;
|
||||
}
|
||||
|
||||
if (!this.selectedCategory || this.selectedCategory.id != categoryId) {
|
||||
if (isHovering) {
|
||||
event.target.style.backgroundColor = '#e9e9e9';
|
||||
event.target.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
|
||||
} else {
|
||||
event.target.style.backgroundColor = '#f9f9f9';
|
||||
event.target.style.boxShadow = 'none';
|
||||
}
|
||||
} else {
|
||||
if (!isHovering) {
|
||||
event.target.style.boxShadow = this.getCategoryStyle(this.selectedCategory).boxShadow || 'none';
|
||||
event.target.style.backgroundColor = this.getCategoryStyle(this.selectedCategory).backgroundColor ||'#d0e0ff';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
findCategoryId(name) {
|
||||
// **** GUARD ADDED HERE ****
|
||||
// Check if categories is an array and has items before using .find()
|
||||
if (!Array.isArray(this.categories) || this.categories.length === 0) {
|
||||
// console.warn('findCategoryId called before categories array is ready or populated.');
|
||||
return null; // Return null if categories are not ready
|
||||
}
|
||||
// Now it's safe to use .find()
|
||||
const found = this.categories.find(cat => cat && cat.name === name); // Added check for cat existence
|
||||
return found ? found.id : null;
|
||||
}
|
||||
|
||||
},
|
||||
async mounted() {
|
||||
this.isLoadingCategories = true; // Set loading true before fetch
|
||||
try {
|
||||
const response = await axios.get(window.TT_CONFIG.BASE_PATH + '/WarehouseCategory/getAll');
|
||||
console.log('Raw category response.data:', response.data); // Log the raw response
|
||||
console.log('Is response.data an array?', Array.isArray(response.data)); // Check if it's an array
|
||||
|
||||
// **** ROBUST ASSIGNMENT ****
|
||||
// Ensure we assign an array, even if the response isn't one.
|
||||
if (Array.isArray(response.data)) {
|
||||
this.categories = response.data;
|
||||
} else if (response.data && Array.isArray(response.data.data)) {
|
||||
// Example: Handle common case where data is nested like { data: [...] }
|
||||
console.log('Assigning categories from response.data.data');
|
||||
this.categories = response.data.data;
|
||||
}
|
||||
else {
|
||||
console.warn('Categories response.data is not an array and not handled structure:', response.data);
|
||||
this.categories = []; // Default to empty array if response is unexpected
|
||||
}
|
||||
console.log('Assigned this.categories:', this.categories);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
this.categories = []; // Ensure it's an array on error
|
||||
} finally {
|
||||
this.isLoadingCategories = false; // Set loading false after fetch/error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Vue.component('warehouse-shipping-note', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<warehouse-shipping-note-see-through
|
||||
@close="shippingNoteSeeThrough = false;$refs.table.$refs.table.refreshTable()"
|
||||
v-if="shippingNoteSeeThrough !== false"
|
||||
:wanted-state="shippingNoteSeeThrough"
|
||||
@status_to_progress="changeStatus($event.id, 'in_progress')"
|
||||
@status_to_new="changeStatus($event.id, 'new')"
|
||||
@status_to_on_hold="changeStatus($event.id, 'on_hold')"
|
||||
@status_to_cancelled="changeStatus($event.id, 'cancelled')"
|
||||
@status_to_invoiced="changeStatus($event.id, 'invoiced')"
|
||||
@status_to_accepted="changeStatus($event.id, 'accepted')"/>
|
||||
<warehouse-shipping-note-modal v-if="shippingNoteModalId" :id="shippingNoteModalId"
|
||||
@close="shippingNoteModalId = null;$refs.table.$refs.table.refreshTable()"
|
||||
@open-signing-modal="signingShippingNoteId = $event"/>
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
<warehouse-shipping-note-signature-pad v-if="signingShippingNoteId" :shipping-note-id="signingShippingNoteId"
|
||||
@close="signingShippingNoteId = null;shippingNoteModalId = null;$refs.table.$refs.table.refreshTable()"/>
|
||||
<add-log-modal-sn v-if="addLogModalId" :shipping-note-id="addLogModalId" @close="addLogModalId = null"/>
|
||||
<tt-card>
|
||||
<warehouse-shipping-note-see-through
|
||||
@close="shippingNoteSeeThrough = false;$refs.table.$refs.table.refreshTable()"
|
||||
v-if="shippingNoteSeeThrough !== false"
|
||||
:wanted-state="shippingNoteSeeThrough"
|
||||
@status_to_progress="changeStatus($event.id, 'in_progress')"
|
||||
@status_to_new="changeStatus($event.id, 'new')"
|
||||
@status_to_on_hold="changeStatus($event.id, 'on_hold')"
|
||||
@status_to_cancelled="changeStatus($event.id, 'cancelled')"
|
||||
@status_to_invoiced="changeStatus($event.id, 'invoiced')"
|
||||
@status_to_accepted="changeStatus($event.id, 'accepted')"/>
|
||||
<warehouse-shipping-note-modal v-if="shippingNoteModalId" :id="shippingNoteModalId"
|
||||
ref="modal"
|
||||
@close="shippingNoteModalId = null;$refs.table.$refs.table.refreshTable()"
|
||||
@open-article-modal="articleModalId = true;window.console.log($event)"
|
||||
@open-signing-modal="signingShippingNoteId = $event"/>
|
||||
|
||||
<warehouse-article-modal
|
||||
v-if="articleModalId"
|
||||
:id="articleModalId"
|
||||
@close="articleModalId = null"
|
||||
@article-selected="$refs.modal.$refs.positionsManager.updateField('article', $event.id);"/>
|
||||
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
<warehouse-shipping-note-signature-pad v-if="signingShippingNoteId" :shipping-note-id="signingShippingNoteId"
|
||||
@close="signingShippingNoteId = null;shippingNoteModalId = null;$refs.table.$refs.table.refreshTable()"/>
|
||||
<add-log-modal-sn v-if="addLogModalId" :shipping-note-id="addLogModalId" @close="addLogModalId = null"/>
|
||||
|
||||
<button @click="shippingNoteModalId = 'create'" class="btn btn-primary">Lieferschein erstellen</button>
|
||||
<div class="dropdown" style="display: inline-block; margin-left: 10px;" v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'">-
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Durchschau-Modus
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'new'">Neue</a>
|
||||
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'in_progress'">In Bearbeitung</a>
|
||||
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'accepted'">Akzeptierte</a>
|
||||
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'on_hold'">On Hold</a>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="shippingNoteModalId = 'create'" class="btn btn-primary">Lieferschein erstellen</button>
|
||||
<div class="dropdown" style="display: inline-block; margin-left: 10px;"
|
||||
v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
Durchschau-Modus
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" style="cursor: pointer;">
|
||||
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'new'">Neue</a>
|
||||
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'in_progress'">In Bearbeitung</a>
|
||||
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'accepted'">Akzeptierte</a>
|
||||
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'on_hold'">On Hold</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<tt-table-crud emit-edit
|
||||
@openHistory="historyModal = true; historyModalId = $event.id"
|
||||
@print="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/createPDF?id=' + $event.id)"
|
||||
@status_to_progress="changeStatus($event.id, 'in_progress')"
|
||||
@status_to_accepted="changeStatus($event.id, 'accepted')"
|
||||
@status_to_invoiced="changeStatus($event.id, 'invoiced')"
|
||||
@status_to_on_hold="changeStatus($event.id, 'on_hold')"
|
||||
@status_to_cancelled="changeStatus($event.id, 'cancelled')"
|
||||
@status_to_new="changeStatus($event.id, 'new')"
|
||||
@add_log="addLogModalId = $event.id"
|
||||
@edit="shippingNoteModalId = $event.id"
|
||||
ref="table">
|
||||
<tt-table-crud emit-edit
|
||||
@openHistory="historyModal = true; historyModalId = $event.id"
|
||||
@print="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/createPDF?id=' + $event.id)"
|
||||
@status_to_progress="changeStatus($event.id, 'in_progress')"
|
||||
@status_to_accepted="changeStatus($event.id, 'accepted')"
|
||||
@status_to_invoiced="changeStatus($event.id, 'invoiced')"
|
||||
@status_to_on_hold="changeStatus($event.id, 'on_hold')"
|
||||
@status_to_cancelled="changeStatus($event.id, 'cancelled')"
|
||||
@status_to_new="changeStatus($event.id, 'new')"
|
||||
@add_log="addLogModalId = $event.id"
|
||||
@edit="shippingNoteModalId = $event.id"
|
||||
ref="table">
|
||||
|
||||
<template v-slot:expandedRow="{ row }">
|
||||
<warehouse-shipping-note-positions :positions="JSON.parse(row.positions)" :hours-entries="JSON.parse(row.hoursEntries)"/>
|
||||
<warehouse-shipping-note-logs :shipping-note-id="row.id"/>
|
||||
</template>
|
||||
<template v-slot:expandedRow="{ row }">
|
||||
<warehouse-shipping-note-positions :positions="JSON.parse(row.positions)"
|
||||
:hours-entries="JSON.parse(row.hoursEntries)"/>
|
||||
<warehouse-shipping-note-logs :shipping-note-id="row.id"/>
|
||||
</template>
|
||||
|
||||
</tt-table-crud>
|
||||
</tt-card>
|
||||
`, data() {
|
||||
</tt-table-crud>
|
||||
</tt-card>
|
||||
`, data() {
|
||||
return {
|
||||
window: window,
|
||||
historyModal: false,
|
||||
historyModalId: null,
|
||||
shippingNoteModalId: null,
|
||||
window: window,
|
||||
historyModal: false,
|
||||
historyModalId: null,
|
||||
shippingNoteModalId: null,
|
||||
signingShippingNoteId: null,
|
||||
addLogModalId: null,
|
||||
shippingNoteSeeThrough: false
|
||||
addLogModalId: null,
|
||||
shippingNoteSeeThrough: false,
|
||||
articleModalId: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -459,7 +742,10 @@ Vue.component('warehouse-shipping-note', {
|
||||
},
|
||||
methods: {
|
||||
async changeStatus(id, status) {
|
||||
const response = await axios.post(window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/changeStatus', {id, status});
|
||||
const response = await axios.post(window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/changeStatus', {
|
||||
id,
|
||||
status
|
||||
});
|
||||
if (response.data.success) {
|
||||
this.window.notify('success', response.data.message || 'Erfolgreich aktualisiert');
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
@@ -472,7 +758,7 @@ Vue.component('warehouse-shipping-note', {
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/WarehouseShippingNote/sw', { scope: '/' })
|
||||
navigator.serviceWorker.register('/WarehouseShippingNote/sw', {scope: '/'})
|
||||
.then(registration => {
|
||||
console.log('Patching PWA Service Worker registered with scope:', registration.scope);
|
||||
})
|
||||
|
||||
@@ -35,6 +35,7 @@ Vue.component('warehouse-shipping-note-modal', {
|
||||
customOrdering: 'article',
|
||||
fields: {
|
||||
article: {
|
||||
style: 'display: grid; grid-template-columns: 4fr 1fr; grid-gap: 10px;',
|
||||
type: 'autocomplete',
|
||||
label: 'Artikel',
|
||||
apiUrl: '/WarehouseArticle/autoComplete',
|
||||
@@ -133,7 +134,13 @@ Vue.component('warehouse-shipping-note-modal', {
|
||||
|
||||
<hr>
|
||||
<h4 class="text-center">Positionen</h4>
|
||||
<tt-positions-manager :config="positionsConfig" v-model="shippingNote.positions" sm row/>
|
||||
<tt-positions-manager :config="positionsConfig" v-model="shippingNote.positions" sm row ref="positionsManager">
|
||||
<template #article-prepend>
|
||||
<tt-button
|
||||
style="max-width: 32px" sm
|
||||
text="" @click="$emit('open-article-modal')" additional-class="btn-outline-primary" icon="fa fa-search"/>
|
||||
</template>
|
||||
</tt-positions-manager>
|
||||
</template>
|
||||
<div v-else class="text-danger text-center font-weight-bolder font-16">Bitte Lieferadresse eingeben oder nach Kunden suchen</div>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ Vue.component('tt-autocomplete', {
|
||||
@input="onInput"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
autocomplete="off"
|
||||
:style="{'padding-right': $slots.append ? '30px' : '0'}"
|
||||
/>
|
||||
<slot name="append"></slot>
|
||||
|
||||
@@ -55,6 +55,7 @@ Vue.component('tt-positions-manager',
|
||||
<template v-for="(field, key) in config.fields">
|
||||
<template v-if="typeof field.showCondition === 'function' ? field.showCondition(formData) : true">
|
||||
<slot :name="key" v-bind:field="field" v-bind:value="formData[key]">
|
||||
<div :style="field.style ?? {}" :key="key + field.label">
|
||||
<tt-input
|
||||
v-if="field.type === 'input'"
|
||||
:label="field.label"
|
||||
@@ -96,6 +97,9 @@ Vue.component('tt-positions-manager',
|
||||
v-model="formData[key]"
|
||||
:options="field.options"
|
||||
/>
|
||||
<slot :name="key + '-prepend'" v-bind:field="field" v-bind:value="formData[key]"/>
|
||||
</div>
|
||||
|
||||
</slot>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user