mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-11 09:42:15 +01:00
Compare commits
1 Commits
feature/fr
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f364d3fd0 |
@@ -9,7 +9,10 @@ async function goToOrganizationSettings(page) {
|
|||||||
|
|
||||||
async function createTimeEntry(page, duration: string) {
|
async function createTimeEntry(page, duration: string) {
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/time');
|
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
|
// Fill in the time entry details
|
||||||
await page.getByTestId('time_entry_description').fill('Test time entry');
|
await page.getByTestId('time_entry_description').fill('Test time entry');
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat
|
|||||||
|
|
||||||
// Then create the time entry
|
// Then create the time entry
|
||||||
await goToTimeOverview(page);
|
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
|
// Fill in the time entry details
|
||||||
await page
|
await page
|
||||||
@@ -52,7 +55,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat
|
|||||||
|
|
||||||
async function createTimeEntryWithTag(page: Page, tagName: string, duration: string) {
|
async function createTimeEntryWithTag(page: Page, tagName: string, duration: string) {
|
||||||
await goToTimeOverview(page);
|
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
|
// Fill in the time entry details
|
||||||
await page
|
await page
|
||||||
@@ -81,7 +87,10 @@ async function createTimeEntryWithBillableStatus(
|
|||||||
duration: string
|
duration: string
|
||||||
) {
|
) {
|
||||||
await goToTimeOverview(page);
|
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
|
// Fill in the time entry details
|
||||||
await page
|
await page
|
||||||
|
|||||||
@@ -16,12 +16,25 @@ import { useProjectsStore } from '@/utils/useProjects';
|
|||||||
import { useTasksStore } from '@/utils/useTasks';
|
import { useTasksStore } from '@/utils/useTasks';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
import { useTagsStore } from '@/utils/useTags';
|
||||||
import TimeTrackerControls from '@/packages/ui/src/TimeTracker/TimeTrackerControls.vue';
|
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 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 { useClientsStore } from '@/utils/useClients';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||||
import { canCreateProjects } from '@/utils/permissions';
|
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<{
|
const page = usePage<{
|
||||||
auth: {
|
auth: {
|
||||||
@@ -47,6 +60,8 @@ const emit = defineEmits<{
|
|||||||
change: [];
|
change: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const showManualTimeEntryModal = ref(false);
|
||||||
|
|
||||||
watch(isActive, () => {
|
watch(isActive, () => {
|
||||||
if (isActive.value) {
|
if (isActive.value) {
|
||||||
startLiveTimer();
|
startLiveTimer();
|
||||||
@@ -93,14 +108,64 @@ function switchToTimeEntryOrganization() {
|
|||||||
switchOrganization(currentTimeEntry.value.organization_id);
|
switchOrganization(currentTimeEntry.value.organization_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function createTag(tag: string) {
|
async function createTag(tag: string): Promise<Tag | undefined> {
|
||||||
return await useTagsStore().createTag(tag);
|
return await useTagsStore().createTag(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>) {
|
||||||
|
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());
|
const { tags } = storeToRefs(useTagsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<TimeEntryCreateModal
|
||||||
|
v-model:show="showManualTimeEntryModal"
|
||||||
|
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||||
|
:create-project="createProject"
|
||||||
|
:create-client="createClient"
|
||||||
|
:create-tag="createTag"
|
||||||
|
:create-time-entry="createTimeEntry"
|
||||||
|
:projects
|
||||||
|
:tasks
|
||||||
|
:tags
|
||||||
|
:clients></TimeEntryCreateModal>
|
||||||
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
|
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<TimeTrackerRunningInDifferentOrganizationOverlay
|
<TimeTrackerRunningInDifferentOrganizationOverlay
|
||||||
@@ -109,24 +174,34 @@ const { tags } = storeToRefs(useTagsStore());
|
|||||||
switchToTimeEntryOrganization
|
switchToTimeEntryOrganization
|
||||||
"></TimeTrackerRunningInDifferentOrganizationOverlay>
|
"></TimeTrackerRunningInDifferentOrganizationOverlay>
|
||||||
|
|
||||||
<TimeTrackerControls
|
<div class="flex w-full items-center gap-2">
|
||||||
v-model:current-time-entry="currentTimeEntry"
|
<div class="flex w-full items-center gap-2">
|
||||||
v-model:live-timer="now"
|
<div class="flex-1">
|
||||||
:create-project
|
<TimeTrackerControls
|
||||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
v-model:current-time-entry="currentTimeEntry"
|
||||||
:can-create-project="canCreateProjects()"
|
v-model:live-timer="now"
|
||||||
:create-client
|
:create-project
|
||||||
:clients
|
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||||
:tags
|
:can-create-project="canCreateProjects()"
|
||||||
:tasks
|
:create-client
|
||||||
:projects
|
:clients
|
||||||
:create-tag
|
:tags
|
||||||
:is-active
|
:tasks
|
||||||
:currency="getOrganizationCurrencyString()"
|
:projects
|
||||||
@start-live-timer="startLiveTimer"
|
:create-tag
|
||||||
@stop-live-timer="stopLiveTimer"
|
:is-active
|
||||||
@start-timer="setActiveState(true)"
|
:currency="getOrganizationCurrencyString()"
|
||||||
@stop-timer="setActiveState(false)"
|
@start-live-timer="startLiveTimer"
|
||||||
@update-time-entry="updateTimeEntry"></TimeTrackerControls>
|
@stop-live-timer="stopLiveTimer"
|
||||||
|
@start-timer="setActiveState(true)"
|
||||||
|
@stop-timer="setActiveState(false)"
|
||||||
|
@update-time-entry="updateTimeEntry"></TimeTrackerControls>
|
||||||
|
</div>
|
||||||
|
<TimeTrackerMoreOptionsDropdown
|
||||||
|
:has-active-timer="isActive"
|
||||||
|
@manual-entry="showManualTimeEntryModal = true"
|
||||||
|
@discard="discardCurrentTimeEntry"></TimeTrackerMoreOptionsDropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import type {
|
|||||||
} from '@/packages/api/src';
|
} from '@/packages/api/src';
|
||||||
import { useElementVisibility } from '@vueuse/core';
|
import { useElementVisibility } from '@vueuse/core';
|
||||||
import { ClockIcon } from '@heroicons/vue/20/solid';
|
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 LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue';
|
||||||
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
import { useTasksStore } from '@/utils/useTasks';
|
||||||
@@ -24,7 +22,6 @@ import { useProjectsStore } from '@/utils/useProjects';
|
|||||||
import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue';
|
import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
import { useTagsStore } from '@/utils/useTags';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsStore } from '@/utils/useClients';
|
||||||
import TimeEntryCreateModal from '@/packages/ui/src/TimeEntry/TimeEntryCreateModal.vue';
|
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
|
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
|
||||||
import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src';
|
import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src';
|
||||||
@@ -73,7 +70,6 @@ onMounted(async () => {
|
|||||||
await timeEntriesStore.fetchTimeEntries();
|
await timeEntriesStore.fetchTimeEntries();
|
||||||
});
|
});
|
||||||
|
|
||||||
const showManualTimeEntryModal = ref(false);
|
|
||||||
const projectStore = useProjectsStore();
|
const projectStore = useProjectsStore();
|
||||||
const { projects } = storeToRefs(projectStore);
|
const { projects } = storeToRefs(projectStore);
|
||||||
const taskStore = useTasksStore();
|
const taskStore = useTasksStore();
|
||||||
@@ -105,33 +101,9 @@ function deleteSelected() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TimeEntryCreateModal
|
|
||||||
v-model:show="showManualTimeEntryModal"
|
|
||||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
|
||||||
:create-project="createProject"
|
|
||||||
:create-client="createClient"
|
|
||||||
:create-tag="createTag"
|
|
||||||
:create-time-entry="createTimeEntry"
|
|
||||||
:projects
|
|
||||||
:tasks
|
|
||||||
:tags
|
|
||||||
:clients></TimeEntryCreateModal>
|
|
||||||
<AppLayout title="Dashboard" data-testid="time_view">
|
<AppLayout title="Dashboard" data-testid="time_view">
|
||||||
<MainContainer class="pt-5 lg:pt-8 pb-4 lg:pb-6">
|
<MainContainer class="pt-5 lg:pt-8 pb-4 lg:pb-6">
|
||||||
<div
|
<TimeTracker></TimeTracker>
|
||||||
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">
|
|
||||||
<div class="flex-1">
|
|
||||||
<TimeTracker></TimeTracker>
|
|
||||||
</div>
|
|
||||||
<div class="pb-2 pt-2 lg:pt-0 lg:pl-4 flex justify-center">
|
|
||||||
<SecondaryButton
|
|
||||||
class="w-full text-center flex justify-center"
|
|
||||||
:icon="PlusIcon"
|
|
||||||
@click="showManualTimeEntryModal = true"
|
|
||||||
>Manual time entry
|
|
||||||
</SecondaryButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<TimeEntryMassActionRow
|
<TimeEntryMassActionRow
|
||||||
:selected-time-entries="selectedTimeEntries"
|
:selected-time-entries="selectedTimeEntries"
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { PlusIcon, XMarkIcon } from '@heroicons/vue/20/solid';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/Components/ui/dropdown-menu';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
hasActiveTimer: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
manualEntry: [];
|
||||||
|
discard: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
aria-label="Time entry actions">
|
||||||
|
<svg
|
||||||
|
class="h-8 w-8 p-1 rounded-full"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.5"
|
||||||
|
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" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent class="min-w-[150px]" align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
class="flex items-center space-x-3 cursor-pointer"
|
||||||
|
@click="emit('manualEntry')">
|
||||||
|
<PlusIcon class="w-5" />
|
||||||
|
<span>Manual time entry</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-if="props.hasActiveTimer"
|
||||||
|
class="flex items-center space-x-3 cursor-pointer text-destructive focus:text-destructive"
|
||||||
|
@click="emit('discard')">
|
||||||
|
<XMarkIcon class="w-5" />
|
||||||
|
<span>Discard</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
Reference in New Issue
Block a user