- {category.name} • {entry.endTime ? formatDuration(entry.endTime.getTime() - entry.startTime.getTime()) : 'Running...'}
+
+
+ {tags.length > 0 ? tags.map((tag: any) => (
+ {tag.name}
+ )) : No tags}
+
+ • {entry.endTime ? formatDuration(entry.endTime.getTime() - entry.startTime.getTime()) : 'Running...'}
))}
diff --git a/src/pages/dashboard/reports.astro b/src/pages/dashboard/reports.astro
index 29d0007..c3d8721 100644
--- a/src/pages/dashboard/reports.astro
+++ b/src/pages/dashboard/reports.astro
@@ -1,12 +1,12 @@
---
import DashboardLayout from '../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
-import CategoryChart from '../../components/CategoryChart.vue';
+import TagChart from '../../components/TagChart.vue';
import ClientChart from '../../components/ClientChart.vue';
import MemberChart from '../../components/MemberChart.vue';
import { db } from '../../db';
-import { timeEntries, members, users, clients, categories, invoices } from '../../db/schema';
-import { eq, and, gte, lte, sql, desc } from 'drizzle-orm';
+import { timeEntries, members, users, clients, tags, timeEntryTags, invoices } from '../../db/schema';
+import { eq, and, gte, lte, sql, desc, inArray, exists } from 'drizzle-orm';
import { formatDuration, formatTimeRange } from '../../lib/formatTime';
const user = Astro.locals.user;
@@ -37,9 +37,9 @@ const teamMembers = await db.select({
.where(eq(members.organizationId, userMembership.organizationId))
.all();
-const allCategories = await db.select()
- .from(categories)
- .where(eq(categories.organizationId, userMembership.organizationId))
+const allTags = await db.select()
+ .from(tags)
+ .where(eq(tags.organizationId, userMembership.organizationId))
.all();
const allClients = await db.select()
@@ -49,7 +49,7 @@ const allClients = await db.select()
const url = new URL(Astro.request.url);
const selectedMemberId = url.searchParams.get('member') || '';
-const selectedCategoryId = url.searchParams.get('category') || '';
+const selectedTagId = url.searchParams.get('tag') || '';
const selectedClientId = url.searchParams.get('client') || '';
const timeRange = url.searchParams.get('range') || 'week';
const customFrom = url.searchParams.get('from');
@@ -102,28 +102,65 @@ if (selectedMemberId) {
conditions.push(eq(timeEntries.userId, selectedMemberId));
}
-if (selectedCategoryId) {
- conditions.push(eq(timeEntries.categoryId, selectedCategoryId));
+if (selectedTagId) {
+ conditions.push(exists(
+ db.select()
+ .from(timeEntryTags)
+ .where(and(
+ eq(timeEntryTags.timeEntryId, timeEntries.id),
+ eq(timeEntryTags.tagId, selectedTagId)
+ ))
+ ));
}
if (selectedClientId) {
conditions.push(eq(timeEntries.clientId, selectedClientId));
}
-const entries = await db.select({
+const entriesData = await db.select({
entry: timeEntries,
user: users,
client: clients,
- category: categories,
})
.from(timeEntries)
.innerJoin(users, eq(timeEntries.userId, users.id))
.innerJoin(clients, eq(timeEntries.clientId, clients.id))
- .innerJoin(categories, eq(timeEntries.categoryId, categories.id))
.where(and(...conditions))
.orderBy(desc(timeEntries.startTime))
.all();
+// Fetch tags for these entries
+const entryIds = entriesData.map(e => e.entry.id);
+const tagsMap = new Map
();
+
+if (entryIds.length > 0) {
+ // Process in chunks if too many entries, but for now simple inArray
+ // Sqlite has limits on variables, but usually ~999. Assuming reasonable page size or volume.
+ // If entryIds is massive, this might fail, but for a dashboard report it's usually acceptable or needs pagination/limits.
+ // However, `inArray` can be empty, so we checked length.
+
+ const entryTagsData = await db.select({
+ timeEntryId: timeEntryTags.timeEntryId,
+ tag: tags
+ })
+ .from(timeEntryTags)
+ .innerJoin(tags, eq(timeEntryTags.tagId, tags.id))
+ .where(inArray(timeEntryTags.timeEntryId, entryIds))
+ .all();
+
+ for (const item of entryTagsData) {
+ if (!tagsMap.has(item.timeEntryId)) {
+ tagsMap.set(item.timeEntryId, []);
+ }
+ tagsMap.get(item.timeEntryId)!.push(item.tag);
+ }
+}
+
+const entries = entriesData.map(e => ({
+ ...e,
+ tags: tagsMap.get(e.entry.id) || []
+}));
+
const statsByMember = teamMembers.map(member => {
const memberEntries = entries.filter(e => e.user.id === member.id);
const totalTime = memberEntries.reduce((sum, e) => {
@@ -140,9 +177,9 @@ const statsByMember = teamMembers.map(member => {
};
}).sort((a, b) => b.totalTime - a.totalTime);
-const statsByCategory = allCategories.map(category => {
- const categoryEntries = entries.filter(e => e.category.id === category.id);
- const totalTime = categoryEntries.reduce((sum, e) => {
+const statsByTag = allTags.map(tag => {
+ const tagEntries = entries.filter(e => e.tags.some(t => t.id === tag.id));
+ const totalTime = tagEntries.reduce((sum, e) => {
if (e.entry.endTime) {
return sum + (e.entry.endTime.getTime() - e.entry.startTime.getTime());
}
@@ -150,9 +187,9 @@ const statsByCategory = allCategories.map(category => {
}, 0);
return {
- category,
+ tag,
totalTime,
- entryCount: categoryEntries.length,
+ entryCount: tagEntries.length,
};
}).sort((a, b) => b.totalTime - a.totalTime);
@@ -336,14 +373,14 @@ function getTimeRangeLabel(range: string) {