release v2.0.0 #4

Merged
crimsen merged 481 commits from develop into master 2024-01-18 15:15:08 +00:00
11 changed files with 727 additions and 65 deletions
Showing only changes of commit c0d57c6a71 - Show all commits

View File

@ -12,7 +12,7 @@ const config = {
// Do not change required Modules !! // Do not change required Modules !!
requiredModules: ['User'], requiredModules: ['User'],
// here you can import plugins. // here you can import plugins.
loadModules: ['Balance', 'Schedule'] loadModules: ['Balance', 'Schedule', 'Pricelist']
}; };
// do not change anything here !! // do not change anything here !!

163
src/flaschengeist.d.ts vendored
View File

@ -1,66 +1,101 @@
declare namespace FG { declare namespace FG {
interface Session { interface Session {
expires: Date; expires: Date;
token: string; token: string;
lifetime: number; lifetime: number;
browser: string; browser: string;
platform: string; platform: string;
userid: string; userid: string;
} }
interface User { interface User {
userid: string; userid: string;
display_name: string; display_name: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
mail: string; mail: string;
birthday?: Date; birthday?: any;
roles: Array<string>; roles: Array<string>;
permissions?: Array<string>; permissions?: any;
avatar_url?: string; avatar_url?: any;
} }
type Permission = string; type Permission = string;
interface Role { interface Role {
id: number; id: number;
name: string; name: string;
permissions: Array<Permission>; permissions: Array<Permission>;
} }
interface Transaction { interface Transaction {
id: number; id: number;
time: Date; time: Date;
amount: number; amount: number;
reversal_id: number; reversal_id: number;
sender_id?: string; sender_id?: any;
receiver_id?: string; receiver_id?: any;
author_id?: string; author_id?: any;
original_id?: number; original_id?: any;
} }
interface Event { interface Drink {
id: number; id: number;
start: Date; name: string;
end: Date; volume: number;
description?: string; cost_price: number;
type: EventType; discount: number;
jobs: Array<Job>; extra_charge?: any;
} prices: Array<DrinkPrice>;
interface EventType { ingredients: Array<Ingredient>;
id: number; tags: Array<any>;
name: string; }
} interface DrinkPrice {
interface Job { id: number;
id: number; volume: number;
start: Date; price: number;
end?: Date; no_auto: boolean;
comment: string; public: boolean;
type: JobType; description?: any;
services: Array<Service>; round_step: number;
required_services: number; }
} interface DrinkType {
interface JobType { id: number;
id: number; name: string;
name: string; }
} interface Ingredient {
interface Service { id: number;
userid: string; volume: number;
value: number; drink_parent_id: number;
} drink_ingredient_id: number;
drink_ingredient?: any;
}
interface Tag {
id: number;
name: string;
}
interface Event {
id: number;
start: Date;
end: Date;
description?: any;
type: EventType;
jobs: Array<Job>;
}
interface EventType {
id: number;
name: string;
}
interface Job {
id: number;
start: Date;
end?: any;
comment: string;
type: JobType;
services: Array<Service>;
required_services: number;
}
interface JobType {
id: number;
name: string;
}
interface Service {
userid: string;
value: number;
}
} }

View File

