mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 19:22:14 +01:00
move time entry mass updates to ui package and remove its dependencies
This commit is contained in:
@@ -39,6 +39,7 @@ const { clients } = storeToRefs(useClientsStore());
|
||||
const gridTemplate = computed(() => {
|
||||
return `grid-template-columns: minmax(300px, 1fr) minmax(150px, auto) minmax(140px, auto) minmax(130px, auto) ${props.showBillableRate ? 'minmax(130px, auto)' : ''} minmax(120px, auto) 80px;`;
|
||||
});
|
||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -47,6 +48,7 @@ const gridTemplate = computed(() => {
|
||||
:createClient
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:clients="clients"
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction"
|
||||
v-model:show="showCreateProjectModal"></ProjectCreateModal>
|
||||
<div class="flow-root max-w-[100vw] overflow-x-auto">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
|
||||
@@ -39,6 +39,10 @@ const { createTimeEntry } = useTimeEntriesStore();
|
||||
const show = defineModel('show', { default: false });
|
||||
const saving = ref(false);
|
||||
|
||||
defineProps<{
|
||||
enableEstimatedTime: boolean;
|
||||
}>();
|
||||
|
||||
async function createProject(
|
||||
project: CreateProjectBody
|
||||
): Promise<Project | undefined> {
|
||||
@@ -135,6 +139,7 @@ async function createTag(tag: string) {
|
||||
size="xlarge"
|
||||
:projects="projects"
|
||||
:tasks="tasks"
|
||||
:enableEstimatedTime="enableEstimatedTime"
|
||||
v-model:project="timeEntry.project_id"
|
||||
v-model:task="
|
||||
timeEntry.task_id
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<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';
|
||||
import { Checkbox, InputLabel } from '@/packages/ui/src';
|
||||
|
||||
const props = defineProps<{
|
||||
selectedTimeEntries: TimeEntry[];
|
||||
deleteSelected: () => void;
|
||||
class?: string;
|
||||
allSelected: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [];
|
||||
selectAll: [];
|
||||
unselectAll: [];
|
||||
}>();
|
||||
|
||||
const showMassUpdateModal = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TimeEntryMassUpdateModal
|
||||
:time-entries="selectedTimeEntries"
|
||||
@submit="emit('submit')"
|
||||
v-model:show="showMassUpdateModal"></TimeEntryMassUpdateModal>
|
||||
<MainContainer
|
||||
:class="
|
||||
twMerge(
|
||||
props.class,
|
||||
'text-sm py-1.5 font-medium border-b border-t border-border-secondary flex items-center space-x-3'
|
||||
)
|
||||
">
|
||||
<Checkbox
|
||||
:checked="allSelected"
|
||||
id="selectAll"
|
||||
@update:checked="
|
||||
allSelected ? emit('unselectAll') : emit('selectAll')
|
||||
">
|
||||
</Checkbox>
|
||||
<InputLabel
|
||||
for="selectAll"
|
||||
class="select-none text-text-secondary"
|
||||
v-if="selectedTimeEntries.length > 0">
|
||||
{{ selectedTimeEntries.length }} selected
|
||||
</InputLabel>
|
||||
<InputLabel
|
||||
for="selectAll"
|
||||
class="text-text-secondary select-none"
|
||||
v-else
|
||||
>Select All</InputLabel
|
||||
>
|
||||
<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>
|
||||
@@ -1,290 +0,0 @@
|
||||
<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, Checkbox } 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 descriptionInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
|
||||
watch(show, (value) => {
|
||||
if (value) {
|
||||
nextTick(() => {
|
||||
descriptionInput.value?.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const description = ref<string>('');
|
||||
const taskId = ref<string | null | undefined>(undefined);
|
||||
const projectId = ref<string | null>(null);
|
||||
const billable = ref<boolean | undefined>(undefined);
|
||||
const selectedTags = ref<string[]>([]);
|
||||
|
||||
const { tags } = storeToRefs(useTagsStore());
|
||||
async function createTag(tag: string) {
|
||||
return await useTagsStore().createTag(tag);
|
||||
}
|
||||
|
||||
const timeEntryBillable = computed({
|
||||
get: () => {
|
||||
if (billable.value === undefined) {
|
||||
return 'do-not-update';
|
||||
}
|
||||
return billable.value ? 'billable' : 'non-billable';
|
||||
},
|
||||
set: (value) => {
|
||||
if (value === 'do-not-update') {
|
||||
billable.value = undefined;
|
||||
} else if (value === 'billable') {
|
||||
billable.value = true;
|
||||
} else {
|
||||
billable.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function submit() {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
saving.value = true;
|
||||
if (organizationId) {
|
||||
const timeEntryUpdatesBody = {} as UpdateMultipleTimeEntriesChangeset;
|
||||
if (description.value && description.value !== '') {
|
||||
timeEntryUpdatesBody.description = description.value;
|
||||
}
|
||||
if (projectId.value !== null) {
|
||||
if (projectId.value === '') {
|
||||
// "No Project" is selected
|
||||
timeEntryUpdatesBody.project_id = null;
|
||||
} else {
|
||||
timeEntryUpdatesBody.project_id = projectId.value;
|
||||
}
|
||||
timeEntryUpdatesBody.task_id = null;
|
||||
if (taskId.value !== undefined) {
|
||||
timeEntryUpdatesBody.task_id = taskId.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (billable.value !== undefined) {
|
||||
timeEntryUpdatesBody.billable = billable.value;
|
||||
}
|
||||
if (selectedTags.value.length > 0) {
|
||||
timeEntryUpdatesBody.tags = selectedTags.value;
|
||||
}
|
||||
if (removeAllTags.value) {
|
||||
timeEntryUpdatesBody.tags = [];
|
||||
}
|
||||
|
||||
try {
|
||||
await 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');
|
||||
description.value = '';
|
||||
projectId.value = null;
|
||||
taskId.value = undefined;
|
||||
selectedTags.value = [];
|
||||
billable.value = undefined;
|
||||
saving.value = false;
|
||||
removeAllTags.value = false;
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
const removeAllTags = ref(false);
|
||||
watch(removeAllTags, () => {
|
||||
if (removeAllTags.value) {
|
||||
selectedTags.value = [];
|
||||
}
|
||||
});
|
||||
</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="descriptionInput"
|
||||
v-model="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"
|
||||
empty-placeholder="Select project..."
|
||||
allow-reset
|
||||
size="xlarge"
|
||||
:projects="projects"
|
||||
:tasks="tasks"
|
||||
v-model:project="projectId"
|
||||
v-model:task="taskId"></TimeTrackerProjectTaskDropdown>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="project" value="Tag" />
|
||||
<div class="flex space-x-5">
|
||||
<TagDropdown
|
||||
:createTag
|
||||
v-model="selectedTags"
|
||||
:tags="tags">
|
||||
<template #trigger>
|
||||
<Badge
|
||||
:disabled="removeAllTags"
|
||||
tag="button"
|
||||
size="xlarge">
|
||||
<span v-if="selectedTags.length > 0">
|
||||
Set {{ selectedTags.length }} tags
|
||||
</span>
|
||||
<span v-else> Select Tags... </span>
|
||||
</Badge>
|
||||
</template>
|
||||
</TagDropdown>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
v-model:checked="removeAllTags"
|
||||
id="no_tags"></Checkbox>
|
||||
<InputLabel for="no_tags" value="Remove all tags" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="project" value="Billable" />
|
||||
<div class="flex">
|
||||
<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="billable === undefined">
|
||||
Set billable status
|
||||
</span>
|
||||
<span v-else-if="billable === true">
|
||||
Billable
|
||||
</span>
|
||||
<span v-else> Non Billable </span></Badge
|
||||
>
|
||||
</template>
|
||||
</SelectDropdown>
|
||||
</div>
|
||||
</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>
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
import TimeTrackerRunningInDifferentOrganizationOverlay from '@/packages/ui/src/TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue';
|
||||
import { useClientsStore } from '@/utils/useClients';
|
||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
|
||||
const page = usePage<{
|
||||
auth: {
|
||||
@@ -111,6 +112,7 @@ const { tags } = storeToRefs(useTagsStore());
|
||||
|
||||
<TimeTrackerControls
|
||||
:createProject
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
||||
:createClient
|
||||
:clients
|
||||
:tags
|
||||
|
||||
Reference in New Issue
Block a user