This commit is contained in:
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<div style="position: relative; height: 100%; width: 100%;">
|
||||
<Doughnut :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { Doughnut } from 'vue-chartjs';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
ArcElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
DoughnutController
|
||||
} from 'chart.js';
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend, DoughnutController);
|
||||
|
||||
interface CategoryData {
|
||||
name: string;
|
||||
totalTime: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
categories: CategoryData[];
|
||||
}>();
|
||||
|
||||
const chartData = computed(() => ({
|
||||
labels: props.categories.map(c => c.name),
|
||||
datasets: [{
|
||||
data: props.categories.map(c => c.totalTime),
|
||||
backgroundColor: props.categories.map(c => c.color || '#3b82f6'),
|
||||
borderWidth: 2,
|
||||
borderColor: '#1e293b',
|
||||
}]
|
||||
}));
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom' as const,
|
||||
labels: {
|
||||
color: '#e2e8f0',
|
||||
padding: 15,
|
||||
font: { size: 12 }
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context: any) {
|
||||
const minutes = Math.round(context.raw / (1000 * 60));
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return ` ${context.label}: ${hours}h ${mins}m`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -3,7 +3,6 @@ import { ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
clients: { id: string; name: string }[];
|
||||
categories: { id: string; name: string; color: string | null }[];
|
||||
tags: { id: string; name: string; color: string | null }[];
|
||||
}>();
|
||||
|
||||
@@ -13,7 +12,6 @@ const emit = defineEmits<{
|
||||
|
||||
const description = ref("");
|
||||
const selectedClientId = ref("");
|
||||
const selectedCategoryId = ref("");
|
||||
const selectedTags = ref<string[]>([]);
|
||||
const startDate = ref("");
|
||||
const startTime = ref("");
|
||||
@@ -53,10 +51,6 @@ function validateForm(): string | null {
|
||||
return "Please select a client";
|
||||
}
|
||||
|
||||
if (!selectedCategoryId.value) {
|
||||
return "Please select a category";
|
||||
}
|
||||
|
||||
if (!startDate.value || !startTime.value) {
|
||||
return "Please enter start date and time";
|
||||
}
|
||||
@@ -101,7 +95,6 @@ async function submitManualEntry() {
|
||||
body: JSON.stringify({
|
||||
description: description.value,
|
||||
clientId: selectedClientId.value,
|
||||
categoryId: selectedCategoryId.value,
|
||||
startTime: startDateTime,
|
||||
endTime: endDateTime,
|
||||
tags: selectedTags.value,
|
||||
@@ -119,7 +112,6 @@ async function submitManualEntry() {
|
||||
|
||||
description.value = "";
|
||||
selectedClientId.value = "";
|
||||
selectedCategoryId.value = "";
|
||||
selectedTags.value = [];
|
||||
startDate.value = today;
|
||||
endDate.value = today;
|
||||
@@ -144,7 +136,6 @@ async function submitManualEntry() {
|
||||
function clearForm() {
|
||||
description.value = "";
|
||||
selectedClientId.value = "";
|
||||
selectedCategoryId.value = "";
|
||||
selectedTags.value = [];
|
||||
startDate.value = today;
|
||||
endDate.value = today;
|
||||
@@ -208,49 +199,22 @@ function clearForm() {
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Client and Category Row -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="form-control">
|
||||
<label class="label pb-2 font-medium" for="manual-client">
|
||||
Client <span class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="manual-client"
|
||||
v-model="selectedClientId"
|
||||
class="select select-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<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 font-medium" for="manual-category">
|
||||
Category <span class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="manual-category"
|
||||
v-model="selectedCategoryId"
|
||||
class="select select-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<option value="">Select a category...</option>
|
||||
<option
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:value="category.id"
|
||||
>
|
||||
{{ category.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Client Row -->
|
||||
<div class="form-control">
|
||||
<label class="label pb-2 font-medium" for="manual-client">
|
||||
Client <span class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="manual-client"
|
||||
v-model="selectedClientId"
|
||||
class="select select-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<option value="">Select a client...</option>
|
||||
<option v-for="client in clients" :key="client.id" :value="client.id">
|
||||
{{ client.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Start Date and Time -->
|
||||
|
||||
@@ -7,10 +7,8 @@ const props = defineProps<{
|
||||
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 }[];
|
||||
}>();
|
||||
|
||||
@@ -19,7 +17,6 @@ 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;
|
||||
|
||||
@@ -63,7 +60,6 @@ onMounted(() => {
|
||||
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!;
|
||||
@@ -81,18 +77,12 @@ async function startTimer() {
|
||||
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,
|
||||
}),
|
||||
});
|
||||
@@ -119,7 +109,6 @@ async function stopTimer() {
|
||||
startTime.value = null;
|
||||
description.value = "";
|
||||
selectedClientId.value = "";
|
||||
selectedCategoryId.value = "";
|
||||
selectedTags.value = [];
|
||||
window.location.reload();
|
||||
}
|
||||
@@ -131,49 +120,22 @@ async function stopTimer() {
|
||||
class="card bg-base-200/50 backdrop-blur-sm shadow-lg border border-base-300/50 mb-6 hover:border-base-300 transition-all duration-200"
|
||||
>
|
||||
<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 font-medium" for="timer-client">
|
||||
Client
|
||||
</label>
|
||||
<select
|
||||
id="timer-client"
|
||||
v-model="selectedClientId"
|
||||
class="select select-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
||||
: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 font-medium" for="timer-category">
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
id="timer-category"
|
||||
v-model="selectedCategoryId"
|
||||
class="select select-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
||||
: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>
|
||||
<!-- Client Row -->
|
||||
<div class="form-control">
|
||||
<label class="label pb-2 font-medium" for="timer-client">
|
||||
Client
|
||||
</label>
|
||||
<select
|
||||
id="timer-client"
|
||||
v-model="selectedClientId"
|
||||
class="select select-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
||||
: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>
|
||||
|
||||
<!-- Description Row -->
|
||||
|
||||
Reference in New Issue
Block a user