mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 04:02:15 +01:00
refactor timeentries queries and mutations, improve activitygraph, add dashboard reporting table
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import VChart, { THEME_KEY } from 'vue-echarts';
|
||||
import { provide, computed, inject, type ComputedRef } from 'vue';
|
||||
import { provide, computed, inject, ref, type ComputedRef } from 'vue';
|
||||
import { use } from 'echarts/core';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import DashboardCard from '@/Components/Dashboard/DashboardCard.vue';
|
||||
import { BoltIcon } from '@heroicons/vue/20/solid';
|
||||
import { HeatmapChart } from 'echarts/charts';
|
||||
@@ -12,13 +13,13 @@ import {
|
||||
VisualMapComponent,
|
||||
} from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
firstDayIndex,
|
||||
formatDate,
|
||||
formatHumanReadableDuration,
|
||||
getDayJsInstance,
|
||||
} from '@/packages/ui/src/utils/time';
|
||||
import chroma from 'chroma-js';
|
||||
import { useCssVariable } from '@/utils/useCssVariable';
|
||||
import { useQuery } from '@tanstack/vue-query';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
@@ -62,8 +63,44 @@ const max = computed(() => {
|
||||
});
|
||||
|
||||
const backgroundColor = useCssVariable('--theme-color-card-background');
|
||||
const itemBackgroundColor = useCssVariable('--color-bg-tertiary');
|
||||
const borderColor = useCssVariable('--color-border');
|
||||
const labelColor = useCssVariable('--color-text-secondary');
|
||||
const chartColorRaw = useCssVariable('--theme-color-chart');
|
||||
|
||||
const chartEmptyColorRaw = useCssVariable('--color-bg-tertiary');
|
||||
const chartEmptyColor = computed(() => {
|
||||
if (!chartEmptyColorRaw.value) return '#2a2c32';
|
||||
return chroma(chartEmptyColorRaw.value).hex();
|
||||
});
|
||||
const chartColor = computed(() => {
|
||||
if (!chartColorRaw.value) return '#bae6fd';
|
||||
return `rgb(${chartColorRaw.value})`;
|
||||
});
|
||||
|
||||
// Track chart container size
|
||||
const chartContainer = ref<HTMLElement | null>(null);
|
||||
const { width: containerWidth } = useElementSize(chartContainer);
|
||||
|
||||
// Calculate number of weeks based on available width
|
||||
// Rough estimate: 40px per cell + 80px for labels = ~360px for 7 weeks
|
||||
const numberOfWeeks = computed(() => {
|
||||
const availableWidth = containerWidth.value || 400;
|
||||
const minCellSize = 25; // Minimum cell size in pixels
|
||||
const labelSpace = 80; // Space for day labels
|
||||
const usableWidth = availableWidth - labelSpace;
|
||||
const maxWeeks = Math.floor(usableWidth / minCellSize);
|
||||
// Clamp between 4 and 12 weeks for reasonable display
|
||||
return Math.max(4, Math.min(12, maxWeeks));
|
||||
});
|
||||
|
||||
// Calculate date range based on dynamic number of weeks
|
||||
const dateRange = computed(() => {
|
||||
const today = getDayJsInstance()();
|
||||
const startOfWeek = today.startOf('week');
|
||||
// Go back (numberOfWeeks - 1) weeks from the start of current week
|
||||
const rangeStart = startOfWeek.subtract(numberOfWeeks.value - 1, 'week');
|
||||
return [today.format('YYYY-MM-DD'), rangeStart.format('YYYY-MM-DD')];
|
||||
});
|
||||
|
||||
const option = computed(() => {
|
||||
return {
|
||||
@@ -76,26 +113,30 @@ const option = computed(() => {
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
inRange: {
|
||||
color: [itemBackgroundColor.value, '#2DBE45'],
|
||||
color: [chartEmptyColor.value, chartColor.value],
|
||||
},
|
||||
show: false,
|
||||
},
|
||||
calendar: {
|
||||
top: 40,
|
||||
top: 35,
|
||||
bottom: 20,
|
||||
left: 40,
|
||||
right: 10,
|
||||
cellSize: [40, 40],
|
||||
left: 35,
|
||||
right: 5,
|
||||
cellSize: 'auto',
|
||||
orient: 'horizontal',
|
||||
dayLabel: {
|
||||
firstDay: firstDayIndex.value,
|
||||
color: labelColor.value,
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
},
|
||||
monthLabel: {
|
||||
color: labelColor.value,
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
range: [
|
||||
dayjs().format('YYYY-MM-DD'),
|
||||
getDayJsInstance()().subtract(50, 'day').startOf('week').format('YYYY-MM-DD'),
|
||||
],
|
||||
range: dateRange.value,
|
||||
itemStyle: {
|
||||
color: 'transparent',
|
||||
borderWidth: 8,
|
||||
@@ -144,7 +185,7 @@ const option = computed(() => {
|
||||
<div v-if="isLoading" class="flex justify-center items-center h-40">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
<div v-else-if="dailyHoursTracked">
|
||||
<div v-else-if="dailyHoursTracked" ref="chartContainer">
|
||||
<v-chart
|
||||
class="chart"
|
||||
:autoresize="true"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<section class="flex flex-col">
|
||||
<section class="flex overflow-hidden flex-col gap-1.5">
|
||||
<CardTitle :title="title" :icon="icon"></CardTitle>
|
||||
<div class="rounded-lg border border-card-border flex-1 flex items-stretch">
|
||||
|
||||
<div
|
||||
class="flex-1 flex items-stretch rounded-lg bg-card-background border border-card-border">
|
||||
<div class="w-full flex flex-col">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
@@ -14,17 +14,16 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-3.5 py-2 flex justify-between @container border-b border-b-background-separator">
|
||||
<div class="px-3.5 py-2 flex justify-between @container">
|
||||
<div class="flex items-center min-w-[70px]">
|
||||
<p class="font-medium text-sm text-text-primary">
|
||||
<p class="text-sm text-text-primary">
|
||||
{{ formatHumanReadableDate(date) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="items-center justify-center flex-1 hidden @2xs:flex">
|
||||
<DayOverviewCardChart :history="history"></DayOverviewCardChart>
|
||||
</div>
|
||||
<div
|
||||
class="flex text-sm items-center justify-center text-text-secondary min-w-[65px] font-medium">
|
||||
<div class="flex text-sm items-center justify-center text-text-secondary min-w-[65px]">
|
||||
{{
|
||||
formatHumanReadableDuration(
|
||||
duration,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/vue-query';
|
||||
import { computed } from 'vue';
|
||||
import RecentlyTrackedTasksCardEntry from '@/Components/Dashboard/RecentlyTrackedTasksCardEntry.vue';
|
||||
import DashboardCard from '@/Components/Dashboard/DashboardCard.vue';
|
||||
import { CheckCircleIcon } from '@heroicons/vue/20/solid';
|
||||
import { CheckCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { PlusCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { getCurrentMembershipId, getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import { api } from '@/packages/api/src';
|
||||
|
||||
@@ -45,11 +45,11 @@ async function startTaskTimer() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-3.5 py-2 grid grid-cols-5 border-b border-b-background-separator">
|
||||
<div class="px-3.5 py-2 grid grid-cols-5">
|
||||
<div class="col-span-4">
|
||||
<p class="font-medium text-text-primary text-sm pb-1 truncate">
|
||||
<p class="text-text-secondary text-sm pb-1.5 truncate">
|
||||
<span v-if="timeEntry.description"> {{ timeEntry.description }}</span>
|
||||
<span v-else class="text-text-tertiary">No description</span>
|
||||
<span v-else>No description</span>
|
||||
</p>
|
||||
<ProjectBadge size="base" class="min-w-0 max-w-full" :color="project?.color">
|
||||
<div class="flex items-center lg:space-x-0.5 min-w-0">
|
||||
|
||||
@@ -7,11 +7,11 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-4 py-2 2xl:py-3 border-b border-b-background-separator">
|
||||
<div class="px-3.5 py-2 2xl:py-3">
|
||||
<div class="col-span-2">
|
||||
<div class="flex justify-between">
|
||||
<p
|
||||
class="font-semibold text-sm min-w-0 overflow-ellipsis overflow-hidden flex-1 text-text-primary">
|
||||
class="text-xs min-w-0 overflow-ellipsis overflow-hidden flex-1 text-text-secondary">
|
||||
{{ name }}
|
||||
</p>
|
||||
<div v-if="working" class="flex space-x-1.5 items-center justify-end">
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ClockIcon } from '@heroicons/vue/20/solid';
|
||||
import CardTitle from '@/packages/ui/src/CardTitle.vue';
|
||||
import LinearGradient from 'zrender/lib/graphic/LinearGradient';
|
||||
import ProjectsChartCard from '@/Components/Dashboard/ProjectsChartCard.vue';
|
||||
import ThisWeekReportingTable from '@/Components/Dashboard/ThisWeekReportingTable.vue';
|
||||
import { formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
|
||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||
import { getWeekStart } from '@/packages/ui/src/utils/settings';
|
||||
@@ -71,6 +72,7 @@ const { data: weeklyProjectOverview } = useQuery({
|
||||
});
|
||||
},
|
||||
enabled: computed(() => !!organizationId.value),
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const { data: totalWeeklyTime } = useQuery({
|
||||
@@ -83,6 +85,7 @@ const { data: totalWeeklyTime } = useQuery({
|
||||
});
|
||||
},
|
||||
enabled: computed(() => !!organizationId.value),
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const { data: totalWeeklyBillableTime } = useQuery({
|
||||
@@ -95,6 +98,7 @@ const { data: totalWeeklyBillableTime } = useQuery({
|
||||
});
|
||||
},
|
||||
enabled: computed(() => !!organizationId.value),
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const { data: totalWeeklyBillableAmount } = useQuery({
|
||||
@@ -107,6 +111,7 @@ const { data: totalWeeklyBillableAmount } = useQuery({
|
||||
});
|
||||
},
|
||||
enabled: computed(() => !!organizationId.value),
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const { data: weeklyHistory } = useQuery({
|
||||
@@ -119,6 +124,7 @@ const { data: weeklyHistory } = useQuery({
|
||||
});
|
||||
},
|
||||
enabled: computed(() => !!organizationId.value),
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const seriesData = computed(() => {
|
||||
@@ -241,6 +247,10 @@ const option = computed(() => {
|
||||
<div class="col-span-2 xl:col-span-3">
|
||||
<CardTitle title="This Week" class="pb-8" :icon="ClockIcon"></CardTitle>
|
||||
<v-chart v-if="weeklyHistory" :autoresize="true" class="chart" :option="option" />
|
||||
|
||||
<div class="mt-6">
|
||||
<ThisWeekReportingTable></ThisWeekReportingTable>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<StatCard
|
||||
|
||||
194
resources/js/Components/Dashboard/ThisWeekReportingTable.vue
Normal file
194
resources/js/Components/Dashboard/ThisWeekReportingTable.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<script setup lang="ts">
|
||||
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
||||
import ReportingGroupBySelect from '@/Components/Common/Reporting/ReportingGroupBySelect.vue';
|
||||
import {
|
||||
formatHumanReadableDuration,
|
||||
getDayJsInstance,
|
||||
getLocalizedDayJs,
|
||||
} from '@/packages/ui/src/utils/time';
|
||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||
import { type GroupingOption, useReportingStore } from '@/utils/useReporting';
|
||||
import { getCurrentMembershipId, getCurrentOrganizationId, getCurrentRole } from '@/utils/useUser';
|
||||
import {
|
||||
api,
|
||||
type AggregatedTimeEntries,
|
||||
type AggregatedTimeEntriesQueryParams,
|
||||
type Organization,
|
||||
} from '@/packages/api/src';
|
||||
import { useQuery } from '@tanstack/vue-query';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { computed, inject, type ComputedRef, watch } from 'vue';
|
||||
|
||||
const organization = inject<ComputedRef<Organization>>('organization');
|
||||
|
||||
const group = useStorage<GroupingOption>('dashboard-reporting-group', 'project');
|
||||
const subGroup = useStorage<GroupingOption>('dashboard-reporting-sub-group', 'task');
|
||||
|
||||
const reportingStore = useReportingStore();
|
||||
const { groupByOptions, getNameForReportingRowEntry } = reportingStore;
|
||||
|
||||
watch(
|
||||
group,
|
||||
() => {
|
||||
if (group.value === subGroup.value) {
|
||||
const fallbackOption = groupByOptions.find((el) => el.value !== group.value);
|
||||
if (fallbackOption?.value) {
|
||||
subGroup.value = fallbackOption.value;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const organizationId = computed(() => getCurrentOrganizationId());
|
||||
|
||||
const weekStartUtc = computed(() => {
|
||||
return getLocalizedDayJs(getDayJsInstance()().format())
|
||||
.startOf('week')
|
||||
.startOf('day')
|
||||
.utc()
|
||||
.format();
|
||||
});
|
||||
|
||||
const weekEndUtc = computed(() => {
|
||||
return getLocalizedDayJs(getDayJsInstance()().format()).endOf('day').utc().format();
|
||||
});
|
||||
|
||||
const queryParams = computed<AggregatedTimeEntriesQueryParams>(() => {
|
||||
return {
|
||||
start: weekStartUtc.value,
|
||||
end: weekEndUtc.value,
|
||||
group: group.value,
|
||||
sub_group: subGroup.value,
|
||||
member_id: getCurrentRole() === 'employee' ? getCurrentMembershipId() : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const { data: reportingResponse, isLoading } = useQuery({
|
||||
queryKey: [
|
||||
'dashboardThisWeekReporting',
|
||||
organizationId,
|
||||
weekStartUtc,
|
||||
weekEndUtc,
|
||||
group,
|
||||
subGroup,
|
||||
],
|
||||
queryFn: () => {
|
||||
return api.getAggregatedTimeEntries({
|
||||
params: {
|
||||
organization: organizationId.value!,
|
||||
},
|
||||
queries: queryParams.value,
|
||||
});
|
||||
},
|
||||
enabled: computed(() => !!organizationId.value),
|
||||
});
|
||||
|
||||
const aggregatedTableTimeEntries = computed<AggregatedTimeEntries | null>(() => {
|
||||
return (reportingResponse.value?.data as AggregatedTimeEntries | undefined) ?? null;
|
||||
});
|
||||
|
||||
const tableData = computed(() => {
|
||||
return (
|
||||
aggregatedTableTimeEntries.value?.grouped_data?.map((entry) => {
|
||||
return {
|
||||
seconds: entry.seconds,
|
||||
cost: entry.cost,
|
||||
description: getNameForReportingRowEntry(
|
||||
entry.key,
|
||||
aggregatedTableTimeEntries.value?.grouped_type ?? null
|
||||
),
|
||||
grouped_data:
|
||||
entry.grouped_data?.map((el) => {
|
||||
return {
|
||||
seconds: el.seconds,
|
||||
cost: el.cost,
|
||||
description: getNameForReportingRowEntry(
|
||||
el.key,
|
||||
entry.grouped_type ?? null
|
||||
),
|
||||
};
|
||||
}) ?? [],
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-lg bg-card-background border border-card-border">
|
||||
<div
|
||||
class="text-sm flex text-text-primary pt-3 items-center space-x-3 font-medium px-6 border-b border-card-background-separator pb-3">
|
||||
<span>Group by</span>
|
||||
<ReportingGroupBySelect
|
||||
v-model="group"
|
||||
:group-by-options="groupByOptions"></ReportingGroupBySelect>
|
||||
<span>and</span>
|
||||
<ReportingGroupBySelect
|
||||
v-model="subGroup"
|
||||
:group-by-options="
|
||||
groupByOptions.filter((el) => el.value !== group)
|
||||
"></ReportingGroupBySelect>
|
||||
</div>
|
||||
|
||||
<div class="grid items-center" style="grid-template-columns: 1fr 100px 150px">
|
||||
<div
|
||||
class="contents [&>*]:border-card-background-separator [&>*]:border-b [&>*]:pb-1.5 [&>*]:pt-1 text-text-tertiary text-sm">
|
||||
<div class="pl-6">Name</div>
|
||||
<div class="text-right">Duration</div>
|
||||
<div class="text-right pr-6">Cost</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="flex justify-center py-10 col-span-3 text-text-tertiary">
|
||||
Loading reporting data…
|
||||
</div>
|
||||
|
||||
<template
|
||||
v-else-if="
|
||||
aggregatedTableTimeEntries?.grouped_data &&
|
||||
aggregatedTableTimeEntries.grouped_data?.length > 0
|
||||
">
|
||||
<ReportingRow
|
||||
v-for="entry in tableData"
|
||||
:key="entry.description ?? 'none'"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:entry="entry"></ReportingRow>
|
||||
<div class="contents [&>*]:transition text-text-tertiary [&>*]:h-[50px]">
|
||||
<div class="flex items-center pl-6 font-medium">
|
||||
<span>Total</span>
|
||||
</div>
|
||||
<div class="justify-end flex items-center font-medium">
|
||||
{{
|
||||
formatHumanReadableDuration(
|
||||
aggregatedTableTimeEntries.seconds,
|
||||
organization?.interval_format,
|
||||
organization?.number_format
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="justify-end pr-6 flex items-center font-medium">
|
||||
{{
|
||||
aggregatedTableTimeEntries.cost
|
||||
? formatCents(
|
||||
aggregatedTableTimeEntries.cost,
|
||||
getOrganizationCurrencyString(),
|
||||
organization?.currency_format,
|
||||
organization?.currency_symbol,
|
||||
organization?.number_format
|
||||
)
|
||||
: '--'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="chart flex flex-col items-center justify-center py-12 col-span-3">
|
||||
<p class="text-lg text-text-primary font-medium">No time entries found</p>
|
||||
<p>Try to track some time entries this week</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -15,7 +15,7 @@ const classes = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Link :href="href ?? ''" :class="classes" prefetch>
|
||||
<Link :href="href ?? ''" :class="classes" prefetch="mount">
|
||||
<slot />
|
||||
</Link>
|
||||
</template>
|
||||
|
||||
@@ -34,10 +34,9 @@ import { getOrganizationCurrencyString } from '@/utils/money';
|
||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
import { canCreateProjects } from '@/utils/permissions';
|
||||
import { ref } from 'vue';
|
||||
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import { useTimeEntriesMutations } from '@/utils/useTimeEntriesMutations';
|
||||
import { useTimeEntriesInfiniteQuery } from '@/utils/useTimeEntriesInfiniteQuery';
|
||||
|
||||
const page = usePage<{
|
||||
auth: {
|
||||
@@ -62,6 +61,10 @@ const emit = defineEmits<{
|
||||
|
||||
const showManualTimeEntryModal = ref(false);
|
||||
|
||||
const { createTimeEntry: createTimeEntryMutation, deleteTimeEntry } = useTimeEntriesMutations();
|
||||
const { data: timeEntriesData } = useTimeEntriesInfiniteQuery();
|
||||
const timeEntries = computed(() => timeEntriesData.value?.pages.flatMap((page) => page.data) || []);
|
||||
|
||||
watch(isActive, () => {
|
||||
if (isActive.value) {
|
||||
startLiveTimer();
|
||||
@@ -113,7 +116,7 @@ async function createTag(tag: string): Promise<Tag | undefined> {
|
||||
}
|
||||
|
||||
async function createTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>) {
|
||||
await useTimeEntriesStore().createTimeEntry(timeEntry);
|
||||
await createTimeEntryMutation(timeEntry);
|
||||
showManualTimeEntryModal.value = false;
|
||||
}
|
||||
|
||||
@@ -124,41 +127,19 @@ async function createTimeEntryFromCurrentEntry() {
|
||||
}
|
||||
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const deleteTimeEntryMutation = useMutation({
|
||||
mutationFn: async (timeEntryId: string) => {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId) {
|
||||
throw new Error('No organization selected');
|
||||
}
|
||||
return await api.deleteTimeEntry(undefined, {
|
||||
params: {
|
||||
organization: organizationId,
|
||||
timeEntry: timeEntryId,
|
||||
},
|
||||
});
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await currentTimeEntryStore.fetchCurrentTimeEntry();
|
||||
await useTimeEntriesStore().fetchTimeEntries();
|
||||
queryClient.invalidateQueries({ queryKey: ['timeEntry'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['timeEntries'] });
|
||||
},
|
||||
});
|
||||
|
||||
async function discardCurrentTimeEntry() {
|
||||
if (currentTimeEntry.value.id) {
|
||||
await handleApiRequestNotifications(
|
||||
() => deleteTimeEntryMutation.mutateAsync(currentTimeEntry.value.id),
|
||||
() => deleteTimeEntry(currentTimeEntry.value.id),
|
||||
'Time entry discarded successfully',
|
||||
'Failed to discard time entry'
|
||||
);
|
||||
await currentTimeEntryStore.fetchCurrentTimeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
const { tags } = useTagsQuery();
|
||||
const { timeEntries } = storeToRefs(useTimeEntriesStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -176,7 +157,7 @@ const { timeEntries } = storeToRefs(useTimeEntriesStore());
|
||||
:tags
|
||||
:clients></TimeEntryCreateModal>
|
||||
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
|
||||
<div class="relative">
|
||||
<div class="relative pt-1">
|
||||
<TimeTrackerRunningInDifferentOrganizationOverlay
|
||||
v-if="isRunningInDifferentOrganization"
|
||||
@switch-organization="
|
||||
|
||||
Reference in New Issue
Block a user