mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 20:52:14 +01:00
add mass updates to time view
This commit is contained in:
@@ -365,3 +365,5 @@ test.skip('test that load more works when the end of page is reached', async ({
|
|||||||
// TODO: Add Test for Date Update
|
// TODO: Add Test for Date Update
|
||||||
|
|
||||||
// TODO: Test that project can be created in the time entry row
|
// TODO: Test that project can be created in the time entry row
|
||||||
|
|
||||||
|
// TODO: Add Tests for Mass Update
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
||||||
|
import { PencilSquareIcon, TrashIcon } from '@heroicons/vue/20/solid';
|
||||||
|
import TimeEntryMassUpdateModal from '@/Components/Common/TimeEntry/TimeEntryMassUpdateModal.vue';
|
||||||
|
import type { TimeEntry } from '@/packages/api/src';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
selectedTimeEntries: TimeEntry[];
|
||||||
|
deleteSelected: () => void;
|
||||||
|
class?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const showMassUpdateModal = ref(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TimeEntryMassUpdateModal
|
||||||
|
:time-entries="selectedTimeEntries"
|
||||||
|
@submit="emit('submit')"
|
||||||
|
v-model:show="showMassUpdateModal"></TimeEntryMassUpdateModal>
|
||||||
|
<MainContainer
|
||||||
|
v-if="selectedTimeEntries.length > 0"
|
||||||
|
:class="
|
||||||
|
twMerge(
|
||||||
|
props.class,
|
||||||
|
'text-sm py-1.5 font-medium border-b border-t border-border-primary flex items-center space-x-3'
|
||||||
|
)
|
||||||
|
">
|
||||||
|
<div>{{ selectedTimeEntries.length }} selected</div>
|
||||||
|
<button
|
||||||
|
class="text-text-tertiary flex space-x-1 items-center hover:text-text-secondary transition focus-visible:ring-2 outline-0 focus-visible:text-text-primary focus-visible:ring-white/80 rounded h-full px-2"
|
||||||
|
@click="showMassUpdateModal = true"
|
||||||
|
v-if="selectedTimeEntries.length">
|
||||||
|
<PencilSquareIcon class="w-4"></PencilSquareIcon>
|
||||||
|
<span> Edit </span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="text-red-400 h-full px-2 space-x-1 items-center flex hover:text-red-500 transition focus-visible:ring-2 outline-0 focus-visible:text-red-500 focus-visible:ring-white/80 rounded"
|
||||||
|
@click="deleteSelected"
|
||||||
|
v-if="selectedTimeEntries.length">
|
||||||
|
<TrashIcon class="w-3.5"></TrashIcon>
|
||||||
|
<span> Delete </span>
|
||||||
|
</button>
|
||||||
|
</MainContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -57,25 +57,23 @@ async function createClient(
|
|||||||
return await useClientsStore().createClient(body);
|
return await useClientsStore().createClient(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = ref<HTMLInputElement | null>(null);
|
const descriptionInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
|
||||||
watch(show, (value) => {
|
watch(show, (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
description.value?.focus();
|
descriptionInput.value?.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeEntryUpdates = ref({
|
const description = ref<string>('');
|
||||||
description: '',
|
const taskId = ref<string | null | undefined>(undefined);
|
||||||
project_id: null,
|
const projectId = ref<string | null | undefined>(undefined);
|
||||||
task_id: null,
|
const billable = ref<boolean | undefined>(undefined);
|
||||||
tags: [] as string[],
|
const selectedTags = ref<string[]>([]);
|
||||||
billable: null as boolean | null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { tags } = storeToRefs(useTagsStore());
|
const { tags } = storeToRefs(useTagsStore());
|
||||||
async function createTag(tag: string) {
|
async function createTag(tag: string) {
|
||||||
@@ -84,46 +82,46 @@ async function createTag(tag: string) {
|
|||||||
|
|
||||||
const timeEntryBillable = computed({
|
const timeEntryBillable = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
if (timeEntryUpdates.value.billable === null) {
|
if (billable.value) {
|
||||||
return 'do-not-update';
|
return 'do-not-update';
|
||||||
}
|
}
|
||||||
return timeEntryUpdates.value.billable ? 'billable' : 'non-billable';
|
return billable.value ? 'billable' : 'non-billable';
|
||||||
},
|
},
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
if (value === 'do-not-update') {
|
if (value === 'do-not-update') {
|
||||||
timeEntryUpdates.value.billable = null;
|
billable.value = undefined;
|
||||||
} else if (value === 'billable') {
|
} else if (value === 'billable') {
|
||||||
timeEntryUpdates.value.billable = true;
|
billable.value = true;
|
||||||
} else {
|
} else {
|
||||||
timeEntryUpdates.value.billable = false;
|
billable.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function submit() {
|
async function submit() {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
const timeEntryUpdatesBody = {} as UpdateMultipleTimeEntriesChangeset;
|
const timeEntryUpdatesBody = {} as UpdateMultipleTimeEntriesChangeset;
|
||||||
if (timeEntryUpdates.value.description !== '') {
|
if (description.value && description.value !== '') {
|
||||||
timeEntryUpdatesBody.description =
|
timeEntryUpdatesBody.description = description.value;
|
||||||
timeEntryUpdates.value.description;
|
|
||||||
}
|
}
|
||||||
if (timeEntryUpdates.value.project_id) {
|
if (projectId.value !== undefined) {
|
||||||
timeEntryUpdatesBody.project_id = timeEntryUpdates.value.project_id;
|
timeEntryUpdatesBody.project_id = projectId.value;
|
||||||
|
timeEntryUpdatesBody.task_id = null;
|
||||||
}
|
}
|
||||||
if (timeEntryUpdates.value.task_id) {
|
if (taskId.value !== undefined) {
|
||||||
timeEntryUpdatesBody.task_id = timeEntryUpdates.value.task_id;
|
timeEntryUpdatesBody.task_id = taskId.value;
|
||||||
}
|
}
|
||||||
if (timeEntryUpdates.value.billable !== null) {
|
if (billable.value !== undefined) {
|
||||||
timeEntryUpdatesBody.billable = timeEntryUpdates.value.billable;
|
timeEntryUpdatesBody.billable = billable.value;
|
||||||
}
|
}
|
||||||
if (timeEntryUpdates.value.tags.length > 0) {
|
if (selectedTags.value.length > 0) {
|
||||||
timeEntryUpdatesBody.tags = timeEntryUpdates.value.tags;
|
timeEntryUpdatesBody.tags = selectedTags.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handleApiRequestNotifications(
|
await handleApiRequestNotifications(
|
||||||
() =>
|
() =>
|
||||||
api.updateMultipleTimeEntries(
|
api.updateMultipleTimeEntries(
|
||||||
{
|
{
|
||||||
@@ -145,13 +143,11 @@ function submit() {
|
|||||||
() => {
|
() => {
|
||||||
show.value = false;
|
show.value = false;
|
||||||
emit('submit');
|
emit('submit');
|
||||||
timeEntryUpdates.value = {
|
description.value = '';
|
||||||
description: '',
|
projectId.value = undefined;
|
||||||
project_id: null,
|
taskId.value = undefined;
|
||||||
task_id: null,
|
selectedTags.value = [];
|
||||||
tags: [],
|
billable.value = undefined;
|
||||||
billable: null,
|
|
||||||
};
|
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -176,8 +172,8 @@ function submit() {
|
|||||||
<InputLabel for="description" value="Description" />
|
<InputLabel for="description" value="Description" />
|
||||||
<TextInput
|
<TextInput
|
||||||
id="description"
|
id="description"
|
||||||
ref="description"
|
ref="descriptionInput"
|
||||||
v-model="timeEntryUpdates.description"
|
v-model="description"
|
||||||
@keydown.enter="submit"
|
@keydown.enter="submit"
|
||||||
type="text"
|
type="text"
|
||||||
class="mt-1 block w-full" />
|
class="mt-1 block w-full" />
|
||||||
@@ -193,21 +189,16 @@ function submit() {
|
|||||||
size="xlarge"
|
size="xlarge"
|
||||||
:projects="projects"
|
:projects="projects"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
v-model:project="timeEntryUpdates.project_id"
|
v-model:project="projectId"
|
||||||
v-model:task="
|
v-model:task="taskId"></TimeTrackerProjectTaskDropdown>
|
||||||
timeEntryUpdates.task_id
|
|
||||||
"></TimeTrackerProjectTaskDropdown>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<InputLabel for="project" value="Tag" />
|
<InputLabel for="project" value="Tag" />
|
||||||
<TagDropdown
|
<TagDropdown :createTag v-model="selectedTags" :tags="tags">
|
||||||
:createTag
|
|
||||||
v-model="timeEntryUpdates.tags"
|
|
||||||
:tags="tags">
|
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<Badge size="xlarge">
|
<Badge size="xlarge">
|
||||||
<span v-if="timeEntryUpdates.tags.length > 0">
|
<span v-if="selectedTags.length > 0">
|
||||||
Set {{ timeEntryUpdates.tags.length }} tags
|
Set {{ selectedTags.length }} tags
|
||||||
</span>
|
</span>
|
||||||
<span v-else> Select Tags... </span>
|
<span v-else> Select Tags... </span>
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -236,13 +227,10 @@ function submit() {
|
|||||||
]">
|
]">
|
||||||
<template v-slot:trigger>
|
<template v-slot:trigger>
|
||||||
<Badge tag="button" size="xlarge">
|
<Badge tag="button" size="xlarge">
|
||||||
<span v-if="timeEntryUpdates.billable === null">
|
<span v-if="billable === undefined">
|
||||||
Set billable status
|
Set billable status
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span v-else-if="billable === true">
|
||||||
v-else-if="
|
|
||||||
timeEntryUpdates.billable === true
|
|
||||||
">
|
|
||||||
Billable
|
Billable
|
||||||
</span>
|
</span>
|
||||||
<span v-else> Non Billable </span></Badge
|
<span v-else> Non Billable </span></Badge
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import {
|
|||||||
ChevronDoubleLeftIcon,
|
ChevronDoubleLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
ChevronDoubleRightIcon,
|
ChevronDoubleRightIcon,
|
||||||
PencilSquareIcon,
|
|
||||||
TrashIcon,
|
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
} from '@heroicons/vue/20/solid';
|
} from '@heroicons/vue/20/solid';
|
||||||
import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue';
|
import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue';
|
||||||
@@ -66,7 +64,7 @@ import {
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
||||||
import TimeEntryMassUpdateModal from '@/Components/Common/TimeEntry/TimeEntryMassUpdateModal.vue';
|
import TimeEntryMassActionRow from '@/Components/Common/TimeEntry/TimeEntryMassActionRow.vue';
|
||||||
|
|
||||||
const startDate = useSessionStorage<string>(
|
const startDate = useSessionStorage<string>(
|
||||||
'reporting-start-date',
|
'reporting-start-date',
|
||||||
@@ -210,7 +208,10 @@ function deleteSelected() {
|
|||||||
deleteTimeEntries(selectedTimeEntries.value);
|
deleteTimeEntries(selectedTimeEntries.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showMassUpdateModal = ref(false);
|
async function clearSelectionAndState() {
|
||||||
|
selectedTimeEntries.value = [];
|
||||||
|
await updateFilteredTimeEntries();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -335,28 +336,10 @@ const showMassUpdateModal = ref(false);
|
|||||||
</div>
|
</div>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
</div>
|
</div>
|
||||||
<TimeEntryMassUpdateModal
|
<TimeEntryMassActionRow
|
||||||
:time-entries="selectedTimeEntries"
|
:selected-time-entries="selectedTimeEntries"
|
||||||
@submit="updateFilteredTimeEntries"
|
@submit="clearSelectionAndState"
|
||||||
v-model:show="showMassUpdateModal"></TimeEntryMassUpdateModal>
|
:delete-selected="deleteSelected"></TimeEntryMassActionRow>
|
||||||
<MainContainer
|
|
||||||
class="text-sm py-1.5 font-medium border-b border-t bg-secondary border-border-tertiary flex items-center space-x-3">
|
|
||||||
<div>{{ selectedTimeEntries.length }} selected</div>
|
|
||||||
<button
|
|
||||||
class="text-text-tertiary flex space-x-1 items-center hover:text-text-secondary transition focus-visible:ring-2 outline-0 focus-visible:text-text-primary focus-visible:ring-white/80 rounded h-full px-2"
|
|
||||||
@click="showMassUpdateModal = true"
|
|
||||||
v-if="selectedTimeEntries.length">
|
|
||||||
<PencilSquareIcon class="w-4"></PencilSquareIcon>
|
|
||||||
<span> Edit </span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="text-red-400 h-full px-2 space-x-1 items-center flex hover:text-red-500 transition focus-visible:ring-2 outline-0 focus-visible:text-red-500 focus-visible:ring-white/80 rounded"
|
|
||||||
@click="deleteSelected"
|
|
||||||
v-if="selectedTimeEntries.length">
|
|
||||||
<TrashIcon class="w-3.5"></TrashIcon>
|
|
||||||
<span> Delete </span>
|
|
||||||
</button>
|
|
||||||
</MainContainer>
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<div v-for="entry in timeEntries" :key="entry.id">
|
<div v-for="entry in timeEntries" :key="entry.id">
|
||||||
<TimeEntryRow
|
<TimeEntryRow
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useTagsStore } from '@/utils/useTags';
|
|||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsStore } from '@/utils/useClients';
|
||||||
import TimeEntryCreateModal from '@/Components/Common/TimeEntry/TimeEntryCreateModal.vue';
|
import TimeEntryCreateModal from '@/Components/Common/TimeEntry/TimeEntryCreateModal.vue';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
|
import TimeEntryMassActionRow from '@/Components/Common/TimeEntry/TimeEntryMassActionRow.vue';
|
||||||
|
|
||||||
const timeEntriesStore = useTimeEntriesStore();
|
const timeEntriesStore = useTimeEntriesStore();
|
||||||
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
|
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
|
||||||
@@ -97,6 +98,17 @@ async function createClient(
|
|||||||
): Promise<Client | undefined> {
|
): Promise<Client | undefined> {
|
||||||
return await useClientsStore().createClient(body);
|
return await useClientsStore().createClient(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedTimeEntries = ref([] as TimeEntry[]);
|
||||||
|
|
||||||
|
async function clearSelectionAndState() {
|
||||||
|
selectedTimeEntries.value = [];
|
||||||
|
await fetchTimeEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelected() {
|
||||||
|
deleteTimeEntries(selectedTimeEntries.value);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -120,7 +132,12 @@ async function createClient(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
|
<TimeEntryMassActionRow
|
||||||
|
:selected-time-entries="selectedTimeEntries"
|
||||||
|
@submit="clearSelectionAndState"
|
||||||
|
:delete-selected="deleteSelected"></TimeEntryMassActionRow>
|
||||||
<TimeEntryGroupedTable
|
<TimeEntryGroupedTable
|
||||||
|
v-model:selected="selectedTimeEntries"
|
||||||
:createProject
|
:createProject
|
||||||
:clients
|
:clients
|
||||||
:createClient
|
:createClient
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ const props = defineProps<{
|
|||||||
updateTimeEntry: (timeEntry: TimeEntry) => void;
|
updateTimeEntry: (timeEntry: TimeEntry) => void;
|
||||||
deleteTimeEntries: (timeEntries: TimeEntry[]) => void;
|
deleteTimeEntries: (timeEntries: TimeEntry[]) => void;
|
||||||
currency: string;
|
currency: string;
|
||||||
|
selectedTimeEntries: TimeEntry[];
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
selected: [TimeEntry[]];
|
||||||
|
unselected: [TimeEntry[]];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function updateTimeEntryDescription(description: string) {
|
function updateTimeEntryDescription(description: string) {
|
||||||
@@ -69,6 +74,14 @@ function updateProjectAndTask(projectId: string, taskId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expanded = ref(false);
|
const expanded = ref(false);
|
||||||
|
function onSelectChange(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
if (target.checked) {
|
||||||
|
emit('selected', [...props.timeEntry.timeEntries]);
|
||||||
|
} else {
|
||||||
|
emit('unselected', [...props.timeEntry.timeEntries]);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -80,6 +93,15 @@ const expanded = ref(false);
|
|||||||
class="sm:flex py-1.5 items-center min-w-0 justify-between group">
|
class="sm:flex py-1.5 items-center min-w-0 justify-between group">
|
||||||
<div class="flex space-x-3 items-center min-w-0">
|
<div class="flex space-x-3 items-center min-w-0">
|
||||||
<input
|
<input
|
||||||
|
@change="onSelectChange"
|
||||||
|
:checked="
|
||||||
|
timeEntry.timeEntries.every(
|
||||||
|
(aggregateTimeEntry: TimeEntry) =>
|
||||||
|
selectedTimeEntries.includes(
|
||||||
|
aggregateTimeEntry
|
||||||
|
)
|
||||||
|
)
|
||||||
|
"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
|
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
|
||||||
<div class="flex items-center min-w-0">
|
<div class="flex items-center min-w-0">
|
||||||
@@ -153,6 +175,13 @@ const expanded = ref(false);
|
|||||||
<TimeEntryRow
|
<TimeEntryRow
|
||||||
:projects="projects"
|
:projects="projects"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
|
:selected="
|
||||||
|
!!selectedTimeEntries.find(
|
||||||
|
(filterEntry) => filterEntry.id === subEntry.id
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@selected="emit('selected', [subEntry])"
|
||||||
|
@unselected="emit('unselected', [subEntry])"
|
||||||
:createClient
|
:createClient
|
||||||
:clients
|
:clients
|
||||||
:createProject
|
:createProject
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import TimeEntryRowHeading from '@/packages/ui/src/TimeEntry/TimeEntryRowHeading
|
|||||||
import TimeEntryRow from '@/packages/ui/src/TimeEntry/TimeEntryRow.vue';
|
import TimeEntryRow from '@/packages/ui/src/TimeEntry/TimeEntryRow.vue';
|
||||||
import type { TimeEntriesGroupedByType } from '@/types/time-entries';
|
import type { TimeEntriesGroupedByType } from '@/types/time-entries';
|
||||||
|
|
||||||
|
const selectedTimeEntries = defineModel<TimeEntry[]>('selected', {
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
timeEntries: TimeEntry[];
|
timeEntries: TimeEntry[];
|
||||||
projects: Project[];
|
projects: Project[];
|
||||||
@@ -124,6 +128,25 @@ function sumDuration(timeEntries: TimeEntry[]) {
|
|||||||
<template v-for="entry in value" :key="entry.id">
|
<template v-for="entry in value" :key="entry.id">
|
||||||
<TimeEntryAggregateRow
|
<TimeEntryAggregateRow
|
||||||
:createProject
|
:createProject
|
||||||
|
:selected-time-entries="selectedTimeEntries"
|
||||||
|
@selected="
|
||||||
|
(timeEntries) => {
|
||||||
|
selectedTimeEntries = [
|
||||||
|
...selectedTimeEntries,
|
||||||
|
...timeEntries,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@unselected="
|
||||||
|
(timeEntriesToUnselect) => {
|
||||||
|
selectedTimeEntries = selectedTimeEntries.filter(
|
||||||
|
(item) =>
|
||||||
|
!timeEntriesToUnselect.find(
|
||||||
|
(filterEntry) => filterEntry.id === item.id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"
|
||||||
:createClient
|
:createClient
|
||||||
:projects="projects"
|
:projects="projects"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
@@ -141,6 +164,17 @@ function sumDuration(timeEntries: TimeEntry[]) {
|
|||||||
:createClient
|
:createClient
|
||||||
:createProject
|
:createProject
|
||||||
:projects="projects"
|
:projects="projects"
|
||||||
|
:selected="
|
||||||
|
!!selectedTimeEntries.find(
|
||||||
|
(filterEntry) => filterEntry.id === entry.id
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@selected="selectedTimeEntries.push(entry)"
|
||||||
|
@unselected="
|
||||||
|
selectedTimeEntries = selectedTimeEntries.filter(
|
||||||
|
(item) => item.id !== entry.id
|
||||||
|
)
|
||||||
|
"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
:tags="tags"
|
:tags="tags"
|
||||||
:clients
|
:clients
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ function onSelectChange(event: Event) {
|
|||||||
<div class="flex space-x-1 items-center min-w-0">
|
<div class="flex space-x-1 items-center min-w-0">
|
||||||
<input
|
<input
|
||||||
@change="onSelectChange"
|
@change="onSelectChange"
|
||||||
:value="selected"
|
:checked="selected"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
|
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
|
||||||
<div class="w-7 h-7" v-if="indent === true"></div>
|
<div class="w-7 h-7" v-if="indent === true"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user