refactor timetracker to seperate data and ui logic

This commit is contained in:
Gregor Vostrak
2024-07-22 17:56:51 +02:00
parent a9c874e540
commit 06fef6e40f
41 changed files with 1984 additions and 961 deletions

View File

@@ -30,7 +30,7 @@ class UserMemberController extends Controller
$members = Member::query()
->whereBelongsTo($user, 'user')
->with(['organization'])
->get();
->paginate(config('app.pagination_per_page_default'));
return new PersonalMemberCollection($members);
}

View File

@@ -24,7 +24,7 @@ class PersonalMemberResource extends BaseResource
/** @var string $id ID of membership */
'id' => $this->resource->id,
'organization' => [
/** @var int $id ID of organization */
/** @var string $id ID of organization */
'id' => $this->resource->organization->id,
/** @var string $name Name of organization */
'name' => $this->resource->organization->name,

View File

@@ -108,7 +108,7 @@ services:
- sail
- reverse-proxy
playwright:
image: mcr.microsoft.com/playwright:v1.44.1-jammy
image: mcr.microsoft.com/playwright:v1.45.2-jammy
command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0']
working_dir: /src
extra_hosts:

File diff suppressed because it is too large Load Diff

1573
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,9 +28,9 @@
"tailwindcss": "^3.1.0",
"typescript": "^5.3.3",
"vite": "^5.0.0",
"vite-plugin-checker": "^0.6.2",
"vite-plugin-checker": "^0.7.2",
"vue": "^3.4.0",
"vue-tsc": "^1.8.27"
"vue-tsc": "^2.0.28"
},
"dependencies": {
"@floating-ui/core": "^1.6.0",

View File

@@ -30,14 +30,12 @@ const iconSizeClasses = computed(() => {
if (props.size === 'small') {
return 'w-5 h-5';
} else {
return 'w-5 sm:w-6 h-5 sm:h-6';
return 'w-5 lg:w-6 h-5 lg:h-6';
}
});
const iconSizeWrapperClasses =
props.size === 'small'
? 'w-6 sm:w-8 h-6 sm:h-8'
: 'w-7 sm:w-10 h-7 sm:h-10';
props.size === 'small' ? 'w-6 sm:w-8 h-6 sm:h-8' : 'w-11 h-11';
</script>
<template>

View File

@@ -8,13 +8,13 @@ defineProps<{
</script>
<template>
<div class="flex w-full items-center justify-between pb-2.5 sm:pb-4">
<div class="flex w-full items-center justify-between pb-2.5 lg:pb-4">
<h3
class="text-white font-bold text-base flex items-center space-x-2.5">
class="text-white font-bold text-sm lg:text-base flex items-center space-x-2 lg:space-x-2.5">
<component
v-if="icon"
:is="icon"
class="w-6 text-icon-default"></component>
class="w-5 lg:w-6 text-icon-default"></component>
<span>
{{ title }}
</span>

View File

@@ -2,17 +2,18 @@
import { PlusCircleIcon } from '@heroicons/vue/20/solid';
import Dropdown from '@/Components/Dropdown.vue';
import { type Component, computed, nextTick, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useClientsStore } from '@/utils/useClients';
import ClientDropdownItem from '@/Components/Common/Client/ClientDropdownItem.vue';
const clientsStore = useClientsStore();
const { clients } = storeToRefs(clientsStore);
import type { CreateClientBody, Client } from '@/utils/api';
const model = defineModel<string | null>({
default: null,
});
const props = defineProps<{
clients: Client[];
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
}>();
const searchInput = ref<HTMLInputElement | null>(null);
const open = ref(false);
const dropdownViewport = ref<Component | null>(null);
@@ -32,7 +33,7 @@ watch(open, (isOpen) => {
});
const filteredClients = computed(() => {
return clients.value.filter((client) => {
return props.clients.filter((client) => {
return client.name
.toLowerCase()
.includes(searchValue.value?.toLowerCase()?.trim() || '');
@@ -41,7 +42,7 @@ const filteredClients = computed(() => {
async function addClientIfNoneExists() {
if (searchValue.value.length > 0 && filteredClients.value.length === 0) {
const newClient = await clientsStore.createClient({
const newClient = await props.createClient({
name: searchValue.value,
});
if (newClient) {
@@ -67,7 +68,7 @@ function updateSearchValue(event: Event) {
searchValue.value = '';
const highlightedClientId = highlightedItemId.value;
if (highlightedClientId) {
const highlightedClient = clients.value.find(
const highlightedClient = props.clients.find(
(client) => client.id === highlightedClientId
);
if (highlightedClient) {
@@ -119,7 +120,7 @@ function moveHighlightDown() {
const highlightedItemId = ref<string | null>(null);
const highlightedItem = computed(() => {
return clients.value.find(
return props.clients.find(
(client) => client.id === highlightedItemId.value
);
});

View File

@@ -18,15 +18,12 @@ async function deleteInvitation() {
if (organizationId) {
await handleApiRequestNotifications(
() =>
api.removeInvitation(
{},
{
params: {
invitation: props.invitation.id,
organization: organizationId,
},
}
),
api.removeInvitation(undefined, {
params: {
invitation: props.invitation.id,
organization: organizationId,
},
}),
'Invitation removed successfully',
'Error removing invitation',
() => {

View File

@@ -32,9 +32,12 @@ const indicatorClasses = {
<div
:style="{ backgroundColor: props.color }"
:class="
twMerge(indicatorClasses[size], 'inline-block rounded-full')
twMerge(
indicatorClasses[size],
'inline-block rounded-full shrink-0'
)
"></div>
<div>
<div class="min-w-0">
<slot>
{{ name }}
</slot>

View File

@@ -3,7 +3,7 @@ import TextInput from '@/Components/TextInput.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import DialogModal from '@/Components/DialogModal.vue';
import { computed, ref } from 'vue';
import type { CreateProjectBody } from '@/utils/api';
import type { CreateClientBody, CreateProjectBody, Project } from '@/utils/api';
import { getRandomColor } from '@/utils/color';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { useFocus } from '@vueuse/core';
@@ -20,10 +20,8 @@ const saving = ref(false);
const props = defineProps<{
clients: Client[];
}>();
const emit = defineEmits<{
submit: [project: CreateProjectBody, callback: () => void];
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
}>();
const project = ref<CreateProjectBody>({
@@ -35,16 +33,15 @@ const project = ref<CreateProjectBody>({
});
async function submit() {
emit('submit', project.value, () => {
show.value = false;
project.value = {
name: '',
color: getRandomColor(),
client_id: null,
billable_rate: null,
is_billable: false,
};
});
await props.createProject(project.value);
show.value = false;
project.value = {
name: '',
color: getRandomColor(),
client_id: null,
billable_rate: null,
is_billable: false,
};
}
const projectNameInput = ref<HTMLInputElement | null>(null);
@@ -96,7 +93,11 @@ const currentClientName = computed(() => {
</div>
<div>
<InputLabel for="client" value="Client" />
<ClientDropdown class="mt-2" v-model="project.client_id">
<ClientDropdown
:createClient="createClient"
:clients="clients"
class="mt-2"
v-model="project.client_id">
<template #trigger>
<Badge
class="bg-input-background cursor-pointer hover:bg-tertiary"

View File

@@ -3,7 +3,7 @@ import TextInput from '@/Components/TextInput.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import DialogModal from '@/Components/DialogModal.vue';
import { computed, ref } from 'vue';
import type { CreateProjectBody, Project } from '@/utils/api';
import type { CreateClientBody, CreateProjectBody, Project } from '@/utils/api';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { useProjectsStore } from '@/utils/useProjects';
import { useFocus } from '@vueuse/core';
@@ -26,6 +26,10 @@ const props = defineProps<{
originalProject: Project;
}>();
async function createClient(body: CreateClientBody) {
return await useClientsStore().createClient(body);
}
const project = ref<CreateProjectBody>({
name: props.originalProject.name,
color: props.originalProject.color,
@@ -97,7 +101,11 @@ async function submitBillableRate() {
</div>
<div class="">
<InputLabel for="client" value="Client" />
<ClientDropdown class="mt-1" v-model="project.client_id">
<ClientDropdown
:createClient
:clients="clients"
class="mt-1"
v-model="project.client_id">
<template #trigger>
<Badge
class="bg-input-background cursor-pointer hover:bg-tertiary"

View File

@@ -7,7 +7,12 @@ 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 { CreateProjectBody, Project } from '@/utils/api';
import type {
CreateProjectBody,
Project,
Client,
CreateClientBody,
} from '@/utils/api';
import { useProjectsStore } from '@/utils/useProjects';
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
@@ -17,17 +22,25 @@ defineProps<{
}>();
const showCreateProjectModal = ref(false);
async function createProject(project: CreateProjectBody, callback: () => void) {
await useProjectsStore().createProject(project);
callback();
async function createProject(
project: CreateProjectBody
): Promise<Project | undefined> {
return await useProjectsStore().createProject(project);
}
async function createClient(
client: CreateClientBody
): Promise<Client | undefined> {
return await useClientsStore().createClient(client);
}
const { clients } = storeToRefs(useClientsStore());
</script>
<template>
<ProjectCreateModal
:createProject
:createClient
:clients="clients"
@submit="createProject"
v-model:show="showCreateProjectModal"></ProjectCreateModal>
<div class="flow-root max-w-[100vw] overflow-x-auto">
<div class="inline-block min-w-full align-middle">

View File

@@ -3,16 +3,23 @@ import Dropdown from '@/Components/Dropdown.vue';
import { type Component, computed, ref, watch } from 'vue';
import SelectDropdownItem from '@/Components/Common/SelectDropdownItem.vue';
import { onKeyStroke } from '@vueuse/core';
import { type Placement } from '@floating-ui/vue';
const model = defineModel<string | null>({
default: null,
});
const props = defineProps<{
items: T[];
getKeyFromItem: (item: T) => string | null;
getNameForItem: (item: T) => string;
}>();
const props = withDefaults(
defineProps<{
items: T[];
getKeyFromItem: (item: T) => string | null;
getNameForItem: (item: T) => string;
align?: Placement;
}>(),
{
align: 'bottom-start',
}
);
const open = ref(false);
const dropdownViewport = ref<Component | null>(null);
@@ -113,7 +120,7 @@ watch(open, () => {
</script>
<template>
<Dropdown v-model="open" align="bottom-start" :closeOnContentClick="false">
<Dropdown v-model="open" :align="align" :closeOnContentClick="false">
<template #trigger>
<slot name="trigger"> </slot>
</template>

View File

@@ -29,7 +29,7 @@ function addOrRemoveTagFromSelection(id: string) {
if (model.value.includes(id)) {
model.value = model.value.filter((tagId) => tagId !== id);
} else {
model.value.push(id);
model.value = [...model.value, id];
}
emit('changed');
}

View File

@@ -1,9 +1,16 @@
<script setup lang="ts">
import MainContainer from '@/Pages/MainContainer.vue';
import TimeTrackerStartStop from '@/Components/Common/TimeTrackerStartStop.vue';
import type { Project, Tag, Task, TimeEntry } from '@/utils/api';
import type {
CreateClientBody,
CreateProjectBody,
Project,
Tag,
Task,
TimeEntry,
Client,
} from '@/utils/api';
import TimeEntryDescriptionInput from '@/Components/Common/TimeEntry/TimeEntryDescriptionInput.vue';
import { type TimeEntriesGroupedByType } from '@/utils/useTimeEntries';
import TimeEntryRowTagDropdown from '@/Components/Common/TimeEntry/TimeEntryRowTagDropdown.vue';
import TimeEntryMoreOptionsDropdown from '@/Components/Common/TimeEntry/TimeEntryMoreOptionsDropdown.vue';
import TimeTrackerProjectTaskDropdown from '@/Components/Common/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
@@ -15,13 +22,17 @@ import {
} from '../../../utils/time';
import TimeEntryRow from '@/Components/Common/TimeEntry/TimeEntryRow.vue';
import GroupedItemsCountButton from '@/Components/Common/GroupedItemsCountButton.vue';
import type { TimeEntriesGroupedByType } from '@/types/time-entries';
const props = defineProps<{
timeEntry: TimeEntriesGroupedByType;
projects: Project[];
tasks: Task[];
tags: Tag[];
clients: Client[];
createTag: (name: string) => Promise<Tag | undefined>;
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
onStartStopClick: (timeEntry: TimeEntry) => void;
updateTimeEntries: (timeEntries: TimeEntry[]) => void;
deleteTimeEntries: (timeEntries: TimeEntry[]) => void;
@@ -64,7 +75,7 @@ const expanded = ref(false);
data-testid="time_entry_row">
<MainContainer>
<div class="sm:flex py-1.5 items-center justify-between group">
<div class="flex space-x-3 items-center">
<div class="flex space-x-3 items-center min-w-0">
<input
type="checkbox"
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
@@ -81,6 +92,9 @@ const expanded = ref(false);
"></TimeEntryDescriptionInput>
</div>
<TimeTrackerProjectTaskDropdown
:clients
:createProject
:createClient
:projects="projects"
:tasks="tasks"
:showBadgeBorder="false"
@@ -134,6 +148,9 @@ const expanded = ref(false);
<TimeEntryRow
:projects="projects"
:tasks="tasks"
:createClient
:clients
:createProject
:tags="tags"
indent
:updateTimeEntry="(arg) => updateTimeEntries([arg])"

View File

@@ -17,15 +17,36 @@ import { storeToRefs } from 'pinia';
import { useTasksStore } from '@/utils/useTasks';
import { useProjectsStore } from '@/utils/useProjects';
import { useTagsStore } from '@/utils/useTags';
import type {
CreateClientBody,
CreateProjectBody,
Project,
Client,
} from '@/utils/api';
import { useClientsStore } from '@/utils/useClients';
const projectStore = useProjectsStore();
const { projects } = storeToRefs(projectStore);
const taskStore = useTasksStore();
const { tasks } = storeToRefs(taskStore);
const clientStore = useClientsStore();
const { clients } = storeToRefs(clientStore);
const { createTimeEntry } = useTimeEntriesStore();
const show = defineModel('show', { default: false });
const saving = ref(false);
async function createProject(
project: CreateProjectBody
): Promise<Project | undefined> {
return await useProjectsStore().createProject(project);
}
async function createClient(
body: CreateClientBody
): Promise<Client | undefined> {
return await useClientsStore().createClient(body);
}
const description = ref<HTMLInputElement | null>(null);
watch(show, (value) => {
@@ -102,6 +123,9 @@ async function createTag(tag: string) {
<div class="flex items-center justify-between">
<div>
<TimeTrackerProjectTaskDropdown
:clients
:createProject
:createClient
class="mt-1"
size="xlarge"
:projects="projects"

View File

@@ -1,29 +1,35 @@
<script setup lang="ts">
import { computed } from 'vue';
import type {
CreateClientBody,
CreateProjectBody,
CreateTimeEntryBody,
Project,
Tag,
Task,
TimeEntry,
Client,
} from '@/utils/api';
import { getDayJsInstance, getLocalizedDateFromTimestamp } from '@/utils/time';
import type { TimeEntriesGroupedByType } from '@/utils/useTimeEntries';
import TimeEntryAggregateRow from '@/Components/Common/TimeEntry/TimeEntryAggregateRow.vue';
import TimeEntryRowHeading from '@/Components/Common/TimeEntry/TimeEntryRowHeading.vue';
import TimeEntryRow from '@/Components/Common/TimeEntry/TimeEntryRow.vue';
import dayjs from 'dayjs';
import type { TimeEntriesGroupedByType } from '@/types/time-entries';
const props = defineProps<{
timeEntries: TimeEntry[];
projects: Project[];
tasks: Task[];
tags: Tag[];
clients: Client[];
createTag: (name: string) => Promise<Tag | undefined>;
updateTimeEntry: (entry: TimeEntry) => void;
updateTimeEntries: (entries: TimeEntry[]) => void;
deleteTimeEntries: (entries: TimeEntry[]) => void;
createTimeEntry: (entry: Omit<CreateTimeEntryBody, 'member_id'>) => void;
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
}>();
const groupedTimeEntries = computed(() => {
@@ -109,9 +115,12 @@ function startTimeEntryFromExisting(entry: TimeEntry) {
<TimeEntryRowHeading :date="key"></TimeEntryRowHeading>
<template v-for="entry in value" :key="entry.id">
<TimeEntryAggregateRow
:createProject
:createClient
:projects="projects"
:tasks="tasks"
:tags="tags"
:clients
:onStartStopClick="startTimeEntryFromExisting"
:updateTimeEntries
:deleteTimeEntries
@@ -119,9 +128,12 @@ function startTimeEntryFromExisting(entry: TimeEntry) {
v-if="'timeEntries' in entry && entry.timeEntries.length > 1"
:time-entry="entry"></TimeEntryAggregateRow>
<TimeEntryRow
:createClient
:createProject
:projects="projects"
:tasks="tasks"
:tags="tags"
:clients
:createTag
:updateTimeEntry
:onStartStopClick="() => startTimeEntryFromExisting(entry)"

View File

@@ -2,7 +2,15 @@
import MainContainer from '@/Pages/MainContainer.vue';
import TimeTrackerStartStop from '@/Components/Common/TimeTrackerStartStop.vue';
import TimeEntryRangeSelector from '@/Components/Common/TimeEntry/TimeEntryRangeSelector.vue';
import type { Project, Tag, Task, TimeEntry } from '@/utils/api';
import type {
Client,
CreateClientBody,
CreateProjectBody,
Project,
Tag,
Task,
TimeEntry,
} from '@/utils/api';
import TimeEntryDescriptionInput from '@/Components/Common/TimeEntry/TimeEntryDescriptionInput.vue';
import TimeEntryRowTagDropdown from '@/Components/Common/TimeEntry/TimeEntryRowTagDropdown.vue';
import TimeEntryRowDurationInput from '@/Components/Common/TimeEntry/TimeEntryRowDurationInput.vue';
@@ -16,7 +24,10 @@ const props = defineProps<{
projects: Project[];
tasks: Task[];
tags: Tag[];
clients: Client[];
createTag: (name: string) => Promise<Tag | undefined>;
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
onStartStopClick: () => void;
deleteTimeEntry: () => void;
updateTimeEntry: (timeEntry: TimeEntry) => void;
@@ -54,7 +65,7 @@ function updateProjectAndTask(projectId: string, taskId: string) {
<MainContainer>
<div
class="sm:flex py-1 lg:py-1.5 items-center justify-between group">
<div class="flex space-x-1 items-center">
<div class="flex space-x-1 items-center min-w-0">
<input
type="checkbox"
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
@@ -66,6 +77,9 @@ function updateProjectAndTask(projectId: string, taskId: string) {
timeEntry.description
"></TimeEntryDescriptionInput>
<TimeTrackerProjectTaskDropdown
:createProject
:createClient
:clients
:projects="projects"
:tasks="tasks"
:showBadgeBorder="false"

View File

@@ -0,0 +1,145 @@
<script setup lang="ts">
import TimeTrackerTagDropdown from '@/Components/Common/TimeTracker/TimeTrackerTagDropdown.vue';
import TimeTrackerStartStop from '@/Components/Common/TimeTrackerStartStop.vue';
import TimeTrackerRangeSelector from '@/Components/Common/TimeTracker/TimeTrackerRangeSelector.vue';
import BillableToggleButton from '@/Components/Common/BillableToggleButton.vue';
import TimeTrackerProjectTaskDropdown from '@/Components/Common/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import type {
CreateClientBody,
CreateProjectBody,
Project,
Tag,
Task,
TimeEntry,
Client,
} from '@/utils/api';
import { ref } from 'vue';
import type { Dayjs } from 'dayjs';
const currentTimeEntry = defineModel<TimeEntry>('currentTimeEntry', {
required: true,
});
const liveTimer = defineModel<Dayjs | null>('liveTimer', { required: true });
const currentTimeEntryDescriptionInput = ref<HTMLInputElement | null>(null);
const props = defineProps<{
projects: Project[];
tasks: Task[];
tags: Tag[];
clients: Client[];
createTag: (name: string) => Promise<Tag | undefined>;
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
isActive: boolean;
}>();
const emit = defineEmits<{
startTimer: [];
stopTimer: [];
updateTimeEntry: [];
startLiveTimer: [];
stopLiveTimer: [];
}>();
function updateProject() {
setBillableDefaultForProject();
emit('updateTimeEntry');
}
function startTimerIfNotActive() {
if (!props.isActive) {
emit('startTimer');
}
}
function setBillableDefaultForProject() {
const project = props.projects.find(
(project) => project.id === currentTimeEntry.value.project_id
);
if (project) {
currentTimeEntry.value.billable = project.is_billable;
}
}
function onToggleButtonPress(newState: boolean) {
if (newState) {
emit('startTimer');
currentTimeEntryDescriptionInput.value?.focus();
} else {
emit('stopTimer');
}
}
</script>
<template>
<div
class="flex items-center relative @container"
data-testid="dashboard_timer">
<div
class="flex flex-col sm:flex-row w-full justify-between rounded-lg bg-card-background border-card-border border transition shadow-card">
<div class="flex items-center pr-6">
<input
placeholder="What are you working on?"
data-testid="time_entry_description"
ref="currentTimeEntryDescriptionInput"
v-model="currentTimeEntry.description"
@keydown.enter="startTimerIfNotActive"
@blur="$emit('updateTimeEntry')"
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator lg:px-4 text-base @4xl:text-lg text-white font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition"
type="text" />
</div>
<div class="flex items-center justify-between pl-2 shrink min-w-0">
<div
class="flex items-center w-[130px] sm:w-auto shrink min-w-0">
<TimeTrackerProjectTaskDropdown
:createClient
:clients
:createProject
:projects="projects"
:tasks="tasks"
@changed="updateProject"
v-model:project="currentTimeEntry.project_id"
v-model:task="
currentTimeEntry.task_id
"></TimeTrackerProjectTaskDropdown>
</div>
<div class="flex items-center lg:space-x-2 px-2 lg:px-4">
<TimeTrackerTagDropdown
@changed="$emit('updateTimeEntry')"
:createTag
:tags="tags"
v-model="
currentTimeEntry.tags
"></TimeTrackerTagDropdown>
<BillableToggleButton
@changed="$emit('updateTimeEntry')"
v-model="
currentTimeEntry.billable
"></BillableToggleButton>
</div>
<div class="border-l border-card-border">
<TimeTrackerRangeSelector
@startLiveTimer="emit('startLiveTimer')"
@stopLiveTimer="emit('stopLiveTimer')"
@updateTimer="emit('updateTimeEntry')"
@startTimer="emit('startTimer')"
v-model:currentTimeEntry="currentTimeEntry"
v-model:liveTimer="liveTimer"
@keydown.enter="
startTimerIfNotActive
"></TimeTrackerRangeSelector>
</div>
</div>
</div>
<div
class="pl-4 lg:pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0">
<TimeTrackerStartStop
:active="isActive"
@changed="onToggleButtonPress"
size="large"></TimeTrackerStartStop>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -3,14 +3,17 @@ import { ChevronRightIcon } from '@heroicons/vue/16/solid';
import Dropdown from '@/Components/Dropdown.vue';
import { type Component, computed, nextTick, ref, watch } from 'vue';
import ProjectDropdownItem from '@/Components/Common/Project/ProjectDropdownItem.vue';
import type { CreateProjectBody, Project, Task } from '@/utils/api';
import type {
CreateClientBody,
CreateProjectBody,
Project,
Task,
Client,
} from '@/utils/api';
import ProjectBadge from '@/Components/Common/Project/ProjectBadge.vue';
import Badge from '@/Components/Common/Badge.vue';
import { PlusIcon, PlusCircleIcon } from '@heroicons/vue/16/solid';
import ProjectCreateModal from '@/Components/Common/Project/ProjectCreateModal.vue';
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
import { useProjectsStore } from '@/utils/useProjects';
const task = defineModel<string | null>('task', {
default: null,
@@ -46,6 +49,11 @@ const props = withDefaults(
size: 'base' | 'large' | 'xlarge';
projects: Project[];
tasks: Task[];
clients: Client[];
createProject: (
project: CreateProjectBody
) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
}>(),
{
showBadgeBorder: true,
@@ -309,14 +317,6 @@ function selectProject(projectId: string) {
}
const showCreateProject = ref(false);
const clientsStore = useClientsStore();
const { clients } = storeToRefs(clientsStore);
async function createProject(project: CreateProjectBody, callback: () => void) {
await useProjectsStore().createProject(project);
callback();
}
</script>
<template>
@@ -338,15 +338,19 @@ async function createProject(project: CreateProjectBody, callback: () => void) {
:border="showBadgeBorder"
tag="button"
:name="selectedProjectName"
class="focus:border-border-tertiary focus:outline-0 focus:bg-card-background-separator hover:bg-card-background-separator">
<div class="flex nowrap items-center space-x-1">
class="focus:border-border-tertiary w-full focus:outline-0 focus:bg-card-background-separator min-w-0 hover:bg-card-background-separator">
<div class="flex items-center lg:space-x-1 min-w-0">
<span class="whitespace-nowrap text-xs lg:text-sm">
{{ selectedProjectName }}
</span>
<ChevronRightIcon
v-if="currentTask"
class="w-5 text-muted"></ChevronRightIcon>
<span v-if="currentTask">{{ currentTask.name }}</span>
class="w-4 lg:w-5 text-muted shrink-0"></ChevronRightIcon>
<div
class="min-w-0 shrink text-xs lg:text-sm truncate"
v-if="currentTask">
{{ currentTask.name }}
</div>
</div>
</ProjectBadge>
</template>
@@ -418,8 +422,9 @@ async function createProject(project: CreateProjectBody, callback: () => void) {
</template>
</Dropdown>
<ProjectCreateModal
:createClient
:clients="clients"
@submit="createProject"
:createProject
v-model:show="showCreateProject"></ProjectCreateModal>
</template>

View File

@@ -2,23 +2,28 @@
import Dropdown from '@/Components/Dropdown.vue';
import { computed, ref } from 'vue';
import TimeRangeSelector from '@/Components/Common/TimeRangeSelector.vue';
import dayjs from 'dayjs';
import dayjs, { Dayjs } from 'dayjs';
import parse from 'parse-duration';
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
import { storeToRefs } from 'pinia';
import { formatDuration, getDayJsInstance } from '@/utils/time';
const currentTimeEntryStore = useCurrentTimeEntryStore();
const { startLiveTimer, stopLiveTimer, updateTimer, startTimer } =
currentTimeEntryStore;
const { currentTimeEntry, now } = storeToRefs(currentTimeEntryStore);
import type { TimeEntry } from '@/utils/api';
defineEmits(['changed']);
const currentTimeEntry = defineModel<TimeEntry>('currentTimeEntry', {
required: true,
});
const now = defineModel<null | Dayjs>('liveTimer');
const emit = defineEmits<{
startLiveTimer: [];
stopLiveTimer: [];
updateTimer: [];
startTimer: [];
}>();
const open = ref(false);
function pauseLiveTimerUpdate(event: FocusEvent) {
(event.target as HTMLInputElement).select();
stopLiveTimer();
emit('stopLiveTimer');
}
function onTimeEntryEnterPress() {
@@ -59,9 +64,9 @@ function updateTimerAndStartLiveTimerUpdate() {
);
currentTimeEntry.value.start = newStartDate.utc().format();
if (currentTimeEntry.value.id !== '') {
currentTimeEntryStore.updateTimer();
emit('updateTimer');
} else {
currentTimeEntryStore.startTimer();
emit('startTimer');
}
} else if (isHHMM(temporaryCustomTimerEntry.value)) {
const results = parseHHMM(temporaryCustomTimerEntry.value);
@@ -71,9 +76,9 @@ function updateTimerAndStartLiveTimerUpdate() {
.subtract(parseInt(results[2]), 'm');
currentTimeEntry.value.start = newStartDate.utc().format();
if (currentTimeEntry.value.id !== '') {
currentTimeEntryStore.updateTimer();
emit('updateTimer');
} else {
currentTimeEntryStore.startTimer();
emit('startTimer');
}
}
}
@@ -82,15 +87,15 @@ function updateTimerAndStartLiveTimerUpdate() {
const newStartDate = dayjs().subtract(time, 's');
currentTimeEntry.value.start = newStartDate.utc().format();
if (currentTimeEntry.value.id !== '') {
currentTimeEntryStore.updateTimer();
emit('updateTimer');
} else {
currentTimeEntryStore.startTimer();
emit('startTimer');
}
}
// fallback to minutes if just a number is given
now.value = dayjs().utc();
temporaryCustomTimerEntry.value = '';
startLiveTimer();
emit('startLiveTimer');
}
function isNumeric(value: string) {
@@ -113,9 +118,9 @@ async function updateTimeRange(newStart: string) {
if (getDayJsInstance()(newStart).isBefore(getDayJsInstance()())) {
currentTimeEntry.value.start = newStart;
if (currentTimeEntry.value.id) {
await updateTimer();
emit('updateTimer');
} else {
await startTimer();
emit('startTimer');
}
}
}
@@ -143,7 +148,7 @@ const startTime = computed(() => {
@blur="updateTimerAndStartLiveTimerUpdate"
@keydown.enter="onTimeEntryEnterPress"
v-model="currentTime"
class="w-[110px] sm:w-[130px] h-full text-white py-2.5 rounded-r-lg text-center px-4 text-sm sm:text-lg font-bold bg-card-background border-none placeholder-muted focus:ring-0 transition"
class="w-[110px] lg:w-[130px] h-full text-white py-2.5 rounded-r-lg text-center px-4 text-base lg:text-lg font-bold bg-card-background border-none placeholder-muted focus:ring-0 transition"
type="text" />
</template>
<template #content>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import SecondaryButton from '../../../../../extensions/Billing/vendor/laravel/spark-paddle/resources/js/Components/SecondaryButton.vue';
defineEmits<{
switchOrganization: [];
}>();
</script>
<template>
<div
class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center">
<div
class="w-full h-[calc(100%+10px)] absolute bg-default-background opacity-75 backdrop-blur-sm"></div>
<div class="flex space-x-3 items-center w-full z-20 justify-center">
<span class="text-sm text-white">
The Timer is running in a different organization.
</span>
<SecondaryButton @click="$emit('switchOrganization')"
>Switch to organization</SecondaryButton
>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -37,10 +37,10 @@ defineProps<{
:class="
twMerge(
iconColorClasses,
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus-visible:bg-card-background-separator hover:bg-card-background-separator rounded-full w-7 sm:w-10 h-7 sm:h-10 flex items-center justify-center'
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus-visible:bg-card-background-separator hover:bg-card-background-separator rounded-full w-11 h-11 flex items-center justify-center'
)
">
<TagIcon class="w-5 sm:w-6 h-5 sm:h-6"></TagIcon>
<TagIcon class="w-5 h-5 lg:h-6 lg:w-6"></TagIcon>
<span
v-if="model.length > 1"
class="font-extrabold absolute rounded-full text-xs w-3 h-3 block top-[15px] rotate-[45deg] right-[14px] text-card-background">

View File

@@ -6,7 +6,7 @@ const emit = defineEmits(['changed']);
const props = withDefaults(
defineProps<{
size: 'base' | 'large';
size: 'base' | 'large' | 'small';
active: boolean;
}>(),
{
@@ -15,10 +15,12 @@ const props = withDefaults(
}
);
const buttonSizeClasses = {
small: 'w-6 h-6 bg-accent-200/40 hover:bg-accent-300/70',
base: 'w-8 h-8 bg-accent-200/40 hover:scale-110 hover:bg-accent-300/70 ring-accent-200/10 focus:ring-accent-200/10 hover:ring-4',
large: 'w-11 h-11 ring-accent-200/10 focus:ring-accent-200/20 ring-4 sm:ring-8 hover:scale-110',
};
const iconClass = {
small: 'w-2.5 h-2.5',
base: 'w-3.5 h-3.5',
large: 'w-4 h-4',
};

View File

@@ -1,6 +1,11 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { flip, type Placement, useFloating } from '@floating-ui/vue';
import {
flip,
type Placement,
type ReferenceElement,
useFloating,
} from '@floating-ui/vue';
import { offset } from '@floating-ui/vue';
import { autoUpdate } from '@floating-ui/vue';
@@ -49,7 +54,7 @@ function onBackgroundClick() {
open.value = false;
}
const reference = ref(null);
const reference = ref<null | ReferenceElement>(null);
const floating = ref(null);
const { floatingStyles } = useFloating(reference, floating, {
placement: props.align,
@@ -59,37 +64,36 @@ const { floatingStyles } = useFloating(reference, floating, {
</script>
<template>
<div>
<div @click.prevent="toggleOpen" ref="reference">
<div class="min-w-0">
<div @click.prevent="toggleOpen" ref="reference" class="min-w-0">
<slot name="trigger" />
</div>
<!-- Full Screen Dropdown Overlay -->
<div
v-show="open"
class="fixed inset-0 z-40"
@click.prevent="onBackgroundClick" />
<Teleport to="body">
<div
v-show="open"
ref="floating"
class="z-50"
:style="floatingStyles"
@click="onContentClick">
<transition
enter-active-class="transition ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95">
class="fixed inset-0 z-40"
@click.prevent="onBackgroundClick" />
<transition
enter-active-class="transition-opacity ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition-opacity ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95">
<div
v-if="open"
class="z-50"
ref="floating"
:style="floatingStyles"
@click="onContentClick">
<div
v-if="open"
class="rounded-lg ring-1 relative ring-black ring-opacity-5 border border-card-border overflow-none shadow-dropdown bg-card-background">
<slot name="content" />
</div>
</transition>
</div>
</div>
</transition>
</Teleport>
</div>
</template>

View File

@@ -1,26 +1,24 @@
<script setup lang="ts">
import { ClockIcon } from '@heroicons/vue/20/solid';
import CardTitle from '@/Components/Common/CardTitle.vue';
import BillableToggleButton from '@/Components/Common/BillableToggleButton.vue';
import TimeTrackerStartStop from '@/Components/Common/TimeTrackerStartStop.vue';
import { usePage } from '@inertiajs/vue3';
import { type User } from '@/types/models';
import { computed, onMounted, ref, watch } from 'vue';
import { computed, onMounted, watch } from 'vue';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import duration from 'dayjs/plugin/duration';
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
import { storeToRefs } from 'pinia';
import TimeTrackerTagDropdown from '@/Components/Common/TimeTracker/TimeTrackerTagDropdown.vue';
import TimeTrackerProjectTaskDropdown from '@/Components/Common/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import { getCurrentOrganizationId } from '@/utils/useUser';
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';
import { useTagsStore } from '@/utils/useTags';
import TimeTrackerControls from '@/Components/Common/TimeTracker/TimeTrackerControls.vue';
import type { CreateClientBody, CreateProjectBody, Project } from '@/utils/api';
import TimeTrackerRunningInDifferentOrganizationOverlay from '@/Components/Common/TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue';
import { useClientsStore } from '@/utils/useClients';
const page = usePage<{
auth: {
@@ -34,12 +32,13 @@ dayjs.extend(utc);
const currentTimeEntryStore = useCurrentTimeEntryStore();
const { currentTimeEntry, isActive, now } = storeToRefs(currentTimeEntryStore);
const { startLiveTimer, stopLiveTimer, setActiveState } = currentTimeEntryStore;
const currentTimeEntryDescriptionInput = ref<HTMLInputElement | null>(null);
const projectStore = useProjectsStore();
const { projects } = storeToRefs(projectStore);
const taskStore = useTasksStore();
const { tasks } = storeToRefs(taskStore);
const clientStore = useClientsStore();
const { clients } = storeToRefs(clientStore);
watch(isActive, () => {
if (isActive.value) {
@@ -56,41 +55,12 @@ onMounted(async () => {
}
});
function setBillableDefaultForProject() {
const projectssStore = useProjectsStore();
const { projects } = storeToRefs(projectssStore);
const project = projects.value.find(
(project) => project.id === currentTimeEntry.value.project_id
);
if (project) {
currentTimeEntry.value.billable = project.is_billable;
}
}
function updateProject() {
setBillableDefaultForProject();
updateTimeEntry();
}
function updateTimeEntry() {
if (currentTimeEntry.value.id) {
useCurrentTimeEntryStore().updateTimer();
}
}
function onToggleButtonPress(newState: boolean) {
setActiveState(newState);
if (newState) {
currentTimeEntryDescriptionInput.value?.focus();
}
}
function startTimerIfNotActive() {
if (!isActive.value) {
setActiveState(true);
}
}
const isRunningInDifferentOrganization = computed(() => {
return (
currentTimeEntry.value.organization_id &&
@@ -99,6 +69,15 @@ const isRunningInDifferentOrganization = computed(() => {
);
});
async function createProject(
project: CreateProjectBody
): Promise<Project | undefined> {
return await useProjectsStore().createProject(project);
}
async function createClient(client: CreateClientBody) {
return await useClientsStore().createClient(client);
}
function switchToTimeEntryOrganization() {
if (currentTimeEntry.value.organization_id) {
switchOrganization(currentTimeEntry.value.organization_id);
@@ -114,74 +93,27 @@ const { tags } = storeToRefs(useTagsStore());
<template>
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
<div class="relative">
<div
class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center"
v-if="isRunningInDifferentOrganization">
<div
class="w-full h-[calc(100%+10px)] absolute bg-default-background opacity-75 backdrop-blur-sm"></div>
<div class="flex space-x-3 items-center w-full z-20 justify-center">
<span class="text-sm text-white">
The Timer is running in a different organization.
</span>
<SecondaryButton @click="switchToTimeEntryOrganization"
>Switch to organization</SecondaryButton
>
</div>
</div>
<div class="flex items-center relative" data-testid="dashboard_timer">
<div
class="flex flex-col sm:flex-row w-full rounded-lg bg-card-background border-card-border border transition shadow-card">
<div class="flex-1 flex items-center pr-6">
<input
placeholder="What are you working on?"
data-testid="time_entry_description"
ref="currentTimeEntryDescriptionInput"
v-model="currentTimeEntry.description"
@keydown.enter="startTimerIfNotActive"
@blur="updateTimeEntry"
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3 border-b border-b-card-background-separator sm:px-4 text-base sm:text-lg text-white font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition"
type="text" />
</div>
<div class="flex items-center justify-between pl-2">
<div class="flex items-center w-[130px] sm:w-auto">
<TimeTrackerProjectTaskDropdown
:projects="projects"
:tasks="tasks"
@changed="updateProject"
v-model:project="currentTimeEntry.project_id"
v-model:task="
currentTimeEntry.task_id
"></TimeTrackerProjectTaskDropdown>
</div>
<div class="flex items-center space-x-2 px-4">
<TimeTrackerTagDropdown
@changed="updateTimeEntry"
:createTag
:tags="tags"
v-model="
currentTimeEntry.tags
"></TimeTrackerTagDropdown>
<BillableToggleButton
@changed="updateTimeEntry"
v-model="
currentTimeEntry.billable
"></BillableToggleButton>
</div>
<div class="border-l border-card-border">
<TimeTrackerRangeSelector
@keydown.enter="
startTimerIfNotActive
"></TimeTrackerRangeSelector>
</div>
</div>
</div>
<div
class="pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0">
<TimeTrackerStartStop
:active="isActive"
@changed="onToggleButtonPress"
size="large"></TimeTrackerStartStop>
</div>
</div>
<TimeTrackerRunningInDifferentOrganizationOverlay
@switchOrganization="switchToTimeEntryOrganization"
v-if="
isRunningInDifferentOrganization
"></TimeTrackerRunningInDifferentOrganizationOverlay>
<TimeTrackerControls
:createProject
:createClient
:clients
:tags
:tasks
:projects
:createTag
:isActive
v-model:currentTimeEntry="currentTimeEntry"
v-model:liveTimer="now"
@startLiveTimer="startLiveTimer"
@stopLiveTimer="stopLiveTimer"
@startTimer="setActiveState(true)"
@stopTimer="setActiveState(false)"
@updateTimeEntry="updateTimeEntry"></TimeTrackerControls>
</div>
</template>

View File

@@ -13,7 +13,12 @@ import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
import { storeToRefs } from 'pinia';
import { useClientsStore } from '@/utils/useClients';
import type { CreateProjectBody } from '@/utils/api';
import type {
CreateClientBody,
Client,
CreateProjectBody,
Project,
} from '@/utils/api';
onMounted(() => {
useProjectsStore().fetchProjects();
@@ -27,11 +32,6 @@ function isActiveTab(tab: string) {
return activeTab.value === tab;
}
async function createProject(project: CreateProjectBody, callback: () => void) {
await useProjectsStore().createProject(project);
callback();
}
const { projects } = storeToRefs(useProjectsStore());
const shownProjects = computed(() => {
@@ -42,6 +42,16 @@ const shownProjects = computed(() => {
return project.is_archived;
});
});
async function createProject(
project: CreateProjectBody
): Promise<Project | undefined> {
return await useProjectsStore().createProject(project);
}
async function createClient(
client: CreateClientBody
): Promise<Client | undefined> {
return await useClientsStore().createClient(client);
}
</script>
<template>
@@ -70,6 +80,8 @@ const shownProjects = computed(() => {
>Create Project
</SecondaryButton>
<ProjectCreateModal
:createProject
:createClient
:clients="clients"
@submit="createProject"
v-model:show="showCreateProjectModal"></ProjectCreateModal>

View File

@@ -35,10 +35,10 @@ import { getCurrentMembershipId, getCurrentRole } from '@/utils/useUser';
import ClientMultiselectDropdown from '@/Components/Common/Client/ClientMultiselectDropdown.vue';
import { useTagsStore } from '@/utils/useTags';
const startDate = ref<string | null>(
const startDate = ref<string>(
getLocalizedDayJs(getDayJsInstance()().format()).subtract(14, 'd').format()
);
const endDate = ref<string | null>(
const endDate = ref<string>(
getLocalizedDayJs(getDayJsInstance()().format()).format()
);
const selectedTags = ref<string[]>([]);

View File

@@ -5,7 +5,14 @@ import { onMounted, ref, watch } from 'vue';
import MainContainer from '@/Pages/MainContainer.vue';
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
import { storeToRefs } from 'pinia';
import type { CreateTimeEntryBody, TimeEntry } from '@/utils/api';
import type {
CreateClientBody,
CreateProjectBody,
CreateTimeEntryBody,
Project,
TimeEntry,
Client,
} from '@/utils/api';
import { useElementVisibility } from '@vueuse/core';
import { ClockIcon } from '@heroicons/vue/20/solid';
import SecondaryButton from '@/Components/SecondaryButton.vue';
@@ -17,6 +24,7 @@ import { useTasksStore } from '@/utils/useTasks';
import { useProjectsStore } from '@/utils/useProjects';
import TimeEntryGroupedTable from '@/Components/Common/TimeEntry/TimeEntryGroupedTable.vue';
import { useTagsStore } from '@/utils/useTags';
import { useClientsStore } from '@/utils/useClients';
const timeEntriesStore = useTimeEntriesStore();
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
@@ -76,10 +84,22 @@ const projectStore = useProjectsStore();
const { projects } = storeToRefs(projectStore);
const taskStore = useTasksStore();
const { tasks } = storeToRefs(taskStore);
const clientStore = useClientsStore();
const { clients } = storeToRefs(clientStore);
async function createTag(name: string) {
return await useTagsStore().createTag(name);
}
async function createProject(
project: CreateProjectBody
): Promise<Project | undefined> {
return await useProjectsStore().createProject(project);
}
async function createClient(
body: CreateClientBody
): Promise<Client | undefined> {
return await useClientsStore().createClient(body);
}
</script>
<template>
@@ -104,6 +124,9 @@ async function createTag(name: string) {
</div>
</MainContainer>
<TimeEntryGroupedTable
:createProject
:clients
:createClient
:updateTimeEntry
:updateTimeEntries
:deleteTimeEntries

3
resources/js/types/time-entries.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
import type { TimeEntry } from '@/utils/api';
export type TimeEntriesGroupedByType = TimeEntry & { timeEntries: TimeEntry[] };

View File

@@ -128,6 +128,13 @@ export type UpdateOrganizationBody = ZodiosBodyByAlias<
'updateOrganization'
>;
export type MyMemberships = ZodiosResponseByAlias<
SolidTimeApi,
'getMyMemberships'
>['data'];
export type MyMembership = MyMemberships[0];
export async function fetchToken() {
return new Promise((resolve) => {
router.reload({

View File

@@ -90,7 +90,7 @@ export const useNotificationsStore = defineStore('notifications', () => {
);
}
}
throw new Error('Failed to handle API request', { cause: error });
throw new Error('Failed to handle API request');
}
}

View File

@@ -76,15 +76,12 @@ export const useClientsStore = defineStore('clients', () => {
if (organization) {
await handleApiRequestNotifications(
() =>
api.deleteClient(
{},
{
params: {
organization: organization,
client: clientId,
},
}
),
api.deleteClient(undefined, {
params: {
organization: organization,
client: clientId,
},
}),
'Client deleted successfully',
'Failed to delete client'
);

View File

@@ -36,15 +36,12 @@ export const useMembersStore = defineStore('members', () => {
if (organization) {
await handleApiRequestNotifications(
() =>
api.removeMember(
{},
{
params: {
organization: organization,
member: membershipId,
},
}
),
api.removeMember(undefined, {
params: {
organization: organization,
member: membershipId,
},
}),
'Member deleted successfully',
'Failed to delete member'
);

View File

@@ -81,15 +81,12 @@ export const useProjectMembersStore = defineStore('project-members', () => {
if (organizationId) {
await handleApiRequestNotifications(
() =>
api.deleteProjectMember(
{},
{
params: {
organization: organizationId,
projectMember: projectMemberId,
},
}
),
api.deleteProjectMember(undefined, {
params: {
organization: organizationId,
projectMember: projectMemberId,
},
}),
'Project member removed successfully',
'Failed to remove project member'
);

View File

@@ -37,7 +37,7 @@ export const useProjectsStore = defineStore('projects', () => {
async function createProject(projectBody: CreateProjectBody) {
const organization = getCurrentOrganizationId();
if (organization) {
await handleApiRequestNotifications(
const response = await handleApiRequestNotifications(
() =>
api.createProject(projectBody, {
params: {
@@ -49,6 +49,7 @@ export const useProjectsStore = defineStore('projects', () => {
);
await fetchProjects();
return response['data'];
}
}
@@ -57,15 +58,12 @@ export const useProjectsStore = defineStore('projects', () => {
if (organizationId) {
await handleApiRequestNotifications(
() =>
api.deleteProject(
{},
{
params: {
organization: organizationId,
project: projectId,
},
}
),
api.deleteProject(undefined, {
params: {
organization: organizationId,
project: projectId,
},
}),
'Project deleted successfully',
'Failed to delete project'
);

View File

@@ -36,15 +36,12 @@ export const useTagsStore = defineStore('tags', () => {
if (organizationId) {
await handleApiRequestNotifications(
() =>
api.deleteTag(
{},
{
params: {
organization: organizationId,
tag: tagId,
},
}
),
api.deleteTag(undefined, {
params: {
organization: organizationId,
tag: tagId,
},
}),
'Tag deleted successfully',
'Failed to delete tag'
);

View File

@@ -68,15 +68,12 @@ export const useTasksStore = defineStore('tasks', () => {
if (organizationId) {
await handleApiRequestNotifications(
() =>
api.deleteTask(
{},
{
params: {
organization: organizationId,
task: taskId,
},
}
),
api.deleteTask(undefined, {
params: {
organization: organizationId,
task: taskId,
},
}),
'Task deleted successfully',
'Failed to delete task'
);

View File

@@ -9,8 +9,6 @@ import type { CreateTimeEntryBody, TimeEntry } from '@/utils/api';
import dayjs from 'dayjs';
import { useNotificationsStore } from '@/utils/notification';
export type TimeEntriesGroupedByType = TimeEntry & { timeEntries: TimeEntry[] };
export const useTimeEntriesStore = defineStore('timeEntries', () => {
const timeEntries = ref<TimeEntry[]>(reactive([]));
@@ -124,15 +122,12 @@ export const useTimeEntriesStore = defineStore('timeEntries', () => {
if (organizationId) {
await handleApiRequestNotifications(
() =>
api.deleteTimeEntry(
{},
{
params: {
organization: organizationId,
timeEntry: timeEntryId,
},
}
),
api.deleteTimeEntry(undefined, {
params: {
organization: organizationId,
timeEntry: timeEntryId,
},
}),
'Time entry deleted successfully',
'Failed to delete time entry'
);