Skip to content

Commit 66a5a1a

Browse files
committed
add time entry mass update
1 parent a74c297 commit 66a5a1a

File tree

6 files changed

+1172
-1710
lines changed

6 files changed

+1172
-1710
lines changed

package-lock.json

+1,063-1,695
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
"@electron-toolkit/utils": "^3.0.0",
3434
"@sentry/electron": "^5.3.0",
3535
"@sentry/vite-plugin": "^2.22.2",
36-
"@solidtime/api": "^0.0.3",
37-
"@solidtime/ui": "^0.0.8",
36+
"@solidtime/api": "^0.0.4",
37+
"@solidtime/ui": "^0.0.9",
3838
"electron-updater": "^6.1.7"
3939
},
4040
"devDependencies": {
@@ -69,7 +69,7 @@
6969
"postcss": "^8.4.39",
7070
"prettier": "^3.3.2",
7171
"tailwindcss": "^3.4.4",
72-
"typescript": "^5.5.2",
72+
"typescript": "~5.5.2",
7373
"vite": "^5.3.1",
7474
"vue": "^3.4.30",
7575
"vue-tsc": "^2.0.22",

src/renderer/src/App.vue

-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ declare global {
66
interface Window {
77
getTimezoneSetting: () => string
88
getWeekStartSetting: () => string
9-
__TAURI__: Record<string, unknown>
109
}
1110
}
1211
@@ -60,7 +59,6 @@ import { useQueryClient } from '@tanstack/vue-query'
6059
<template>
6160
<VueQueryDevtools></VueQueryDevtools>
6261
<AutoUpdaterOverlay></AutoUpdaterOverlay>
63-
6462
<div class="h-10 w-full border-b border-border-primary flex justify-end items-center pr-3">
6563
<div class="flex-1 h-full" style="-webkit-app-region: drag"></div>
6664
<div v-if="isLoggedIn" class="flex items-center space-x-2">

src/renderer/src/components/MainTimeEntryTable.vue

+66-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<script setup lang="ts">
22
import { useQuery } from '@tanstack/vue-query'
3-
import { type Component, computed, nextTick, onMounted, watch, watchEffect } from 'vue'
3+
import { type Component, computed, nextTick, onMounted, ref, watch, watchEffect } from 'vue'
44
55
import {
66
TimeEntryGroupedTable,
77
TimeTrackerControls,
88
TimeTrackerRunningInDifferentOrganizationOverlay,
9+
TimeEntryMassActionRow,
910
} from '@solidtime/ui'
1011
import {
1112
emptyTimeEntry,
@@ -17,6 +18,7 @@ import {
1718
useCurrentTimeEntryUpdateMutation,
1819
useTimeEntriesUpdateMutation,
1920
useTimeEntryUpdateMutation,
21+
useTimeEntriesDeleteMutation,
2022
} from '../utils/timeEntries.ts'
2123
import { getAllProjects, useProjectCreateMutation } from '../utils/projects.ts'
2224
import { getAllTasks } from '../utils/tasks.ts'
@@ -27,6 +29,7 @@ import type {
2729
Project,
2830
Tag,
2931
TimeEntry,
32+
UpdateMultipleTimeEntriesChangeset,
3033
} from '@solidtime/api'
3134
import { getAllTags, useTagCreateMutation } from '../utils/tags.ts'
3235
import { LoadingSpinner } from '@solidtime/ui'
@@ -44,11 +47,15 @@ import { apiClient } from '../utils/api'
4447
import { updateTrayState } from '../utils/tray'
4548
import { getMe } from '../utils/me'
4649
47-
const { currentOrganizationId } = useMyMemberships()
48-
const currentOrganizationLoaded = computed(() => !!currentOrganizationId)
50+
const { currentOrganizationId, currentMembership } = useMyMemberships()
51+
const currentOrganizationLoaded = computed(() => !!currentOrganizationId.value)
4952
5053
const { liveTimer, startLiveTimer, stopLiveTimer } = useLiveTimer()
5154
55+
watch(currentOrganizationId, () => {
56+
selectedTimeEntries.value = []
57+
})
58+
5259
const { data: timeEntriesResponse } = useQuery({
5360
queryKey: ['timeEntries', currentOrganizationId],
5461
queryFn: () => getAllTimeEntries(currentOrganizationId.value, currentMembershipId.value),
@@ -61,8 +68,8 @@ const { data: currentTimeEntryResponse, isError: currentTimeEntryResponseIsError
6168
queryFn: () => getCurrentTimeEntry(),
6269
})
6370
64-
const currentTimeEntry = useStorage('currentTimeEntry', { ...emptyTimeEntry })
65-
const lastTimeEntry = useStorage('lastTimeEntry', { ...emptyTimeEntry })
71+
const currentTimeEntry = useStorage<TimeEntry>('currentTimeEntry', { ...emptyTimeEntry })
72+
const lastTimeEntry = useStorage<TimeEntry>('lastTimeEntry', { ...emptyTimeEntry })
6673
6774
watch(timeEntries, () => {
6875
if (timeEntries.value?.[0]) {
@@ -116,6 +123,7 @@ const tags = computed(() => tagsResponse.value?.data)
116123
117124
const currentTimeEntryUpdateMutation = useCurrentTimeEntryUpdateMutation()
118125
const timeEntriesUpdate = useTimeEntriesUpdateMutation()
126+
const timeEntriesDelete = useTimeEntriesDeleteMutation()
119127
const timeEntryUpdate = useTimeEntryUpdateMutation()
120128
const timeEntryDelete = useTimeEntryDeleteMutation()
121129
const timeEntryCreate = useTimeEntryCreateMutation()
@@ -264,8 +272,30 @@ watch(meResponse, () => {
264272
}
265273
})
266274
275+
const selectedTimeEntries = ref([] as TimeEntry[])
276+
277+
function deleteSelected() {
278+
timeEntriesDelete.mutate(selectedTimeEntries.value)
279+
selectedTimeEntries.value = []
280+
}
281+
282+
async function clearSelectionAndState() {
283+
selectedTimeEntries.value = []
284+
}
285+
267286
// TODO: Fix me
268287
const currency = 'EUR'
288+
289+
const canCreateProjects = computed(() => {
290+
if (currentMembership.value) {
291+
return (
292+
currentMembership.value?.role === 'admin' ||
293+
currentMembership.value?.role === 'owner' ||
294+
currentMembership.value?.role === 'manager'
295+
)
296+
}
297+
return false
298+
})
269299
</script>
270300

271301
<template>
@@ -289,6 +319,8 @@ const currency = 'EUR'
289319
v-model:currentTimeEntry="currentTimeEntry"
290320
v-model:liveTimer="liveTimer"
291321
:tags
322+
:enableEstimatedTime="false"
323+
:canCreateProject="canCreateProjects"
292324
:createProject
293325
:createClient
294326
:tasks
@@ -305,8 +337,33 @@ const currency = 'EUR'
305337
</div>
306338
</div>
307339
<div class="overflow-y-scroll w-full flex-1">
340+
<TimeEntryMassActionRow
341+
:selectedTimeEntries
342+
:deleteSelected
343+
:allSelected="selectedTimeEntries.length === timeEntries.length"
344+
:tasks
345+
:tags
346+
:currency
347+
:enableEstimatedTime="false"
348+
:canCreateProject="canCreateProjects"
349+
:projects
350+
:clients
351+
:updateTimeEntries="
352+
(args: UpdateMultipleTimeEntriesChangeset) =>
353+
timeEntriesUpdate.mutate({
354+
ids: selectedTimeEntries.map((timeEntry) => timeEntry.id),
355+
changes: args,
356+
})
357+
"
358+
:createProject
359+
:createClient
360+
:createTag
361+
@submit="clearSelectionAndState"
362+
@select-all="selectedTimeEntries = [...timeEntries]"
363+
@unselect-all="selectedTimeEntries = []"></TimeEntryMassActionRow>
308364
<TimeEntryGroupedTable
309365
v-if="timeEntries && projects && tasks && tags && clients"
366+
v-model:selected="selectedTimeEntries"
310367
:projects
311368
:tasks
312369
:tags
@@ -324,7 +381,10 @@ const currency = 'EUR'
324381
timeEntriesUpdate.mutate({ ids: ids, changes: changes })
325382
"
326383
:deleteTimeEntries="
327-
(args) => args.forEach((timeEntry) => timeEntryDelete.mutate(timeEntry))
384+
(args: TimeEntry[]) =>
385+
args.forEach((timeEntry: TimeEntry) =>
386+
timeEntryDelete.mutate(timeEntry)
387+
)
328388
"
329389
:createTimeEntry="createTimeEntry"
330390
:createTag

src/renderer/src/main.ts

-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ Sentry.init({
2323

2424
window.addEventListener('keypress', (event) => {
2525
if (event.key === 'Escape') {
26-
// your code
2726
event.preventDefault()
2827
}
2928
})

src/renderer/src/utils/timeEntries.ts

+40-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { apiClient } from './api.ts'
2-
import type { CreateTimeEntryBody, TimeEntry, TimeEntryResponse } from '@solidtime/api'
2+
import type {
3+
CreateTimeEntryBody,
4+
TimeEntry,
5+
TimeEntryResponse,
6+
UpdateMultipleTimeEntriesChangeset,
7+
} from '@solidtime/api'
38
import { useMutation, useQueryClient } from '@tanstack/vue-query'
49
import { useMyMemberships } from './myMemberships.ts'
510

@@ -143,6 +148,39 @@ export function useTimeEntryUpdateMutation() {
143148
})
144149
}
145150

151+
export function useTimeEntriesDeleteMutation() {
152+
const queryClient = useQueryClient()
153+
const { currentOrganizationId } = useMyMemberships()
154+
155+
return useMutation({
156+
scope: {
157+
id: 'timeEntry',
158+
},
159+
mutationFn: (timeEntries: TimeEntry[]) => {
160+
if (currentOrganizationId.value === null) {
161+
throw new Error('No current organization id - create time entry')
162+
}
163+
const timeEntryIds = timeEntries.map((timeEntry) => {
164+
if (offlineUuidStore[timeEntry.id]) {
165+
return offlineUuidStore[timeEntry.id]
166+
}
167+
return timeEntry.id
168+
})
169+
return apiClient.value.deleteTimeEntries(undefined, {
170+
queries: {
171+
ids: timeEntryIds,
172+
},
173+
params: {
174+
organization: currentOrganizationId.value,
175+
},
176+
})
177+
},
178+
onSettled: () => {
179+
queryClient.invalidateQueries({ queryKey: ['timeEntries', currentOrganizationId] })
180+
},
181+
})
182+
}
183+
146184
export function useTimeEntriesUpdateMutation() {
147185
const queryClient = useQueryClient()
148186
const { currentOrganizationId } = useMyMemberships()
@@ -151,8 +189,7 @@ export function useTimeEntriesUpdateMutation() {
151189
scope: {
152190
id: 'timeEntry',
153191
},
154-
mutationFn: (changes: { ids: string[]; changes: Partial<TimeEntry> }) => {
155-
console.log(changes)
192+
mutationFn: (changes: { ids: string[]; changes: UpdateMultipleTimeEntriesChangeset }) => {
156193
if (currentOrganizationId.value === null) {
157194
throw new Error('No current organization id - update time entry')
158195
}

0 commit comments

Comments
 (0)