{
- return projects.value.reduce((filtered: ProjectWithTasks[], project) => {
- const projectNameIncludesSearchTerm = project.name
- .toLowerCase()
- .includes(searchValue.value?.toLowerCase()?.trim() || '');
-
- // check if one of the project tasks
- const projectTasks = tasks.value.filter((task) => {
- return task.project_id === project.id;
- });
-
- const filteredTasks = projectTasks.filter((task) => {
- return task.name
+ return projects.value.reduce(
+ (filtered: ProjectWithTasks[], project) => {
+ const projectNameIncludesSearchTerm = project.name
.toLowerCase()
.includes(searchValue.value?.toLowerCase()?.trim() || '');
- });
- if (projectNameIncludesSearchTerm || filteredTasks.length > 0) {
- filtered.push({ project: project, tasks: filteredTasks });
- }
+ // check if one of the project tasks
+ const projectTasks = tasks.value.filter((task) => {
+ return task.project_id === project.id;
+ });
- return filtered;
- }, []);
+ const filteredTasks = projectTasks.filter((task) => {
+ return task.name
+ .toLowerCase()
+ .includes(searchValue.value?.toLowerCase()?.trim() || '');
+ });
+
+ if (projectNameIncludesSearchTerm || filteredTasks.length > 0) {
+ filtered.push({ project: project, tasks: filteredTasks });
+ }
+
+ return filtered;
+ },
+ [
+ {
+ project: {
+ id: '',
+ name: 'No Project',
+ color: 'var(--theme-color-icon-default)',
+ value: '',
+ client_id: null,
+ billable_rate: null,
+ },
+ tasks: [],
+ },
+ ]
+ );
});
async function addClientIfNoneExists() {
diff --git a/resources/js/Components/Common/TimeTrackerStartStop.vue b/resources/js/Components/Common/TimeTrackerStartStop.vue
index 9b553b0e..b0e1d8bc 100644
--- a/resources/js/Components/Common/TimeTrackerStartStop.vue
+++ b/resources/js/Components/Common/TimeTrackerStartStop.vue
@@ -16,11 +16,11 @@ const props = withDefaults(
);
const buttonSizeClasses = {
base: 'w-8 h-8 bg-accent-200/40 hover:scale-110 hover:bg-accent-300/70 ring-accent-200/10 focus:ring-accent-200/10 hover:ring-4',
- large: 'w-8 sm:w-11 h-8 sm:h-11 ring-accent-200/10 focus:ring-accent-200/20 ring-4 sm:ring-8 hover:scale-110',
+ large: 'w-11 h-11 ring-accent-200/10 focus:ring-accent-200/20 ring-4 sm:ring-8 hover:scale-110',
};
const iconClass = {
base: 'w-3.5 h-3.5',
- large: 'w-3.5 h-3.5 sm:w-4 sm:h-4',
+ large: 'w-4 h-4',
};
const buttonColorClasses = computed(() => {
diff --git a/resources/js/Components/Dropdown.vue b/resources/js/Components/Dropdown.vue
index b1363360..946cc828 100644
--- a/resources/js/Components/Dropdown.vue
+++ b/resources/js/Components/Dropdown.vue
@@ -8,18 +8,11 @@ const props = withDefaults(
defineProps<{
align: Placement;
width: string;
- contentClasses?: string[];
closeOnContentClick: boolean;
}>(),
{
align: 'bottom-start',
width: '48',
- contentClasses: () => [
- 'overflow-none',
- 'bg-card-background',
- 'border',
- 'border-card-border',
- ],
closeOnContentClick: true,
}
);
@@ -101,8 +94,7 @@ const { floatingStyles } = useFloating(reference, floating, {
leave-to-class="transform opacity-0 scale-95">
+ class="rounded-lg ring-1 relative ring-black ring-opacity-5 border border-card-border overflow-none bg-card-background shadow-lg">
diff --git a/resources/js/Components/TimeTracker.vue b/resources/js/Components/TimeTracker.vue
index ef4907a7..9e374672 100644
--- a/resources/js/Components/TimeTracker.vue
+++ b/resources/js/Components/TimeTracker.vue
@@ -194,7 +194,7 @@ function switchToTimeEntryOrganization() {
v-model="currentTimeEntry.description"
@keydown.enter="startTimerIfNotActive"
@blur="updateTimeEntry"
- class="w-full rounded-l-lg py-2.5 px-3 border-b border-b-card-background-separator sm:px-4 text-sm sm:text-lg text-white focus:bg-card-background-active 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 border-b border-b-card-background-separator sm:px-4 text-base sm:text-lg text-white focus:bg-card-background-active font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition"
type="text" />
diff --git a/resources/js/Pages/Time.vue b/resources/js/Pages/Time.vue
index ba4e7806..80802099 100644
--- a/resources/js/Pages/Time.vue
+++ b/resources/js/Pages/Time.vue
@@ -3,17 +3,21 @@ import AppLayout from '@/Layouts/AppLayout.vue';
import TimeTracker from '@/Components/TimeTracker.vue';
import { computed, onMounted, ref, watch } from 'vue';
import MainContainer from '@/Pages/MainContainer.vue';
-import { useTimeEntriesStore } from '@/utils/useTimeEntries';
+import {
+ type TimeEntriesGroupedByType,
+ useTimeEntriesStore,
+} from '@/utils/useTimeEntries';
import { storeToRefs } from 'pinia';
import type { TimeEntry } from '@/utils/api';
import TimeEntryRowHeading from '@/Components/Common/TimeEntry/TimeEntryRowHeading.vue';
import TimeEntryRow from '@/Components/Common/TimeEntry/TimeEntryRow.vue';
import { useElementVisibility } from '@vueuse/core';
import { ClockIcon } from '@heroicons/vue/20/solid';
-import { getLocalizedDateFromTimestamp } from '@/utils/time';
+import { getDayJsInstance, getLocalizedDateFromTimestamp } from '@/utils/time';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import { PlusIcon } from '@heroicons/vue/16/solid';
import TimeEntryCreateModal from '@/Components/Common/TimeEntry/TimeEntryCreateModal.vue';
+import TimeEntryAggregateRow from '@/Components/Common/TimeEntry/TimeEntryAggregateRow.vue';
const timeEntriesStore = useTimeEntriesStore();
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
@@ -38,14 +42,67 @@ onMounted(async () => {
});
const groupedTimeEntries = computed(() => {
- const groupedEntries: Record = {};
+ const groupedEntriesByDay: Record = {};
for (const entry of timeEntries.value) {
const oldEntries =
- groupedEntries[getLocalizedDateFromTimestamp(entry.start)];
- const newEntries = [...(oldEntries ?? []), entry];
- groupedEntries[getLocalizedDateFromTimestamp(entry.start)] = newEntries;
+ groupedEntriesByDay[getLocalizedDateFromTimestamp(entry.start)];
+ groupedEntriesByDay[getLocalizedDateFromTimestamp(entry.start)] = [
+ ...(oldEntries ?? []),
+ entry,
+ ];
}
- return groupedEntries;
+ const groupedEntriesByDayAndType: Record<
+ string,
+ TimeEntriesGroupedByType[]
+ > = {};
+ for (const dailyEntriesKey in groupedEntriesByDay) {
+ const dailyEntries = groupedEntriesByDay[dailyEntriesKey];
+ const newDailyEntries: TimeEntriesGroupedByType[] = [];
+
+ for (const entry of dailyEntries) {
+ // check if same entry already exists
+ const oldEntriesIndex = newDailyEntries.findIndex(
+ (e) =>
+ e.project_id === entry.project_id &&
+ e.task_id === entry.task_id &&
+ e.billable === entry.billable &&
+ e.description === entry.description
+ );
+ console.log(oldEntriesIndex);
+ if (oldEntriesIndex !== -1 && newDailyEntries[oldEntriesIndex]) {
+ newDailyEntries[oldEntriesIndex].timeEntries.push(entry);
+
+ // Add up durations for time entries of the same type
+ console.log(newDailyEntries[oldEntriesIndex], entry?.duration);
+ newDailyEntries[oldEntriesIndex].duration =
+ (newDailyEntries[oldEntriesIndex].duration ?? 0) +
+ (entry?.duration ?? 0);
+
+ // adapt start end times so they show the earliest start and latest end time
+ if (
+ getDayJsInstance()(entry.start).isBefore(
+ getDayJsInstance()(
+ newDailyEntries[oldEntriesIndex].start
+ )
+ )
+ ) {
+ newDailyEntries[oldEntriesIndex].start = entry.start;
+ }
+ if (
+ getDayJsInstance()(entry.end).isAfter(
+ getDayJsInstance()(newDailyEntries[oldEntriesIndex].end)
+ )
+ ) {
+ newDailyEntries[oldEntriesIndex].end = entry.end;
+ }
+ } else {
+ newDailyEntries.push({ ...entry, timeEntries: [entry] });
+ }
+ }
+
+ groupedEntriesByDayAndType[dailyEntriesKey] = newDailyEntries;
+ }
+ return groupedEntriesByDayAndType;
});
const showManualTimeEntryModal = ref(false);
@@ -66,17 +123,21 @@ const showManualTimeEntryModal = ref(false);
class="w-full text-center flex justify-center"
@click="showManualTimeEntryModal = true"
:icon="PlusIcon"
- >Manual time entry
+ >Manual time entry
+
-
+
+
+
+
{
}
}
+ async function removeMember(membershipId: string) {
+ const organization = getCurrentOrganizationId();
+ if (organization) {
+ await handleApiRequestNotifications(
+ api.removeMember(
+ {},
+ {
+ params: {
+ organization: organization,
+ membership: membershipId,
+ },
+ }
+ ),
+ 'Member deleted successfully',
+ 'Failed to delete member'
+ );
+ await fetchMembers();
+ }
+ }
+
const members = computed(() => {
return membersResponse.value?.data || [];
});
- return { members, fetchMembers };
+ return { members, fetchMembers, removeMember };
});
diff --git a/resources/js/utils/useTimeEntries.ts b/resources/js/utils/useTimeEntries.ts
index 08d41325..8d98f03e 100644
--- a/resources/js/utils/useTimeEntries.ts
+++ b/resources/js/utils/useTimeEntries.ts
@@ -5,6 +5,7 @@ import { reactive, ref } from 'vue';
import type { CreateTimeEntryBody, TimeEntry } from '@/utils/api';
import dayjs from 'dayjs';
import { useNotificationsStore } from '@/utils/notification';
+export type TimeEntriesGroupedByType = TimeEntry & { timeEntries: TimeEntry[] };
export const useTimeEntriesStore = defineStore('timeEntries', () => {
const timeEntries = ref(reactive([]));