Skip to content

Commit

Permalink
add mass updates to time view
Browse files Browse the repository at this point in the history
  • Loading branch information
Onatcer committed Oct 8, 2024
1 parent d5a4df7 commit f147fb9
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 78 deletions.
2 changes: 2 additions & 0 deletions e2e/time.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: Test that project can be created in the time entry row

// TODO: Add Tests for Mass Update
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,23 @@ async function createClient(
return await useClientsStore().createClient(body);
}
const description = ref<HTMLInputElement | null>(null);
const descriptionInput = ref<HTMLInputElement | null>(null);
const { handleApiRequestNotifications } = useNotificationsStore();
watch(show, (value) => {
if (value) {
nextTick(() => {
description.value?.focus();
descriptionInput.value?.focus();
});
}
});
const timeEntryUpdates = ref({
description: '',
project_id: null,
task_id: null,
tags: [] as string[],
billable: null as boolean | null,
});
const description = ref<string>('');
const taskId = ref<string | null | undefined>(undefined);
const projectId = ref<string | null | undefined>(undefined);
const billable = ref<boolean | undefined>(undefined);
const selectedTags = ref<string[]>([]);
const { tags } = storeToRefs(useTagsStore());
async function createTag(tag: string) {
Expand All @@ -84,46 +82,46 @@ async function createTag(tag: string) {
const timeEntryBillable = computed({
get: () => {
if (timeEntryUpdates.value.billable === null) {
if (billable.value) {
return 'do-not-update';
}
return timeEntryUpdates.value.billable ? 'billable' : 'non-billable';
return billable.value ? 'billable' : 'non-billable';
},
set: (value) => {
if (value === 'do-not-update') {
timeEntryUpdates.value.billable = null;
billable.value = undefined;
} else if (value === 'billable') {
timeEntryUpdates.value.billable = true;
billable.value = true;
} else {
timeEntryUpdates.value.billable = false;
billable.value = false;
}
},
});
function submit() {
async function submit() {
const organizationId = getCurrentOrganizationId();
saving.value = true;
if (organizationId) {
const timeEntryUpdatesBody = {} as UpdateMultipleTimeEntriesChangeset;
if (timeEntryUpdates.value.description !== '') {
timeEntryUpdatesBody.description =
timeEntryUpdates.value.description;
if (description.value && description.value !== '') {
timeEntryUpdatesBody.description = description.value;
}
if (timeEntryUpdates.value.project_id) {
timeEntryUpdatesBody.project_id = timeEntryUpdates.value.project_id;
if (projectId.value !== undefined) {
timeEntryUpdatesBody.project_id = projectId.value;
timeEntryUpdatesBody.task_id = null;
}
if (timeEntryUpdates.value.task_id) {
timeEntryUpdatesBody.task_id = timeEntryUpdates.value.task_id;
if (taskId.value !== undefined) {
timeEntryUpdatesBody.task_id = taskId.value;
}
if (timeEntryUpdates.value.billable !== null) {
timeEntryUpdatesBody.billable = timeEntryUpdates.value.billable;
if (billable.value !== undefined) {
timeEntryUpdatesBody.billable = billable.value;
}
if (timeEntryUpdates.value.tags.length > 0) {
timeEntryUpdatesBody.tags = timeEntryUpdates.value.tags;
if (selectedTags.value.length > 0) {
timeEntryUpdatesBody.tags = selectedTags.value;
}
try {
handleApiRequestNotifications(
await handleApiRequestNotifications(
() =>
api.updateMultipleTimeEntries(
{
Expand All @@ -145,13 +143,11 @@ function submit() {
() => {
show.value = false;
emit('submit');
timeEntryUpdates.value = {
description: '',
project_id: null,
task_id: null,
tags: [],
billable: null,
};
description.value = '';
projectId.value = undefined;
taskId.value = undefined;
selectedTags.value = [];
billable.value = undefined;
saving.value = false;
}
);
Expand All @@ -176,8 +172,8 @@ function submit() {
<InputLabel for="description" value="Description" />
<TextInput
id="description"
ref="description"
v-model="timeEntryUpdates.description"
ref="descriptionInput"
v-model="description"
@keydown.enter="submit"
type="text"
class="mt-1 block w-full" />
Expand All @@ -193,21 +189,16 @@ function submit() {
size="xlarge"
:projects="projects"
:tasks="tasks"
v-model:project="timeEntryUpdates.project_id"
v-model:task="
timeEntryUpdates.task_id
"></TimeTrackerProjectTaskDropdown>
v-model:project="projectId"
v-model:task="taskId"></TimeTrackerProjectTaskDropdown>
</div>
<div class="space-y-2">
<InputLabel for="project" value="Tag" />
<TagDropdown
:createTag
v-model="timeEntryUpdates.tags"
:tags="tags">
<TagDropdown :createTag v-model="selectedTags" :tags="tags">
<template #trigger>
<Badge size="xlarge">
<span v-if="timeEntryUpdates.tags.length > 0">
Set {{ timeEntryUpdates.tags.length }} tags
<span v-if="selectedTags.length > 0">
Set {{ selectedTags.length }} tags
</span>
<span v-else> Select Tags... </span>
</Badge>
Expand Down Expand Up @@ -236,13 +227,10 @@ function submit() {
]">
<template v-slot:trigger>
<Badge tag="button" size="xlarge">
<span v-if="timeEntryUpdates.billable === null">
<span v-if="billable === undefined">
Set billable status
</span>
<span
v-else-if="
timeEntryUpdates.billable === true
">
<span v-else-if="billable === true">
Billable
</span>
<span v-else> Non Billable </span></Badge
Expand Down
35 changes: 9 additions & 26 deletions resources/js/Pages/ReportingDetailed.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import {
ChevronDoubleLeftIcon,
ChevronRightIcon,
ChevronDoubleRightIcon,
PencilSquareIcon,
TrashIcon,
ClockIcon,
} from '@heroicons/vue/20/solid';
import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue';
Expand Down Expand Up @@ -66,7 +64,7 @@ import {
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
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>(
'reporting-start-date',
Expand Down Expand Up @@ -210,7 +208,10 @@ function deleteSelected() {
deleteTimeEntries(selectedTimeEntries.value);
}
const showMassUpdateModal = ref(false);
async function clearSelectionAndState() {
selectedTimeEntries.value = [];
await updateFilteredTimeEntries();
}
</script>

<template>
Expand Down Expand Up @@ -335,28 +336,10 @@ const showMassUpdateModal = ref(false);
</div>
</MainContainer>
</div>
<TimeEntryMassUpdateModal
:time-entries="selectedTimeEntries"
@submit="updateFilteredTimeEntries"
v-model:show="showMassUpdateModal"></TimeEntryMassUpdateModal>
<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>
<TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries"
@submit="clearSelectionAndState"
:delete-selected="deleteSelected"></TimeEntryMassActionRow>
<div class="w-full relative">
<div v-for="entry in timeEntries" :key="entry.id">
<TimeEntryRow
Expand Down
17 changes: 17 additions & 0 deletions resources/js/Pages/Time.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useTagsStore } from '@/utils/useTags';
import { useClientsStore } from '@/utils/useClients';
import TimeEntryCreateModal from '@/Components/Common/TimeEntry/TimeEntryCreateModal.vue';
import { getOrganizationCurrencyString } from '@/utils/money';
import TimeEntryMassActionRow from '@/Components/Common/TimeEntry/TimeEntryMassActionRow.vue';
const timeEntriesStore = useTimeEntriesStore();
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
Expand Down Expand Up @@ -97,6 +98,17 @@ async function createClient(
): Promise<Client | undefined> {
return await useClientsStore().createClient(body);
}
const selectedTimeEntries = ref([] as TimeEntry[]);
async function clearSelectionAndState() {
selectedTimeEntries.value = [];
await fetchTimeEntries();
}
function deleteSelected() {
deleteTimeEntries(selectedTimeEntries.value);
}
</script>

<template>
Expand All @@ -120,7 +132,12 @@ async function createClient(
</div>
</div>
</MainContainer>
<TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries"
@submit="clearSelectionAndState"
:delete-selected="deleteSelected"></TimeEntryMassActionRow>
<TimeEntryGroupedTable
v-model:selected="selectedTimeEntries"
:createProject
:clients
:createClient
Expand Down
29 changes: 29 additions & 0 deletions resources/js/packages/ui/src/TimeEntry/TimeEntryAggregateRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ const props = defineProps<{
updateTimeEntry: (timeEntry: TimeEntry) => void;
deleteTimeEntries: (timeEntries: TimeEntry[]) => void;
currency: string;
selectedTimeEntries: TimeEntry[];
}>();
const emit = defineEmits<{
selected: [TimeEntry[]];
unselected: [TimeEntry[]];
}>();
function updateTimeEntryDescription(description: string) {
Expand Down Expand Up @@ -69,6 +74,14 @@ function updateProjectAndTask(projectId: string, taskId: string) {
}
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>

<template>
Expand All @@ -80,6 +93,15 @@ const expanded = ref(false);
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">
<input
@change="onSelectChange"
:checked="
timeEntry.timeEntries.every(
(aggregateTimeEntry: TimeEntry) =>
selectedTimeEntries.includes(
aggregateTimeEntry
)
)
"
type="checkbox"
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">
Expand Down Expand Up @@ -153,6 +175,13 @@ const expanded = ref(false);
<TimeEntryRow
:projects="projects"
:tasks="tasks"
:selected="
!!selectedTimeEntries.find(
(filterEntry) => filterEntry.id === subEntry.id
)
"
@selected="emit('selected', [subEntry])"
@unselected="emit('unselected', [subEntry])"
:createClient
:clients
:createProject
Expand Down
Loading

0 comments on commit f147fb9

Please sign in to comment.