Switch to tags
Some checks failed
Docker Deploy / build-and-push (push) Has been cancelled

This commit is contained in:
2026-01-20 11:09:09 -07:00
parent de5b1063b7
commit ad7dc18780
22 changed files with 1516 additions and 1057 deletions

View File

@@ -4,7 +4,7 @@ import { Icon } from 'astro-icon/components';
import Timer from '../../components/Timer.vue';
import ManualEntry from '../../components/ManualEntry.vue';
import { db } from '../../db';
import { timeEntries, clients, members, tags, timeEntryTags, categories, users } from '../../db/schema';
import { timeEntries, clients, members, tags, timeEntryTags, users } from '../../db/schema';
import { eq, desc, asc, and, sql, or, like } from 'drizzle-orm';
import { formatTimeRange } from '../../lib/formatTime';
@@ -33,11 +33,6 @@ const allClients = await db.select()
.where(eq(clients.organizationId, organizationId))
.all();
const allCategories = await db.select()
.from(categories)
.where(eq(categories.organizationId, organizationId))
.all();
const allTags = await db.select()
.from(tags)
.where(eq(tags.organizationId, organizationId))
@@ -50,7 +45,7 @@ const pageSize = 20;
const offset = (page - 1) * pageSize;
const filterClient = url.searchParams.get('client') || '';
const filterCategory = url.searchParams.get('category') || '';
const filterStatus = url.searchParams.get('status') || '';
const filterType = url.searchParams.get('type') || '';
const sortBy = url.searchParams.get('sort') || 'start-desc';
@@ -62,10 +57,6 @@ if (filterClient) {
conditions.push(eq(timeEntries.clientId, filterClient));
}
if (filterCategory) {
conditions.push(eq(timeEntries.categoryId, filterCategory));
}
if (filterStatus === 'completed') {
conditions.push(sql`${timeEntries.endTime} IS NOT NULL`);
} else if (filterStatus === 'running') {
@@ -107,12 +98,10 @@ switch (sortBy) {
const entries = await db.select({
entry: timeEntries,
client: clients,
category: categories,
user: users,
})
.from(timeEntries)
.leftJoin(clients, eq(timeEntries.clientId, clients.id))
.leftJoin(categories, eq(timeEntries.categoryId, categories.id))
.leftJoin(users, eq(timeEntries.userId, users.id))
.where(and(...conditions))
.orderBy(orderBy)
@@ -169,12 +158,6 @@ const paginationPages = getPaginationPages(page, totalPages);
<span class="flex-1 text-center sm:text-left">You need to create a client before tracking time.</span>
<a href="/dashboard/clients/new" class="btn btn-sm btn-primary whitespace-nowrap">Add Client</a>
</div>
) : allCategories.length === 0 ? (
<div class="alert alert-warning flex flex-col sm:flex-row items-center gap-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<span class="flex-1 text-center sm:text-left">You need to create a category before tracking time.</span>
<a href="/dashboard/team/settings" class="btn btn-sm btn-primary whitespace-nowrap">Team Settings</a>
</div>
) : (
<Timer
client:load
@@ -182,10 +165,8 @@ const paginationPages = getPaginationPages(page, totalPages);
startTime: runningEntry.entry.startTime.getTime(),
description: runningEntry.entry.description,
clientId: runningEntry.entry.clientId,
categoryId: runningEntry.entry.categoryId,
} : null}
clients={allClients.map(c => ({ id: c.id, name: c.name }))}
categories={allCategories.map(c => ({ id: c.id, name: c.name, color: c.color }))}
tags={allTags.map(t => ({ id: t.id, name: t.name, color: t.color }))}
/>
)}
@@ -199,17 +180,10 @@ const paginationPages = getPaginationPages(page, totalPages);
<span class="flex-1 text-center sm:text-left">You need to create a client before adding time entries.</span>
<a href="/dashboard/clients/new" class="btn btn-sm btn-primary whitespace-nowrap">Add Client</a>
</div>
) : allCategories.length === 0 ? (
<div class="alert alert-warning flex flex-col sm:flex-row items-center gap-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<span class="flex-1 text-center sm:text-left">You need to create a category before adding time entries.</span>
<a href="/dashboard/team/settings" class="btn btn-sm btn-primary whitespace-nowrap">Team Settings</a>
</div>
) : (
<ManualEntry
client:idle
clients={allClients.map(c => ({ id: c.id, name: c.name }))}
categories={allCategories.map(c => ({ id: c.id, name: c.name, color: c.color }))}
tags={allTags.map(t => ({ id: t.id, name: t.name, color: t.color }))}
/>
)}
@@ -252,19 +226,7 @@ const paginationPages = getPaginationPages(page, totalPages);
</select>
</div>
<div class="form-control">
<label class="label font-medium" for="tracker-category">
Category
</label>
<select id="tracker-category" name="category" class="select select-bordered bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors w-full" onchange="this.form.submit()">
<option value="">All Categories</option>
{allCategories.map(category => (
<option value={category.id} selected={filterCategory === category.id}>
{category.name}
</option>
))}
</select>
</div>
<div class="form-control">
<label class="label font-medium" for="tracker-status">
@@ -318,7 +280,7 @@ const paginationPages = getPaginationPages(page, totalPages);
<Icon name="heroicons:list-bullet" class="w-6 h-6" />
Time Entries ({totalCount?.count || 0} total)
</h2>
{(filterClient || filterCategory || filterStatus || filterType || searchTerm) && (
{(filterClient || filterStatus || filterType || searchTerm) && (
<a href="/dashboard/tracker" class="btn btn-sm btn-ghost hover:bg-base-300/50 transition-colors">
<Icon name="heroicons:x-mark" class="w-4 h-4" />
Clear Filters
@@ -331,7 +293,6 @@ const paginationPages = getPaginationPages(page, totalPages);
<tr class="bg-base-300/30">
<th>Type</th>
<th>Client</th>
<th>Category</th>
<th>Description</th>
<th>Member</th>
<th>Start Time</th>
@@ -341,7 +302,7 @@ const paginationPages = getPaginationPages(page, totalPages);
</tr>
</thead>
<tbody>
{entries.map(({ entry, client, category, user: entryUser }) => (
{entries.map(({ entry, client, user: entryUser }) => (
<tr class="hover:bg-base-300/20 transition-colors">
<td>
{entry.isManual ? (
@@ -357,14 +318,6 @@ const paginationPages = getPaginationPages(page, totalPages);
)}
</td>
<td class="font-medium">{client?.name || 'Unknown'}</td>
<td>
{category ? (
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full shadow-sm" style={`background-color: ${category.color}`}></span>
<span>{category.name}</span>
</div>
) : '-'}
</td>
<td class="text-base-content/80">{entry.description || '-'}</td>
<td>{entryUser?.name || 'Unknown'}</td>
<td class="whitespace-nowrap">
@@ -407,7 +360,7 @@ const paginationPages = getPaginationPages(page, totalPages);
{totalPages > 1 && (
<div class="flex justify-center items-center gap-2 mt-6">
<a
href={`?page=${Math.max(1, page - 1)}${filterClient ? `&client=${filterClient}` : ''}${filterCategory ? `&category=${filterCategory}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`}
href={`?page=${Math.max(1, page - 1)}${filterClient ? `&client=${filterClient}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`}
class={`btn btn-sm transition-all ${page === 1 ? 'btn-disabled' : 'hover:bg-base-300/50'}`}
>
<Icon name="heroicons:chevron-left" class="w-4 h-4" />
@@ -417,7 +370,7 @@ const paginationPages = getPaginationPages(page, totalPages);
<div class="flex gap-1">
{paginationPages.map(pageNum => (
<a
href={`?page=${pageNum}${filterClient ? `&client=${filterClient}` : ''}${filterCategory ? `&category=${filterCategory}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`}
href={`?page=${pageNum}${filterClient ? `&client=${filterClient}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`}
class={`btn btn-sm transition-all ${page === pageNum ? 'btn-active' : 'hover:bg-base-300/50'}`}
>
{pageNum}
@@ -426,7 +379,7 @@ const paginationPages = getPaginationPages(page, totalPages);
</div>
<a
href={`?page=${Math.min(totalPages, page + 1)}${filterClient ? `&client=${filterClient}` : ''}${filterCategory ? `&category=${filterCategory}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`}
href={`?page=${Math.min(totalPages, page + 1)}${filterClient ? `&client=${filterClient}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`}
class={`btn btn-sm transition-all ${page === totalPages ? 'btn-disabled' : 'hover:bg-base-300/50'}`}
>
Next