Skip to content

Commit 55d12aa

Browse files
committed
add discard option for running timer
1 parent 9a1dd48 commit 55d12aa

File tree

5 files changed

+171
-54
lines changed

5 files changed

+171
-54
lines changed

e2e/organization.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ async function goToOrganizationSettings(page) {
99

1010
async function createTimeEntry(page, duration: string) {
1111
await page.goto(PLAYWRIGHT_BASE_URL + '/time');
12-
await page.getByRole('button', { name: 'Manual time entry' }).click();
12+
13+
// Open the dropdown menu and click "Manual time entry"
14+
await page.getByRole('button', { name: 'Time entry actions' }).click();
15+
await page.getByRole('menuitem', { name: 'Manual time entry' }).click();
1316

1417
// Fill in the time entry details
1518
await page.getByTestId('time_entry_description').fill('Test time entry');

e2e/reporting.spec.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat
2626

2727
// Then create the time entry
2828
await goToTimeOverview(page);
29-
await page.getByRole('button', { name: 'Manual time entry' }).click();
29+
30+
// Open the dropdown menu and click "Manual time entry"
31+
await page.getByRole('button', { name: 'Time entry actions' }).click();
32+
await page.getByRole('menuitem', { name: 'Manual time entry' }).click();
3033

3134
// Fill in the time entry details
3235
await page
@@ -52,7 +55,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat
5255

5356
async function createTimeEntryWithTag(page: Page, tagName: string, duration: string) {
5457
await goToTimeOverview(page);
55-
await page.getByRole('button', { name: 'Manual time entry' }).click();
58+
59+
// Open the dropdown menu and click "Manual time entry"
60+
await page.getByRole('button', { name: 'Time entry actions' }).click();
61+
await page.getByRole('menuitem', { name: 'Manual time entry' }).click();
5662

5763
// Fill in the time entry details
5864
await page
@@ -81,7 +87,10 @@ async function createTimeEntryWithBillableStatus(
8187
duration: string
8288
) {
8389
await goToTimeOverview(page);
84-
await page.getByRole('button', { name: 'Manual time entry' }).click();
90+
91+
// Open the dropdown menu and click "Manual time entry"
92+
await page.getByRole('button', { name: 'Time entry actions' }).click();
93+
await page.getByRole('menuitem', { name: 'Manual time entry' }).click();
8594

8695
// Fill in the time entry details
8796
await page

resources/js/Components/TimeTracker.vue

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,25 @@ import { useProjectsStore } from '@/utils/useProjects';
1616
import { useTasksStore } from '@/utils/useTasks';
1717
import { useTagsStore } from '@/utils/useTags';
1818
import TimeTrackerControls from '@/packages/ui/src/TimeTracker/TimeTrackerControls.vue';
19-
import type { CreateClientBody, CreateProjectBody, Project } from '@/packages/api/src';
19+
import type {
20+
CreateClientBody,
21+
CreateProjectBody,
22+
CreateTimeEntryBody,
23+
Project,
24+
Tag,
25+
} from '@/packages/api/src';
2026
import TimeTrackerRunningInDifferentOrganizationOverlay from '@/packages/ui/src/TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue';
27+
import TimeTrackerMoreOptionsDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerMoreOptionsDropdown.vue';
28+
import TimeEntryCreateModal from '@/packages/ui/src/TimeEntry/TimeEntryCreateModal.vue';
2129
import { useClientsStore } from '@/utils/useClients';
2230
import { getOrganizationCurrencyString } from '@/utils/money';
2331
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
2432
import { canCreateProjects } from '@/utils/permissions';
33+
import { ref } from 'vue';
34+
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
35+
import { useMutation, useQueryClient } from '@tanstack/vue-query';
36+
import { api } from '@/packages/api/src';
37+
import { useNotificationsStore } from '@/utils/notification';
2538
2639
const page = usePage<{
2740
auth: {
@@ -47,6 +60,8 @@ const emit = defineEmits<{
4760
change: [];
4861
}>();
4962
63+
const showManualTimeEntryModal = ref(false);
64+
5065
watch(isActive, () => {
5166
if (isActive.value) {
5267
startLiveTimer();
@@ -93,14 +108,64 @@ function switchToTimeEntryOrganization() {
93108
switchOrganization(currentTimeEntry.value.organization_id);
94109
}
95110
}
96-
async function createTag(tag: string) {
111+
async function createTag(tag: string): Promise<Tag | undefined> {
97112
return await useTagsStore().createTag(tag);
98113
}
99114
115+
async function createTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>) {
116+
await useTimeEntriesStore().createTimeEntry(timeEntry);
117+
showManualTimeEntryModal.value = false;
118+
}
119+
120+
const { handleApiRequestNotifications } = useNotificationsStore();
121+
const queryClient = useQueryClient();
122+
123+
const deleteTimeEntryMutation = useMutation({
124+
mutationFn: async (timeEntryId: string) => {
125+
const organizationId = getCurrentOrganizationId();
126+
if (!organizationId) {
127+
throw new Error('No organization selected');
128+
}
129+
return await api.deleteTimeEntry(undefined, {
130+
params: {
131+
organization: organizationId,
132+
timeEntry: timeEntryId,
133+
},
134+
});
135+
},
136+
onSuccess: async () => {
137+
await currentTimeEntryStore.fetchCurrentTimeEntry();
138+
await useTimeEntriesStore().fetchTimeEntries();
139+
queryClient.invalidateQueries({ queryKey: ['timeEntry'] });
140+
queryClient.invalidateQueries({ queryKey: ['timeEntries'] });
141+
},
142+
});
143+
144+
async function discardCurrentTimeEntry() {
145+
if (currentTimeEntry.value.id) {
146+
await handleApiRequestNotifications(
147+
() => deleteTimeEntryMutation.mutateAsync(currentTimeEntry.value.id),
148+
'Time entry discarded successfully',
149+
'Failed to discard time entry'
150+
);
151+
}
152+
}
153+
100154
const { tags } = storeToRefs(useTagsStore());
101155
</script>
102156

103157
<template>
158+
<TimeEntryCreateModal
159+
v-model:show="showManualTimeEntryModal"
160+
:enable-estimated-time="isAllowedToPerformPremiumAction()"
161+
:create-project="createProject"
162+
:create-client="createClient"
163+
:create-tag="createTag"
164+
:create-time-entry="createTimeEntry"
165+
:projects
166+
:tasks
167+
:tags
168+
:clients></TimeEntryCreateModal>
104169
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
105170
<div class="relative">
106171
<TimeTrackerRunningInDifferentOrganizationOverlay
@@ -109,24 +174,34 @@ const { tags } = storeToRefs(useTagsStore());
109174
switchToTimeEntryOrganization
110175
"></TimeTrackerRunningInDifferentOrganizationOverlay>
111176

112-
<TimeTrackerControls
113-
v-model:current-time-entry="currentTimeEntry"
114-
v-model:live-timer="now"
115-
:create-project
116-
:enable-estimated-time="isAllowedToPerformPremiumAction()"
117-
:can-create-project="canCreateProjects()"
118-
:create-client
119-
:clients
120-
:tags
121-
:tasks
122-
:projects
123-
:create-tag
124-
:is-active
125-
:currency="getOrganizationCurrencyString()"
126-
@start-live-timer="startLiveTimer"
127-
@stop-live-timer="stopLiveTimer"
128-
@start-timer="setActiveState(true)"
129-
@stop-timer="setActiveState(false)"
130-
@update-time-entry="updateTimeEntry"></TimeTrackerControls>
177+
<div class="flex w-full items-center gap-2">
178+
<div class="flex w-full items-center gap-2">
179+
<div class="flex-1">
180+
<TimeTrackerControls
181+
v-model:current-time-entry="currentTimeEntry"
182+
v-model:live-timer="now"
183+
:create-project
184+
:enable-estimated-time="isAllowedToPerformPremiumAction()"
185+
:can-create-project="canCreateProjects()"
186+
:create-client
187+
:clients
188+
:tags
189+
:tasks
190+
:projects
191+
:create-tag
192+
:is-active
193+
:currency="getOrganizationCurrencyString()"
194+
@start-live-timer="startLiveTimer"
195+
@stop-live-timer="stopLiveTimer"
196+
@start-timer="setActiveState(true)"
197+
@stop-timer="setActiveState(false)"
198+
@update-time-entry="updateTimeEntry"></TimeTrackerControls>
199+
</div>
200+
<TimeTrackerMoreOptionsDropdown
201+
:has-active-timer="isActive"
202+
@manual-entry="showManualTimeEntryModal = true"
203+
@discard="discardCurrentTimeEntry"></TimeTrackerMoreOptionsDropdown>
204+
</div>
205+
</div>
131206
</div>
132207
</template>

resources/js/Pages/Time.vue

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,13 @@ import type {
1515
} from '@/packages/api/src';
1616
import { useElementVisibility } from '@vueuse/core';
1717
import { ClockIcon } from '@heroicons/vue/20/solid';
18-
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
19-
import { PlusIcon } from '@heroicons/vue/16/solid';
2018
import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue';
2119
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
2220
import { useTasksStore } from '@/utils/useTasks';
2321
import { useProjectsStore } from '@/utils/useProjects';
2422
import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue';
2523
import { useTagsStore } from '@/utils/useTags';
2624
import { useClientsStore } from '@/utils/useClients';
27-
import TimeEntryCreateModal from '@/packages/ui/src/TimeEntry/TimeEntryCreateModal.vue';
2825
import { getOrganizationCurrencyString } from '@/utils/money';
2926
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
3027
import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src';
@@ -73,7 +70,6 @@ onMounted(async () => {
7370
await timeEntriesStore.fetchTimeEntries();
7471
});
7572
76-
const showManualTimeEntryModal = ref(false);
7773
const projectStore = useProjectsStore();
7874
const { projects } = storeToRefs(projectStore);
7975
const taskStore = useTasksStore();
@@ -105,33 +101,9 @@ function deleteSelected() {
105101
</script>
106102

107103
<template>
108-
<TimeEntryCreateModal
109-
v-model:show="showManualTimeEntryModal"
110-
:enable-estimated-time="isAllowedToPerformPremiumAction()"
111-
:create-project="createProject"
112-
:create-client="createClient"
113-
:create-tag="createTag"
114-
:create-time-entry="createTimeEntry"
115-
:projects
116-
:tasks
117-
:tags
118-
:clients></TimeEntryCreateModal>
119104
<AppLayout title="Dashboard" data-testid="time_view">
120105
<MainContainer class="pt-5 lg:pt-8 pb-4 lg:pb-6">
121-
<div
122-
class="lg:flex items-end lg:divide-x divide-default-background-separator divide-y lg:divide-y-0 space-y-2 lg:space-y-0 lg:space-x-2">
123-
<div class="flex-1">
124-
<TimeTracker></TimeTracker>
125-
</div>
126-
<div class="pb-2 pt-2 lg:pt-0 lg:pl-4 flex justify-center">
127-
<SecondaryButton
128-
class="w-full text-center flex justify-center"
129-
:icon="PlusIcon"
130-
@click="showManualTimeEntryModal = true"
131-
>Manual time entry
132-
</SecondaryButton>
133-
</div>
134-
</div>
106+
<TimeTracker></TimeTracker>
135107
</MainContainer>
136108
<TimeEntryMassActionRow
137109
:selected-time-entries="selectedTimeEntries"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<script setup lang="ts">
2+
import { PlusIcon, XMarkIcon } from '@heroicons/vue/20/solid';
3+
import {
4+
DropdownMenu,
5+
DropdownMenuContent,
6+
DropdownMenuItem,
7+
DropdownMenuTrigger,
8+
} from '@/Components/ui/dropdown-menu';
9+
10+
const props = defineProps<{
11+
hasActiveTimer: boolean;
12+
}>();
13+
14+
const emit = defineEmits<{
15+
manualEntry: [];
16+
discard: [];
17+
}>();
18+
</script>
19+
20+
<template>
21+
<DropdownMenu>
22+
<DropdownMenuTrigger as-child>
23+
<button
24+
class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-2 focus-visible:ring-ring hover:bg-card-background hover:opacity-100 opacity-20 transition-opacity text-text-secondary"
25+
aria-label="Time entry actions">
26+
<svg
27+
class="h-8 w-8 p-1 rounded-full"
28+
viewBox="0 0 24 24"
29+
xmlns="http://www.w3.org/2000/svg">
30+
<path
31+
fill="none"
32+
stroke="currentColor"
33+
stroke-linecap="round"
34+
stroke-linejoin="round"
35+
stroke-width="1.5"
36+
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
37+
</svg>
38+
</button>
39+
</DropdownMenuTrigger>
40+
<DropdownMenuContent class="min-w-[150px]" align="end">
41+
<DropdownMenuItem
42+
class="flex items-center space-x-3 cursor-pointer"
43+
@click="emit('manualEntry')">
44+
<PlusIcon class="w-5" />
45+
<span>Manual time entry</span>
46+
</DropdownMenuItem>
47+
<DropdownMenuItem
48+
v-if="props.hasActiveTimer"
49+
class="flex items-center space-x-3 cursor-pointer text-destructive focus:text-destructive"
50+
@click="emit('discard')">
51+
<XMarkIcon class="w-5" />
52+
<span>Discard</span>
53+
</DropdownMenuItem>
54+
</DropdownMenuContent>
55+
</DropdownMenu>
56+
</template>
57+
58+
<style scoped></style>

0 commit comments

Comments
 (0)