refactor: extract ReportingFilterBar and migrate reporting to TanStack Query

This commit is contained in:
Gregor Vostrak
2026-02-02 01:03:00 +01:00
parent 756b423295
commit bc562bf76f
6 changed files with 139 additions and 404 deletions
@@ -1,7 +1,12 @@
<script setup lang="ts">
import { SecondaryButton } from '@/packages/ui/src';
import { ArrowDownTrayIcon, LockClosedIcon } from '@heroicons/vue/20/solid';
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/Components/ui/dropdown-menu';
import type { ExportFormat } from '@/types/reporting';
import { ref } from 'vue';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
@@ -25,32 +30,24 @@ function triggerDownload(format: ExportFormat) {
</script>
<template>
<Dropdown align="end">
<template #trigger>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SecondaryButton :icon="ArrowDownTrayIcon" :loading> Export </SecondaryButton>
</template>
<template #content>
<div class="flex flex-col space-y-1 p-1.5">
<SecondaryButton class="border-0 px-2" @click="triggerDownload('pdf')">
<div class="flex items-center space-x-2">
<span> Export as PDF </span>
<LockClosedIcon
v-if="!isAllowedToPerformPremiumAction()"
class="w-3.5 text-text-tertiary"></LockClosedIcon>
</div>
</SecondaryButton>
<SecondaryButton class="border-0 px-2" @click="triggerDownload('xlsx')"
>Export as Excel</SecondaryButton
>
<SecondaryButton class="border-0 px-2" @click="triggerDownload('csv')"
>Export as CSV</SecondaryButton
>
<SecondaryButton class="border-0 px-2" @click="triggerDownload('ods')"
>Export as ODS
</SecondaryButton>
</div>
</template>
</Dropdown>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="triggerDownload('pdf')">
<div class="flex items-center space-x-2">
<span>Export as PDF</span>
<LockClosedIcon
v-if="!isAllowedToPerformPremiumAction()"
class="w-3.5 text-text-tertiary" />
</div>
</DropdownMenuItem>
<DropdownMenuItem @click="triggerDownload('xlsx')"> Export as Excel </DropdownMenuItem>
<DropdownMenuItem @click="triggerDownload('csv')"> Export as CSV </DropdownMenuItem>
<DropdownMenuItem @click="triggerDownload('ods')"> Export as ODS </DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<UpgradeModal v-model:show="showPremiumModal">
<strong>PDF Reports</strong> are only available in solidtime Professional.
</UpgradeModal>
@@ -1,12 +1,20 @@
<script setup lang="ts">
import SelectDropdown from '@/packages/ui/src/Input/SelectDropdown.vue';
import Badge from '@/packages/ui/src/Badge.vue';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { type Component, computed } from 'vue';
const model = defineModel<string | null>({ default: null });
const props = defineProps<{
groupByOptions: { value: string; label: string; icon: Component }[];
}>();
const emit = defineEmits<{
changed: [];
}>();
const icon = computed(() => {
return props.groupByOptions.find((option) => option.value === model.value)?.icon;
});
@@ -16,21 +24,19 @@ const title = computed(() => {
</script>
<template>
<SelectDropdown
v-model="model"
:get-key-from-item="(item) => item.value"
:get-name-for-item="(item) => item.label"
:items="groupByOptions">
<template #trigger>
<Badge
size="large"
tag="button"
class="cursor-pointer hover:bg-card-background transition space-x-5 flex">
<component :is="icon" class="h-4 text-text-secondary"></component>
<span> {{ title }} </span>
</Badge>
</template>
</SelectDropdown>
<Select v-model="model" @update:model-value="emit('changed')">
<SelectTrigger size="small" :show-chevron="false">
<SelectValue class="flex items-center gap-2">
<component :is="icon" class="h-4 text-icon-default" />
<span>{{ title }}</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="option in groupByOptions" :key="option.value" :value="option.value">
{{ option.label }}
</SelectItem>
</SelectContent>
</Select>
</template>
<style scoped></style>
@@ -1,7 +1,5 @@
<script setup lang="ts">
import { ChartBarIcon, CheckCircleIcon, TagIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
import { FolderIcon } from '@heroicons/vue/16/solid';
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
import { ChartBarIcon } from '@heroicons/vue/20/solid';
import { getOrganizationCurrencyString } from '@/utils/money';
import {
formatHumanReadableDuration,
@@ -11,43 +9,33 @@ import {
import { formatCents } from '@/packages/ui/src/utils/money';
import ReportingTabNavbar from '@/Components/Common/Reporting/ReportingTabNavbar.vue';
import ReportingExportButton from '@/Components/Common/Reporting/ReportingExportButton.vue';
import ReportingRoundingControls from '@/Components/Common/Reporting/ReportingRoundingControls.vue';
import TaskMultiselectDropdown from '@/Components/Common/Task/TaskMultiselectDropdown.vue';
import ClientMultiselectDropdown from '@/Components/Common/Client/ClientMultiselectDropdown.vue';
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
import MemberMultiselectDropdown from '@/Components/Common/Member/MemberMultiselectDropdown.vue';
import ReportingFilterBadge from '@/Components/Common/Reporting/ReportingFilterBadge.vue';
import PageTitle from '@/Components/Common/PageTitle.vue';
import ProjectMultiselectDropdown from '@/Components/Common/Project/ProjectMultiselectDropdown.vue';
import ReportingChart from '@/Components/Common/Reporting/ReportingChart.vue';
import SelectDropdown from '../../../packages/ui/src/Input/SelectDropdown.vue';
import ReportingGroupBySelect from '@/Components/Common/Reporting/ReportingGroupBySelect.vue';
import MainContainer from '@/packages/ui/src/MainContainer.vue';
import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue';
import ReportingExportModal from '@/Components/Common/Reporting/ReportingExportModal.vue';
import ReportSaveButton from '@/Components/Common/Report/ReportSaveButton.vue';
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
import ReportingFilterBar from '@/Components/Common/Reporting/ReportingFilterBar.vue';
import { computed, type ComputedRef, inject, onMounted, ref, watch } from 'vue';
import { computed, type ComputedRef, inject, ref, watch } from 'vue';
import { type GroupingOption, useReportingStore } from '@/utils/useReporting';
import { storeToRefs } from 'pinia';
import {
type AggregatedTimeEntries,
type AggregatedTimeEntriesQueryParams,
api,
type CreateReportBodyProperties,
type Organization,
} from '@/packages/api/src';
import { getCurrentMembershipId, getCurrentOrganizationId, getCurrentRole } from '@/utils/useUser';
import { useTagsQuery } from '@/utils/useTagsQuery';
import { useTagsStore } from '@/utils/useTags';
import { useSessionStorage, useStorage } from '@vueuse/core';
import { useNotificationsStore } from '@/utils/notification';
import type { ExportFormat } from '@/types/reporting';
import { getRandomColorWithSeed } from '@/packages/ui/src/utils/color';
import { useProjectsQuery } from '@/utils/useProjectsQuery';
import { useAggregatedTimeEntriesQuery } from '@/utils/useAggregatedTimeEntriesQuery';
// TimeEntryRoundingType is now defined in ReportingRoundingControls component
type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
const { handleApiRequestNotifications } = useNotificationsStore();
@@ -75,71 +63,26 @@ const group = useStorage<GroupingOption>('reporting-group', 'project');
const subGroup = useStorage<GroupingOption>('reporting-sub-group', 'task');
const reportingStore = useReportingStore();
const { aggregatedGraphTimeEntries, aggregatedTableTimeEntries } = storeToRefs(reportingStore);
const { groupByOptions } = reportingStore;
const { groupByOptions, getNameForReportingRowEntry, emptyPlaceholder } = reportingStore;
const organization = inject<ComputedRef<Organization>>('organization');
// Watch rounding enabled state to trigger updates
watch(roundingEnabled, () => {
updateReporting();
});
function getFilterAttributes(): AggregatedTimeEntriesQueryParams {
let params: AggregatedTimeEntriesQueryParams = {
start: getLocalizedDayJs(startDate.value).startOf('day').utc().format(),
end: getLocalizedDayJs(endDate.value).endOf('day').utc().format(),
};
params = {
...params,
member_ids: selectedMembers.value.length > 0 ? selectedMembers.value : undefined,
project_ids: selectedProjects.value.length > 0 ? selectedProjects.value : undefined,
task_ids: selectedTasks.value.length > 0 ? selectedTasks.value : undefined,
client_ids: selectedClients.value.length > 0 ? selectedClients.value : undefined,
tag_ids: selectedTags.value.length > 0 ? selectedTags.value : undefined,
billable: billable.value !== null ? billable.value : undefined,
member_id: getCurrentRole() === 'employee' ? getCurrentMembershipId() : undefined,
rounding_type: roundingEnabled.value ? roundingType.value : undefined,
rounding_minutes: roundingEnabled.value ? roundingMinutes.value : undefined,
};
return params;
}
function updateGraphReporting() {
const params = getFilterAttributes();
if (getCurrentRole() === 'employee') {
params.member_id = getCurrentMembershipId();
}
params.fill_gaps_in_time_groups = 'true';
params.group = getOptimalGroupingOption(startDate.value, endDate.value);
useReportingStore().fetchGraphReporting(params);
}
function updateTableReporting() {
const params = getFilterAttributes();
if (group.value === subGroup.value) {
const fallbackOption = groupByOptions.find((el) => el.value !== group.value);
if (fallbackOption?.value) {
subGroup.value = fallbackOption.value;
// Ensure sub-group falls back when it collides with group
watch(
group,
() => {
if (group.value === subGroup.value) {
const fallbackOption = groupByOptions.find((el) => el.value !== group.value);
if (fallbackOption?.value) {
subGroup.value = fallbackOption.value;
}
}
}
if (getCurrentRole() === 'employee') {
params.member_id = getCurrentMembershipId();
}
params.group = group.value;
params.sub_group = subGroup.value;
useReportingStore().fetchTableReporting(params);
}
},
{ immediate: true }
);
function updateReporting() {
updateGraphReporting();
updateTableReporting();
}
function getOptimalGroupingOption(startDate: string, endDate: string): 'day' | 'week' | 'month' {
const diffInDays = getDayJsInstance()(endDate).diff(getDayJsInstance()(startDate), 'd');
function getOptimalGroupingOption(start: string, end: string): 'day' | 'week' | 'month' {
const diffInDays = getDayJsInstance()(end).diff(getDayJsInstance()(start), 'd');
if (diffInDays <= 31) {
return 'day';
@@ -150,20 +93,52 @@ function getOptimalGroupingOption(startDate: string, endDate: string): 'day' | '
}
}
onMounted(() => {
updateGraphReporting();
updateTableReporting();
const filterParams = computed<AggregatedTimeEntriesQueryParams>(() => {
return {
start: getLocalizedDayJs(startDate.value).startOf('day').utc().format(),
end: getLocalizedDayJs(endDate.value).endOf('day').utc().format(),
member_ids: selectedMembers.value.length > 0 ? selectedMembers.value : undefined,
project_ids: selectedProjects.value.length > 0 ? selectedProjects.value : undefined,
task_ids: selectedTasks.value.length > 0 ? selectedTasks.value : undefined,
client_ids: selectedClients.value.length > 0 ? selectedClients.value : undefined,
tag_ids: selectedTags.value.length > 0 ? selectedTags.value : undefined,
billable: billable.value !== null ? billable.value : undefined,
member_id: getCurrentRole() === 'employee' ? getCurrentMembershipId() : undefined,
rounding_type: roundingEnabled.value ? roundingType.value : undefined,
rounding_minutes: roundingEnabled.value ? roundingMinutes.value : undefined,
};
});
const { tags } = useTagsQuery();
const graphQueryParams = computed<AggregatedTimeEntriesQueryParams>(() => {
return {
...filterParams.value,
fill_gaps_in_time_groups: 'true',
group: getOptimalGroupingOption(startDate.value, endDate.value),
};
});
async function createTag(tag: string) {
return await useTagsStore().createTag(tag);
}
const tableQueryParams = computed<AggregatedTimeEntriesQueryParams>(() => {
return {
...filterParams.value,
group: group.value,
sub_group: subGroup.value,
};
});
const { data: graphResponse } = useAggregatedTimeEntriesQuery('graph', graphQueryParams);
const { data: tableResponse } = useAggregatedTimeEntriesQuery('table', tableQueryParams);
const aggregatedGraphTimeEntries = computed<AggregatedTimeEntries | undefined>(() => {
return graphResponse.value?.data as AggregatedTimeEntries | undefined;
});
const aggregatedTableTimeEntries = computed<AggregatedTimeEntries | undefined>(() => {
return tableResponse.value?.data as AggregatedTimeEntries | undefined;
});
const reportProperties = computed(() => {
return {
...getFilterAttributes(),
...filterParams.value,
group: group.value,
sub_group: subGroup.value,
history_group: getOptimalGroupingOption(startDate.value, endDate.value),
@@ -180,7 +155,7 @@ async function downloadExport(format: ExportFormat) {
organization: organizationId,
},
queries: {
...getFilterAttributes(),
...filterParams.value,
group: group.value,
sub_group: subGroup.value,
history_group: getOptimalGroupingOption(startDate.value, endDate.value),
@@ -198,8 +173,6 @@ async function downloadExport(format: ExportFormat) {
}
}
const { getNameForReportingRowEntry, emptyPlaceholder } = useReportingStore();
const { projects } = useProjectsQuery();
const showExportModal = ref(false);
const exportUrl = ref<string | null>(null);
@@ -272,100 +245,19 @@ const tableData = computed(() => {
<ReportSaveButton :report-properties="reportProperties"></ReportSaveButton>
</div>
</MainContainer>
<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 class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-3">
<div class="text-sm font-medium">Filters</div>
<MemberMultiselectDropdown v-model="selectedMembers" @submit="updateReporting">
<template #trigger>
<ReportingFilterBadge
:count="selectedMembers.length"
:active="selectedMembers.length > 0"
title="Members"
:icon="UserGroupIcon"></ReportingFilterBadge>
</template>
</MemberMultiselectDropdown>
<ProjectMultiselectDropdown v-model="selectedProjects" @submit="updateReporting">
<template #trigger>
<ReportingFilterBadge
:count="selectedProjects.length"
:active="selectedProjects.length > 0"
title="Projects"
:icon="FolderIcon"></ReportingFilterBadge>
</template>
</ProjectMultiselectDropdown>
<TaskMultiselectDropdown v-model="selectedTasks" @submit="updateReporting">
<template #trigger>
<ReportingFilterBadge
:count="selectedTasks.length"
:active="selectedTasks.length > 0"
title="Tasks"
:icon="CheckCircleIcon"></ReportingFilterBadge>
</template>
</TaskMultiselectDropdown>
<ClientMultiselectDropdown v-model="selectedClients" @submit="updateReporting">
<template #trigger>
<ReportingFilterBadge
:count="selectedClients.length"
:active="selectedClients.length > 0"
title="Clients"
:icon="FolderIcon"></ReportingFilterBadge>
</template>
</ClientMultiselectDropdown>
<TagDropdown
v-model="selectedTags"
:create-tag
:tags="tags"
@submit="updateReporting">
<template #trigger>
<ReportingFilterBadge
:count="selectedTags.length"
:active="selectedTags.length > 0"
title="Tags"
:icon="TagIcon"></ReportingFilterBadge>
</template>
</TagDropdown>
<SelectDropdown
v-model="billable"
:get-key-from-item="(item) => item.value"
:get-name-for-item="(item) => item.label"
:items="[
{
label: 'Both',
value: null,
},
{
label: 'Billable',
value: 'true',
},
{
label: 'Non Billable',
value: 'false',
},
]"
@changed="updateReporting">
<template #trigger>
<ReportingFilterBadge
:active="billable !== null"
:title="billable === 'false' ? 'Non Billable' : 'Billable'"
:icon="BillableIcon"></ReportingFilterBadge>
</template>
</SelectDropdown>
<ReportingRoundingControls
v-model:enabled="roundingEnabled"
v-model:type="roundingType"
v-model:minutes="roundingMinutes"
@change="updateReporting"></ReportingRoundingControls>
</div>
<div>
<DateRangePicker
v-model:start="startDate"
v-model:end="endDate"
@submit="updateReporting"></DateRangePicker>
</div>
</MainContainer>
</div>
<ReportingFilterBar
v-model:selected-members="selectedMembers"
v-model:selected-projects="selectedProjects"
v-model:selected-tasks="selectedTasks"
v-model:selected-clients="selectedClients"
v-model:selected-tags="selectedTags"
v-model:billable="billable"
v-model:rounding-enabled="roundingEnabled"
v-model:rounding-type="roundingType"
v-model:rounding-minutes="roundingMinutes"
v-model:start-date="startDate"
v-model:end-date="endDate"
/>
<MainContainer>
<div class="pt-10 w-full px-3 relative">
<ReportingChart
@@ -382,12 +274,12 @@ const tableData = computed(() => {
<ReportingGroupBySelect
v-model="group"
:group-by-options="groupByOptions"
@changed="updateTableReporting"></ReportingGroupBySelect>
></ReportingGroupBySelect>
<span>and</span>
<ReportingGroupBySelect
v-model="subGroup"
:group-by-options="groupByOptions.filter((el) => el.value !== group)"
@changed="updateTableReporting"></ReportingGroupBySelect>
></ReportingGroupBySelect>
</div>
<div class="grid items-center" style="grid-template-columns: 1fr 100px 150px">
<div
+1 -1
View File
@@ -22,7 +22,7 @@ import {
import NavigationSidebarItem from '@/Components/NavigationSidebarItem.vue';
import UserSettingsIcon from '@/Components/UserSettingsIcon.vue';
import MainContainer from '@/packages/ui/src/MainContainer.vue';
import { computed, onMounted, provide, ref } from 'vue';
import { onMounted, provide, ref } from 'vue';
import NotificationContainer from '@/Components/NotificationContainer.vue';
import { initializeStores } from '@/utils/init';
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
+14 -116
View File
@@ -1,26 +1,18 @@
<script setup lang="ts">
import MainContainer from '@/packages/ui/src/MainContainer.vue';
import AppLayout from '@/Layouts/AppLayout.vue';
import { FolderIcon } from '@heroicons/vue/16/solid';
import PageTitle from '@/Components/Common/PageTitle.vue';
import {
ChartBarIcon,
UserGroupIcon,
CheckCircleIcon,
TagIcon,
ChevronLeftIcon,
ChevronDoubleLeftIcon,
ChevronRightIcon,
ChevronDoubleRightIcon,
ClockIcon,
} from '@heroicons/vue/20/solid';
import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue';
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
import ReportingRoundingControls from '@/Components/Common/Reporting/ReportingRoundingControls.vue';
import { computed, onMounted, ref, watch } from 'vue';
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
import { storeToRefs } from 'pinia';
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
import {
api,
type Client,
@@ -29,12 +21,6 @@ import {
type Project,
type TimeEntry,
} from '@/packages/api/src';
import ReportingFilterBadge from '@/Components/Common/Reporting/ReportingFilterBadge.vue';
import ProjectMultiselectDropdown from '@/Components/Common/Project/ProjectMultiselectDropdown.vue';
import MemberMultiselectDropdown from '@/Components/Common/Member/MemberMultiselectDropdown.vue';
import TaskMultiselectDropdown from '@/Components/Common/Task/TaskMultiselectDropdown.vue';
import SelectDropdown from '@/packages/ui/src/Input/SelectDropdown.vue';
import ClientMultiselectDropdown from '@/Components/Common/Client/ClientMultiselectDropdown.vue';
import { useTagsQuery } from '@/utils/useTagsQuery';
import { useTagsStore } from '@/utils/useTags';
import { useSessionStorage } from '@vueuse/core';
@@ -67,6 +53,7 @@ import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassAct
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
import { canCreateProjects, canViewAllTimeEntries } from '@/utils/permissions';
import ReportingExportModal from '@/Components/Common/Reporting/ReportingExportModal.vue';
import ReportingFilterBar from '@/Components/Common/Reporting/ReportingFilterBar.vue';
import { useTimeEntriesReportQuery } from '@/utils/useTimeEntriesReportQuery';
import { useTimeEntriesMutations } from '@/utils/useTimeEntriesMutations';
@@ -258,108 +245,19 @@ async function downloadExport(format: ExportFormat) {
<ReportingExportButton :download="downloadExport"></ReportingExportButton>
</MainContainer>
<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 class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-3">
<div class="text-sm font-medium">Filters</div>
<MemberMultiselectDropdown
v-model="selectedMembers"
@submit="updateFilteredTimeEntries">
<template #trigger>
<ReportingFilterBadge
:count="selectedMembers.length"
:active="selectedMembers.length > 0"
title="Members"
:icon="UserGroupIcon"></ReportingFilterBadge>
</template>
</MemberMultiselectDropdown>
<ProjectMultiselectDropdown
v-model="selectedProjects"
@submit="updateFilteredTimeEntries">
<template #trigger>
<ReportingFilterBadge
:count="selectedProjects.length"
:active="selectedProjects.length > 0"
title="Projects"
:icon="FolderIcon"></ReportingFilterBadge>
</template>
</ProjectMultiselectDropdown>
<TaskMultiselectDropdown
v-model="selectedTasks"
@submit="updateFilteredTimeEntries">
<template #trigger>
<ReportingFilterBadge
:count="selectedTasks.length"
:active="selectedTasks.length > 0"
title="Tasks"
:icon="CheckCircleIcon"></ReportingFilterBadge>
</template>
</TaskMultiselectDropdown>
<ClientMultiselectDropdown
v-model="selectedClients"
@submit="updateFilteredTimeEntries">
<template #trigger>
<ReportingFilterBadge
:count="selectedClients.length"
:active="selectedClients.length > 0"
title="Clients"
:icon="FolderIcon"></ReportingFilterBadge>
</template>
</ClientMultiselectDropdown>
<TagDropdown
v-model="selectedTags"
:create-tag
:tags="tags"
@submit="updateFilteredTimeEntries">
<template #trigger>
<ReportingFilterBadge
:count="selectedTags.length"
:active="selectedTags.length > 0"
title="Tags"
:icon="TagIcon"></ReportingFilterBadge>
</template>
</TagDropdown>
<SelectDropdown
v-model="billable"
:get-key-from-item="(item) => item.value"
:get-name-for-item="(item) => item.label"
:items="[
{
label: 'Both',
value: null,
},
{
label: 'Billable',
value: 'true',
},
{
label: 'Non Billable',
value: 'false',
},
]"
@changed="updateFilteredTimeEntries">
<template #trigger>
<ReportingFilterBadge
:active="billable !== null"
:title="billable === 'false' ? 'Non Billable' : 'Billable'"
:icon="BillableIcon"></ReportingFilterBadge>
</template>
</SelectDropdown>
<ReportingRoundingControls
v-model:enabled="roundingEnabled"
v-model:type="roundingType"
v-model:minutes="roundingMinutes"
@change="updateFilteredTimeEntries" />
</div>
<div>
<DateRangePicker
v-model:start="startDate"
v-model:end="endDate"
@submit="updateFilteredTimeEntries"></DateRangePicker>
</div>
</MainContainer>
</div>
<ReportingFilterBar
v-model:selected-members="selectedMembers"
v-model:selected-projects="selectedProjects"
v-model:selected-tasks="selectedTasks"
v-model:selected-clients="selectedClients"
v-model:selected-tags="selectedTags"
v-model:billable="billable"
v-model:rounding-enabled="roundingEnabled"
v-model:rounding-type="roundingType"
v-model:rounding-minutes="roundingMinutes"
v-model:start-date="startDate"
v-model:end-date="endDate"
@submit="updateFilteredTimeEntries" />
<TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries"
:can-create-project="canCreateProjects()"
+2 -60
View File
@@ -1,13 +1,6 @@
import { defineStore } from 'pinia';
import { api } from '@/packages/api/src';
import { type Component, computed, ref } from 'vue';
import type {
AggregatedTimeEntries,
AggregatedTimeEntriesQueryParams,
ReportingResponse,
} from '@/packages/api/src';
import { getCurrentOrganizationId, getCurrentRole, getCurrentUser } from '@/utils/useUser';
import { useNotificationsStore } from '@/utils/notification';
import { type Component } from 'vue';
import { getCurrentRole, getCurrentUser } from '@/utils/useUser';
import { useProjectsQuery } from '@/utils/useProjectsQuery';
import { useMembersQuery } from '@/utils/useMembersQuery';
import { useTasksQuery } from '@/utils/useTasksQuery';
@@ -27,11 +20,6 @@ export type GroupingOption =
| 'tag';
export const useReportingStore = defineStore('reporting', () => {
const reportingGraphResponse = ref<ReportingResponse | null>(null);
const reportingTableResponse = ref<ReportingResponse | null>(null);
const { handleApiRequestNotifications } = useNotificationsStore();
// Cache query composables to avoid creating new subscriptions on every call
const { projects } = useProjectsQuery();
const { members } = useMembersQuery();
@@ -39,48 +27,6 @@ export const useReportingStore = defineStore('reporting', () => {
const { clients } = useClientsQuery();
const { tags } = useTagsQuery();
async function fetchGraphReporting(params: AggregatedTimeEntriesQueryParams) {
const organization = getCurrentOrganizationId();
if (organization) {
reportingGraphResponse.value = await handleApiRequestNotifications(
() =>
api.getAggregatedTimeEntries({
params: {
organization: organization,
},
queries: params,
}),
undefined,
'Failed to fetch reporting data'
);
}
}
async function fetchTableReporting(params: AggregatedTimeEntriesQueryParams) {
const organization = getCurrentOrganizationId();
if (organization) {
reportingTableResponse.value = await handleApiRequestNotifications(
() =>
api.getAggregatedTimeEntries({
params: {
organization: organization,
},
queries: params,
}),
undefined,
'Failed to fetch reporting data'
);
}
}
const aggregatedGraphTimeEntries = computed<AggregatedTimeEntries>(() => {
return reportingGraphResponse.value?.data as AggregatedTimeEntries;
});
const aggregatedTableTimeEntries = computed<AggregatedTimeEntries>(() => {
return reportingTableResponse.value?.data as AggregatedTimeEntries;
});
const emptyPlaceholder = {
user: 'No User',
project: 'No Project',
@@ -170,10 +116,6 @@ export const useReportingStore = defineStore('reporting', () => {
];
return {
aggregatedGraphTimeEntries,
fetchGraphReporting,
fetchTableReporting,
aggregatedTableTimeEntries,
getNameForReportingRowEntry,
groupByOptions,
emptyPlaceholder,