Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Worklog Tracker (alpha) #2

Merged
merged 19 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4773ce3
Update npm dependencies
LSViana Sep 1, 2024
8744313
Introduce the auth, list, and storage composables for the worklog tra…
LSViana Sep 2, 2024
ccf3da7
Introduce the `WlWorklogAuthForm` component
LSViana Sep 2, 2024
4f41bf4
Introduce the `WlWorklogDetailsForm` component
LSViana Sep 2, 2024
f4ac203
Introduce the `WlWorklogList*` components
LSViana Sep 2, 2024
ea67927
Introduce the `WlWorklogTracker` component (entry point)
LSViana Sep 2, 2024
58c09a8
Introduce the `WorklogItem` class
LSViana Sep 2, 2024
384bace
Introduce the auth, duration, now, storage, and Supabase server compo…
LSViana Sep 2, 2024
8b82336
Introduce the server authentication routes
LSViana Sep 2, 2024
e31e5d1
Introduce the server worklog management routes
LSViana Sep 2, 2024
a2f6fef
Introduce the Worklog Tracker application
LSViana Sep 2, 2024
a34c3a6
Fix validation rules in `WlWorklogDetailsForm`
LSViana Sep 2, 2024
409059b
Fix bugs in the `WlWorklogDetailsForm` component after changing to se…
LSViana Sep 2, 2024
69c75a5
Fix `worklogLines` in the `WlWorklogDetailsForm` component
LSViana Sep 2, 2024
74521f8
Update values when selecting item in the `WlWorklogList` component
LSViana Sep 2, 2024
dc6546f
Include worklogId and issueId in in the `@save` event of the `WlWorkl…
LSViana Sep 2, 2024
b4faf09
Load worklogs after login in the `WlWorklogTracker` component
LSViana Sep 2, 2024
665c236
Implement closing the edit mode in the worklog form
LSViana Sep 3, 2024
510a58b
Implement the date picker when viewing the worklogs
LSViana Sep 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions components/applications/worklog-tracker/WlWorklogAuthForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<div class="flex items-center justify-center">
<form class="flex flex-col gap-3 rounded border p-3" @submit.prevent="listeners.submit">
<p class="text-2xl">Sign In</p>
<label for="email">Email</label>
<WlInput id="email" v-model="email" label="Email" placeholder="[email protected]"/>
<label for="password">Password</label>
<WlInput id="password" v-model="password" label="Password" type="password" placeholder="••••••••"/>
<WlButton variant="primary" type="submit">Login</WlButton>
</form>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

import WlButton from '~/components/experiments/forms-input/buttons/WlButton.vue'
import WlInput from '~/components/experiments/forms-input/input/WlInput.vue'

type Emits = {
(e: 'login', email: string, password: string): void
}

const emit = defineEmits<Emits>()

const email = ref('')
const password = ref('')

const listeners = {
async submit(): Promise<void> {
emit('login', email.value, password.value)
}
}
</script>
134 changes: 134 additions & 0 deletions components/applications/worklog-tracker/WlWorklogDetailsForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<template>
<div class="flex flex-col gap-3 rounded border p-3">
<div class="flex gap-3">
<WlInput v-model="ticket" placeholder="DEV-XXX" class="w-40"/>
<div class="flex grow flex-col gap-1">
<textarea
v-model="content"
class="max-h-40 min-h-10 overflow-y-hidden rounded border bg-slate-200 px-3 py-2 outline-0 focus:border-slate-400 dark:bg-slate-800"
placeholder="Enter worklog here..."
:rows="worklogLines"
/>
<div class="flex gap-1 text-xs">
<a href="#" class="rounded bg-slate-700 px-2 py-1" @click="listeners.testdome3849">TESTDOME-3849</a>
<a href="#" class="rounded bg-slate-700 px-2 py-1" @click="listeners.testdome5928">TESTDOME-5928</a>
<a href="#" class="rounded bg-slate-700 px-2 py-1" @click="listeners.clear">Clear</a>
</div>
</div>
<WlTimeInput v-model="startTime" :step="60"/>
<WlTimeInput v-model="endTime" :step="60"/>
<span class="w-16 text-center">{{ worklogDuration }}</span>
</div>
<div class="flex gap-3">
<template v-if="edit">
<WlButton variant="primary" @click="listeners.save">Save</WlButton>
<WlButton variant="secondary" @click="listeners.remove">Remove</WlButton>
<WlButton variant="secondary" @click="listeners.close">Close</WlButton>
</template>
<template v-else>
<WlButton variant="primary" @click="listeners.save">Save</WlButton>
</template>
</div>
</div>
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue'

import WlButton from '~/components/experiments/forms-input/buttons/WlButton.vue'
import WlInput from '~/components/experiments/forms-input/input/WlInput.vue'
import WlTimeInput from '~/components/experiments/forms-input/input/WlTimeInput.vue'
import { WorklogItem } from '~/composables/server/worklog-tracker/types/worklogItem'
import { useWorklogDuration } from '~/composables/server/worklog-tracker/useWorklogDuration'
import { useWorklogNow } from '~/composables/server/worklog-tracker/useWorklogNow'

type Props = {
item: WorklogItem;
edit: boolean;
}

type Emits = {
(e: 'save', item: WorklogItem): void;
(e: 'remove'): void;
(e: 'close'): void;
}

const props = defineProps<Props>()
const emit = defineEmits<Emits>()

const worklogNow = useWorklogNow()

const ticket = ref('')
const content = ref('')
const startTime = ref(worklogNow.get())
const endTime = ref(worklogNow.get())

const worklogLines = computed(() => content.value.split('\n').length)
const worklogDuration = useWorklogDuration(() => [startTime.value, endTime.value])

watch(
() => props.item,
(newItem) => {
ticket.value = newItem.ticket
content.value = newItem.content
startTime.value = newItem.startTime
endTime.value = newItem.endTime
}
)

const listeners = {
save(): void {
if (!methods.validateSave()) {
return
}

emit('save', new WorklogItem(ticket.value, content.value, startTime.value, endTime.value, props.item.id, props.item.issueId))
},
remove(): void {
emit('remove')
},
close(): void {
emit('close')
},
testdome3849(): void {
ticket.value = 'TESTDOME-3849'
content.value = '- Check multiple mentions from Jira, Docs, and Gmail\n- Plan tasks for the day'
},
testdome5928(): void {
ticket.value = 'TESTDOME-5928'
content.value = 'Platform team daily meeting'
},
clear(): void {
ticket.value = ''
content.value = ''
}
}

const methods = {
validateSave(): boolean {
const errors: string[] = []

if (ticket.value.trim().length === 0) {
errors.push('Ticket is required')
}

if (content.value.trim().length === 0) {
errors.push('Content is required')
}

if (startTime.value.getTime() === endTime.value.getTime()) {
errors.push('Start time and end time must be different')
}

if (startTime.value.getTime() > endTime.value.getTime()) {
errors.push('Start time must be before end time')
}

if (errors.length > 0) {
alert(errors.join('\n'))
}

return errors.length === 0
}
}
</script>
35 changes: 35 additions & 0 deletions components/applications/worklog-tracker/WlWorklogList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<template>
<template v-if="props.items.length > 0">
<p class="text-right">{{ totalDuration }}</p>
<ul>
<WlWorklogListItem v-for="item in props.items" :key="item.id" :item="item" @click="listeners.click(item)"/>
</ul>
</template>
<p v-else>No worklogs found.</p>
</template>

<script setup lang="ts">
import { computed } from 'vue'

import WlWorklogListItem from '~/components/applications/worklog-tracker/WlWorklogListItem.vue'
import type { WorklogItem } from '~/composables/server/worklog-tracker/types/worklogItem'

type Props = {
items: WorklogItem[]
}

type Emits = {
(e: 'select', item: WorklogItem): void
}

const props = defineProps<Props>()
const emits = defineEmits<Emits>()

const totalDuration = computed(() => '0 min')

const listeners = {
click(item: WorklogItem): void {
emits('select', item)
}
}
</script>
47 changes: 47 additions & 0 deletions components/applications/worklog-tracker/WlWorklogListItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<template>
<li
class="flex cursor-pointer border-x border-t first:rounded-t last:rounded-b last:border-b"
@click="listeners.click"
>
<span class="border-r p-3">{{ props.item.ticket }}</span>
<span class="grow whitespace-pre-wrap border-r p-3">{{ props.item.content }}</span>
<span class="border-r p-3">{{ formattedDates.start }} - {{ formattedDates.end }}</span>
<span class="p-3">{{ duration }}</span>
</li>
</template>

<script setup lang="ts">
import { computed } from 'vue'

import type { WorklogItem } from '~/composables/server/worklog-tracker/types/worklogItem'
import { useWorklogDuration } from '~/composables/server/worklog-tracker/useWorklogDuration'

type Props = {
item: WorklogItem
}

type Emits = {
(e: 'click'): void
}

const props = defineProps<Props>()
const emit = defineEmits<Emits>()

const formattedDates = computed(() => ({
start: props.item.startTime.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
}),
end: props.item.endTime.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})
}))
const duration = useWorklogDuration(() => [props.item.startTime, props.item.endTime])

const listeners = {
click(): void {
emit('click')
}
}
</script>
106 changes: 106 additions & 0 deletions components/applications/worklog-tracker/WlWorklogTracker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<template>
<template v-if="worklogAuth.authenticated.value">
<div class="flex justify-between">
<h1 class="text-2xl">Worklog Tracker</h1>
<div class="flex items-center gap-3">
<WlDateInput v-model="date" @change="listeners.updateDate"/>
<a href="#" class="underline" @click="listeners.logout">Logout</a>
</div>
</div>
<WlWorklogDetailsForm
:item="item"
:edit="isEditing"
@save="listeners.save"
@remove="listeners.remove"
@close="listeners.close"
/>
<WlWorklogList :items="worklogList.value" @select="listeners.select"/>
</template>
<WlWorklogAuthForm v-else @login="listeners.login"/>
</template>

<script setup lang="ts">
import { useHead } from '@vueuse/head'
import { onMounted, ref } from 'vue'

import { useWorklogAuth } from '~/components/applications/worklog-tracker/useWorklogAuth'
import { useWorklogList } from '~/components/applications/worklog-tracker/useWorklogList'
import { useWorklogStorage } from '~/components/applications/worklog-tracker/useWorklogStorage'
import { useWorklogToday } from '~/components/applications/worklog-tracker/useWorklogToday'
import WlWorklogAuthForm from '~/components/applications/worklog-tracker/WlWorklogAuthForm.vue'
import WlWorklogDetailsForm from '~/components/applications/worklog-tracker/WlWorklogDetailsForm.vue'
import WlWorklogList from '~/components/applications/worklog-tracker/WlWorklogList.vue'
import WlDateInput from '~/components/experiments/forms-input/input/WlDateInput.vue'
import { WorklogItem } from '~/composables/server/worklog-tracker/types/worklogItem'

useHead({
title: 'Worklog Tracker'
})

const worklogAuth = useWorklogAuth()
const worklogList = useWorklogList()
const worklogStorage = useWorklogStorage()
const worklogToday = useWorklogToday()

onMounted(() => methods.loadWorklogs())

const item = ref(new WorklogItem())
const isEditing = ref(false)
const date = ref(worklogToday.get())

const listeners = {
updateDate(): void {
methods.loadWorklogs()
},
async save(newItem: WorklogItem): Promise<void> {
newItem.startTime.setFullYear(date.value.getFullYear(), date.value.getMonth(), date.value.getDate())
newItem.endTime.setFullYear(date.value.getFullYear(), date.value.getMonth(), date.value.getDate())

if (isEditing.value) {
await worklogStorage.update(newItem)
worklogList.update(newItem)
} else {
const savedItem = await worklogStorage.save(newItem)
worklogList.add(savedItem)
}

listeners.close()
},
async remove(): Promise<void> {
await worklogStorage.remove(item.value)
worklogList.remove(item.value)

listeners.close()
},
close(): void {
item.value = new WorklogItem()
isEditing.value = false

if (worklogList.value.length > 0) {
item.value.startTime = worklogList.value[worklogList.value.length - 1].endTime
}
},
select(selectedItem: WorklogItem): void {
item.value = selectedItem
isEditing.value = true
},
async login(email: string, password: string): Promise<void> {
await worklogAuth.login(email, password)
await methods.loadWorklogs()
},
logout(): void {
worklogAuth.logout()
}
}

const methods = {
async loadWorklogs(): Promise<void> {
const items = await worklogStorage.load(date.value)
worklogList.load(items)

if (worklogList.value.length > 0) {
item.value.startTime = worklogList.value[worklogList.value.length - 1].endTime
}
}
}
</script>
Loading
Loading