new update

This commit is contained in:
Luca Haid
2025-04-24 13:39:26 +02:00
parent adabe9a7a2
commit 4108eb99c9
22 changed files with 992 additions and 337 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -95,4 +95,8 @@ class WarehouseArticlePriceController extends TTCrud {
WarehouseArticleController::updateSellPrices($postData['articleId']);
}
public function afterDelete($postData) {
WarehouseArticleController::updateSellPrices($postData['articleId']);
}
}

View 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;

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -529,9 +529,6 @@ class WarehouseShippingNoteController extends TTCrud {
return $distance;
}
$fromData = geocode($from);
$toData = geocode($to);
$distance = route($from, $to);
$roundedDistanceKm = round($distance / 1000, 0);

View File

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

View File

@@ -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);
}
}

View File

@@ -1,10 +0,0 @@
<?php
class WarehouseShippingNoteTextElementModel extends TTCrudBaseModel {
public int $id;
public string $title;
public string $content;
public int $create;
public int $createBy;
}

View 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 ---
}
}
}

View File

@@ -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)) {

View File

@@ -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 */
}
}

View File

@@ -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();
}
}
})
});

View File

@@ -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>

View File

@@ -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');
}
}
}
});

View File

@@ -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"

View File

@@ -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);
})

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>