update logic

This commit is contained in:
Christian Seyfferth 2021-07-18 01:04:13 +02:00
parent 20490ef9b6
commit 8d18e5838e
36 changed files with 7103 additions and 3695 deletions

View File

@ -1,6 +0,0 @@
DB_SERVER=localhost
DB_NAME=inventur
DB_USER=inventur
DB_PASSWORD=inventur
NODE_ENV=production

View File

@ -23,6 +23,8 @@
<InventoryTable
ref="inventory"
:articles="articles"
:groups="groups"
:dimensions="dimensions"
/>
</div>
<div
@ -37,120 +39,11 @@
id="article"
class="col s12"
>
<div
v-for="(a, index) in articles"
:key="'article_'+index"
class="card"
>
<div class="card-content row">
<span class="card-title">{{ a.name }}</span>
<div class="input-field inline col s8">
<input
:id="'a_name_'+index"
v-model="a.name"
placeholder="Artikelname"
class="validate"
>
<label
:for="'a_name_'+index"
class="active"
>Name</label>
</div>
<div class="input-field inline col s4">
<input
:id="'a_short_'+index"
v-model="a.short"
placeholder="Kurzname"
class="validate"
>
<label
:for="'a_short_'+index"
class="active"
>Kürzel</label>
</div>
<div class="input-field col s8">
<input
:id="'a_csize_'+index"
v-model.number="a.content.size"
placeholder="Gesamtinhalt"
type="number"
class="validate"
step="0.01"
>
<label
:for="'a_csize_'+index"
class="active"
>Gesamtinhalt</label>
</div>
<div class="input-field col s4">
<input
:id="'a_dim_'+index"
v-model="a.dimension"
placeholder="Dimension"
class="validate"
max="5"
>
<label
:for="'a_dim_'+index"
class="active"
>Dimension</label>
<span class="helper-text">z.B. Liter(l), Stück(Stk.)</span>
</div>
<div class="input-field col s4">
<input
:id="'a_psize_'+index"
v-model.number="a.portion.size"
placeholder="Gesamtinhalt"
type="number"
class="validate"
step="0.01"
max="5"
>
<label
:for="'a_psize_'+index"
class="active"
>Portionsinhalt</label>
</div>
<div class="input-field col s4">
<input
:id="'a_ptype_'+index"
v-model="a.portion.type"
placeholder="Art"
class="validate"
max="5"
>
<label
:for="'a_ptype_'+index"
class="active"
>Portionsbezeichnung</label>
</div>
<div class="input-field col s4">
<input
:id="'a_pprice_'+index"
v-model.number="a.portion.price"
placeholder="Preis"
type="number"
step="0.01"
class="validate"
>
<label
:for="'a_pprice_'+index"
class="active"
>Portionspreis in </label>
</div>
<div class="col s12 right">
<span class="right">Gesamtpreis {{ a.ContentPrice }}</span>
</div>
</div>
<div class="card-action">
<a
class
href="#"
>Artikel löschen</a>
</div>
</div>
<ArticlesView
:articles="articles"
:groups="groups"
:dimensions="dimensions"
/>
</div>
</transition>
</div>
@ -165,17 +58,6 @@
<i class="large material-icons">more_vert</i>
</a>
<ul>
<li v-show="view == 'article'">
<a
href="#"
class="btn-floating tooltipped"
data-position="left"
data-tooltip="Artikelliste speichern"
@click="storeArticles"
>
<i class="material-icons">save</i>
</a>
</li>
<li v-show="view == 'article'">
<a
href="#"
@ -217,20 +99,23 @@
<script>
import Calculator from './components/Calculator';
import InventoryTable from './components/InventoryTable';
import ArticlesView from './components/ArticlesView';
import NavigationBar from './components/NavigationBar';
import {Article, thawArticle} from './model/article';
export default {
name: "App",
components: { InventoryTable, NavigationBar, Calculator },
components: { InventoryTable, NavigationBar, Calculator, ArticlesView },
data() {
return {
articles: [],
dimensions: [],
groups: [],
bon: [],
ready: false,
caretPosition: 0,
fab: null,
view: "inventory",
view: "article",
location: 'fairwertbar'
};
},
computed: {
@ -245,9 +130,14 @@ export default {
default:
return "red";
}
},
ready() {
return this.dimensions.length && this.groups.length;
}
},
created: function() {
this.loadDimensions();
this.loadGroups();
this.loadArticles();
},
mounted() {
@ -261,35 +151,59 @@ export default {
addArticle: function() {
this.articles.push(new Article());
},
storeArticles: function() {
loadDimensions: function() {
var app = this;
this.$http
.post(
"/api/artikel",
JSON.stringify(this.articles)
)
.get("api/dimension")
.then(response => {
this.$M.toast({ html: response.body });
return response.data;
})
.then(json => {
if (json != null) {
json.forEach(element => {
app.dimensions.push(element);
});
}
})
.then(() => {
this.$M.toast({ html: "Dimensionen wurden geladen." });
});
},
loadGroups: function() {
var app = this;
this.$http
.get("api/group")
.then(response => {
return response.data;
})
.then(json => {
if (json != null) {
json.forEach(element => {
app.groups.push(element);
});
}
})
.then(() => {
this.$M.toast({ html: "Artikelgruppen wurden geladen." });
});
},
loadArticles: function() {
var app = this;
this.$http
.get("api/artikel/theater")
.get("api/artikel")
.then(response => {
return response.data;
})
.then(json => {
console.log(json);
json.forEach(element => {
app.articles.push(thawArticle(element));
});
if (json != null) {
json.forEach(element => {
app.articles.push(thawArticle(element));
});
}
})
.then(() => {
this.$M.toast({ html: "Artikel wurden geladen." });
})
.then(() => {
this.$M.updateTextFields();
app.ready = true;
});
},
exportInventur() {

View File

@ -0,0 +1,315 @@
<template>
<div
class="card"
>
<div class="card-content">
<span class="card-title">
{{ article.name }}
</span>
</div>
<div class="card-tabs">
<ul class="tabs tabs-fixed-width">
<li class="tab">
<a
:href="'#' + identifier + '_info'"
class="active"
>Info</a>
</li>
<li class="tab">
<a :href="'#' + identifier + '_variants'">Varianten</a>
</li>
</ul>
</div>
<div class="card-content">
<div
:id="identifier + '_info'"
class="row"
>
<div class="input-field inline col s8">
<input
:id="'a_name_' + article.id"
v-model="article.name"
placeholder="Artikelname"
class="validate"
>
<label
:for="'a_name_' + article.id"
class="active"
>Name</label>
</div>
<div class="input-field inline col s4">
<input
:id="'a_short_' + article.id"
v-model="article.short"
placeholder="Kurzname"
class="validate"
>
<label
:for="'a_short_' + article.id"
class="active"
>Kürzel</label>
</div>
<div class="input-field col s4">
<materialize-select
:id="'a_group_' + article.id"
v-model.number="article.group"
:value="article.group"
placeholder="Artikelgruppe"
>
<option
v-for="group in groups"
:key="group.id"
:value="group.id"
>
{{ group.name }}
</option>
</materialize-select>
<label :for="'a_group_' + article.id">Artikelgruppe</label>
</div>
<div class="input-field col s4">
<input
:id="'a_csize_' + article.id"
v-model.number="article.content.size"
placeholder="Gesamtinhalt"
type="number"
class="validate"
step="0.01"
>
<label
:for="'a_csize_' + article.id"
class="active"
>Gesamtinhalt</label>
</div>
<div class="input-field col s4">
<materialize-select
:id="'a_dim_' + article.id"
v-model.number="article.dimension"
:value="article.dimension"
placeholder="Dimension"
>
<option
v-for="dim in dimensions"
:key="dim.id"
:value="dim.id"
>
{{ dim.name }}
</option>
</materialize-select>
<label :for="'a_dim_' + article.id">Dimension</label>
</div>
<div class="input-field col s4">
<input
:id="'a_psize_' + article.id"
v-model.number="article.portion.size"
placeholder="Gesamtinhalt"
type="number"
class="validate"
step="0.01"
max="5"
>
<label
:for="'a_psize_' + article.id"
class="active"
>Portionsinhalt</label>
</div>
<div class="input-field col s4">
<MaterializeSelect
:id="'a_ptype_' + article.id"
v-model.number="article.portion.type"
:value="article.portion.type"
placeholder="Art"
>
<option
v-for="dim in dimensions"
:key="dim.id"
:value="dim.id"
>
{{ dim.name }}
</option>
</MaterializeSelect>
<label :for="'a_ptype_' + article.id">Portionsbezeichnung</label>
</div>
<div class="input-field col s4">
<input
:id="'a_pprice_' + article.id"
v-model.number="article.portion.price"
placeholder="Preis"
type="number"
step="0.01"
class="validate"
>
<label
:for="'a_pprice_' + article.id"
class="active"
>Portionspreis in </label>
</div>
<div class="col s12 right">
<span
class="right"
>Gesamtpreis
<TweenedNumber
:wert="article.ContentPrice"
:einheit="'€'"
:precision="2"
/></span>
</div>
</div>
<div
:id="identifier + '_variants'"
class="row"
>
<div class="col s12">
<ArticleVariant
v-for="(variant, index) in variants"
:key="'article_' + article.id + '_variant_' + index"
:identifier="'article_' + article.id + '_variant_' + index"
:variant="variant"
:article="article"
/>
<a
href="#"
@click="addVariant(article)"
>Variante hinzufügen</a>
</div>
</div>
</div>
<div class="card-action">
<a
href="#"
@click="deleteArticle(article)"
>Artikel löschen</a>
<a
v-if="changed"
href="#"
@click="storeArticle(article)"
>Artikel speichern</a>
</div>
</div>
</template>
<script>
import { Article, thawArticle } from '../model/article';
import MaterializeSelect from "./MaterializeSelect";
import TweenedNumber from "./TweenedNumber";
import ArticleVariant from "./ArticleVariant";
import { thawVariant, Variant } from '../model/variant';
export default {
name: "Article",
components: {MaterializeSelect, TweenedNumber, ArticleVariant},
props: {
a: {
type: Object,
default() { return new Article(); }
},
dimensions: {
type: Array,
default() { return []; }
},
groups: {
type: Array,
default() { return []; }
},
identifier: {
type: String,
}
},
data() {
return {
changed: false,
article: null,
original: null,
variants: [],
tabs: null
}
},
computed: {
},
watch: {
article: {
handler(newValue, oldVal) {
this.changed = JSON.stringify(newValue) != this.original;
},
deep: true
}
},
created() {
this.article = this.a;
this.original = JSON.stringify(this.a);
this.loadVariants(this.article);
},
mounted() {
var tabs = this.$el.getElementsByClassName("tabs")[0];
M.Tabs.init(tabs, {});
this.tabs = M.Tabs.getInstance(tabs);
this.$M.updateTextFields();
},
methods: {
storeArticle: function(article) {
this.$http
.post(
"/api/artikel" + (article.id > 0 ? '/' + article.id : ''),
JSON.stringify(article),
{
headers: {
'Content-Type': 'application/json'
}
}
)
.then(response => {
this.$M.toast({ html: response.data + " Artikel aktualisiert." });
this.article = thawArticle(response.data);
})
.catch(error => {
this.$M.toast({ html: response });
});
},
deleteArticle: function(article) {
this.$http
.delete(
"/api/artikel/" + article.id,
)
.then(response => {
this.$emit('delete-article', article);
this.$M.toast({ html: response.data + " Artikel gelöscht." });
})
.catch(error => {
this.$M.toast({ html: response });
});
},
addVariant(article) {
var variant = new Variant();
variant.ArticleId = article.id;
this.variants.push(variant);
},
loadVariants: function(article) {
this.$http
.get(
"/api/artikel/" + article.id + '/varianten',
)
.then(response => {
return response.data;
})
.then(json => {
if (json != null) {
json.forEach(element => {
this.variants.push(thawVariant(element));
});
}
})
.then(() => {
this.$M.updateTextFields();
})
.catch(error => {
this.$M.toast({ html: error });
});
},
}
};
</script>

View File

@ -0,0 +1,100 @@
<template>
<div class="row">
<div class="input-field col s4">
<input
:id="identifier + '_name'"
v-model="variant.name"
placeholder="Variantenbezeichnung"
type="text"
class="validate"
>
<label
:for="identifier + '_name'"
class="active"
>Variantenbezeichnung</label>
</div>
<div class="input-field col s4">
<input
:id="identifier + '_price'"
v-model.number="variant.price"
placeholder="Preis"
type="number"
class="validate"
step="0.01"
>
<label
:for="identifier + '_price'"
class="active"
>Preis</label>
</div>
<div class="input-field col s2">
<input
:value="article.PortionPrice + variant.Price"
class="validate"
disabled
>
</div>
<div class="input-field col s2">
<a
href="#"
@click="storeVariant(variant)"
>Variante speichern</a>
</div>
</div>
</template>
<script>
import { Article } from '../model/article';
import { thawVariant, Variant } from '../model/variant'
export default {
name: 'Variant',
props: {
variant: {
type: Object,
default() { return new Variant(); }
},
article: {
type: Object,
default() { return new Article(); }
},
identifier: {
type: String
}
},
methods: {
storeVariant: function(variant) {
variant.ArticleId = this.article.id;
this.$http
.post(
"/api/varianten" + (variant.id > 0 ? '/' + variant.id : ''),
JSON.stringify(variant),
{
headers: {
'Content-Type': 'application/json'
}
}
)
.then(response => {
this.$M.toast({ html: response.data + " Variante aktualisiert." });
this.variant = thawVariant(response.data);
})
.catch(error => {
this.$M.toast({ html: error });
});
},
deleteVariant: function(variant) {
this.$http
.delete(
"/api/varianten/" + variant.id,
)
.then(response => {
this.$emit('delete-variant', variant);
this.$M.toast({ html: response.data + " Varianten gelöscht." });
})
.catch(error => {
this.$M.toast({ html: response });
});
},
}
}
</script>

View File

@ -0,0 +1,51 @@
<template>
<div>
<ArticleCard
v-for="(a, index) in articles"
:key="'article_' + index"
:identifier="'article_' + index"
:a="a"
:dimensions="dimensions"
:groups="groups"
@delete-article="removeFromList(a)"
/>
</div>
</template>
<script>
import ArticleCard from "./ArticleCard";
export default {
name: "ArticlesView",
components: {ArticleCard},
props: {
articles: {
type: Array,
default() { return []; }
},
groups: {
type: Array,
default() { return []; }
},
dimensions: {
type: Array,
default() { return []; }
}
},
data() {
return {
}
},
computed: {
},
mounted() {
},
methods: {
removeFromList(article) {
this.articles.splice(this.articles.indexOf(article),1);
}
}
};
</script>

View File

@ -2,7 +2,7 @@
<tr
style="border: 0;"
class="inventory-item"
:class="{
:class="{
'teal lighten-5' : item.Sale > 0,
'orange darken-2 white-text' : item.Sold %1 !=0,
'red darken-2 white-text' : item.Sale < 0,
@ -26,20 +26,20 @@
>
{{ item[prop] }}
</td>
<td
class="right-align border-bottom border-diagram"
>
<TweenedNumber
:style="{
'border-image': 'linear-gradient(to right , rgba(0,150,136,.01) 0%, rgba(0,150,136,.3) '
+ item.Sold*100/total
:style="{
'border-image': 'linear-gradient(to right , rgba(0,150,136,.01) 0%, rgba(0,150,136,.3) '
+ item.Sold*100/total
+ '%, transparent '
+ item.Sold*100/total
+'%,transparent 100%) 1'
}"
:wert="item.Sold"
:einheit="item.article.PortionType"
:einheit="Dimension.short"
:precision="item.PortionPrecision"
/>
</td>
@ -74,6 +74,12 @@ export default {
total: {
type: Number,
default: 0
},
dimensions: {
type: Array,
default() {
return [];
}
}
},
data() {
@ -89,10 +95,13 @@ export default {
},
SaleAnimated() {
return this.tweenedSale.toFixed(2);
},
Dimension() {
return this.dimensions.find(element => element.id == this.item.article.PortionType)
}
},
watch: {
},
methods: {
setCaretPosition(el){
@ -139,15 +148,15 @@ export default {
if (event.data.match(regex)){
var value = event.target.innerText;
this.item[property] = Number(value);
}
}
}
this.setCaretPosition(event.target);
this.setCaretPosition(event.target);
},
}
}
</script>
<style>
</style>
</style>

View File

@ -29,6 +29,7 @@
<InventoryItem
v-for="(item,index) in inventory"
:key="'item-'+index"
:dimensions="dimensions"
:item="item"
:total="sold"
/>
@ -68,6 +69,18 @@ export default {
default() {
return [];
}
},
groups: {
type: Array,
default() {
return [];
}
},
dimensions: {
type: Array,
default() {
return [];
}
}
},
data() {
@ -148,4 +161,4 @@ export default {
</script>
<style>
</style>
</style>

View File

@ -0,0 +1,75 @@
<template>
<select :value="value">
<option
value=""
disabled
selected
>
Bitte wählen
</option>
<slot />
</select>
</template>
<script>
export default {
name: "MaterializeSelect",
props: {
value: {
type: [Number, String],
default: "",
},
},
/**
* @description Component local variables
* @return {Object} data
* @return {undefined|FormSelect} data.instance
*/
data() {
return {
instance: undefined,
};
},
watch: {
value() {
this.instance.destroy();
this.$nextTick(() => (this.instance = this.initializeSelect()));
},
},
mounted() {
this.instance = this.initializeSelect();
this.$el.addEventListener("change", this.changeHandler);
},
destroyed() {
this.$el.removeEventListener("change", this.changeHandler);
},
methods: {
/**
* @description Initialize a new Materialize select component
* @param {Object} options
* @return {FormSelect}
* @see https://materializecss.com/select.html#options
*/
initializeSelect(options = {}) {
return M.FormSelect.init(this.$el, options);
},
/**
* @description Send the proper input event to the parents components
* @param {Event} event
* @param {HTMLSelectElement} target
* @see https://developer.mozilla.org/fr/docs/Web/API/Event/target
*/
changeHandler({ target }) {
this.$emit("input", target.value);
},
},
};
</script>

View File

@ -8,12 +8,13 @@
<ul class="tabs tabs-transparent">
<li class="tab">
<a
class="active"
@click.stop.prevent="changeTab('inventory', $event)"
>Inventur</a>
</li>
<li class="tab">
<a
class="active"
TweenedNumber
@click.stop.prevent="changeTab('article', $event)"
>Artikelliste</a>
</li>
@ -39,7 +40,7 @@ export default {
},
data() {
return {
tabs: []
tabs: []
}
},
mounted() {
@ -60,4 +61,4 @@ export default {
<style>
</style>
</style>

View File

@ -1,12 +1,11 @@
<template>
<div class="tweened">
<div>{{ intAnimated }}</div>
<div v-if="precision>0">
<span class="tweened">
{{ intAnimated }}
<span v-if="precision>0">
,{{ deciAnimated }}
</div>
<div v-else />
<div>{{ einheit }}</div>
</div>
</span>
{{ einheit }}
</span>
</template>
<script>
@ -31,15 +30,15 @@ export default {
},
data(){
return {
tweenedValue: 0,
tweenedValue: this.wert,
}
},
computed: {
valueAnimated() {
return this.tweenedValue.toLocaleString(
"de-DE",
{
maximumFractionDigits: 2,
"de-DE",
{
maximumFractionDigits: 2,
minimumFractionDigits: this.precision
}
);

View File

@ -1,8 +1,10 @@
export class Article{
constructor() {
this.id = 0;
this.name = "";
this.short = "";
this.dimension = "";
this.dimension = 0;
this.group = 0;
this.content = {
size : 0,
price: 0
@ -10,7 +12,7 @@ export class Article{
this.portion = {
size : 0,
price : 0,
type : ""
type : 0
};
}
get Name() {
@ -55,6 +57,12 @@ export class Article{
set PortionPrice(value) {
this.portion.price = value;
}
get Group() {
return this.group;
}
set Group(value) {
this.group = value;
}
get ContentPrice() {
return this.Portions * this.portion.price;
}
@ -68,13 +76,15 @@ Article.thaw = function (json) {
article.id = json.id;
article.Name = json.name;
article.Short = json.short;
article.Dimension = json.dimension;
article.ContentSize = json.content.size;
article.PortionPrice = json.portion.price;
article.PortionSize = json.portion.size;
article.PortionType = json.portion.type;
article.Dimension = json.content_dimension;
article.ContentSize = json.content_size;
article.PortionPrice = json.portion_price;
article.PortionSize = json.portion_size;
article.PortionType = json.portion_dimension;
article.Group = json.group_id;
article.variants = json.variants;
return article;
};
export const thawArticle = Article.thaw;
export const thawArticle = Article.thaw;

38
app/js/model/variant.js Normal file
View File

@ -0,0 +1,38 @@
export class Variant{
constructor() {
this.id = 0;
this.name = "";
this.article_id = 0;
this.price = 0;
}
get Name() {
return this.name;
}
set Name(value) {
this.name = value;
}
get ArticleId() {
return this.article_id;
}
set ArticleId(value) {
this.article_id = value;
}
get Price() {
return this.price;
}
set Price(value) {
this.price = value;
}
}
Variant.thaw = function (json) {
var variant = new Variant();
variant.id = json.id;
variant.Name = json.name;
variant.ArticleId = json.article_id;
variant.Price = json.price;
return variant;
};
export const thawVariant = Variant.thaw;

View File

@ -9,24 +9,18 @@
],
"require": {
"slim/slim": "^4.3",
"vlucas/phpdotenv": "^4.1",
"catfan/medoo": "^1.7",
"slim/psr7": "^0.6.0",
"slim/php-view": "^2.2",
"vlucas/phpdotenv": "^5.3",
"slim/psr7": "^1.4",
"slim/twig-view": "^3.0",
"php-di/php-di": "^6.0",
"symfony/webpack-encore-bundle": "^1.7",
"illuminate/database": "^6.10",
"illuminate/database": "^7.3",
"phpoffice/phpspreadsheet": "^1.10",
"monolog/monolog": "^2.0",
"illuminate/filesystem": "^6.12"
"illuminate/filesystem": "^7.3"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.5",
"doctrine/coding-standard": "^7.0",
"thecodingmachine/phpstan-strict-rules": "^0.12.0",
"thecodingmachine/safe": "^1.0",
"thecodingmachine/phpstan-safe-rule": "@dev"
"config": {
"sort-packages": true
},
"scripts": {
"csfix": "phpcbf",
@ -37,5 +31,9 @@
"psr-4": {
"Chrosey\\Inventur\\":"src"
}
},
"require-dev": {
"laravel/tinker": "^2.6",
"symfony/var-dumper": "^5.3"
}
}

3535
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Capsule\Manager as Capsule;
class AddBons extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$builder = Capsule::schema();
$builder->table('articles', function($table) {
$table->softDeletes();
});
$builder->create(
'bons',
function ($table) {
$table->increments('id');
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent();
}
);
$builder->create(
'bonitems',
function ($table) {
$table->increments('id');
$table->unsignedInteger('bon_id');
$table->foreign('bon_id')->references('id')->on('bons');
$table->unsignedInteger('article_id');
$table->foreign('article_id')->references('id')->on('articles');
$table->unsignedInteger('count');
}
);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$builder = Capsule::schema();
$builder->table('articles', function($table) {
$table->dropSoftDeletes();
});
$builder->drop('bons');
$builder->drop('bonitems');
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Capsule\Manager as Capsule;
class AddSubarticles extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$builder = Capsule::schema();
$builder->table('articles', function($table) {
$table->unsignedInteger('main_article_id')->nullable();
$table->foreign('main_article_id')->references('id')->on('articles');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$builder = Capsule::schema();
$builder->table('articles', function($table) {
$table->dropColum('main_article_id');
});
}
}

View File

@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Capsule\Manager as Capsule;
class ArticleVariants extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$builder = Capsule::schema();
$builder->create(
'variants',
function ($table) {
$table->increments('id');
$table->unsignedInteger('article_id');
$table->foreign('article_id')->references('id')->on('articles');
$table->string('name');
$table->string('short');
$table->float('price', 8, 2);
$table->timestamps();
}
);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$builder = Capsule::schema();
$builder->drop('variants');
}
}

View File

@ -1 +1 @@
[{"name":"Edelpils","short":"Pils","dimension":"l","content":{"size":0.33000000000000002,"price":0},"portion":{"size":0.33000000000000002,"price":3.5,"type":"Fl."},"group":"Bier"},{"name":"Schwarzbier","short":"Sb","dimension":"l","content":{"size":0.33000000000000002,"price":0},"portion":{"size":0.33000000000000002,"price":3.5,"type":"Fl."},"group":"Bier"},{"name":"Sch\u00f6fferhofer","short":"Sch\u00f6","dimension":"l","content":{"size":0.33000000000000002,"price":0},"portion":{"size":0.33000000000000002,"price":3.5,"type":"Fl."},"group":"Bier"},{"name":"Bitburger af.","short":"0%","dimension":"l","content":{"size":0.33000000000000002,"price":0},"portion":{"size":0.33000000000000002,"price":3.5,"type":"Fl."},"group":"Bier"},{"name":"Kellerbier","short":"Keller","dimension":"l","content":{"size":0.33000000000000002,"price":0},"portion":{"size":0.33000000000000002,"price":3.5,"type":"Fl."},"group":"Bier"},{"name":"Weiswein","short":"WW","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":6,"type":"Gl."},"group":"Wein und Sekt"},{"name":"Ros\u00e9wein","short":"Ros\u00e9","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":6.5,"type":"Gl."},"group":"Wein und Sekt"},{"name":"Rotwein","short":"RW","dimension":"l","content":{"size":0.75,"price":0},"portion":{"size":0.20000000000000001,"price":6.5,"type":"Gl."},"group":"Wein und Sekt"},{"name":"Secco","short":"Sekt","dimension":"l","content":{"size":0.75,"price":0},"portion":{"size":0.10000000000000001,"price":6,"type":"Gl."},"group":"Wein und Sekt"},{"name":"B-Saft","short":"BS","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":3.5,"type":"Gl."},"group":"afG offen"},{"name":"K-Saft","short":"KS","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":3.5,"type":"Gl."},"group":"afG offen"},{"name":"O-Saft","short":"OS","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":3.5,"type":"Gl."},"group":"afG offen"},{"name":"M-Saft","short":"MS","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":3.5,"type":"Gl."},"group":"afG offen"},{"name":"Vita Cola","short":"Co","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":2.5,"type":"Gl."},"group":"afG offen"},{"name":"Vita Orange","short":"Fa","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":2.5,"type":"Gl."},"group":"afG offen"},{"name":"Vita Zitrone","short":"Spr","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":2.5,"type":"Gl."},"group":"afG offen"},{"name":"Tonic","short":"To","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."},"group":"afG Gastro"},{"name":"Bitter Lemon","short":"BL","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."},"group":"afG Gastro"},{"name":"Ginger Ale","short":"GA","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."},"group":"afG Gastro"},{"name":"Apfelschorle","short":"AS","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."},"group":"afG Gastro"},{"name":"Cola light","short":"Clight","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."},"group":"afG Gastro"},{"name":"TWQ naturell","short":"W-","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":2.5,"type":"Fl."},"group":"afG Gastro"},{"name":"TWQ medium","short":"W","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":2.5,"type":"Fl."},"group":"afG Gastro"},{"name":"TWQ classic","short":"W+","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":2.5,"type":"Fl."},"group":"afG Gastro"},{"name":"Kaffee","short":"TK","dimension":"T","content":{"size":1,"price":0},"portion":{"size":1,"price":2.5,"type":"T"},"group":"Hei\u00dfgetr\u00e4nke"},{"name":"Latte Macchiato","short":"LM","dimension":"Gl","content":{"size":1,"price":0},"portion":{"size":1,"price":3.5,"type":"Gl."},"group":"Hei\u00dfgetr\u00e4nke"},{"name":"dopp. Esp.","short":"dEsp","dimension":"T","content":{"size":1,"price":0},"portion":{"size":1,"price":3.5,"type":"T"},"group":"Hei\u00dfgetr\u00e4nke"},{"name":"Brezel","short":"Br","dimension":"stk","content":{"size":1,"price":0},"portion":{"size":1,"price":2.5,"type":"Stk."},"group":"Fingerfood"},{"name":"Schokoriegel","short":"Schoko","dimension":"stk","content":{"size":1,"price":0},"portion":{"size":1,"price":2,"type":"Stk."},"group":"Fingerfood"},{"name":"Duplo","short":"Duplo","dimension":"stk","content":{"size":1,"price":0},"portion":{"size":1,"price":1,"type":"Stk."},"group":"Fingerfood"}]
[{"name":"Edelpils","short":"Pils","dimension":"l","content":{"size":0.33000000000000002,"price":0},"portion":{"size":0.33000000000000002,"price":3.5,"type":"Fl."}},{"name":"Schwarzbier","short":"Sb","dimension":"l","content":{"size":0.33000000000000002,"price":0},"portion":{"size":0.33000000000000002,"price":3.5,"type":"Fl."}},{"name":"Sch\u00f6fferhofer","short":"Sch\u00f6","dimension":"l","content":{"size":0.33000000000000002,"price":0},"portion":{"size":0.33000000000000002,"price":3.5,"type":"Fl."}},{"name":"Bitburger af.","short":"0%","dimension":"l","content":{"size":0.33000000000000002,"price":0},"portion":{"size":0.33000000000000002,"price":3.5,"type":"Fl."}},{"name":"Weiswein","short":"WW","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":6,"type":"Gl."}},{"name":"Rotwein","short":"RW","dimension":"l","content":{"size":0.75,"price":0},"portion":{"size":0.20000000000000001,"price":6.5,"type":"Gl."}},{"name":"Secco","short":"Sekt","dimension":"l","content":{"size":0.75,"price":0},"portion":{"size":0.10000000000000001,"price":6,"type":"Gl."}},{"name":"B-Saft","short":"BS","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":3.5,"type":"Gl."}},{"name":"K-Saft","short":"KS","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":3.5,"type":"Gl."}},{"name":"O-Saft","short":"OS","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":3.5,"type":"Gl."}},{"name":"G-Saft","short":"GS","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":3.5,"type":"Gl."}},{"name":"Vita Cola","short":"Co","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":2.5,"type":"Gl."}},{"name":"Vita Orange","short":"Fa","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":2.5,"type":"Gl."}},{"name":"Vita Zitrone","short":"Spr","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.20000000000000001,"price":2.5,"type":"Gl."}},{"name":"Tonic","short":"To","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."}},{"name":"Bitter Lemon","short":"BL","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."}},{"name":"Ginger Ale","short":"GA","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."}},{"name":"Apfelschorle","short":"AS","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."}},{"name":"TWQ naturell","short":"W-","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":2.5,"type":"Fl."}},{"name":"TWQ medium","short":"W","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":2.5,"type":"Fl."}},{"name":"TWQ classic","short":"W+","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":2.5,"type":"Fl."}},{"name":"Kaffee","short":"TK","dimension":"Tasse","content":{"size":1,"price":0},"portion":{"size":1,"price":2.5,"type":"T"}},{"name":"Latte Macchiato","short":"LM","dimension":"Glas","content":{"size":1,"price":0},"portion":{"size":1,"price":3.5,"type":"Gl."}},{"name":"dopp. Esp.","short":"dEsp","dimension":"Tasse","content":{"size":1,"price":0},"portion":{"size":1,"price":3.5,"type":"T"}},{"name":"Brezel","short":"Br","dimension":"St\u00fcck","content":{"size":1,"price":0},"portion":{"size":1,"price":2.5,"type":"Stk."}},{"name":"Schokoriegel","short":"Schoko","dimension":"St\u00fcck","content":{"size":1,"price":0},"portion":{"size":1,"price":2,"type":"Stk."}}]

9
env.example Normal file
View File

@ -0,0 +1,9 @@
DB_HOST=db
DB_NAME=db
DB_USER=db
DB_PASSWORD=db
DB_DRIVER=mysql
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_general_ci
NODE_ENV=production

3275
package-lock.json generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"build/app.css": "/build/app.css",
"build/app.js": "/build/app.js",
"build/vendors~app.css": "/build/vendors~app.css",
"build/vendors~app.js": "/build/vendors~app.js"
"build/vendors~app.js": "/build/vendors~app.js",
"build/app.css": "/build/app.css"
}

