Add fixed pricing and subscription distribution views with API integrations and conditional rendering adjustments
This commit is contained in:
@@ -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>
|
||||
|
||||
+290
-1
@@ -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>
|
||||
|
||||
|
||||
+1
@@ -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
|
||||
};
|
||||
|
||||
|
||||
|
||||
+11
-4
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user