[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

157
.ddev/config.yaml Normal file
View File

@ -0,0 +1,157 @@
APIVersion: v1.12.0
name: inventar
type: php
docroot: public
php_version: "7.2"
webserver_type: nginx-fpm
router_http_port: "80"
router_https_port: "443"
xdebug_enabled: false
additional_hostnames: []
additional_fqdns: []
nfs_mount_enabled: false
provider: default
use_dns_when_possible: true
timezone: ""
# This config.yaml was created with ddev version v1.12.0
# webimage: drud/ddev-webserver:v1.12.1
# dbimage: drud/ddev-dbserver-mariadb-10.2:v1.12.0
# dbaimage: drud/phpmyadmin:v1.12.0
# bgsyncimage: drud/ddev-bgsync:v1.12.0
# However we do not recommend explicitly wiring these images into the
# config.yaml as they may break future versions of ddev.
# You can update this config.yaml using 'ddev config'.
# Key features of ddev's config.yaml:
# name: <projectname> # Name of the project, automatically provides
# http://projectname.ddev.site and https://projectname.ddev.site
# type: <projecttype> # drupal6/7/8, backdrop, typo3, wordpress, php
# docroot: <relative_path> # Relative path to the directory containing index.php.
# php_version: "7.2" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4"
# You can explicitly specify the webimage, dbimage, dbaimage lines but this
# is not recommended, as the images are often closely tied to ddev's' behavior,
# so this can break upgrades.
# webimage: <docker_image> # nginx/php docker image.
# dbimage: <docker_image> # mariadb docker image.
# dbaimage: <docker_image>
# bgsyncimage: <docker_image>
# mariadb_version and mysql_version
# ddev can use many versions of mariadb and mysql
# However these directives are mutually exclusive
# mariadb_version: 10.2
# mysql_version: 8.0
# router_http_port: <port> # Port to be used for http (defaults to port 80)
# router_https_port: <port> # Port for https (defaults to 443)
# xdebug_enabled: false # Set to true to enable xdebug and "ddev start" or "ddev restart"
# Note that for most people the commands
# "ddev exec enable_xdebug" and "ddev exec disable_xdebug" work better,
# as leaving xdebug enabled all the time is a big performance hit.
# webserver_type: nginx-fpm # Can be set to apache-fpm or apache-cgi as well
# timezone: Europe/Berlin
# This is the timezone used in the containers and by PHP;
# it can be set to any valid timezone,
# see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# For example Europe/Dublin or MST7MDT
# additional_hostnames:
# - somename
# - someothername
# would provide http and https URLs for "somename.ddev.site"
# and "someothername.ddev.site".
# additional_fqdns:
# - example.com
# - sub1.example.com
# would provide http and https URLs for "example.com" and "sub1.example.com"
# Please take care with this because it can cause great confusion.
# upload_dir: custom/upload/dir
# would set the destination path for ddev import-files to custom/upload/dir.
# working_dir:
# web: /var/www/html
# db: /home
# would set the default working directory for the web and db services.
# These values specify the destination directory for ddev ssh and the
# directory in which commands passed into ddev exec are run.
# omit_containers: ["dba", "ddev-ssh-agent"]
# would omit the dba (phpMyAdmin) and ddev-ssh-agent containers. Currently
# only those two containers can be omitted here.
# Note that these containers can also be omitted globally in the
# ~/.ddev/global_config.yaml or with the "ddev config global" command.
# nfs_mount_enabled: false
# Great performance improvement but requires host configuration first.
# See https://ddev.readthedocs.io/en/stable/users/performance/#using-nfs-to-mount-the-project-into-the-container
# webcache_enabled: false (deprecated)
# Was only for macOS, but now deprecated.
# See https://ddev.readthedocs.io/en/stable/users/performance/#webcache
# host_https_port: "59002"
# The host port binding for https can be explicitly specified. It is
# dynamic unless otherwise specified.
# This is not used by most people, most people use the *router* instead
# of the localhost port.
# host_webserver_port: "59001"
# The host port binding for the ddev-webserver can be explicitly specified. It is
# dynamic unless otherwise specified.
# This is not used by most people, most people use the *router* instead
# of the localhost port.
# host_db_port: "59002"
# The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic
# unless explicitly specified.
# phpmyadmin_port: "1000"
# The PHPMyAdmin port can be changed from the default 8036
# mailhog_port: "1001"
# The MailHog port can be changed from the default 8025
# webimage_extra_packages: [php-yaml, php7.3-ldap]
# Extra Debian packages that are needed in the webimage can be added here
# dbimage_extra_packages: [telnet,netcat]
# Extra Debian packages that are needed in the dbimage can be added here
# use_dns_when_possible: true
# If the host has internet access and the domain configured can
# successfully be looked up, DNS will be used for hostname resolution
# instead of editing /etc/hosts
# Defaults to true
# project_tld: ddev.site
# The top-level domain used for project URLs
# The default "ddev.site" allows DNS lookup via a wildcard
# If you prefer you can change this to "ddev.local" to preserve
# pre-v1.9 behavior.
# ngrok_args: --subdomain mysite --auth username:pass
# Provide extra flags to the "ngrok http" command, see
# https://ngrok.com/docs#http or run "ngrok http -h"
# provider: default # Currently either "default" or "pantheon"
#
# Many ddev commands can be extended to run tasks before or after the
# ddev command is executed, for example "post-start", "post-import-db",
# "pre-composer", "post-composer"
# See https://ddev.readthedocs.io/en/stable/users/extending-commands/ for more
# information on the commands that can be extended and the tasks you can define
# for them. Example:
#hooks:

6
.env.example Normal file
View File

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

19
.eslintrc.json Normal file
View File

@ -0,0 +1,19 @@
{
"root": true,
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"parser": "babel-eslint"
},
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:vue/recommended"
],
"rules": {
"indent": ["error", 2]
}
}

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/vendor/
/node_modules/
.env
data/
!data/*.example

304
app/js/App.vue Normal file
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>

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>

View File

@ -0,0 +1,7 @@
.inventory-item {
transition: all .2s;
}
.inventory-item>* {
transition: all .2s .2s;
}

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>

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

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>

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>

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

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
app/js/index.js Normal file
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
app/js/model/article.js Normal file
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;

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;

View File

@ -1,54 +0,0 @@
<?php
require "localconf.php";
if ($_SERVER['REQUEST_METHOD'] != "POST") {
echo "Sorry, ich spreche kein GET.";
return;
}
if (empty($_POST) && empty(file_get_contents('php://input'))) {
echo "Ich brauche INPUT";
return;
}
if ($_GET['controller'] == "Article"){
if ($_GET['action'] == "store") {
$json = file_get_contents('php://input');
$file = fopen(__DIR__.'/data/articles.json','w');
fwrite($file, $json);
fclose($file);
echo "Artikel wurden gespeichert.";
}
}
if ($_GET['controller'] == "Inventur"){
if ($_GET['action'] == "export") {
$json = file_get_contents('php://input');
$list = json_decode($json, true);
$fp = fopen('data/inventur_'.date('Y-m-d').'.csv', 'w');
fputcsv($fp,array_keys(flatten($list[0])));
foreach ($list as $obj) {
$fields = flatten($obj);
//array_walk_recursive($obj, function($a)use (&$fields) { $fields[] = $a;});
//print_r($fields);
fputcsv($fp,$fields);
}
echo "Inventur wurde gespeichert.";
fclose($fp);
}
}
function flatten($array, $prefix = '') {
$result = array();
foreach($array as $key=>$value) {
if(is_array($value)) {
$result = $result + flatten($value, $prefix . $key . '_');
}
else {
$result[$prefix . $key] = $value;
}
}
return $result;
}

38
composer.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "chrosey/inventar",
"type": "project",
"authors": [
{
"name": "chrosey",
"email": "christian.seyfferth@gmail.com"
}
],
"require": {
"slim/slim": "^4.3",
"vlucas/phpdotenv": "^4.1",
"catfan/medoo": "^1.7",
"slim/psr7": "^0.6.0",
"slim/php-view": "^2.2",
"slim/twig-view": "^3.0",
"php-di/php-di": "^6.0",
"symfony/webpack-encore-bundle": "^1.7",
"illuminate/database": "^6.10"
},
"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"
},
"scripts": {
"csfix": "phpcbf",
"cscheck": "phpcs",
"phpstan": "phpstan analyse src/ -c phpstan.neon --level=7 --no-progress -vvv --memory-limit=1024M"
},
"autoload": {
"psr-4": {
"Chrosey\\Inventur\\":"src"
}
}
}

3563
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
[{"name":"Edelpils","dimension":"l","content":{"size":0.33,"price":0},"portion":{"size":0.33,"price":3.5,"type":"Fl."}},{"name":"Schwarzbier","dimension":"l","content":{"size":0.33,"price":0},"portion":{"size":0.33,"price":3.5,"type":"Fl."}},{"name":"Schöfferhofer","dimension":"l","content":{"size":0.33,"price":0},"portion":{"size":0.33,"price":3.5,"type":"Fl."}},{"name":"Bitburger af.","dimension":"l","content":{"size":0.33,"price":0},"portion":{"size":0.33,"price":3.5,"type":"Fl."}},{"name":"Weiswein","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.2,"price":6,"type":"Gl."}},{"name":"Rotwein","dimension":"l","content":{"size":0.75,"price":0},"portion":{"size":0.2,"price":6.5,"type":"Gl."}},{"name":"Secco","dimension":"l","content":{"size":0.75,"price":0},"portion":{"size":0.1,"price":6,"type":"Gl."}},{"name":"B-Saft","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.2,"price":3.5,"type":"Gl."}},{"name":"K-Saft","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.2,"price":3.5,"type":"Gl."}},{"name":"O-Saft","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.2,"price":3.5,"type":"Gl."}},{"name":"G-Saft","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.2,"price":3.5,"type":"Gl."}},{"name":"Vita Cola","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.2,"price":2.5,"type":"Gl."}},{"name":"Vita Orange","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.2,"price":2.5,"type":"Gl."}},{"name":"Vita Zitrone","dimension":"l","content":{"size":1,"price":0},"portion":{"size":0.2,"price":2.5,"type":"Gl."}},{"name":"Tonic","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."}},{"name":"Bitter Lemon","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."}},{"name":"Ginger Ale","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."}},{"name":"Apfelschorle","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":3,"type":"Fl."}},{"name":"TWQ naturell","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":2.5,"type":"Fl."}},{"name":"TWQ medium","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":2.5,"type":"Fl."}},{"name":"TWQ classic","dimension":"l","content":{"size":0.25,"price":0},"portion":{"size":0.25,"price":2.5,"type":"Fl."}},{"name":"Kaffee","dimension":"Tasse","content":{"size":1,"price":0},"portion":{"size":1,"price":2.5,"type":"T"}},{"name":"Latte Macchiato","dimension":"Glas","content":{"size":1,"price":0},"portion":{"size":1,"price":3.5,"type":"Gl."}},{"name":"dopp. Esp.","dimension":"Tasse","content":{"size":1,"price":0},"portion":{"size":1,"price":3.5,"type":"T"}},{"name":"Brezel","dimension":"Stück","content":{"size":1,"price":0},"portion":{"size":1,"price":2.5,"type":"Stk."}},{"name":"Schokoriegel","dimension":"Stück","content":{"size":1,"price":0},"portion":{"size":1,"price":2,"type":"Stk."}}]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,188 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta lang="de">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="theme-color" content="#004d40">
<title>Inventur</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="css/lib/materialize.min.css">
<link rel="manifest" href="manifest.json">
<link rel="icon" sizes="192x192" href="/favicons/android-chrome-192x192.png">
<link rel="apple-touch-icon" href="/favicons/android-chrome-192x192.png">
</head>
<body>
<div id="app">
<nav class="nav-extended teal" >
<div class="nav-wrapper container">
<a href="#" class="brand-logo">Inventur</a>
</div>
<div class="nav-content container">
<ul class="tabs tabs-transparent">
<li class="tab"><a href="#article" @click="view = 'article';">Artikelliste</a></li>
<li class="tab"><a href="#inventory" class="active" @click="view = 'inventur';">Inventur</a></li>
<li class="tab"><a href="#calc" @click="view = 'calc';">Rechner</a></li>
</ul>
</div>
</nav>
<div class="container">
<div class="valign-wrapper" style="height: 90vh" v-if="!ready">
<div class="progress">
<div class="indeterminate"></div>
</div>
</div>
<div class="col s12" id="article" v-if="ready">
<div v-for="(a, index) in articles" class="card">
<div class="card-content row">
<span class="card-title">{{ a.name }}</span>
<div class="input-field inline col s8">
<input v-model="a.name" placeholder="Artikelname" class="validate" :id="'a_name_'+index">
<label :for="'a_name_'+index" class="active">Name</label>
</div>
<div class="input-field inline col s4">
<input v-model="a.short" placeholder="Kurzname" class="validate" :id="'a_short_'+index">
<label :for="'a_short_'+index" class="active">Kürzel</label>
</div>
<div class="input-field col s8">
<input v-model.number="a.content.size" placeholder="Gesamtinhalt" type="number" class="validate" step="0.01" :id="'a_csize_'+index">
<label :for="'a_csize_'+index" class="active">Gesamtinhalt</label>
</div>
<div class="input-field col s4">
<input v-model="a.dimension" placeholder="Dimension" class="validate" :id="'a_dim_'+index" 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 v-model.number="a.portion.size" placeholder="Gesamtinhalt" type="number" class="validate" step="0.01" :id="'a_psize_'+index" max="5">
<label :for="'a_psize_'+index" class="active">Portionsinhalt</label>
</div>
<div class="input-field col s4">
<input v-model="a.portion.type" placeholder="Art" class="validate" :id="'a_ptype_'+index" max="5">
<label :for="'a_ptype_'+index" class="active">Portionsbezeichnung</label>
</div>
<div class="input-field col s4">
<input v-model.number="a.portion.price" placeholder="Preis" type="number" step="0.01" class="validate" :id="'a_pprice_'+index">
<label :for="'a_pprice_'+index" class="active">Portionspreis in €</label>
</div>
<div class="col s12 right">
<span class="right">Gesamtpreis {{ a.ContentPrice | currency }}</span>
</div>
</div>
<div class="card-action">
<a class="" href="#">Artikel löschen</a>
</div>
</div>
</div>
<div class="col s12" id="inventory" v-if="ready">
<div v-for="(a, index) in inventory.ug" class="card hoverable">
<div class="card-content row">
<div class="col s12 m2">{{ a.article.name }}</div>
<div class="input-field col s6 m2">
<input v-model.number="a.start" placeholder="Anfang" title="Anfang" type="number" :step="a.StepSize" :id="'i_s_'+index">
<label :for="'i_s_'+index" class="active">Beginn</label>
</div>
<div class="input-field col s6 m2 inline">
<input v-model.number="a.fetched" placeholder="Zugang" title="Zugang" type="number" :step="a.StepSize" :id="'i_f_'+index">
<label :for="'i_f_'+index" class="active">Zugang</label>
</div>
<div class="input-field col s6 m2">
<input v-model.number="a.end" placeholder="Ende" title="Ende" type="number" :step="a.StepSize" :id="'i_e_'+index">
<label :for="'i_e_'+index" class="active">Ende</label>
</div>
<div class="input-field col s6 m2">
<input v-model.number="a.lost" placeholder="Verlust" title="Verlust" type="number" :step="a.StepSize" :id="'i_l_'+index">
<label :for="'i_l_'+index" class="active">Verlust</label>
</div>
<div class="col m2 right right-align"> <b>{{a.article.portion.price | currency }} &times; {{ a.Sold | number(a.PortionPrecision) }} {{ a.article.PortionType }} <br/>&equals; {{ a.Sale | currency }}</b></div>
</div>
</div>
<div class="card teal lighten-2 white-text">
<div class="card-content">
<h5>Gesamtsumme: {{ sales_ug | currency }}</h5>
</div>
</div>
</div>
<div class="col s12" id="calc" v-if="ready">
<div class="row pin-top">
<div class="col s12 m4 card darken-4 grey grey-text text-lighten-2">
<table class="card-content">
<tr v-for="item in bonned(bon)">
<td class="right">{{ item.count }} &times;</td>
<td>{{ item.name }}</td>
<td class="right">{{ item.price * item.count | currency }}</td>
</tr>
<tr class="white-text">
<th class="right">{{ bon_sum > 0 ?bon_sum: "" }}</th>
<th>{{ bon_sum > 0 ? "Artikel": ""}}</th>
<th class="right">{{ bon_price }}</th>
</tr>
</table>
</div>
<div class="col s12 m8">
<div class="row">
<div class="col s3" v-for="a in bon">
<button class="waves-effect waves-light btn-large btn-flat col s12" @click="a.count++;">{{ 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>
</div>
</div>
<div class="fixed-action-btn">
<a href="#" class="btn-floating btn-large" :class="[ view == 'article' ? 'orange' : (view == 'inventur' ? 'teal' : 'brown') ]">
<i class="large material-icons">more</i>
</a>
<ul>
<li v-if="view == 'article'">
<a href="#" class="btn-floating tooltipped" data-position="left" @click="storeArticles" data-tooltip="Artikelliste speichern"><i class="material-icons">save</i></a>
</li>
<li v-if="view == 'article'">
<a href="#" class="btn-floating tooltipped" data-position="left" @click="addArticle" data-tooltip="Artikel hinzufügen"><i class="material-icons">add</i></a>
</li>
<li v-if="view == 'inventur'">
<a href="#" class="btn-floating tooltipped" data-position="left" @click="resetInventur" data-tooltip="Inventur zurücksetzen"><i class="material-icons">reset</i></a>
</li>
<li v-if="view == 'inventur'">
<a href="#" class="btn-floating tooltipped" data-position="left" @click="exportInventur" data-tooltip="Inventur exportieren"><i class="material-icons">export</i></a>
</li>
</ul>
</div>
</div>
<script src="js/lib/jquery.min.js"></script>
<script src="js/lib/materialize.min.js"></script>
<script src="js/lib/moment-with-locales.min.js"></script>
<script src="js/lib/accounting.min.js"></script>
<script src="js/lib/vue-dev.js"></script>
<script src="js/lib/vue-resource.min.js"></script>
<script src="js/site.js"></script>
<script src="js/app.js"></script>
<script src="js/model/article.js"></script>
<script src="js/model/inventory_article.js"></script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('serviceWorker.js')
.then(function(registration) {
console.log("[ServiceWorker] registration successful with scope: ", registration.scope);
}).catch(function(err){
console.error("[ServiceWorker] registration failed: ", err);
})
}
</script>
</body>
</html>

View File

@ -1,97 +0,0 @@
Vue.filter('currency', function(money){
return accounting.formatMoney(money);
});
Vue.filter('number', function(number, precision = 2){
return accounting.formatNumber(number, precision);
});
var app = new Vue({
el: "#app",
data : {
articles : [],
inventory : {
ug : [],
mob : [],
stud : []
},
bon: [],
view: 'inventur',
ready: false,
},
computed: {
sales_ug: function (){
var total_sales = this.inventory.ug.reduce(function(total, item ) {
return total + item.Sale;
}, 0);
return total_sales;
},
bon_price: function() {
var total = this.bon.reduce(function(total, item) {
return total + item.count * item.price;
}, 0);
return accounting.formatMoney(total);
},
bon_sum: function() {
var total = this.bon.reduce(function(total, item) {
return total + item.count;
}, 0);
return total;
}
},
methods: {
addArticle: function() {
this.articles.push(new Article());
},
storeArticles: function() {
this.$http.post('./backend?controller=Article&action=store', JSON.stringify(this.articles))
.then(response => {
M.toast({html: response.body});
})
},
loadArticles: function() {
this.$http.get('./data/articles.json')
.then(response => { return response.json();})
.then(json => {
json.forEach(element => {
this.articles.push(Article.thaw(element));
});
}).then( x => {
M.toast({ html: 'Artikel wurden geladen.'});
}).then( x => {
this.articles.forEach(a => {
ia = new InventoryArticle();
ia.article = a;
this.inventory.ug.push(ia);
this.bon.push({count: 0, name: a.name, short: a.short, price: a.portion.price});
});
}).then( x => {
M.updateTextFields();
this.ready = true;
});
},
resetInventur: function() {
},
exportInventur: function() {
this.$http.post('./backend?controller=Inventur&action=export', JSON.stringify(this.inventory.ug))
.then(response => {
M.toast({html: response.body});
})
},
resetBon: function(article) {
this.bon.forEach(function (item){
item.count = 0;
});
},
bonned: function (items) {
return items.filter(function (item) {
return item.count > 0;
});
}
},
created: function(){
this.loadArticles();
}
});

View File

@ -1,4 +0,0 @@
/*!
* accounting.js v0.4.2, copyright 2014 Open Exchange Rates, MIT license, http://openexchangerates.github.io/accounting.js
*/
(function(p,z){function q(a){return!!(""===a||a&&a.charCodeAt&&a.substr)}function m(a){return u?u(a):"[object Array]"===v.call(a)}function r(a){return"[object Object]"===v.call(a)}function s(a,b){var d,a=a||{},b=b||{};for(d in b)b.hasOwnProperty(d)&&null==a[d]&&(a[d]=b[d]);return a}function j(a,b,d){var c=[],e,h;if(!a)return c;if(w&&a.map===w)return a.map(b,d);for(e=0,h=a.length;e<h;e++)c[e]=b.call(d,a[e],e,a);return c}function n(a,b){a=Math.round(Math.abs(a));return isNaN(a)?b:a}function x(a){var b=c.settings.currency.format;"function"===typeof a&&(a=a());return q(a)&&a.match("%v")?{pos:a,neg:a.replace("-","").replace("%v","-%v"),zero:a}:!a||!a.pos||!a.pos.match("%v")?!q(b)?b:c.settings.currency.format={pos:b,neg:b.replace("%v","-%v"),zero:b}:a}var c={version:"0.4.1",settings:{currency:{symbol:"$",format:"%s%v",decimal:".",thousand:",",precision:2,grouping:3},number:{precision:0,grouping:3,thousand:",",decimal:"."}}},w=Array.prototype.map,u=Array.isArray,v=Object.prototype.toString,o=c.unformat=c.parse=function(a,b){if(m(a))return j(a,function(a){return o(a,b)});a=a||0;if("number"===typeof a)return a;var b=b||".",c=RegExp("[^0-9-"+b+"]",["g"]),c=parseFloat((""+a).replace(/\((.*)\)/,"-$1").replace(c,"").replace(b,"."));return!isNaN(c)?c:0},y=c.toFixed=function(a,b){var b=n(b,c.settings.number.precision),d=Math.pow(10,b);return(Math.round(c.unformat(a)*d)/d).toFixed(b)},t=c.formatNumber=c.format=function(a,b,d,i){if(m(a))return j(a,function(a){return t(a,b,d,i)});var a=o(a),e=s(r(b)?b:{precision:b,thousand:d,decimal:i},c.settings.number),h=n(e.precision),f=0>a?"-":"",g=parseInt(y(Math.abs(a||0),h),10)+"",l=3<g.length?g.length%3:0;return f+(l?g.substr(0,l)+e.thousand:"")+g.substr(l).replace(/(\d{3})(?=\d)/g,"$1"+e.thousand)+(h?e.decimal+y(Math.abs(a),h).split(".")[1]:"")},A=c.formatMoney=function(a,b,d,i,e,h){if(m(a))return j(a,function(a){return A(a,b,d,i,e,h)});var a=o(a),f=s(r(b)?b:{symbol:b,precision:d,thousand:i,decimal:e,format:h},c.settings.currency),g=x(f.format);return(0<a?g.pos:0>a?g.neg:g.zero).replace("%s",f.symbol).replace("%v",t(Math.abs(a),n(f.precision),f.thousand,f.decimal))};c.formatColumn=function(a,b,d,i,e,h){if(!a)return[];var f=s(r(b)?b:{symbol:b,precision:d,thousand:i,decimal:e,format:h},c.settings.currency),g=x(f.format),l=g.pos.indexOf("%s")<g.pos.indexOf("%v")?!0:!1,k=0,a=j(a,function(a){if(m(a))return c.formatColumn(a,f);a=o(a);a=(0<a?g.pos:0>a?g.neg:g.zero).replace("%s",f.symbol).replace("%v",t(Math.abs(a),n(f.precision),f.thousand,f.decimal));if(a.length>k)k=a.length;return a});return j(a,function(a){return q(a)&&a.length<k?l?a.replace(f.symbol,f.symbol+Array(k-a.length+1).join(" ")):Array(k-a.length+1).join(" ")+a:a})};if("undefined"!==typeof exports){if("undefined"!==typeof module&&module.exports)exports=module.exports=c;exports.accounting=c}else"function"===typeof define&&define.amd?define([],function(){return c}):(c.noConflict=function(a){return function(){p.accounting=a;c.noConflict=z;return c}}(p.accounting),p.accounting=c)})(this);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6
js/lib/vue.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,79 +0,0 @@
function Article() {
this.name = "";
this.short = "";
this.dimension = "";
this.content = {
size : 0,
price: 0
};
this.portion = {
size : 0,
price : 0,
type : ""
};
}
Article.prototype = {
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;
};

