[TASK] Inital State

This commit is contained in:
chrosey
2020-01-13 16:09:25 +01:00
parent 0cef46b3b2
commit 42c50342ab
66 changed files with 53145 additions and 12594 deletions
+304
View File
@@ -0,0 +1,304 @@
<template>
<div>
<NavigationBar
:primary-color="primaryColor"
@changed-tab="changeView"
/>
<div class="container">
<div
v-if="!ready"
class="valign-wrapper"
style="height: 90vh"
>
<div class="progress">
<div class="indeterminate" />
</div>
</div>
<transition name="fade">
<div
v-if="ready && view == 'inventory'"
id="inventory"
class="col s12"
>
<InventoryTable
ref="inventory"
:articles="articles"
/>
</div>
<div
v-if="ready && view == 'calc'"
id="calc"
class="col s12"
>
<Calculator :articles="articles" />
</div>
<div
v-if="ready && view == 'article'"
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>
</div>
</transition>
</div>
<div
ref="fab"
class="fixed-action-btn"
>
<a
class="btn-floating btn-large"
:class="primaryColor"
>
<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="#"
class="btn-floating tooltipped"
data-position="left"
data-tooltip="Artikel hinzufügen"
@click="addArticle"
>
<i class="material-icons">add</i>
</a>
</li>
<li v-show="view == 'inventory'">
<a
href="#"
class="btn-floating tooltipped"
data-position="left"
data-tooltip="Inventur zurücksetzen"
@click="$refs.inventory.$emit('reset-inventur')"
>
<i class="material-icons">clear_all</i>
</a>
</li>
<li v-show="view == 'inventory'">
<a
href="#"
class="btn-floating tooltipped"
data-position="left"
data-tooltip="Inventur exportieren"
@click="$refs.inventory.$emit('export-inventur')"
>
<i class="material-icons">save</i>
</a>
</li>
</ul>
</div>
</div>
</template>
<script>
import Calculator from './components/Calculator';
import InventoryTable from './components/InventoryTable';
import NavigationBar from './components/NavigationBar';
import {Article, thawArticle} from './model/article';
export default {
name: "App",
components: { InventoryTable, NavigationBar, Calculator },
data() {
return {
articles: [],
bon: [],
ready: false,
caretPosition: 0,
fab: null,
view: "inventory",
};
},
computed: {
primaryColor() {
switch (this.view) {
case "inventory":
return "teal";
case "calc":
return "orange";
case "article":
return "blue";
default:
return "red";
}
}
},
created: function() {
this.loadArticles();
},
mounted() {
var fab = this.$refs.fab;
this.fab = this.$M.FloatingActionButton.init(fab, {});
},
methods: {
changeView(tabId) {
this.view = tabId;
},
addArticle: function() {
this.articles.push(new Article());
},
storeArticles: function() {
this.$http
.post(
"/api/artikel",
JSON.stringify(this.articles)
)
.then(response => {
this.$M.toast({ html: response.body });
});
},
loadArticles: function() {
var app = this;
this.$http
.get("api/artikel/theater")
.then(response => {
return response.data;
})
.then(json => {
console.log(json);
json.forEach(element => {
app.articles.push(thawArticle(element));
});
})
.then(() => {
this.$M.toast({ html: "Artikel wurden geladen." });
})
.then(() => {
this.$M.updateTextFields();
app.ready = true;
});
},
exportInventur() {
}
}
};
</script>
<style>
</style>
+124
View File
@@ -0,0 +1,124 @@
<template>
<div class="row pin-top">
<div class="col s12 m4 card darken-4 grey grey-text text-lighten-2">
<table class="card-content">
<tbody style="max-height:300px">
<tr
v-for="item in items"
v-show="item.count > 0"
:key="'bon_'+item.short"
>
<td class="right-align">
{{ item.count }} &times;
</td>
<td>{{ item.name }}</td>
<td class="right-align">
<TweenedNumber
:wert="item.sum"
:einheit="'€'"
:precision="2"
/>
</td>
</tr>
</tbody>
<tfoot>
<tr class="white-text">
<th class="right-align">
<TweenedNumber
:wert="totalCount"
/>
</th>
<th>
Artikel
</th>
<th class="right-align">
<TweenedNumber
:wert="totalSum"
:einheit="'€'"
:precision="2"
/>
</th>
</tr>
</tfoot>
</table>
</div>
<div class="col s12 m8">
<div class="row">
<div
v-for="a in items"
:key="'bon_btn_'+a.short"
class="col s3"
>
<button
class="waves-effect waves-light btn-large btn-flat col s12"
@click="a.count++;a.sum+=a.price;"
>
{{ a.short }}
</button>
</div>
</div>
<div class="row">
<div class="col s3">
<button
class="waves-effect waves-light btn-large orange col s12"
@click="resetBon"
>
Reset
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import TweenedNumber from './TweenedNumber';
export default {
name: "Calculator",
components: { TweenedNumber },
props: {
articles: {
type: Array,
default() { return []; }
}
},
data() {
return {
items: []
}
},
computed: {
totalSum() {
return this.items.reduce((total, item) => {
return total + item.sum;
}, 0);
},
totalCount() {
return this.items.reduce((total, item) => {
return total + item.count;
}, 0);
}
},
mounted() {
this.resetBon();
},
methods: {
resetBon() {
this.items = this.articles.map(a => {
return {
count: 0,
name: a.name,
short: a.short,
price: a.portion.price,
sum: 0,
}
});
}
}
}
</script>
<style>
</style>
+7
View File
@@ -0,0 +1,7 @@
.inventory-item {
transition: all .2s;
}
.inventory-item>* {
transition: all .2s .2s;
}
+153
View File
@@ -0,0 +1,153 @@
<template>
<tr
style="border: 0;"
class="inventory-item"
:class="{
'teal lighten-5' : item.Sale > 0,
'orange darken-2 white-text' : item.Sold %1 !=0,
'red darken-2 white-text' : item.Sale < 0,
}"
>
<th
class="right-align"
>
{{ item.article.name }}
</th>
<td
v-for="prop in ['start','fetched','end','lost']"
:key="item.article.short+'_'+prop"
class="border-bottom center-align"
:class="{active:classObject.active == prop}"
contenteditable
@keypress="restrictInput(prop, $event)"
@focus="onFocus(prop, $event)"
@blur="onBlur(prop, $event)"
>
{{ 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
+ '%, transparent '
+ item.Sold*100/total
+'%,transparent 100%) 1'
}"
:wert="item.Sold"
:einheit="item.article.PortionType"
:precision="item.PortionPrecision"
/>
</td>
<td
class="right-align border-bottom"
>
<TweenedNumber
:wert="item.Sale"
:einheit="'€'"
:precision="2"
/>
</td>
</tr>
</template>
<script>
import TweenedNumber from './TweenedNumber';
import './InventoryItem.css';
export default {
name:"InventoryItem",
components: {
TweenedNumber,
},
props: {
item: {
type: Object,
default() {
return {}
}
},
total: {
type: Number,
default: 0
}
},
data() {
return {
tweenedSold: 0,
tweenedSale: 0,
classObject: {active : null}
}
},
computed: {
SoldAnimated() {
return this.tweenedSold.toFixed(this.item.PortionPrecision);
},
SaleAnimated() {
return this.tweenedSale.toFixed(2);
}
},
watch: {
},
methods: {
setCaretPosition(el){
if (el != null) {
var range = document.createRange();
var sel = window.getSelection();
range.setStart(el.firstChild, el.innerText.length);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
el.focus();
}
},
restrictInput(prop, event){
var x = event.key;
console.log(prop + " inserting " + x)
if (x === "Enter" ) {
event.target.blur();
}
if (isNaN(x) && x != ',') {
event.preventDefault();
}
},
onFocus(property, event){
this.classObject.active = property;
var range = document.createRange();
var sel = window.getSelection();
range.selectNodeContents(event.target);
sel.removeAllRanges();
sel.addRange(range);
},
onBlur(property, event) {
this.classObject.active = null;
var value = event.target.innerText;
value = value.replace(',', '.')
console.log(property + " left value: " + value);
if (!isNaN(value)) {
this.item[property] = Number(value);
}
},
onInput(property, event){
if (event.inputType == "insertText") {
var regex = /\d+\.?\d{0,2}/;
if (event.data.match(regex)){
var value = event.target.innerText;
this.item[property] = Number(value);
}
}
this.setCaretPosition(event.target);
},
}
}
</script>
<style>
</style>
+25
View File
@@ -0,0 +1,25 @@
table.condensed td,
table.condensed th {
padding: 5px 5px;
}
.border-bottom {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.border-right {
border-right: 1px solid rgba(0, 0, 0, 0.12);
}
.border-diagram {
padding: 0;
}
.border-diagram>div {
margin-bottom: -3px;
border-radius: 0;
border-bottom: 3px solid transparent;
}
.active {
border-color: rgb(38, 166, 154);
border-width: 2px;
}
+151
View File
@@ -0,0 +1,151 @@
<template>
<table class="table condensed highlight">
<thead>
<tr>
<th class="right-align">
Artikel
</th>
<th class="center-align">
Beginn
</th>
<th class="center-align">
Zugang
</th>
<th class="center-align">
Ende
</th>
<th class="center-align">
Verlust
</th>
<th class="right-align">
Verkauft
</th>
<th class="right-align">
Summe
</th>
</tr>
</thead>
<tbody>
<InventoryItem
v-for="(item,index) in inventory"
:key="'item-'+index"
:item="item"
:total="sold"
/>
</tbody>
<tfoot>
<tr>
<th
colspan="6"
class="right-align"
>
Gesamtsumme:
</th>
<td class="right-align">
<TweenedNumber
:wert="sales"
:precision="2"
:einheit="'€'"
/>
</td>
</tr>
</tfoot>
</table>
</template>
<script>
import InventoryItem from "./InventoryItem";
import TweenedNumber from "./TweenedNumber";
import { InventoryArticle } from '../model/inventory_article';
import './InventoryTable.css';
export default {
name: "InventoryTable",
components: { InventoryItem, TweenedNumber },
props: {
articles: {
type: Array,
default() {
return [];
}
}
},
data() {
return {
inventory: []
};
},
computed: {
meta() {
return {
datum: new Date().toString(),
}
},
sales() {
var total_sales = this.inventory.reduce(function(total, item) {
return total + item.Sale;
}, 0);
return total_sales;
},
sold() {
var total_sold = this.inventory.reduce((total, item) => {
return total + item.Sold;
},0);
return total_sold;
}
},
mounted() {
this.$on('reset-inventur', this.resetInventory);
this.$on('export-inventur', this.exportInventory);
this.resetInventory();
},
methods: {
resetInventory() {
this.inventory = this.articles.map(item => {
return new InventoryArticle(item);
});
},
getInventurBlob() {
let inventur = this.inventory.map(T => {
return {
name: T.article.name,
preis: T.article.portion.price,
beginn: T.start,
zugang: T.fetched,
ende: T.end,
verlust: T.lost,
verkauft: T.Sold,
umsatz: T.Sale
};
});
const data = JSON.stringify({
meta: this.meta,
data: inventur
});
const blob = new Blob([data],{type: 'text/json' });
return blob;
},
exportInventory() {
var formData = new FormData();
formData.append('file', this.getInventurBlob(), 'inventur.json');
this.$http
.post(
"/api/inventur",
formData,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
)
.then(response => {
this.$M.toast({ html: response.body });
});
}
}
};
</script>
<style>
</style>
+63
View File
@@ -0,0 +1,63 @@
<template>
<nav
class="nav-extended"
:class="primaryColor"
>
<div class="nav-wrapper container" />
<div class="nav-content container">
<ul class="tabs tabs-transparent">
<li class="tab">
<a
class="active"
@click.stop.prevent="changeTab('inventory', $event)"
>Inventur</a>
</li>
<li class="tab">
<a
@click.stop.prevent="changeTab('article', $event)"
>Artikelliste</a>
</li>
<li class="tab">
<a
@click.stop.prevent="changeTab('calc', $event)"
>Rechner</a>
</li>
</ul>
</div>
</nav>
</template>
<script>
import M from 'materialize-css';
export default {
props: {
primaryColor: {
type: String,
default: "red"
}
},
data() {
return {
tabs: []
}
},
mounted() {
var tabs = document.getElementsByClassName("tabs")[0];
M.Tabs.init(tabs, {});
this.tabs = M.Tabs.getInstance(tabs);
},
methods: {
changeTab(tabId, event) {
this.view = tabId;
this.tabs._handleTabClick(event);
this.tabs.updateTabIndicator();
this.$emit("changed-tab", this.view);
},
}
}
</script>
<style>
</style>
+15
View File
@@ -0,0 +1,15 @@
.tweened {
display: flex;
align-content: end;
justify-content: end;
}
.tweened>div:first-child {
text-align: right;
min-width: 1.5rem;
}
.tweened>div:nth-child(0n+2) {
min-width: 1.5rem;
}
.tweened>div:nth-child(0n+3) {
min-width: 2rem;
}
+63
View File
@@ -0,0 +1,63 @@
<template>
<div class="tweened">
<div>{{ intAnimated }}</div>
<div v-if="precision>0">
,{{ deciAnimated }}
</div>
<div v-else />
<div>{{ einheit }}</div>
</div>
</template>
<script>
import gsap from 'gsap';
import './TweenedNumber.css';
export default {
name: "TweenedNumber",
props: {
wert: {
type: Number,
default: 0
},
einheit: {
type: String,
default: ""
},
precision: {
type: Number,
default: 0
}
},
data(){
return {
tweenedValue: 0,
}
},
computed: {
valueAnimated() {
return this.tweenedValue.toLocaleString(
"de-DE",
{
maximumFractionDigits: 2,
minimumFractionDigits: this.precision
}
);
},
intAnimated() {
return this.valueAnimated.split(',')[0];
},
deciAnimated() {
return this.valueAnimated.split(',')[1];
}
},
watch: {
wert: {
handler(newValue) {
gsap.to(this.$data, 0.5, {tweenedValue: newValue });
},
deep: true
}
}
}
</script>
+16
View File
@@ -0,0 +1,16 @@
import Vue from "vue";
import App from "./App";
import 'materialize-css/dist/css/materialize.min.css';
import Axios from 'axios';
import M from 'materialize-css';
Vue.prototype.$http = Axios;
Vue.prototype.$M = M;
new Vue({
delimiters: ["${", "}"],
components: {
App
},
template: "<App/>"
}).$mount("#app");
+80
View File
@@ -0,0 +1,80 @@
export class Article{
constructor() {
this.name = "";
this.short = "";
this.dimension = "";
this.content = {
size : 0,
price: 0
};
this.portion = {
size : 0,
price : 0,
type : ""
};
}
get Name() {
return this.name;
}
set Name(value) {
this.name = value;
}
get Short() {
return this.short;
}
set Short(value) {
this.short = value;
}
get ContentSize() {
return this.content.size;
}
set ContentSize(value) {
this.content.size = value;
}
get Dimension() {
return this.dimension;
}
set Dimension(value) {
this.dimension = value;
}
get PortionSize() {
return this.portion.size;
}
set PortionSize(value) {
this.portion.size = value;
}
get PortionType() {
return this.portion.type;
}
set PortionType(value) {
this.portion.type = value;
}
get PortionPrice() {
return this.portion.price;
}
set PortionPrice(value) {
this.portion.price = value;
}
get ContentPrice() {
return this.Portions * this.portion.price;
}
get Portions() {
return this.content.size / (this.portion.size || 1);
}
}
Article.thaw = function (json) {
var article = new Article();
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;
return article;
};
export const thawArticle = Article.thaw;
+72
View File
@@ -0,0 +1,72 @@
export class InventoryArticle {
constructor(article) {
this.article = article;
this.end = 0;
this.start = 0;
this.fetched = 0;
this.lost = 0;
}
get StartPortions() {
var countFull = Math.floor(this.start);
var fullPortions = countFull * this.article.Portions;
var rest = this.start - countFull;
var restPortions = rest / this.article.PortionSize;
return fullPortions + restPortions;
}
get FetchedPortions() {
var countFull = Math.floor(this.fetched);
var fullPortions = countFull * this.article.Portions;
var rest = this.fetched - countFull;
var restPortions = rest / this.article.PortionSize;
return fullPortions + restPortions;
}
get EndPortions() {
var countFull = Math.floor(this.end);
var fullPortions = countFull * this.article.Portions;
var rest = this.end - countFull;
var restPortions = rest / this.article.PortionSize;
return fullPortions + restPortions;
}
get LostPortions() {
var countFull = Math.floor(this.lost);
var fullPortions = countFull * this.article.Portions;
var rest = this.lost - countFull;
var restPortions = rest / this.article.PortionSize;
return fullPortions + restPortions;
}
get Sold() {
var adds = this.StartPortions + this.FetchedPortions;
var subs = this.EndPortions + this.LostPortions;
return adds - subs;
}
get Sale() {
return this.Sold * this.article.PortionPrice;
}
get StepSize() {
var singlePack = this.article.Portions == 1;
return singlePack ? 1 : 0.05;
}
get PortionPrecision() {
var singlePack = this.article.Portions == 1;
return singlePack ? 0 : 2;
}
reset() {
this.start = 0;
this.fetched = 0;
this.end = 0;
this.lost = 0;
}
}
InventoryArticle.thaw = function (json) {
this.name = json.name;
this.start = json.start;
this.fetched = json.fetched;
this.end = json.end;
this.lost = json.lost;
};
export const thawInventoryArticle = InventoryArticle.thaw;