New basket table, add empty state on basket page, new BasketItemModel
This commit is contained in:
@@ -52,7 +52,7 @@
|
|||||||
"categoryId": 3,
|
"categoryId": 3,
|
||||||
"discount": 0,
|
"discount": 0,
|
||||||
"rating": 4.2,
|
"rating": 4.2,
|
||||||
"description": "",
|
"description": "Goethe schrieb über 60 Jahre an seinem »Faust« und nannte »diese sehr ernsten Scherze« am Ende sein »Hauptgeschäft«: Dabei entstand eines der großartigsten und gleichzeitig komplexesten Werke der Weltliteratur. Den »Faust« gibt es bei Reclam in vielen Ausgaben, preisniedrig für Schüler, mit Kommentar für Studenten, bibliophil für Liebhaber – jetzt endlich auch eine Doppelausgabe der beiden klassischen Theatertexte in der Universal-Bibliothek: »Faust I« und »II« in einem Band.",
|
||||||
"imageUrl": "https://f.media-amazon.com/images/I/71p1k4JwDqL._SL1500_.jpg"
|
"imageUrl": "https://f.media-amazon.com/images/I/71p1k4JwDqL._SL1500_.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"categoryId": 3,
|
"categoryId": 3,
|
||||||
"discount": 0,
|
"discount": 0,
|
||||||
"rating": 3.5,
|
"rating": 3.5,
|
||||||
"description": "",
|
"description": " Hauke Haien ist ein genialer Außenseiter, der sich als junger Deichgraf einen Jugendtraum erfüllt: den Bau eines neuartigen Deiches, der den Wellen besser standhalten soll. Die Dorfbewohner sind skeptisch und sehen in ihm die Verkörperung einer uralten Sage: Wenn er auf seinem Schimmel über den Deich reitet, wird Hauke Haien zum dämonischen Reiter, der ihr Leben und ihre Gesetze aus dem Gleichgewicht bringt. Theodor Storms bekannteste Novelle ist ein Meisterwerk realistischer Erzählkunst, in dem es um den Widerstreit von Rationalität und Aberglaube, Fortschritt und Tradition geht.",
|
||||||
"imageUrl": "https://f.media-amazon.com/images/I/81uUWtGmKtL._SL1500_.jpg"
|
"imageUrl": "https://f.media-amazon.com/images/I/81uUWtGmKtL._SL1500_.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const sequelize = new Sequelize({
|
|||||||
|
|
||||||
export function startDatabase() {
|
export function startDatabase() {
|
||||||
// Create database and tables
|
// Create database and tables
|
||||||
sequelize.sync({ force: true })
|
sequelize.sync({ force: false })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log(`Database & tables created!`)
|
console.log(`Database & tables created!`)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,10 +3,17 @@ import { Product } from "../models/product.model";
|
|||||||
|
|
||||||
export const product = Router()
|
export const product = Router()
|
||||||
|
|
||||||
product.get("/", (req: Request, res: Response, next: NextFunction)=> {
|
product.get("/", (req: Request, res: Response, next: NextFunction) => {
|
||||||
Product.findAll()
|
Product.findAll()
|
||||||
.then(products => {
|
.then(products => {
|
||||||
res.json(products)
|
res.json(products)
|
||||||
})
|
})
|
||||||
.catch(next)
|
.catch(next)
|
||||||
|
})
|
||||||
|
|
||||||
|
product.get("/:productId", (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
Product.findByPk(req.params.productId)
|
||||||
|
.then(product => {
|
||||||
|
res.json(product)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -41,7 +41,7 @@ requestAllCategories()
|
|||||||
<v-list-item title="Produkte" prepend-icon="mdi-store" to="/products" link />
|
<v-list-item title="Produkte" prepend-icon="mdi-store" to="/products" link />
|
||||||
<v-list-item to="/basket" link title="Warenkorb">
|
<v-list-item to="/basket" link title="Warenkorb">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-badge color="primary" :content="basketStore.productsInBasket.length">
|
<v-badge color="primary" :content="basketStore.itemsInBasket.length">
|
||||||
<v-icon icon="mdi-cart" />
|
<v-icon icon="mdi-cart" />
|
||||||
</v-badge>
|
</v-badge>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
10
software/src/data/models/basketItemModel.ts
Normal file
10
software/src/data/models/basketItemModel.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export class BasketItemModel {
|
||||||
|
productId: number = -1
|
||||||
|
brand: string = ""
|
||||||
|
name: string = ""
|
||||||
|
categoryName: string = ""
|
||||||
|
categoryIcon: string = ""
|
||||||
|
price: number = 0
|
||||||
|
discount: number = 0
|
||||||
|
quantity: number = 1
|
||||||
|
}
|
||||||
6
software/src/data/models/orderedItemModel.ts
Normal file
6
software/src/data/models/orderedItemModel.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export class OrderedItemModel {
|
||||||
|
orderId: number = -1
|
||||||
|
productId: number = -1
|
||||||
|
quantity: number = 1
|
||||||
|
totalPrice: number = 0
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ export class ProductModel {
|
|||||||
price: number = 0
|
price: number = 0
|
||||||
discount: number = 0
|
discount: number = 0
|
||||||
rating: number = 1
|
rating: number = 1
|
||||||
nrOfArticles: number = 2
|
|
||||||
imageUrl: string = ""
|
imageUrl: string = ""
|
||||||
createdAt: string = ""
|
createdAt: string = ""
|
||||||
updatedAt: string = ""
|
updatedAt: string = ""
|
||||||
|
|||||||
@@ -1,30 +1,39 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
import { ProductModel } from "../models/productModel";
|
|
||||||
import { calcProductPrice } from "@/scripts/productScripts";
|
import { calcProductPrice } from "@/scripts/productScripts";
|
||||||
|
import { BasketItemModel } from "../models/basketItemModel";
|
||||||
|
|
||||||
export const useBasketStore = defineStore('basket', {
|
export const useBasketStore = defineStore('basket', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
productsInBasket: useLocalStorage<Array<ProductModel>>("hackmycart/basketStore/productsInBasket", [])
|
itemsInBasket: useLocalStorage<Array<BasketItemModel>>("hackmycart/basketStore/productsInBasket", [])
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
getTotalPrice() {
|
getTotalPrice() {
|
||||||
let result = 0
|
let result = 0
|
||||||
|
|
||||||
for (let product of this.productsInBasket) {
|
for (let item of this.itemsInBasket) {
|
||||||
result += calcProductPrice(product)
|
result += calcProductPrice(item, item.quantity)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return Math.round(result * 100) / 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
removeProductFromBasket(product: ProductModel) {
|
removeItemFromBasket(item: BasketItemModel) {
|
||||||
this.productsInBasket = this.productsInBasket.filter((p: ProductModel) =>
|
this.itemsInBasket = this.itemsInBasket.filter((basketItemModel: BasketItemModel) =>
|
||||||
p.id != product.id
|
basketItemModel.productId != item.productId
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
addItemToBasket(item: BasketItemModel) {
|
||||||
|
// Product is already in the basket, increase number of items
|
||||||
|
if (this.itemsInBasket.find((basketItem) => basketItem.productId == item.productId)) {
|
||||||
|
this.itemsInBasket.find((basketItem) => basketItem.productId == item.productId).quantity += item.quantity
|
||||||
|
} else {
|
||||||
|
this.itemsInBasket.push(item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ProductModel } from '@/data/models/productModel';
|
|
||||||
import { useBasketStore } from '@/data/stores/basketStore';
|
|
||||||
import { calcProductPrice } from '@/scripts/productScripts';
|
|
||||||
|
|
||||||
const basketStore = useBasketStore()
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
product: {
|
|
||||||
type: ProductModel,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function removeProductFromBasket(product: ProductModel) {
|
|
||||||
basketStore.removeProductFromBasket(product)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-list-item
|
|
||||||
:title="product.name"
|
|
||||||
:subtitle="product.brand"
|
|
||||||
>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-btn icon="mdi-delete" flat @click="removeProductFromBasket(product)"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:append>
|
|
||||||
{{ calcProductPrice(product) }} €
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
@@ -1,32 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useBasketStore } from '@/data/stores/basketStore';
|
import { useBasketStore } from '@/data/stores/basketStore';
|
||||||
import basketItem from './basketItem.vue';
|
import productsTable from './productsTable.vue';
|
||||||
|
|
||||||
const basketStore = useBasketStore()
|
const basketStore = useBasketStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container max-width="800">
|
<v-container max-width="1000">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-card title="Warenkorb" >
|
<v-card title="Warenkorb" >
|
||||||
<v-card-subtitle>
|
<v-card-subtitle v-if="basketStore.itemsInBasket.length > 0">
|
||||||
{{ basketStore.productsInBasket.length }} Artikel
|
{{ basketStore.itemsInBasket.length }} Artikel
|
||||||
</v-card-subtitle>
|
</v-card-subtitle>
|
||||||
|
|
||||||
<v-list>
|
<products-table v-if="basketStore.itemsInBasket.length > 0" />
|
||||||
<basket-item
|
|
||||||
v-for="product in basketStore.productsInBasket"
|
|
||||||
:product="product"
|
|
||||||
/>
|
|
||||||
</v-list>
|
|
||||||
|
|
||||||
<v-card-text class="text-right">
|
<v-empty-state v-else
|
||||||
|
icon="mdi-basket-off"
|
||||||
|
title="Keine Artikel im Warenkorb"
|
||||||
|
text="Gehe zu unseren Produkten und lege Artikel in den Warenkorb"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-card-text class="text-right" v-if="basketStore.itemsInBasket.length > 0">
|
||||||
Total: {{ basketStore.getTotalPrice }} €
|
Total: {{ basketStore.getTotalPrice }} €
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-btn prepend-icon="mdi-basket-check">Bestellen</v-btn>
|
<v-btn
|
||||||
|
prepend-icon="mdi-basket-check"
|
||||||
|
:disabled="basketStore.itemsInBasket.length == 0"
|
||||||
|
>
|
||||||
|
Bestellen
|
||||||
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|||||||
68
software/src/pages/basketPage/productsTable.vue
Normal file
68
software/src/pages/basketPage/productsTable.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { BasketItemModel } from '@/data/models/basketItemModel';
|
||||||
|
import { useBasketStore } from '@/data/stores/basketStore';
|
||||||
|
import { calcPrice, calcProductPrice } from '@/scripts/productScripts';
|
||||||
|
|
||||||
|
const basketStore = useBasketStore()
|
||||||
|
|
||||||
|
function removeFromBasket(basketItem: BasketItemModel) {
|
||||||
|
basketStore.removeItemFromBasket(basketItem)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Brand</th>
|
||||||
|
<th>Products</th>
|
||||||
|
<th class="text-center">Quantity</th>
|
||||||
|
<th class="text-right">Product price</th>
|
||||||
|
<th class="text-right">Total price</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="basketItem in basketStore.itemsInBasket">
|
||||||
|
<td><v-btn icon="mdi-delete" flat @click="removeFromBasket(basketItem)"/></td>
|
||||||
|
<td><v-icon :icon="basketItem.categoryIcon" /> {{ basketItem.categoryName }} </td>
|
||||||
|
<td>{{ basketItem.brand }}</td>
|
||||||
|
<td>{{ basketItem.name }}</td>
|
||||||
|
<td class="text-center">{{ basketItem.quantity }}x</td>
|
||||||
|
|
||||||
|
<td class="text-right">
|
||||||
|
<div v-if="basketItem.discount > 0">
|
||||||
|
<strong class="font-weight-bold text-body-1 text-red-lighten-1">
|
||||||
|
{{ calcPrice(basketItem.price, basketItem.discount) }} €
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<div class="text-decoration-line-through ml-3 mt-1 text-caption">{{ basketItem.price }} €</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
{{ basketItem.price }} €
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
<td class="text-right">
|
||||||
|
<div v-if="basketItem.discount > 0">
|
||||||
|
<strong class="font-weight-bold text-body-1 text-red-lighten-1">
|
||||||
|
{{ calcPrice(basketItem.price, basketItem.discount, basketItem.quantity) }} €
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<div class="text-decoration-line-through ml-3 mt-1 text-caption">
|
||||||
|
{{ calcPrice(basketItem.price, 0, basketItem.quantity) }} €
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
{{ calcPrice(basketItem.price, 0, basketItem.quantity) }} €
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</template>
|
||||||
@@ -145,5 +145,9 @@ watch(() => onlyDiscounts.value, () => { filterProducts() })
|
|||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<product-details v-model="showProductDetails" :product="dialogProduct" :productCategory="getCategoryById(dialogProduct.categoryId)" />
|
<product-details
|
||||||
|
v-model="showProductDetails"
|
||||||
|
:product="dialogProduct"
|
||||||
|
:productCategory="getCategoryById(dialogProduct.categoryId)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -2,10 +2,11 @@
|
|||||||
import { VNumberInput } from 'vuetify/labs/VNumberInput'
|
import { VNumberInput } from 'vuetify/labs/VNumberInput'
|
||||||
import { ProductModel } from '@/data/models/productModel';
|
import { ProductModel } from '@/data/models/productModel';
|
||||||
import { CategoryModel } from '@/data/models/categoryModel';
|
import { CategoryModel } from '@/data/models/categoryModel';
|
||||||
import { ref } from 'vue';
|
import { ModelRef, ref } from 'vue';
|
||||||
import { useBasketStore } from '@/data/stores/basketStore';
|
import { useBasketStore } from '@/data/stores/basketStore';
|
||||||
|
import { calcProductPrice, productToBasketItem } from '@/scripts/productScripts';
|
||||||
|
|
||||||
const showDialog = defineModel("showDialog", { type: Boolean })
|
const showDialog: ModelRef<boolean> = defineModel()
|
||||||
const nrOfArticles = ref(1)
|
const nrOfArticles = ref(1)
|
||||||
const basketStore = useBasketStore()
|
const basketStore = useBasketStore()
|
||||||
|
|
||||||
@@ -15,7 +16,8 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function addProductToBasket() {
|
function addProductToBasket() {
|
||||||
basketStore.productsInBasket.push(props.product)
|
basketStore.addItemToBasket(productToBasketItem(props.product, props.productCategory, nrOfArticles.value))
|
||||||
|
nrOfArticles.value = 1
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -46,11 +48,16 @@ function addProductToBasket() {
|
|||||||
:hideInput="false"
|
:hideInput="false"
|
||||||
:inset="false"
|
:inset="false"
|
||||||
v-model="nrOfArticles"
|
v-model="nrOfArticles"
|
||||||
|
:min="1"
|
||||||
|
:max="10"
|
||||||
|
density="comfortable"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col>
|
<v-spacer />
|
||||||
{{ nrOfArticles * product.price }} €
|
|
||||||
|
<v-col cols="2" class="justify-center d-flex">
|
||||||
|
{{ calcProductPrice(product, nrOfArticles) }} €
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|||||||
@@ -1,5 +1,44 @@
|
|||||||
|
import { BasketItemModel } from "@/data/models/basketItemModel";
|
||||||
|
import { CategoryModel } from "@/data/models/categoryModel";
|
||||||
import { ProductModel } from "@/data/models/productModel";
|
import { ProductModel } from "@/data/models/productModel";
|
||||||
|
|
||||||
export function calcProductPrice(product: ProductModel): number {
|
export function calcProductPrice(product: ProductModel, quantity: number = 1): number {
|
||||||
return Math.round(product.price * ((100 - product.discount) / 100) * 100) / 100
|
return calcPrice(product.price, product.discount, quantity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate a price based on parameters
|
||||||
|
*
|
||||||
|
* @param price Original price for one unit
|
||||||
|
* @param discount Discount in percent
|
||||||
|
* @param quantity Number of units
|
||||||
|
*
|
||||||
|
* @returns Price rounded to two digits
|
||||||
|
*/
|
||||||
|
export function calcPrice(price: number, discount: number = 0, quantity: number = 1): number {
|
||||||
|
return Math.round(quantity * price * ((100 - discount) / 100) * 100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a ProductModel to a BasketModel
|
||||||
|
*
|
||||||
|
* @param product ProductModel to convert
|
||||||
|
* @param productCategory Category of the product
|
||||||
|
* @param quantity Number of units
|
||||||
|
*
|
||||||
|
* @returns BasketItemModel
|
||||||
|
*/
|
||||||
|
export function productToBasketItem(product: ProductModel, productCategory: CategoryModel, quantity: number): BasketItemModel {
|
||||||
|
let result = new BasketItemModel()
|
||||||
|
|
||||||
|
result.productId = product.id
|
||||||
|
result.quantity = quantity
|
||||||
|
result.price = product.price
|
||||||
|
result.brand = product.brand
|
||||||
|
result.discount = product.discount
|
||||||
|
result.name = product.name
|
||||||
|
result.categoryName = productCategory.name
|
||||||
|
result.categoryIcon = productCategory.icon
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user