View File

@ -1,66 +0,0 @@
function InventoryArticle(){
this.article = new Article();
this.end = 0,
this.start = 0;
this.fetched = 0;
this.lost = 0;
}
InventoryArticle.prototype = {
get StartPortions() {
countFull = Math.floor(this.start);
fullPortions = countFull * this.article.Portions;
rest = this.start - countFull;
restPortions = rest / this.article.PortionSize;
return fullPortions + restPortions;
},
get FetchedPortions() {
countFull = Math.floor(this.fetched);
fullPortions = countFull * this.article.Portions;
rest = this.fetched - countFull;
restPortions = rest / this.article.PortionSize;
return fullPortions + restPortions;
},
get EndPortions() {
countFull = Math.floor(this.end);
fullPortions = countFull * this.article.Portions;
rest = this.end - countFull;
restPortions = rest / this.article.PortionSize;
return fullPortions + restPortions;
},
get LostPortions() {
countFull = Math.floor(this.lost);
fullPortions = countFull * this.article.Portions;
rest = this.lost - countFull;
restPortions = rest / this.article.PortionSize;
return fullPortions + restPortions;
},
get Sold() {
adds = this.StartPortions + this.FetchedPortions;
subs = this.EndPortions + this.LostPortions;
return adds - subs;
},
get Sale() {
return this.Sold * this.article.PortionPrice;
},
get StepSize() {
singlePack = this.article.Portions == 1;
return singlePack ? 1 : 0.05;
},
get PortionPrecision() {
singlePack = this.article.Portions == 1;
return singlePack ? 0 : 2;
}
};
InventoryArticle.thaw = function(json){
this.name = json.name;
this.start = json.start;
this.fetched = json.fetched;
this.end = json.end;
this.lost = json.lost;
};

