diff --git a/Layout/default/VueViews/Vue.php b/Layout/default/VueViews/Vue.php index d6011f604..16ffac393 100644 --- a/Layout/default/VueViews/Vue.php +++ b/Layout/default/VueViews/Vue.php @@ -12,9 +12,9 @@ $additionalCSS = $additionalCSS ?? []; $additionalJS = $additionalJS ?? []; $additionalJS = [ - ...$additionalJS, "bundler.php", "js/pages/" . $vueViewName . "/" . $vueViewName . ".js", + ...$additionalJS, ]; $additionalCSS = [ diff --git a/Layout/default/menu.php b/Layout/default/menu.php index 73875f175..d717c8a77 100644 --- a/Layout/default/menu.php +++ b/Layout/default/menu.php @@ -130,6 +130,27 @@ + is(["Admin"])&& isset($_GET['warehouse'])): ?> +
  • + + Lager
    +
    + +
  • + + is(["Admin"]) || $me->can("Voipnumbering")): ?>
  • diff --git a/application/VoiceCallHistory/VoiceCallHistoryController.php b/application/VoiceCallHistory/VoiceCallHistoryController.php index 6a97af7e6..a55f1766c 100644 --- a/application/VoiceCallHistory/VoiceCallHistoryController.php +++ b/application/VoiceCallHistory/VoiceCallHistoryController.php @@ -24,20 +24,7 @@ class VoiceCallHistoryController extends mfBaseController { } protected function indexAction(): void { - $JSGlobals = ["BASE_URL" => self::getUrl("Domain"), - "DASHBOARD_URL" => self::getUrl("Dashboard"), - "MFAPPNAME" => MFAPPNAME_SLUG, - "PAGE_TITLE" => "Voice Call History", - "PATH" => [ - ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], - ["text" => "Voice Call History", "href" => self::getUrl("VoiceCallHistory")] - ], - "VOICE_CALL_HISTORY_API_URL" => self::getUrl("VoiceCallHistory/api"), - ]; - - $this->layout()->set("vueViewName", "VoiceCallHistory"); - $this->layout()->set("JSGlobals", $JSGlobals); - $this->layout()->setTemplate("VueViews/Vue"); + Helper::renderVue($this, "Voice Call History", $this->mod, ["VOICE_CALL_HISTORY_API_URL" => $this->getUrl("VoiceCallHistory", "api")]); } protected function apiAction() { diff --git a/application/WarehouseArticle/WarehouseArticle.php b/application/WarehouseArticle/WarehouseArticle.php new file mode 100644 index 000000000..67e268c27 --- /dev/null +++ b/application/WarehouseArticle/WarehouseArticle.php @@ -0,0 +1,9 @@ + 'title', 'text' => 'Titel', 'required' => true, 'table' => ['priority' => 9]], + ['key' => 'description', 'text' => 'Beschreibung', 'required' => true, 'table' => false], + ['key' => 'category', 'text' => 'Kategorie', 'required' => true], + ['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', 'modal' => ['type' => 'number'], 'table' => ['class' => 'text-center']], // Stock/inventory related + ['key' => 'criticalAmount', 'text' => 'Kritische Menge', 'modal' => ['type' => 'number'], 'table' => ['class' => 'text-center']], // Stock/inventory related + ['key' => 'isEShop', 'text' => 'Ist E-Shop', 'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value + ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 8]] + ]; + + protected array $additionalActions = [ + ['key' => 'openHistory','title' => 'Historie','class' => 'fas fa-history text-secondary'], + ['key' => 'editDistributorEntries','title' => 'Lieferanten','class' => 'fas fa-truck text-cyan'], + ['key' => 'editThresholdEntries','title' => 'Schwellenwerte','class' => 'far fa-fw fa-box-full text-orange'], + ['key' => 'editPricesEntries','title' => 'Preise','class' => 'fas fa-euro-sign text-green'], + ]; + // @formatter:on + + protected array $infoMessages = ['create' => 'Artikel wurde erstellt', + 'update' => 'Artikel wurde aktualisiert', + 'delete' => 'Artikel wurde gelöscht', + 'noChanges' => 'Keine Änderungen',]; + + protected function beforeUpdate($postData): bool { + (new WarehouseHistoryController)->create($postData, $this->mod); + return true; + } + + protected function afterUpdate($postData) { + self::updateCheapestPurchasePrice($postData['id']); + } + + public static function updateCheapestPurchasePrice($articleId) { + $cheapestPurchasePrice = WarehouseArticleDistributorModel::getAll(['articleId' => $articleId], 1, 0, + ['key' => 'purchasePrice', 'order' => 'ASC'])[0]->purchasePrice; + + $article = WarehouseArticleModel::get($articleId); + + if (!$article instanceof WarehouseArticleModel) { + throw new Exception("Article is not an instance of WarehouseArticleModel"); + } + + //TODO: think of a new way as we have multiple sell prices now and article sellPriceOverride and sellPriceMultiplier do not exist anymore + // $cheapestSellPrice = $article->sellPriceOverride ?? $article->sellPriceMultiplier * $cheapestPurchasePrice; + + if ($article->cheapestPurchasePrice != $cheapestPurchasePrice) { + WarehouseArticleModel::update([...get_object_vars($article), // Unpack properties into an array + 'cheapestPurchasePrice' => $cheapestPurchasePrice]); + } + } + + protected function afterCreate($postData) { + self::updateCheapestPurchasePrice($postData['id']); + } + + protected function updateAllCheapestPurchasePricesAction() { + $articles = WarehouseArticleModel::getAll(); + foreach ($articles as $article) { + self::updateCheapestPurchasePrice($article->id); + } + } + + protected function getHistoryAction() { + self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns)); + } + + protected function importAction() { + error_reporting(E_ALL); + ini_set('display_errors', 1); + // read import.json from directory of this file + $json = fopen(dirname(__FILE__) . '/import.json', 'r') or die('Unable to open file!'); + + // read file content + $data = fread($json, filesize(dirname(__FILE__) . '/import.json')); + + // decode json + $data = json_decode($data, true); + + // close file + fclose($json); + + // die with data length + // loop through data + echo 'Importing ' . count($data) . ' items' . PHP_EOL; + $count = 0; + foreach ($data as $item) { + // echo count + 1 + echo ++$count . PHP_EOL; + // Check if Distributor exists + $distributor = WarehouseDistributorModel::getAll(['name' => $item['Lieferant']]); + if (empty($distributor)) { + $distributorId = WarehouseDistributorModel::create(['name' => $item['Lieferant'], + 'address' => 'Missing', + 'plz' => 'Missing', + 'city' => 'Missing', + 'countryId' => 1, + 'email' => 'Missing', + 'phone' => 'Missing', + 'contactPerson' => 'Missing',]); + } else { + $distributorId = $distributor[0]->id; + } + + // only continue if PRODUKT 1.ZEILE and PRODUKT 2.ZEILE and ARTIKEL GRUPPE and VK and EK and Lieferant/ Hersteller Artikelnr: are set + if (!isset($item['PRODUKT 1.ZEILE'])) { + echo 'Missing data for ' . $item['PRODUKT 1.ZEILE'] . PHP_EOL; + continue; + } + + if (empty($item['VK'])) { + $item['VK'] = 0; + $calcSellPriceMultiplier = 0; + } else { + $item['VK'] = floatval(str_replace(',', '', $item['VK'])); + } + + if (empty($item['EK'])) { + $item['EK'] = 0; + $calcSellPriceMultiplier = 0; + $purchasePrice = 0; + } else { + $item['EK'] = floatval(str_replace(',', '', $item['EK'])); + } + + if (!empty($item['VK']) && !empty($item['EK'])) { + $calcSellPriceMultiplier = $item['VK'] / $item['EK']; + + // if calcSellPriceMultiplier has more than 2 decimal places assign $calcSellPriceOverride + echo strlen(substr(strrchr($calcSellPriceMultiplier, "."), 1)) . PHP_EOL; + if (strlen(substr(strrchr($calcSellPriceMultiplier, "."), 1)) > 2) { + $calcSellPriceOverride = $item['VK']; + } + + $purchasePrice = str_replace(',', '', $item['EK']); + } + + if (!isset($calcSellPriceMultiplier) && !isset($calcSellPriceOverride) || !isset($purchasePrice)) { + echo 'Missing data for ' . $item['PRODUKT 1.ZEILE'] . PHP_EOL; + continue; + } + + + // Check if Article exists + $article = WarehouseArticleModel::getAll(['title' => ['exact' => $item['PRODUKT 1.ZEILE']]]); + if (empty($article)) { + $articleCreateData = ['title' => $item['PRODUKT 1.ZEILE'], + 'description' => $item['PRODUKT 2. ZEILE'], + 'category' => $item['ARTIKEL GRUPPE'], + 'cheapestPurchasePrice' => 0, + 'warningAmount' => 25, + 'criticalAmount' => 10, + 'isEShop' => 0,]; + + // if calcSellPriceOverride is set, add it to the $articleCreateData array + if (isset($calcSellPriceOverride)) { + $articleCreateData['sellPriceOverride'] = $calcSellPriceOverride; + } else if (isset($calcSellPriceMultiplier)) { + $articleCreateData['sellPriceMultiplier'] = $calcSellPriceMultiplier; + } + + $articleId = WarehouseArticleModel::create($articleCreateData); + } else { + echo 'Article already exists with title ' . $item['PRODUKT 1.ZEILE'] . PHP_EOL; + $articleId = $article[0]->id; + } + + // Check if ArticleDistributor exists + $articleDistributor = WarehouseArticleDistributorModel::getAll(['articleId' => $articleId, + 'distributorId' => $distributorId]); + if (empty($articleDistributor)) { + WarehouseArticleDistributorModel::create(['articleId' => $articleId, + 'distributorId' => $distributorId, + 'purchasePrice' => $purchasePrice, + 'externalArticleNumber' => $item['Lieferant/ Hersteller Artikelnr:'],]); + } + + } + } +} \ No newline at end of file diff --git a/application/WarehouseArticle/WarehouseArticleModel.php b/application/WarehouseArticle/WarehouseArticleModel.php new file mode 100644 index 000000000..30634f526 --- /dev/null +++ b/application/WarehouseArticle/WarehouseArticleModel.php @@ -0,0 +1,15 @@ + 'purchasePrice', 'text' => 'Einkaufspreis', 'required' => true, 'modal' => ['type' => 'number']], + ['key' => 'articleId', 'text' => 'Artikel', 'required' => true, 'modal' => ['type' => 'select', 'data' => 'WarehouseArticle']], + ['key' => 'distributorId', 'text' => 'Lieferant', 'required' => true, 'modal' => ['type' => 'select', 'data' => 'WarehouseDistributor']], + ['key' => 'externalArticleNumber', 'text' => 'Externe Artikelnummer', 'required' => true], + ['key' => 'note', 'text' => 'Notiz', 'required' => false, 'modal' => ['type' => 'textarea']], + ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']] + ]; + // @formatter:on + + protected array $infoMessages = ['create' => 'Lieferanteintrag wurde erstellt', + 'update' => 'Lieferanteintrag wurde aktualisiert', + 'delete' => 'Lieferanteintrag wurde gelöscht', + 'noChanges' => 'Keine Änderungen',]; + + protected function checkExistingThresholdEntry($postData): bool { + $count = WarehouseLocationThresholdOverrideModel::count(['articleId' => $postData['articleId'], + 'distributorId' => $postData['distributorId']]); + + if ($count > 0) { + self::returnJson(['success' => false, + 'message' => 'Es existiert bereits ein Eintrag mit dieser Artikelnummer und diesem Lieferanten.']); + return false; + } + + return true; + } + + protected function beforeCreate($postData): bool { + return $this->checkExistingThresholdEntry($postData); + } + + protected function afterCreate($postData) { + WarehouseArticleController::updateCheapestPurchasePrice($postData['articleId']); + } + + protected function beforeUpdate($postData): bool { + $existing = $this->checkExistingThresholdEntry($postData); + + if (!$existing) { + return false; + } + + (new WarehouseHistoryController)->create($postData, $this->mod); + + return true; + } + + protected function afterUpdate($postData) { + WarehouseArticleController::updateCheapestPurchasePrice($postData['articleId']); + } + + 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); + } + +} \ No newline at end of file diff --git a/application/WarehouseArticleDistributor/WarehouseArticleDistributorModel.php b/application/WarehouseArticleDistributor/WarehouseArticleDistributorModel.php new file mode 100644 index 000000000..d22dd64ca --- /dev/null +++ b/application/WarehouseArticleDistributor/WarehouseArticleDistributorModel.php @@ -0,0 +1,10 @@ + 'articleId', 'text' => 'Artikel', 'required' => true], + ['key' => 'articlePriceTypeId', 'text' => 'Preistyp', 'required' => true], + ['key' => 'priceMultiplier', 'text' => 'Preismultiplikator', 'required' => false], + ['key' => 'priceOverride', 'text' => 'Preisüberschreibung', 'required' => false] + ]; + // @formatter:on + + protected array $infoMessages = ['create' => 'Artikel Verkaufspreis wurde erstellt', + 'update' => 'Artikel Verkaufspreis wurde aktualisiert', + 'delete' => 'Artikel Verkaufspreis wurde gelöscht', + 'noChanges' => 'Keine Änderungen']; + + protected function beforeCreate($postData): bool { + return $this->validate($postData); + } + + protected function validate($postData): bool { + // check if postData priceOverride or priceMultiplier is set but only one of them + if (isset($postData['priceOverride']) && isset($postData['priceMultiplier'])) { + self::returnJson(['success' => false, + 'message' => 'Entweder Preismultiplikator oder Preisüberschreibung kann gesetzt werden.']); + return false; + } else if (!isset($postData['priceOverride']) && !isset($postData['priceMultiplier'])) { + self::returnJson(['success' => false, + 'message' => 'Entweder Preismultiplikator oder Preisüberschreibung muss gesetzt werden.']); + return false; + } + + + if (isset($postData['id'])) { + $count = WarehouseArticlePriceModel::count(['articleId' => $postData['articleId'], + 'articlePriceTypeId' => $postData['articlePriceTypeId'], + 'id' => $postData['id']]); + if ($count > 0) return true; + } else { + $count = WarehouseArticlePriceModel::count(['articleId' => $postData['articleId'], + 'articlePriceTypeId' => $postData['articlePriceTypeId']]); + if ($count > 0) { + self::returnJson(['success' => false, + 'message' => 'Es existiert bereits ein Preis für diesen Artikel und diesen Preistyp.']); + return false; + + } + } + return true; + } + + protected function beforeUpdate($postData): bool { + $existing = $this->validate($postData); + + if (!$existing) { + return false; + } + + (new WarehouseHistoryController)->create($postData, $this->mod); + + return true; + } + + protected function getHistoryAction() { + $history = WarehouseHistoryModel::getByRowId($this->request->id, 'WarehouseArticleDistributor'); + + $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); + } + +} \ No newline at end of file diff --git a/application/WarehouseArticlePrice/WarehouseArticlePriceModel.php b/application/WarehouseArticlePrice/WarehouseArticlePriceModel.php new file mode 100644 index 000000000..86f83ef3e --- /dev/null +++ b/application/WarehouseArticlePrice/WarehouseArticlePriceModel.php @@ -0,0 +1,9 @@ + 'title', 'text' => 'Titel', 'required' => true], + ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]], + ]; + // @formatter:on + + protected array $infoMessages = ['create' => 'Artikel Verkaufspreis wurde erstellt', + 'update' => 'Artikel Verkaufspreis wurde aktualisiert', + 'delete' => 'Artikel Verkaufspreis wurde gelöscht', + 'noChanges' => 'Keine Änderungen']; + + protected function checkExistingDistributorEntry($postData): bool { + // if postData id exists check if there is already an entry with the same articleId and locationId if postdata id and WarehouseLocationThresholdOverrideModel id are different return false + if (isset($postData['id'])) { + $count = WarehouseArticlePriceTypeModel::count(['title' => $postData['title'], 'id' => $postData['id']]); + if ($count > 0) { + return true; + } + } else { + $count = WarehouseArticlePriceTypeModel::count(['title' => $postData['title']]); + if ($count > 0) { + self::returnJson(['success' => false, + 'message' => 'Es existiert bereits ein Preis Typ mit diesem Titel.']); + return false; + + } + } + return true; + } + + protected function beforeCreate($postData): bool { + return $this->checkExistingDistributorEntry($postData); + } + + protected function beforeUpdate($postData): bool { + $existing = $this->checkExistingDistributorEntry($postData); + + if (!$existing) { + return false; + } + + (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); + } + +} \ No newline at end of file diff --git a/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeModel.php b/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeModel.php new file mode 100644 index 000000000..7a87f539e --- /dev/null +++ b/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeModel.php @@ -0,0 +1,6 @@ + 'name', 'text' => 'Name', 'required' => true, 'table' => ['priority' => 11]], + ['key' => 'address', 'text' => 'Adresse', 'required' => true], + ['key' => 'plz', 'text' => 'PLZ', 'required' => true], + ['key' => 'city', 'text' => 'Stadt', 'required' => true], + ['key' => 'countryId', 'text' => 'Land', 'required' => true, 'modal' => ['type' => 'select', 'items' => []]], + ['key' => 'email', 'text' => 'E-Mail', 'required' => true], + ['key' => 'phone', 'text' => 'Telefon', 'required' => true], + ['key' => 'contactPerson', 'text' => 'Kontaktperson', 'required' => true], + ['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 array $infoMessages = ['create' => 'Lieferant wurde erstellt', + 'update' => 'Lieferant wurde aktualisiert', + 'delete' => 'Lieferant wurde gelöscht', + 'noChanges' => 'Keine Änderungen',]; + + public function prepareCrudConfig() { + $countries = array_map(function ($country) { + return ['value' => $country->id, 'text' => $country->name]; + }, CountryModel::getAll()); + + $this->columns[4]['modal']['items'] = $countries; + } + + 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)); + } +} \ No newline at end of file diff --git a/application/WarehouseDistributor/WarehouseDistributorModel.php b/application/WarehouseDistributor/WarehouseDistributorModel.php new file mode 100644 index 000000000..a35d1769e --- /dev/null +++ b/application/WarehouseDistributor/WarehouseDistributorModel.php @@ -0,0 +1,13 @@ + 'title', 'text' => 'Titel'], + ['key' => 'category', 'text' => 'Kategorie'], + ['key' => 'amount', 'text' => 'Menge', 'table' => ['filter' => false,'sortable' => false,'class' => 'p-0 width-80']], + ['key' => 'add', 'text' => 'Hinzufügen', 'table' => ['filter' => false,'sortable' => false, 'class' => 'width-120 text-center']] + ]; + + protected array $infoMessages = [ + 'create' => 'Not possible', + 'update' => 'Not possible', + 'delete' => 'Not possible', + 'noChanges' => 'Keine Änderungen', + ]; + + public function getAction() { + $filter = $this->postData['filters'] ?? []; + $order = $this->postData['order'] ?? ['key' => null, 'order' => 'ASC']; + $page = $this->postData['pagination']['page'] ?? 1; + $perPage = $this->postData['pagination']['per_page'] ?? 10; + + $filter['isEShop'] = 1; + + $rows = WarehouseArticleModel::getAll($filter, $perPage, ($page - 1) * $perPage, $order); + $filteredAvailable = WarehouseArticleModel::count($filter); + $totalRows = WarehouseArticleModel::count(['isEShop' => 1]); + + self::returnJson(["rows" => $rows, + "pagination" => ["page" => $page, + "total_pages" => ceil($filteredAvailable / $perPage), + "per_page" => $perPage, + "filtered_available" => intval($filteredAvailable), + "total_rows" => intval($totalRows)]]); } + + +} \ No newline at end of file diff --git a/application/WarehouseEShop/WarehouseEShopModel.php b/application/WarehouseEShop/WarehouseEShopModel.php new file mode 100644 index 000000000..9e322309a --- /dev/null +++ b/application/WarehouseEShop/WarehouseEShopModel.php @@ -0,0 +1,7 @@ + 'id', 'text' => 'ID', 'modal' => false], + ['key' => 'status', 'text' => 'Status', 'required' => true], + ['key' => 'deliveryMode', 'text' => 'Liefermodus', 'required' => true, 'modal' => ['type' => 'select', 'items' => [ + ['value' => 'singleAddress', 'text' => 'Einzelne Adresse'], + ['value' => 'multipleAddresses', 'text' => 'Mehrere Adressen'], + ]]], + ['key' => 'deliveryAddressName', 'text' => 'Lieferadresse Name', 'required' => true, 'table' => false], + ['key' => 'deliveryAddressLine', 'text' => 'Lieferadresse', 'required' => true, 'required_length' => 4], + ['key' => 'deliveryAddressPLZ', 'text' => 'Lieferadresse PLZ', 'required' => true, 'regex' => '/^\d{4}$/', 'table' => false], + ['key' => 'deliveryAddressCity', 'text' => 'Lieferadresse Stadt', 'required' => true, 'required_length' => 3, 'table' => false], + ['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false, 'filter' => 'datetime'], + ['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true,'modal' => ['type' => 'select', 'items' => []]], + ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']], + ]; + + protected array $additionalActions = [ + ['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'], + ]; + + protected array $infoMessages = [ + 'create' => 'Bestellung wurde erfolgreich erstellt, sie erhalten in Kürze eine Bestätigungsmail', + 'update' => 'Lagerort wurde aktualisiert', + 'delete' => 'Lagerort wurde gelöscht', + 'noChanges' => 'Keine Änderungen', + ]; + + protected function prepareCrudConfig() { + $users = array_map(function($user) { + return ['value' => intval($user->id), 'text' => $user->name]; + }, UserModel::search(['employee' => true])); + + $this->columns[8]['modal']['items'] = $users; + } + + + + protected function createOrderAction() { + //TODO: change this to beforeCreate and afterCreate + $json = json_decode(file_get_contents('php://input'), true); + + $shoppingCart = $json['shoppingCart']; + unset($json['shoppingCart']); + + $json['status'] = 'new'; + $json['create'] = time(); + $json['createBy'] = $this->user->id; + + Helper::validateArray($json, $this->getCheckArray()); + + $id = WarehouseEShopOrderModel::create([ + 'status' => 'new', + 'deliveryMode' => $json['deliveryMode'], + 'deliveryAddressName' => $json['deliveryAddressName'], + 'deliveryAddressLine' => $json['deliveryAddressLine'], + 'deliveryAddressPLZ' => $json['deliveryAddressPLZ'], + 'deliveryAddressCity' => $json['deliveryAddressCity'], + 'create' => $json['create'], + 'createBy' => $json['createBy'], + ]); + + // now create WarehouseEShopOrderItems for each item in the shopping cart + foreach ($shoppingCart as $item) { + WarehouseEShopOrderItemModel::create([ + 'orderId' => $id, + 'articleId' => $item['itemId'], + 'quantity' => intval($item['amount']), + ]); + } + + self::returnJson(['success' => true, + 'message' => $this->infoMessages['create'], + 'id' => $id]); + + $json['id'] = $id; + die(json_encode($json)); + } + + 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)); + } + +} \ No newline at end of file diff --git a/application/WarehouseEShopOrder/WarehouseEShopOrderModel.php b/application/WarehouseEShopOrder/WarehouseEShopOrderModel.php new file mode 100644 index 000000000..fcf5bd215 --- /dev/null +++ b/application/WarehouseEShopOrder/WarehouseEShopOrderModel.php @@ -0,0 +1,24 @@ +loadMe(); + + foreach (array_diff_assoc($postData, (array) $currentData) as $key => $value) { + WarehouseHistoryModel::create(['table' => $mod, + 'row_id' => $postData['id'], + 'key' => $key, + 'old_value' => $currentData->$key, + 'new_value' => $value, + 'note' => '', + 'user_id' => $me->id, + 'create' => date('U')]); + } + } + + public function getHistory($id, string $mod, $columns): array { + $history = WarehouseHistoryModel::getByRowId($id, $mod); + + return array_map(function ($item) use ($columns) { + $item = (array) $item; + + if (isset($this->columns[array_search($item['key'], array_column($columns, 'key'))]['modal']['type'])) { + + if($this->columns[array_search($item['key'], array_column($columns, 'key'))]['modal']['type'] === 'checkbox') { + $item['old_value'] = $item['old_value'] === '1' ? 'Ja' : 'Nein'; + $item['new_value'] = $item['new_value'] === '1' ? 'Ja' : 'Nein'; + } + + if($this->columns[array_search($item['key'], array_column($columns, 'key'))]['modal']['type'] === 'select') { + $column = $this->columns[array_search($item['key'], array_column($columns, 'key'))]; + $item['old_value'] = $column['modal']['items'][array_search($item['old_value'], array_column($column['modal']['items'], 'value'))]['text']; + $item['new_value'] = $column['modal']['items'][array_search($item['new_value'], array_column($column['modal']['items'], 'value'))]['text']; + } + + } + + + + $item['columnHeader'] = $columns[array_search($item['key'], array_column($columns, 'key'))]['text']; + return $item; + }, $history); + } + +} \ No newline at end of file diff --git a/application/WarehouseHistory/WarehouseHistoryModel.php b/application/WarehouseHistory/WarehouseHistoryModel.php new file mode 100644 index 000000000..5e92ee25b --- /dev/null +++ b/application/WarehouseHistory/WarehouseHistoryModel.php @@ -0,0 +1,65 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + $this->$field = $value; + } + } + } + + public static function create($data) { + $FronkDB = FronkDB::singleton(); + $db = $FronkDB->link; + $sql = /** @lang text */ "INSERT INTO `WarehouseHistory` (`table`, `row_id`, `key`, `old_value`, `new_value`, `note`, `user_id`, `create`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + $stmt = $db->prepare($sql); + $stmt->execute([ + $data["table"], + $data["row_id"], + $data["key"], + $data["old_value"], + $data["new_value"], + $data["note"], + $data["user_id"], + $data["create"] + ]); + + return $stmt->insert_id; + } + /** + * Retrieves an array of WarehouseHistoryModel objects by row ID. + * + * @param int $id The row ID. + * @param string $table The table name. + * @return WarehouseHistoryModel[] Array of WarehouseHistoryModel objects. + */ + public static function getByRowId(int $id, string $table): array { + $db = FronkDB::singleton(); + $id = $db->escape($id); + $sql = /** @lang text */ "SELECT WH.*, W.name as user_name FROM `WarehouseHistory` WH + LEFT JOIN `Worker` W ON WH.user_id = W.id + WHERE `row_id` = $id AND `table` = '$table' ORDER BY `create` DESC"; + $result = $db->query($sql); + + $rows = []; + + while ($row = $result->fetch_assoc()) { + $rows[] = new WarehouseHistoryModel($row); + } + + return $rows; + } +} diff --git a/application/WarehouseItem/WarehouseItem.php b/application/WarehouseItem/WarehouseItem.php new file mode 100644 index 000000000..b9e4ced01 --- /dev/null +++ b/application/WarehouseItem/WarehouseItem.php @@ -0,0 +1,9 @@ + 'articleId', 'text' => 'Artikel', 'required' => true, 'type' => 'select','table' => ['class' => 'text-nowrap'], 'modal' => ['items' => [], 'type' => 'select']], + ['key' => 'warehouseLocationId', 'text' => 'Lagerort', 'required' => true, 'type' => 'select', 'modal' => ['items' => [], 'type' => 'select']], + ['key' => 'quantity', 'text' => 'Menge', 'required' => true, 'type' => 'number'], + ['key' => 'serialNumber', 'text' => 'Seriennummer', 'required' => false], + ['key' => 'note', 'text' => 'Notiz', 'required' => false], + ['key' => 'actions', 'text' => 'Aktionen', 'table' => ['filter' => false], 'required' => false, 'modal' => false] + ]; + // @formatter:on + + protected array $additionalActions = [ + ['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'], + ]; + + protected array $infoMessages = [ + 'create' => 'Eintrag wurde erstellt', + 'update' => 'Eintrag wurde aktualisiert', + 'delete' => 'Eintrag wurde gelöscht', + 'noChanges' => 'Keine Änderungen', + 'alreadyExists' => 'Eintrag existiert bereits an diesem Lagerort.' + ]; + + protected function checkExistingItemOnLocation($postData): bool { + $count = WarehouseItemModel::count(['articleId' => $postData['articleId'], + 'warehouseLocationId' => $postData['warehouseLocationId'], + 'serialNumber' => null]); + + if ($count > 0) { + self::returnJson(['success' => false, 'message' => $this->infoMessages['alreadyExists']]); + return false; + } + + return true; + } + + protected function beforeCreate($postData): bool { + return $this->checkExistingItemOnLocation($postData); + } + + protected function beforeUpdate($postData): bool { + $existing = $this->checkExistingItemOnLocation($postData); + + if (!$existing) { + return false; + } + + (new WarehouseHistoryController)->create($postData, $this->mod); + + return true; + } + + public function prepareCrudConfig() { + $articles = array_map(function($article) { + return ['value' => $article->id, 'text' => $article->title]; + }, WarehouseArticleModel::getAll()); + $this->columns[0]['modal']['items'] = $articles; + + $locations = array_map(function($location) { + return ['value' => $location->id, 'text' => $location->title]; + }, WarehouseLocationModel::getAll()); + $this->columns[1]['modal']['items'] = $locations; + } + + protected function getHistoryAction() { + self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns)); + } + +} \ No newline at end of file diff --git a/application/WarehouseItem/WarehouseItemModel.php b/application/WarehouseItem/WarehouseItemModel.php new file mode 100644 index 000000000..56eb73af7 --- /dev/null +++ b/application/WarehouseItem/WarehouseItemModel.php @@ -0,0 +1,10 @@ + 'title', 'text' => 'Titel', 'required' => true], + ['key' => 'assignedTo', 'text' => 'Zugewiesen an', 'required' => true, 'modal' => ['type' => 'select', 'items' => []]], + ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']], + ]; + + protected array $additionalActions = [ + ['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'], + ]; + + protected array $infoMessages = [ + 'create' => 'Lagerort wurde erstellt', + 'update' => 'Lagerort wurde aktualisiert', + 'delete' => 'Lagerort wurde gelöscht', + 'noChanges' => 'Keine Änderungen', + ]; + + public function prepareCrudConfig() { + $users = array_map(function($user) { + return ['value' => $user->id, 'text' => $user->name]; + }, UserModel::search(['employee' => true])); + + $this->columns[1]['modal']['items'] = $users; + } + + protected function beforeUpdate($postData): bool { + (new WarehouseHistoryController)->create($postData, $this->mod); + return true; + } + + protected function getHistoryAction() { + $this->prepareCrudConfig(); + self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns)); + } + +} \ No newline at end of file diff --git a/application/WarehouseLocation/WarehouseLocationModel.php b/application/WarehouseLocation/WarehouseLocationModel.php new file mode 100644 index 000000000..90705745d --- /dev/null +++ b/application/WarehouseLocation/WarehouseLocationModel.php @@ -0,0 +1,7 @@ + 'locationId', 'text' => 'Lagerort', 'required' => true, 'modal' => ['type' => 'select', 'data' => 'WarehouseLocation']], + ['key' => 'articleId', 'text' => 'Artikel', 'required' => true, 'modal' => ['type' => 'select', 'data' => 'WarehouseArticle']], + ['key' => 'warningAmount', 'text' => 'Warnmenge', 'required' => true, 'modal' => ['type' => 'number']], + ['key' => 'criticalAmount', 'text' => 'Kritische Menge', 'required' => true, 'modal' => ['type' => 'number']], + ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']] + ]; + // @formatter:on + + protected array $infoMessages = ['create' => 'Lagerort Schwellenwert wurde erstellt', + 'update' => 'Lagerort Schwellenwert wurde aktualisiert', + 'delete' => 'Lagerort Schwellenwert wurde gelöscht', + 'noChanges' => 'Keine Änderungen']; + + protected function checkExistingDistributorEntry($postData): bool { + // if postData id exists check if there is already an entry with the same articleId and locationId if postdata id and WarehouseLocationThresholdOverrideModel id are different return false + if (isset($postData['id'])) { + $count = WarehouseLocationThresholdOverrideModel::count(['locationId' => $postData['locationId'], + 'articleId' => $postData['articleId'], + 'id' => $postData['id']]); + if ($count > 0) { + return true; + } + } else { + $count = WarehouseLocationThresholdOverrideModel::count(['locationId' => $postData['locationId'], + 'articleId' => $postData['articleId']]); + if ($count > 0) { + self::returnJson(['success' => false, + 'message' => 'Es existiert bereits ein Eintrag mit dieser Artikelnummer und diesem Lagerort.']); + return false; + + } + } + return true; + } + + protected function beforeCreate($postData): bool { + return $this->checkExistingDistributorEntry($postData); + } + + protected function beforeUpdate($postData): bool { + $existing = $this->checkExistingDistributorEntry($postData); + + if (!$existing) { + return false; + } + + (new WarehouseHistoryController)->create($postData, $this->mod); + + return true; + } + + protected function getHistoryAction() { + $history = WarehouseHistoryModel::getByRowId($this->request->id, 'WarehouseArticleDistributor'); + + $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); + } + +} \ No newline at end of file diff --git a/application/WarehouseLocationThresholdOverride/WarehouseLocationThresholdOverrideModel.php b/application/WarehouseLocationThresholdOverride/WarehouseLocationThresholdOverrideModel.php new file mode 100644 index 000000000..4c6ffc765 --- /dev/null +++ b/application/WarehouseLocationThresholdOverride/WarehouseLocationThresholdOverrideModel.php @@ -0,0 +1,9 @@ + 'articleTitle', 'text' => 'Artikel', 'required' => true], + ['key' => 'locationTitle', 'text' => 'Lagerort', 'required' => true, 'table' => ['filter' => false, 'sortable' => false]], + ['key' => 'count', 'text' => 'Anzahl', 'required' => true, 'table' => ['filter' => false, 'sortable' => false]], + ['key' => 'status', 'text' => 'Status', 'required' => true, 'table' => ['filter' => false, 'sortable' => false]], + ['key' => 'recommendation', 'text' => 'Empfehlung', 'required' => true, 'table' => ['filter' => false, 'sortable' => false]], +// ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']], + ]; + + protected array $additionalActions = [ + ['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'], + ]; + + protected array $infoMessages = [ + 'create' => 'Lagerort wurde erstellt', + 'update' => 'Lagerort wurde aktualisiert', + 'delete' => 'Lagerort wurde gelöscht', + 'noChanges' => 'Keine Änderungen', + ]; +} \ No newline at end of file diff --git a/application/WarehouseOrderRecommendation/WarehouseOrderRecommendationModel.php b/application/WarehouseOrderRecommendation/WarehouseOrderRecommendationModel.php new file mode 100644 index 000000000..b182b667b --- /dev/null +++ b/application/WarehouseOrderRecommendation/WarehouseOrderRecommendationModel.php @@ -0,0 +1,171 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + $this->$field = $value; + } + } + } + + /** + * Retrieves recommendations based on the provided filter. + * + * @param array $filter An associative array of filter options. + * @return array An array of WarehouseOrderRecommendationModel instances. + */ + private static function customGet(array $filter = []): array { + error_reporting(E_ALL); + ini_set('display_errors', 1); + + $filterOptions = []; + + // Convert filter to filterOptions because the filter is not directly usable + if (isset($filter['articleTitle'])) { + $filterOptions['title'] = $filter['articleTitle']; + } + + $articles = []; + foreach (WarehouseArticleModel::getAll($filterOptions) as $article) { + $articles[$article->id] = $article; + } + + $items = WarehouseItemModel::getAll(); + + $recommendations = []; + // Check if the amount of items is below the critical or warning amount + foreach ($articles as $article) { + $items = array_filter($items, function ($item) use ($article) { + return $item->articleId === $article->id; + }); + + $count = array_reduce($items, function ($carry, $item) { + return $carry + $item->quantity; + }, 0); + + if ($count <= $article->criticalAmount) { + $minOrderAmount = $article->criticalAmount - $count; + $recommendedOrderAmount = $article->warningAmount - $count; + $recommendations[] = new WarehouseOrderRecommendationModel(["articleId" => $article->id, + "articleTitle" => $article->title, + "count" => $count, + "status" => "Kritisch", + "recommendation" => "Bestellen, min. $minOrderAmount, empfohlen: $recommendedOrderAmount"]); + } else if ($count <= $article->warningAmount) { + $minOrderAmount = $article->warningAmount - $count; + $recommendations[] = new WarehouseOrderRecommendationModel(["articleId" => $article->id, + "articleTitle" => $article->title, + "count" => $count, + "status" => "Warnung", + "recommendation" => "Bestellen, min. $minOrderAmount"]); + } + } + + $locations = []; + foreach (WarehouseLocationModel::getAll() as $location) { + $locations[$location->id] = $location; + } + + $locationThresholdOverrides = WarehouseLocationThresholdOverrideModel::getAll(); + + foreach ($locationThresholdOverrides as $locationThresholdOverride) { + if (!isset($articles[$locationThresholdOverride->articleId])) { + continue; + } + + $items = array_filter($items, function ($item) use ($locationThresholdOverride) { + return $item->articleId === $locationThresholdOverride->articleId; + }); + + $count = array_reduce($items, function ($carry, $item) { + return $carry + $item->quantity; + }, 0); + + if ($count <= $locationThresholdOverride->criticalAmount) { + $minOrderAmount = $locationThresholdOverride->criticalAmount - $count; + $recommendedOrderAmount = $locationThresholdOverride->warningAmount - $count; + $recommendations[] = new WarehouseOrderRecommendationModel(["articleId" => $locationThresholdOverride->articleId, + "articleTitle" => $articles[$locationThresholdOverride->articleId]->title, + "locationId" => $locationThresholdOverride->locationId, + "locationTitle" => $locations[$locationThresholdOverride->locationId]->title, + "count" => $count, + "status" => "Kritisch", + "recommendation" => "Bestellen, min. $minOrderAmount, empfohlen: $recommendedOrderAmount"]); + } else if ($count <= $locationThresholdOverride->warningAmount) { + $minOrderAmount = $locationThresholdOverride->warningAmount - $count; + $recommendations[] = new WarehouseOrderRecommendationModel(["articleId" => $locationThresholdOverride->articleId, + "articleTitle" => $articles[$locationThresholdOverride->articleId]->title, + "locationId" => $locationThresholdOverride->locationId, + "locationTitle" => $locations[$locationThresholdOverride->locationId]->title, + "count" => $count, + "status" => "Warnung", + "recommendation" => "Bestellen, min. $minOrderAmount"]); + } + } + + return $recommendations; + } + + /** + * Counts the number of recommendations based on the provided filter. + * + * @param array $filter An associative array of filter options. + * @return int The number of recommendations. + */ + public static function count(array $filter = []): int { + return count(self::customGet($filter)); + } + + /** + * Retrieves all recommendations based on the provided filter, limit, offset, and order. + * + * @param array $filter An associative array of filter options. + * @param int|null $limit The maximum number of recommendations to return. + * @param int $offset The number of recommendations to skip. + * @param array $order An associative array containing the key to sort by and the order direction ('ASC' or 'DESC'). + * @return array An array of WarehouseOrderRecommendationModel instances. + */ + public static function getAll(array $filter = [], int $limit = null, int $offset = 0, array $order = ["key" => null]): array { + $recommendations = self::customGet($filter); + + // apply limit,offset and order + if ($order['key'] !== null) { + usort($recommendations, function ($a, $b) use ($order) { + return $order['order'] === 'ASC' ? $a->{$order['key']} <=> $b->{$order['key']} : $b->{$order['key']} <=> $a->{$order['key']}; + }); + } + + if ($limit !== null) { + return array_slice($recommendations, $offset, $limit); + } + return $recommendations; + } + +} \ No newline at end of file diff --git a/db/migrations/20240715161500_add_warehouse_tables.php b/db/migrations/20240715161500_add_warehouse_tables.php new file mode 100644 index 000000000..57c13c945 --- /dev/null +++ b/db/migrations/20240715161500_add_warehouse_tables.php @@ -0,0 +1,161 @@ +getEnvironment() == "thetool") { + + // WarehouseArticle Table + $warehouseArticleTable = $this->table("WarehouseArticle", ["signed" => true]); + $warehouseArticleTable->addColumn("title", "string", ["null" => false, "limit" => 255]); + $warehouseArticleTable->addColumn("description", "text", ["null" => false]); + $warehouseArticleTable->addColumn("category", "string", ["null" => false, "limit" => 255]); + $warehouseArticleTable->addColumn("cheapestPurchasePrice", "float", ["null" => true]); + $warehouseArticleTable->addColumn("cheapestSellPrice", "float", ["null" => true]); + $warehouseArticleTable->addColumn("warningAmount", "integer", ["null" => false]); + $warehouseArticleTable->addColumn("criticalAmount", "integer", ["null" => false]); + $warehouseArticleTable->addColumn("isEShop", "integer", ["null" => false, "limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY]); + $warehouseArticleTable->addIndex(["category"]); + $warehouseArticleTable->addIndex(["isEShop"]); + $warehouseArticleTable->save(); + // WarehouseArticle Table End + + + // WarehouseArticleDistributor Table + $warehouseArticleDistributorTable = $this->table("WarehouseArticleDistributor", ["signed" => true]); + $warehouseArticleDistributorTable->addColumn("articleId", "integer", ["null" => false]); + $warehouseArticleDistributorTable->addColumn("distributorId", "integer", ["null" => false]); + $warehouseArticleDistributorTable->addColumn("purchasePrice", "float", ["null" => false]); + $warehouseArticleDistributorTable->addColumn("externalArticleNumber", "string", ["null" => false, "limit" => 255]); + $warehouseArticleDistributorTable->addColumn("note", "string", ["null" => true, "limit" => 255]); + $warehouseArticleDistributorTable->addIndex(["articleId"]); + $warehouseArticleDistributorTable->addIndex(["distributorId"]); + $warehouseArticleDistributorTable->save(); + // WarehouseArticleDistributor Table End + + + // WarehouseArticlePrice Table + $warehouseArticlePriceTable = $this->table("WarehouseArticlePrice", ["signed" => true]); + $warehouseArticlePriceTable->addColumn("articleId", "integer", ["null" => false]); + $warehouseArticlePriceTable->addColumn("articlePriceTypeId", "integer", ["null" => false]); + $warehouseArticlePriceTable->addColumn("priceMultiplier", "float", ["null" => true]); + $warehouseArticlePriceTable->addColumn("priceOverride", "float", ["null" => true]); + $warehouseArticlePriceTable->addIndex(["articleId"]); + $warehouseArticlePriceTable->addIndex(["articlePriceTypeId"]); + $warehouseArticlePriceTable->save(); + // WarehouseArticlePrice Table End + + + // WarehouseArticlePriceType Table + $warehouseArticlePriceTypeTable = $this->table("WarehouseArticlePriceType", ["signed" => true]); + $warehouseArticlePriceTypeTable->addColumn("title", "string", ["null" => false, "limit" => 255]); + $warehouseArticlePriceTypeTable->save(); + // WarehouseArticlePriceType Table End + + + // WarehouseDistributor Table + $warehouseDistributorTable = $this->table("WarehouseDistributor", ["signed" => true]); + $warehouseDistributorTable->addColumn("name", "string", ["null" => false, "limit" => 255]); + $warehouseDistributorTable->addColumn("address", "string", ["null" => false, "limit" => 255]); + $warehouseDistributorTable->addColumn("plz", "string", ["null" => false, "limit" => 16]); + $warehouseDistributorTable->addColumn("city", "string", ["null" => false, "limit" => 255]); + $warehouseDistributorTable->addColumn("countryId", "integer", ["null" => false]); + $warehouseDistributorTable->addColumn("email", "string", ["null" => false, "limit" => 255]); + $warehouseDistributorTable->addColumn("phone", "string", ["null" => false, "limit" => 255]); + $warehouseDistributorTable->addColumn("contactPerson", "string", ["null" => false, "limit" => 255]); + $warehouseDistributorTable->save(); + // WarehouseDistributor Table End + + + // WarehouseEShopOrder Table + $warehouseEShopOrderTable = $this->table("WarehouseEShopOrder", ["signed" => true]); + $warehouseEShopOrderTable->addColumn("status", "enum", ["values" => ["new", "accepted", "sent", "done"], "null" => false]); + $warehouseEShopOrderTable->addColumn("deliveryMode", "enum", ["values" => ["singleAddress", "multipleAddresses"], "null" => false]); + $warehouseEShopOrderTable->addColumn("deliveryAddressName", "string", ["null" => false, "limit" => 255]); + $warehouseEShopOrderTable->addColumn("deliveryAddressLine", "string", ["null" => false, "limit" => 255]); + $warehouseEShopOrderTable->addColumn("deliveryAddressPLZ", "string", ["null" => false, "limit" => 255]); + $warehouseEShopOrderTable->addColumn("deliveryAddressCity", "string", ["null" => false, "limit" => 255]); + $warehouseEShopOrderTable->addColumn("create", "integer", ["null" => false]); + $warehouseEShopOrderTable->addColumn("createBy", "integer", ["null" => false]); + $warehouseEShopOrderTable->save(); + // WarehouseEShopOrder Table End + + + // WarehouseEShopOrderItem Table + $warehouseEShopOrderItemTable = $this->table("WarehouseEShopOrderItem", ["signed" => true]); + $warehouseEShopOrderItemTable->addColumn("orderId", "integer", ["null" => false]); + $warehouseEShopOrderItemTable->addColumn("articleId", "integer", ["null" => false]); + $warehouseEShopOrderItemTable->addColumn("quantity", "integer", ["null" => false]); + $warehouseEShopOrderItemTable->addIndex(["orderId"]); + $warehouseEShopOrderItemTable->save(); + // WarehouseEShopOrderItem Table End + + + // WarehouseHistory Table + $warehouseHistoryTable = $this->table("WarehouseHistory", ["signed" => true]); + $warehouseHistoryTable->addColumn("table", "string", ["null" => false, "limit" => 255]); + $warehouseHistoryTable->addColumn("row_id", "integer", ["null" => false]); + $warehouseHistoryTable->addColumn("key", "string", ["null" => false, "limit" => 255]); + $warehouseHistoryTable->addColumn("old_value", "string", ["null" => true, "limit" => 255]); + $warehouseHistoryTable->addColumn("new_value", "string", ["null" => true, "limit" => 255]); + $warehouseHistoryTable->addColumn("note", "string", ["null" => true, "limit" => 255]); + $warehouseHistoryTable->addColumn("user_id", "integer", ["null" => false]); + $warehouseHistoryTable->addColumn("create", "integer", ["null" => false]); + $warehouseHistoryTable->addIndex(["table"]); + $warehouseHistoryTable->addIndex(["row_id"]); + $warehouseHistoryTable->save(); + // WarehouseHistory Table End + + + // WarehouseItem Table + $warehouseItemTable = $this->table("WarehouseItem", ["signed" => true]); + $warehouseItemTable->addColumn("articleId", "integer", ["null" => false]); + $warehouseItemTable->addColumn("warehouseLocationId", "integer", ["null" => false]); + $warehouseItemTable->addColumn("quantity", "integer", ["null" => false]); + $warehouseItemTable->addColumn("serialNumber", "string", ["null" => true, "limit" => 255]); + $warehouseItemTable->addColumn("note", "string", ["null" => true, "limit" => 255]); + $warehouseItemTable->addIndex(["articleId"]); + $warehouseItemTable->addIndex(["warehouseLocationId"]); + $warehouseItemTable->save(); + // WarehouseItem Table End + + // WarehouseLocation Table + $warehouseLocationTable = $this->table("WarehouseLocation", ["signed" => true]); + $warehouseLocationTable->addColumn("title", "string", ["null" => false, "limit" => 255]); + $warehouseLocationTable->addColumn("assignedTo", "integer", ["null" => false]); + $warehouseLocationTable->addIndex(["assignedTo"]); + $warehouseLocationTable->save(); + // WarehouseLocation Table End + + + // WarehouseLocationThresholdOverride Table + $warehouseLocationThresholdOverrideTable = $this->table("WarehouseLocationThresholdOverride", ["signed" => true]); + $warehouseLocationThresholdOverrideTable->addColumn("locationId", "integer", ["null" => false]); + $warehouseLocationThresholdOverrideTable->addColumn("articleId", "integer", ["null" => false]); + $warehouseLocationThresholdOverrideTable->addColumn("warningAmount", "integer", ["null" => false]); + $warehouseLocationThresholdOverrideTable->addColumn("criticalAmount", "integer", ["null" => false]); + $warehouseLocationThresholdOverrideTable->addIndex(["locationId"]); + $warehouseLocationThresholdOverrideTable->addIndex(["articleId"]); + $warehouseLocationThresholdOverrideTable->save(); + // WarehouseLocationThresholdOverride Table End + } + } + + public function down(): void { + if ($this->getEnvironment() == "thetool") { + $this->table("WarehouseArticle")->drop()->save(); + $this->table("WarehouseArticleDistributor")->drop()->save(); + $this->table("WarehouseArticlePrice")->drop()->save(); + $this->table("WarehouseArticlePriceType")->drop()->save(); + $this->table("WarehouseDistributor")->drop()->save(); + $this->table("WarehouseEShopOrder")->drop()->save(); + $this->table("WarehouseEShopOrderItem")->drop()->save(); + $this->table("WarehouseHistory")->drop()->save(); + $this->table("WarehouseItem")->drop()->save(); + $this->table("WarehouseLocation")->drop()->save(); + $this->table("WarehouseLocationThresholdOverride")->drop()->save(); + } + } +} diff --git a/lib/Helper/Helper.php b/lib/Helper/Helper.php index 8a7a44a28..2feba1cbb 100644 --- a/lib/Helper/Helper.php +++ b/lib/Helper/Helper.php @@ -4,10 +4,9 @@ class Helper { /** * Generate SQL Filter condition (space separated) for a given column. * - * @param string|null $filterValue The filter value to match against. + * @param string|null|array $filterValue The filter value to match against. * @param string $columnName The name of the column in the database table. * @return string The SQL condition generated based on the filter value and column name. - * @noinspection PhpMissingParamTypeInspection */ public static function generateFilterCondition($filterValue, string $columnName, bool $exactMatch = false): string { $sql = ""; @@ -19,6 +18,8 @@ class Helper { $sql = " AND `$columnName` >= " . $filterValue['from']; } elseif (isset($filterValue['to'])) { $sql = " AND `$columnName` <= " . $filterValue['to']; + } else if (isset($filterValue['exact'])) { + $sql = " AND `$columnName` = " . "'{$filterValue['exact']}'"; } } else if ($filterValue === "0" || $filterValue === "1") { $sql .= " AND `$columnName` = " . $filterValue; @@ -37,4 +38,95 @@ class Helper { return $sql; } + + /** + * Validates an array of data based on a set of predefined rules. + * + * @param array $data The data to validate. Keys represent field names, and values are the corresponding data. + * @param array $checkArray An associative array defining validation rules for each field: + * - key: The field name to validate. + * - value: An associative array of validation rules for that field: + * - required (bool, optional): Whether the field is required. Default: false. + * - title (string, optional): The human-readable name of the field to use in error messages. + * - required_length (int, optional): The minimum required length of the value. Default: 1. + * - regex (string, optional): A regular expression pattern the value must match. + * + * @return array|true Returns `true` if validation passes for all fields. Otherwise, returns an associative array of errors + * where keys are field names, and values are error messages. + */ + public static function validateArray(array $data, array $checkArray, bool $printErrors = true) { + $errors = []; + + foreach ($checkArray as $key => $rules) { + $value = $data[$key] ?? null; + $title = $rules['title'] ?? $key; + + // Apply default values for missing rules + + $rules = array_merge([ + 'required' => false, + 'required_length' => 1, + 'regex' => false, + ], $rules); + + // Required Check + if ($rules['required'] && (is_null($value) || $value === '')) { + $errors[$key] = "$title wird benötigt."; + } + + // Length Check (only if value exists) + if (!is_null($value) && strlen($value) < $rules['required_length']) { + $errors[$key] = "$title muss mindestens $rules[required_length] Zeichen lang sein."; + } + + // Regex Check (only if value exists and regex is provided) + if (!is_null($value) && $rules['regex'] && !preg_match($rules['regex'], $value)) { + $errors[$key] = "$title hat ein ungültiges Format."; + } + } + + if ($printErrors) { + if (!empty($errors)) { + header('Content-Type: application/json'); + die(json_encode( + [ + 'success' => false, + 'errors' => $errors + ] + )); + } + } + + return empty($errors) ? true : $errors; + } + + /** + * Displays Vue component with the given header title. + * + * @param mfBaseController $controller The controller instance to generate $JSGlobals for. + * @param string $pageName The name of the Vue component to render. + * @param string $headerTitle The title to display in the header. + * @param array $additionalGlobals Additional global variables to pass to the Vue component. + */ + public static function renderVue(mfBaseController $controller, string $pageName, string $headerTitle, array $additionalGlobals = []) { + $JSGlobals = ["BASE_URL" => $controller::getUrl($pageName), + "MF_URL" => $controller::getUrl(""), + "DASHBOARD_URL" => $controller::getUrl("Dashboard"), + "MF_APP_NAME" => MFAPPNAME_SLUG, + "BASE_PATH" => $controller::getUrl(""), + "PAGE_TITLE" => $headerTitle, + "PATH" => [ + ["text" => MFAPPNAME_SLUG, "href" => $controller::getUrl("Dashboard")], + ["text" => $headerTitle, "href" => $controller::getUrl($pageName)] + ], + ]; + + $JSGlobals = array_merge($JSGlobals, $additionalGlobals); + + $controller->layout()->set("vueViewName", $pageName); + $controller->layout()->set("JSGlobals", $JSGlobals); + $controller->layout()->setTemplate("VueViews/Vue"); + } + + } \ No newline at end of file diff --git a/lib/TTCrud/TTCrud.php b/lib/TTCrud/TTCrud.php new file mode 100644 index 000000000..a53f0aa3d --- /dev/null +++ b/lib/TTCrud/TTCrud.php @@ -0,0 +1,178 @@ +needlogin = true; + $me = new User(); + $me->loadMe(); + $this->user = $me; + $this->layout()->set('me', $me); + + if (!$me->is(["Admin"])) { + $this->redirect("Dashboard"); + } + + $modelName = $this->mod . 'Model'; + $this->model = new $modelName(); + $this->postData = json_decode(file_get_contents('php://input'), true); + $this->checkArray = $this->getCheckArray(); + } + + /** + * Returns the checkArray for the CRUD component. + * @return array + */ + protected function getCheckArray(): array { + $checkArray = []; + + foreach ($this->columns as $column) { + $checkArray[$column['key']] = ['required' => $column['required'] ?? false, + 'required_length' => $column['required_length'] ?? 0, + 'title' => $column['text'] ?? $column['key'], + 'regex' => $column['regex'] ?? false]; + } + + return $checkArray; + } + + protected function indexAction() { + $this->layout()->set('additionalJS', ['js/pages/WarehouseHistory/WarehouseHistoryModal.js']); + $customJsFile = defined('BASEDIR') ? BASEDIR . "/public/js/pages/{$this->mod}/{$this->mod}.js" : null; + + if ($customJsFile && file_exists($customJsFile)) { + $pageName = $this->mod; + } else { + $pageName = "DefaultCrudView"; + } + + Helper::renderVue($this, $pageName, $this->headerTitle, ["CRUD_CONFIG" => $this->getCrudConfig(), + "CREATE_URL" => $this::getUrl($this->mod . "/create"), + "TABLE_URL" => $this::getUrl($this->mod . "/get"), + "UPDATE_URL" => $this::getUrl($this->mod . "/update"), + "DELETE_URL" => $this::getUrl($this->mod . "/delete"),]); + } + + /** + * Returns the configuration for the CRUD component for the Vue component. + * @return array + */ + protected function getCrudConfig(): array { + if (method_exists($this, 'prepareCrudConfig')) { + $this->prepareCrudConfig(); + } + + + $columns = array_map(function ($column) { + $newColumn = $column; + unset($newColumn['required'], $newColumn['required_length'], $newColumn['regex']); + return $newColumn; + }, $this->columns); + + return ['key' => $this->mod, + 'tableHeader' => $this->headerTitle, + 'createText' => $this->createText, + 'columns' => $columns, + 'additionalActions' => $this->additionalActions]; + } + + protected function getAction() { + $filter = $this->postData['filters'] ?? []; + $order = $this->postData['order'] ?? ['key' => null, 'order' => 'ASC']; + $page = $this->postData['pagination']['page'] ?? 1; + $perPage = $this->postData['pagination']['per_page'] ?? 10; + + $rows = $this->model::getAll($filter, $perPage, ($page - 1) * $perPage, $order); + $filteredAvailable = $this->model::count($filter); + $totalRows = $this->model::count(); + + self::returnJson(["rows" => $rows, + "pagination" => ["page" => $page, + "total_pages" => ceil($filteredAvailable / $perPage), + "per_page" => $perPage, + "filtered_available" => intval($filteredAvailable), + "total_rows" => intval($totalRows)]]); + } + + protected function createAction() { + Helper::validateArray($this->postData, $this->checkArray); + + if (method_exists($this, 'beforeCreate') && !$this->beforeCreate($this->postData)) { + self::returnJson(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']); + } + + $id = $this->model::create($this->postData); + + if (method_exists($this, 'afterCreate')) { + $this->afterCreate($this->postData); + } + + self::returnJson(['success' => true, + 'message' => $this->infoMessages['create'], + 'id' => $id]); + } + + protected function updateAction() { + Helper::validateArray($this->postData, array_merge($this->checkArray, ['id' => ['required' => true]])); + + if (method_exists($this, 'beforeUpdate') && !$this->beforeUpdate($this->postData)) { + self::returnJson(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']); + } + + $affectedRows = $this->model::update($this->postData); + + if (method_exists($this, 'afterUpdate')) { + $this->afterUpdate($this->postData); + } + + self::returnJson(['success' => $affectedRows > 0, + 'message' => $affectedRows > 0 ? $this->infoMessages['update'] : $this->infoMessages['noChanges']]); + } + + protected function deleteAction() { + Helper::validateArray($this->postData, ['id' => ['required' => true]]); + + if (method_exists($this, 'beforeDelete') && !$this->beforeDelete($this->postData)) { + self::returnJson(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']); + } + + $affectedRows = $this->model::delete($this->postData['id']); + + self::returnJson(['success' => $affectedRows > 0, + 'message' => $affectedRows > 0 ? $this->infoMessages['delete'] : $this->infoMessages['noChanges']]); + } + + protected function autocompleteAction() { + $searchedID = $this->request->searchedID; + + $textKey = property_exists($this->model, 'name') ? 'name' : 'title'; + + if (strlen($searchedID) > 0) { + $filter = ['id' => $searchedID]; + } else { + $filter = [$textKey => $this->request->q]; + } + + $data = $this->model::getAll($filter, 10); + + self::returnJson(array_map(function ($item) use ($textKey) { + return ['value' => $item->id, 'text' => $item->$textKey]; + }, $data)); + } +} + +?> diff --git a/lib/TTCrudBaseModel/TTCrudBaseModel.php b/lib/TTCrudBaseModel/TTCrudBaseModel.php new file mode 100644 index 000000000..9f7039f32 --- /dev/null +++ b/lib/TTCrudBaseModel/TTCrudBaseModel.php @@ -0,0 +1,170 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + $this->$field = $value; + } + } + } + + public static function create($data) { + $FronkDB = FronkDB::singleton(); + $db = $FronkDB->link; + $table = self::getTable(); + self::checkAllFields($data, ['id']); + + $sqlColumns = []; + $sqlValues = []; + foreach ($data as $field => $value) { + if (!property_exists(get_called_class(), $field)) { + throw new Exception("Field $field does not exist in " . get_called_class()); + } + + $sqlValues[] = $value === null ? 'NULL' : "'" . $db->real_escape_string($value) . "'"; + $sqlColumns[] = "`$field`"; + } + + $sql = "INSERT INTO `$table` (" . implode(", ", $sqlColumns) . ") VALUES (" . implode(", ", $sqlValues) . ")"; + $db->query($sql) or die($db->error); + + return $db->insert_id; + } + + public static function getTable(): string { + return str_replace('Model', '', get_called_class()); + } + + /** + * Checks if all required fields of the current class are present in a given data array. + * + * This method uses reflection to determine which fields are required based on their type declarations. + * It then iterates over these required fields and throws an exception if any of them are missing + * from the provided data array. + * + * @param array $data The data array to check. + * @param array $skip An optional array of field names to skip during the check. + * + * @return void + * @throws Exception If any required field is missing in the `$data` array. + * + */ + public static function checkAllFields(array $data, array $skip = []) { + $requiredVars = array_filter(get_class_vars(get_called_class()), function ($value, $key) { + $reflectionProperty = new ReflectionProperty(get_called_class(), $key); + return !$reflectionProperty->hasType() || !$reflectionProperty->getType()->allowsNull(); + }, ARRAY_FILTER_USE_BOTH); + + foreach ($requiredVars as $field => $value) { + if (in_array($field, $skip)) { + continue; + } + + if (!isset($data[$field])) { + throw new Exception("Required field $field is missing in data array"); + } + } + } + + + public static function get($id): TTCrudBaseModel { + $FronkDB = FronkDB::singleton(); + $db = $FronkDB->link; + $id = $db->real_escape_string($id); + $table = self::getTable(); + $sql = "SELECT * FROM `$table` WHERE `id` = $id"; + $result = $db->query($sql); + // as TTCRudBaseModel is abstract, we need to get the class name of the child class + $class = get_called_class(); + return new $class($result->fetch_assoc()); + } + + public static function count($filter = []): int { + $FronkDB = FronkDB::singleton(); + $db = $FronkDB->link; + $table = self::getTable(); + $filter = self::getSQLFilter($filter); + $sql = "SELECT COUNT(*) as count FROM `$table` $filter"; + $result = $db->query($sql); + + return $result->fetch_assoc()['count']; + } + + public static function getSQLFilter($filter): string { + if (empty($filter)) { + return ""; + } + + $sql = "WHERE 1=1"; + foreach ($filter as $key => $value) { + if (!property_exists(get_called_class(), $key)) { + throw new Exception("Field $key does not exist in " . get_called_class()); + } + $sql .= Helper::generateFilterCondition($value, $key, gettype($value) === "integer"); + } + + return $sql; + } + + public static function getAll($filter = [], $limit = null, $offset = 0, $order = ["key" => null]): array { + $FronkDB = FronkDB::singleton(); + $db = $FronkDB->link; + $table = self::getTable(); + $filter = self::getSQLFilter($filter); + $sql = "SELECT * FROM `$table` $filter"; + + $sql .= $order['key'] === null ? " ORDER BY `id` ASC" : " ORDER BY `" . $order['key'] . "` " . $order['order']; + $sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset; + try { + $result = $db->query($sql); + } catch (Exception $e) { + echo $sql; + die($e->getMessage()); + } + + $rows = []; + $class = get_called_class(); + while ($row = $result->fetch_assoc()) { + $rows[] = new $class($row); + } + + return $rows; + } + + public static function update($data) { + $FronkDB = FronkDB::singleton(); + $db = $FronkDB->link; + $table = self::getTable(); + + // Check if all fields are set + self::checkAllFields($data); + + $values = []; + foreach ($data as $field => $value) { + if (!property_exists(get_called_class(), $field)) { + throw new Exception("Field $field does not exist in " . get_called_class()); + } + if ($field === "id") { + continue; + } + + $values[] = $value === null ? "`$field` = NULL" : "`$field` = '" . $db->real_escape_string($value) . "'"; + } + + $sql = "UPDATE `$table` SET " . implode(", ", $values) . " WHERE `id` = " . $db->real_escape_string($data['id']); + $db->query($sql); + return $db->affected_rows; + } + + public static function delete($id) { + $FronkDB = FronkDB::singleton(); + $db = $FronkDB->link; + $table = self::getTable(); + $id = $db->real_escape_string($id); + $sql = "DELETE FROM `$table` WHERE `id` = $id"; + $db->query($sql); + return $db->affected_rows; + } + +} \ No newline at end of file diff --git a/public/bundler.php b/public/bundler.php index 86e68d4b3..25b2a0777 100644 --- a/public/bundler.php +++ b/public/bundler.php @@ -27,19 +27,23 @@ function combineAndMinifyJS($files) { $jsFiles = [ "plugins/axios/axios.min.js", + "plugins/axios/axios.inject.js", "plugins/moment/moment.min.js", "plugins/daterangepicker/daterangepicker.js", "plugins/vue/" . (isset($_GET['VUE_DEBUG']) || $_SERVER['HTTP_HOST'] === "localhost" ? "vue.js" : "vue.min.js"), "plugins/vue/tt-components/tt-card.js", "plugins/vue/tt-components/tt-table.js", + "plugins/vue/tt-components/tt-table-crud.js", "plugins/vue/tt-components/tt-page-title.js", "plugins/vue/tt-components/tt-loader.js", "plugins/vue/tt-components/tt-select.js", "plugins/vue/tt-components/tt-datepicker.js", "plugins/vue/tt-components/tt-input.js", + "plugins/vue/tt-components/tt-modal.js", "plugins/vue/tt-components/tt-autocomplete.js", "plugins/vue/tt-components/tt-icon-select.js", "plugins/vue/tt-components/tt-number-range.js", + "plugins/vue/tt-components/tt-checkbox.js", ]; diff --git a/public/js/pages/DefaultCrudView/DefaultCrudView.js b/public/js/pages/DefaultCrudView/DefaultCrudView.js new file mode 100644 index 000000000..45bff46a4 --- /dev/null +++ b/public/js/pages/DefaultCrudView/DefaultCrudView.js @@ -0,0 +1,13 @@ +Vue.component('default-crud-view', { + //language=Vue + template: ` + + + + + `, data() { + return { + window: window, historyModal: false, historyModalId: null, + } + }, +}) diff --git a/public/js/pages/WarehouseArticle/WarehouseArticle.js b/public/js/pages/WarehouseArticle/WarehouseArticle.js new file mode 100644 index 000000000..96f2a781f --- /dev/null +++ b/public/js/pages/WarehouseArticle/WarehouseArticle.js @@ -0,0 +1,287 @@ +Vue.component('warehouse-distributor-modal', { + //language=Vue + template: ` + +
    +
    + + + +
    +
    +
    + + Lieferant hinzufügen + + + `, props: { + show: {type: Boolean, default: false}, id: {type: Number, default: null}, + }, data() { + return { + window: window, showModal: false, rows: [] + }; + }, async mounted() { + + }, methods: { + async fetchArticleDistributor() { + const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticleDistributor/get', {filters: {articleId: this.id}}); + + this.rows = response.data.rows + }, addRow() { + this.rows.push({distributor: null, price: null, externalArticleNumber: null}); + }, async saveRow(index) { + // post to /WarehouseArticleDistributor/save with rows data and articleId + const row = this.rows[index]; + + const data = { + articleId: this.id, distributorId: row.distributorId, purchasePrice: row.purchasePrice, externalArticleNumber: row.externalArticleNumber + } + + if (row.id) { + data.id = row.id; + } + + const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/${row.id ? 'update' : 'create'}`, data); + this.$emit('doUpdate'); + this.window.notify(response.data.success ? 'success' : 'error', + response.data.errors ? Object.values(response.data.errors).join('
    ') : response.data.message); + await this.fetchArticleDistributor(); + }, async deleteRow(index) { + const row = this.rows[index]; + + if (!row.id) { + this.window.notify('error', 'Eintrag hat keine ID'); + } + + const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticleDistributor/delete', {id: row.id}); + this.$emit('doUpdate'); + this.window.notify(response.data.success ? 'success' : 'error', + response.data.errors ? Object.values(response.data.errors).join('
    ') : response.data.message); + await this.fetchArticleDistributor(); + } + }, watch: { + show(newVal) { + this.showModal = newVal; + if (!newVal) { + this.rows = []; + return; + } + this.rows = []; + this.fetchArticleDistributor().then(); + } + } +}); + +Vue.component('warehouse-threshold-modal', { + //language=Vue + template: ` + +
    +
    + + + +
    + +
    + + Schwellenwert hinzufügen + +
    + `, props: { + show: {type: Boolean, default: false}, id: {type: Number, default: null}, + }, data() { + return { + window: window, showModal: false, rows: [] + }; + }, async mounted() { + + }, methods: { + async fetchLocationThreshold() { + const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseLocationThresholdOverride/get', {filters: {articleId: this.id}}); + + this.rows = response.data.rows + }, addRow() { + this.rows.push({distributor: null, price: null, externalArticleNumber: null}); + }, async saveRow(index) { + // post to /WarehouseArticleDistributor/save with rows data and articleId + const row = this.rows[index]; + + const data = { + articleId: this.id, locationId: row.locationId, warningAmount: row.warningAmount, criticalAmount: row.criticalAmount + } + + if (row.id) { + data.id = row.id; + } + + const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseLocationThresholdOverride/${row.id ? 'update' : 'create'}`, data); + this.$emit('doUpdate'); + this.window.notify(response.data.success ? 'success' : 'error', + response.data.errors ? Object.values(response.data.errors).join('
    ') : response.data.message); + await this.fetchLocationThreshold() + }, async deleteRow(index) { + const row = this.rows[index]; + + if (!row.id) { + this.window.notify('error', 'Eintrag hat keine ID'); + } + + const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseLocationThresholdOverride/delete', {id: row.id}); + this.$emit('doUpdate'); + this.window.notify(response.data.success ? 'success' : 'error', + response.data.errors ? Object.values(response.data.errors).join('
    ') : response.data.message); + await this.fetchLocationThreshold(); + } + }, watch: { + show(newVal) { + this.showModal = newVal; + if (!newVal) { + this.rows = []; + return; + } + this.rows = []; + this.fetchLocationThreshold().then(); + } + } +}); + +Vue.component('warehouse-article-price-modal', { + //language=Vue + template: ` + +
    +
    + + + +
    + +
    + + Preis hinzufügen + +
    + `, props: { + show: {type: Boolean, default: false}, id: {type: Number, default: null}, + }, data() { + return { + window: window, showModal: false, rows: [] + }; + }, async mounted() { + + }, methods: { + async fetchLocationThreshold() { + const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/get', {filters: {articleId: this.id}}); + + this.rows = response.data.rows + }, addRow() { + this.rows.push({articlePriceTypeId: null, priceMultiplier: null, priceOverride: null}); + }, async saveRow(index) { + // post to /WarehouseArticleDistributor/save with rows data and articleId + const row = this.rows[index]; + + const data = { + articleId: this.id, articlePriceTypeId: row.articlePriceTypeId, priceMultiplier: row.priceMultiplier, priceOverride: row.priceOverride + } + + if (row.id) { + data.id = row.id; + } + + const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/${row.id ? 'update' : 'create'}`, data); + this.$emit('doUpdate'); + this.window.notify(response.data.success ? 'success' : 'error', + response.data.errors ? Object.values(response.data.errors).join('
    ') : response.data.message); + await this.fetchLocationThreshold() + }, async deleteRow(index) { + const row = this.rows[index]; + + if (!row.id) { + this.window.notify('error', 'Eintrag hat keine ID'); + } + + const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/delete', {id: row.id}); + this.$emit('doUpdate'); + this.window.notify(response.data.success ? 'success' : 'error', + response.data.errors ? Object.values(response.data.errors).join('
    ') : response.data.message); + await this.fetchLocationThreshold(); + } + }, watch: { + show(newVal) { + this.showModal = newVal; + if (!newVal) { + this.rows = []; + return; + } + this.rows = []; + this.fetchLocationThreshold().then(); + } + } +}) + + +Vue.component('warehouse-article', { + //language=Vue + template: ` + + + + + + + + + + + + + `, data() { + return { + window: window, + historyModal: false, + historyModalId: null, + distributorModal: false, + distributorModalId: null, + thresholdModal: false, + thresholdModalId: null, + priceModal: false, + priceModalId: null + } + }, methods: { + refreshTable() { + this.$refs.table.$refs.table.refreshTable(); + } + } +}) diff --git a/public/js/pages/WarehouseDistributor/WarehouseDistributor.js b/public/js/pages/WarehouseDistributor/WarehouseDistributor.js new file mode 100644 index 000000000..2763aec39 --- /dev/null +++ b/public/js/pages/WarehouseDistributor/WarehouseDistributor.js @@ -0,0 +1,13 @@ +Vue.component('warehouse-distributor', { + //language=Vue + template: ` + + + + + `, data() { + return { + window: window, historyModal: false, historyModalId: null, + } + }, +}) diff --git a/public/js/pages/WarehouseEShop/WarehouseEShop.js b/public/js/pages/WarehouseEShop/WarehouseEShop.js new file mode 100644 index 000000000..e27989598 --- /dev/null +++ b/public/js/pages/WarehouseEShop/WarehouseEShop.js @@ -0,0 +1,130 @@ + + +Vue.component('tt-expandable-shopping-cart', { + props: { + cartItems: Array, + }, + data() { + return { + isExpanded: false, + }; + }, + methods: { + }, + template: ` +
    + +
    +
    +

    Einkaufswagen

    +
      + +
    +
    +

    Der Einkaufswagen ist leer.

    +
    +
    + ` +}); + + +Vue.component('warehouse-e-shop', { + //language=Vue + template: ` + + + + + + + + + + + + + + + + + + + + + + `, data() { + return { + window: window, itemAmounts: {}, shoppingCart: [], createOrderDialog: false, createOrderDialogData: { + deliveryMode: 'singleAddress', + deliveryAddressName: '', + deliveryAddressLine: '', + deliveryAddressPLZ: '', + deliveryAddressCity: '', + }, + } + }, + methods: { + async openOrderDialog() { + this.createOrderDialog = true; + }, + async submitOrder() { + const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseEShopOrder/createOrder`, { + shoppingCart: this.shoppingCart, + deliveryMode: this.createOrderDialogData.deliveryMode, + deliveryAddressName: this.createOrderDialogData.deliveryAddressName, + deliveryAddressLine: this.createOrderDialogData.deliveryAddressLine, + deliveryAddressPLZ: this.createOrderDialogData.deliveryAddressPLZ, + deliveryAddressCity: this.createOrderDialogData.deliveryAddressCity, + }); + if (response.data.success) { + this.window.notify('success', response.data.message || 'Erfolgreich gespeichert'); + this.shoppingCart = []; + this.createOrderDialog = false; + } else { + this.window.notify('error', + response.data.errors ? Object.values(response.data.errors).join('
    ') : response.data.message || + 'Ein Fehler ist aufgetreten'); + } + }, + addToCart(row) { + if (!this.itemAmounts[row.id] || this.itemAmounts[row.id] === 0) { + window.notify('error', 'Bitte geben Sie eine Menge ein.'); + return; + } + + // Check if Article is already in the shopping cart + if (this.shoppingCart.some(item => item.itemId === row.id)) { + window.notify('warning', `${row.title} ist bereits im Warenkorb.`); + return; + } + + this.shoppingCart.push({itemId: row.id, title: row.title, amount: this.itemAmounts[row.id]}); + window.notify('success', `${this.itemAmounts[row.id]}x ${row.title} wurde dem Warenkorb hinzugefügt.`); + + } + }, +}) diff --git a/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js b/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js new file mode 100644 index 000000000..d83003151 --- /dev/null +++ b/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js @@ -0,0 +1,19 @@ +Vue.component('warehouse-e-shop-order', { + //language=Vue + template: ` + + + + + + + + + `, data() { + return { + window: window, historyModal: false, historyModalId: null, + } + }, +}) diff --git a/public/js/pages/WarehouseHistory/WarehouseHistoryModal.js b/public/js/pages/WarehouseHistory/WarehouseHistoryModal.js new file mode 100644 index 000000000..47df603f7 --- /dev/null +++ b/public/js/pages/WarehouseHistory/WarehouseHistoryModal.js @@ -0,0 +1,45 @@ +Vue.component('warehouse-history-modal', { + //language=Vue + template: ` + + + + + + + + `, + props: { + show: { type: Boolean, default: false }, + id: { type: Number, default: null }, + }, + data() { + return { + window: window, + showModal: false, + WarehouseHistoryTableConfig: { + key: 'WarehouseHistory', + tableHeader: 'Lager Historie', + headers: [ + {text: "Zeit", key: "create", filter: false, sortable: false}, + {text: "Feld", key: "columnHeader", filter: false, sortable: false}, + {text: "Alter Wert", key: "old_value", filter: false, sortable: false}, + {text: "Neuer Wert", key: "new_value", filter: false, sortable: false}, + {text: "Geändert von", key: "user_name", filter: false, sortable: false}, + ], + } + } + }, + watch: { + show(newVal) { + this.showModal = newVal + } + } +}) diff --git a/public/js/pages/WarehouseItem/WarehouseItem.js b/public/js/pages/WarehouseItem/WarehouseItem.js new file mode 100644 index 000000000..7c678c55f --- /dev/null +++ b/public/js/pages/WarehouseItem/WarehouseItem.js @@ -0,0 +1,13 @@ +Vue.component('warehouse-item', { + //language=Vue + template: ` + + + + + `, data() { + return { + window: window, historyModal: false, historyModalId: null, + } + }, +}) diff --git a/public/js/pages/WarehouseLocation/WarehouseLocation.js b/public/js/pages/WarehouseLocation/WarehouseLocation.js new file mode 100644 index 000000000..db6c4f032 --- /dev/null +++ b/public/js/pages/WarehouseLocation/WarehouseLocation.js @@ -0,0 +1,13 @@ +Vue.component('warehouse-location', { + //language=Vue + template: ` + + + + + `, data() { + return { + window: window, historyModal: false, historyModalId: null, + } + }, +}) diff --git a/public/js/pages/WarehouseOrderRecommendation/WarehouseOrderRecommendation.js b/public/js/pages/WarehouseOrderRecommendation/WarehouseOrderRecommendation.js new file mode 100644 index 000000000..9f35663b7 --- /dev/null +++ b/public/js/pages/WarehouseOrderRecommendation/WarehouseOrderRecommendation.js @@ -0,0 +1,12 @@ +Vue.component('warehouse-order-recommendation', { + //language=Vue + template: ` + + + + `, data() { + return { + window: window, historyModal: false, historyModalId: null, + } + }, +}) diff --git a/public/plugins/axios/axios.inject.js b/public/plugins/axios/axios.inject.js new file mode 100644 index 000000000..ee884c086 --- /dev/null +++ b/public/plugins/axios/axios.inject.js @@ -0,0 +1,6 @@ +axios.interceptors.response.use(function (response) { + return response; +}, function (error) { + window.notify('error', error.response.data.message || 'Ein Fehler ist aufgetreten') + return Promise.reject(error); +}); diff --git a/public/plugins/vue/tt-components/css/tt-table.css b/public/plugins/vue/tt-components/css/tt-table.css index 7d47be8b9..235d1365b 100644 --- a/public/plugins/vue/tt-components/css/tt-table.css +++ b/public/plugins/vue/tt-components/css/tt-table.css @@ -2,6 +2,10 @@ position: relative; } +.tt-table { + /*width: unset !important;*/ +} + .tt-table.loading tbody:after { position: absolute; top: 0; @@ -151,4 +155,175 @@ input[type=number]::-webkit-outer-spin-button { td { vertical-align: middle !important; -} \ No newline at end of file +} + +/*TODO: export this classes somewhere else*/ + +.text-cyan { + color: var(--cyan); +} + +.text-green { + color: var(--green); +} + +.text-red { + color: var(--red); +} + +.text-yellow { + color: var(--yellow); +} + +.text-blue { + color: var(--blue); +} + +.text-purple { + color: var(--purple); +} + +.text-orange { + color: var(--orange); +} + +.text-pink { + color: var(--pink); +} + +.text-gray { + color: var(--gray); +} + +.text-white { + color: var(--white); +} + +.text-light { + color: var(--light); +} + +.text-dark { + color: var(--dark); +} + +.width-40 { + width: 40px; +} + +.width-80 { + width: 80px; +} +.width-120 { + width: 120px; +} + +.width-160 { + width: 160px; +} + +.width-200 { + width: 200px; +} + +.width-240 { + width: 240px; +} + +.width-280 { + width: 280px; +} + +.width-320 { + width: 320px; +} + +.width-360 { + width: 360px; +} + + + +/*TODO: remove this*/ +/* Styles for the component */ +.tt-expandable-shopping-cart { + position: fixed; + bottom: 65px; + right: 20px; + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 5px; + width: 50px; /* Initial width */ + height: 50px; + transition: width 0.3s ease, height 0.3s ease; +} + +.expanded { + width: 500px; /* Expanded width */ + height: 600px; /* Expanded height */ + z-index: 1000; +} + +.toggle-button { + background: none; + border: none; + cursor: pointer; + font-size: 24px; + padding: 10px; +} + +.cart-content { + padding: 15px; + display: none; /* Initially hidden */ +} + +.expanded .cart-content { + display: block; /* Show when expanded */ +} + +.cart-count { + background-color: #dc3545; /* Red background */ + color: white; + border-radius: 50%; + padding: 0 6px; + font-size: 12px; + position: absolute; + top: 5px; + right: 5px; +} + +.tt-expandable-shopping-cart.expanded { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + +} + +.tt-expandable-shopping-cart.expanded .cart-content { + padding-top: 8px; +} + +.tt-expandable-shopping-cart.expanded .cart-content h3 { + margin-top: 0; +} + + +.tt-expandable-shopping-cart.expanded .toggle-button { + display: grid; + grid-template-columns: 2fr 3fr auto auto; + grid-gap: 10px; + width: 100%; + justify-content: space-between; + align-items: center; + background-color: var(--gray) +} + +.tt-expandable-shopping-cart.expanded .toggle-button i { + justify-self: left; +} + +.tt-expandable-shopping-cart.expanded .cart-count { + position: unset !important; + width: 21px; + height: 21px; + grid-column-start: 3; +} diff --git a/public/plugins/vue/tt-components/tt-autocomplete.js b/public/plugins/vue/tt-components/tt-autocomplete.js index 37cee43e4..84712f01c 100644 --- a/public/plugins/vue/tt-components/tt-autocomplete.js +++ b/public/plugins/vue/tt-components/tt-autocomplete.js @@ -1,12 +1,15 @@ Vue.component('tt-autocomplete', { template: ` -
    +
    - -
    + +
    [], - } + items: { type: Array, default: () => [] }, + sm: { type: Boolean, default: true }, + row: { type: Boolean, default: false }, }, async mounted() { if (this.value && this.apiUrl) { @@ -70,6 +67,8 @@ Vue.component('tt-autocomplete', { } else if (this.value) { const selectedItem = this.items.find(item => item.value === this.value); this.displayValue = selectedItem ? selectedItem.text : ''; + } else { + this.displayValue = ''; } }, @@ -86,6 +85,7 @@ Vue.component('tt-autocomplete', { watch: { value(newValue) { const selectedItem = this.displayingItems.find(item => item.value === newValue); + console.log(selectedItem); this.displayValue = selectedItem ? selectedItem.text : ''; }, apiUrl() { @@ -95,7 +95,7 @@ Vue.component('tt-autocomplete', { methods: { onInput(event) { this.displayValue = event.target.value; - this.$emit('input', undefined); + this.$emit('input', ''); this.fetchSuggestions(); }, onFocus() { diff --git a/public/plugins/vue/tt-components/tt-checkbox.js b/public/plugins/vue/tt-components/tt-checkbox.js new file mode 100644 index 000000000..53247e9d2 --- /dev/null +++ b/public/plugins/vue/tt-components/tt-checkbox.js @@ -0,0 +1,42 @@ +Vue.component('tt-checkbox', { + props: { + label: String, + required: Boolean, + row: Boolean, + value: [String, Number], + hint: String, + additionalProps: Object, + sm: { type: Boolean, default: false }, + }, + data() { + return { + checkedValue: this.value, + }; + }, + watch: { + value(val) { + this.checkedValue = val; + } + }, + template: ` +
    + + + + + {{ hint }} +
    + ` +}); diff --git a/public/plugins/vue/tt-components/tt-input.js b/public/plugins/vue/tt-components/tt-input.js index 7ee8c38fc..bd35fd7c3 100644 --- a/public/plugins/vue/tt-components/tt-input.js +++ b/public/plugins/vue/tt-components/tt-input.js @@ -4,6 +4,7 @@ Vue.component('tt-input', { type: String, placeholder: String, required: Boolean, + row: Boolean, value: [String, Number], hint: String, additionalProps: Object, @@ -20,15 +21,17 @@ Vue.component('tt-input', { } }, template: ` -
    +
    + +
    + ` +}) \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-select.js b/public/plugins/vue/tt-components/tt-select.js index c6969e43e..a08b4e652 100644 --- a/public/plugins/vue/tt-components/tt-select.js +++ b/public/plugins/vue/tt-components/tt-select.js @@ -1,5 +1,13 @@ Vue.component('tt-select', { - props: ['options', 'label', 'required', 'value', 'suffix'], + props: { + options: {type: Array, required: true}, + label: {type: String, required: false}, + required: {type: Boolean, default: false}, + value: {type: [String, Number], required: false}, + suffix: {type: String, required: false}, + sm: {type: Boolean, default: false}, + row: {type: Boolean, default: false}, + }, data() { return { selectedOption: undefined, @@ -14,9 +22,13 @@ Vue.component('tt-select', { }, }, template: ` -
    - -