timetracker update

This commit is contained in:
Daniel Goc
2026-03-11 09:33:36 +01:00
parent bbf8a2c133
commit 9ef4bb219b
121 changed files with 4328 additions and 2231 deletions

View File

@@ -1,15 +0,0 @@
<script setup lang="ts"></script>
<template>
<main class="flex gap-4">
<RouterLink class="bg-(--color-blue-600) dark:bg-(--color-blue-500) px-2 py-1 rounded text-white flex items-center shadow-md"
:to="{ name: 'login' }">Login
</RouterLink>
<RouterLink class="bg-(--color-blue-600) dark:bg-(--color-blue-500) px-2 py-1 rounded text-white flex items-center shadow-md"
:to="{ name: 'register' }">
Register</RouterLink>
<RouterLink class="bg-(--color-blue-600) dark:bg-(--color-blue-500) px-2 py-1 rounded text-white flex items-center shadow-md"
:to="{ name: 'chart' }">Chart
</RouterLink>
</main>
</template>

View File

@@ -5,9 +5,8 @@ import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui'
import { useI18n } from 'vue-i18n'
import { i18n } from '@/plugins/i18n'
import { i18n } from '@/plugins/02_i18n'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
@@ -20,7 +19,7 @@ async function handleLogin() {
const success = await authStore.login(email.value, password.value)
if (success) {
const redirectTo = route.query.redirect as string
router.push(redirectTo || { name: 'chart' })
router.push(redirectTo || { name: 'home' })
}
}
@@ -75,7 +74,14 @@ const PrivacyComponent = computed(() =>
<UButton @click="showPrivacy = false" class="mx-auto px-12">close</UButton>
</template>
</UDrawer>
<div class="h-[100vh] flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="text-center mb-15">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
<div class="w-full max-w-md flex flex-col gap-4">
<UForm :validate="validate" @submit="handleLogin" class="space-y-5">
<UAlert v-if="authStore.error" color="error" variant="subtle" :title="authStore.error"
@@ -85,28 +91,28 @@ const PrivacyComponent = computed(() =>
<UFormField :label="$t('general.email_address')" name="email" required
class="w-full dark:text-white text-black">
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
class="w-full dark:text-white text-black" />
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
</UFormField>
<UFormField :label="$t('general.password')" name="password" required class="w-full dark:text-white text-black">
<UInput v-model="password" :placeholder="$t('general.enter_your_password')"
:type="showPassword ? 'text' : 'password'" class="w-full" :ui="{ trailing: 'pe-1' }">
:type="showPassword ? 'text' : 'password'" class="w-full placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
<template #trailing>
<UIcon color="neutral" variant="link" size="sm"
:name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
<UIcon color="neutral" variant="link" size="sm" :name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:aria-label="showPassword ? 'Hide password' : 'Show password'" :aria-pressed="showPassword"
aria-controls="password" @click="showPassword = !showPassword" />
aria-controls="password" @click="showPassword = !showPassword" class="mr-2"/>
</template>
</UInput>
</UFormField>
<div class="flex items-center justify-between w-full dark:text-white text-black">
<button variant="link" size="sm" @click="goToPasswordRecovery" class="text-[15px] w-full flex justify-end text-(--color-blue-600) dark:text-(--color-blue-500)">
{{$t('general.forgot_password')}}?
<button variant="link" size="sm" @click="goToPasswordRecovery"
class="text-[15px] w-full flex justify-end text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">
{{ $t('general.forgot_password') }}?
</button>
</div>
<UButton type="submit" :loading="authStore.loading"
class="w-full flex justify-center text-white bg-(--color-blue-600) dark:bg-(--color-blue-500)">
class="w-full flex justify-center text-white bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
{{ $t('general.sign_in') }}
</UButton>
</UForm>
@@ -120,7 +126,8 @@ const PrivacyComponent = computed(() =>
<!-- Google Sign In -->
<UButton type="button" color="neutral" variant="outline" size="lg" block :disabled="authStore.loading"
@click="authStore.loginWithGoogle()" class="flex items-center justify-center gap-2 dark:text-white text-black">
@click="authStore.loginWithGoogle()"
class="flex items-center justify-center gap-2 dark:text-white text-black cursor-pointer">
<svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
@@ -140,18 +147,20 @@ const PrivacyComponent = computed(() =>
<div class="text-center">
<p class="dark:text-white text-black">
{{$t('general.dont_have_an_account')}}?
<button variant="link" size="sm" class="text-[15px] text-(--color-blue-600) dark:text-(--color-blue-500)" @click="goToRegister">{{ $t('general.create_account_now') }}</button>
{{ $t('general.dont_have_an_account') }}?
<button variant="link" size="sm"
class="text-[15px] text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer"
@click="goToRegister">{{ $t('general.create_account_now') }}</button>
</p>
</div>
<p class="mt-8 text-center text-xs dark:text-white text-black">
{{ $t('general.by_signing_in_you_agree_to_our') }}
<span @click="showTherms = !showTherms"
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">{{
$t('general.terms_of_service') }}</span>
{{ $t('general.and') }}
<span @click="showPrivacy = !showPrivacy"
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">{{
$t('general.privacy_policy') }}</span>
</p>
</div>