View File

@ -1,21 +0,0 @@
accounting.settings = {
currency: {
symbol: "€",
format: "%v %s",
decimal: ",",
thousand: ".",
precision: 2
},
number: {
precision : 2, // default precision on numbers is 0
thousand: ".",
decimal : ","
}
};
$(document).ready(function(){
$('.tabs').tabs();
$('.fixed-action-btn').floatingActionButton();
$('.tooltipped').tooltip();
});

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"target": "es6"
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

View File

@ -1,8 +0,0 @@
<?php
$database = [
'server' => 'localhost',
'user' => 'inventur',
'password' => '*inv2018#',
'db' => 'inventur'
];

View File

@ -1,8 +0,0 @@
<?php
$database = [
'server' => 'localhost',
'user' => 'inventur',
'password' => '*inv2018#',
'db' => 'inventur'
];

9646
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,51 @@
{
"name": "inventur",
"name": "inventar",
"version": "1.0.0",
"description": "",
"main": "index.html",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 0"
"main": "index.js",
"directories": {
"test": "tests"
},
"author": "chrosey",
"license": "ISC"
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "encore production --progress",
"watch": "encore dev --watch",
"dev": "encore dev",
"csfix": "eslint app/js --ext .js,.vue, --fix"
},
"repository": {
"type": "git",
"url": "https://git.chrosey.de/chrosey/inventur.git"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.19.0",
"core-js": "^3.6.1",
"gsap": "^3.0.4",
"materialize-css": "^1.0.0",
"moment": "^2.24.0",
"vue": "^2.6.11"
},
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/runtime": "^7.7.7",
"@symfony/webpack-encore": "^0.28.2",
"babel-eslint": "^10.0.3",
"css-loader": "^2.1.1",
"dotenv": "^8.2.0",
"eslint": "^6.8.0",
"eslint-loader": "^3.0.3",
"eslint-plugin-vue": "^6.1.2",
"vue-loader": "^15.8.3",
"vue-template-compiler": "^2.6.11",
"webpack-notifier": "^1.8.0"
},
"browserslist": [
"> 0.5%",
"last 2 versions",
"Firefox ESR",
"not dead"
]
}

