Files
pleno-vue/src/components/displays/department/moduleNavigation/DepartmentModulesDisplay.vue
T
2026-06-12 11:32:44 +02:00

374 lines
11 KiB
Vue

<script setup>
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
import { useRouter } from 'vue-router';
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import {BSwitch} from "buefy";
import {useI18n} from 'vue-i18n';
import { useAppToast } from "@/composables/useAppToast.js";
import {
getDepartmentSelfServeEnabled,
setDepartmentSelfServeEnabled,
} from "@/composables/departmentSelfServeEnabled.js";
const router = useRouter();
const toast = useAppToast();
const { t } = useI18n();
const selfServeEnabled = ref(false);
const isLoadingSelfServeEnabled = ref(false);
const isSavingSelfServeEnabled = ref(false);
const bookingsCount = ref(0);
const selfServeLoadingMinimumMs = import.meta.env.MODE === "test" ? 0 : import.meta.env.VITE_IS_PLAYWRIGHT ? 5000 : 250;
const selfServeLoadingStartedAt = ref(0);
let selfServeLoadingTimer = null;
const clearSelfServeLoadingTimer = () => {
if (selfServeLoadingTimer !== null) {
clearTimeout(selfServeLoadingTimer);
selfServeLoadingTimer = null;
}
};
const setSelfServeLoading = (isLoading) => {
if (isLoading) {
clearSelfServeLoadingTimer();
selfServeLoadingStartedAt.value = Date.now();
isLoadingSelfServeEnabled.value = true;
return;
}
const elapsed = Date.now() - selfServeLoadingStartedAt.value;
const remaining = Math.max(0, selfServeLoadingMinimumMs - elapsed);
clearSelfServeLoadingTimer();
if (remaining === 0) {
isLoadingSelfServeEnabled.value = false;
return;
}
selfServeLoadingTimer = setTimeout(() => {
isLoadingSelfServeEnabled.value = false;
selfServeLoadingTimer = null;
}, remaining);
};
const getDepartmentId = () => {
return parseInt(router.currentRoute.value.params.departmentId);
};
const buildDepartmentModulePath = (modulePath = "") => {
const departmentId = getDepartmentId();
return `/admin/${departmentId}${modulePath}`;
};
const modules = computed(() => [
{
id: 1,
name: t('admin.department_modules.pos.name'),
description: t('admin.department_modules.pos.description'),
icon: "fas fa-book",
onClick: () => {
router.push(buildDepartmentModulePath('/modules/pos'));
},
onClickCount: () => {
router.push(buildDepartmentModulePath('/modules/pos'));
},
count: 0
},
{
id: 2,
name: t('admin.department_modules.bookings.name'),
description: t('admin.department_modules.bookings.description'),
icon: "fas fa-calendar-alt",
onClick: () => {
router.push(buildDepartmentModulePath('/modules/bookings'));
},
onClickCount: () => {
router.push(buildDepartmentModulePath('/modules/bookings?search=pending'));
},
count: bookingsCount.value
},
{
id: 3,
name: t('admin.department_modules.daily_report.name'),
description: t('admin.department_modules.daily_report.description'),
icon: "fas fa-book",
onClick: () => {
router.push(buildDepartmentModulePath('/modules/daily-report'));
},
onClickCount: () => {
router.push(buildDepartmentModulePath('/modules/daily-report'));
},
count: 0
},
{
id: 5,
name: t('admin.department_modules.goals.name'),
description: t('admin.department_modules.goals.description'),
icon: "fas fa-bullseye",
onClick: () => {
router.push(buildDepartmentModulePath('/modules/goals'));
},
onClickCount: () => {
router.push(buildDepartmentModulePath('/modules/goals'));
},
count: 0
},
{
id: 4,
name: t('admin.department_modules.self_wash.name'),
description: t('admin.department_modules.self_wash.description'),
icon: "fas fa-tint",
onClick: () => {
router.push(buildDepartmentModulePath('/modules/wash-lanes'));
},
onClickCount: () => {
router.push(buildDepartmentModulePath('/modules/wash-lanes'));
},
count: 0,
hasSwitch: true,
switchLoading: isLoadingSelfServeEnabled.value || isSavingSelfServeEnabled.value,
switchValue: selfServeEnabled,
switchDisabled: isLoadingSelfServeEnabled.value || isSavingSelfServeEnabled.value,
onToggle: async (newValue) => {
const departmentId = getDepartmentId();
if (!departmentId) return;
const currentStatus = selfServeEnabled.value;
selfServeEnabled.value = newValue;
isSavingSelfServeEnabled.value = true;
try {
selfServeEnabled.value = await setDepartmentSelfServeEnabled(departmentId, newValue);
toast.success(newValue ? t('admin.department_modules.self_wash.enabled') : t('admin.department_modules.self_wash.disabled'));
} catch (error) {
console.error("Failed to update self-serve status", error);
selfServeEnabled.value = currentStatus;
toast.error(t('admin.department_modules.self_wash.update_error'));
} finally {
isSavingSelfServeEnabled.value = false;
}
}
},
{
id: 6,
name: 'Døgnvask konfiguration',
description: 'Gå til konfiguration af Døgnvask',
icon: "fas fa-project-diagram",
onClick: () => {
router.push(buildDepartmentModulePath('/modules/self-serve/studio'));
},
onClickCount: () => {
router.push(buildDepartmentModulePath('/modules/self-serve/studio'));
},
count: 0
}
/**
* {
* id: 4,
* name: "Notifikationer",
* description: "Gå til notifikationer",
* icon: "fas fa-bell",
* onClick: () => {
* // Get the departmentId from the URL
* const departmentId = router.currentRoute.value.params.departmentId;
* // Redirect to the notifications
* router.push(`/admin/${departmentId}/modules/notifications`);
* },
* onClickCount: () => {
* this.onClick();
* },
* count: ref(0)
* },
*/
/**
* {
* id: 5,
* name: "Tidsbookinger",
* description: "Gå til tidsbookinger",
* icon: "fas fa-clock",
* onClick: () => {
* // Get the departmentId from the URL
* const departmentId = router.currentRoute.value.params.departmentId;
* // Redirect to the time bookings
* router.push(`/admin/${departmentId}/modules/time-bookings`);
* },
* onClickCount: () => {
* this.onClick();
* },
* count: ref(0)
* }
*/
]);
// Get today's bookings
const getTodaysBookings = async () => {
// Return, if the departmentId is not set
if (!getDepartmentId()) {
return;
}
const response = await authenticatedRequest('/admin/bookings/department/count?department_id=' + getDepartmentId(),
'GET', {
department_id: getDepartmentId()
});
bookingsCount.value = response.data.data.message;
};
// Get the bookings
getTodaysBookings();
// Get the self-serve status
const getSelfServeStatus = async () => {
const departmentId = getDepartmentId();
if (!departmentId) return;
setSelfServeLoading(true);
try {
selfServeEnabled.value = await getDepartmentSelfServeEnabled(departmentId);
} catch (error) {
console.error("Failed to fetch self-serve status", error);
} finally {
setSelfServeLoading(false);
}
};
onMounted(() => {
getSelfServeStatus();
});
onUnmounted(() => {
clearSelfServeLoadingTimer();
});
// Watch the departmentId
watch(() => router.currentRoute.value.params.departmentId, () => {
getTodaysBookings();
getSelfServeStatus();
});
// Update the bookings every 30 seconds
setInterval(() => {
getTodaysBookings();
}, 30000);
</script>
<template>
<div class="columns is-multiline department-modules" data-testid="admin-mobile-modules">
<div class="column is-12-mobile is-6-tablet is-4-desktop is-3-widescreen" v-for="module in modules" :key="module.id">
<div class="card is-clickable department-module-card" @click="module.onClick">
<header class="card-header">
<div class="card-header-icon">
<span class="icon">
<i :class="module.icon"></i>
</span>
</div>
<p class="card-header-title department-module-card__header-title">{{ module.name }}</p>
<div class="department-module-card__actions" data-testid="department-module-card-actions">
<span class="button is-small is-dark department-module-card__count" @click.stop="module.onClickCount" v-if="module.count > 0">{{ module.count }}</span>
<span
v-else-if="module?.switchLoading"
class="tag is-rounded is-small department-module-card__state-loading"
data-testid="department-module-self-serve-state-loading"
role="status"
:aria-label="t('global.loading')"
@click.stop
></span>
<span class="button is-small is-outlined department-module-card__switch" v-else-if="module?.hasSwitch" @click.stop>
<b-switch
size="is-small"
type="is-link"
:model-value="Boolean(module?.switchValue?.value ?? module?.switchValue)"
:disabled="module?.switchDisabled"
@update:model-value="module.onToggle"
/>
</span>
</div>
</header>
<div class="card-content">
<p class="title is-4 department-module-card__title">{{ module.name }}</p>
<p class="subtitle">{{ module.description }}</p>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.department-module-card {
height: 100%;
}
.department-module-card__header-title {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.department-module-card__actions {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: flex-end;
margin-left: auto;
padding-right: 0.75rem;
}
.department-module-card__count,
.department-module-card__switch,
.department-module-card__state-loading {
position: relative;
z-index: 1;
}
.department-module-card__state-loading {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.5rem;
min-height: 1.25rem;
padding: 0 0.5rem !important;
background-color: #e5e7eb !important;
border: 1px solid #d1d5db !important;
color: transparent !important;
background-image:
radial-gradient(circle, rgba(156, 163, 175, 0.95) 0.16rem, transparent 0.18rem),
radial-gradient(circle, rgba(156, 163, 175, 0.95) 0.16rem, transparent 0.18rem),
radial-gradient(circle, rgba(156, 163, 175, 0.95) 0.16rem, transparent 0.18rem);
background-repeat: no-repeat;
background-size: 0.4rem 0.4rem;
background-position:
calc(50% - 0.5rem) 50%,
50% 50%,
calc(50% + 0.5rem) 50%;
animation: department-module-state-loading-pulse 1.1s ease-in-out infinite;
}
@keyframes department-module-state-loading-pulse {
0%,
100% {
opacity: 0.68;
transform: scale(0.98);
}
50% {
opacity: 1;
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.department-module-card__state-loading {
animation: none;
}
}
@media (max-width: 768px) {
.department-module-card__title {
display: none;
}
}
</style>