Add fixed pricing and subscription distribution views with API integrations and conditional rendering adjustments

This commit is contained in:
Jeppe Bundgaard
2026-03-10 10:21:41 +01:00
parent 1820532840
commit f3ecf7261a
6 changed files with 318 additions and 15 deletions
+2 -1
View File
@@ -124,7 +124,8 @@ const ratelimit_data = ref({
warning: null,
});
SessionUser.auth.reCAPTCHA.preCheck.get().then((response) => {
onMounted(async () => {
const response = await SessionUser.auth.reCAPTCHA.preCheck.get();
console.log(response.data.data);
reCAPTCHA_data.value = response.data.data.recaptcha;
ratelimit_data.value = response.data.data.rate_limit;
@@ -1,5 +1,5 @@
<script setup>
import {BButton, BIcon, BLoading, BSkeleton} from "buefy";
import {BButton, BIcon, BLoading} from "buefy";
let props = defineProps(['loadFunction', 'icon', 'isLoading', 'disabled'])
import { ref, computed } from 'vue'
@@ -25,13 +25,18 @@ const loadWhileAwait = async (loadFunction) => {
}
}
const handleClick = () => loadWhileAwait(props.loadFunction)
</script>
<template>
<b-button :loading="false" :icon-left="icon" @click="(isLoading ? null : loadWhileAwait(loadFunction))" :disabled="isLoading || disabled" :iconPack='"fas"' class="is-relative">
<slot v-if="!isLoading"></slot>
<b-loading v-if="isLoading" :is-full-page="false" :modelValue="true" :canCancel="false" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255, 255, 255, 0.7); display: flex; align-items: center; justify-content: center;">
<b-icon pack="fas" icon="circle-notch" size="is-large" type="is-primary" custom-class="fa-spin"></b-icon>
</b-loading>
<b-button
:loading="isLoading"
:icon-left="icon"
:icon-pack="'fas'"
@click="handleClick"
:disabled="isLoading || (props.disabled ?? false)"
>
<slot />
</b-button>
</template>
@@ -16,13 +16,13 @@ const isDesktop = useMediaQuery('(min-width: 1025px)', getMatch('(min-width: 102
</script>
<template>
<template v-if="isMobile">
<template v-if="isMobile && $slots.mobile">
<slot name="mobile" v-if="$slots.mobile" />
</template>
<template v-else-if="isTablet">
<template v-else-if="isTablet && $slots.tablet">
<slot name="tablet" v-if="$slots.tablet" />
</template>
<template v-else-if="isDesktop">
<template v-else-if="isDesktop && $slots.desktop">
<slot name="desktop" v-if="$slots.desktop" />
</template>
<template v-else>
@@ -1,10 +1,119 @@
<script setup lang="ts">
import { ref, computed, watch } 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 {departments} from "@/components/pagination/departmentTabs.vue";
import {SessionUser} from "@/components/session/token/SessionUser.vue";
/**
* Fixed pricing distribution data structure:
*/
const fixed_pricing_department_distribution = ref<any[]>([]);
const fetchFixedPricingDistribution = async () => {
SessionUser.request(
'/superuser/invoicing/period/distribution/fixed-pricing',
'GET',
{
dateFrom: dates.variables.start.value.toISOString().split('T')[0],
dateTo: dates.variables.end.value.toISOString().split('T')[0],
}
).then((response: any) => {
console.warn('Response from fixed pricing distribution API:', response);
// Assuming the response data structure is { data: { distribution: [...] } }
if (response.data && response.data.includes.collective_fixed_pricing_results) {
console.log('Fixed pricing distribution fetched successfully');
fixed_pricing_department_distribution.value = response.data.includes.collective_fixed_pricing_results;
/**
* Fixed pricing distribution data structure:
* { "total_fixed_price": 137962, "total_original_price": 72799, "total_department_totals": { "1": 11826, "2": 22687, "3": 17009, "4": 12457, "5": 1161, "6": 6611, "7": 1048 }, "total_department_totals_relative": { "1": 30962.876996916584, "2": 28790.813863868243, "3": 32997.60337384933, "4": 15442.430865260254, "5": 1407.808993176734, "6": 9521.78041528052, "7": 1010.6854916483431 }, "total_department_totals_parsed": { "Glostrup": 17009, "Taastrup": 22687, "Hvidovre": 11826, "Roskilde": 6611, "Køge": 12457, "Aarhus C": 1161, "Taulov": 1048 }, "total_department_totals_relative_parsed": { "Glostrup": 32997.60337384933, "Taastrup": 28790.813863868243, "Hvidovre": 30962.876996916584, "Roskilde": 9521.78041528052, "Køge": 15442.430865260254, "Aarhus C": 1407.808993176734, "Taulov": 1010.6854916483431 } }
*/
} else {
console.error('Unexpected response structure:', response);
}
}).catch((error) => {
console.error('Error fetching fixed pricing distribution:', error);
});
}
const clearFixedPricingDistribution = () => {
fixed_pricing_department_distribution.value = [];
}
const getPercentageOfTotal = (value: number, total: number) => {
return Math.round((value / total) * 100);
}
watch(
() => view.variables.currentView.value,
(newView) => {
if (newView === 'fixed_pricing') {
fetchFixedPricingDistribution();
}
},
{ immediate: true }
);
watch(
() => [dates.variables.start.value, dates.variables.end.value],
([newStart, newEnd]) => {
if (view.variables.currentView.value === 'fixed_pricing') {
clearFixedPricingDistribution();
fetchFixedPricingDistribution();
}
}
);
/**
* Subscription display logic:
*/
const vehicleSubscriptionDistribution = ref<any>(null);
const fetchVehicleSubscriptionDistribution = async () => {
SessionUser.request(
'/superuser/invoicing/period/distribution/wash-subscriptions',
'GET',
{
dateFrom: dates.variables.start.value.toISOString().split('T')[0],
dateTo: dates.variables.end.value.toISOString().split('T')[0],
}
).then((response: any) => {
/**
* { "total_subscription_price": 166335, "subscription_price_department_distribution": { "1": 17918.36178266178, "2": 51132.05248698669, "3": 69845.80538847117, "4": 9739.713278388279, "6": 16872.567063492064, "7": 826.5 }, "subscription_price_department_distribution_parsed": { "Roskilde": 16872.567063492064, "Glostrup": 69845.80538847117, "Taastrup": 51132.05248698669, "Hvidovre": 17918.36178266178, "Køge": 9739.713278388279, "Taulov": 826.5 } }
*/
console.warn('Response from vehicle subscription distribution API:', response);
if (response.data && response.data.includes.collective_subscription_results) {
console.log('Vehicle subscription distribution fetched successfully');
vehicleSubscriptionDistribution.value = response.data.includes.collective_subscription_results;
} else {
console.error('Unexpected response structure:', response);
}
}).catch((error) => {
console.error('Error fetching vehicle subscription distribution:', error);
});
}
const clearVehicleSubscriptionDistribution = () => {
vehicleSubscriptionDistribution.value = null;
}
watch(
() => view.variables.currentView.value,
(newView) => {
if (newView === 'vehicle_subscriptions') {
fetchVehicleSubscriptionDistribution();
} else {
clearVehicleSubscriptionDistribution();
}
},
{ immediate: true }
);
watch(
() => [dates.variables.start.value, dates.variables.end.value],
([newStart, newEnd]) => {
if (view.variables.currentView.value === 'vehicle_subscriptions') {
clearVehicleSubscriptionDistribution();
fetchVehicleSubscriptionDistribution();
}
}
);
</script>
<template>
@@ -56,6 +165,186 @@ import { view } from "../../imports/InvoicingBillingPeriodImportView.vue";
/>
</WhiteBoxCard>
</div>
<template v-if="view.functions.isCurrentView('fixed_pricing')">
<!-- Divider -->
<div class="column is-full">
<div class="divider">
<div class="divider-text">
Fast-pris fordeling
</div>
</div>
</div>
<!-- Total original price -->
<div class="column is-one-third">
<WhiteBoxCard>
<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>
<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="'Faktisk pris'"
v-bind:color="'has-text-info'"
/>
</WhiteBoxCard>
</div>
<!-- Total loss/gain between original and fixed price -->
<div class="column is-one-third">
<WhiteBoxCard>
<StatisticsIncomeCard
v-bind:force-display="{
value: (fixed_pricing_department_distribution?.total_fixed_price || 0) - (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_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'"
/>
</WhiteBoxCard>
</div>
<!-- Fixed pricing distribution -->
<div class="column is-full">
<WhiteBoxCard>
<!-- Distribution for each department -->
<table class="table is-fullwidth">
<thead>
<tr>
<th>Department</th>
<th>Original pris</th>
<th>Faktisk pris</th>
<th>Procent betalt ud fra original pris</th>
</tr>
</thead>
<tbody>
<template v-for="(department) in departments" :key="index" v-if="fixed_pricing_department_distribution && (fixed_pricing_department_distribution.total_department_totals_parsed || fixed_pricing_department_distribution.total_department_totals_relative_parsed)">
<tr v-if="fixed_pricing_department_distribution?.total_department_totals_parsed[department.name] || fixed_pricing_department_distribution?.total_department_totals_relative_parsed[department.name]">
<td>{{ department.name }}</td>
<td>{{ SessionUser.functions.currency.toLocal(Math.round(fixed_pricing_department_distribution.total_department_totals_parsed[department.name] || 0)) }}</td>
<td>{{ SessionUser.functions.currency.toLocal(Math.round(fixed_pricing_department_distribution.total_department_totals_relative_parsed[department.name] || 0)) }}</td>
<td>{{ getPercentageOfTotal(fixed_pricing_department_distribution.total_department_totals_relative_parsed[department.name] || 0, fixed_pricing_department_distribution.total_department_totals_parsed[department.name] || 0) }}%</td>
</tr>
<tr v-else>
<!-- Loader -->
<td>{{ department.name }}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
</template>
<!-- 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>{{ 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>
<template v-else>
<tr>
<td colspan="4" class="has-text-centered">
<span class="icon is-large">
<i class="fas fa-spinner fa-spin fa-2x"></i>
</span>
<p>Loading distribution data...</p>
</td>
</tr>
</template>
</tbody>
</table>
</WhiteBoxCard>
</div>
</template>
<!-- Vehicle subscription distribution view -->
<template v-if="view.functions.isCurrentView('vehicle_subscriptions')">
<!-- Divider -->
<div class="column is-full">
<div class="divider">
<div class="divider-text">
Abonnementsfordeling
</div>
</div>
</div>
<!-- Total vehicle subscription price -->
<div class="column is-one-third">
<WhiteBoxCard>
<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="'Total'"
/>
</WhiteBoxCard>
</div>
<!-- Subscription price distribution for each department -->
<div class="column is-full">
<WhiteBoxCard>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Department</th>
<th>Abonnementspris</th>
<th>Procent af total</th>
</tr>
</thead>
<tbody>
<template v-for="(department) in departments" :key="index" v-if="vehicleSubscriptionDistribution && vehicleSubscriptionDistribution.subscription_price_department_distribution_parsed">
<tr v-if="vehicleSubscriptionDistribution.subscription_price_department_distribution_parsed[department.name]">
<td>{{ department.name }}</td>
<td>{{ SessionUser.functions.currency.toLocal(Math.round(vehicleSubscriptionDistribution.subscription_price_department_distribution_parsed[department.name])) }}</td>
<td>{{ getPercentageOfTotal(vehicleSubscriptionDistribution.subscription_price_department_distribution_parsed[department.name], vehicleSubscriptionDistribution.total_subscription_price) }}%</td>
</tr>
<tr v-else>
<!-- Loader -->
<td>{{ department.name }}</td>
<td>-</td>
<td>-</td>
</tr>
</template>
<!-- Total row -->
<tr v-if="vehicleSubscriptionDistribution && vehicleSubscriptionDistribution.total_subscription_price !== undefined">
<td><strong>Total</strong></td>
<td><strong>{{ SessionUser.functions.currency.toLocal(vehicleSubscriptionDistribution.total_subscription_price) }}</strong></td>
<td><strong>100%</strong></td>
</tr>
<template v-else>
<tr>
<td colspan="2" class="has-text-centered">
<span class="icon is-large">
<i class="fas fa-spinner fa-spin fa-2x"></i>
</span>
<p>Loading distribution data...</p>
</td>
</tr>
</template>
</tbody>
</table>
</WhiteBoxCard>
</div>
</template>
</div>
</template>
@@ -149,6 +149,7 @@ const functionsViews = {
addViewCustomerTransaction,
updateViewCustomerTransaction,
filterExcluded, // Transaction filter function
isCurrentView: (view: string) => currentView.value === view, // Function to check if a view is the current view
};
@@ -28,6 +28,10 @@ const props = defineProps({
error: null,
card_class: 'card',
})
},
loading: {
type: Boolean,
default: false
}
})
//const props = defineProps(['name', 'icon', 'color', 'endpoint']);
@@ -71,6 +75,9 @@ const renderCurrency = (value) => {
}
onMounted(() => {
if (props.loading) {
return
}
if (props.forceDisplay && props.forceDisplay.value !== null) {
value.value = props.forceDisplay.value
start_date.value = props.forceDisplay.start_date
@@ -100,14 +107,14 @@ 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"></i>
<i :class="icon" :style="{ color: props.color }" 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-4">{{ name }}</p>
<p class="subtitle is-6" v-if="value !== null">{{ renderCurrency(value) }}</p>
<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>
</div>
@@ -119,7 +126,7 @@ const cardClass = ref(props.forceDisplay.card_class || 'card')
<span class="icon" v-if="start_date && end_date">
<i class="fas fa-calendar-day"></i>
</span>
<span v-if="start_date && end_date">
<span v-if="start_date && end_date" class="is-size-7 has-text-weight-bold">
{{ 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">