Refactor department dashboard: replace date shortcuts with DatePeriodSelector, improve loading state handling, and enhance transaction fetch logic

This commit is contained in:
Jeppe Bundgaard
2025-12-17 12:03:26 +01:00
parent b860417cc4
commit 3370bf7469
5 changed files with 218 additions and 131 deletions
@@ -6,24 +6,16 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { ref } from "vue";
import DepartmentDailyReportSmall
from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportSmall.vue";
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
import DepartmentDailyReportThisWeek
from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportThisWeek.vue";
import DepartmentDashboardOverviewNavigation
from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDashboardOverviewNavigation.vue";
import { show_this_week } from "@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue";
import RequiresPermission from "@/components/displays/permissionbased/RequiresPermission.vue";
import DepartmentDailyBookingReportSmall
from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDailyBookingReportSmall.vue";
import DepartmentDashboardReports
from "@/views/dashboards/departmentDashboard/modules/overview/DepartmentDashboardReports.vue";
import DepartmentDailyReport
from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReport.vue";
import {BTabItem, BTabs} from "buefy";
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
const departments = ref(null);
// Get the departments
const accessibleDepartments = SessionUser.functions.getAccessibleDepartments();
/**
@@ -49,33 +41,37 @@ const listDisplay = ref<listDisplayType>(listDisplayTabIndexes.cards);
icon="th-large"
:visible="true"
>
<DepartmentDailyReport :departmentSpecific="false"/>
<template v-if="true">
<DepartmentDailyReport :departmentSpecific="false"/>
</template>
</b-tab-item>
<b-tab-item label="Liste"
icon-pack="fas"
icon="list"
>
<DepartmentDailyReportThisWeek :departments="accessibleDepartments"/>
<div class="columns is-multiline">
<div class="column is-12 pb-0" v-for="department_id in accessibleDepartments" :key="department_id">
<WhiteBoxCard :has-hover-effect="true" class="is-clickable mb-4" :has-selected-style="false" :has-selection-style="true" @click="$router.push('/admin/' + department_id)">
<template #header>
<div class="card-header-title" style="width: 100%;">
<div class="columns is-vcentered is-mobile" style="width: 100%;">
<div class="column">
<RequiresPermission permission="list_department_daily_reports">
<DepartmentDailyReportSmall :department_id="department_id" />
</RequiresPermission>
<RequiresPermission permission="list_bookings" v-show="!SessionUser.hasPermission('list_department_daily_reports')">
<DepartmentDailyBookingReportSmall v-bind:department_id="department_id" :options="{show_department_name: (!SessionUser.hasPermission('list_department_daily_reports'))}" />
</RequiresPermission>
<template v-if="true">
<DepartmentDailyReportThisWeek :departments="accessibleDepartments"/>
<div class="columns is-multiline">
<div class="column is-12 pb-0" v-for="department_id in accessibleDepartments" :key="department_id">
<WhiteBoxCard :has-hover-effect="true" class="is-clickable mb-4" :has-selected-style="false" :has-selection-style="true" @click="$router.push('/admin/' + department_id)">
<template #header>
<div class="card-header-title" style="width: 100%;">
<div class="columns is-vcentered is-mobile" style="width: 100%;">
<div class="column">
<RequiresPermission permission="list_department_daily_reports">
<DepartmentDailyReportSmall :department_id="department_id" />
</RequiresPermission>
<RequiresPermission permission="list_bookings" v-show="!SessionUser.hasPermission('list_department_daily_reports')">
<DepartmentDailyBookingReportSmall v-bind:department_id="department_id" :options="{show_department_name: (!SessionUser.hasPermission('list_department_daily_reports'))}" />
</RequiresPermission>
</div>
</div>
</div>
</div>
</template>
</WhiteBoxCard>
</template>
</WhiteBoxCard>
</div>
</div>
</div>
</template>
</b-tab-item>
</b-tabs>
</DepartmentDashboardPageWrapper>
@@ -1,7 +1,8 @@
<script setup>
import {ref, defineProps, watch} from 'vue';
import { SessionUser } from '@/components/session/token/SessionUser.vue';
import {show_this_week} from "@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue";
import {selected_date, selected_date_to, is_loading, is_latest_fetch, next_fetch_id, finished_loading} from "@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue";
import {BLoading} from "buefy";
const props = defineProps({
department_id: {
@@ -30,22 +31,13 @@ const isCollapsed = ref(true);
const earnings = ref(0);
const sales = ref(0);
/** Get the dates in the current week */
const getDatesThisWeek = () => {
/** Get the dates in the current selecton */
const getDatesInSelection = () => {
const dates = [];
const today = new Date();
// If only today, return today
if (!show_this_week.value) {
dates.push(today.toISOString().split('T')[0]);
return dates;
}
const day = today.getDay();
const diff = today.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
const monday = new Date(today.setDate(diff));
for (let i = 0; i < 7; i++) {
const date = new Date(monday);
date.setDate(monday.getDate() + i);
dates.push(date.toISOString().split('T')[0]);
const startDate = new Date(selected_date.value);
const endDate = new Date(selected_date_to.value);
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
dates.push(new Date(d).toISOString().split('T')[0]);
}
return dates;
};
@@ -57,26 +49,39 @@ const getDepartment = async () => {
department.value = response;
});
};
const identifier = props.department_id ? props.department_id + '_DepartmentDailyReportSmall' : 'DepartmentDailyReportSmall';
// Get the transactions for the current day
const getTransactions = async () => {
// If the department ID is not set, return
if (!props.department_id) {
return;
// Start a new fetch
const current_fetch_id = next_fetch_id(identifier);
let tmp = {
sales: 0,
earnings: 0
}
// Reset the values
sales.value = 0;
earnings.value = 0;
// Update the dates
const dates = getDatesThisWeek();
const dates = getDatesInSelection();
// Get the transactions
for (const date of dates) {
if (!is_latest_fetch(current_fetch_id, identifier)) {
console.warn('Aborting fetching of department transactions, a newer fetch is in progress. id:', current_fetch_id, 'identifier:', identifier);
break;
}
await SessionUser.objects.department_daily_reports.functions.getTransactionCount(props.department_id, date)
.then(response => {
sales.value += response.data.data.quantity;
earnings.value += response.data.data.earnings ?? 0;
if (is_latest_fetch(current_fetch_id, identifier)) {
tmp.sales += response.data.data.quantity;
tmp.earnings += response.data.data.earnings ?? 0;
}
});
}
// Finished loading
if (is_latest_fetch(current_fetch_id, identifier)) {
sales.value = tmp.sales;
earnings.value = tmp.earnings;
console.log('Fetched transactions for department', props.department_id, 'sales:', tmp.sales, 'earnings:', tmp.earnings);
finished_loading(current_fetch_id, identifier);
}
};
// Get the department
@@ -84,8 +89,8 @@ getDepartment().then(
() => getTransactions()
)
// Watch the show_this_week prop
watch(() => show_this_week.value, () => {
// Watch the selected_date and selected_date_to for changes
watch([selected_date, selected_date_to], () => {
getTransactions();
});
@@ -96,21 +101,25 @@ watch(() => show_this_week.value, () => {
<!-- Name -->
<div class="column is-6">
<h3 class="title is-5">{{ department.name }}</h3>
<b-loading :is-full-page="false" :modelValue="is_loading(identifier)" />
</div>
<!-- Sales -->
<div class="column is-2 has-text-centered">
<h3 class="title is-5">{{ sales }}</h3>
<h6 class="subtitle is-6">Enheder</h6>
<b-loading :is-full-page="false" :modelValue="is_loading(identifier)" />
</div>
<!-- Income per sale -->
<div class="column is-2 has-text-centered">
<h3 class="title is-5">{{ earnings > 0 ? economicParser.format(earnings / sales) : economicParser.format(0) }}</h3>
<h6 class="subtitle is-6">Gns. pr. enhed</h6>
<b-loading :is-full-page="false" :modelValue="is_loading(identifier)" />
</div>
<!-- Total income -->
<div class="column is-2 has-text-centered">
<h3 class="title is-5">{{ economicParser.format(earnings) }}</h3>
<h6 class="subtitle is-6">Omsætning</h6>
<b-loading :is-full-page="false" :modelValue="is_loading(identifier)" />
</div>
</div>
</template>
@@ -1,9 +1,10 @@
<script setup>
import { ref, defineProps, watch } from 'vue';
import {ref, defineProps, watch, onMounted} from 'vue';
import { SessionUser } from '@/components/session/token/SessionUser.vue';
import { show_this_week } from "@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue";
import { is_latest_fetch, next_fetch_id, is_loading, finished_loading, selected_date_to, selected_date } from "@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue";
import DepartmentDashboardOverviewNavigation
from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDashboardOverviewNavigation.vue";
import {BSkeleton} from "buefy";
const props = defineProps({
departments: {
@@ -18,59 +19,77 @@ const income = ref(0);
const sales = ref(0);
const transactions = ref(0);
/** Get the dates in the current week */
const getDatesThisWeek = () => {
/** Get the dates in the current selection */
const getDatesInSelection = () => {
const dates = [];
const today = new Date();
// If only today, return today
if (show_this_week.value === false) {
dates.push(today.toISOString().split('T')[0]);
return dates;
}
const day = today.getDay();
const diff = today.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
const monday = new Date(today.setDate(diff));
for (let i = 0; i < 7; i++) {
const date = new Date(monday);
date.setDate(monday.getDate() + i);
dates.push(date.toISOString().split('T')[0]);
const startDate = new Date(selected_date.value);
const endDate = new Date(selected_date_to.value);
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
dates.push(new Date(d).toISOString().split('T')[0]);
}
return dates;
};
/** Fetch the transactions for the current week */
const getTransactionsThisWeek = async () => {
const identifier = 'DepartmentDailyReportThisWeek';
/** Fetch the transactions for the selected date range */
const getTransactionsInSelection = async () => {
// If the department ID is not set, return
if (!props.departments) {
return;
}
// Start a new fetch
const fetch_id = next_fetch_id(identifier);
console.warn('Fetching transactions for departments, fetch id:', fetch_id);
// Reset the values
income.value = 0;
sales.value = 0;
transactions.value = 0;
const dates = getDatesThisWeek();
// Fetch the data
let tmp = {
income: 0,
sales: 0,
transactions: 0
};
const dates = getDatesInSelection();
for (const department of props.departments) {
for (const date of dates) {
console.log(department, date);
await SessionUser.objects.department_daily_reports.functions.getTransactionCount(department, date)
.then(response => {
sales.value += response.data.data.products ?? 0;
income.value += response.data.data.earnings ?? 0;
transactions.value += response.data.data.quantity ?? 0;
if (is_latest_fetch(fetch_id, identifier)) {
tmp.income += response.data.data.earnings ?? 0;
tmp.sales += response.data.data.products ?? 0;
tmp.transactions += response.data.data.quantity ?? 0;
} else {
console.warn('Ignoring outdated fetch response for department', department, 'date', date, 'fetch id:', fetch_id);
}
});
}
}
// Set the values
if (is_latest_fetch(fetch_id, identifier)) {
console.log('Fetched transactions for departments', props.departments, 'income:', tmp.income, 'sales:', tmp.sales, 'transactions:', tmp.transactions);
income.value = tmp.income;
sales.value = tmp.sales;
transactions.value = tmp.transactions;
} else {
console.warn('Aborting setting of department transactions, a newer fetch is in progress. id:', fetch_id);
return;
}
// Finished loading
finished_loading(fetch_id, identifier);
console.log(sales.value, income.value, transactions.value);
};
getTransactionsThisWeek();
// Watch the only_today prop
watch(() => show_this_week.value, () => {
getTransactionsThisWeek();
// Watch the selected date range for changes
watch([() => selected_date.value, () => selected_date_to.value], () => {
getTransactionsInSelection();
});
console.log((getDatesThisWeek()));
onMounted(() => {
getTransactionsInSelection();
});
</script>
<template>
@@ -79,25 +98,52 @@ console.log((getDatesThisWeek()));
<div class="column is-12">
<DepartmentDashboardOverviewNavigation/>
</div>
<!-- Income -->
<div class="column is-4">
<div class="box">
<p class="subtitle is-6">Omsætning {{ !show_this_week ? 'i dag' : 'denne uge' }}</p>
<p class="title is-4">{{ SessionUser.functions.currency.toLocal(income) }}</p>
</div>
</div>
<!-- Gns. pr. enhed -->
<div class="column is-4">
<div class="box">
<p class="subtitle is-6">Gns. pr. enhed {{ !show_this_week ? 'i dag' : 'denne uge' }}</p>
<p class="title is-4">{{ SessionUser.functions.currency.toLocal((income / transactions) || 0) }}</p>
</div>
</div>
<!-- Transactions -->
<div class="column is-4">
<div class="box">
<p class="subtitle is-6">Enheder {{ !show_this_week ? 'i dag' : 'denne uge' }}</p>
<p class="title is-4">{{ transactions }}</p>
</div>
<div class="columns is-multiline is-vcentered">
<div class="column is-12">
<div class="columns is-multiline is-vcentered">
<!-- Income -->
<div class="column is-4">
<div class="box">
<p class="subtitle is-6">Omsætning {{ SessionUser.functions.date.isToday(selected_date) && SessionUser.functions.date.isToday(selected_date_to) ? 'i dag' : '' }}</p>
<p class="title is-4">
<template v-if="is_loading(identifier)">
<b-skeleton :width="'100%'" :height="'1em'"/>
</template>
<template v-else>
{{ SessionUser.functions.currency.toLocal(income) }}
</template>
</p>
</div>
</div>
<!-- Gns. pr. enhed -->
<div class="column is-4">
<div class="box">
<p class="subtitle is-6">Gns. pr. enhed {{ SessionUser.functions.date.isToday(selected_date) && SessionUser.functions.date.isToday(selected_date_to) ? 'i dag' : '' }}</p>
<p class="title is-4">
<template v-if="is_loading(identifier)">
<b-skeleton :width="'100%'" :height="'1em'"/>
</template>
<template v-else>
{{ SessionUser.functions.currency.toLocal((income / transactions) || 0) }}
</template>
</p>
</div>
</div>
<!-- Transactions -->
<div class="column is-4">
<div class="box">
<p class="subtitle is-6">Enheder {{ SessionUser.functions.date.isToday(selected_date) && SessionUser.functions.date.isToday(selected_date_to) ? 'i dag' : '' }}</p>
<p class="title is-4">
<template v-if="is_loading(identifier)">
<b-skeleton :width="'100%'" :height="'1em'"/>
</template>
<template v-else>
{{ transactions }}
</template>
</p>
</div>
</div>
</div>
</div>
</div>
@@ -1,38 +1,32 @@
<script setup>
import { defineProps, ref } from 'vue';
import { show_this_week } from "@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue";
import { defineEmits } from 'vue';
import { selected_date, selected_date_to } from "@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue";
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
// Emits for parents that want to react to date changes (non-breaking if unused)
const emit = defineEmits(['date-change']);
const selected_option = ref(0);
const options = [
{ id: 0, label: 'I dag' },
{ id: 1, label: 'Denne uge' },
];
const selectOption = (option) => {
selected_option.value = option;
show_this_week.value = option === 1;
// Local selection state (ISO yyyy-mm-dd)
const todayIso = new Date().toISOString().split('T')[0];
const selectDate = (startIso, endIso) => {
console.warn('DepartmentDashboardOverviewNavigation selectDate', startIso, endIso);
selected_date.value = startIso;
selected_date_to.value = endIso;
emit('date-change', { startDate: startIso, endDate: endIso });
};
</script>
<template>
<div class="navbar is-white">
<div class="navbar-menu">
<!-- Date shortcuts -->
<div class="navbar-start">
<div class="navbar-item">
<div class="buttons">
<button v-for="(shortcut, index) in options" :key="index" @click="selectOption(shortcut.id)"
class="button is-light" :class="{ 'is-info': selected_option === shortcut.id }">
{{ shortcut.label }}
</button>
</div>
</div>
</div>
</div>
</div>
<DatePeriodSelector
:on-selection-change="(startDate, endDate) => selectDate(startDate.toISOString().split('T')[0], endDate.toISOString().split('T')[0])"
:selection="{ startDate: new Date(selected_date), endDate: new Date(selected_date_to) }"
:visibility="{ showDailySelector: true, showWeeklySelector: true, showMultipleMonthWarning: false, showUpdateButton: false }"
:reverse-level-order="true"
@update:selection="(newSelection) => {
selectDate(newSelection.startDate.toISOString().split('T')[0], newSelection.endDate.toISOString().split('T')[0]);
}"
/>
</template>
<style scoped>
@@ -1,7 +1,49 @@
<script>
<script lang="ts">
import { ref, watch } from 'vue';
import { SessionUser } from '@/components/session/token/SessionUser.vue';
type fetch = {
id: number,
identifier: string | null,
loading: boolean,
date_from: string,
date_to: string
};
/** Define the variables */
export const show_this_week = ref(false);
export const selected_date = ref(new Date().toISOString().split('T')[0]);
export const selected_date_to = ref(new Date().toISOString().split('T')[0]);
export const last_fetch_id = ref(0);
export const fetches = ref<fetch[]>([]);
/** Define the functions */
export const next_fetch_id = (identifier: string | null = null): number => {
last_fetch_id.value += 1;
fetches.value.push({
id: last_fetch_id.value,
identifier: identifier,
loading: true,
date_from: selected_date.value,
date_to: selected_date_to.value
});
return last_fetch_id.value;
};
export const is_latest_fetch = (id: number, identifier: string | null = null): boolean => {
const relevantFetches = fetches.value.filter(f => f.identifier === identifier);
if (relevantFetches.length === 0) return false;
const latestFetch = relevantFetches.reduce((prev, current) => (prev.id > current.id) ? prev : current);
return latestFetch.id === id;
};
export const is_loading = (identifier: string | null = null): boolean => {
const relevantFetches = fetches.value.filter(f => f.identifier === identifier && f.loading);
return relevantFetches.length > 0;
};
export const finished_loading = (id: number, identifier: string | null = null): void => {
const fetchIndex = fetches.value.findIndex(f => f.id === id && f.identifier === identifier);
if (fetchIndex !== -1) {
fetches.value[fetchIndex].loading = false;
}
};
// Watch for changes in selected_date and selected_date_to to reset fetches
watch([selected_date, selected_date_to], () => {
fetches.value = [];
});
</script>