refactor time entry and projecttaskdropdown components to not rely on pinia stores

This commit is contained in:
Gregor Vostrak
2024-07-09 18:40:12 +02:00
parent 375cee7589
commit a519c119d4
12 changed files with 201 additions and 256 deletions

View File

@@ -61,12 +61,6 @@ async function assertThatTimeEntryRowIsStopped(newTimeEntry: Locator) {
);
}
async function assertThatTimeEntryRowIsStarted(newTimeEntry: Locator) {
await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass(
/bg-red-400\/80/
);
}
test('test that updating a description of a time entry in the overview works on blur', async ({
page,
}) => {
@@ -259,48 +253,6 @@ test('test that updating a the duration in the overview works on blur', async ({
).toHaveValue('0h 20min');
});
// Test that start stop button stops running timer
test('test that stopping a time entry from the overview works', async ({
page,
}) => {
await goToTimeOverview(page);
const timeEntryRows = page.locator('[data-testid="time_entry_row"]');
await Promise.all([
newTimeEntryResponse(page),
startOrStopTimerWithButton(page),
assertThatTimerHasStarted(page),
page.waitForResponse(
(response) =>
response.url().includes('/time-entries') &&
response.status() === 200
),
]);
await page.waitForTimeout(1500);
const newTimeEntry = timeEntryRows.first();
const stopButton = newTimeEntry.getByTestId('timer_button');
await assertThatTimeEntryRowIsStarted(newTimeEntry);
await Promise.all([
page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.json()).data.id !== null &&
(await response.json()).data.start !== null &&
(await response.json()).data.end !== null
);
}),
stopButton.click(),
]);
await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass(
/bg-accent-300\/70/
);
});
// Test that start stop button stops running timer
test('test that starting a time entry from the overview works', async ({
page,
@@ -327,7 +279,8 @@ test('test that starting a time entry from the overview works', async ({
startButton.click(),
]);
await expect(startButton).toHaveClass(/bg-red-500\/80/);
await assertThatTimerHasStarted(page);
await page.waitForTimeout(1500);
await Promise.all([
page.waitForResponse(async (response) => {
@@ -341,67 +294,7 @@ test('test that starting a time entry from the overview works', async ({
);
}),
startOrStopTimerWithButton(page),
expect(startButton).toHaveClass(/bg-accent-300\/70/),
]);
});
test('test that updating a the duration in the overview for a running timer works on blur', async ({
page,
}) => {
await goToTimeOverview(page);
const timeEntryRows = page.locator('[data-testid="time_entry_row"]');
await Promise.all([
newTimeEntryResponse(page),
startOrStopTimerWithButton(page),
assertThatTimerHasStarted(page),
page.waitForResponse(
(response) =>
response.url().includes('/time-entries') &&
response.status() === 200
),
]);
await page.waitForTimeout(1500);
const newTimeEntry = timeEntryRows.first();
const startButton = newTimeEntry.getByTestId('timer_button');
await page.waitForTimeout(1500);
const timeEntryDurationInput = newTimeEntry.getByTestId(
'time_entry_duration_input'
);
await timeEntryDurationInput.fill('20min');
await Promise.all([
page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.json()).data.id !== null &&
// TODO! Actually check the value
(await response.json()).data.start !== null &&
(await response.json()).data.end !== null
);
}),
timeEntryDurationInput.press('Tab'),
]);
await expect(page.getByTestId('time_entry_time')).toHaveValue('00:20:00');
await Promise.all([
page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.json()).data.id !== null &&
(await response.json()).data.start !== null &&
(await response.json()).data.end !== null
);
}),
startOrStopTimerWithButton(page),
expect(startButton).toHaveClass(/bg-accent-300\/70/),
assertThatTimerIsStopped(page),
]);
});
@@ -468,3 +361,5 @@ test.skip('test that load more works when the end of page is reached', async ({
// TODO: Test Grouped time entries by description/project
// TODO: Add Test for Date Update
// TODO: Test that project can be created in the time entry row

View File

@@ -279,3 +279,5 @@ test('test that adding a new tag when the timer is running', async ({
// test that sidebar timetracker changes state when tmer on dashboard is started
// test billable toggle
// TODO: Test that project can be created in the time tracker row

View File

@@ -6,22 +6,26 @@ import { computed, ref } from 'vue';
import type { CreateProjectBody } from '@/utils/api';
import { getRandomColor } from '@/utils/color';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { useProjectsStore } from '@/utils/useProjects';
import { useFocus } from '@vueuse/core';
import ClientDropdown from '@/Components/Common/Client/ClientDropdown.vue';
import Badge from '@/Components/Common/Badge.vue';
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
import ProjectColorSelector from '@/Components/Common/Project/ProjectColorSelector.vue';
import { UserCircleIcon } from '@heroicons/vue/20/solid';
import InputLabel from '@/Components/InputLabel.vue';
import ProjectEditBillableSection from '@/Components/Common/Project/ProjectEditBillableSection.vue';
import type { Client } from '@/utils/api';
const { createProject } = useProjectsStore();
const { clients } = storeToRefs(useClientsStore());
const show = defineModel('show', { default: false });
const saving = ref(false);
const props = defineProps<{
clients: Client[];
}>();
const emit = defineEmits<{
submit: [project: CreateProjectBody, callback: () => void];
}>();
const project = ref<CreateProjectBody>({
name: '',
color: getRandomColor(),
@@ -31,15 +35,16 @@ const project = ref<CreateProjectBody>({
});
async function submit() {
await createProject(project.value);
show.value = false;
project.value = {
name: '',
color: getRandomColor(),
client_id: null,
billable_rate: null,
is_billable: false,
};
emit('submit', project.value, () => {
show.value = false;
project.value = {
name: '',
color: getRandomColor(),
client_id: null,
billable_rate: null,
is_billable: false,
};
});
}
const projectNameInput = ref<HTMLInputElement | null>(null);
@@ -48,7 +53,7 @@ useFocus(projectNameInput, { initialValue: true });
const currentClientName = computed(() => {
if (project.value.client_id) {
return clients.value.find(
return props.clients.find(
(client) => client.id === project.value.client_id
)?.name;
}

View File

@@ -7,17 +7,28 @@ import ProjectCreateModal from '@/Components/Common/Project/ProjectCreateModal.v
import ProjectTableHeading from '@/Components/Common/Project/ProjectTableHeading.vue';
import ProjectTableRow from '@/Components/Common/Project/ProjectTableRow.vue';
import { canCreateProjects } from '@/utils/permissions';
import type { Project } from '@/utils/api';
import type { CreateProjectBody, Project } from '@/utils/api';
import { useProjectsStore } from '@/utils/useProjects';
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
defineProps<{
projects: Project[];
}>();
const createProject = ref(false);
const showCreateProjectModal = ref(false);
async function createProject(project: CreateProjectBody, callback: () => void) {
await useProjectsStore().createProject(project);
callback();
}
const { clients } = storeToRefs(useClientsStore());
</script>
<template>
<ProjectCreateModal v-model:show="createProject"></ProjectCreateModal>
<ProjectCreateModal
:clients="clients"
@submit="createProject"
v-model:show="showCreateProjectModal"></ProjectCreateModal>
<div class="flow-root max-w-[100vw] overflow-x-auto">
<div class="inline-block min-w-full align-middle">
<div
@@ -36,7 +47,7 @@ const createProject = ref(false);
</p>
<SecondaryButton
v-if="canCreateProjects()"
@click="createProject = true"
@click="showCreateProjectModal = true"
:icon="PlusIcon"
>Create your First Project
</SecondaryButton>

View File

@@ -1,16 +1,10 @@
<script setup lang="ts">
import MainContainer from '@/Pages/MainContainer.vue';
import TimeTrackerStartStop from '@/Components/Common/TimeTrackerStartStop.vue';
import type { TimeEntry } from '@/utils/api';
import { storeToRefs } from 'pinia';
import type { Project, Task, TimeEntry } from '@/utils/api';
import TimeEntryDescriptionInput from '@/Components/Common/TimeEntry/TimeEntryDescriptionInput.vue';
import {
type TimeEntriesGroupedByType,
useTimeEntriesStore,
} from '@/utils/useTimeEntries';
import { type TimeEntriesGroupedByType } from '@/utils/useTimeEntries';
import TimeEntryRowTagDropdown from '@/Components/Common/TimeEntry/TimeEntryRowTagDropdown.vue';
import dayjs from 'dayjs';
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
import TimeEntryMoreOptionsDropdown from '@/Components/Common/TimeEntry/TimeEntryMoreOptionsDropdown.vue';
import TimeTrackerProjectTaskDropdown from '@/Components/Common/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import BillableToggleButton from '@/Components/Common/BillableToggleButton.vue';
@@ -22,80 +16,49 @@ import {
import TimeEntryRow from '@/Components/Common/TimeEntry/TimeEntryRow.vue';
import GroupedItemsCountButton from '@/Components/Common/GroupedItemsCountButton.vue';
const currentTimeEntryStore = useCurrentTimeEntryStore();
const { stopTimer } = currentTimeEntryStore;
const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
const props = defineProps<{
timeEntry: TimeEntriesGroupedByType;
projects: Project[];
tasks: Task[];
}>();
const { updateTimeEntry, createTimeEntry, fetchTimeEntries } =
useTimeEntriesStore();
async function onStartStopClick() {
if (props.timeEntry.start && !props.timeEntry.end) {
await updateTimeEntry({
...props.timeEntry,
end: dayjs().utc().format(),
});
} else {
if (currentTimeEntry.value.id) {
await stopTimer();
}
await createTimeEntry({
...props.timeEntry,
start: dayjs().utc().format(),
end: null,
});
}
useCurrentTimeEntryStore().fetchCurrentTimeEntry();
fetchTimeEntries();
}
function deleteTimeEntry() {
const timeEntries = props.timeEntry.timeEntries;
timeEntries.forEach((entry) => {
useTimeEntriesStore().deleteTimeEntry(entry.id);
});
fetchTimeEntries();
}
const emit = defineEmits<{
onStartStopClick: [timeEntry: TimeEntry];
updateTimeEntries: [timeEntries: TimeEntry[]];
deleteTimeEntries: [timeEntries: TimeEntry[]];
}>();
function updateTimeEntryDescription(description: string) {
const timeEntries = props.timeEntry.timeEntries;
timeEntries.forEach((entry) => {
updateTimeEntry({ ...entry, description });
entry.description = description;
});
emit('updateTimeEntries', timeEntries);
}
function updateTimeEntryTags(tags: string[]) {
const timeEntries = props.timeEntry.timeEntries as TimeEntry[];
timeEntries.forEach((entry) => {
updateTimeEntry({ ...entry, tags });
entry.tags = tags;
});
emit('updateTimeEntries', timeEntries);
}
function updateTimeEntryBillable(billable: boolean) {
const timeEntries = props.timeEntry.timeEntries as TimeEntry[];
timeEntries.forEach((entry) => {
updateTimeEntry({ ...entry, billable });
entry.billable = billable;
});
emit('updateTimeEntries', timeEntries);
}
function updateProjectAndTask(projectId: string, taskId: string) {
const timeEntries = props.timeEntry.timeEntries as TimeEntry[];
timeEntries.forEach((entry) => {
updateTimeEntry({
...entry,
project_id: projectId,
task_id: taskId,
});
entry.project_id = projectId;
entry.task_id = taskId;
});
emit('updateTimeEntries', timeEntries);
}
const expanded = ref(false);
@@ -124,6 +87,8 @@ const expanded = ref(false);
"></TimeEntryDescriptionInput>
</div>
<TimeTrackerProjectTaskDropdown
:projects="projects"
:tasks="tasks"
:showBadgeBorder="false"
@changed="updateProjectAndTask"
:project="timeEntry.project_id"
@@ -157,12 +122,12 @@ const expanded = ref(false);
</button>
<TimeTrackerStartStop
@changed="onStartStopClick"
@changed="emit('onStartStopClick', timeEntry)"
:active="!!(timeEntry.start && !timeEntry.end)"
class="opacity-20 hidden sm:flex group-hover:opacity-100"></TimeTrackerStartStop>
<TimeEntryMoreOptionsDropdown
@delete="
deleteTimeEntry
emit('deleteTimeEntries', timeEntry.timeEntries)
"></TimeEntryMoreOptionsDropdown>
</div>
</div>
@@ -171,7 +136,14 @@ const expanded = ref(false);
v-if="expanded"
class="w-full border-t border-default-background-separator bg-black/15">
<TimeEntryRow
:projects="projects"
:tasks="tasks"
indent
@updateTimeEntry="
(timeEntry) => emit('updateTimeEntries', [timeEntry])
"
@onStartStopClick="emit('onStartStopClick', subEntry)"
@deleteTimeEntry="emit('deleteTimeEntries', [subEntry])"
:key="subEntry.id"
v-for="subEntry in timeEntry.timeEntries"
:time-entry="subEntry"></TimeEntryRow>

View File

@@ -13,6 +13,13 @@ import InputLabel from '@/Components/InputLabel.vue';
import TimePicker from '@/Components/Common/TimePicker.vue';
import DatePicker from '@/Components/Common/DatePicker.vue';
import { getDayJsInstance, getLocalizedDayJs } from '@/utils/time';
import { storeToRefs } from 'pinia';
import { useTasksStore } from '@/utils/useTasks';
import { useProjectsStore } from '@/utils/useProjects';
const projectStore = useProjectsStore();
const { projects } = storeToRefs(projectStore);
const taskStore = useTasksStore();
const { tasks } = storeToRefs(taskStore);
const { createTimeEntry } = useTimeEntriesStore();
const show = defineModel('show', { default: false });
@@ -92,6 +99,8 @@ async function submit() {
<TimeTrackerProjectTaskDropdown
class="mt-1"
size="xlarge"
:projects="projects"
:tasks="tasks"
v-model:project="timeEntry.project_id"
v-model:task="
timeEntry.task_id

View File

@@ -9,7 +9,9 @@ defineProps<{
end: string | null;
}>();
const emit = defineEmits(['changed']);
const emit = defineEmits<{
changed: [start: string, end: string | null];
}>();
const open = ref(false);
</script>

View File

@@ -2,80 +2,45 @@
import MainContainer from '@/Pages/MainContainer.vue';
import TimeTrackerStartStop from '@/Components/Common/TimeTrackerStartStop.vue';
import TimeEntryRangeSelector from '@/Components/Common/TimeEntry/TimeEntryRangeSelector.vue';
import type { TimeEntry } from '@/utils/api';
import { storeToRefs } from 'pinia';
import type { Project, Task, TimeEntry } from '@/utils/api';
import TimeEntryDescriptionInput from '@/Components/Common/TimeEntry/TimeEntryDescriptionInput.vue';
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
import TimeEntryRowTagDropdown from '@/Components/Common/TimeEntry/TimeEntryRowTagDropdown.vue';
import TimeEntryRowDurationInput from '@/Components/Common/TimeEntry/TimeEntryRowDurationInput.vue';
import dayjs from 'dayjs';
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
import TimeEntryMoreOptionsDropdown from '@/Components/Common/TimeEntry/TimeEntryMoreOptionsDropdown.vue';
import TimeTrackerProjectTaskDropdown from '@/Components/Common/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import BillableToggleButton from '@/Components/Common/BillableToggleButton.vue';
const currentTimeEntryStore = useCurrentTimeEntryStore();
const { stopTimer, updateTimer } = currentTimeEntryStore;
const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
const props = defineProps<{
timeEntry: TimeEntry;
indent?: boolean;
projects: Project[];
tasks: Task[];
}>();
const { updateTimeEntry, createTimeEntry, fetchTimeEntries } =
useTimeEntriesStore();
async function updateStartEndTime(start: string, end: string | null) {
if (currentTimeEntry.value.id === props.timeEntry.id) {
currentTimeEntry.value.start = start;
currentTimeEntry.value.end = end;
await updateTimer();
} else {
await updateTimeEntry({ ...props.timeEntry, start, end });
}
await fetchTimeEntries();
}
async function onStartStopClick() {
if (props.timeEntry.start && !props.timeEntry.end) {
await updateTimeEntry({
...props.timeEntry,
end: dayjs().utc().format(),
});
} else {
if (currentTimeEntry.value.id) {
await stopTimer();
}
await createTimeEntry({
...props.timeEntry,
start: dayjs().utc().format(),
end: null,
});
}
useCurrentTimeEntryStore().fetchCurrentTimeEntry();
fetchTimeEntries();
}
function deleteTimeEntry() {
useTimeEntriesStore().deleteTimeEntry(props.timeEntry.id);
fetchTimeEntries();
}
const emit = defineEmits<{
onStartStopClick: [];
deleteTimeEntry: [];
updateTimeEntry: [timeEntry: TimeEntry];
}>();
function updateTimeEntryDescription(description: string) {
updateTimeEntry({ ...props.timeEntry, description });
emit('updateTimeEntry', { ...props.timeEntry, description });
}
function updateTimeEntryTags(tags: string[]) {
updateTimeEntry({ ...props.timeEntry, tags });
emit('updateTimeEntry', { ...props.timeEntry, tags });
}
function updateTimeEntryBillable(billable: boolean) {
updateTimeEntry({ ...props.timeEntry, billable });
emit('updateTimeEntry', { ...props.timeEntry, billable });
}
function updateStartEndTime(start: string, end: string | null) {
emit('updateTimeEntry', { ...props.timeEntry, start, end });
}
function updateProjectAndTask(projectId: string, taskId: string) {
updateTimeEntry({
emit('updateTimeEntry', {
...props.timeEntry,
project_id: projectId,
task_id: taskId,
@@ -100,6 +65,8 @@ function updateProjectAndTask(projectId: string, taskId: string) {
timeEntry.description
"></TimeEntryDescriptionInput>
<TimeTrackerProjectTaskDropdown
:projects="projects"
:tasks="tasks"
:showBadgeBorder="false"
@changed="updateProjectAndTask"
:project="timeEntry.project_id"
@@ -132,12 +99,12 @@ function updateProjectAndTask(projectId: string, taskId: string) {
updateStartEndTime
"></TimeEntryRowDurationInput>
<TimeTrackerStartStop
@changed="onStartStopClick"
@changed="emit('onStartStopClick')"
:active="!!(timeEntry.start && !timeEntry.end)"
class="opacity-20 hidden sm:flex group-hover:opacity-100"></TimeTrackerStartStop>
<TimeEntryMoreOptionsDropdown
@delete="
deleteTimeEntry
emit('deleteTimeEntry')
"></TimeEntryMoreOptionsDropdown>
</div>
</div>

View File

@@ -2,20 +2,15 @@
import { ChevronRightIcon } from '@heroicons/vue/16/solid';
import Dropdown from '@/Components/Dropdown.vue';
import { type Component, computed, nextTick, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useProjectsStore } from '@/utils/useProjects';
import { useTasksStore } from '@/utils/useTasks';
import ProjectDropdownItem from '@/Components/Common/Project/ProjectDropdownItem.vue';
import type { Project, Task } from '@/utils/api';
import type { CreateProjectBody, Project, Task } from '@/utils/api';
import ProjectBadge from '@/Components/Common/Project/ProjectBadge.vue';
import Badge from '@/Components/Common/Badge.vue';
import { PlusIcon, PlusCircleIcon } from '@heroicons/vue/16/solid';
import ProjectCreateModal from '@/Components/Common/Project/ProjectCreateModal.vue';
const projectStore = useProjectsStore();
const { projects } = storeToRefs(projectStore);
const taskStore = useTasksStore();
const { tasks } = storeToRefs(taskStore);
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
import { useProjectsStore } from '@/utils/useProjects';
const task = defineModel<string | null>('task', {
default: null,
@@ -45,10 +40,12 @@ type ProjectWithTasks = {
tasks: Task[];
};
withDefaults(
const props = withDefaults(
defineProps<{
showBadgeBorder: boolean;
size: 'base' | 'large' | 'xlarge';
projects: Project[];
tasks: Task[];
}>(),
{
showBadgeBorder: true,
@@ -57,14 +54,14 @@ withDefaults(
);
const filteredProjects = computed(() => {
return projects.value.reduce(
return props.projects.reduce(
(filtered: ProjectWithTasks[], filterProject) => {
const projectNameIncludesSearchTerm = filterProject.name
.toLowerCase()
.includes(searchValue.value?.toLowerCase()?.trim() || '');
// check if one of the project tasks
const projectTasks = tasks.value.filter((task) => {
const projectTasks = props.tasks.filter((task) => {
return task.project_id === filterProject.id;
});
@@ -277,13 +274,13 @@ function moveHighlightDown() {
const highlightedItemId = ref<string | null>(null);
const currentProject = computed(() => {
return projects.value.find(
return props.projects.find(
(iteratingProject) => iteratingProject.id === project.value
);
});
const currentTask = computed(() => {
return tasks.value.find(
return props.tasks.find(
(iteratingTasks) => iteratingTasks.id === task.value
);
});
@@ -299,7 +296,7 @@ const selectedProjectColor = computed(() => {
function selectTask(taskId: string) {
task.value = taskId;
project.value =
tasks.value.find((task) => task.id === taskId)?.project_id || null;
props.tasks.find((task) => task.id === taskId)?.project_id || null;
open.value = false;
emit('changed', project.value, task.value);
}
@@ -312,6 +309,14 @@ function selectProject(projectId: string) {
}
const showCreateProject = ref(false);
const clientsStore = useClientsStore();
const { clients } = storeToRefs(clientsStore);
async function createProject(project: CreateProjectBody, callback: () => void) {
await useProjectsStore().createProject(project);
callback();
}
</script>
<template>
@@ -412,7 +417,10 @@ const showCreateProject = ref(false);
</div>
</template>
</Dropdown>
<ProjectCreateModal v-model:show="showCreateProject"></ProjectCreateModal>
<ProjectCreateModal
:clients="clients"
@submit="createProject"
v-model:show="showCreateProject"></ProjectCreateModal>
</template>
<style scoped></style>

View File

@@ -19,6 +19,7 @@ import { switchOrganization } from '@/utils/useOrganization';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TimeTrackerRangeSelector from '@/Components/Common/TimeTracker/TimeTrackerRangeSelector.vue';
import { useProjectsStore } from '@/utils/useProjects';
import { useTasksStore } from '@/utils/useTasks';
const page = usePage<{
auth: {
@@ -34,6 +35,11 @@ const { currentTimeEntry, isActive, now } = storeToRefs(currentTimeEntryStore);
const { startLiveTimer, stopLiveTimer, setActiveState } = currentTimeEntryStore;
const currentTimeEntryDescriptionInput = ref<HTMLInputElement | null>(null);
const projectStore = useProjectsStore();
const { projects } = storeToRefs(projectStore);
const taskStore = useTasksStore();
const { tasks } = storeToRefs(taskStore);
watch(isActive, () => {
if (isActive.value) {
startLiveTimer();
@@ -133,6 +139,8 @@ function switchToTimeEntryOrganization() {
<div class="flex items-center justify-between pl-2">
<div class="flex items-center w-[130px] sm:w-auto">
<TimeTrackerProjectTaskDropdown
:projects="projects"
:tasks="tasks"
@changed="updateProject"
v-model:project="currentTimeEntry.project_id"
v-model:task="

View File

@@ -12,12 +12,14 @@ import { canCreateProjects } from '@/utils/permissions';
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
import { storeToRefs } from 'pinia';
import { useClientsStore } from '@/utils/useClients';
import type { CreateProjectBody } from '@/utils/api';
onMounted(() => {
useProjectsStore().fetchProjects();
});
const createProject = ref(false);
const { clients } = storeToRefs(useClientsStore());
const showCreateProjectModal = ref(false);
const activeTab = ref<'active' | 'archived'>('active');
@@ -25,6 +27,11 @@ function isActiveTab(tab: string) {
return activeTab.value === tab;
}
async function createProject(project: CreateProjectBody, callback: () => void) {
await useProjectsStore().createProject(project);
callback();
}
const { projects } = storeToRefs(useProjectsStore());
const shownProjects = computed(() => {
@@ -59,11 +66,13 @@ const shownProjects = computed(() => {
<SecondaryButton
v-if="canCreateProjects()"
:icon="PlusIcon"
@click="createProject = true"
@click="showCreateProjectModal = true"
>Create Project
</SecondaryButton>
<ProjectCreateModal
v-model:show="createProject"></ProjectCreateModal>
:clients="clients"
@submit="createProject"
v-model:show="showCreateProjectModal"></ProjectCreateModal>
</MainContainer>
<ProjectTable :projects="shownProjects"></ProjectTable>
</AppLayout>

View File

@@ -19,14 +19,51 @@ import { PlusIcon } from '@heroicons/vue/16/solid';
import TimeEntryCreateModal from '@/Components/Common/TimeEntry/TimeEntryCreateModal.vue';
import TimeEntryAggregateRow from '@/Components/Common/TimeEntry/TimeEntryAggregateRow.vue';
import LoadingSpinner from '@/Components/LoadingSpinner.vue';
import dayjs from 'dayjs';
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
import { useTasksStore } from '@/utils/useTasks';
import { useProjectsStore } from '@/utils/useProjects';
const timeEntriesStore = useTimeEntriesStore();
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
const { updateTimeEntry, fetchTimeEntries, createTimeEntry } =
useTimeEntriesStore();
function updateTimeEntries(timeEntries: TimeEntry[]) {
timeEntries.forEach((entry) => {
useTimeEntriesStore().updateTimeEntry(entry);
});
fetchTimeEntries();
}
const loading = ref(false);
const loadMoreContainer = ref<HTMLDivElement | null>(null);
const isLoadMoreVisible = useElementVisibility(loadMoreContainer);
async function onStartStopClick(timeEntry: TimeEntry) {
if (timeEntry.start && !timeEntry.end) {
await updateTimeEntry({
...timeEntry,
end: dayjs().utc().format(),
});
} else {
await createTimeEntry({
...timeEntry,
start: dayjs().utc().format(),
end: null,
});
}
fetchTimeEntries();
useCurrentTimeEntryStore().fetchCurrentTimeEntry();
}
function deleteTimeEntries(timeEntries: TimeEntry[]) {
timeEntries.forEach((entry) => {
useTimeEntriesStore().deleteTimeEntry(entry.id);
});
fetchTimeEntries();
}
watch(isLoadMoreVisible, async (isVisible) => {
if (
isVisible &&
@@ -45,6 +82,10 @@ onMounted(async () => {
const groupedTimeEntries = computed(() => {
const groupedEntriesByDay: Record<string, TimeEntry[]> = {};
for (const entry of timeEntries.value) {
// skip current time entry
if (entry.end === null) {
continue;
}
const oldEntries =
groupedEntriesByDay[getLocalizedDateFromTimestamp(entry.start)];
groupedEntriesByDay[getLocalizedDateFromTimestamp(entry.start)] = [
@@ -104,6 +145,10 @@ const groupedTimeEntries = computed(() => {
return groupedEntriesByDayAndType;
});
const showManualTimeEntryModal = ref(false);
const projectStore = useProjectsStore();
const { projects } = storeToRefs(projectStore);
const taskStore = useTasksStore();
const { tasks } = storeToRefs(taskStore);
</script>
<template>
@@ -131,11 +176,23 @@ const showManualTimeEntryModal = ref(false);
<TimeEntryRowHeading :date="key"></TimeEntryRowHeading>
<template v-for="entry in value" :key="entry.id">
<TimeEntryAggregateRow
:projects="projects"
:tasks="tasks"
@onStartStopClick="onStartStopClick"
@updateTimeEntries="updateTimeEntries"
@deleteTimeEntries="deleteTimeEntries"
v-if="
'timeEntries' in entry && entry.timeEntries.length > 1
"
:time-entry="entry"></TimeEntryAggregateRow>
<TimeEntryRow v-else :time-entry="entry"></TimeEntryRow>
<TimeEntryRow
:projects="projects"
:tasks="tasks"
@updateTimeEntry="updateTimeEntry"
@onStartStopClick="onStartStopClick(entry)"
@deleteTimeEntry="deleteTimeEntries([entry])"
v-else
:time-entry="entry"></TimeEntryRow>
</template>
</div>
<div