-
-
Notifications
You must be signed in to change notification settings - Fork 120
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add mass update to detailed reporting page
- Loading branch information
Showing
15 changed files
with
778 additions
and
93 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
268 changes: 268 additions & 0 deletions
268
resources/js/Components/Common/TimeEntry/TimeEntryMassUpdateModal.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
<script setup lang="ts"> | ||
import TextInput from '@/packages/ui/src/Input/TextInput.vue'; | ||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue'; | ||
import DialogModal from '@/packages/ui/src/DialogModal.vue'; | ||
import { computed, nextTick, ref, watch } from 'vue'; | ||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue'; | ||
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue'; | ||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue'; | ||
import { storeToRefs } from 'pinia'; | ||
import { useTasksStore } from '@/utils/useTasks'; | ||
import { useProjectsStore } from '@/utils/useProjects'; | ||
import { useTagsStore } from '@/utils/useTags'; | ||
import { | ||
type CreateClientBody, | ||
type CreateProjectBody, | ||
type Project, | ||
type Client, | ||
api, | ||
type TimeEntry, | ||
type UpdateMultipleTimeEntriesChangeset, | ||
} from '@/packages/api/src'; | ||
import { useClientsStore } from '@/utils/useClients'; | ||
import { getOrganizationCurrencyString } from '@/utils/money'; | ||
import { Badge } from '@/packages/ui/src'; | ||
import SelectDropdown from '../../../packages/ui/src/Input/SelectDropdown.vue'; | ||
import { getCurrentOrganizationId } from '@/utils/useUser'; | ||
import { useNotificationsStore } from '@/utils/notification'; | ||
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue'; | ||
const projectStore = useProjectsStore(); | ||
const { projects } = storeToRefs(projectStore); | ||
const taskStore = useTasksStore(); | ||
const { tasks } = storeToRefs(taskStore); | ||
const clientStore = useClientsStore(); | ||
const { clients } = storeToRefs(clientStore); | ||
const show = defineModel('show', { default: false }); | ||
const saving = ref(false); | ||
async function createProject( | ||
project: CreateProjectBody | ||
): Promise<Project | undefined> { | ||
return await useProjectsStore().createProject(project); | ||
} | ||
const props = defineProps<{ | ||
timeEntries: TimeEntry[]; | ||
}>(); | ||
const emit = defineEmits<{ | ||
submit: []; | ||
}>(); | ||
async function createClient( | ||
body: CreateClientBody | ||
): Promise<Client | undefined> { | ||
return await useClientsStore().createClient(body); | ||
} | ||
const description = ref<HTMLInputElement | null>(null); | ||
const { handleApiRequestNotifications } = useNotificationsStore(); | ||
watch(show, (value) => { | ||
if (value) { | ||
nextTick(() => { | ||
description.value?.focus(); | ||
}); | ||
} | ||
}); | ||
const timeEntryUpdates = ref({ | ||
description: '', | ||
project_id: null, | ||
task_id: null, | ||
tags: [] as string[], | ||
billable: null as boolean | null, | ||
}); | ||
const { tags } = storeToRefs(useTagsStore()); | ||
async function createTag(tag: string) { | ||
return await useTagsStore().createTag(tag); | ||
} | ||
const timeEntryBillable = computed({ | ||
get: () => { | ||
if (timeEntryUpdates.value.billable === null) { | ||
return 'do-not-update'; | ||
} | ||
return timeEntryUpdates.value.billable ? 'billable' : 'non-billable'; | ||
}, | ||
set: (value) => { | ||
if (value === 'do-not-update') { | ||
timeEntryUpdates.value.billable = null; | ||
} else if (value === 'billable') { | ||
timeEntryUpdates.value.billable = true; | ||
} else { | ||
timeEntryUpdates.value.billable = false; | ||
} | ||
}, | ||
}); | ||
function submit() { | ||
const organizationId = getCurrentOrganizationId(); | ||
saving.value = true; | ||
if (organizationId) { | ||
const timeEntryUpdatesBody = {} as UpdateMultipleTimeEntriesChangeset; | ||
if (timeEntryUpdates.value.description !== '') { | ||
timeEntryUpdatesBody.description = | ||
timeEntryUpdates.value.description; | ||
} | ||
if (timeEntryUpdates.value.project_id) { | ||
timeEntryUpdatesBody.project_id = timeEntryUpdates.value.project_id; | ||
} | ||
if (timeEntryUpdates.value.task_id) { | ||
timeEntryUpdatesBody.task_id = timeEntryUpdates.value.task_id; | ||
} | ||
if (timeEntryUpdates.value.billable !== null) { | ||
timeEntryUpdatesBody.billable = timeEntryUpdates.value.billable; | ||
} | ||
if (timeEntryUpdates.value.tags.length > 0) { | ||
timeEntryUpdatesBody.tags = timeEntryUpdates.value.tags; | ||
} | ||
try { | ||
handleApiRequestNotifications( | ||
() => | ||
api.updateMultipleTimeEntries( | ||
{ | ||
ids: props.timeEntries.map( | ||
(timeEntry) => timeEntry.id | ||
), | ||
changes: { | ||
...timeEntryUpdatesBody, | ||
}, | ||
}, | ||
{ | ||
params: { | ||
organization: organizationId, | ||
}, | ||
} | ||
), | ||
'Time entries updated', | ||
'Failed to update time entries', | ||
() => { | ||
show.value = false; | ||
emit('submit'); | ||
timeEntryUpdates.value = { | ||
description: '', | ||
project_id: null, | ||
task_id: null, | ||
tags: [], | ||
billable: null, | ||
}; | ||
saving.value = false; | ||
} | ||
); | ||
} catch (e) { | ||
saving.value = false; | ||
} | ||
} | ||
} | ||
</script> | ||
|
||
<template> | ||
<DialogModal closeable :show="show" @close="show = false"> | ||
<template #title> | ||
<div class="flex space-x-2"> | ||
<span> Update {{ timeEntries.length }} time entries </span> | ||
</div> | ||
</template> | ||
|
||
<template #content> | ||
<div class="space-y-4"> | ||
<div class="space-y-2"> | ||
<InputLabel for="description" value="Description" /> | ||
<TextInput | ||
id="description" | ||
ref="description" | ||
v-model="timeEntryUpdates.description" | ||
@keydown.enter="submit" | ||
type="text" | ||
class="mt-1 block w-full" /> | ||
</div> | ||
<div class="space-y-2"> | ||
<InputLabel for="project" value="Project" /> | ||
<TimeTrackerProjectTaskDropdown | ||
:clients | ||
:createProject | ||
:createClient | ||
:currency="getOrganizationCurrencyString()" | ||
class="mt-1" | ||
size="xlarge" | ||
:projects="projects" | ||
:tasks="tasks" | ||
v-model:project="timeEntryUpdates.project_id" | ||
v-model:task=" | ||
timeEntryUpdates.task_id | ||
"></TimeTrackerProjectTaskDropdown> | ||
</div> | ||
<div class="space-y-2"> | ||
<InputLabel for="project" value="Tag" /> | ||
<TagDropdown | ||
:createTag | ||
v-model="timeEntryUpdates.tags" | ||
:tags="tags"> | ||
<template #trigger> | ||
<Badge size="xlarge"> | ||
<span v-if="timeEntryUpdates.tags.length > 0"> | ||
Set {{ timeEntryUpdates.tags.length }} tags | ||
</span> | ||
<span v-else> Select Tags... </span> | ||
</Badge> | ||
</template> | ||
</TagDropdown> | ||
</div> | ||
<div class="space-y-2"> | ||
<InputLabel for="project" value="Billable" /> | ||
<SelectDropdown | ||
v-model="timeEntryBillable" | ||
:get-key-from-item="(item) => item.value" | ||
:get-name-for-item="(item) => item.label" | ||
:items="[ | ||
{ | ||
label: 'Keep current billable status', | ||
value: 'do-not-update', | ||
}, | ||
{ | ||
label: 'Billable', | ||
value: 'billable', | ||
}, | ||
{ | ||
label: 'Non Billable', | ||
value: 'non-billable', | ||
}, | ||
]"> | ||
<template v-slot:trigger> | ||
<Badge tag="button" size="xlarge"> | ||
<span v-if="timeEntryUpdates.billable === null"> | ||
Set billable status | ||
</span> | ||
<span | ||
v-else-if=" | ||
timeEntryUpdates.billable === true | ||
"> | ||
Billable | ||
</span> | ||
<span v-else> Non Billable </span></Badge | ||
> | ||
</template> | ||
</SelectDropdown> | ||
</div> | ||
</div> | ||
</template> | ||
<template #footer> | ||
<SecondaryButton @click="show = false"> Cancel</SecondaryButton> | ||
<PrimaryButton | ||
class="ms-3" | ||
:class="{ 'opacity-25': saving }" | ||
:disabled="saving" | ||
@click="submit"> | ||
Update Time Entries | ||
</PrimaryButton> | ||
</template> | ||
</DialogModal> | ||
</template> | ||
|
||
<style scoped></style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.