diff --git a/e2e/organization.spec.ts b/e2e/organization.spec.ts index d2386577..78541c25 100644 --- a/e2e/organization.spec.ts +++ b/e2e/organization.spec.ts @@ -9,7 +9,10 @@ async function goToOrganizationSettings(page) { async function createTimeEntry(page, duration: string) { await page.goto(PLAYWRIGHT_BASE_URL + '/time'); - await page.getByRole('button', { name: 'Manual time entry' }).click(); + + // Open the dropdown menu and click "Manual time entry" + await page.getByRole('button', { name: 'Time entry actions' }).click(); + await page.getByRole('menuitem', { name: 'Manual time entry' }).click(); // Fill in the time entry details await page.getByTestId('time_entry_description').fill('Test time entry'); diff --git a/e2e/reporting.spec.ts b/e2e/reporting.spec.ts index 0cfa872c..4c3a777d 100644 --- a/e2e/reporting.spec.ts +++ b/e2e/reporting.spec.ts @@ -26,7 +26,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat // Then create the time entry await goToTimeOverview(page); - await page.getByRole('button', { name: 'Manual time entry' }).click(); + + // Open the dropdown menu and click "Manual time entry" + await page.getByRole('button', { name: 'Time entry actions' }).click(); + await page.getByRole('menuitem', { name: 'Manual time entry' }).click(); // Fill in the time entry details await page @@ -52,7 +55,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat async function createTimeEntryWithTag(page: Page, tagName: string, duration: string) { await goToTimeOverview(page); - await page.getByRole('button', { name: 'Manual time entry' }).click(); + + // Open the dropdown menu and click "Manual time entry" + await page.getByRole('button', { name: 'Time entry actions' }).click(); + await page.getByRole('menuitem', { name: 'Manual time entry' }).click(); // Fill in the time entry details await page @@ -81,7 +87,10 @@ async function createTimeEntryWithBillableStatus( duration: string ) { await goToTimeOverview(page); - await page.getByRole('button', { name: 'Manual time entry' }).click(); + + // Open the dropdown menu and click "Manual time entry" + await page.getByRole('button', { name: 'Time entry actions' }).click(); + await page.getByRole('menuitem', { name: 'Manual time entry' }).click(); // Fill in the time entry details await page diff --git a/resources/js/Components/TimeTracker.vue b/resources/js/Components/TimeTracker.vue index 6498af65..0aec12df 100644 --- a/resources/js/Components/TimeTracker.vue +++ b/resources/js/Components/TimeTracker.vue @@ -16,12 +16,25 @@ import { useProjectsStore } from '@/utils/useProjects'; import { useTasksStore } from '@/utils/useTasks'; import { useTagsStore } from '@/utils/useTags'; import TimeTrackerControls from '@/packages/ui/src/TimeTracker/TimeTrackerControls.vue'; -import type { CreateClientBody, CreateProjectBody, Project } from '@/packages/api/src'; +import type { + CreateClientBody, + CreateProjectBody, + CreateTimeEntryBody, + Project, + Tag, +} from '@/packages/api/src'; import TimeTrackerRunningInDifferentOrganizationOverlay from '@/packages/ui/src/TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue'; +import TimeTrackerMoreOptionsDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerMoreOptionsDropdown.vue'; +import TimeEntryCreateModal from '@/packages/ui/src/TimeEntry/TimeEntryCreateModal.vue'; import { useClientsStore } from '@/utils/useClients'; 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'; const page = usePage<{ auth: { @@ -47,6 +60,8 @@ const emit = defineEmits<{ change: []; }>(); +const showManualTimeEntryModal = ref(false); + watch(isActive, () => { if (isActive.value) { startLiveTimer(); @@ -93,14 +108,64 @@ function switchToTimeEntryOrganization() { switchOrganization(currentTimeEntry.value.organization_id); } } -async function createTag(tag: string) { +async function createTag(tag: string): Promise { return await useTagsStore().createTag(tag); } +async function createTimeEntry(timeEntry: Omit) { + await useTimeEntriesStore().createTimeEntry(timeEntry); + showManualTimeEntryModal.value = false; +} + +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), + 'Time entry discarded successfully', + 'Failed to discard time entry' + ); + } +} + const { tags } = storeToRefs(useTagsStore()); diff --git a/resources/js/Pages/Time.vue b/resources/js/Pages/Time.vue index eb89fad9..f555e10d 100644 --- a/resources/js/Pages/Time.vue +++ b/resources/js/Pages/Time.vue @@ -15,8 +15,6 @@ import type { } from '@/packages/api/src'; import { useElementVisibility } from '@vueuse/core'; import { ClockIcon } from '@heroicons/vue/20/solid'; -import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue'; -import { PlusIcon } from '@heroicons/vue/16/solid'; import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue'; import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry'; import { useTasksStore } from '@/utils/useTasks'; @@ -24,7 +22,6 @@ import { useProjectsStore } from '@/utils/useProjects'; import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue'; import { useTagsStore } from '@/utils/useTags'; import { useClientsStore } from '@/utils/useClients'; -import TimeEntryCreateModal from '@/packages/ui/src/TimeEntry/TimeEntryCreateModal.vue'; import { getOrganizationCurrencyString } from '@/utils/money'; import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue'; import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src'; @@ -73,7 +70,6 @@ onMounted(async () => { await timeEntriesStore.fetchTimeEntries(); }); -const showManualTimeEntryModal = ref(false); const projectStore = useProjectsStore(); const { projects } = storeToRefs(projectStore); const taskStore = useTasksStore(); @@ -105,33 +101,9 @@ function deleteSelected() {