File diff suppressed because one or more lines are too long

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,83 @@
<?php
namespace Chrosey\Inventur\Controller;
use Chrosey\Inventur\Models\Article;
use Chrosey\Inventur\Models\Variant;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
class ArticleController extends BaseController
{
public function create(Request $request, Response $response, $args)
{
$artikel = dump($request->getParsedBody());
$article = new Article([
'name' => $artikel['name'],
'short' => $artikel['short'],
'content_size' => $artikel['content']['size'],
'content_dimension' => $artikel['dimension'],
'portion_size' => $artikel['portion']['size'],
'portion_price' => $artikel['portion']['price'],
'portion_dimension' => $artikel['portion']['type'],
'group_id' => $artikel['group'],
]);
$article->save();
$response->getBody()->write(\json_encode($article));
return $response
->withHeader('Content-Type', 'application/json');
}
public function update(Request $request, Response $response, $args)
{
$artikel = dump($request->getParsedBody());
$table = $this->db->table('articles');
$updatedRowsCount = $table
->where('id','=', $artikel['id'])
->update([
'name' => $artikel['name'],
'short' => $artikel['short'],
'content_size' => $artikel['content']['size'],
'content_dimension' => $artikel['dimension'],
'portion_size' => $artikel['portion']['size'],
'portion_price' => $artikel['portion']['price'],
'portion_dimension' => $artikel['portion']['type'],
'group_id' => $artikel['group'],
]);
$response->getBody()->write(\json_encode($updatedRowsCount));
return $response
->withHeader('Content-Type', 'application/json');
}
public function delete(Request $request, Response $response, $args)
{
$affectedRows = Article::where('id', $args['id'])->delete();
$response->getBody()->write(\json_encode($affectedRows));
return $response
->withHeader('Content-Type', 'application/json');
}
public function list(Request $request, Response $response, $args)
{
$articles = Article::all();
foreach ($articles as $article) {
$variants = Variant::where('article_id', $article->id)->get();
$article->variants = $variants;
}
$response->getBody()->write(\json_encode($articles));
return $response
->withHeader('Content-Type', 'application/json');
}
public function show()
{
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Chrosey\Inventur\Controller;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
class ArticleGroupController extends BaseController
{
public function create(Request $request, Response $response, $args)
{
$dimension = dump($request->getParsedBody());
}
public function update()
{
}
public function delete()
{
}
public function list(Request $request, Response $response, $args)
{
$table = $this->db->table('groups');
$items = $table->get();
dump($items);
$response->getBody()->write(\json_encode($items));
return $response
->withHeader('Content-Type', 'application/json');
}
public function show()
{
}
}

View File

@ -2,12 +2,28 @@
namespace Chrosey\Inventur\Controller;
use Illuminate\Database\Capsule\Manager;
use Monolog\Logger;
use Psr\Container\ContainerInterface;
class BaseController
{
protected $container;
/**
* Database
*
* @var Manager
*/
protected $db;
/**
* Logger
*
* @var Logger
*/
protected $logger;
function __construct(ContainerInterface $container)
{
$this->container = $container;

View File

@ -0,0 +1,42 @@
<?php
namespace Chrosey\Inventur\Controller;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
class DimensionController extends BaseController
{
public function create(Request $request, Response $response, $args)
{
$dimension = dump($request->getParsedBody());
}
public function update()
{
}
public function delete()
{
}
public function list(Request $request, Response $response, $args)
{
$table = $this->db->table('dimensions');
$dimensions = $table->get();
dump($dimensions);
$response->getBody()->write(\json_encode($dimensions));
return $response
->withHeader('Content-Type', 'application/json');
}
public function show()
{
}
}

View File

@ -6,7 +6,7 @@ use Illuminate\Database\Migrations\DatabaseMigrationRepository;
use Illuminate\Database\Migrations\Migrator;
use Illuminate\Filesystem\Filesystem;
class DatabaseController extends BaseController
class MigrationController extends BaseController
{
function migrate($request, $response, $args)

View File

@ -0,0 +1,63 @@
<?php
namespace Chrosey\Inventur\Controller;
use Chrosey\Inventur\Models\Variant;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
class VariantController extends BaseController
{
public function list(Request $request, Response $response, $args)
{
$items = Variant::where('article_id', $args['id'])->get();
dump($items);
$response->getBody()->write(\json_encode($items));
return $response
->withHeader('Content-Type', 'application/json');
}
public function show()
{
}
public function create(Request $request, Response $response, $args)
{
$data = dump($request->getParsedBody());
$variant = new Variant([
'name' => $data['name'],
'short' => $data['short'] ?: $data['name'],
'price' => $data['price'],
'article_id' => $data['article_id'],
]);
$variant->save();
$response->getBody()->write(\json_encode($variant));
return $response
->withHeader('Content-Type', 'application/json');
}
public function update(Request $request, Response $response, $args)
{
$data = dump($request->getParsedBody());
$table = $this->db->table('variants');
$updatedRowsCount = $table
->where('id','=', $data['id'])
->update([
'name' => $data['name'],
'short' => $data['short'] ?: $data['name'],
'price' => $data['price'],
'article_id' => $data['article_id'],
]);
$response->getBody()->write(\json_encode($updatedRowsCount));
return $response
->withHeader('Content-Type', 'application/json');
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Chrosey\Inventur\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class JsonBodyParserMiddleware implements MiddlewareInterface
{
public function process(Request $request, RequestHandler $handler): Response
{
$contentType = $request->getHeaderLine('Content-Type');
if (strstr($contentType, 'application/json')) {
$contents = json_decode(file_get_contents('php://input'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$request = $request->withParsedBody($contents);
}
}
return $handler->handle($request);
}
}

View File

20
src/Models/Article.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace Chrosey\Inventur\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Article extends Model
{
use SoftDeletes;
protected $fillable = ['name',
'short',
'content_size',
'content_dimension',
'portion_size',
'portion_price',
'portion_dimension',
'group_id'];
}

15
src/Models/Variant.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace Chrosey\Inventur\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Variant extends Model
{
protected $fillable = ['name',
'short',
'price',
'article_id'];
}

View File

@ -2,6 +2,7 @@
declare(strict_types=1);
use Chrosey\Inventur\Middleware\JsonBodyParserMiddleware;
use DI\Container;
use Illuminate\Database\Capsule\Manager as Capsule;
use Monolog\Handler\StreamHandler;
@ -9,6 +10,13 @@ use Monolog\Logger;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\ContextProvider\CliContextProvider;
use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Symfony\Component\VarDumper\Dumper\ServerDumper;
use Symfony\Component\VarDumper\VarDumper;
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/env.php';
@ -23,14 +31,14 @@ $container->set('view', static function () {
$container->set('db', static function () {
$capsule = new Capsule();
$capsule->addConnection([
'driver' => getenv('DB_DRIVER'),
'host' => getenv('DB_HOST'),
'database' => getenv('DB_NAME'),
'username' => getenv('DB_USER'),
'password' => getenv('DB_PASSWORD'),
'charset' => getenv('DB_CHARSET'),
'collation' => getenv('DB_COLLATION'),
'prefix' => getenv('DB_PREFIX'),
'driver' => $_ENV['DB_DRIVER'] ?: 'mysql',
'host' => $_ENV['DB_HOST'] ?: 'localhost',
'database' => $_ENV['DB_NAME'] ?: 'inventar',
'username' => $_ENV['DB_USER'] ?: 'dbuser',
'password' => $_ENV['DB_PASSWORD'] ?: 'password',
'charset' => $_ENV['DB_CHARSET'] ?: 'utf8',
'collation' => $_ENV['DB_COLLATION'] ?: 'utf8_general_ci',
'prefix' => $_ENV['DB_PREFIX'] ?: '',
]);
$capsule->setAsGlobal();
@ -49,10 +57,36 @@ $container->set('log', static function() {
$container->set('upload_directory', __DIR__ . '/../data/uploads');
$container->set('data_directory', __DIR__ . '/../data/');
$app = AppFactory::create();
$app->addMiddleware(new JsonBodyParserMiddleware());
$app->add(TwigMiddleware::createFromContainer($app));
$cloner = new VarCloner();
$fallbackDumper = in_array(PHP_SAPI, ['cli', 'phpdbg']) ? new CliDumper() : new HtmlDumper();
$dumper = new ServerDumper('tcp://127.0.0.1:9912', $fallbackDumper, [
'cli' => new CliContextProvider(),
'source' => new SourceContextProvider(),
]);
VarDumper::setHandler(function ($var) use ($cloner, $dumper) {
$dumper->dump($cloner->cloneVar($var));
});
/**
* Add Error Middleware
*
* @param bool $displayErrorDetails -> Should be set to false in production
* @param bool $logErrors -> Parameter is passed to the default ErrorHandler
* @param bool $logErrorDetails -> Display error details in error log
* @param LoggerInterface|null $logger -> Optional PSR-3 Logger
*
* Note: This middleware should be added last. It will not handle any exceptions/errors
* for middleware added after it.
*/
$errorMiddleware = $app->addErrorMiddleware(true, true, true, $container->get('log'));
require 'routes.php';
return $app;

View File

@ -2,41 +2,33 @@
declare(strict_types=1);
use Chrosey\Inventur\Controller\DatabaseController;
use Chrosey\Inventur\Controller\ArticleController;
use Chrosey\Inventur\Controller\ArticleGroupController;
use Chrosey\Inventur\Controller\MigrationController;
use Chrosey\Inventur\Controller\DimensionController;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Psr7\Stream;
use Slim\Psr7\UploadedFile;
use Slim\Routing\RouteCollectorProxy;
use Chrosey\Inventur\Controller\SpreadsheetController;
use Chrosey\Inventur\Controller\VariantController;
$app->get('/', function (Request $request, Response $response, $args) {
return $this->get('view')->render($response, 'frontend.html', []);
})->setName('frontend');
$app->group('/api', function (RouteCollectorProxy $group): void {
$group->post('/artikel', function (Request $request, Response $response, $args) {
$directory = $this->get('upload_directory');
$uploadedFile = $request->getUploadedFiles()['articles'];
$group->post('/artikel', ArticleController::class . ':create')->setName('article.create');
$group->post('/artikel/{id}', ArticleController::class . ':update')->setName('article.update');
$group->delete('/artikel/{id}', ArticleController::class . ':delete')->setName('article.delete');
$group->get('/artikel/{id}/varianten', VariantController::class . ':list')->setName('article.list.variants');
$group->post('/varianten', VariantController::class . ':create')->setName('variant.create');
$group->post('/varianten/{id}', VariantController::class . ':update')->setName('variant.update');
$group->delete('/varianten/{id}', VariantController::class . ':list')->setName('variant.delete');
$group->get('/artikel[/{location}]', ArticleController::class . ':list')->setName('article.list');
if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
$filename = moveUploadedFile($directory, $uploadedFile);
$response->write('uploaded ' . $filename . '<br/>');
}
return $response;
});
$group->get('/artikel/theater', function (Request $request, Response $response, $args) {
$file = __DIR__ . '/../data/articles.theater.json';
$fh = fopen($file, 'rb');
$stream = new Stream($fh);
return $response
->withHeader('Content-Type', 'application/json')
->withBody($stream);
});
$group->get('/dimension', DimensionController::class . ':list')->setName('dimension.list');
$group->get('/group', ArticleGroupController::class . ':list')->setName('group.list');
$group->post('/inventur', function (Request $request, Response $response, $args) {
$directory = $this->get('upload_directory');
@ -55,9 +47,9 @@ $app->group('/api', function (RouteCollectorProxy $group): void {
$group->get('/excel/download', SpreadsheetController::class . ':download');
});
$app->get('/migrate', DatabaseController::class . ':migrate')->setName('migrate');
$app->get('/migrate', MigrationController::class . ':migrate')->setName('migrate');
$app->get('/seed', DatabaseController::class . ':seed')->setName('seed');
$app->get('/seed', MigrationController::class . ':seed')->setName('seed');
/**
* Move Uploaded File to Target Destination