Creating admin pages, new brand API endpoint
This commit is contained in:
@@ -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, () => {
|
||||
|
||||
@@ -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>
|
||||
7
software/src/data/api/brandApi.ts
Normal file
7
software/src/data/api/brandApi.ts
Normal 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)
|
||||
}
|
||||
@@ -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> = [""]
|
||||
}
|
||||
0
software/src/data/stores/brandStore.ts
Normal file
0
software/src/data/stores/brandStore.ts
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
6
software/src/pages/admin/accountsPage/index.vue
Normal file
6
software/src/pages/admin/accountsPage/index.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Accounts Page
|
||||
</template>
|
||||
6
software/src/pages/admin/categoriesPage/index.vue
Normal file
6
software/src/pages/admin/categoriesPage/index.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Categories Page
|
||||
</template>
|
||||
6
software/src/pages/admin/dashboardPage/index.vue
Normal file
6
software/src/pages/admin/dashboardPage/index.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Admin Dashboard Page
|
||||
</template>
|
||||
98
software/src/pages/admin/productsPage/index.vue
Normal file
98
software/src/pages/admin/productsPage/index.vue
Normal 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>
|
||||
116
software/src/pages/admin/productsPage/productEditDialog.vue
Normal file
116
software/src/pages/admin/productsPage/productEditDialog.vue
Normal 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>
|
||||
@@ -91,6 +91,7 @@ async function doOrder() {
|
||||
@click="showDialog = false"
|
||||
prepend-icon="mdi-close"
|
||||
color="orange"
|
||||
:disabled="orderingInProgress"
|
||||
>
|
||||
{{ $t('dialog.cancel') }}
|
||||
</outlined-button>
|
||||
|
||||
@@ -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 }">
|
||||
|
||||
23
software/src/router/admin.routes.ts
Normal file
23
software/src/router/admin.routes.ts
Normal 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
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user