Files
chronus/src/components/Timer.vue
2025-12-25 22:10:06 -07:00

203 lines
6.0 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps<{
initialRunningEntry: { startTime: number; description: string | null; clientId: string; categoryId: string } | null;
clients: { id: string; name: string }[];
categories: { id: string; name: string; color: string | null }[];
tags: { id: string; name: string; color: string | null }[];
}>();
const isRunning = ref(false);
const startTime = ref<number | null>(null);
const elapsedTime = ref(0);
const description = ref('');
const selectedClientId = ref('');
const selectedCategoryId = ref('');
const selectedTags = ref<string[]>([]);
let interval: ReturnType<typeof setInterval> | null = null;
function formatTime(ms: number) {
// Round to nearest minute (10 seconds = 1 minute)
const totalMinutes = Math.round(ms / 1000 / 60);
const minutes = totalMinutes % 60;
const hours = Math.floor(totalMinutes / 60);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
function toggleTag(tagId: string) {
const index = selectedTags.value.indexOf(tagId);
if (index > -1) {
selectedTags.value.splice(index, 1);
} else {
selectedTags.value.push(tagId);
}
}
onMounted(() => {
if (props.initialRunningEntry) {
isRunning.value = true;
startTime.value = props.initialRunningEntry.startTime;
description.value = props.initialRunningEntry.description || '';
selectedClientId.value = props.initialRunningEntry.clientId;
selectedCategoryId.value = props.initialRunningEntry.categoryId;
elapsedTime.value = Date.now() - startTime.value;
interval = setInterval(() => {
elapsedTime.value = Date.now() - startTime.value!;
}, 1000);
}
});
onUnmounted(() => {
if (interval) clearInterval(interval);
});
async function startTimer() {
if (!selectedClientId.value) {
alert('Please select a client');
return;
}
if (!selectedCategoryId.value) {
alert('Please select a category');
return;
}
const res = await fetch('/api/time-entries/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
description: description.value,
clientId: selectedClientId.value,
categoryId: selectedCategoryId.value,
tags: selectedTags.value,
}),
});
if (res.ok) {
const data = await res.json();
startTime.value = new Date(data.startTime).getTime();
isRunning.value = true;
interval = setInterval(() => {
elapsedTime.value = Date.now() - startTime.value!;
}, 1000);
}
}
async function stopTimer() {
const res = await fetch('/api/time-entries/stop', {
method: 'POST',
});
if (res.ok) {
isRunning.value = false;
if (interval) clearInterval(interval);
elapsedTime.value = 0;
startTime.value = null;
description.value = '';
selectedClientId.value = '';
selectedCategoryId.value = '';
selectedTags.value = [];
window.location.reload();
}
}
</script>
<template>
<div class="card bg-base-200 shadow-xl border border-base-300 mb-6">
<div class="card-body gap-6">
<!-- Client and Description Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-medium">Client</span>
</label>
<select
v-model="selectedClientId"
class="select select-bordered w-full"
:disabled="isRunning"
>
<option value="">Select a client...</option>
<option v-for="client in clients" :key="client.id" :value="client.id">
{{ client.name }}
</option>
</select>
</div>
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-medium">Category</span>
</label>
<select
v-model="selectedCategoryId"
class="select select-bordered w-full"
:disabled="isRunning"
>
<option value="">Select a category...</option>
<option v-for="category in categories" :key="category.id" :value="category.id">
{{ category.name }}
</option>
</select>
</div>
</div>
<!-- Description Row -->
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-medium">Description</span>
</label>
<input
v-model="description"
type="text"
placeholder="What are you working on?"
class="input input-bordered w-full"
:disabled="isRunning"
/>
</div>
<!-- Tags Section -->
<div v-if="tags.length > 0" class="form-control">
<label class="label pb-2">
<span class="label-text font-medium">Tags</span>
</label>
<div class="flex flex-wrap gap-2">
<button
v-for="tag in tags"
:key="tag.id"
@click="toggleTag(tag.id)"
:class="[
'badge badge-lg cursor-pointer transition-all',
selectedTags.includes(tag.id) ? 'badge-primary' : 'badge-outline'
]"
:disabled="isRunning"
type="button"
>
{{ tag.name }}
</button>
</div>
</div>
<!-- Timer and Action Row -->
<div class="flex flex-col sm:flex-row items-center gap-6 pt-4">
<div class="font-mono text-5xl font-bold tabular-nums tracking-tight text-center sm:text-left flex-grow">
{{ formatTime(elapsedTime) }}
</div>
<button
v-if="!isRunning"
@click="startTimer"
class="btn btn-primary btn-lg min-w-40"
>
Start Timer
</button>
<button
v-else
@click="stopTimer"
class="btn btn-error btn-lg min-w-40"
>
Stop Timer
</button>
</div>
</div>
</div>
</template>