Files
pleno-vue/src/views/dashboards/UserDashboard.vue
T
Jeppe Bundgaard bb8ae993eb Localize user dashboard and enhance translation support
- Replaced hardcoded dashboard text in `UserDashboard.vue` with dynamic translations using `$t`.
- Added new translation keys in `en.json` and `da.json` for user dashboard actions, descriptions, and messages.
- Improved multilingual support for subuser actions, empty state messages, and contact information.
2026-02-17 12:44:45 +01:00

310 lines
12 KiB
Vue

<script setup lang="ts">
import SessionUser from "@/components/session/token/SessionUser.vue";
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
import SubuserGrantSelector from "@/components/session/subuser/SubuserGrantSelector.vue";
import {ref, onMounted, nextTick, computed, onUnmounted} from "vue";
import { IS_DEV } from '@/config.js';
/**
* Subuser grant selection gate
* Subusers must select a grant before accessing the dashboard
*/
const isSubuser = computed(() => SessionUser.isSubuser.value);
const hasSelectedGrant = computed(() => {
if (!isSubuser.value) return true;
return SessionUser.subuser.selectedGrantCustomerNumber.value !== null;
});
/**
* Make sure the height of the cards are equal
*/
const cardMinHeight = ref(0); // Pixel height
const setEqualHeights = () => {
nextTick(() => {
if (SessionUser.functions.device.isMobile()) {
return;
}
const cardContents = document.querySelectorAll('.identical-height-spacer');
let maxHeight = 0;
cardContents.forEach((element) => {
const elementHeight = element.clientHeight;
if (elementHeight > maxHeight) {
maxHeight = elementHeight;
}
});
cardMinHeight.value = maxHeight;
});
};
const handleResize = () => {
setEqualHeights();
};
onMounted(() => {
setEqualHeights();
window.addEventListener('resize', handleResize);
});
const cleanup = () => {
window.removeEventListener('resize', handleResize);
};
onUnmounted(() => {
cleanup();
});
const isTablet = computed(() => {
return SessionUser.functions.device.isTablet();
});
/**
* Check if the subuser has permission to any of the dashboard shortcuts
*/
const hasAnyShortcutPermission = computed(() => {
// Regular users always have access
if (!isSubuser.value) return true;
// Check if subuser has any of the shortcut permissions
return (
SessionUser.hasPermission('user') ||
SessionUser.hasPermission('BOOKINGS_ADD') ||
SessionUser.hasPermission('VEHICLES_ADD') ||
SessionUser.hasPermission('ORDERS_LIST') ||
SessionUser.hasPermission('user_invoices')
);
});
const windowInnerWidth = ref(window.innerWidth);
const updateWindowInnerWidth = () => {
windowInnerWidth.value = window.innerWidth;
// Trigger any layout updates if necessary
classes.value; // Access to trigger recomputation
};
window.addEventListener('resize', updateWindowInnerWidth);
onUnmounted(() => {
window.removeEventListener('resize', updateWindowInnerWidth);
});
type ClassesType = {
column?: string[];
title?: string[];
button?: string[];
};
const classes = computed(() => {
if (computed(() => SessionUser.functions.device.isMobile()).value) {
return {
column: ['is-12-mobile', 'is-half-tablet', 'is-half-desktop'],
title: [],
button: [],
} as ClassesType;
} else if (SessionUser.functions.device.isTablet()) {
return {
column: ['is-half-tablet', 'is-12-mobile', 'is-half-desktop'],
title: ['is-size-7', 'has-text-weight-bold'],
button: ['is-size-7-tablet'],
} as ClassesType;
} else if ((SessionUser.functions.device.isDesktop() || SessionUser.functions.device.isWidescreen()) && !SessionUser.functions.device.isUltraWideScreen()) {
return {
column: ['is-half-desktop', 'is-half-tablet', 'is-12-mobile'],
title: [],
button: [],
} as ClassesType;
} else if (SessionUser.functions.device.isUltraWideScreen()) {
return {
column: ['is-one-third-desktop', 'is-half-tablet', 'is-12-mobile'],
title: [],
button: [],
} as ClassesType;
} else {
return {
// Default to desktop layout
column: ['is-half'],
title: [],
button: [],
} as ClassesType;
}
});
</script>
<template>
<div>
<!-- Subuser grant selection gate -->
<div v-if="isSubuser && !hasSelectedGrant" class="box">
<div class="has-text-centered mb-4">
<span class="icon is-large has-text-info">
<i class="fas fa-users fa-2x"></i>
</span>
<h2 class="title is-4 mt-3">{{ $t('user_home.select_customer') }}</h2>
<p class="subtitle is-6">{{ $t('user_home.select_customer_desc') }}</p>
</div>
<SubuserGrantSelector :showLabel="false" />
</div>
<!-- Empty state for subusers without permissions -->
<div v-else-if="isSubuser && !hasAnyShortcutPermission" class="box has-text-centered">
<span class="icon is-large has-text-grey-light">
<i class="fas fa-inbox fa-3x"></i>
</span>
<h2 class="title is-4 mt-4 has-text-grey">{{ $t('user_home.empty_state_title') }}</h2>
<p class="subtitle is-6 has-text-grey">{{ $t('user_home.empty_state_desc') }}</p>
</div>
<!-- Main dashboard content -->
<div v-else class="columns is-vcentered mb-4 is-multiline">
<div class="column is-12">
<span class="is-auto-size-text is-size-6 has-text-weight-bold">
{{ $t('user_home.welcome') }} {{ SessionUser.getName() }}
</span>
</div>
<div class="column is-half-desktop is-hidden-desktop is-12-tablet" v-show="IS_DEV">
<!-- Start wash card -->
<WhiteBoxCard :force-state="true" :defaultOpen="true" :toggleable="false" :loading="false">
<template #header>
<div class="card-header-title has-text-link has-text-weight-bold">
{{ $t('user_home.start_wash') }}
</div>
<div class="card-header-icon has-text-link">
<i class="fas fa-soap"></i>
</div>
</template>
<template #content>
<div class="identical-height-spacer" :style="{ minHeight: cardMinHeight + 'px' }">
{{ $t('user_home.start_wash_desc') }}
</div>
</template>
<template #footer>
<div class="card-footer-item">
<router-link to="/user/wash/start" class="button is-link is-fullwidth">
{{ $t('user_home.start_wash') }}
</router-link>
</div>
</template>
</WhiteBoxCard>
</div>
<div class="column" :class="classes.column" v-if="SessionUser.hasPermission('user') || SessionUser.hasPermission('BOOKINGS_ADD')">
<!-- Book wash card -->
<WhiteBoxCard :force-state="true" :defaultOpen="true" :toggleable="false" :loading="false">
<template #header>
<div class="card-header-title" :class="classes.title">
{{ $t('user_home.book_wash') }}
</div>
<div class="card-header-icon">
<i class="fas fa-calendar-check"></i>
</div>
</template>
<template #content>
<div class="identical-height-spacer" :style="{ minHeight: cardMinHeight + 'px' }">
{{ $t('user_home.book_wash_desc') }}
</div>
<!--<b-loading :is-full-page="false" :model-value="true" :can-cancel="true"/>-->
</template>
<template #footer>
<div class="card-footer-item">
<router-link to="/user/bookings/book" class="button is-link is-fullwidth" :class="classes.button">
{{ $t('user_home.book_wash') }}
</router-link>
</div>
</template>
</WhiteBoxCard>
</div>
<div class="column" :class="classes.column" v-if="SessionUser.hasPermission('user') || SessionUser.hasPermission('VEHICLES_ADD')">
<!-- Add vehicle card -->
<WhiteBoxCard :force-state="true" :defaultOpen="true" :toggleable="false" :loading="false">
<template #header>
<div class="card-header-title" :class="classes.title">
{{ $t('user_home.create_vehicle') }}
</div>
<div class="card-header-icon">
<i class="fas fa-truck"></i>
</div>
</template>
<template #content>
<div class="identical-height-spacer" :style="{ minHeight: cardMinHeight + 'px' }">
{{ $t('user_home.create_vehicle_desc') }}
</div>
<!--<b-loading :is-full-page="false" :model-value="true" :can-cancel="true"/>-->
</template>
<template #footer>
<div class="card-footer-item">
<router-link to="/user/vehicles" class="button is-link is-fullwidth" :class="classes.button">
{{ $t('user_home.create_vehicle') }}
</router-link>
</div>
</template>
</WhiteBoxCard>
</div>
<div class="column" :class="classes.column" v-if="SessionUser.hasPermission('user') || SessionUser.hasPermission('ORDERS_LIST')">
<!-- Download wash certificate card -->
<WhiteBoxCard :force-state="true" :defaultOpen="true" :toggleable="false" :loading="false">
<template #header>
<div class="card-header-title" :class="classes.title">
{{ $t('user_home.download_certificates') }}
</div>
<div class="card-header-icon">
<i class="fas fa-file-download"></i>
</div>
</template>
<template #content>
<div class="identical-height-spacer" :style="{ minHeight: cardMinHeight + 'px' }">
{{ $t('user_home.download_certificates_desc') }}
</div>
<!--<b-loading :is-full-page="false" :model-value="true" :can-cancel="true"/>-->
</template>
<template #footer>
<div class="card-footer-item">
<router-link to="/user/orders" class="button is-link is-fullwidth" :class="classes.button">
{{ $t('user_home.download_certificates') }}
</router-link>
</div>
</template>
</WhiteBoxCard>
</div>
<div class="column" :class="classes.column" v-if="SessionUser.hasPermission('user') || SessionUser.hasPermission('user_invoices')">
<!-- View invoices card -->
<WhiteBoxCard :force-state="true" :defaultOpen="true" :toggleable="false" :loading="false">
<template #header>
<div class="card-header-title" :class="classes.title">
{{ $t('user_home.download_invoices') }}
</div>
<div class="card-header-icon">
<i class="fas fa-file-invoice-dollar"></i>
</div>
</template>
<template #content>
<div class="identical-height-spacer" :style="{ minHeight: cardMinHeight + 'px' }">
{{ $t('user_home.download_invoices_desc') }}
</div>
<!--<b-loading :is-full-page="false" :model-value="true" :can-cancel="true"/>-->
</template>
<template #footer>
<div class="card-footer-item">
<router-link to="/user/invoices" class="button is-link is-fullwidth" :class="classes.button">
{{ $t('user_home.download_invoices') }}
</router-link>
</div>
</template>
</WhiteBoxCard>
</div>
<div class="column is-12">
<span class="is-size-6 has-text-weight-bold">
{{ $t('user_home.contact_info_prefix') }} <a href="tel:+4543717886"><span class="has-text-link">43 71 78 86</span></a> {{ $t('user_home.contact_info_or_email') }} <a href="mailto:cph@truckwash.dk"><span class="has-text-link">cph@truckwash.dk</span></a>.
</span>
</div>
</div>
<!--<b-loading
:is-full-page="true"
:model-value="true"
:can-cancel="true"
></b-loading>-->
<!--<Home />-->
</div>
</template>
<style scoped>
.is-auto-size-text {
display: inline-block;
white-space: nowrap;
max-width: 100%;
vertical-align: top;
padding-right: 0.5rem;
padding-left: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
}
</style>