New basket table, add empty state on basket page, new BasketItemModel

This commit is contained in:
2024-09-09 19:47:46 +02:00
parent 6ff577ece1
commit 7ebc3c1c77
14 changed files with 190 additions and 68 deletions

View File

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

View File

@@ -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!`)
}) })

View File

@@ -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)
})
})

View File

@@ -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>

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

View File

@@ -0,0 +1,6 @@
export class OrderedItemModel {
orderId: number = -1
productId: number = -1
quantity: number = 1
totalPrice: number = 0
}

View File

@@ -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 = ""

View File

@@ -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)
}
} }
} }
}) })

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

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