mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 11:42:15 +01:00
add Export download buttons
This commit is contained in:
committed by
Constantin Graf
parent
64535ceea6
commit
d080b07e60
@@ -147,6 +147,8 @@ class TimeEntryController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
|
*
|
||||||
|
* @operationId exportTimeEntries
|
||||||
*/
|
*/
|
||||||
public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request): JsonResponse
|
public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ 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';
|
||||||
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
|
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import {
|
import {
|
||||||
formatHumanReadableDuration,
|
formatHumanReadableDuration,
|
||||||
@@ -21,7 +23,7 @@ import {
|
|||||||
import { type GroupingOption, useReportingStore } from '@/utils/useReporting';
|
import { type GroupingOption, useReportingStore } from '@/utils/useReporting';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
|
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
|
||||||
import type { AggregatedTimeEntriesQueryParams } from '@/packages/api/src';
|
import { type AggregatedTimeEntriesQueryParams, api } from '@/packages/api/src';
|
||||||
import ReportingFilterBadge from '@/Components/Common/Reporting/ReportingFilterBadge.vue';
|
import ReportingFilterBadge from '@/Components/Common/Reporting/ReportingFilterBadge.vue';
|
||||||
import ProjectMultiselectDropdown from '@/Components/Common/Project/ProjectMultiselectDropdown.vue';
|
import ProjectMultiselectDropdown from '@/Components/Common/Project/ProjectMultiselectDropdown.vue';
|
||||||
import MemberMultiselectDropdown from '@/Components/Common/Member/MemberMultiselectDropdown.vue';
|
import MemberMultiselectDropdown from '@/Components/Common/Member/MemberMultiselectDropdown.vue';
|
||||||
@@ -31,7 +33,11 @@ import ReportingGroupBySelect from '@/Components/Common/Reporting/ReportingGroup
|
|||||||
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
||||||
import { getCurrentMembershipId, getCurrentRole } from '@/utils/useUser';
|
import {
|
||||||
|
getCurrentMembershipId,
|
||||||
|
getCurrentOrganizationId,
|
||||||
|
getCurrentRole,
|
||||||
|
} from '@/utils/useUser';
|
||||||
import ClientMultiselectDropdown from '@/Components/Common/Client/ClientMultiselectDropdown.vue';
|
import ClientMultiselectDropdown from '@/Components/Common/Client/ClientMultiselectDropdown.vue';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
import { useTagsStore } from '@/utils/useTags';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
@@ -39,6 +45,10 @@ 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';
|
||||||
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
|
||||||
const startDate = useSessionStorage<string>(
|
const startDate = useSessionStorage<string>(
|
||||||
'reporting-start-date',
|
'reporting-start-date',
|
||||||
@@ -149,6 +159,29 @@ const { tags } = storeToRefs(useTagsStore());
|
|||||||
async function createTag(tag: string) {
|
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) {
|
||||||
|
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>
|
||||||
@@ -170,6 +203,36 @@ async function createTag(tag: string) {
|
|||||||
>
|
>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
</div>
|
</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>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<div class="p-3 w-full border-b border-default-background-separator">
|
<div class="p-3 w-full border-b border-default-background-separator">
|
||||||
<MainContainer
|
<MainContainer
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const ProjectMemberResource = z
|
|||||||
.passthrough();
|
.passthrough();
|
||||||
const ProjectMemberStoreRequest = z
|
const ProjectMemberStoreRequest = z
|
||||||
.object({
|
.object({
|
||||||
member_id: z.string().uuid(),
|
member_id: z.string(),
|
||||||
billable_rate: z.union([z.number(), z.null()]).optional(),
|
billable_rate: z.union([z.number(), z.null()]).optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
@@ -172,14 +172,14 @@ const TimeEntryResource = z
|
|||||||
.passthrough();
|
.passthrough();
|
||||||
const TimeEntryStoreRequest = z
|
const TimeEntryStoreRequest = z
|
||||||
.object({
|
.object({
|
||||||
member_id: z.string().uuid(),
|
member_id: z.string(),
|
||||||
project_id: z.union([z.string(), z.null()]).optional(),
|
project_id: z.union([z.string(), z.null()]).optional(),
|
||||||
task_id: z.union([z.string(), z.null()]).optional(),
|
task_id: z.union([z.string(), z.null()]).optional(),
|
||||||
start: z.string(),
|
start: z.string(),
|
||||||
end: z.union([z.string(), z.null()]).optional(),
|
end: z.union([z.string(), z.null()]).optional(),
|
||||||
billable: z.boolean(),
|
billable: z.boolean(),
|
||||||
description: z.union([z.string(), z.null()]).optional(),
|
description: z.union([z.string(), z.null()]).optional(),
|
||||||
tags: z.union([z.array(z.string().uuid()), z.null()]).optional(),
|
tags: z.union([z.array(z.string()), z.null()]).optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const TimeEntryUpdateMultipleRequest = z
|
const TimeEntryUpdateMultipleRequest = z
|
||||||
@@ -187,12 +187,12 @@ const TimeEntryUpdateMultipleRequest = z
|
|||||||
ids: z.array(z.string().uuid()),
|
ids: z.array(z.string().uuid()),
|
||||||
changes: z
|
changes: z
|
||||||
.object({
|
.object({
|
||||||
member_id: z.string().uuid(),
|
member_id: z.string(),
|
||||||
project_id: z.union([z.string(), z.null()]),
|
project_id: z.union([z.string(), z.null()]),
|
||||||
task_id: z.union([z.string(), z.null()]),
|
task_id: z.union([z.string(), z.null()]),
|
||||||
billable: z.boolean(),
|
billable: z.boolean(),
|
||||||
description: z.union([z.string(), z.null()]),
|
description: z.union([z.string(), z.null()]),
|
||||||
tags: z.union([z.array(z.string().uuid()), z.null()]),
|
tags: z.union([z.array(z.string()), z.null()]),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.passthrough(),
|
.passthrough(),
|
||||||
@@ -200,14 +200,14 @@ const TimeEntryUpdateMultipleRequest = z
|
|||||||
.passthrough();
|
.passthrough();
|
||||||
const TimeEntryUpdateRequest = z
|
const TimeEntryUpdateRequest = z
|
||||||
.object({
|
.object({
|
||||||
member_id: z.string().uuid(),
|
member_id: z.string(),
|
||||||
project_id: z.union([z.string(), z.null()]),
|
project_id: z.union([z.string(), z.null()]),
|
||||||
task_id: z.union([z.string(), z.null()]),
|
task_id: z.union([z.string(), z.null()]),
|
||||||
start: z.string(),
|
start: z.string(),
|
||||||
end: z.union([z.string(), z.null()]),
|
end: z.union([z.string(), z.null()]),
|
||||||
billable: z.boolean(),
|
billable: z.boolean(),
|
||||||
description: z.union([z.string(), z.null()]),
|
description: z.union([z.string(), z.null()]),
|
||||||
tags: z.union([z.array(z.string().uuid()), z.null()]),
|
tags: z.union([z.array(z.string()), z.null()]),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.passthrough();
|
.passthrough();
|
||||||
@@ -370,7 +370,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'page',
|
name: 'page',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.number().int().gte(1).optional(),
|
schema: z.number().int().gte(1).lte(2147483647).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'archived',
|
name: 'archived',
|
||||||
@@ -1317,7 +1317,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'page',
|
name: 'page',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.number().int().gte(1).optional(),
|
schema: z.number().int().gte(1).lte(2147483647).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'archived',
|
name: 'archived',
|
||||||
@@ -1889,7 +1889,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'project_id',
|
name: 'project_id',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.string().uuid().optional(),
|
schema: z.string().optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'done',
|
name: 'done',
|
||||||
@@ -2118,7 +2118,7 @@ Users with the permission `time-entries:view:own` can only use this en
|
|||||||
{
|
{
|
||||||
name: 'member_id',
|
name: 'member_id',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.string().uuid().optional(),
|
schema: z.string().optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'start',
|
name: 'start',
|
||||||
@@ -2148,7 +2148,7 @@ Users with the permission `time-entries:view:own` can only use this en
|
|||||||
{
|
{
|
||||||
name: 'offset',
|
name: 'offset',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.number().int().gte(0).optional(),
|
schema: z.number().int().gte(0).lte(2147483647).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'only_full_dates',
|
name: 'only_full_dates',
|
||||||
@@ -2158,27 +2158,27 @@ Users with the permission `time-entries:view:own` can only use this en
|
|||||||
{
|
{
|
||||||
name: 'member_ids',
|
name: 'member_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string().uuid()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'client_ids',
|
name: 'client_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string().uuid()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'project_ids',
|
name: 'project_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string().uuid()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tag_ids',
|
name: 'tag_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string().uuid()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'task_ids',
|
name: 'task_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string().uuid()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'user_id',
|
name: 'user_id',
|
||||||
@@ -2524,12 +2524,12 @@ If the group parameters are all set to `null` or are all missing, the
|
|||||||
{
|
{
|
||||||
name: 'member_id',
|
name: 'member_id',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.string().uuid().optional(),
|
schema: z.string().optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'user_id',
|
name: 'user_id',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.string().uuid().optional(),
|
schema: z.string().optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'start',
|
name: 'start',
|
||||||
@@ -2559,27 +2559,27 @@ If the group parameters are all set to `null` or are all missing, the
|
|||||||
{
|
{
|
||||||
name: 'member_ids',
|
name: 'member_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string().uuid()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'project_ids',
|
name: 'project_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string().uuid()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'client_ids',
|
name: 'client_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string().uuid()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tag_ids',
|
name: 'tag_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string().uuid()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'task_ids',
|
name: 'task_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string().uuid()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z
|
response: z
|
||||||
@@ -2656,6 +2656,107 @@ If the group parameters are all set to `null` or are all missing, the
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
path: '/v1/organizations/:organization/time-entries/export',
|
||||||
|
alias: 'exportTimeEntries',
|
||||||
|
requestFormat: 'json',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'organization',
|
||||||
|
type: 'Path',
|
||||||
|
schema: z.string(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'format',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.enum(['csv', 'pdf', 'xlsx', 'ods']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'member_id',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.string().uuid().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: 'limit',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.number().int().gte(1).lte(500).optional(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'only_full_dates',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.enum(['true', 'false']).optional(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'member_ids',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.array(z.string().uuid()).min(1).optional(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'project_ids',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.array(z.string().uuid()).min(1).optional(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tag_ids',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.array(z.string().uuid()).min(1).optional(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'task_ids',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.array(z.string().uuid()).min(1).optional(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response: z.object({ download_url: z.string() }).passthrough(),
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
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/users/me',
|
path: '/v1/users/me',
|
||||||
|
|||||||
Reference in New Issue
Block a user