mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 12:12:15 +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(() => {
|
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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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="
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user