move time entry mass updates to ui package and remove its dependencies

This commit is contained in:
Gregor Vostrak
2024-10-25 16:46:55 +02:00
parent fd77e1e901
commit eb4debe481
16 changed files with 172 additions and 119 deletions

View File

@@ -39,6 +39,7 @@ const { clients } = storeToRefs(useClientsStore());
const gridTemplate = computed(() => { 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;`; 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> </script>
<template> <template>
@@ -47,6 +48,7 @@ const gridTemplate = computed(() => {
:createClient :createClient
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
:clients="clients" :clients="clients"
:enableEstimatedTime="isAllowedToPerformPremiumAction"
v-model:show="showCreateProjectModal"></ProjectCreateModal> v-model:show="showCreateProjectModal"></ProjectCreateModal>
<div class="flow-root max-w-[100vw] overflow-x-auto"> <div class="flow-root max-w-[100vw] overflow-x-auto">
<div class="inline-block min-w-full align-middle"> <div class="inline-block min-w-full align-middle">

View File

@@ -39,6 +39,10 @@ const { createTimeEntry } = useTimeEntriesStore();
const show = defineModel('show', { default: false }); const show = defineModel('show', { default: false });
const saving = ref(false); const saving = ref(false);
defineProps<{
enableEstimatedTime: boolean;
}>();
async function createProject( async function createProject(
project: CreateProjectBody project: CreateProjectBody
): Promise<Project | undefined> { ): Promise<Project | undefined> {
@@ -135,6 +139,7 @@ async function createTag(tag: string) {
size="xlarge" size="xlarge"
:projects="projects" :projects="projects"
:tasks="tasks" :tasks="tasks"
:enableEstimatedTime="enableEstimatedTime"
v-model:project="timeEntry.project_id" v-model:project="timeEntry.project_id"
v-model:task=" v-model:task="
timeEntry.task_id timeEntry.task_id

View File

@@ -24,6 +24,7 @@ import type {
import TimeTrackerRunningInDifferentOrganizationOverlay from '@/packages/ui/src/TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue'; import TimeTrackerRunningInDifferentOrganizationOverlay from '@/packages/ui/src/TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue';
import { useClientsStore } from '@/utils/useClients'; import { useClientsStore } from '@/utils/useClients';
import { getOrganizationCurrencyString } from '@/utils/money'; import { getOrganizationCurrencyString } from '@/utils/money';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
const page = usePage<{ const page = usePage<{
auth: { auth: {
@@ -111,6 +112,7 @@ const { tags } = storeToRefs(useTagsStore());
<TimeTrackerControls <TimeTrackerControls
:createProject :createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:createClient :createClient
:clients :clients
:tags :tags

View File

@@ -22,6 +22,7 @@ import type {
import { getOrganizationCurrencyString } from '@/utils/money'; import { getOrganizationCurrencyString } from '@/utils/money';
import { getCurrentRole } from '@/utils/useUser'; import { getCurrentRole } from '@/utils/useUser';
import { useOrganizationStore } from '@/utils/useOrganization'; import { useOrganizationStore } from '@/utils/useOrganization';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
onMounted(() => { onMounted(() => {
useProjectsStore().fetchProjects(); useProjectsStore().fetchProjects();
@@ -94,6 +95,7 @@ const showBillableRate = computed(() => {
</SecondaryButton> </SecondaryButton>
<ProjectCreateModal <ProjectCreateModal
:createProject :createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction"
:createClient :createClient
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
:clients="clients" :clients="clients"

View File

@@ -64,7 +64,8 @@ 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 TimeEntryMassActionRow from '@/Components/Common/TimeEntry/TimeEntryMassActionRow.vue'; import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
const startDate = useSessionStorage<string>( const startDate = useSessionStorage<string>(
'reporting-start-date', 'reporting-start-date',
@@ -118,7 +119,8 @@ function getFilterAttributes() {
const currentTimeEntryStore = useCurrentTimeEntryStore(); const currentTimeEntryStore = useCurrentTimeEntryStore();
const { currentTimeEntry } = storeToRefs(currentTimeEntryStore); const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
const { setActiveState, startLiveTimer } = currentTimeEntryStore; const { setActiveState, startLiveTimer } = currentTimeEntryStore;
const { createTimeEntry, updateTimeEntry } = useTimeEntriesStore(); const { createTimeEntry, updateTimeEntry, updateTimeEntries } =
useTimeEntriesStore();
const { tags } = storeToRefs(useTagsStore()); const { tags } = storeToRefs(useTagsStore());
@@ -338,13 +340,27 @@ async function clearSelectionAndState() {
</div> </div>
<TimeEntryMassActionRow <TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries" :selected-time-entries="selectedTimeEntries"
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
@submit="clearSelectionAndState" @submit="clearSelectionAndState"
:delete-selected="deleteSelected" :delete-selected="deleteSelected"
@select-all="selectedTimeEntries = [...timeEntries]" @select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []" @unselect-all="selectedTimeEntries = []"
:all-selected=" :all-selected="selectedTimeEntries.length === timeEntries.length"
selectedTimeEntries.length === timeEntries.length :projects="projects"
"></TimeEntryMassActionRow> :tasks="tasks"
:tags="tags"
:currency="getOrganizationCurrencyString()"
:clients="clients"
:update-time-entries="
(args) =>
updateTimeEntries(
selectedTimeEntries.map((timeEntry) => timeEntry.id),
args
)
"
:create-project="createProject"
:create-client="createClient"
:createTag="createTag"></TimeEntryMassActionRow>
<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
@@ -357,6 +373,7 @@ async function clearSelectionAndState() {
" "
:createClient :createClient
:createProject :createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:projects="projects" :projects="projects"
:tasks="tasks" :tasks="tasks"
:tags="tags" :tags="tags"

View File

@@ -26,14 +26,19 @@ 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'; import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
const timeEntriesStore = useTimeEntriesStore(); const timeEntriesStore = useTimeEntriesStore();
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore); const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
const { updateTimeEntry, fetchTimeEntries, createTimeEntry } = const { updateTimeEntry, fetchTimeEntries, createTimeEntry } =
useTimeEntriesStore(); useTimeEntriesStore();
async function updateTimeEntries(ids: string[], changes: Partial<TimeEntry>) { async function updateTimeEntries(
ids: string[],
changes: UpdateMultipleTimeEntriesChangeset
) {
await useTimeEntriesStore().updateTimeEntries(ids, changes); await useTimeEntriesStore().updateTimeEntries(ids, changes);
fetchTimeEntries(); fetchTimeEntries();
} }
@@ -114,6 +119,7 @@ function deleteSelected() {
<template> <template>
<TimeEntryCreateModal <TimeEntryCreateModal
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
v-model:show="showManualTimeEntryModal"></TimeEntryCreateModal> v-model:show="showManualTimeEntryModal"></TimeEntryCreateModal>
<AppLayout title="Dashboard" data-testid="time_view"> <AppLayout title="Dashboard" data-testid="time_view">
<MainContainer <MainContainer
@@ -135,14 +141,31 @@ function deleteSelected() {
</MainContainer> </MainContainer>
<TimeEntryMassActionRow <TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries" :selected-time-entries="selectedTimeEntries"
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
@submit="clearSelectionAndState" @submit="clearSelectionAndState"
:all-selected="selectedTimeEntries.length === timeEntries.length" :all-selected="selectedTimeEntries.length === timeEntries.length"
@select-all="selectedTimeEntries = [...timeEntries]" @select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []" @unselect-all="selectedTimeEntries = []"
:delete-selected="deleteSelected"></TimeEntryMassActionRow> :delete-selected="deleteSelected"
:projects="projects"
:tasks="tasks"
:tags="tags"
:currency="getOrganizationCurrencyString()"
:clients="clients"
:update-time-entries="
(args) =>
updateTimeEntries(
selectedTimeEntries.map((timeEntry) => timeEntry.id),
args
)
"
:create-project="createProject"
:create-client="createClient"
:createTag="createTag"></TimeEntryMassActionRow>
<TimeEntryGroupedTable <TimeEntryGroupedTable
v-model:selected="selectedTimeEntries" v-model:selected="selectedTimeEntries"
:createProject :createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:clients :clients
:createClient :createClient
:updateTimeEntry :updateTimeEntry

View File

@@ -19,7 +19,6 @@ import EstimatedTimeSection from '@/packages/ui/src/EstimatedTimeSection.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue'; import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import ProjectEditBillableSection from '@/packages/ui/src/Project/ProjectEditBillableSection.vue'; import ProjectEditBillableSection from '@/packages/ui/src/Project/ProjectEditBillableSection.vue';
import type { Client } from '@/packages/api/src'; import type { Client } from '@/packages/api/src';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
const show = defineModel('show', { default: false }); const show = defineModel('show', { default: false });
const saving = ref(false); const saving = ref(false);
@@ -29,6 +28,7 @@ const props = defineProps<{
createProject: (project: CreateProjectBody) => Promise<Project | undefined>; createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>; createClient: (client: CreateClientBody) => Promise<Client | undefined>;
currency: string; currency: string;
enableEstimatedTime: boolean;
}>(); }>();
const activeClients = computed(() => { const activeClients = computed(() => {
@@ -138,7 +138,7 @@ const currentClientName = computed(() => {
</div> </div>
<div> <div>
<EstimatedTimeSection <EstimatedTimeSection
v-if="isAllowedToPerformPremiumAction()" v-if="enableEstimatedTime"
@submit="submit()" @submit="submit()"
v-model="project.estimated_time"></EstimatedTimeSection> v-model="project.estimated_time"></EstimatedTimeSection>
</div> </div>

View File

@@ -39,6 +39,7 @@ const props = defineProps<{
deleteTimeEntries: (timeEntries: TimeEntry[]) => void; deleteTimeEntries: (timeEntries: TimeEntry[]) => void;
currency: string; currency: string;
selectedTimeEntries: TimeEntry[]; selectedTimeEntries: TimeEntry[];
enableEstimatedTime: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
selected: [TimeEntry[]]; selected: [TimeEntry[]];
@@ -125,6 +126,7 @@ function onSelectChange(event: Event) {
:showBadgeBorder="false" :showBadgeBorder="false"
@changed="updateProjectAndTask" @changed="updateProjectAndTask"
:project="timeEntry.project_id" :project="timeEntry.project_id"
:enableEstimatedTime
:currency="currency" :currency="currency"
:task=" :task="
timeEntry.task_id timeEntry.task_id
@@ -174,6 +176,7 @@ function onSelectChange(event: Event) {
class="w-full border-t border-default-background-separator bg-black/15"> class="w-full border-t border-default-background-separator bg-black/15">
<TimeEntryRow <TimeEntryRow
:projects="projects" :projects="projects"
:enableEstimatedTime
:tasks="tasks" :tasks="tasks"
:selected=" :selected="
!!selectedTimeEntries.find( !!selectedTimeEntries.find(

View File

@@ -37,6 +37,7 @@ const props = defineProps<{
createProject: (project: CreateProjectBody) => Promise<Project | undefined>; createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>; createClient: (client: CreateClientBody) => Promise<Client | undefined>;
currency: string; currency: string;
enableEstimatedTime: boolean;
}>(); }>();
const groupedTimeEntries = computed(() => { const groupedTimeEntries = computed(() => {
@@ -160,6 +161,7 @@ function unselectAllTimeEntries(value: TimeEntriesGroupedByType[]) {
<template v-for="entry in value" :key="entry.id"> <template v-for="entry in value" :key="entry.id">
<TimeEntryAggregateRow <TimeEntryAggregateRow
:createProject :createProject
:enableEstimatedTime
:selected-time-entries="selectedTimeEntries" :selected-time-entries="selectedTimeEntries"
@selected=" @selected="
(timeEntries) => { (timeEntries) => {
@@ -194,6 +196,7 @@ function unselectAllTimeEntries(value: TimeEntriesGroupedByType[]) {
:time-entry="entry"></TimeEntryAggregateRow> :time-entry="entry"></TimeEntryAggregateRow>
<TimeEntryRow <TimeEntryRow
:createClient :createClient
:enableEstimatedTime
:createProject :createProject
:projects="projects" :projects="projects"
:selected=" :selected="

View File

@@ -1,8 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import MainContainer from '@/packages/ui/src/MainContainer.vue'; import MainContainer from '@/packages/ui/src/MainContainer.vue';
import { PencilSquareIcon, TrashIcon } from '@heroicons/vue/20/solid'; import { PencilSquareIcon, TrashIcon } from '@heroicons/vue/20/solid';
import TimeEntryMassUpdateModal from '@/Components/Common/TimeEntry/TimeEntryMassUpdateModal.vue'; import TimeEntryMassUpdateModal from '@/packages/ui/src/TimeEntry/TimeEntryMassUpdateModal.vue';
import type { TimeEntry } from '@/packages/api/src'; import type {
Client,
CreateClientBody,
CreateProjectBody,
Project,
Tag,
Task,
TimeEntry,
UpdateMultipleTimeEntriesChangeset,
} from '@/packages/api/src';
import { ref } from 'vue'; import { ref } from 'vue';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { Checkbox, InputLabel } from '@/packages/ui/src'; import { Checkbox, InputLabel } from '@/packages/ui/src';
@@ -12,6 +21,18 @@ const props = defineProps<{
deleteSelected: () => void; deleteSelected: () => void;
class?: string; class?: string;
allSelected: boolean; allSelected: boolean;
projects: Project[];
tasks: Task[];
tags: Tag[];
clients: Client[];
createTag: (name: string) => Promise<Tag | undefined>;
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
updateTimeEntries: (
changeset: UpdateMultipleTimeEntriesChangeset
) => Promise<void>;
currency: string;
enableEstimatedTime: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -25,6 +46,16 @@ const showMassUpdateModal = ref(false);
<template> <template>
<TimeEntryMassUpdateModal <TimeEntryMassUpdateModal
:projects
:tasks
:tags
:clients
:createTag
:createProject
:createClient
:updateTimeEntries
:enableEstimatedTime
:currency
:time-entries="selectedTimeEntries" :time-entries="selectedTimeEntries"
@submit="emit('submit')" @submit="emit('submit')"
v-model:show="showMassUpdateModal"></TimeEntryMassUpdateModal> v-model:show="showMassUpdateModal"></TimeEntryMassUpdateModal>

View File

@@ -1,66 +1,49 @@
<script setup lang="ts"> <script setup lang="ts">
import TextInput from '@/packages/ui/src/Input/TextInput.vue'; import TextInput from '../Input/TextInput.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue'; import SecondaryButton from '../Buttons/SecondaryButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue'; import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { computed, nextTick, ref, watch } from 'vue'; import { computed, nextTick, ref, watch } from 'vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue'; import PrimaryButton from '../Buttons/PrimaryButton.vue';
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue'; import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue'; import InputLabel from '../Input/InputLabel.vue';
import { storeToRefs } from 'pinia';
import { useTasksStore } from '@/utils/useTasks';
import { useProjectsStore } from '@/utils/useProjects';
import { useTagsStore } from '@/utils/useTags';
import { import {
type CreateClientBody, type CreateClientBody,
type CreateProjectBody, type CreateProjectBody,
type Project, type Project,
type Client, type Client,
api,
type TimeEntry, type TimeEntry,
type UpdateMultipleTimeEntriesChangeset, type UpdateMultipleTimeEntriesChangeset,
} from '@/packages/api/src'; } from '@/packages/api/src';
import { useClientsStore } from '@/utils/useClients';
import { getOrganizationCurrencyString } from '@/utils/money';
import { Badge, Checkbox } from '@/packages/ui/src'; import { Badge, Checkbox } from '@/packages/ui/src';
import SelectDropdown from '../../../packages/ui/src/Input/SelectDropdown.vue'; import SelectDropdown from '../Input/SelectDropdown.vue';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { useNotificationsStore } from '@/utils/notification';
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue'; import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
const projectStore = useProjectsStore(); import type { Tag, Task } from '@/packages/api/src';
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 show = defineModel('show', { default: false });
const saving = ref(false); const saving = ref(false);
async function createProject(
project: CreateProjectBody
): Promise<Project | undefined> {
return await useProjectsStore().createProject(project);
}
const props = defineProps<{ const props = defineProps<{
timeEntries: TimeEntry[]; timeEntries: TimeEntry[];
projects: Project[];
tasks: Task[];
clients: Client[];
tags: Tag[];
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
createTag: (name: string) => Promise<Tag | undefined>;
updateTimeEntries: (
changeset: UpdateMultipleTimeEntriesChangeset
) => Promise<void>;
currency: string;
enableEstimatedTime: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
submit: []; submit: [];
}>(); }>();
async function createClient(
body: CreateClientBody
): Promise<Client | undefined> {
return await useClientsStore().createClient(body);
}
const descriptionInput = ref<HTMLInputElement | null>(null); const descriptionInput = ref<HTMLInputElement | null>(null);
const { handleApiRequestNotifications } = useNotificationsStore();
watch(show, (value) => { watch(show, (value) => {
if (value) { if (value) {
nextTick(() => { nextTick(() => {
@@ -75,11 +58,6 @@ const projectId = ref<string | null>(null);
const billable = ref<boolean | undefined>(undefined); const billable = ref<boolean | undefined>(undefined);
const selectedTags = ref<string[]>([]); const selectedTags = ref<string[]>([]);
const { tags } = storeToRefs(useTagsStore());
async function createTag(tag: string) {
return await useTagsStore().createTag(tag);
}
const timeEntryBillable = computed({ const timeEntryBillable = computed({
get: () => { get: () => {
if (billable.value === undefined) { if (billable.value === undefined) {
@@ -99,71 +77,48 @@ const timeEntryBillable = computed({
}); });
async function submit() { async function submit() {
const organizationId = getCurrentOrganizationId();
saving.value = true; saving.value = true;
if (organizationId) { const timeEntryUpdatesBody = {} as UpdateMultipleTimeEntriesChangeset;
const timeEntryUpdatesBody = {} as UpdateMultipleTimeEntriesChangeset; if (description.value && description.value !== '') {
if (description.value && description.value !== '') { timeEntryUpdatesBody.description = 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;
} }
if (projectId.value !== null) { timeEntryUpdatesBody.task_id = null;
if (projectId.value === '') { if (taskId.value !== undefined) {
// "No Project" is selected timeEntryUpdatesBody.task_id = taskId.value;
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) { if (billable.value !== undefined) {
timeEntryUpdatesBody.billable = billable.value; timeEntryUpdatesBody.billable = billable.value;
} }
if (selectedTags.value.length > 0) { if (selectedTags.value.length > 0) {
timeEntryUpdatesBody.tags = selectedTags.value; timeEntryUpdatesBody.tags = selectedTags.value;
} }
if (removeAllTags.value) { if (removeAllTags.value) {
timeEntryUpdatesBody.tags = []; timeEntryUpdatesBody.tags = [];
} }
try { try {
await handleApiRequestNotifications( await props.updateTimeEntries({ ...timeEntryUpdatesBody });
() =>
api.updateMultipleTimeEntries( show.value = false;
{ emit('submit');
ids: props.timeEntries.map( description.value = '';
(timeEntry) => timeEntry.id projectId.value = null;
), taskId.value = undefined;
changes: { selectedTags.value = [];
...timeEntryUpdatesBody, billable.value = undefined;
}, saving.value = false;
}, removeAllTags.value = false;
{ } catch (e) {
params: { saving.value = false;
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); const removeAllTags = ref(false);
@@ -200,11 +155,12 @@ watch(removeAllTags, () => {
:clients :clients
:createProject :createProject
:createClient :createClient
:currency="getOrganizationCurrencyString()" :currency="currency"
class="mt-1" class="mt-1"
empty-placeholder="Select project..." empty-placeholder="Select project..."
allow-reset allow-reset
size="xlarge" size="xlarge"
:enableEstimatedTime
:projects="projects" :projects="projects"
:tasks="tasks" :tasks="tasks"
v-model:project="projectId" v-model:project="projectId"

View File

@@ -38,6 +38,7 @@ const props = defineProps<{
showMember?: boolean; showMember?: boolean;
showDate?: boolean; showDate?: boolean;
selected?: boolean; selected?: boolean;
enableEstimatedTime: boolean;
}>(); }>();
const emit = defineEmits<{ selected: []; unselected: [] }>(); const emit = defineEmits<{ selected: []; unselected: [] }>();
@@ -117,6 +118,7 @@ function onSelectChange(event: Event) {
@changed="updateProjectAndTask" @changed="updateProjectAndTask"
:project="timeEntry.project_id" :project="timeEntry.project_id"
:currency="currency" :currency="currency"
:enableEstimatedTime
:task=" :task="
timeEntry.task_id timeEntry.task_id
"></TimeTrackerProjectTaskDropdown> "></TimeTrackerProjectTaskDropdown>

View File

@@ -33,6 +33,7 @@ const props = defineProps<{
createClient: (client: CreateClientBody) => Promise<Client | undefined>; createClient: (client: CreateClientBody) => Promise<Client | undefined>;
isActive: boolean; isActive: boolean;
currency: string; currency: string;
enableEstimatedTime: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -92,7 +93,7 @@ function updateTimeEntryDescription() {
class="flex items-center relative @container" class="flex items-center relative @container"
data-testid="dashboard_timer"> data-testid="dashboard_timer">
<div <div
class="flex flex-col lg:flex-row w-full justify-between rounded-lg bg-card-background border-card-border border transition shadow-card"> class="flex flex-col @2xl:flex-row w-full justify-between rounded-lg bg-card-background border-card-border border transition shadow-card">
<div class="flex flex-1 items-center pr-6"> <div class="flex flex-1 items-center pr-6">
<input <input
placeholder="What are you working on?" placeholder="What are you working on?"
@@ -101,12 +102,12 @@ function updateTimeEntryDescription() {
v-model="tempDescription" v-model="tempDescription"
@keydown.enter="startTimerIfNotActive" @keydown.enter="startTimerIfNotActive"
@blur="updateTimeEntryDescription" @blur="updateTimeEntryDescription"
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator lg:px-4 text-base @4xl:text-lg text-white font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition" class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator @2xl:px-4 text-base @4xl:text-lg text-white font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition"
type="text" /> type="text" />
</div> </div>
<div class="flex items-center justify-between pl-2 shrink min-w-0"> <div class="flex items-center justify-between pl-2 shrink min-w-0">
<div <div
class="flex items-center w-[130px] sm:w-auto shrink min-w-0"> class="flex items-center w-[130px] @2xl:w-auto shrink min-w-0">
<TimeTrackerProjectTaskDropdown <TimeTrackerProjectTaskDropdown
:createClient :createClient
:clients :clients
@@ -115,12 +116,13 @@ function updateTimeEntryDescription() {
:projects="projects" :projects="projects"
:tasks="tasks" :tasks="tasks"
@changed="updateProject" @changed="updateProject"
:enableEstimatedTime="enableEstimatedTime"
v-model:project="currentTimeEntry.project_id" v-model:project="currentTimeEntry.project_id"
v-model:task=" v-model:task="
currentTimeEntry.task_id currentTimeEntry.task_id
"></TimeTrackerProjectTaskDropdown> "></TimeTrackerProjectTaskDropdown>
</div> </div>
<div class="flex items-center lg:space-x-2 px-2 lg:px-4"> <div class="flex items-center @2xl:space-x-2 px-2 @2xl:px-4">
<TimeTrackerTagDropdown <TimeTrackerTagDropdown
@changed="$emit('updateTimeEntry')" @changed="$emit('updateTimeEntry')"
:createTag :createTag
@@ -149,7 +151,7 @@ function updateTimeEntryDescription() {
</div> </div>
</div> </div>
<div <div
class="pl-4 lg:pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0"> class="pl-4 @2xl:pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0">
<TimeTrackerStartStop <TimeTrackerStartStop
:active="isActive" :active="isActive"
@changed="onToggleButtonPress" @changed="onToggleButtonPress"

View File

@@ -63,6 +63,7 @@ const props = withDefaults(
currency: string; currency: string;
emptyPlaceholder: string; emptyPlaceholder: string;
allowReset: boolean; allowReset: boolean;
enableEstimatedTime: boolean;
}>(), }>(),
{ {
showBadgeBorder: true, showBadgeBorder: true,
@@ -704,6 +705,7 @@ const showCreateProject = ref(false);
</Dropdown> </Dropdown>
<ProjectCreateModal <ProjectCreateModal
:createClient :createClient
:enableEstimatedTime="enableEstimatedTime"
:currency="currency" :currency="currency"
:clients="clients" :clients="clients"
:createProject :createProject

View File

@@ -25,6 +25,7 @@ import SelectDropdown from './Input/SelectDropdown.vue';
import Badge from './Badge.vue'; import Badge from './Badge.vue';
import Checkbox from './Input/Checkbox.vue'; import Checkbox from './Input/Checkbox.vue';
import TimeEntryGroupedTable from './TimeEntry/TimeEntryGroupedTable.vue'; import TimeEntryGroupedTable from './TimeEntry/TimeEntryGroupedTable.vue';
import TimeEntryMassActionRow from './TimeEntry/TimeEntryMassActionRow.vue';
export { export {
money, money,
@@ -46,4 +47,5 @@ export {
Badge, Badge,
Checkbox, Checkbox,
TimeEntryGroupedTable, TimeEntryGroupedTable,
TimeEntryMassActionRow,
}; };

View File

@@ -13,6 +13,7 @@ import {
} from '@/packages/api/src'; } from '@/packages/api/src';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useNotificationsStore } from '@/utils/notification'; import { useNotificationsStore } from '@/utils/notification';
import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src';
export const useTimeEntriesStore = defineStore('timeEntries', () => { export const useTimeEntriesStore = defineStore('timeEntries', () => {
const timeEntries = ref<TimeEntry[]>(reactive([])); const timeEntries = ref<TimeEntry[]>(reactive([]));
@@ -83,7 +84,7 @@ export const useTimeEntriesStore = defineStore('timeEntries', () => {
async function updateTimeEntries( async function updateTimeEntries(
ids: string[], ids: string[],
changes: Partial<TimeEntry> changes: UpdateMultipleTimeEntriesChangeset
) { ) {
const organizationId = getCurrentOrganizationId(); const organizationId = getCurrentOrganizationId();
if (organizationId) { if (organizationId) {