Files
memberApp/src/pages/ReportPage.vue
2026-02-13 12:28:26 +01:00

365 lines
9.5 KiB
Vue

<template>
<q-inner-loading
:showing="loading"
label="Please wait..."
label-class="text-teal"
label-style="font-size: 1.1em"
/>
<div class="colums">
<div class="row justify-end">
<div class="column">
<q-btn-dropdown
no-caps
ref="dropdownRef"
class="q-ma-sm"
color="primary"
:label="$t('selectDates')"
@show="loadSettings"
>
<DateDaySelect @update:dates="updateReport" v-model:weekdays="weekdays" />
<div class="column justify-end q-pa-md">
<div class="row q-ma-md">
<q-input
class="col-7"
label-color="primary"
:label="$t('filterEventName')"
:hint="$t('hintFilterEventName')"
type="text"
v-model:model-value="filter"
></q-input>
</div>
<div class="row q-ma-md">
<q-select
class="col-7"
:label="$t('filterByColumnValue')"
dense
v-model="group"
:options="groups"
option-label="name"
option-value="id"
multiple
clearable
/>
</div>
<div class="row justify-end">
<q-btn dense class="q-ma-md" color="primary" no-caps @click="applyDateChoice">{{
$t('apply')
}}</q-btn>
</div>
</div>
</q-btn-dropdown>
<div class="column q-ma-sm" v-if="amounts.length">
<q-btn
dense
no-caps
class="q-ma-sm"
color="grey-9"
icon="print"
:label="$t('print')"
@click="printReport"
/>
<q-btn
dense
no-caps
color="secondary"
icon="picture_as_pdf"
:label="$t('exportPdf')"
@click="downloadPDF"
/>
</div>
</div>
</div>
</div>
<div id="report-content" ref="reportExportRef">
<div class="row justify-center q-ma-xs">
<h3 class="col-12 text-center text-primary text-bold">{{ $t('report') }}</h3>
<ReportStat :amounts="amounts" />
</div>
<div class="row justify-center">
<div
v-if="attendees !== undefined"
:class="
nonAttendees !== undefined
? 'col-12 col-sm-5 col-md-5 q-pa-md'
: 'col-12 col-md-8 col-lg-5'
"
>
<q-table
flat
dense
:no-data-label="$t('noDataAvailable')"
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:rows-per-page-options="[0]"
:title="$t('attendees')"
title-class="text-bold text-primary"
:rows="attendees"
:columns="columns"
>
</q-table>
</div>
<div
v-if="nonAttendees !== undefined"
:class="
attendees !== undefined ? 'col-12 col-sm-5 col-md-5 q-pa-md' : 'col-12 col-md-8 col-lg-5'
"
>
<q-table
flat
dense
:title="$t('noneAttendees')"
:no-data-label="$t('noDataAvailable')"
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:rows-per-page-options="[0]"
title-class="text-bold text-primary"
:rows="nonAttendees"
:columns="columns"
>
</q-table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import DateDaySelect from 'src/components/DateDaySelect.vue';
import { computed, onMounted, ref } from 'vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'src/boot/lang';
import { appName, databaseName } from 'src/vueLib/models/settings';
import type { Amount } from 'src/vueLib/models/report';
import ReportStat from 'src/components/ReportStat.vue';
import type { Group, Groups } from 'src/vueLib/models/group';
import { getLocalPageDefaults, setLocalPageDefaults } from 'src/localstorage/localStorage';
import html2pdf from 'html2pdf.js';
import type { PageDefault } from 'src/vueLib/models/pagedefaults';
const filter = ref<string>('');
const group = ref<Group[]>([]);
const groups = ref<Groups>([]);
const allDates = ref<string[]>([]);
const attendees = ref();
const nonAttendees = ref();
const { NotifyResponse } = useNotify();
const dropdownRef = ref();
const loading = ref(false);
const amounts = ref<Amount[]>([]);
const reportExportRef = ref<HTMLElement | null>(null);
const weekdays = ref<number[]>([0, 3]);
const columns = computed(() => [
{
name: 'firstName',
align: 'left' as const,
label: i18n.global.t('prename'),
field: 'firstName',
sortable: true,
},
{
name: 'lastName',
align: 'left' as const,
label: i18n.global.t('lastName'),
field: 'lastName',
sortable: true,
},
]);
onMounted(() => {
loading.value = true;
appApi
.post('database/open', { dbPath: databaseName.value })
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
appApi
.get('/groups')
.then((resp) => (groups.value = resp.data))
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
});
function loadSettings() {
const settings = getLocalPageDefaults('report') as PageDefault;
if (!settings) return;
if (settings.groups) {
group.value = settings.groups;
}
if (settings.weekdays) {
weekdays.value = settings.weekdays;
}
}
function applyDateChoice() {
amounts.value = [];
loading.value = true;
dropdownRef.value.hide();
const payload: { name: null | string[]; date: string[]; groupIds: number[] | null } = {
name: null,
date: allDates.value,
groupIds: null,
};
if (filter.value) {
//name is a array of string and search works as example Test*, Ge*, *st
payload.name = [filter.value];
}
if (group.value) {
// group has to be array of id numbers
payload.groupIds = group.value.map((g) => g.id);
}
payload.date = allDates.value;
setLocalPageDefaults('report', <PageDefault>{ groups: group.value, weekdays: weekdays.value });
appApi
.post('report', payload)
.then((resp) => {
attendees.value = [];
nonAttendees.value = [];
if (!resp.data) return;
if (resp.data.data === undefined) return;
const data = resp.data.data;
if (data.data === undefined) return;
if (data.attendees) {
attendees.value = data.attendees;
}
if (data.attendees) {
nonAttendees.value = data.nonAttendees;
}
const days = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
'total',
];
amounts.value = days
.filter((day) => data.data[day]) // Only include days that exist in the response
.map((day) => ({
...data.data[day],
name: day, // Dynamically translate the name
}));
if (amounts.value.length == 2) {
amounts.value.splice(1);
}
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => (loading.value = false));
}
function updateReport(dates: string[]) {
allDates.value = dates;
}
function printReport() {
window.print();
}
async function downloadPDF() {
const element = reportExportRef.value;
if (!element) return;
// Generate date string (YYYY-MM-DD)
const today = new Date().toISOString().split('T')[0];
// Optionally, add time for more precision (HH-mm)
const time = new Date()
.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
.replace(':', '-');
const options = {
margin: [10, 10, 10, 10] as [number, number, number, number],
filename: appName.value + `${today}_${time}.pdf`,
image: {
type: 'jpeg' as const,
quality: 1.0, // Set quality to 100%
},
html2canvas: {
scale: 4, // Increase this (2 is standard, 4 is crisp/retina)
useCORS: true,
letterRendering: true, // Improves text spacing
dpi: 300, // Standard print resolution
},
jsPDF: {
unit: 'mm' as const,
format: 'a4' as const,
orientation: 'portrait' as const,
compress: true, // Keeps file size manageable despite high scale
},
};
await html2pdf()
.set(options)
.from(element)
.save()
.catch((error) => {
console.error('PDF Generation failed:', error);
});
}
</script>
<style>
@media print {
/* 1. Hide the URL, Date, and Page Title */
@page {
margin: 0; /* This is what removes the URL and headers/footers */
}
body {
padding: 1.5cm; /* Add padding here so the content isn't at the very edge */
}
/* 2. Hide UI elements */
.q-btn,
.q-btn-dropdown,
.q-header,
.q-drawer,
.q-footer,
.q-notifications {
display: none !important;
}
/* 3. Ensure the layout uses full width */
.q-page-container {
padding: 0 !important;
}
/* Remove shadows for cleaner printing */
.q-card,
.q-table__card {
box-shadow: none !important;
border: 1px solid #ddd !important;
}
}
/* This ensures the PDF version has a white background and visible text */
#report-content {
background: white;
color: black;
padding: 20px;
}
/* Force tables to expand to full width in the PDF */
#report-content .q-table__container {
width: 100% !important;
}
/* If you want to force a page break before the tables */
.pdf-page-break {
page-break-before: always;
}
</style>