Refactor dashboard and UI components; add theming system

Introduce a centralized theming configuration via `ThemeConfig.vue`. Refactor various dashboard components for consistency and modularity, including new wrappers (`DepartmentDashboardPageWrapper`, `WhiteBox`, `ButtonsBox`). Enhance user context with `UserNavigationTop` and improve menu structure with reusable menus (`AdminLeftMenu`, `MenuDefault`). Update button and UI element styles using dynamic theming.
This commit is contained in:
Jepp9350
2025-02-25 11:29:41 +01:00
parent 18dc8fb40f
commit edc979d8fb
16 changed files with 667 additions and 20 deletions
+100
View File
@@ -0,0 +1,100 @@
<script>
import { ref } from "vue";
export const GlobalColors = {
/** The primary color of the application */
primaryColor: "#0064ff",
/** The secondary color of the application */
secondaryColor: "#363636",
/** The secondary background color of the application */
secondaryBackgroundColorDark: "#363636",
/** The text color of the application */
textColor: "#c8c8c8",
/** The light text color of the application */
lightTextColor: "#ffffff",
/** The dark text color of the application */
textColorDark: "#5a5a5a",
/** The hover color of the application */
hoverColor: "#e6e6e6",
/** The active color of the application */
activeColor: "#e6e6e6",
/** The background color of the application */
backgroundColor: "#e6efff",
/** The secondary background color of the application */
secondaryBackgroundColor: "#5a5a5a",
/** Active Background Color */
activeBackgroundColor: "#475c7c",
};
export const MenuColors = {
/** The background color of the menu */
backgroundColor: GlobalColors.secondaryBackgroundColor,
/** The text color of the menu */
textColor: GlobalColors.textColor,
/** The hover color of the menu */
hoverColor: GlobalColors.hoverColor,
/** The active color of the menu */
activeColor: GlobalColors.primaryColor,
/** The active background color of the menu */
activeBackgroundColor: GlobalColors.activeBackgroundColor,
/** The parent background color of the menu */
parentBackgroundColor: GlobalColors.secondaryBackgroundColor,
/** The parent text color of the menu */
parentTextColor: GlobalColors.textColor,
/** The parent hover color of the menu */
parentHoverColor: GlobalColors.hoverColor,
/** The parent active color of the menu */
parentActiveColor: GlobalColors.activeColor,
/** The child background color of the menu */
childBackgroundColor: GlobalColors.secondaryBackgroundColor,
/** The child text color of the menu */
childTextColor: GlobalColors.textColor,
/** The child hover color of the menu */
childHoverColor: GlobalColors.hoverColor,
/** The child active color of the menu */
childActiveColor: GlobalColors.activeColor,
};
export const HeaderColors = {
/** The background color of the header */
backgroundColor: GlobalColors.primaryColor,
/** The text color of the header */
textColor: GlobalColors.lightTextColor,
/** The secondary text color of the header */
textColorSecondary: GlobalColors.secondaryBackgroundColorDark,
};
export const ButtonColors = {
success: {
/** The background color of the button */
backgroundColor: "#00be29",
/** The text color of the button */
textColor: GlobalColors.lightTextColor,
/** The hover color of the button */
hoverColor: "#00a226",
/** The active color of the button */
activeColor: "#00a226",
},
default: {
/** The background color of the button */
backgroundColor: GlobalColors.secondaryBackgroundColor,
/** The text color of the button */
textColor: GlobalColors.lightTextColor,
/** The hover color of the button */
hoverColor: GlobalColors.hoverColor,
/** The active color of the button */
activeColor: GlobalColors.activeColor,
},
};
export const Colors = {
/** The colors of the application */
global: GlobalColors,
/** The colors of the menus */
menus: MenuColors,
/** The colors of the headers */
headers: HeaderColors,
/** The colors of the buttons */
buttons: ButtonColors,
};
</script>
@@ -0,0 +1,13 @@
<script setup>
</script>
<template>
<div>
<slot></slot>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,20 @@
<script setup>
import {useSlots} from 'vue';
const slots = useSlots();
const isSlotEmpty = () => {
return Object.keys(slots).length === 0;
}
</script>
<template>
<div class="box has-sharp-edges" v-if="!isSlotEmpty()">
<slot name="default"></slot>
</div>
</template>
<style scoped>
.box.has-sharp-edges {
border-radius: 0;
}
</style>
@@ -8,6 +8,9 @@ import PosNotes from "@/components/displays/department/pos/PosNotes.vue";
import PosSelectedCustomer from "@/components/displays/department/pos/PosSelectedCustomer.vue";
import Cancel from "@/components/forms/department/pos/buttons/Cancel.vue";
import { clearCache } from "@/components/shop/POSDepartmentProcess.vue";
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
import ButtonsBox from "@/components/displays/boxes/ButtonsBox.vue";
import { isCustomerSelected } from "@/components/shop/POSDepartmentProcess.vue";
// Clear cache
clearCache();
@@ -17,18 +20,39 @@ clearCache();
<div>
<div class="columns">
<!-- Choose customer -->
<div class="column is-8-desktop">
<SelectCustomerFormPOS/>
<div class="column is-8-desktop pl-0 pr-3">
<WhiteBox>
<template #default>
<SelectCustomerFormPOS/>
</template>
</WhiteBox>
<ButtonsBox>
<div class="buttons">
<Cancel
style="width: 25%;"
tabindex="2"
class="is-fullwidth"
/>
</div>
</ButtonsBox>
</div>
<!-- Last seen License Plates in department -->
<div class="column is-4-desktop">
<PosSelectedCustomer />
<PosLastScannedLicensePlates class="my-3"/>
<div class="column is-4-desktop pl-3 pr-0">
<WhiteBox>
<template #default v-if="isCustomerSelected()">
<PosSelectedCustomer />
</template>
</WhiteBox>
<WhiteBox>
<template #default>
<PosLastScannedLicensePlates class="my-3"/>
</template>
</WhiteBox>
<NextStepError class="is-fullwidth" />
<div class="buttons">
<ButtonsBox>
<NextStep class="is-fullwidth" tabindex="6"/>
<Cancel class="is-fullwidth" tabindex="7"/>
</div>
</ButtonsBox>
<NextStepError class="is-fullwidth" />
</div>
</div>
</div>
@@ -0,0 +1,13 @@
<script setup>
import { SessionUser } from "@/components/session/token/SessionUser.vue";
console.log(SessionUser);
console.log(SessionUser.user);
</script>
<template>
<p>{{SessionUser.user.display_name}}</p>
</template>
<style scoped>
</style>
@@ -1,9 +1,16 @@
<script setup>
import { deleteEverything, showDeleteEverythingDialog } from "@/components/shop/POSDepartmentProcess.vue";
import { Colors } from "@/ThemeConfig.vue";
</script>
<template>
<button class="button is-inverted is-danger" @click="showDeleteEverythingDialog()">Slet alt</button>
<button
class="button"
@click="showDeleteEverythingDialog()"
:style="{backgroundColor: Colors.buttons.default.backgroundColor, color: Colors.buttons.default.textColor}"
>
Slet alt
</button>
</template>
<style scoped>
@@ -1,9 +1,15 @@
<script setup>
import { nextStep, nextStepDelay } from "@/components/shop/POSDepartmentProcess.vue";
import { Colors } from "@/ThemeConfig.vue";
</script>
<template>
<button class="button" @click="nextStep" v-if="nextStepDelay === 0">Next Step</button>
<button
class="button"
@click="nextStep" v-if="nextStepDelay === 0"
:style="{ 'background-color': Colors.buttons.success.backgroundColor, 'color': Colors.buttons.success.textColor }"
>
Næste</button>
<button class="button is-loading" v-else>Please wait... {{ nextStepDelay }}</button>
</template>
+13 -2
View File
@@ -4,14 +4,25 @@ defineProps({
subtitle: String
})
import { defineSlots } from 'vue'
import { Colors } from '@/ThemeConfig.vue'
</script>
<template>
<div class="mb-4">
<div class="columns is-vcentered is-mobile">
<div class="column">
<h1 class="title is-3">{{ title }}</h1>
<h2 class="subtitle is-5">{{ subtitle }}</h2>
<h1
style="margin-bottom: 0"
class="is-size-3"
:style="{ 'color': Colors.global.textColorDark }"
>
{{ title }}
</h1>
<p class="is-size-5"
:style="{ 'color': Colors.global.textColorDark }"
>
{{ subtitle }}
</p>
</div>
<div class="column is-narrow">
<slot name="buttons"></slot>
+228
View File
@@ -0,0 +1,228 @@
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { defineProps } from 'vue'
import { Colors } from '@/ThemeConfig.vue'
const props = defineProps({
menu_items: {
type: Array,
required: true
},
show_icons: {
type: Boolean,
default: true
},
})
const route = useRoute()
const menu_items = ref([
{
name: 'Overblik',
route: '/admin',
icon: 'fas fa-tachometer-alt',
children: []
},
{
name: 'Vælg afdeling',
options: [
{
label: 'Afdeling 1',
value: 1,
icon: 'fas fa-building',
children: []
},
{
label: 'Afdeling 2',
value: 2,
icon: 'fas fa-building',
children: []
},
{
label: 'Afdeling 3',
value: 3,
icon: 'fas fa-building',
children: []
}
],
selected: null,
onSelect: async (value) => {
console.log(value);
},
isExpanded: false
},
{
name: 'Administration',
route: '/administration',
icon: 'fas fa-cogs',
children: [
{
name: 'Team Settings',
route: '/team-settings',
icon: 'fas fa-users-cog',
children: []
},
{
name: 'Manage Your Team',
route: '/manage-your-team',
icon: 'fas fa-users',
children: [
{
name: 'Members',
route: '/members',
icon: 'fas fa-user',
children: []
},
{
name: 'Plugins',
route: '/plugins',
icon: 'fas fa-plug',
children: []
},
{
name: 'Add a member',
route: '/add-a-member',
icon: 'fas fa-user-plus',
children: []
}
]
},
{
name: 'Invitations',
route: '/invitations',
icon: 'fas fa-envelope',
children: []
},
{
name: 'Cloud Storage Environment Settings',
route: '/cloud-storage-environment-settings',
icon: 'fas fa-cloud',
children: []
},
{
name: 'Authentication',
route: '/authentication',
icon: 'fas fa-key',
children: []
}
]
},
{
name: 'Transactions',
route: '/transactions',
icon: 'fas fa-exchange-alt',
children: [
{
name: 'Payments',
route: '/payments',
icon: 'fas fa-money-bill-wave',
children: []
},
{
name: 'Transfers',
route: '/transfers',
icon: 'fas fa-exchange-alt',
children: []
},
{
name: 'Balance',
route: '/balance',
icon: 'fas fa-balance-scale',
children: []
}
]
}
]);
// If the menu items are passed as props, use them instead
if (props.menu_items.length > 0) {
menu_items.value = props.menu_items
}
</script>
<template>
<aside
class="menu"
:style="{ 'background-color': Colors.menus.parentBackgroundColor, 'color': Colors.menus.parentTextColor }"
>
<!-- Image -->
<div class="menu-image" style="padding: 1rem;">
<img src="@/assets/branding/truckwash-banner-white-compressed.png" alt="Truck Wash Logo" />
</div>
<p class="menu-label"
:style="{ 'color': Colors.menus.parentTextColor, 'background-color': Colors.menus.parentBackgroundColor }"
>
</p>
<ul class="menu-list"
:style="{ 'color': Colors.menus.textColor, 'background-color': Colors.menus.backgroundColor }"
>
<li v-for="item in menu_items" :key="item.name" :style="{'border-left':
(item.selected)
? '0.8rem solid ' + Colors.menus.activeColor
: '0.8rem solid transparent' }
">
<a :class="{'is-active': route.path === item.route}" :href="item.route"
:style="{ 'color': Colors.menus.textColor,
'background-color': (item.selected)
? Colors.menus.activeBackgroundColor
: Colors.menus.backgroundColor }
"
@click="(item.options) ? item.isExpanded = !item.isExpanded : null"
>
<!-- If the item has options, show the dropdown icon -->
<span class="icon is-small is-pulled-left mr-2 is-vcentered"
v-if="item.options"
:class="{'is-active': item.isExpanded}"
:style="{ 'rotate': item.isExpanded ? '180deg' : '0deg' }"
><i class="fas fa-angle-down"></i></span>
<!-- If the item has an icon, show it -->
<span class="icon is-small" v-if="props.show_icons"><i :class="item.icon"></i></span>
<!-- If the item contains a selected value, show it -->
<span v-if="item.selected && !item.isExpanded"><strong>{{ item.options.find(option => option.value === item.selected).label }}</strong></span>
<span v-else><strong>{{ item.name }}</strong></span>
</a>
<ul v-if="item.options" class="menu-list" style="border-left: 0;">
<!-- Dropdown -->
<li v-for="option in item.options" :key="option.label" :style="{ 'color': Colors.menus.childTextColor,
'background-color': (item.selected === option.value)
? Colors.menus.activeBackgroundColor
: Colors.menus.backgroundColor }"
v-if="item.isExpanded">
<a :class="{ 'is-active': item.selected === option.value }"
class="menu-list-custom-hover-able-child-item"
@click="item.onSelect(option.value)"
:style="{ 'color': Colors.menus.childTextColor,
'background-color': (item.selected === option.value)
? Colors.menus.activeBackgroundColor
: Colors.menus.childBackgroundColor
}"
>
<span class="icon is-small" v-if="props.show_icons"><i :class="option.icon"></i></span>
<span v-if="item.selected === option.value"><strong>{{ option.label }}</strong></span>
<span v-else>{{ option.label }}</span>
</a>
</li>
</ul>
<ul v-else class="menu-list">
<!-- Children -->
<li v-for="child in item.children" :key="child.name">
<a :class="{'is-active': route.path === child.route}" :href="child.route"
:style="{ 'color': Colors.menus.childTextColor, 'background-color': Colors.menus.childBackgroundColor }">
<span class="icon is-small" v-if="props.show_icons"><i :class="child.icon"></i></span>
{{ child.name }}
</a>
</li>
</ul>
</li>
</ul>
</aside>
</template>
<style scoped>
.menu-list-custom-hover-able-child-item:hover {
background-color: #4a4a4a !important;
}
</style>
@@ -0,0 +1,116 @@
<script setup>
import MenuDefault from "@/components/menus/MenuDefault.vue";
import {ref} from "vue";
import {SessionUser} from "@/components/session/token/SessionUser.vue";
// Define the isLoaded function
// This is here, because the departments need to be loaded before the menu is shown,
// otherwise the menu will look for a department with an id not defined.
const isLoaded = ref(false);
// Load the departments
SessionUser.objects.departments.get.all().then((departments) => {
const options = [];
departments.forEach((department) => {
options.push({
label: department.name,
value: department.id,
icon: 'fas fa-building',
children: []
});
});
console.log(departments);
menu_items.value[1].options = options;
isLoaded.value = true;
}).catch((error) => {
console.error(error);
});
const menu_items = ref([
{
name: 'Overblik',
route: '/admin',
icon: 'fas fa-tachometer-alt',
children: []
},
{
name: 'Vælg afdeling',
options: [
{
label: 'Afdeling 1',
value: 1,
icon: 'fas fa-building',
children: []
},
{
label: 'Afdeling 2',
value: 2,
icon: 'fas fa-building',
children: []
},
{
label: 'Afdeling 3',
value: 3,
icon: 'fas fa-building',
children: []
}
],
selected: SessionUser.functions.getDepartmentIdFromUrl() !== null ? parseInt(SessionUser.functions.getDepartmentIdFromUrl()) : null,
onSelect: async (value) => {
selectDepartment(value);
}
},
{
name: 'Kassesystem',
route: '/admin/' + SessionUser.functions.getDepartmentIdFromUrl() + '/modules/pos',
permissions: [
'canAccessAdmin'
],
icon: 'fas fa-cogs',
children: [
{
name: 'Opret ny',
route: '/admin/' + SessionUser.functions.getDepartmentIdFromUrl() + '/modules/pos',
icon: 'fas fa-plus',
children: []
},
{
name: 'Transaktioner',
route: '/admin/' + SessionUser.functions.getDepartmentIdFromUrl() + '/modules/pos/orders',
icon: 'fas fa-exchange-alt',
children: []
},
]
},
{
name: 'Bookinger',
route: '/admin/' + SessionUser.functions.getDepartmentIdFromUrl() + '/modules/bookings',
icon: 'fas fa-exchange-alt',
children: []
}
]);
const selectDepartment = (department) => {
// Get the current path (After /admin/:department_id)
const currentPath = window.location.pathname.split('/').slice(3).join('/');
console.log('Selected department: ' + department);
menu_items.value[1].selected = department;
// Hide the menu
menu_items.value[1].isExpanded = false;
// Redirect to the department
SessionUser.functions.redirectTo.department(department, currentPath);
}
</script>
<template>
<MenuDefault v-if="isLoaded"
v-bind="{menu_items}"
:show_icons="false"
/>
</template>
<style scoped>
</style>
@@ -4,7 +4,7 @@ import '@sweetalert2/theme-bulma/bulma.scss';
</script>
<template>
<div class="container" style="padding-left: 10px; padding-right: 10px;">
<div>
<slot></slot>
</div>
</template>
@@ -16,6 +16,7 @@ import {EconomicProducts} from "@/components/session/token/SessionUser/Objects/e
import {Roles} from "@/components/session/token/SessionUser/Objects/Roles.vue";
import {Permissions} from "@/components/session/token/SessionUser/Objects/Permissions.vue";
import {ObjectsGlobal} from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import {useRouter} from "vue-router";
/**
* Initiate the user session on app start
@@ -68,6 +69,7 @@ export const getSessionData = async () => {
SessionUser.user.group_id.value = response.data.data.group_id;
SessionUser.user.created_at.value = response.data.data.created_at;
SessionUser.user.updated_at.value = response.data.data.updated_at;
SessionUser.user.display_name.value = response.data.data.display_name;
// Set the last cached time to now, this is used to determine if the user's data is outdated
SessionUser.user.cached_at.value = new Date();
SessionUser.permissions.value = response.data.data.permissions;
@@ -75,6 +77,10 @@ export const getSessionData = async () => {
if (response.data.data.economic_customer.length > 0) {
SessionUser.economicData.customerNumber.value = response.data.data.economic_customer.customerNumber;
SessionUser.economicData.name.value = response.data.data.economic_customer.name;
// Set the display name to the e-conomic name if it's empty
if (SessionUser.user.display_name.value === null) {
SessionUser.user.display_name.value = response.data.data.economic_customer.name;
}
SessionUser.economicData.address.value = response.data.data.economic_customer.address;
SessionUser.economicData.zip.value = response.data.data.economic_customer.zip;
SessionUser.economicData.city.value = response.data.data.economic_customer.city;
@@ -251,6 +257,42 @@ export const SessionUser = {
/** reCAPTCHA */
reCAPTCHA: reCAPTCHA,
},
functions: {
/** Get the department id from the url */
getDepartmentIdFromUrl: () => {
// Check if the department id is in the route
const router = useRouter();
return router.currentRoute.value.params.departmentId;
},
/** Redirect to */
redirectTo: {
/**
* Redirect to a department
* @note This will redirect the user to '/admin/:department(/:path)'
* @param department The department id
* @param path The path to redirect to (Optional)
*/
department: (department, path = null) => {
window.location.href = '/admin/' + department + (path !== null ? '/' + path : '');
},
/**
* Redirect to a superuser page
* @note This will redirect the user to '/superuser(/:path)'
* @param path The path to redirect to (Optional)
*/
superUser: (path = null) => {
window.location.href = '/superuser' + (path !== null ? path : '');
},
/**
* Redirect to a user page
* @note This will redirect the user to '/user(/:path)'
* @param path The path to redirect to (Optional)
*/
user: (path = null) => {
window.location.href = '/user' + (path !== null ? path : '');
},
}
},
request: authenticatedRequest,
editField: EditFieldForm,
getSessionData: getSessionData,
+2
View File
@@ -9,6 +9,7 @@ import { createApp } from 'vue'
import App from './App.vue'
import store from './store/user.vue'
import {router} from "@/router.js";
import {Colors} from "./ThemeConfig.vue";
import applyMiddleware from '@/middleware/index.js';
applyMiddleware(router);
@@ -16,4 +17,5 @@ applyMiddleware(router);
createApp(App)
.use(store)
.use(router)
.provide('Colors', Colors)
.mount('#app')
@@ -13,8 +13,8 @@ const departmentId = ref(router.currentRoute.value.params.departmentId);
<template>
<div>
<UserNavigation />
<DepartmentDashboardNavigation />
<!-- <UserNavigation /> -->
<!-- <DepartmentDashboardNavigation /> -->
<PageTitle :title="title" :subtitle="subtitle"><slot></slot></PageTitle>
</div>
</template>
@@ -0,0 +1,56 @@
<script setup>
import AdminLeftMenu from "@/components/menus/admin/AdminLeftMenu.vue";
import { Colors } from "@/ThemeConfig.vue";
import { defineProps } from "vue";
import { defineSlots } from "vue";
import PageTitle from "@/components/global/PageTitle.vue";
import UserNavigationTop from "@/components/displays/user/UserNavigationTop.vue";
defineProps({
title: String,
subtitle: String
})
defineSlots()
</script>
<template>
<div class="columns is-multiline">
<!-- Header -->
<div
class="column is-12 is-vcentered"
:style="{ 'background-color': Colors.headers.backgroundColor, 'color': Colors.headers.textColor }"
style="margin-bottom: 0; padding-bottom: 0;"
>
<slot name="header"></slot>
<!-- If the header slot is empty, show the default header -->
<UserNavigationTop class="my-4 is-float-right" style="margin-right: 5rem;" :style="{ 'color': Colors.headers.textColor }" />
</div>
<!-- Left menu -->
<div class="column is-2"
:style="{ 'background-color': Colors.menus.parentBackgroundColor, 'color': Colors.menus.textColor }">
<AdminLeftMenu />
</div>
<!-- Page content -->
<div
class="column is-10"
:style="{ 'background-color': Colors.global.backgroundColor, 'color': Colors.global.textColor }"
>
<div class="pl-6 pt-6 mb-0" v-if="title || subtitle">
<div class="columns is-vcentered is-mobile">
<PageTitle :title="title" :subtitle="subtitle">
<slot name="buttons"></slot>
</PageTitle>
</div>
</div>
<div class="pl-6 pr-6 pb-6">
<slot></slot>
</div>
</div>
</div>
</template>
<style scoped>
</style>
@@ -6,14 +6,23 @@ import PosDepartmentMVP from "@/components/displays/department/pos/PosDepartment
import DepartmentPosNavigation from "@/views/dashboards/departmentDashboard/modules/Pos/DepartmentPosNavigation.vue";
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import DepartmentDashboardPageWrapper from "@/views/dashboards/departmentDashboard/DepartmentDashboardPageWrapper.vue";
import PageTitle from "@/components/global/PageTitle.vue";
</script>
<template>
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessAdmin()">
<DepartmentDashboardHero title="Kassesystem" subtitle="Point of Sale" />
<DepartmentPosNavigation />
<PosDepartmentMVP />
</RestrictedPageWrapper>
<DepartmentDashboardPageWrapper
title="Kassesystem"
subtitle="Point of Sale"
>
<template #default>
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessAdmin()">
<!-- <DepartmentDashboardHero /> -->
<!-- <DepartmentPosNavigation /> -->
<PosDepartmentMVP />
</RestrictedPageWrapper>
</template>
</DepartmentDashboardPageWrapper>
</template>
<style scoped>