mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 20:22:15 +01:00
add export buttons for aggregated export and pdf export
This commit is contained in:
committed by
Constantin Graf
parent
b0bcc4f330
commit
7c1fe35754
@@ -273,7 +273,9 @@ class TimeEntryController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Export aggregated time entries in organization
|
* Export aggregated time entries in organization
|
||||||
*
|
*
|
||||||
|
* @operationId exportAggregatedTimeEntries
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request): JsonResponse
|
public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
UserGroupIcon,
|
UserGroupIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
TagIcon,
|
TagIcon,
|
||||||
ArrowDownTrayIcon,
|
|
||||||
} from '@heroicons/vue/20/solid';
|
} from '@heroicons/vue/20/solid';
|
||||||
import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue';
|
import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue';
|
||||||
import ReportingChart from '@/Components/Common/Reporting/ReportingChart.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 TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
||||||
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||||
import { router } from '@inertiajs/vue3';
|
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 { useNotificationsStore } from '@/utils/notification';
|
||||||
|
import ReportingExportButton from '@/Components/Common/Reporting/ReportingExportButton.vue';
|
||||||
|
import type { ExportFormat } from '@/types/reporting';
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
|
||||||
const startDate = useSessionStorage<string>(
|
const startDate = useSessionStorage<string>(
|
||||||
@@ -160,19 +159,19 @@ async function createTag(tag: string) {
|
|||||||
return await useTagsStore().createTag(tag);
|
return await useTagsStore().createTag(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExportFormat = 'xlsx' | 'csv' | 'ods';
|
|
||||||
|
|
||||||
async function downloadExport(format: ExportFormat) {
|
async function downloadExport(format: ExportFormat) {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
const response = await handleApiRequestNotifications(
|
const response = await handleApiRequestNotifications(
|
||||||
() =>
|
() =>
|
||||||
api.exportTimeEntries({
|
api.exportAggregatedTimeEntries({
|
||||||
params: {
|
params: {
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
},
|
},
|
||||||
queries: {
|
queries: {
|
||||||
...getFilterAttributes(),
|
...getFilterAttributes(),
|
||||||
|
group: group.value,
|
||||||
|
sub_group: subGroup.value,
|
||||||
format: format,
|
format: format,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -203,38 +202,10 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
>
|
>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown align="bottom-end">
|
<ReportingExportButton
|
||||||
<template #trigger>
|
@submit="downloadExport"></ReportingExportButton>
|
||||||
<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>
|
|
||||||
</MainContainer>
|
</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
|
<MainContainer
|
||||||
class="sm:flex space-y-4 sm:space-y-0 justify-between">
|
class="sm:flex space-y-4 sm:space-y-0 justify-between">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ import {
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
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 TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
|
||||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||||
import { canCreateProjects } from '@/utils/permissions';
|
import { canCreateProjects } from '@/utils/permissions';
|
||||||
@@ -120,6 +123,7 @@ function getFilterAttributes() {
|
|||||||
const currentTimeEntryStore = useCurrentTimeEntryStore();
|
const currentTimeEntryStore = useCurrentTimeEntryStore();
|
||||||
const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
|
const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
|
||||||
const { setActiveState, startLiveTimer } = currentTimeEntryStore;
|
const { setActiveState, startLiveTimer } = currentTimeEntryStore;
|
||||||
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
const { createTimeEntry, updateTimeEntry, updateTimeEntries } =
|
const { createTimeEntry, updateTimeEntry, updateTimeEntries } =
|
||||||
useTimeEntriesStore();
|
useTimeEntriesStore();
|
||||||
|
|
||||||
@@ -215,6 +219,26 @@ async function clearSelectionAndState() {
|
|||||||
selectedTimeEntries.value = [];
|
selectedTimeEntries.value = [];
|
||||||
await updateFilteredTimeEntries();
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -237,6 +261,8 @@ async function clearSelectionAndState() {
|
|||||||
</TabBarItem>
|
</TabBarItem>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
</div>
|
</div>
|
||||||
|
<ReportingExportButton
|
||||||
|
@submit="downloadExport"></ReportingExportButton>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<div class="py-2.5 w-full border-b border-default-background-separator">
|
<div class="py-2.5 w-full border-b border-default-background-separator">
|
||||||
<MainContainer
|
<MainContainer
|
||||||
|
|||||||
@@ -2656,6 +2656,159 @@ If the group parameters are all set to `null` 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',
|
method: 'get',
|
||||||
path: '/v1/organizations/:organization/time-entries/export',
|
path: '/v1/organizations/:organization/time-entries/export',
|
||||||
@@ -2730,6 +2883,17 @@ If the group parameters are all set to `null` or are all missing, the
|
|||||||
],
|
],
|
||||||
response: z.object({ download_url: z.string() }).passthrough(),
|
response: z.object({ download_url: z.string() }).passthrough(),
|
||||||
errors: [
|
errors: [
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
description: `API exception`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
error: z.boolean(),
|
||||||
|
key: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 401,
|
status: 401,
|
||||||
description: `Unauthenticated`,
|
description: `Unauthenticated`,
|
||||||
|
|||||||
1
resources/js/types/reporting.d.ts
vendored
Normal file
1
resources/js/types/reporting.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type ExportFormat = 'xlsx' | 'csv' | 'ods' | 'pdf';
|
||||||
Reference in New Issue
Block a user