46
phpcs.xml.dist Normal file
View File

@ -0,0 +1,46 @@
<?xml version="1.0"?>
<ruleset>
<arg name="basepath" value="."/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="16"/>
<arg name="colors"/>
<!-- Ignore warnings, show progress of the run and show sniff names -->
<arg value="nps"/>
<!-- Directories to be checked -->
<file>src</file>
<file>tests</file>
<file>public</file>
<exclude-pattern>tests/dependencies/*</exclude-pattern>
<!-- Include full Doctrine Coding Standard -->
<rule ref="Doctrine">
<exclude name="SlevomatCodingStandard.Classes.SuperfluousInterfaceNaming.SuperfluousSuffix"/>
<exclude name="SlevomatCodingStandard.Classes.SuperfluousExceptionNaming.SuperfluousSuffix"/>
<exclude name="SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix"/>
<exclude name="Squiz.Commenting.FunctionComment.InvalidNoReturn" />
<exclude name="Generic.Formatting.MultipleStatementAlignment" />
</rule>
<!-- Do not align assignments -->
<rule ref="Generic.Formatting.MultipleStatementAlignment">
<severity>0</severity>
</rule>
<!-- Do not align comments -->
<rule ref="Squiz.Commenting.FunctionComment.SpacingAfterParamName">
<severity>0</severity>
</rule>
<rule ref="Squiz.Commenting.FunctionComment.SpacingAfterParamType">
<severity>0</severity>
</rule>
<!-- Require no space before colon in return types -->
<rule ref="SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing">
<properties>
<property name="spacesCountBeforeColon" value="0"/>
</properties>
</rule>
</ruleset>

7
phpstan.neon Normal file
View File

@ -0,0 +1,7 @@
includes:
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
- vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon
parameters:
excludes_analyse:
- %currentWorkingDirectory%/src/Migrations/*.php
- %currentWorkingDirectory%/src/Kernel.php

49
public/build/app.css Normal file
View File

@ -0,0 +1,49 @@
.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;
}
.inventory-item {
transition: all .2s;
}
.inventory-item>* {
transition: all .2s .2s;
}
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;
}
/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vVHdlZW5lZE51bWJlci5jc3MiLCJ3ZWJwYWNrOi8vL0ludmVudG9yeUl0ZW0uY3NzIiwid2VicGFjazovLy9JbnZlbnRvcnlUYWJsZS5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7SUFDSSxhQUFhO0lBQ2Isa0JBQWtCO0lBQ2xCLG9CQUFvQjtBQUN4QjtBQUNBO0lBQ0ksaUJBQWlCO0lBQ2pCLGlCQUFpQjtBQUNyQjtBQUNBO0lBQ0ksaUJBQWlCO0FBQ3JCO0FBQ0E7SUFDSSxlQUFlO0FBQ25CLEM7QUNkQTtJQUNJLG1CQUFtQjtBQUN2Qjs7QUFFQTtJQUNJLHVCQUF1QjtBQUMzQixDO0FDTkE7O0lBRUksZ0JBQWdCO0FBQ3BCOztBQUVBO0lBQ0ksNENBQTRDO0FBQ2hEO0FBQ0E7SUFDSSwyQ0FBMkM7QUFDL0M7O0FBRUE7SUFDSSxVQUFVO0FBQ2Q7QUFDQTtJQUNJLG1CQUFtQjtJQUNuQixnQkFBZ0I7SUFDaEIsb0NBQW9DO0FBQ3hDOztBQUVBO0lBQ0ksK0JBQStCO0lBQy9CLGlCQUFpQjtBQUNyQixDIiwiZmlsZSI6ImFwcC5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyIudHdlZW5lZCB7XG4gICAgZGlzcGxheTogZmxleDtcbiAgICBhbGlnbi1jb250ZW50OiBlbmQ7XG4gICAganVzdGlmeS1jb250ZW50OiBlbmQ7XG59XG4udHdlZW5lZD5kaXY6Zmlyc3QtY2hpbGQge1xuICAgIHRleHQtYWxpZ246IHJpZ2h0O1xuICAgIG1pbi13aWR0aDogMS41cmVtO1xufVxuLnR3ZWVuZWQ+ZGl2Om50aC1jaGlsZCgwbisyKSB7XG4gICAgbWluLXdpZHRoOiAxLjVyZW07XG59XG4udHdlZW5lZD5kaXY6bnRoLWNoaWxkKDBuKzMpIHtcbiAgICBtaW4td2lkdGg6IDJyZW07XG59IiwiLmludmVudG9yeS1pdGVtIHtcbiAgICB0cmFuc2l0aW9uOiBhbGwgLjJzO1xufVxuXG4uaW52ZW50b3J5LWl0ZW0+KiB7XG4gICAgdHJhbnNpdGlvbjogYWxsIC4ycyAuMnM7XG59IiwidGFibGUuY29uZGVuc2VkIHRkLFxudGFibGUuY29uZGVuc2VkIHRoIHtcbiAgICBwYWRkaW5nOiA1cHggNXB4O1xufVxuXG4uYm9yZGVyLWJvdHRvbSB7XG4gICAgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkIHJnYmEoMCwgMCwgMCwgMC4xMik7XG59XG4uYm9yZGVyLXJpZ2h0IHtcbiAgICBib3JkZXItcmlnaHQ6IDFweCBzb2xpZCByZ2JhKDAsIDAsIDAsIDAuMTIpO1xufVxuXG4uYm9yZGVyLWRpYWdyYW0ge1xuICAgIHBhZGRpbmc6IDA7XG59XG4uYm9yZGVyLWRpYWdyYW0+ZGl2IHtcbiAgICBtYXJnaW4tYm90dG9tOiAtM3B4OyBcbiAgICBib3JkZXItcmFkaXVzOiAwOyBcbiAgICBib3JkZXItYm90dG9tOiAzcHggc29saWQgdHJhbnNwYXJlbnQ7XG59XG5cbi5hY3RpdmUge1xuICAgIGJvcmRlci1jb2xvcjogcmdiKDM4LCAxNjYsIDE1NCk7XG4gICAgYm9yZGVyLXdpZHRoOiAycHg7XG59Il0sInNvdXJjZVJvb3QiOiIifQ==*/

