Creating admin pages, new brand API endpoint

This commit is contained in:
2024-09-25 15:42:05 +02:00
parent cbd01f6d59
commit f41cf1ba90
28 changed files with 417 additions and 76 deletions

View File

@@ -5,13 +5,11 @@ import { ref, watch } from 'vue';
import vuetify from './plugins/vuetify';
import navigationItems from './components/navigationItems.vue';
import { useProductStore } from './data/stores/productStore';
import { useCategoryStore } from './data/stores/categoryStore';
import { usePreferencesStore } from './data/stores/preferencesStore';
import { useFeedbackStore } from './data/stores/feedbackStore';
const preferencesStore = usePreferencesStore()
const productStore = useProductStore()
const categoryStore = useCategoryStore()
const feedbackStore = useFeedbackStore()
const theme = useTheme()
const navRail = ref(vuetify.display.mobile)
@@ -19,7 +17,8 @@ const navRail = ref(vuetify.display.mobile)
theme.global.name.value = preferencesStore.theme
productStore.fetchAllProducts()
categoryStore.fetchAllCategories()
productStore.fetchAllCategories()
productStore.fetchAllBrands()
// Global watcher
watch(() => preferencesStore.language, () => {

View File

@@ -61,5 +61,21 @@ const navRail = defineModel("navRail", { type: Boolean })
<v-list-item :title="$t('menu.helpInstructions')" prepend-icon="mdi-chat-question" to="/help" link />
<v-list-item :title="$t('menu.scoreBoard')" prepend-icon="mdi-podium-gold" to="/scoreBoard" link />
<v-list-item :title="$t('menu.preferences')" prepend-icon="mdi-cog" to="/preferences" link />
<div v-if="accountStore.userAccount.accountRole.privilegeAdminPanel">
<v-divider />
<v-list-subheader>
<div v-if="!navRail">{{ $t('menu.admin.admin') }}</div>
<div v-else></div>
</v-list-subheader>
<v-list-item :title="$t('menu.admin.dashboard')" prepend-icon="mdi-view-dashboard" to="/admin/dashboard" link />
<v-list-item :title="$t('menu.admin.categories')" prepend-icon="mdi-label" to="/admin/categories" link />
<v-list-item :title="$t('menu.admin.products')" prepend-icon="mdi-store-cog" to="/admin/products" link />
<v-list-item :title="$t('menu.admin.accounts')" prepend-icon="mdi-account-multiple" to="/admin/accounts" link />
</div>
</v-list>
</template>

View File

@@ -0,0 +1,7 @@
import axios from "axios";
let BASE_URL = "http://localhost:3000/brands"
export async function getAllBrands() {
return await axios.get(BASE_URL)
}

View File

@@ -10,7 +10,8 @@ export class ProductModel {
price: number = 0
discount: number = 0
rating: number = 1
inStock: number
inStock: number = 0
offered: boolean = true
specs: Array<string> = []
images: Array<string> = [""]
}

View File

View File

@@ -1,25 +0,0 @@
import { useLocalStorage } from "@vueuse/core";
import { defineStore } from "pinia";
import { CategoryModel } from "../models/categoryModel";
import { getAllCategories } from "../api/categoryApi";
export const useCategoryStore = defineStore("categoryStore", {
state: () => ({
categories: useLocalStorage<Array<CategoryModel>>("hackmycart/categoryStore/categories", [])
}),
actions: {
async fetchAllCategories() {
await getAllCategories()
.then(categories => {
this.categories = categories.data
})
},
getProductById(id: number): CategoryModel {
return this.categories.find(category =>
category.id === id
)
}
}
})

View File

@@ -4,6 +4,9 @@ import { getAllProducts } from "../api/productApi";
import { SortOrder } from "../enums/sortOrderEnum";
import { CategoryModel } from "../models/categoryModel";
import { ProductModel } from "../models/productModel";
import { BrandModel } from "../models/brandModel";
import { getAllCategories } from "../api/categoryApi";
import { getAllBrands } from "../api/brandApi";
export const useProductStore = defineStore("productStore", {
@@ -12,7 +15,9 @@ export const useProductStore = defineStore("productStore", {
filteredProducts: useLocalStorage<Array<ProductModel>>("hackmycart/productStore/filteredProducts", []),
sortOrder: useLocalStorage<SortOrder>("hackmycart/productStore/sortOrder", SortOrder.NAMEATOZ),
filteredCategory: useLocalStorage<CategoryModel>("hackmycart/productStore/filteredCategory", new CategoryModel()),
onlyDiscounts: useLocalStorage<Boolean>("hackmycart/productStore/onlyDiscounts", false)
onlyDiscounts: useLocalStorage<Boolean>("hackmycart/productStore/onlyDiscounts", false),
brands: useLocalStorage<Array<BrandModel>>("hackmycart/productStore/brands", []),
categories: useLocalStorage<Array<CategoryModel>>("hackmycart/productStore/categories", [])
}),
actions: {
@@ -24,6 +29,20 @@ export const useProductStore = defineStore("productStore", {
})
},
async fetchAllCategories() {
await getAllCategories()
.then(categories => {
this.categories = categories.data
})
},
async fetchAllBrands() {
await getAllBrands()
.then(brands => {
this.brands = brands.data
})
},
async filterProducts() {
if (this.filteredCategory.id == -1 || this.filteredCategory.id == 0) {
this.filteredProducts = this.products

View File

@@ -10,7 +10,14 @@
"helpInstructions": "Hilfestellung",
"preferences": "Einstellungen",
"logout": "Ausloggen",
"scoreBoard": "Score Board"
"scoreBoard": "Score Board",
"admin": {
"admin": "Administration",
"dashboard": "Dashboard",
"categories": "Kategorien",
"products": "Produkte",
"accounts": "Accounts"
}
},
"preferences": {
"pageSetup": "Seiteneinstellungen",
@@ -29,10 +36,12 @@
"category": "Kategorie",
"description": "Beschreibung",
"storedItemsAvailable": "{0} verfügbar",
"soldOut": "Ausverkauft"
"soldOut": "Ausverkauft",
"discount": "Rabatt",
"inStock": "Warenbestand"
},
"offers": "Angebote",
"categories": "Kategorien",
"category": "Kategorie | Kategorien",
"sortBy": "Sortieren nach",
"quantity": "Anzahl",
"addToBasket": "Zum Warenkorb hinzufügen",
@@ -133,5 +142,8 @@
"emailRequired": "E-Mail-Adresse benötigt",
"tooMuchChars": "Zu lang",
"digitsAtStartNeeded": "Muss mit einer Zahl beginnen",
"wrongIban": "Falsches IBAN Format, nur deutsche IBAN-Nummern erlaubt!"
"wrongIban": "Falsches IBAN Format, nur deutsche IBAN-Nummern erlaubt!",
"save": "Speichern",
"editProduct": "Produkt bearbeiten",
"brand": "Marke | Marken"
}

View File

@@ -10,7 +10,14 @@
"helpInstructions": "Help instructions",
"preferences": "Preferences",
"logout": "Logout",
"scoreBoard": "Score Board"
"scoreBoard": "Score Board",
"admin": {
"admin": "Administration",
"dashboard": "Dashboard",
"categories": "Categories",
"products": "Products",
"accounts": "Accounts"
}
},
"preferences": {
"pageSetup": "Page setup",
@@ -29,10 +36,12 @@
"category": "Category",
"description": "Description",
"storedItemsAvailable": "{0} items available",
"soldOut": "Sold Out"
"soldOut": "Sold Out",
"discount": "Discount",
"inStock": "In Stock"
},
"offers": "Offers",
"categories": "Categories",
"category": "Category | Categories",
"sortBy": "Sort by",
"quantity": "Quantity",
"addToBasket": "Add to basket",
@@ -133,5 +142,8 @@
"emailRequired": "E-Mail is required",
"tooMuchChars": "Too long",
"digitsAtStartNeeded": "Has to beginn with a number",
"wrongIban": "Wrong IBAN format, only German IBANs are allowed!"
"wrongIban": "Wrong IBAN format, only German IBANs are allowed!",
"save": "Save",
"editProduct": "Edit product",
"brand": "Brand | Brands"
}

View File

@@ -12,7 +12,7 @@ const accountStore = useAccountStore()
<template>
<card-view
icon="mdi-home"
:title="$t('account.addresses')"
:title="$t('account.address')"
>
<v-expansion-panels v-if="accountStore.userAccount.addresses.length > 0">
<v-expansion-panel

View File

@@ -12,7 +12,7 @@ const accountStore = useAccountStore()
<template>
<card-view
icon="mdi-currency-usd"
:title="$t('account.payments')"
:title="$t('account.payment')"
>
<v-expansion-panels
v-if="accountStore.userAccount.payments.length > 0"

View File

@@ -0,0 +1,6 @@
<script setup lang="ts">
</script>
<template>
Accounts Page
</template>

View File

@@ -0,0 +1,6 @@
<script setup lang="ts">
</script>
<template>
Categories Page
</template>

View File

@@ -0,0 +1,6 @@
<script setup lang="ts">
</script>
<template>
Admin Dashboard Page
</template>

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import cardView from '@/components/cardView.vue';
import { ProductModel } from '@/data/models/productModel';
import { useProductStore } from '@/data/stores/productStore';
import productEditDialog from './productEditDialog.vue';
import { ref } from 'vue';
const productStore = useProductStore()
const editProduct = ref(new ProductModel())
const showEditProductDialog = ref(false)
const headers = [
{ title: "Category", value: "category.name" },
{ title: "Brand", value: "brand.name" },
{ title: "Name", value: "name" },
{ title: "Price", value: "price" },
{ title: "Discount", value: "discount" },
{ title: "Rating", value: "rating" },
{ title: "In stock", value: "inStock" },
{ title: "Edit", value: "edit" },
]
function openEditProductDialog(product: ProductModel) {
editProduct.value = product
showEditProductDialog.value = true
}
</script>
<template>
<v-container>
<v-row>
<v-col>
<card-view
:title="$t('product.product', 2)"
:subtitle="productStore.products.length + ' ' + $t('product.product', productStore.products.length)"
icon="mdi-store"
>
<v-data-table
:headers="headers"
:items="productStore.products"
>
<template v-slot:item.price="{ item }">
{{ item.price }}
</template>
<template v-slot:item.discount="{ item }">
<div v-if="item.discount > 0">
{{ item.discount }} %
</div>
</template>
<template v-slot:item.inStock="{ item }">
<div v-if="item.inStock > 5" class="text-green-lighten-1">
<v-icon icon="mdi-check" />
{{ $t("product.storedItemsAvailable", [item.inStock]) }}
</div>
<div v-else-if="item.inStock > 0" class="text-orange-lighten-1">
<v-icon icon="mdi-alert" />
{{ $t("product.storedItemsAvailable", [item.inStock]) }}
</div>
<div v-else class="text-red">
<v-icon icon="mdi-alert-circle" />
{{ $t("product.soldOut") }}
</div>
</template>
<template v-slot:item.edit="{ item }">
<v-btn
icon="mdi-pencil"
color="orange"
variant="text"
@click="openEditProductDialog(item)"
/>
<v-btn
icon="mdi-store-off"
color="red"
variant="text"
v-if="item.offered"
/>
<v-btn
icon="mdi-store"
color="green"
variant="text"
v-if="!item.offered"
/>
</template>
</v-data-table>
</card-view>
</v-col>
</v-row>
</v-container>
<product-edit-dialog
v-model="showEditProductDialog"
:edit-product="editProduct" />
</template>

View File

@@ -0,0 +1,116 @@
<script setup lang="ts">
import actionDialog from '@/components/actionDialog.vue';
import { ProductModel } from '@/data/models/productModel';
import outlinedButton from '@/components/outlinedButton.vue';
import { useProductStore } from '@/data/stores/productStore';
import { ModelRef } from 'vue';
const showDialog: ModelRef<boolean> = defineModel()
const editProduct = defineModel("editProduct", { type: ProductModel, required: true})
const productStore = useProductStore()
function saveProduct() {
// todo
showDialog.value = false
}
</script>
<template>
<action-dialog
:title="$t('editProduct')"
icon="mdi-store-edit"
v-model="showDialog"
>
<v-row>
<v-col>
<v-select
:items="productStore.categories"
v-model="editProduct.category"
:label="$t('category')"
>
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props" :prepend-icon="item.raw.icon" :title="item.raw.name" />
</template>
<template v-slot:selection="{ item }">
<v-list-item :prepend-icon="item.raw.icon" :title="item.raw.name" />
</template>
</v-select>
</v-col>
<v-col>
<v-select
:items="productStore.brands"
v-model="editProduct.brand"
:label="$t('brand')"
>
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props" :title="item.raw.name" />
</template>
<template v-slot:selection="{ item }">
<v-list-item :title="item.raw.name" />
</template>
</v-select>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model="editProduct.name"
:label="$t('product.productName')"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-textarea
v-model="editProduct.description"
:label="$t('product.description')"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model="editProduct.price"
:label="$t('product.productPrice')"
suffix="€"
/>
</v-col>
<v-col>
<v-text-field
v-model="editProduct.discount"
:label="$t('product.discount')"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model="editProduct.inStock"
:label="$t('product.inStock')"
/>
</v-col>
</v-row>
<!-- todo -->
{{ editProduct.images }}
{{ editProduct.specs }}
<template #actions>
<outlined-button
color="green"
prepend-icon="mdi-content-save"
@click="saveProduct"
>
{{ $t('save') }}
</outlined-button>
</template>
</action-dialog>
</template>

View File

@@ -91,6 +91,7 @@ async function doOrder() {
@click="showDialog = false"
prepend-icon="mdi-close"
color="orange"
:disabled="orderingInProgress"
>
{{ $t('dialog.cancel') }}
</outlined-button>

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import { SortOrder } from '@/data/enums/sortOrderEnum';
import { useCategoryStore } from '@/data/stores/categoryStore';
import { useProductStore } from '@/data/stores/productStore';
const productStore = useProductStore()
const categoryStore = useCategoryStore()
const sortOrderItems = Object.values(SortOrder)
</script>
@@ -19,8 +17,8 @@ const sortOrderItems = Object.values(SortOrder)
<v-list-item>
<v-select
:items="categoryStore.categories"
:label="$t('categories')"
:items="productStore.categories"
:label="$t('category', 2)"
v-model="productStore.filteredCategory"
>
<template v-slot:item="{ props, item }">

View File

@@ -0,0 +1,23 @@
import DashboardPage from "@/pages/admin/dashboardPage/index.vue"
import CategoriesPage from "@/pages/admin/categoriesPage/index.vue"
import AccountsPage from "@/pages/admin/accountsPage/index.vue"
import ProductsPage from "@/pages/admin/productsPage/index.vue"
export default [
{
path: '/admin/dashboard',
component: DashboardPage
},
{
path: '/admin/categories',
component: CategoriesPage
},
{
path: '/admin/accounts',
component: AccountsPage
},
{
path: '/admin/products',
component: ProductsPage
}
]

View File

@@ -6,6 +6,7 @@ import LoginPage from "@/pages/loginPage/index.vue"
import BasketPage from "@/pages/basketPage/index.vue"
import HelpPage from "@/pages/helpPage/index.vue"
import ScoreBoardPage from "@/pages/scoreBoardPage/index.vue"
import adminRoutes from "./admin.routes";
const routes = [
{ path: '/', component: ProductsPage },
@@ -15,7 +16,8 @@ const routes = [
{ path: '/login', component: LoginPage },
{ path: '/basket', component: BasketPage },
{ path: '/scoreboard', component: ScoreBoardPage },
{ path: '/help', component: HelpPage }
{ path: '/help', component: HelpPage },
...adminRoutes
]
export default routes