Merge remote-tracking branch 'origin/master'

This commit is contained in:
Jeppe Bundgaard
2026-03-18 11:06:11 +01:00
6 changed files with 410 additions and 36 deletions
@@ -15,6 +15,7 @@ import {
search,
setFilter
} from "@/components/pagination/paginatedList.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
@@ -28,6 +29,7 @@ import PaginationDisplayGeneralSearchReload
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.vue";
import { useI18n } from 'vue-i18n';
import {exportCsvFile} from "@/services/CSVService.js";
const { t } = useI18n();
const props = defineProps({
@@ -55,6 +57,46 @@ if (props.autoLoad) {
loadList();
}
/**
*
*/
const downloadCsv = () => {
const blob = exportCsvFile(
list.value.map((item) => ({
id: item.id,
department_id: item.department_id,
reg_1: item.reg_1,
reference: item.reference,
notes: item.notes,
created_at: item.created_at,
total_net_amount: item.total_net_amount
})),
[
SessionUser.objects.orders.columns.id.label,
SessionUser.objects.orders.columns.department_id.label,
SessionUser.functions.ucFirst(SessionUser.objects.vehicles.meta.labels.multiple),
SessionUser.objects.orders.columns.reference.label,
SessionUser.objects.orders.columns.notes.label,
SessionUser.objects.orders.columns.created_at.label,
t('tables.orders.amount')
]
)
if (blob) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', 'orders.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
</script>
<template>
@@ -63,6 +105,11 @@ if (props.autoLoad) {
<PaginationDisplayFilters
:departmentColumn="'department_id'"
/>
<div class="column is-narrow">
<button @click="downloadCsv" class="button is-dark button-same-width mt-5">
Download
</button>
</div>
</template>
<template #default>
<orders-table
+5
View File
@@ -77,6 +77,11 @@
"title": "Bookinger"
},
"daily_report": {
"titles": {
"customers": "Kunder",
"optional_products": "Tilvalgsprodukter",
"operations": "Drift"
},
"count": {
"title": "Antal"
},
+5
View File
@@ -77,6 +77,11 @@
"title": "Buchungen"
},
"daily_report": {
"titles": {
"customers": "Kunden",
"optional_products": "Optionale Produkte",
"operations": "Betrieb"
},
"count": {
"title": "Anzahl"
},
+5
View File
@@ -77,6 +77,11 @@
"title": "Bookings"
},
"daily_report": {
"titles": {
"customers": "Customers",
"optional_products": "Optional Products",
"operations": "Operations"
},
"count": {
"title": "Count"
},
+22
View File
@@ -0,0 +1,22 @@
export const exportCsvFile = (data, csvHeaders) => {
if (!Array.isArray(data) || data.length === 0) {
return false;
}
const headers = Object.keys(data[0]);
const csvRows = [];
if (Array.isArray(csvHeaders) && csvHeaders.length === headers.length) {
csvRows.push(csvHeaders.join(','));
}
for (const row of data) {
const values = headers.map(header => {
const escaped = ('' + row[header]).replace(/"/g, '\\"');
return `"${escaped}"`;
});
csvRows.push(values.join(','));
}
const csvString = csvRows.join('\n');
return new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
}
@@ -151,6 +151,7 @@ watch(() => selected_department_id.value, (newVal) => {
});
const reports = computed(() => [
// Bookinger
{
title: t('admin.daily_report.reports.bookings.title'),
subtitle: t('admin.daily_report.reports.bookings.subtitle'),
@@ -173,6 +174,7 @@ const reports = computed(() => [
}
},
// Omsætning
{
title: t('admin.daily_report.reports.revenue.title'),
subtitle: t('admin.daily_report.reports.revenue.subtitle'),
@@ -190,6 +192,7 @@ const reports = computed(() => [
quantity_suffix: ''
}
},
// Gns. omsætning per vask
{
title: t('admin.daily_report.reports.avg_revenue.title'),
subtitle: t('admin.daily_report.reports.avg_revenue.subtitle'),
@@ -208,6 +211,7 @@ const reports = computed(() => [
quantity_suffix: ''
}
},
// Vaske
{
title: t('admin.daily_report.reports.washes.title'),
subtitle: t('admin.daily_report.reports.washes.subtitle'),
@@ -225,6 +229,7 @@ const reports = computed(() => [
quantity_suffix: ''
}
},
// Transaktioner
{
title: t('admin.daily_report.reports.transactions.title'),
subtitle: t('admin.daily_report.reports.transactions.subtitle'),
@@ -239,6 +244,7 @@ const reports = computed(() => [
quantity_suffix: null
}
},
// Produkter solgt
{
title: t('admin.daily_report.reports.products_sold.title'),
subtitle: t('admin.daily_report.reports.products_sold.subtitle'),
@@ -253,6 +259,7 @@ const reports = computed(() => [
quantity_suffix: ''
}
},
// Vandforbrug
{
title: t('admin.daily_report.reports.water_usage.title'),
subtitle: t('admin.daily_report.reports.water_usage.subtitle'),
@@ -270,6 +277,7 @@ const reports = computed(() => [
quantity_suffix: ' m³'
}
},
// Vandforbrug pr. vask
{
title: t('admin.daily_report.reports.water_usage_per_wash.title'),
subtitle: t('admin.daily_report.reports.water_usage_per_wash.subtitle'),
@@ -298,10 +306,10 @@ const product_ids = ref([
out_of: 100,
},
// Spot Free (Varevogn)
{
id: 23,
out_of: 100,
},
// {
// id: 23,
// out_of: 100,
// },
// Spot Free (Lastbil)
{
id: 24,
@@ -378,6 +386,271 @@ watch(() => reports.value.map(report => report.count), () => {
minColumnHeight.value = maxHeight + 'px';
}, 100);
});
// Kunder:
// Bookinger, Kundeklager, Døgnvask, Omsætning, Gns. omsætning per vask, Vaske, Produkter solgt, Transaktioner
// Tilvalgsprodukter:
// Spot Free, Fælg Flex, Extraordinær 10 min, Højglans, Undervognsskyl, Double duty kemi
// Drift:
// Vandforbrug, Vandforbrug per vask, Overarbejde
const statistics = computed(() => [
{
title: t('admin.daily_report.titles.customers'),
columns: [
{ // Bookinger
component: DepartmentDashboardDailyReportCount,
props: {
title: t('admin.daily_report.reports.bookings.title'),
subtitle: t('admin.daily_report.reports.bookings.subtitle'),
count: count_bookings.value,
icon: 'fas fa-calendar-check',
color: Colors.global.primaryColor,
buttons: [],
parseFunction: (value) => {
return (value !== null) ? value : 0;
},
out_of: count_bookings_out_of.value,
labels: {
out_of: t('admin.daily_report.reports.bookings.possible_count'),
out_of_suffix: '',
quantity: t('admin.daily_report.reports.bookings.completed_count'),
quantity_suffix: ''
},
clickFunction: () => {
console.warn('Clicked bookings report - redirecting to bookings page');
}
}
},
{ // Omsætning
component: DepartmentDashboardDailyReportCount,
props: {
title: t('admin.daily_report.reports.revenue.title'),
subtitle: t('admin.daily_report.reports.revenue.subtitle'),
count: count_earnings.value,
icon: 'fas fa-money-bill-wave',
color: Colors.global.primaryColor,
buttons: [],
parseFunction: (value) => {
return SessionUser.functions.currency.toLocal(value);
},
labels: {
out_of: t('admin.daily_report.reports.revenue.possible_revenue'),
out_of_suffix: 'kr.',
quantity: t('admin.daily_report.reports.revenue.revenue'),
quantity_suffix: ''
}
}
},
{ // Gns. omsætning per vask
component: DepartmentDashboardDailyReportCount,
props: {
title: t('admin.daily_report.reports.avg_revenue.title'),
subtitle: t('admin.daily_report.reports.avg_revenue.subtitle'),
count: count_earnings.value,
icon: 'fas fa-money-bill-wave',
color: Colors.global.primaryColor,
buttons: [],
parseFunction: (value) => {
// Return the average earnings per wash, with 2 decimals, using the locale settings for the decimal separator
return SessionUser.functions.currency.toLocal(value / (count_washes.value || 1));
},
labels: {
out_of: '',
out_of_suffix: 'kr.',
quantity: t('admin.daily_report.reports.avg_revenue.label'),
quantity_suffix: ''
}
}
},
{ // Vaske
component: DepartmentDashboardDailyReportCount,
props: {
title: t('admin.daily_report.reports.washes.title'),
subtitle: t('admin.daily_report.reports.washes.subtitle'),
count: count_washes.value,
icon: 'fas fa-car',
color: Colors.global.primaryColor,
buttons: [],
parseFunction: (value) => {
return value;
},
labels: {
out_of: t('admin.daily_report.reports.washes.possible_washes'),
out_of_suffix: '',
quantity: t('admin.daily_report.reports.washes.completed_washes'),
quantity_suffix: ''
}
}
},
{ // Produkter solgt
component: DepartmentDashboardDailyReportCount,
props: {
title: t('admin.daily_report.reports.products_sold.title'),
subtitle: t('admin.daily_report.reports.products_sold.subtitle'),
count: count_products_sold.value,
icon: 'fas fa-shopping-cart',
color: Colors.global.primaryColor,
buttons: [],
labels: {
out_of: t('admin.daily_report.reports.products_sold.possible_sold'),
out_of_suffix: '',
quantity: t('admin.daily_report.reports.products_sold.sold'),
quantity_suffix: ''
}
}
},
{ // Transaktioner
component: DepartmentDashboardDailyReportCount,
props: {
title: t('admin.daily_report.reports.transactions.title'),
subtitle: t('admin.daily_report.reports.transactions.subtitle'),
count: count_transactions.value,
icon: 'fas fa-exchange-alt',
color: Colors.global.primaryColor,
buttons: [],
labels: {
out_of: t('admin.daily_report.reports.transactions.possible_transactions'),
out_of_suffix: null,
quantity: t('admin.daily_report.reports.transactions.transaction_count'),
quantity_suffix: null
}
}
},
]
},
{
title: t('admin.daily_report.titles.optional_products'),
columns: [
{
component: DepartmentDashboardDailyReportProductSales,
props: {
styleContent: { minHeight: minColumnHeight },
product_id: 24,
date: getDate(),
department_id: department_id,
color: Colors.global.primaryColor,
force_out_of: null,
is_loading: isLoading('daily_report_counts'),
subtitle: formatSubtitle(t('admin.daily_report.product_sales.subtitle_suffix'))
}
},
{
component: DepartmentDashboardDailyReportProductSales,
props: {
styleContent: { minHeight: minColumnHeight },
product_id: 25,
date: getDate(),
department_id: department_id,
color: Colors.global.primaryColor,
force_out_of: null,
is_loading: isLoading('daily_report_counts'),
subtitle: formatSubtitle(t('admin.daily_report.product_sales.subtitle_suffix'))
}
},
{
component: DepartmentDashboardDailyReportProductSales,
props: {
styleContent: { minHeight: minColumnHeight },
product_id: 27,
date: getDate(),
department_id: department_id,
color: Colors.global.primaryColor,
force_out_of: count_washes.value,
is_loading: isLoading('daily_report_counts'),
subtitle: formatSubtitle(t('admin.daily_report.product_sales.subtitle_suffix'))
}
},
{
component: DepartmentDashboardDailyReportProductSales,
props: {
styleContent: { minHeight: minColumnHeight },
product_id: 26,
date: getDate(),
department_id: department_id,
color: Colors.global.primaryColor,
force_out_of: null,
is_loading: isLoading('daily_report_counts'),
subtitle: formatSubtitle(t('admin.daily_report.product_sales.subtitle_suffix'))
}
},
{
component: DepartmentDashboardDailyReportProductSales,
props: {
styleContent: { minHeight: minColumnHeight },
product_id: 21,
date: getDate(),
department_id: department_id,
color: Colors.global.primaryColor,
force_out_of: null,
is_loading: isLoading('daily_report_counts'),
subtitle: formatSubtitle(t('admin.daily_report.product_sales.subtitle_suffix'))
}
},
{
component: DepartmentDashboardDailyReportProductSales,
props: {
styleContent: { minHeight: minColumnHeight },
product_id: 22,
date: getDate(),
department_id: department_id,
color: Colors.global.primaryColor,
force_out_of: null,
is_loading: isLoading('daily_report_counts'),
subtitle: formatSubtitle(t('admin.daily_report.product_sales.subtitle_suffix')),
}
},
]
},
{
title: t('admin.daily_report.titles.operations'),
columns: [
{ // Vandforbrug
component: DepartmentDashboardDailyReportCount,
props: {
title: t('admin.daily_report.reports.water_usage.title'),
subtitle: t('admin.daily_report.reports.water_usage.subtitle'),
count: count_water_usage.value,
icon: 'fas fa-car',
color: Colors.global.primaryColor,
buttons: [],
parseFunction: (value) => {
return value;
},
labels: {
out_of: t('admin.daily_report.reports.washes.possible_washes'),
out_of_suffix: '',
quantity: t('admin.daily_report.reports.water_usage.water_usage'),
quantity_suffix: ' m³'
}
}
},
{ // Vandforbrug per vask
component: DepartmentDashboardDailyReportCount,
props: {
title: t('admin.daily_report.reports.water_usage_per_wash.title'),
subtitle: t('admin.daily_report.reports.water_usage_per_wash.subtitle'),
count: count_water_usage.value,
icon: 'fas fa-car',
color: Colors.global.primaryColor,
buttons: [],
parseFunction: (value) => {
// Return the average water usage per wash, with 3 decimals, using the locale settings for the decimal separator
return (value / (count_washes.value || 1)).toLocaleString('da-DK', { minimumFractionDigits: 3, maximumFractionDigits: 3 });
},
labels: {
out_of: '',
out_of_suffix: '',
quantity: t('admin.daily_report.reports.water_usage_per_wash.avg_water_usage'),
quantity_suffix: ' m³'
}
}
}
]
}
]);
</script>
<template>
@@ -393,40 +666,57 @@ watch(() => reports.value.map(report => report.count), () => {
<div class="column is-12">
<DepartmentDashboardDailyReportNavigation :selectAllDepartments="!props.departmentSpecific" :loading="isLoading('daily_report_counts')" />
</div>
<!-- NEW STATISTICS BEGIN -->
<template v-for="section in statistics" :key="section.title">
<div class="column is-12">
<h2 class="title is-4 mt-3">
{{ section.title }}
</h2>
</div>
<div class="column is-4" v-for="chart in section.columns" :key="chart.props">
<component
:is="chart.component"
v-bind="chart.props"
/>
</div>
</template>
<!-- NEW STATISTICS END -->
<!-- Display the daily report counts -->
<div class="column is-4" v-for="report in reports" :key="report.title">
<DepartmentDashboardDailyReportCount
:styleContent="{ minHeight: minColumnHeight }"
:title="report.title"
:subtitle="formatSubtitle(report.subtitle)"
:count="report.count"
:icon="report.icon"
:color="report.color"
:buttons="report.buttons"
:out_of="(report?.out_of || 0)"
:label_out_of="report.labels.out_of"
:label_out_of_suffix="report.labels.out_of_suffix"
:force_out_of="report.labels.out_of ? report.labels.out_of : null"
:label_quantity="report.labels.quantity"
:label_quantity_suffix="report.labels.quantity_suffix"
v-bind:parseFunction="report.parseFunction ? report.parseFunction : (value) => { return value; }"
:clickFunction="report?.clickFunction || (() => {})"
:isLoading="isLoading('daily_report_counts')"
/>
</div>
<!-- <div class="column is-4" v-for="report in reports" :key="report.title">-->
<!-- <DepartmentDashboardDailyReportCount-->
<!-- :styleContent="{ minHeight: minColumnHeight }"-->
<!-- :title="report.title"-->
<!-- :subtitle="formatSubtitle(report.subtitle)"-->
<!-- :count="report.count"-->
<!-- :icon="report.icon"-->
<!-- :color="report.color"-->
<!-- :buttons="report.buttons"-->
<!-- :out_of="(report?.out_of || 0)"-->
<!-- :label_out_of="report.labels.out_of"-->
<!-- :label_out_of_suffix="report.labels.out_of_suffix"-->
<!-- :force_out_of="report.labels.out_of ? report.labels.out_of : null"-->
<!-- :label_quantity="report.labels.quantity"-->
<!-- :label_quantity_suffix="report.labels.quantity_suffix"-->
<!-- v-bind:parseFunction="report.parseFunction ? report.parseFunction : (value) => { return value; }"-->
<!-- :clickFunction="report?.clickFunction || (() => {})"-->
<!-- :isLoading="isLoading('daily_report_counts')"-->
<!-- />-->
<!-- </div>-->
<!-- Display the daily report product sales -->
<div class="column is-4" v-for="product in product_ids" :key="product.id">
<DepartmentDashboardDailyReportProductSales
:styleContent="{ minHeight: minColumnHeight }"
:product_id="product.id"
:date="getDate()"
:department_id="department_id"
:color="Colors.global.primaryColor"
:force_out_of="product.force_out_of || null"
:is_loading="isLoading('daily_report_counts')"
:subtitle="formatSubtitle(t('admin.daily_report.product_sales.subtitle_suffix'))"
/>
</div>
<!-- <div class="column is-4" v-for="product in product_ids" :key="product.id">-->
<!-- <DepartmentDashboardDailyReportProductSales-->
<!-- :styleContent="{ minHeight: minColumnHeight }"-->
<!-- :product_id="product.id"-->
<!-- :date="getDate()"-->
<!-- :department_id="department_id"-->
<!-- :color="Colors.global.primaryColor"-->
<!-- :force_out_of="product.force_out_of || null"-->
<!-- :is_loading="isLoading('daily_report_counts')"-->
<!-- :subtitle="formatSubtitle(t('admin.daily_report.product_sales.subtitle_suffix'))"-->
<!-- />-->
<!-- </div>-->
</div>
<ElementTabsBox
v-if="props.departmentSpecific"