2874
public/build/app.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
{
"entrypoints": {
"app": {
"css": [
"/build/vendors~app.css",
"/build/app.css"
],
"js": [
"/build/vendors~app.js",
"/build/app.js"
]
}
}
}

View File

@ -0,0 +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"
}

File diff suppressed because one or more lines are too long

35319
public/build/vendors~app.js Normal file

File diff suppressed because one or more lines are too long

6
public/index.php Normal file
View File

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
$app = require_once __DIR__.'/../src/bootstrap.php';
$app->run();

View File

@ -1,5 +1,5 @@
var APP_SHELL_VERSION = 2;
/*
var APP_SHELL_VERSION = 3;
var APP_SHELL_CACHE = `inventur_shell-v${APP_SHELL_VERSION}`;
var APP_SHELL_URLS = [
'./',
@ -8,7 +8,7 @@ var APP_SHELL_URLS = [
'js/lib/accounting.min.js',
'js/lib/moment-with-locales.min.js',
'js/lib/jquery.min.js',
'js/lib/vue-dev.js',
'js/lib/vue.js',
'js/lib/vue.min.js',
'js/lib/vue-resource.min.js'
];
@ -48,3 +48,4 @@ self.addEventListener('fetch', function(event){
);
});
*/

48
src/bootstrap.php Normal file
View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
use DI\Container;
use Illuminate\Database\Capsule\Manager as Capsule;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/env.php';
$container = new Container();
AppFactory::setContainer($container);
$container->set('view', static function () {
return Twig::create(__DIR__ . '/../templates', []);
});
$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'),
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
return $capsule;
});
$container->set('upload_directory', __DIR__ . '/../data/uploads');
$app = AppFactory::create();
$app->add(TwigMiddleware::createFromContainer($app));
require 'routes.php';
return $app;

