diff --git a/e2e/time.spec.ts b/e2e/time.spec.ts index 648cfd25..18464b8b 100644 --- a/e2e/time.spec.ts +++ b/e2e/time.spec.ts @@ -61,12 +61,6 @@ async function assertThatTimeEntryRowIsStopped(newTimeEntry: Locator) { ); } -async function assertThatTimeEntryRowIsStarted(newTimeEntry: Locator) { - await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass( - /bg-red-400\/80/ - ); -} - test('test that updating a description of a time entry in the overview works on blur', async ({ page, }) => { @@ -259,48 +253,6 @@ test('test that updating a the duration in the overview works on blur', async ({ ).toHaveValue('0h 20min'); }); -// Test that start stop button stops running timer -test('test that stopping a time entry from the overview works', async ({ - page, -}) => { - await goToTimeOverview(page); - const timeEntryRows = page.locator('[data-testid="time_entry_row"]'); - await Promise.all([ - newTimeEntryResponse(page), - startOrStopTimerWithButton(page), - assertThatTimerHasStarted(page), - page.waitForResponse( - (response) => - response.url().includes('/time-entries') && - response.status() === 200 - ), - ]); - - await page.waitForTimeout(1500); - - const newTimeEntry = timeEntryRows.first(); - const stopButton = newTimeEntry.getByTestId('timer_button'); - await assertThatTimeEntryRowIsStarted(newTimeEntry); - - await Promise.all([ - page.waitForResponse(async (response) => { - return ( - response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && - (await response.json()).data.id !== null && - (await response.json()).data.start !== null && - (await response.json()).data.end !== null - ); - }), - stopButton.click(), - ]); - - await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass( - /bg-accent-300\/70/ - ); -}); - // Test that start stop button stops running timer test('test that starting a time entry from the overview works', async ({ page, @@ -327,7 +279,8 @@ test('test that starting a time entry from the overview works', async ({ startButton.click(), ]); - await expect(startButton).toHaveClass(/bg-red-500\/80/); + await assertThatTimerHasStarted(page); + await page.waitForTimeout(1500); await Promise.all([ page.waitForResponse(async (response) => { @@ -341,67 +294,7 @@ test('test that starting a time entry from the overview works', async ({ ); }), startOrStopTimerWithButton(page), - expect(startButton).toHaveClass(/bg-accent-300\/70/), - ]); -}); - -test('test that updating a the duration in the overview for a running timer works on blur', async ({ - page, -}) => { - await goToTimeOverview(page); - const timeEntryRows = page.locator('[data-testid="time_entry_row"]'); - - await Promise.all([ - newTimeEntryResponse(page), - startOrStopTimerWithButton(page), - assertThatTimerHasStarted(page), - page.waitForResponse( - (response) => - response.url().includes('/time-entries') && - response.status() === 200 - ), - ]); - - await page.waitForTimeout(1500); - - const newTimeEntry = timeEntryRows.first(); - const startButton = newTimeEntry.getByTestId('timer_button'); - await page.waitForTimeout(1500); - const timeEntryDurationInput = newTimeEntry.getByTestId( - 'time_entry_duration_input' - ); - await timeEntryDurationInput.fill('20min'); - - await Promise.all([ - page.waitForResponse(async (response) => { - return ( - response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && - (await response.json()).data.id !== null && - // TODO! Actually check the value - (await response.json()).data.start !== null && - (await response.json()).data.end !== null - ); - }), - timeEntryDurationInput.press('Tab'), - ]); - - await expect(page.getByTestId('time_entry_time')).toHaveValue('00:20:00'); - - await Promise.all([ - page.waitForResponse(async (response) => { - return ( - response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && - (await response.json()).data.id !== null && - (await response.json()).data.start !== null && - (await response.json()).data.end !== null - ); - }), - startOrStopTimerWithButton(page), - expect(startButton).toHaveClass(/bg-accent-300\/70/), + assertThatTimerIsStopped(page), ]); }); @@ -468,3 +361,5 @@ test.skip('test that load more works when the end of page is reached', async ({ // TODO: Test Grouped time entries by description/project // TODO: Add Test for Date Update + +// TODO: Test that project can be created in the time entry row diff --git a/e2e/timetracker.spec.ts b/e2e/timetracker.spec.ts index 7b19b197..a4d016d0 100644 --- a/e2e/timetracker.spec.ts +++ b/e2e/timetracker.spec.ts @@ -279,3 +279,5 @@ test('test that adding a new tag when the timer is running', async ({ // test that sidebar timetracker changes state when tmer on dashboard is started // test billable toggle + +// TODO: Test that project can be created in the time tracker row diff --git a/resources/js/Components/Common/Project/ProjectCreateModal.vue b/resources/js/Components/Common/Project/ProjectCreateModal.vue index 2c61fd32..6dc6b069 100644 --- a/resources/js/Components/Common/Project/ProjectCreateModal.vue +++ b/resources/js/Components/Common/Project/ProjectCreateModal.vue @@ -6,22 +6,26 @@ import { computed, ref } from 'vue'; import type { CreateProjectBody } from '@/utils/api'; import { getRandomColor } from '@/utils/color'; import PrimaryButton from '@/Components/PrimaryButton.vue'; -import { useProjectsStore } from '@/utils/useProjects'; import { useFocus } from '@vueuse/core'; import ClientDropdown from '@/Components/Common/Client/ClientDropdown.vue'; import Badge from '@/Components/Common/Badge.vue'; -import { useClientsStore } from '@/utils/useClients'; -import { storeToRefs } from 'pinia'; import ProjectColorSelector from '@/Components/Common/Project/ProjectColorSelector.vue'; import { UserCircleIcon } from '@heroicons/vue/20/solid'; import InputLabel from '@/Components/InputLabel.vue'; import ProjectEditBillableSection from '@/Components/Common/Project/ProjectEditBillableSection.vue'; +import type { Client } from '@/utils/api'; -const { createProject } = useProjectsStore(); -const { clients } = storeToRefs(useClientsStore()); const show = defineModel('show', { default: false }); const saving = ref(false); +const props = defineProps<{ + clients: Client[]; +}>(); + +const emit = defineEmits<{ + submit: [project: CreateProjectBody, callback: () => void]; +}>(); + const project = ref({ name: '', color: getRandomColor(), @@ -31,15 +35,16 @@ const project = ref({ }); async function submit() { - await createProject(project.value); - show.value = false; - project.value = { - name: '', - color: getRandomColor(), - client_id: null, - billable_rate: null, - is_billable: false, - }; + emit('submit', project.value, () => { + show.value = false; + project.value = { + name: '', + color: getRandomColor(), + client_id: null, + billable_rate: null, + is_billable: false, + }; + }); } const projectNameInput = ref(null); @@ -48,7 +53,7 @@ useFocus(projectNameInput, { initialValue: true }); const currentClientName = computed(() => { if (project.value.client_id) { - return clients.value.find( + return props.clients.find( (client) => client.id === project.value.client_id )?.name; } diff --git a/resources/js/Components/Common/Project/ProjectTable.vue b/resources/js/Components/Common/Project/ProjectTable.vue index 305deb09..463aaba4 100644 --- a/resources/js/Components/Common/Project/ProjectTable.vue +++ b/resources/js/Components/Common/Project/ProjectTable.vue @@ -7,17 +7,28 @@ import ProjectCreateModal from '@/Components/Common/Project/ProjectCreateModal.v import ProjectTableHeading from '@/Components/Common/Project/ProjectTableHeading.vue'; import ProjectTableRow from '@/Components/Common/Project/ProjectTableRow.vue'; import { canCreateProjects } from '@/utils/permissions'; -import type { Project } from '@/utils/api'; +import type { CreateProjectBody, Project } from '@/utils/api'; +import { useProjectsStore } from '@/utils/useProjects'; +import { useClientsStore } from '@/utils/useClients'; +import { storeToRefs } from 'pinia'; defineProps<{ projects: Project[]; }>(); -const createProject = ref(false); +const showCreateProjectModal = ref(false); +async function createProject(project: CreateProjectBody, callback: () => void) { + await useProjectsStore().createProject(project); + callback(); +} +const { clients } = storeToRefs(useClientsStore()); diff --git a/resources/js/Components/TimeTracker.vue b/resources/js/Components/TimeTracker.vue index d3c84b7d..590247ac 100644 --- a/resources/js/Components/TimeTracker.vue +++ b/resources/js/Components/TimeTracker.vue @@ -19,6 +19,7 @@ import { switchOrganization } from '@/utils/useOrganization'; import SecondaryButton from '@/Components/SecondaryButton.vue'; import TimeTrackerRangeSelector from '@/Components/Common/TimeTracker/TimeTrackerRangeSelector.vue'; import { useProjectsStore } from '@/utils/useProjects'; +import { useTasksStore } from '@/utils/useTasks'; const page = usePage<{ auth: { @@ -34,6 +35,11 @@ const { currentTimeEntry, isActive, now } = storeToRefs(currentTimeEntryStore); const { startLiveTimer, stopLiveTimer, setActiveState } = currentTimeEntryStore; const currentTimeEntryDescriptionInput = ref(null); +const projectStore = useProjectsStore(); +const { projects } = storeToRefs(projectStore); +const taskStore = useTasksStore(); +const { tasks } = storeToRefs(taskStore); + watch(isActive, () => { if (isActive.value) { startLiveTimer(); @@ -133,6 +139,8 @@ function switchToTimeEntryOrganization() {
Create Project + :clients="clients" + @submit="createProject" + v-model:show="showCreateProjectModal"> diff --git a/resources/js/Pages/Time.vue b/resources/js/Pages/Time.vue index 24825127..c6d47927 100644 --- a/resources/js/Pages/Time.vue +++ b/resources/js/Pages/Time.vue @@ -19,14 +19,51 @@ import { PlusIcon } from '@heroicons/vue/16/solid'; import TimeEntryCreateModal from '@/Components/Common/TimeEntry/TimeEntryCreateModal.vue'; import TimeEntryAggregateRow from '@/Components/Common/TimeEntry/TimeEntryAggregateRow.vue'; import LoadingSpinner from '@/Components/LoadingSpinner.vue'; +import dayjs from 'dayjs'; +import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry'; +import { useTasksStore } from '@/utils/useTasks'; +import { useProjectsStore } from '@/utils/useProjects'; const timeEntriesStore = useTimeEntriesStore(); const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore); +const { updateTimeEntry, fetchTimeEntries, createTimeEntry } = + useTimeEntriesStore(); + +function updateTimeEntries(timeEntries: TimeEntry[]) { + timeEntries.forEach((entry) => { + useTimeEntriesStore().updateTimeEntry(entry); + }); + fetchTimeEntries(); +} const loading = ref(false); const loadMoreContainer = ref(null); const isLoadMoreVisible = useElementVisibility(loadMoreContainer); +async function onStartStopClick(timeEntry: TimeEntry) { + if (timeEntry.start && !timeEntry.end) { + await updateTimeEntry({ + ...timeEntry, + end: dayjs().utc().format(), + }); + } else { + await createTimeEntry({ + ...timeEntry, + start: dayjs().utc().format(), + end: null, + }); + } + fetchTimeEntries(); + useCurrentTimeEntryStore().fetchCurrentTimeEntry(); +} + +function deleteTimeEntries(timeEntries: TimeEntry[]) { + timeEntries.forEach((entry) => { + useTimeEntriesStore().deleteTimeEntry(entry.id); + }); + fetchTimeEntries(); +} + watch(isLoadMoreVisible, async (isVisible) => { if ( isVisible && @@ -45,6 +82,10 @@ onMounted(async () => { const groupedTimeEntries = computed(() => { const groupedEntriesByDay: Record = {}; for (const entry of timeEntries.value) { + // skip current time entry + if (entry.end === null) { + continue; + } const oldEntries = groupedEntriesByDay[getLocalizedDateFromTimestamp(entry.start)]; groupedEntriesByDay[getLocalizedDateFromTimestamp(entry.start)] = [ @@ -104,6 +145,10 @@ const groupedTimeEntries = computed(() => { return groupedEntriesByDayAndType; }); const showManualTimeEntryModal = ref(false); +const projectStore = useProjectsStore(); +const { projects } = storeToRefs(projectStore); +const taskStore = useTasksStore(); +const { tasks } = storeToRefs(taskStore);