@ -0,0 +1,125 @@
<template>
<div>
<q-card>
<q-card-section>
<div class="text-h4">Neues Getränk</div>
</q-card-section>
<q-form @submit="save">
<q-card-section>
<div class="text-h5">Getränkinformationen</div>
<div class="row">
<q-input class="col-12 col-sm-6 q-px-sm q-py-md" v-model="drink.name" filled label="Name" />
<q-input
class="col-12 col-sm-6 q-px-sm q-py-md"
v-model="drink.volume"
filled
label="Inhalt in Liter"
type="number"
step="0.01"
/>
<q-input
class="col-12 col-sm-6 q-px-sm q-py-md"
v-model="drink.cost_price"
filled
label="Einkaufspreis"
type="number"
step="0.01"
/>
<q-input
class="col-12 col-sm-6 q-px-sm q-py-md"
v-model="drink.discount"
filled
label="Aufschlag in Prozent"
type="number"
hint="Wenn nicht gesetzt wird default-wert genommen."
step="0.01"
/>
<q-input
class="col-12 col-sm-6 q-px-sm q-py-md"
v-model="drink.extra_charge"
filled
label="Extra Aufschlag in Euro"
type="number"
step="0.1"
/>
<q-input class="col-12 col-sm-6 q-px-sm q-py-md" filled label="Tags" />
</div>
</q-card-section>
<q-card-section>
<div class="row justify-between">
<div class="text-h5">
Preise
</div>
<q-btn round icon="mdi-plus" @click="addPrice" color="primary" />
</div>
<q-card class="q-ma-sm" v-for="(price,index) in drink.prices" :key="index">
<div class="row">
<q-input class="col-12 col-sm-6 q-px-sm q-py-md" v-model="price.volume" label="Inhalt in Liter" filled type="number" step="0.01" />
<q-input class="col-12 col-sm-6 q-px-sm q-py-md" v-model="price.price" label="Preis in €" filled :disable="price.no_auto" type="number" step="0.1"/>
<q-toggle class="col-12 col-sm-6 q-px-sm q-py-md" v-model="price.no_auto" label="Automatische Preiskalkulation" color="primary" />
<q-input class="col-12 col-sm-6 q-px-sm q-py-md" v-model="price.round_step" label="Rundungsschritt" type="number" filled step="0.1"/>
<q-toggle class="col-12 col-sm-6 q-px-sm q-py-md" v-model="price.public" label="Öffentlich" color="primary" />
<q-input class="col-12 col-sm-6 q-px-sm q-py-md" v-model="price.description" label="Beschreibung" filled />
</div>
</q-card>
</q-card-section>
<q-card-section>
<div class="row justify-between">
<div class="text-h5">
Zutaten
</div>
<q-btn round icon="mdi-plus" color="primary" />
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn type="submit" label="Speichern" color="primary" />
</q-card-actions>
</q-form>
</q-card>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';
import { StateInterface } from 'src/store';
import { Store } from 'vuex';
export default defineComponent({
name: 'Drink',
setup(_, {root}) {
const store = <Store<StateInterface>>root.$store;
const drink = ref({
name: '',
volume: '',
cost_price: '',
discount: '',
extra_charge: '',
prices: [],
ingredients: []
})
const emptyPrice = {
volume: '',
price: '',
description: '',
no_auto: false,
round_step: '',
public: true
}
function addPrice() {
drink.value.prices.unshift({...emptyPrice})
}
function save() {
console.log(drink)
drink.value.prices.forEach((price: FG.DrinkPrice) => {
price.no_auto = !price.no_auto
})
void store.dispatch('drink/createDrink', drink.value)
}
return {drink, addPrice, save};
}
});
</script>
<style scoped></style>

View File

