mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-05-07 20:32:26 +00:00
refactor: extract ReportingFilterBar and migrate reporting to TanStack Query
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user