Refactor InvoicingBillingPeriodStatistics.vue: add department totals calculation, enhance subscription and pricing views, improve font and color styling, and update conditional rendering logic.

This commit is contained in:
Jeppe Bundgaard
2026-03-11 12:52:18 +01:00
parent 1923b69f4a
commit f91089ebe0
2 changed files with 210 additions and 72 deletions
@@ -1,10 +1,10 @@
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import {computed, ref, watch, onMounted} from "vue";
import StatisticsIncomeCard
from "@/views/dashboards/superUserDashboard/statistics/displays/overview/StatisticsIncomeCard.vue";
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
import { dates } from "../../imports/InvoicingBillingPeriodImportDates.vue";
import { view } from "../../imports/InvoicingBillingPeriodImportView.vue";
import {dates} from "../../imports/InvoicingBillingPeriodImportDates.vue";
import {view} from "../../imports/InvoicingBillingPeriodImportView.vue";
import {departments} from "@/components/pagination/departmentTabs.vue";
import {SessionUser} from "@/components/session/token/SessionUser.vue";
@@ -47,7 +47,7 @@ const getPercentageOfTotal = (value: number, total: number) => {
watch(
() => view.variables.currentView.value,
(newView) => {
if (newView === 'fixed_pricing') {
if (['all', 'fixed_pricing'].includes(newView)) {
fetchFixedPricingDistribution();
}
},
@@ -57,7 +57,7 @@ watch(
watch(
() => [dates.variables.start.value, dates.variables.end.value],
([newStart, newEnd]) => {
if (view.variables.currentView.value === 'fixed_pricing') {
if (['all', 'fixed_pricing'].includes(view.variables.currentView.value)) {
clearFixedPricingDistribution();
fetchFixedPricingDistribution();
}
@@ -96,60 +96,57 @@ const clearVehicleSubscriptionDistribution = () => {
watch(
() => view.variables.currentView.value,
(newView) => {
if (newView === 'vehicle_subscriptions') {
if (['all', 'vehicle_subscriptions'].includes(newView)) {
fetchVehicleSubscriptionDistribution();
} else {
clearVehicleSubscriptionDistribution();
}
},
},
{ immediate: true }
);
watch(
() => [dates.variables.start.value, dates.variables.end.value],
([newStart, newEnd]) => {
if (view.variables.currentView.value === 'vehicle_subscriptions') {
if (['all', 'vehicle_subscriptions'].includes(view.variables.currentView.value)) {
clearVehicleSubscriptionDistribution();
fetchVehicleSubscriptionDistribution();
}
}
);
const department75_total = ref<number | null>(null);
const calculate_department75_total = () => {
if (fixed_pricing_department_distribution.value?.total_fixed_price !== undefined && vehicleSubscriptionDistribution.value?.total_subscription_price !== undefined) {
return fixed_pricing_department_distribution.value?.total_fixed_price + vehicleSubscriptionDistribution.value?.total_subscription_price;
}
// If either is undefined, return undefined
return null;
};
onMounted(() => {
if (['all', 'fixed_pricing'].includes(view.variables.currentView.value)) {
fetchFixedPricingDistribution();
}
if (['all', 'vehicle_subscriptions'].includes(view.variables.currentView.value)) {
fetchVehicleSubscriptionDistribution();
}
department75_total.value = calculate_department75_total();
});
watch(
() => [fixed_pricing_department_distribution.value, vehicleSubscriptionDistribution.value],
() => {
department75_total.value = calculate_department75_total();
if (department75_total.value === null) {
console.warn('Department 75 total calculation resulted in null');
}
}
);
</script>
<template>
<!-- Booked, not booked and total income -->
<div class="columns is-multiline is-mobile">
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
<!-- Income booked -->
<StatisticsIncomeCard
v-bind:force-display="{
value: view.computed.currentViewTotalNetAmountBooked.value,
start_date: dates.variables.start.value.toISOString().split('T')[0],
end_date: dates.variables.end.value.toISOString().split('T')[0],
error: null,
}"
v-bind:icon="'fas fa-money-bill-wave'"
v-bind:name="'Bogført'"
v-bind:color="'has-text-success'"
/>
</WhiteBoxCard>
</div>
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
<!-- Income not booked -->
<StatisticsIncomeCard
v-bind:force-display="{
value: view.computed.currentViewTotalNetAmountNotBooked.value,
start_date: dates.variables.start.value.toISOString().split('T')[0],
end_date: dates.variables.end.value.toISOString().split('T')[0],
error: null,
}"
v-bind:icon="'fas fa-money-bill-wave'"
v-bind:name="'Ikke bogført'"
/>
</WhiteBoxCard>
</div>
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
<!-- Income total -->
@@ -165,30 +162,114 @@ watch(
/>
</WhiteBoxCard>
</div>
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
<!-- Income booked -->
<StatisticsIncomeCard
v-bind:force-display="{
value: view.computed.currentViewTotalNetAmountBooked.value,
start_date: dates.variables.start.value.toISOString().split('T')[0],
end_date: dates.variables.end.value.toISOString().split('T')[0],
error: null,
}"
v-bind:icon="'fas fa-money-bill-wave'"
v-bind:name="'Bogført'"
v-bind:color="'has-text-success'"
v-bind:font-style="'oblique'"
/>
</WhiteBoxCard>
</div>
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
<!-- Income not booked -->
<StatisticsIncomeCard
v-bind:force-display="{
value: view.computed.currentViewTotalNetAmountNotBooked.value,
start_date: dates.variables.start.value.toISOString().split('T')[0],
end_date: dates.variables.end.value.toISOString().split('T')[0],
error: null,
}"
v-bind:icon="'fas fa-money-bill-wave'"
v-bind:name="'Ikke bogført'"
v-bind:color="(view.computed.currentViewTotalNetAmountNotBooked.value > 0) ? 'danger' : 'black'"
v-bind:font-style="'oblique'"
/>
</WhiteBoxCard>
</div>
<!-- If an entire month is selected, show overall distribution -->
<template v-if="dates.computed.isEntireMonth.value">
<!-- Divider -->
<div class="column is-full">
<div class="divider">
<div class="divider-text has-text-weight-bold">
Heraf afdeling 75
</div>
</div>
</div>
<!-- Department 75 -->
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
<StatisticsIncomeCard v-bind:force-display="{
value: department75_total || 0,
start_date: dates.variables.start.value.toISOString().split('T')[0],
end_date: dates.variables.end.value.toISOString().split('T')[0],
error: null,
}"
v-bind:loading="department75_total === null"
v-bind:icon="'fas fa-money-bill-wave'"
v-bind:name="'Afdeling 75'"
v-bind:color="'black'"
v-bind:font-style="'normal'"
/>
</WhiteBoxCard>
</div>
<!-- Wash subscriptions -->
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
<StatisticsIncomeCard v-bind:force-display="{
value: vehicleSubscriptionDistribution?.total_subscription_price || 0,
start_date: dates.variables.start.value.toISOString().split('T')[0],
end_date: dates.variables.end.value.toISOString().split('T')[0],
error: null,
}"
v-bind:loading="vehicleSubscriptionDistribution?.total_subscription_price === undefined"
v-bind:icon="'fas fa-money-bill-wave'"
v-bind:name="'Vaske-aftale'"
v-bind:color="'black'"
v-bind:font-style="'oblique'"
/>
</WhiteBoxCard>
</div>
<!-- Fixed pricing -->
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
<StatisticsIncomeCard v-bind:force-display="{
value: fixed_pricing_department_distribution?.total_fixed_price || 0,
start_date: dates.variables.start.value.toISOString().split('T')[0],
end_date: dates.variables.end.value.toISOString().split('T')[0],
error: null,
}"
v-bind:loading="fixed_pricing_department_distribution?.total_fixed_price === undefined"
v-bind:icon="'fas fa-money-bill-wave'"
v-bind:name="'Fast-pris'"
v-bind:color="'black'"
v-bind:font-style="'oblique'"
/>
</WhiteBoxCard>
</div>
</template>
<template v-if="view.functions.isCurrentView('fixed_pricing')">
<!-- Divider -->
<div class="column is-full">
<div class="divider">
<div class="divider-text has-text-weight-bold">
Fast-pris fordeling
Afdeling 75 - Fast-pris fordeling
</div>
</div>
</div>
<!-- Total original price -->
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
<StatisticsIncomeCard v-bind:force-display="{
value: fixed_pricing_department_distribution?.total_original_price || 0,
start_date: dates.variables.start.value.toISOString().split('T')[0],
end_date: dates.variables.end.value.toISOString().split('T')[0],
error: null,
}"
v-bind:loading="fixed_pricing_department_distribution?.total_original_price === undefined"
v-bind:icon="'fas fa-money-bill-wave'"
v-bind:name="'Før pris'"
/>
</WhiteBoxCard>
</div>
<!-- Total fixed price -->
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
@@ -202,7 +283,25 @@ watch(
v-bind:loading="fixed_pricing_department_distribution?.total_fixed_price === undefined"
v-bind:icon="'fas fa-money-bill-wave'"
v-bind:name="'Faktisk pris'"
v-bind:color="'has-text-info'"
v-bind:color="'black'"
/>
</WhiteBoxCard>
</div>
<!-- Total original price -->
<div class="column is-one-third">
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
<StatisticsIncomeCard v-bind:force-display="{
value: fixed_pricing_department_distribution?.total_original_price || 0,
start_date: dates.variables.start.value.toISOString().split('T')[0],
end_date: dates.variables.end.value.toISOString().split('T')[0],
error: null,
}"
v-bind:loading="fixed_pricing_department_distribution?.total_original_price === undefined"
v-bind:icon="'fas fa-money-bill-wave'"
v-bind:name="'Før pris'"
v-bind:color="'black'"
v-bind:font-style="'oblique'"
/>
</WhiteBoxCard>
</div>
@@ -219,7 +318,8 @@ watch(
v-bind:loading="fixed_pricing_department_distribution?.total_fixed_price === undefined || fixed_pricing_department_distribution?.total_original_price === undefined"
v-bind:icon="((fixed_pricing_department_distribution?.total_fixed_price || 0) - (fixed_pricing_department_distribution?.total_original_price || 0)) >= 0 ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"
v-bind:name="'Difference'"
v-bind:color="((fixed_pricing_department_distribution?.total_fixed_price || 0) - (fixed_pricing_department_distribution?.total_original_price || 0)) >= 0 ? 'has-text-success' : 'has-text-danger'"
v-bind:color="((fixed_pricing_department_distribution?.total_fixed_price || 0) - (fixed_pricing_department_distribution?.total_original_price || 0)) >= 0 ? 'black' : 'danger'"
v-bind:font-style="'oblique'"
/>
</WhiteBoxCard>
</div>
@@ -255,7 +355,7 @@ watch(
<!-- Total row -->
<tr v-if="fixed_pricing_department_distribution && fixed_pricing_department_distribution.total_original_price !== undefined && fixed_pricing_department_distribution.total_fixed_price !== undefined">
<td><strong>Total</strong></td>
<td><strong>{{ SessionUser.functions.currency.toLocal(fixed_pricing_department_distribution.total_original_price) }}</strong></td>
<td><strong class="has-text-strikethrough">{{ SessionUser.functions.currency.toLocal(fixed_pricing_department_distribution.total_original_price) }}</strong></td>
<td><strong>{{ SessionUser.functions.currency.toLocal(fixed_pricing_department_distribution.total_fixed_price) }}</strong></td>
<td><strong>{{ getPercentageOfTotal(fixed_pricing_department_distribution.total_fixed_price, fixed_pricing_department_distribution.total_original_price) }}%</strong></td>
</tr>
@@ -349,5 +449,7 @@ watch(
</template>
<style scoped>
.has-text-strikethrough {
text-decoration: line-through;
}
</style>
@@ -1,5 +1,5 @@
<script setup>
import { onMounted, watch} from 'vue'
import {computed, onMounted, watch} from 'vue'
import { ref } from 'vue'
import {authenticatedRequest} from "@/components/session/authenticatedRequest.vue";
const props = defineProps({
@@ -13,7 +13,13 @@ const props = defineProps({
},
color: {
type: String,
default: 'black'
default: 'black',
enum: ['black', 'info', 'success', 'warning', 'danger'],
},
fontStyle: {
type: String,
default: 'normal',
enum: ['normal', 'italic', 'oblique', 'strikethrough'],
},
endpoint: {
type: String,
@@ -97,8 +103,30 @@ watch(() => props.forceDisplay, (newValue) => {
}
}, { immediate: true })
const cardClass = ref(props.forceDisplay.card_class || 'card')
const fontStyleClass = computed(() => {
switch (props.fontStyle.toLowerCase()) {
case 'italic':
return 'is-italic'
case 'oblique':
return 'is-oblique'
case 'strikethrough':
return 'has-text-decoration-line-through'
default:
return ''
}
})
const cardClass = ref(props.forceDisplay.card_class || 'card')
// Color class nuances (title, subtitle, icon)
const colorClassNuances = computed(() => {
return {
title: [`has-text-${props.color}`, 'is-size-5', 'has-text-weight-bold'],
subtitle: [`has-text-${props.color}-light`, 'is-size-6', 'has-text-weight-semibold', fontStyleClass.value],
icon: [`has-text-${props.color}`, 'is-size-4'],
footer: [`has-text-${props.color}`, 'is-size-7', 'has-text-weight-semibold'],
footerIcon: [`has-text-${props.color}`, 'is-size-7'],
}
})
</script>
<template>
@@ -107,16 +135,16 @@ const cardClass = ref(props.forceDisplay.card_class || 'card')
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<i :class="icon" :style="{ color: props.color }" v-if="value !== null && !props.loading"></i>
<i :class="[icon, colorClassNuances.icon]" v-if="value !== null && !props.loading"></i>
<i class="fas fa-exclamation-triangle has-text-warning" v-else-if="error"></i>
<i class="fas fa-spinner fa-spin" v-else></i>
</figure>
</div>
<div class="media-content">
<p class="title is-5">{{ name }}</p>
<p class="subtitle is-6" v-if="value !== null && !props.loading">{{ renderCurrency(value) }}</p>
<p class="subtitle is-6" v-else-if="error">{{ error }}</p>
<p class="subtitle is-6" v-else>{{ $t('common.loading') }}</p>
<p class="title is-5" :class="colorClassNuances.title">{{ name }}</p>
<p class="subtitle is-6" :class="colorClassNuances.subtitle" v-if="value !== null && !props.loading">{{ renderCurrency(value) }}</p>
<p class="subtitle is-6" :class="colorClassNuances.subtitle" v-else-if="error">{{ error }}</p>
<p class="subtitle is-6" :class="colorClassNuances.subtitle" v-else>{{ $t('common.loading') }}</p>
</div>
</div>
</div>
@@ -124,20 +152,20 @@ const cardClass = ref(props.forceDisplay.card_class || 'card')
<p class="card-footer-item">
<span>
<span class="icon" v-if="start_date && end_date">
<i class="fas fa-calendar-day"></i>
<i class="fas fa-calendar-day" :class="colorClassNuances.footerIcon"></i>
</span>
<span v-if="start_date && end_date" class="is-size-7 has-text-weight-bold">
<span v-if="start_date && end_date" :class="colorClassNuances.footer">
{{ start_date }} - {{ end_date }} ({{ getDaysBetweenDates(start_date, end_date) === 0 ? '1 ' + $t('statistics.day') : getDaysBetweenDates(start_date, end_date) + ' ' + $t('statistics.days') }})
</span>
<span v-else-if="error">
<span class="icon">
<i class="fas fa-exclamation-triangle has-text-warning"></i>
<i class="fas fa-exclamation-triangle has-text-warning" :class="colorClassNuances.footerIcon"></i>
</span>
<span>{{ $t('common.error') }}</span>
</span>
<span v-else>
<span class="icon">
<i class="fas fa-spinner fa-spin"></i>
<i class="fas fa-spinner fa-spin" :class="colorClassNuances.footerIcon"></i>
</span>
<span>{{ $t('common.loading') }}</span>
</span>
@@ -148,5 +176,13 @@ const cardClass = ref(props.forceDisplay.card_class || 'card')
</template>
<style scoped>
.has-text-decoration-line-through {
text-decoration: line-through;
}
.is-oblique {
font-style: oblique;
}
.is-italic {
font-style: italic;
}
</style>