@ -0,0 +1,133 @@
<template>
<div>
<q-dialog v-model="edittype">
<q-card>
<q-card-section>
<div class="text-h6">Editere Getränkeart {{ actualDrinkType.name }}</div>
</q-card-section>
<q-card-section>
<q-input dense label="name" filled v-model="newDrinkTypeName" />
</q-card-section>
<q-card-actions>
<q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
<q-btn flat color="primary" label="Speichern" @click="saveChanges()" />
</q-card-actions>
</q-card>
</q-dialog>
<q-page padding>
<q-table title="Getränkearten" :data="rows" :row-key="row => row.id" :columns="columns">
<template v-slot:top-right>
<q-input
class="q-px-sm"
dense
v-model="newDrinkType"
placeholder="Neue Getränkeart"
filled
/>
<div></div>
<q-btn color="primary" icon="mdi-plus" label="Hinzufügen" @click="addType" />
</template>
<template v-slot:body-cell-actions="props">
<q-td :props="props" align="right" :auto-width="true">
<q-btn
round
flat
icon="mdi-pencil"
@click="editType({ id: props.row.id, name: props.row.name })"
/>
<q-btn round flat icon="mdi-delete" @click="deleteType(props.row.id)" />
</q-td>
</template>
</q-table>
</q-page>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeMount, ref } from '@vue/composition-api';
import { Store } from 'vuex';
import { StateInterface } from 'src/store';
import { DrinkInterface } from 'src/plugins/pricelist/store/drinks';
export default defineComponent({
name: 'DrinkTypes',
setup(_, { root }) {
const store = <Store<StateInterface>>root.$store;
const state = <DrinkInterface>store.state.drink;
const newDrinkType = ref('');
const newDrinkTypeName = ref('');
const edittype = ref(false);
const emptyDrinkType: FG.DrinkType = { id: -1, name: '' };
const actualDrinkType = ref(emptyDrinkType);
onBeforeMount(() => {
console.log(store);
void store.dispatch('drink/getDrinkTypes');
});
const rows = computed(() => state.drinkTypes);
const columns = [
{
name: 'drinkTypeName',
label: 'Getränkeart',
field: 'name',
align: 'left',
sortable: true
},
{
name: 'actions',
label: 'Aktionen',
field: 'actions',
align: 'right'
}
];
function addType() {
void store.dispatch('drink/addDrinkType', { name: newDrinkType.value });
newDrinkType.value = '';
}
function editType(drinkType: FG.DrinkType) {
edittype.value = true;
actualDrinkType.value = drinkType;
}
function saveChanges() {
void store
.dispatch('drink/changeDrinkTypeName', {
id: actualDrinkType.value.id,
name: newDrinkTypeName.value
})
.finally(() => discardChanges());
}
function discardChanges() {
actualDrinkType.value = emptyDrinkType;
newDrinkTypeName.value = '';
edittype.value = false;
}
function deleteType(id: number) {
void store.dispatch('drink/removeDrinkType', id);
}
return {
columns,
rows,
addType,
newDrinkType,
deleteType,
edittype,
editType,
actualDrinkType,
newDrinkTypeName,
discardChanges,
saveChanges
};
}
});
</script>
<style scoped></style>

View File

@ -0,0 +1,67 @@
<template>
<div>
<q-table
title="Getränke"
:columns="columns"
:data="drinks"
row-key="name"
>
<template v-slot:body-cell-prices="{row: {prices}}">
<q-td>
<div v-for="price in prices" :key="price.id" class="row">
<div class="col">
{{price.volume | setVolume}}
</div>
<div class="col">
{{price.price | setCurrency}}
</div>
<div class="col">
{{price.description}}
</div>
</div>
</q-td>
</template>
</q-table>
</div>
</template>
<script lang="ts">
import { defineComponent, onBeforeMount, ref} from '@vue/composition-api';
import {StateInterface} from 'src/store';
import {DrinkInterface} from '../store/drinks';
import {Store} from 'vuex';
export default defineComponent({
name: 'Pricelist',
filters: {
setVolume(volume: number) {
if (volume*10 > 1) {
return `${volume}l`
}
return `${volume*100}cl`
},
setCurrency(price: number) {
return `${price.toFixed(2)}`
}
},
setup(_, {root}) {
const store = <Store<StateInterface>>root.$store;
const state = <DrinkInterface>store.state.drink;
const drinks = ref(state.drinks)
onBeforeMount(() => {
void store.dispatch('drink/getDrinks');
})
const columns = [
{
name: 'name',
label: 'Getränk',
field: 'name'
},
{
name: 'prices',
label: 'Preise',
field: 'prices'
}
]
return {columns, drinks}
}
})
</script>

View File

