374 lines
11 KiB
Vue
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>
|