View File

@ -0,0 +1,3 @@
<?php
declare(strict_types=1);

7
src/container/view.php Normal file
View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
$container->set('view', static function () {
return Twig::create(__DIR__ . '/../templates', []);
});

8
src/env.php Normal file
View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->load();

70
src/routes.php Normal file
View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
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;
$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'];
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->post('/inventur', function (Request $request, Response $response, $args) {
$directory = $this->get('upload_directory');
$uploadedFile = $request->getUploadedFiles()['file'];
if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
$filename = moveUploadedFile($directory, $uploadedFile);
$response->getBody()->write('uploaded ' . $filename . '<br/>');
}
return $response;
});
});
/**
* Move Uploaded File to Target Destination
*
* Create random hexName
*
* @param String $directory Target Direcotry
* @param UploadedFile $uploadedFile the File that was uploaded
**/
function moveUploadedFile(string $directory, UploadedFile $uploadedFile): string
{
$extension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
$basename = bin2hex(random_bytes(8)); // see http://php.net/manual/en/function.random-bytes.php
$filename = sprintf('%s.%0.8s', $basename, $extension);
$uploadedFile->moveTo($directory . DIRECTORY_SEPARATOR . $filename);
return $filename;
}

21
templates/frontend.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta lang="de">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="theme-color" content="#004d40">
<title>Inventur</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="build/vendors~app.css">
<link rel="stylesheet" href="build/app.css">
</head>
<body>
<div id="app"></div>
<script src="/build/vendors~app.js"></script>
<script src="/build/app.js"></script>
</body>
</html>