View File

@@ -4,10 +4,8 @@ import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui'
import { useI18n } from 'vue-i18n'
import { i18n } from '@/plugins/i18n'
import { i18n } from '@/plugins/02_i18n'
const { t } = useI18n()
const router = useRouter()
const authStore = useAuthStore()
const validation = useValidation()
@@ -30,20 +28,24 @@ function goToRegister() {
router.push({ name: 'register' })
}
function validate(): FormError[] {
validation.reset()
validation.validateEmail(email, 'email', i18n.t('validate_error.email_required'))
return validation.errors
}
</script>
<template>
<div class="h-[100vh] flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="text-center mb-15">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
<div class="w-full max-w-md flex flex-col gap-4">
<!-- Success State -->
<template v-if="submitted">
<div class="text-center flex flex-col gap-4">
<UIcon name="i-heroicons-envelope" class="w-12 h-12 mx-auto text-primary-500" />
@@ -51,13 +53,13 @@ function validate(): FormError[] {
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.password_reset_link_sent_notice') }}
</p>
<UButton color="neutral" variant="outline" block @click="goToLogin" class="dark:text-white text-black">
<UButton color="neutral" variant="outline" block @click="goToLogin"
class="dark:text-white text-black cursor-pointer">
{{ $t('general.back_to_sign_in') }}
</UButton>
</div>
</template>
<!-- Form State -->
<template v-else>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
@@ -73,29 +75,31 @@ function validate(): FormError[] {
<UFormField :label="$t('general.email_address')" name="email" required
class="w-full dark:text-white text-black">
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
class="w-full dark:text-white text-black" />
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
</UFormField>
<UButton type="submit" block :loading="authStore.loading"
class="text-white bg-(--color-blue-600) dark:bg-(--color-blue-500)">
class="text-white bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
{{ $t('general.send_password_reset_link') }}
</UButton>
</UForm>
<div class="text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4">
<UButton color="neutral" variant="outline" :loading="authStore.loading"
class="w-full flex justify-center dark:text-white text-black" @click="goToLogin">
<button color="neutral" variant="outline" :loading="authStore.loading"
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
@click="goToLogin">
<UIcon name="mingcute:arrow-left-line" class="text-(--color-blue-600) dark:text-(--color-blue-500) text-[16px]" />
{{ $t('general.back_to_sign_in') }}
</UButton>
</button>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.dont_have_an_account') }}
<UButton variant="link" size="sm" @click="goToRegister"
class="text-(--color-blue-600) dark:text-(--color-blue-500)">{{ $t('general.create_account_now') }}
</UButton>
<button variant="link" size="sm" @click="goToRegister"
class=" text-[15px] text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">{{
$t('general.create_account_now') }}
</button>
</p>
</div>
</template>
</div>
</div>
</template>

View File

@@ -17,50 +17,62 @@
</template>
</UDrawer>
<div class="h-[100vh] flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="text-center mb-15">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
<div class="w-full max-w-md">
<UForm :validate="validate" @submit="handleRegister" class="flex flex-col gap-3">
<UAlert v-if="authStore.error" color="error" variant="subtle" icon="i-heroicons-exclamation-triangle"
:title="authStore.error" :close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
@close="authStore.clearError" />
<UFormField :label="$t('general.first_name')" name="first_name" required class="w-full dark:text-white text-black ">
<UInput class="w-full" v-model="first_name" type="text" :placeholder="$t('general.first_name')"
<UFormField :label="$t('general.first_name')" name="first_name" required
class="w-full dark:text-white text-black ">
<UInput class="w-full placeholder:text-(--placeholder)" v-model="first_name" type="text" :placeholder="$t('general.first_name')"
:disabled="authStore.loading">
</UInput>
</UFormField>
<UFormField :label="$t('general.last_name')" name="last_name" required class="w-full dark:text-white text-black">
<UInput class="w-full dark:text-white text-black" v-model="last_name" type="text" :placeholder="$t('general.last_name')"
:disabled="authStore.loading">
<UFormField :label="$t('general.last_name')" name="last_name" required
class="w-full dark:text-white text-black">
<UInput class="w-full dark:text-white text-black placeholder:text-(--placeholder)" v-model="last_name" type="text"
:placeholder="$t('general.last_name')" :disabled="authStore.loading">
</UInput>
</UFormField>
<UFormField :label="$t('general.email_address')" name="email" required class="w-full dark:text-white text-black">
<UFormField :label="$t('general.email_address')" name="email" required
class="w-full dark:text-white text-black">
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
class="w-full dark:text-white text-black" />
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
</UFormField>
<UFormField :label="$t('general.password')" name="password" required class="w-full dark:text-white text-black">
<UInput v-model="password" :placeholder="$t('general.enter_your_password')" :disabled="authStore.loading"
class="w-full dark:text-white text-black" :type="showPassword ? 'text' : 'password'" :ui="{ trailing: 'pe-1' }">
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :type="showPassword ? 'text' : 'password'"
:ui="{ trailing: 'pe-1' }">
<template #trailing>
<UIcon color="neutral" variant="link" size="sm"
:name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
<UIcon color="neutral" variant="link" size="sm" :name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:aria-label="showPassword ? 'Hide password' : 'Show password'" :aria-pressed="showPassword"
aria-controls="password" @click="showPassword = !showPassword" />
aria-controls="password" @click="showPassword = !showPassword" class="mr-2"/>
</template>
</UInput>
</UFormField>
<UFormField :label="$t('general.confirm_password')" name="confirm_password" required class="w-full dark:text-white text-black">
<UInput v-model="confirm_password_ref" :type="showConfirmPassword ? 'text' : 'password'" class="w-full dark:text-white text-black"
:placeholder="$t('general.confirm_your_password')" :disabled="authStore.loading" :ui="{ trailing: 'pe-1' }">
<UFormField :label="$t('general.confirm_password')" name="confirm_password" required
class="w-full dark:text-white text-black">
<UInput v-model="confirm_password_ref" :type="showConfirmPassword ? 'text' : 'password'"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :placeholder="$t('general.confirm_your_password')"
:disabled="authStore.loading" :ui="{ trailing: 'pe-1' }">
<template #trailing>
<UIcon color="neutral" variant="ghost" size="sm"
:name="showConfirmPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
@click="showConfirmPassword = !showConfirmPassword" />
@click="showConfirmPassword = !showConfirmPassword" class="mr-2"/>
</template>
</UInput>
</UFormField>
@@ -69,16 +81,21 @@
<template #label>
<span class="dark:text-white text-black">
{{ $t('general.i_agree_to_the') }}
<span @click="showTherms = !showTherms" class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{ $t('general.terms_of_service')
}}</span>
<span @click="showTherms = !showTherms"
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{
$t('general.terms_of_service')
}}</span>
{{ $t('general.and') }}
<span @click="showPrivacy = !showPrivacy" class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{ $t('general.privacy_policy')
}}</span>
<span @click="showPrivacy = !showPrivacy"
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{
$t('general.privacy_policy')
}}</span>
</span>
</template>
</UCheckbox>
<UButton type="submit" block :loading="authStore.loading" :disabled="!acceptTerms" class="text-white bg-(--color-blue-600) dark:bg-(--color-blue-500)">
<UButton type="submit" block :loading="authStore.loading" :disabled="!acceptTerms"
class="text-white bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
{{ $t('general.create_account') }}
</UButton>
@@ -86,7 +103,8 @@
<p class="dark:text-white text-black">
{{ $t('general.already_have_an_account') }}
</p>
<UButton color="neutral" variant="outline" :loading="authStore.loading" class="w-full flex justify-center dark:text-white hover:text-white hover:bg-(--color-blue-600) dark:hover:bg-(--color-blue-500) border border-(--border-light)! dark:border-(--border-dark)!"
<UButton color="neutral" variant="outline" :loading="authStore.loading"
class="w-full flex justify-center dark:text-white hover:text-white cursor-pointer hover:bg-(--color-blue-600) dark:hover:bg-(--color-blue-500) border border-(--border-light)! dark:border-(--border-dark)!"
@click="goToLogin">{{ $t('general.sign_in') }}</UButton>
</div>
</UForm>
@@ -101,7 +119,7 @@ import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui'
import { useI18n } from 'vue-i18n'
import { i18n } from '@/plugins/i18n'
import { i18n } from '@/plugins/02_i18n'
const { locale } = useI18n()
const router = useRouter()
@@ -149,7 +167,5 @@ const PrivacyComponent = computed(() =>
defineAsyncComponent(() =>
import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')),
),
)
</script>