@ -0,0 +1,37 @@
<template>
<div>
<q-page padding v-if="checkMain">
<q-card>
<q-card-section>
<q-list v-for="(mainRoute, index) in mainRoutes" :key="'mainRoute' + index">
<essential-link
v-for="(route, index2) in mainRoute.children"
:key="'route' + index2"
:title="route.title"
:icon="route.icon"
:link="route.name"
:permissions="route.meta.permissions"
/>
</q-list>
</q-card-section>
</q-card>
</q-page>
<router-view />
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from '@vue/composition-api';
import EssentialLink from 'src/components/navigation/EssentialLink.vue';
import mainRoutes from '../routes';
export default defineComponent({
// name: 'PageName'
components: { EssentialLink },
setup(_, { root }) {
const checkMain = computed(() => {
return root.$route.matched.length == 2;
});
return { checkMain, mainRoutes };
}
});
</script>

View File

@ -0,0 +1,15 @@
<template>
<Pricelist />
</template>
<script>
import Pricelist from '../components/Pricelist.vue'
export default {
name: 'PricelistPage',
components: {Pricelist}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,86 @@
<template>
<div>
<q-tabs v-model="tab" v-if="$q.screen.gt.sm">
<q-tab
v-for="(tabindex, index) in tabs"
:key="'tab' + index"
:name="tabindex.name"
:label="tabindex.label"
/>
</q-tabs>
<div class="fit row justify-end" v-else>
<q-btn flat round icon="mdi-menu" @click="showDrawer = !showDrawer"></q-btn>
</div>
<q-drawer side="right" v-model="showDrawer" @click="showDrawer = !showDrawer" behavior="mobile">
<q-list v-model="tab">
<q-item
v-for="(tabindex, index) in tabs"
:key="'tab' + index"
:active="tab == tabindex.name"
clickable
@click="tab = tabindex.name"
>
<q-item-label>{{ tabindex.label }}</q-item-label>
</q-item>
</q-list>
</q-drawer>
<q-page paddding class="fit row justify-center content-start items-start q-gutter-sm">
<q-tab-panels
v-model="tab"
style="background-color: transparent;"
animated
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
>
<q-tab-panel name="pricelist">
<h1>preisliste</h1>
</q-tab-panel>
<q-tab-panel name="new_drink">
<Drink />
</q-tab-panel>
<q-tab-panel name="drink_types">
<DrinkTypes />
</q-tab-panel>
</q-tab-panels>
</q-page>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from '@vue/composition-api';
import { Screen } from 'quasar';
import DrinkTypes from 'src/plugins/pricelist/components/DrinkTypes.vue';
import Drink from 'src/plugins/pricelist/components/Drink.vue';
export default defineComponent({
name: 'Settings',
components: { DrinkTypes, Drink },
setup(_) {
interface Tab {
name: string;
label: string;
}
const drawer = ref<boolean>(false);
const showDrawer = computed({
get: () => {
return !Screen.gt.sm && drawer.value;
},
set: (val: boolean) => {
drawer.value = val;
}
});
const tabs: Tab[] = [
{ name: 'pricelist', label: 'Getränke' },
{ name: 'new_drink', label: 'Neues Getränk' },
{ name: 'drink_types', label: 'Getränketypen' }
];
const tab = ref<string>('pricelist');
return { tabs, tab, showDrawer };
}
});
</script>
<style scoped></style>

View File

@ -0,0 +1,25 @@
import { Module } from 'vuex';
import routes from './routes';
import { StateInterface } from 'src/store';
import drink, { DrinkInterface } from 'src/plugins/pricelist/store/drinks';
import { FG_Plugin } from 'src/plugins';
const plugin: FG_Plugin.Plugin = {
name: 'Pricelist',
mainRoutes: routes,
requiredModules: [],
requiredBackendModules: ['pricelist'],
version: '0.0.1',
store: new Map<string, Module<DrinkInterface, StateInterface>>([['drink', drink]]),
widgets: []
// widgets: [
// {
// priority: 1,
// name: 'greeting',
// permissions: []
// widget: () => import('./components/Widget.vue')
// }
// ]
};
export default plugin;

View File

@ -0,0 +1,33 @@
import { FG_Plugin } from 'src/plugins';
const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
{
title: 'Getränke',
icon: 'mdi-glass-mug-variant',
path: 'drinks',
name: 'drinks',
component: () => import('../pages/MainPage.vue'),
meta: { permissions: ['user'] },
children: [
{
title: 'Preisliste',
icon: 'mdi-cash-100',
path: 'pricelist',
name: 'drinks-pricelist',
shortcut: true,
meta: { permissions: ['user'] },
component: () => import('../pages/Pricelist.vue')
},
{
title: 'Einstellungen',
icon: 'mdi-coffee-to-go',
path: 'settings',
name: 'drinks-settings',
shortcut: false,
meta: { permissions: ['users_edit_other'] },
component: () => import('../pages/Settings.vue')
}
]
}
];
export default mainRoutes;