93
webpack.config.js Normal file
View File

@ -0,0 +1,93 @@
let Encore = require('@symfony/webpack-encore');
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
Encore
// directory where compiled assets will be stored
.setOutputPath('public/build/')
// public path used by the web server to access the output path
.setPublicPath('/build')
// only needed for CDN's or sub-directory deploy
//.setManifestKeyPrefix('build/')
/*
* ENTRY CONFIG
*
* Add 1 entry for each "page" of your app
* (including one that's included on every page - e.g. "app")
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if you JavaScript imports CSS.
*/
.addEntry('app', './app/js/index.js')
//.addEntry('page1', './assets/js/page1.js')
//.addEntry('page2', './assets/js/page2.js')
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
//.enableSingleRuntimeChunk()
.disableSingleRuntimeChunk()
/*
* FEATURE CONFIG
*
* Enable & configure other features below. For a full
* list of features, see:
* https://symfony.com/doc/current/frontend.html#adding-more-features
*/
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
// enables @babel/preset-env polyfills
.configureBabel((babelConfig) => {
babelConfig.plugins.push('@babel/plugin-transform-runtime');
}, {
useBuiltIns: 'usage',
corejs: 3
})
// enables Vue.js support
.enableVueLoader()
// enables Sass/SCSS support
//.enableSassLoader()
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
//.enableIntegrityHashes()
// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
// uncomment if you use API Platform Admin (composer req api-admin)
//.enableReactPreset()
//.addEntry('admin', './assets/js/admin.js')
// enable ESLint
.addLoader({
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /node_modules/,
options: {
fix: true,
emitError: true,
emitWarning: true,
},
})
;
module.exports = Encore.getWebpackConfig();