add export buttons for aggregated export and pdf export

This commit is contained in:
Gregor Vostrak
2024-10-23 14:33:02 +02:00
committed by Constantin Graf
parent b0bcc4f330
commit 7c1fe35754
6 changed files with 251 additions and 37 deletions

View File

@@ -273,7 +273,9 @@ class TimeEntryController extends Controller
/**
* Export aggregated time entries in organization
*
* @operationId exportAggregatedTimeEntries
* @throws AuthorizationException
*
*/
public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request): JsonResponse
{

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { SecondaryButton } from '@/packages/ui/src';
import { ArrowDownTrayIcon } from '@heroicons/vue/20/solid';
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
import type { ExportFormat } from '@/types/reporting';
const emit = defineEmits<{
submit: [ExportFormat];
}>();
</script>
<template>
<Dropdown align="bottom-end">
<template #trigger>
<SecondaryButton>
<div class="flex space-x-2 items-center">
<ArrowDownTrayIcon
class="w-4 text-text-tertiary"></ArrowDownTrayIcon>
<span>Export</span>
</div>
</SecondaryButton>
</template>
<template #content>
<div class="flex flex-col space-y-1 p-1.5">
<SecondaryButton
class="border-0 px-2"
@click="emit('submit', 'pdf')"
>Export as PDF</SecondaryButton
>
<SecondaryButton
class="border-0 px-2"
@click="emit('submit', 'xlsx')"
>Export as Excel</SecondaryButton
>
<SecondaryButton
class="border-0 px-2"
@click="emit('submit', 'csv')"
>Export as CSV</SecondaryButton
>
<SecondaryButton
class="border-0 px-2"
@click="emit('submit', 'ods')"
>Export as ODS
</SecondaryButton>
</div>
</template>
</Dropdown>
</template>
<style scoped></style>

View File

@@ -8,7 +8,6 @@ import {
UserGroupIcon,
CheckCircleIcon,
TagIcon,
ArrowDownTrayIcon,
} from '@heroicons/vue/20/solid';
import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue';
import ReportingChart from '@/Components/Common/Reporting/ReportingChart.vue';
@@ -45,9 +44,9 @@ import { useSessionStorage, useStorage } from '@vueuse/core';
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
import { router } from '@inertiajs/vue3';
import { SecondaryButton } from '@/packages/ui/src';
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
import { useNotificationsStore } from '@/utils/notification';
import ReportingExportButton from '@/Components/Common/Reporting/ReportingExportButton.vue';
import type { ExportFormat } from '@/types/reporting';
const { handleApiRequestNotifications } = useNotificationsStore();
const startDate = useSessionStorage<string>(
@@ -160,19 +159,19 @@ async function createTag(tag: string) {
return await useTagsStore().createTag(tag);
}
type ExportFormat = 'xlsx' | 'csv' | 'ods';
async function downloadExport(format: ExportFormat) {
const organizationId = getCurrentOrganizationId();
if (organizationId) {
const response = await handleApiRequestNotifications(
() =>
api.exportTimeEntries({
api.exportAggregatedTimeEntries({
params: {
organization: organizationId,
},
queries: {
...getFilterAttributes(),
group: group.value,
sub_group: subGroup.value,
format: format,
},
}),
@@ -203,38 +202,10 @@ async function downloadExport(format: ExportFormat) {
>
</TabBar>
</div>
<Dropdown align="bottom-end">
<template #trigger>
<SecondaryButton>
<div class="flex space-x-2 items-center">
<ArrowDownTrayIcon
class="w-4 text-text-tertiary"></ArrowDownTrayIcon>
<span>Export</span>
</div>
</SecondaryButton>
</template>
<template #content>
<div class="flex flex-col space-y-1 p-1.5">
<SecondaryButton
class="border-0 px-2"
@click="downloadExport('xlsx')"
>Export as Excel</SecondaryButton
>
<SecondaryButton
class="border-0 px-2"
@click="downloadExport('csv')"
>Export as CSV</SecondaryButton
>
<SecondaryButton
class="border-0 px-2"
@click="downloadExport('ods')"
>Export as ODS
</SecondaryButton>
</div>
</template>
</Dropdown>
<ReportingExportButton
@submit="downloadExport"></ReportingExportButton>
</MainContainer>
<div class="p-3 w-full border-b border-default-background-separator">
<div class="py-2.5 w-full border-b border-default-background-separator">
<MainContainer
class="sm:flex space-y-4 sm:space-y-0 justify-between">
<div

View File

@@ -64,6 +64,9 @@ import {
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
import ReportingExportButton from '@/Components/Common/Reporting/ReportingExportButton.vue';
import type { ExportFormat } from '@/types/reporting';
import { useNotificationsStore } from '@/utils/notification';
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
import { canCreateProjects } from '@/utils/permissions';
@@ -120,6 +123,7 @@ function getFilterAttributes() {
const currentTimeEntryStore = useCurrentTimeEntryStore();
const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
const { setActiveState, startLiveTimer } = currentTimeEntryStore;
const { handleApiRequestNotifications } = useNotificationsStore();
const { createTimeEntry, updateTimeEntry, updateTimeEntries } =
useTimeEntriesStore();
@@ -215,6 +219,26 @@ async function clearSelectionAndState() {
selectedTimeEntries.value = [];
await updateFilteredTimeEntries();
}
async function downloadExport(format: ExportFormat) {
const organizationId = getCurrentOrganizationId();
if (organizationId) {
const response = await handleApiRequestNotifications(
() =>
api.exportTimeEntries({
params: {
organization: organizationId,
},
queries: {
...getFilterAttributes(),
format: format,
},
}),
'Export successful',
'Export failed'
);
window.open(response.download_url, '_self')?.focus();
}
}
</script>
<template>
@@ -237,6 +261,8 @@ async function clearSelectionAndState() {
</TabBarItem>
</TabBar>
</div>
<ReportingExportButton
@submit="downloadExport"></ReportingExportButton>
</MainContainer>
<div class="py-2.5 w-full border-b border-default-background-separator">
<MainContainer

View File

@@ -2656,6 +2656,159 @@ If the group parameters are all set to &#x60;null&#x60; or are all missing, the
},
],
},
{
method: 'get',
path: '/v1/organizations/:organization/time-entries/aggregate/export',
alias: 'exportAggregatedTimeEntries',
requestFormat: 'json',
parameters: [
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
{
name: 'format',
type: 'Query',
schema: z.enum(['csv', 'pdf', 'xlsx', 'ods']),
},
{
name: 'group',
type: 'Query',
schema: z
.enum([
'day',
'week',
'month',
'year',
'user',
'project',
'task',
'client',
'billable',
'description',
])
.optional(),
},
{
name: 'sub_group',
type: 'Query',
schema: z
.enum([
'day',
'week',
'month',
'year',
'user',
'project',
'task',
'client',
'billable',
'description',
])
.optional(),
},
{
name: 'member_id',
type: 'Query',
schema: z.string().optional(),
},
{
name: 'user_id',
type: 'Query',
schema: z.string().optional(),
},
{
name: 'start',
type: 'Query',
schema: start,
},
{
name: 'end',
type: 'Query',
schema: start,
},
{
name: 'active',
type: 'Query',
schema: z.enum(['true', 'false']).optional(),
},
{
name: 'billable',
type: 'Query',
schema: z.enum(['true', 'false']).optional(),
},
{
name: 'fill_gaps_in_time_groups',
type: 'Query',
schema: z.enum(['true', 'false']).optional(),
},
{
name: 'member_ids',
type: 'Query',
schema: z.array(z.string()).min(1).optional(),
},
{
name: 'project_ids',
type: 'Query',
schema: z.array(z.string()).min(1).optional(),
},
{
name: 'client_ids',
type: 'Query',
schema: z.array(z.string()).min(1).optional(),
},
{
name: 'tag_ids',
type: 'Query',
schema: z.array(z.string()).min(1).optional(),
},
{
name: 'task_ids',
type: 'Query',
schema: z.array(z.string()).min(1).optional(),
},
],
response: z.object({ download_url: z.string() }).passthrough(),
errors: [
{
status: 400,
description: `API exception`,
schema: z
.object({
error: z.boolean(),
key: z.string(),
message: z.string(),
})
.passthrough(),
},
{
status: 401,
description: `Unauthenticated`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 403,
description: `Authorization error`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 404,
description: `Not found`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 422,
description: `Validation error`,
schema: z
.object({
message: z.string(),
errors: z.record(z.array(z.string())),
})
.passthrough(),
},
],
},
{
method: 'get',
path: '/v1/organizations/:organization/time-entries/export',
@@ -2730,6 +2883,17 @@ If the group parameters are all set to &#x60;null&#x60; or are all missing, the
],
response: z.object({ download_url: z.string() }).passthrough(),
errors: [
{
status: 400,
description: `API exception`,
schema: z
.object({
error: z.boolean(),
key: z.string(),
message: z.string(),
})
.passthrough(),
},
{
status: 401,
description: `Unauthenticated`,

1
resources/js/types/reporting.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export type ExportFormat = 'xlsx' | 'csv' | 'ods' | 'pdf';