View File

@ -0,0 +1,106 @@
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex';
import { StateInterface } from 'src/store';
import { axios } from 'src/boot/axios';
import { AxiosResponse } from 'axios';
export interface DrinkInterface {
drinkTypes: FG.DrinkType[];
drinks: FG.Drink[];
}
const state: DrinkInterface = {
drinkTypes: [],
drinks: []
};
const mutations: MutationTree<DrinkInterface> = {
setDrinkTypes(state, drinkTypes: FG.DrinkType[]) {
state.drinkTypes = drinkTypes;
},
setDrinkType(state, drinkType: FG.DrinkType) {
const item = state.drinkTypes.find(item => item.id == drinkType.id);
if (item) {
item.name = drinkType.name;
}
},
addDrinkType(state, drinkType: FG.DrinkType) {
state.drinkTypes.unshift(drinkType);
},
removeDrinkType(state, id: number) {
const index = state.drinkTypes.findIndex(item => item.id == id);
state.drinkTypes.splice(index, 1);
},
setDrinks(state, drinks: FG.Drink[]) {
state.drinks = drinks
},
setDrink(state, drink: FG.Drink) {
const index = state.drinks.findIndex(item => item.id = drink.id)
if (index) {
state.drinks[index] = drink
} else {
state.drinks.push(drink)
}
}
};
const actions: ActionTree<DrinkInterface, StateInterface> = {
getDrinkTypes({ commit }) {
axios
.get('/pricelist/drink-types')
.then((response: AxiosResponse<FG.DrinkType[]>) => {
commit('setDrinkTypes', response.data);
})
.catch(err => console.warn(err));
},
addDrinkType({ commit }, data) {
axios
.post('/pricelist/drink-types', data)
.then((response: AxiosResponse<FG.DrinkType>) => {
commit('addDrinkType', response.data);
})
.catch(err => {
console.warn(err);
});
},
removeDrinkType({ commit }, data: number) {
axios
.delete(`/pricelist/drink-types/${data}`)
.then(() => {
commit('removeDrinkType', data);
})
.catch(err => console.warn(err));
},
changeDrinkTypeName({ commit }, drinkType: FG.DrinkType) {
axios
.put(`/pricelist/drink-types/${drinkType.id}`, drinkType)
.then(() => {
commit('setDrinkType', drinkType);
})
.catch(err => console.warn(err));
},
getDrinks({commit}) {
axios.get('/pricelist/drinks')
.then((response: AxiosResponse<FG.Drink[]>) => {
commit('setDrinks', response.data)
})
.catch(err => console.warn(err))
},
createDrink({commit}, data) {
axios.post('/pricelist/drinks', data)
.then((response: AxiosResponse<FG.Drink>) => {
commit('setDrink', response.data)
})
.catch(err => console.warn(err))
}
};
const getters: GetterTree<DrinkInterface, StateInterface> = {};
const schedule: Module<DrinkInterface, StateInterface> = {
namespaced: true,
state,
mutations,
actions,
getters
};
export default schedule;