View File

@@ -12,7 +12,8 @@ import {
} from 'chart.js'
import { getRepos, getYears, getQuarters, getIssues, type QuarterData, type IssueTimeSummary } from '@/composable/useRepoApi'
import { useAuthStore } from '@/stores/auth'
import { i18n } from '@/plugins/i18n'
import { i18n } from '@/plugins/02_i18n'
import type { TableColumn } from '@nuxt/ui'
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
@@ -86,7 +87,7 @@ async function loadIssues(repoID: number, year: number, quarterStr: string) {
loading.value = true
error.value = null
try {
const response = await getIssues(repoID, year, parseInt(quarterPart), page.value, 50)
const response = await getIssues(repoID, year, parseInt(quarterPart), page.value, 10)
issues.value = response.items || []
totalItems.value = response.items_count || 0
} catch (e: any) {
@@ -136,23 +137,64 @@ const quarterItems = computed(() => [
label: `${q.quarter} (${q.time.toFixed(1)}h)`
}))
])
const columns: TableColumn<IssueTimeSummary>[] = [
{
accessorKey: 'IssueID',
header: 'ID',
},
{
accessorKey: 'IssueName',
header: i18n.t('repo_chart.issue_name'),
},
// {
// accessorKey: 'Initials',
// header: i18n.t('repo_chart.user_initials'),
// },
{
accessorKey: 'CreatedDate',
header: i18n.t('repo_chart.created_on'),
cell: ({ row }) => {
const date = new Date(row.getValue('CreatedDate'))
return date.toLocaleDateString(i18n.locale.value)
}
},
{
accessorKey: 'CreatedDate',
header: i18n.t('repo_chart.created_on'),
cell: ({ row }) => {
const date = new Date(row.getValue('CreatedDate'))
return date.toLocaleTimeString(i18n.locale.value)
}
},
{
accessorKey: 'TotalHoursSpent',
header: i18n.t('repo_chart.hours_spent'),
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const hours = row.getValue('TotalHoursSpent')
return `${hours}h`
}
},
]
</script>
<template>
<div class="container">
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
<h1 class="text-2xl font-bold mb-6 text-black">{{ $t('repo_chart.repository_work_chart') }}</h1>
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }}
</h1>
<div v-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">{{ error }}</div>
<div v-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ error }}
</div>
<div v-if="loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
{{ $t('repo_chart.loading') }}...
<div v-if="loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">{{ $t('repo_chart.loading') }}...
</div>
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
<!-- Please log in to view repository work charts. -->
{{ $t('repo_chart.login_to_view_charts') }}
</div>
@@ -161,24 +203,22 @@ const quarterItems = computed(() => [
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.repository')
}}</label>
<USelect v-model="selectedRepo" :items="items" :disabled="loading"
:placeholder="$t('repo_chart.select_a_repository')" class="dark:text-white text-black "/>
:placeholder="$t('repo_chart.select_a_repository')" class="dark:text-white text-black " />
<!-- Select a repository -->
</div>
<div class="flex flex-col min-w-[160px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.year')
}}</label>
<USelect v-model="selectedYear" :items="yearItems"
:disabled="loading || !selectedRepo || years.length === 0"
:placeholder="$t('repo_chart.select_a_year')" class="dark:text-white text-black "/>
:placeholder="$t('repo_chart.select_a_year')" class="dark:text-white text-black placeholder:text-(--placeholder)" />
</div>
<div class="flex flex-col min-w-[192px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.quarter')
}}</label>
<USelect v-model="selectedQuarter" :items="quarterItems"
:disabled="loading || !selectedYear || quarters.length === 0"
:placeholder="$t('repo_chart.all_quarters')" class="dark:text-white text-black "/>
:placeholder="$t('repo_chart.all_quarters')" class="dark:text-white text-black placeholder:text-(--placeholder)" />
</div>
</div>
@@ -196,50 +236,19 @@ const quarterItems = computed(() => [
<h2 class="text-xl font-medium mb-4 text-black dark:text-white">{{ $t('repo_chart.issues_for') }} {{
selectedQuarter }}
</h2>
<table class="w-full border border-(--border-light) dark:border-(--border-dark)">
<thead>
<tr class="bg-gray-100 dark:bg-(--black)">
<th
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
ID</th>
<th
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
{{ $t('repo_chart.issue_name') }}</th>
<th
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
{{ $t('repo_chart.user_initials') }}</th>
<th
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
{{ $t('repo_chart.created_on') }}</th>
<th
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
{{ $t('repo_chart.hours_spent') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="issue in issues" :key="issue.IssueID"
class=" border-b border-(--border-light) dark:border-(--border-dark)">
<td class="p-3 text-black dark:text-white">{{ issue.IssueID }}</td>
<td class="p-3 text-black dark:text-white">{{ issue.IssueName }}</td>
<td class="p-3 text-black dark:text-white">{{ issue.Initials }}</td>
<td class="p-3 text-black dark:text-white">{{ issue.CreatedDate }}</td>
<td class="p-3 text-black dark:text-white">{{ issue.TotalHoursSpent }}h</td>
</tr>
</tbody>
</table>
<div class="pt-4 flex justify-center items-center dark:text-white!">
<UTable :data="issues" :columns="columns" class="flex-1 dark:text-white! text-dark" />
<div class="pt-4 flex justify-center items-center dark:text-white! text-dark">
<UPagination v-model:page="page" :total="totalItems" />
</div>
</div>
<div v-else-if="selectedQuarter && !loading && authStore.isAuthenticated && !hasIssues"
class="mt-4 p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) dark:text-white text-black rounded">
class="mt-4 p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) dark:text-white! text-black rounded">
{{ $t('validate_error.no_issues_for_quarter') }}.
</div>
<div v-else-if="!loading && authStore.isAuthenticated" class="p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) rounded">
<div v-else-if="!loading && authStore.isAuthenticated"
class="p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) rounded dark:text-white! text-black">
<span v-if="!selectedRepo">{{ $t('repo_chart.select_repo_to_view_data') }}</span>
<span v-else-if="!selectedYear">{{ $t('repo_chart.select_year_to_view_data') }}</span>
<span v-else-if="!selectedQuarter">{{ $t('repo_chart.select_quarter_to_view_issues') }}</span>

View File

@@ -4,15 +4,12 @@ import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui'
import { useI18n } from 'vue-i18n'
import { i18n } from '@/plugins/i18n'
import { i18n } from '@/plugins/02_i18n'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
const validation = useValidation()
const new_password = ref('')
const confirm_new_password = ref('')
const showNewPassword = ref(false)
@@ -52,13 +49,19 @@ function validate(): FormError[] {
</script>
<template>
<div class="h-[100vh] flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="text-center mb-15">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
<div class="w-full max-w-md flex flex-col gap-4">
<!-- Success State -->
<template v-if="submitted">
<div class="text-center flex flex-col gap-4">
<UIcon name="i-heroicons-check-circle" class="w-12 h-12 mx-auto text-green-500" />
<UIcon name="i-heroicons-check-circle" class="w-12 h-12 mx-auto text-green-800" />
<h2 class="text-xl font-semibold dark:text-white text-black">
{{ $t('general.password_updated') }}
</h2>
@@ -71,7 +74,6 @@ function validate(): FormError[] {
</div>
</template>
<!-- Form State -->
<template v-else>
<UForm :validate="validate" @submit="handleReset" class="flex flex-col gap-3">
<UAlert v-if="authStore.error" color="error" variant="subtle"
@@ -79,16 +81,17 @@ function validate(): FormError[] {
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
@close="authStore.clearError" />
<UFormField :label="$t('general.new_password')" name="new_password" required class="w-full dark:text-white text-black">
<UFormField :label="$t('general.new_password')" name="new_password" required
class="w-full dark:text-white text-black">
<UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'"
:placeholder="$t('general.enter_your_new_password')" :disabled="authStore.loading"
class="w-full dark:text-white text-black" :ui="{ trailing: 'pe-1' }">
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
<template #trailing>
<UIcon color="neutral" variant="link" size="sm"
:name="showNewPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:aria-label="showNewPassword ? 'Hide password' : 'Show password'"
:aria-pressed="showNewPassword" aria-controls="new_password"
@click="showNewPassword = !showNewPassword" />
@click="showNewPassword = !showNewPassword" class="mr-2"/>
</template>
</UInput>
</UFormField>
@@ -97,27 +100,29 @@ function validate(): FormError[] {
class="w-full dark:text-white text-black">
<UInput v-model="confirm_new_password" :type="showConfirmPassword ? 'text' : 'password'"
:placeholder="$t('general.confirm_your_new_password')" :disabled="authStore.loading"
class="w-full dark:text-white text-black" :ui="{ trailing: 'pe-1' }">
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
<template #trailing>
<UIcon color="neutral" variant="ghost" size="sm"
:name="showConfirmPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
@click="showConfirmPassword = !showConfirmPassword" />
@click="showConfirmPassword = !showConfirmPassword" class="mr-2"/>
</template>
</UInput>
</UFormField>
<UButton type="submit" block :loading="authStore.loading" class="text-white! bg-(--color-blue-600) dark:bg-(--color-blue-500)">
<UButton type="submit" block :loading="authStore.loading"
class="text-white! bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
{{ $t('general.reset_password') }}
</UButton>
<div class="text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4">
<UButton color="neutral" variant="ghost" @click="goToLogin" class="dark:text-white text-black">
<button color="neutral" variant="ghost" @click="goToLogin"
class="text-[15px] flex items-center gap-2 text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">
<UIcon name="mingcute:arrow-left-line" />
{{ $t('general.back_to_sign_in') }}
</UButton>
</button>
</div>
</UForm>
</template>
</div>
</div>
</template>

View File

@@ -3,12 +3,12 @@ import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useFetchJson } from '@/composable/useFetchJson'
import { i18n } from '@/plugins/02_i18n'
const { t, te } = useI18n()
const router = useRouter()
const route = useRoute()
// Helper function to get translation with fallback
function tt(key: string, fallback: string): string {
return te(key) ? t(key) : fallback
}
@@ -20,22 +20,19 @@ const success = ref(false)
const verificationInProgress = ref(true)
onMounted(() => {
// Get token from URL query params
token.value = (route.query.token as string) || ''
if (!token.value) {
error.value = tt('verify_email.invalid_token', 'Invalid or missing verification token')
error.value = i18n.t('verify_email.invalid_token')
verificationInProgress.value = false
return
}
// Automatically verify email on page load
handleVerifyEmail()
})
async function handleVerifyEmail() {
if (!token.value) {
error.value = tt('verify_email.invalid_token', 'Invalid or missing verification token')
error.value = i18n.t('verify_email.invalid_token')
return
}
@@ -43,7 +40,7 @@ async function handleVerifyEmail() {
error.value = null
try {
await useFetchJson('/api/v1/auth/complete-registration', {
await useFetchJson('/api/v1/public/auth/complete-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token.value }),
@@ -52,12 +49,11 @@ async function handleVerifyEmail() {
success.value = true
verificationInProgress.value = false
// Redirect to login after 3 seconds
setTimeout(() => {
router.push({ name: 'login' })
}, 3000)
} catch (e: any) {
error.value = e?.message ?? tt('verify_email.verification_failed', 'Email verification failed')
error.value = e?.message ?? i18n.t('verify_email.verification_failed')
verificationInProgress.value = false
} finally {
loading.value = false
@@ -74,81 +70,72 @@ function goToLogin() {
class="min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
<!-- Logo/Brand Section -->
<div class="text-center mb-8">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-envelope-check" class="w-8 h-8" />
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
<!-- Verify Email Card -->
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
<template #header>
<div class="text-center">
<!-- Loading State -->
<div v-if="verificationInProgress && loading">
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-primary-500 mx-auto mb-4" />
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ tt('verify_email.verifying', 'Verifying your email...') }}
{{ $t('verify_email.verifying') }}
</h2>
</div>
<!-- Success State -->
<div v-else-if="success">
<div
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 text-green-600 mb-4">
<UIcon name="i-heroicons-check-circle" class="w-6 h-6" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ tt('verify_email.success_title', 'Email Verified!') }}
{{ $t('verify_email.success_title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ tt('verify_email.success_message', 'Your email has been verified successfully.') }}
{{ $t('verify_email.success_message') }}
</p>
</div>
<!-- Error State -->
<div v-else-if="error">
<div
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 text-red-600 mb-4">
<UIcon name="i-heroicons-exclamation-circle" class="w-6 h-6" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ tt('verify_email.error_title', 'Verification Failed') }}
{{ $t('verify_email.error_title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ tt('verify_email.error_message', 'We could not verify your email.') }}
{{ $t('verify_email.error_message') }}
</p>
</div>
</div>
</template>
<!-- Success State Content -->
<div v-if="success" class="text-center py-4">
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ tt('verify_email.redirect_message', 'You will be redirected to login page...') }}</p>
<UButton color="primary" @click="goToLogin">{{ tt('verify_email.go_to_login', 'Go to Login') }}</UButton>
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ $t('verify_email.redirect_message') }}</p>
<UButton color="primary" @click="goToLogin">{{ $t('verify_email.go_to_login') }}</UButton>
</div>
<!-- Error State Content -->
<div v-else-if="error" class="text-center py-4">
<UAlert :color="'error'" variant="subtle" icon="i-heroicons-exclamation-triangle" :title="error"
class="mb-4" />
<UButton color="primary" @click="goToLogin">{{ tt('verify_email.go_to_login', 'Go to Login') }}</UButton>
<UButton color="primary" @click="goToLogin" class="cursor-pointer">{{ $t('verify_email.go_to_login') }}
</UButton>
</div>
<!-- Loading State Content -->
<div v-else-if="verificationInProgress && loading" class="text-center py-4">
<p class="text-gray-500 dark:text-gray-400">{{ tt('verify_email.please_wait', 'Please wait while we verify your email address.') }}</p>
<p class="text-gray-500 dark:text-gray-400">{{ $t('verify_email.please_wait') }}</p>
</div>
<template #footer>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ tt('verify_email.already_registered', 'Already have an account?') }}
<UButton variant="link" size="sm" @click="goToLogin"> {{ tt('verify_email.sign_in', 'Sign in') }}
</UButton>
{{ $t('verify_email.already_registered') }}
<button variant="link" size="sm" @click="goToLogin"
class="cursor-pointer text-(--color-blue-600) dark:text-(--color-blue-500)"> {{ $t('general.sign_in')
}}
</button>
</p>
</div>
</template>