-
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/src/db/schema.ts b/src/db/schema.ts
index 8d2c61a..adf0224 100644
--- a/src/db/schema.ts
+++ b/src/db/schema.ts
@@ -97,30 +97,6 @@ export const clients = sqliteTable(
}),
);
-export const categories = sqliteTable(
- "categories",
- {
- id: text("id")
- .primaryKey()
- .$defaultFn(() => nanoid()),
- organizationId: text("organization_id").notNull(),
- name: text("name").notNull(),
- color: text("color"),
- createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
- () => new Date(),
- ),
- },
- (table: any) => ({
- orgFk: foreignKey({
- columns: [table.organizationId],
- foreignColumns: [organizations.id],
- }),
- organizationIdIdx: index("categories_organization_id_idx").on(
- table.organizationId,
- ),
- }),
-);
-
export const timeEntries = sqliteTable(
"time_entries",
{
@@ -130,7 +106,6 @@ export const timeEntries = sqliteTable(
userId: text("user_id").notNull(),
organizationId: text("organization_id").notNull(),
clientId: text("client_id").notNull(),
- categoryId: text("category_id").notNull(),
startTime: integer("start_time", { mode: "timestamp" }).notNull(),
endTime: integer("end_time", { mode: "timestamp" }),
description: text("description"),
@@ -153,10 +128,6 @@ export const timeEntries = sqliteTable(
columns: [table.clientId],
foreignColumns: [clients.id],
}),
- categoryFk: foreignKey({
- columns: [table.categoryId],
- foreignColumns: [categories.id],
- }),
userIdIdx: index("time_entries_user_id_idx").on(table.userId),
organizationIdIdx: index("time_entries_organization_id_idx").on(
table.organizationId,
diff --git a/src/lib/validation.ts b/src/lib/validation.ts
index f19c8ab..8445ccd 100644
--- a/src/lib/validation.ts
+++ b/src/lib/validation.ts
@@ -1,49 +1,28 @@
import { db } from "../db";
-import { clients, categories, tags as tagsTable } from "../db/schema";
+import { clients, tags as tagsTable } from "../db/schema";
import { eq, and, inArray } from "drizzle-orm";
export async function validateTimeEntryResources({
organizationId,
clientId,
- categoryId,
tagIds,
}: {
organizationId: string;
clientId: string;
- categoryId: string;
tagIds?: string[];
}) {
- const [client, category] = await Promise.all([
- db
- .select()
- .from(clients)
- .where(
- and(
- eq(clients.id, clientId),
- eq(clients.organizationId, organizationId),
- ),
- )
- .get(),
- db
- .select()
- .from(categories)
- .where(
- and(
- eq(categories.id, categoryId),
- eq(categories.organizationId, organizationId),
- ),
- )
- .get(),
- ]);
+ const client = await db
+ .select()
+ .from(clients)
+ .where(
+ and(eq(clients.id, clientId), eq(clients.organizationId, organizationId)),
+ )
+ .get();
if (!client) {
return { valid: false, error: "Invalid client" };
}
- if (!category) {
- return { valid: false, error: "Invalid category" };
- }
-
if (tagIds && tagIds.length > 0) {
const validTags = await db
.select()
diff --git a/src/pages/api/categories/[id]/delete.ts b/src/pages/api/categories/[id]/delete.ts
deleted file mode 100644
index 17038c6..0000000
--- a/src/pages/api/categories/[id]/delete.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import type { APIRoute } from "astro";
-import { db } from "../../../../db";
-import { categories, members, timeEntries } from "../../../../db/schema";
-import { eq, and } from "drizzle-orm";
-
-export const POST: APIRoute = async ({ request, locals, redirect, params }) => {
- const user = locals.user;
- if (!user) {
- return new Response("Unauthorized", { status: 401 });
- }
-
- const { id } = params;
- let redirectTo: string | undefined;
-
- if (request.headers.get("Content-Type")?.includes("application/json")) {
- const body = await request.json();
- redirectTo = body.redirectTo;
- } else {
- const formData = await request.formData();
- redirectTo = formData.get("redirectTo")?.toString();
- }
-
- const userOrg = await db
- .select()
- .from(members)
- .where(eq(members.userId, user.id))
- .get();
-
- if (!userOrg) {
- return new Response("No organization found", { status: 400 });
- }
-
- const isAdmin = userOrg.role === "owner" || userOrg.role === "admin";
- if (!isAdmin) {
- return new Response("Forbidden", { status: 403 });
- }
-
- const hasEntries = await db
- .select()
- .from(timeEntries)
- .where(eq(timeEntries.categoryId, id!))
- .get();
-
- if (hasEntries) {
- return new Response("Cannot delete category with time entries", {
- status: 400,
- });
- }
-
- await db
- .delete(categories)
- .where(
- and(
- eq(categories.id, id!),
- eq(categories.organizationId, userOrg.organizationId),
- ),
- );
-
- if (locals.scopes) {
- return new Response(JSON.stringify({ success: true }), {
- status: 200,
- headers: { "Content-Type": "application/json" },
- });
- }
-
- return redirect(redirectTo || "/dashboard/team/settings");
-};
diff --git a/src/pages/api/categories/[id]/update.ts b/src/pages/api/categories/[id]/update.ts
deleted file mode 100644
index 9e52bd8..0000000
--- a/src/pages/api/categories/[id]/update.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import type { APIRoute } from "astro";
-import { db } from "../../../../db";
-import { categories, members } from "../../../../db/schema";
-import { eq, and } from "drizzle-orm";
-
-export const POST: APIRoute = async ({ request, locals, redirect, params }) => {
- const user = locals.user;
- if (!user) {
- return new Response("Unauthorized", { status: 401 });
- }
-
- const { id } = params;
- let name: string | undefined;
- let color: string | undefined;
- let redirectTo: string | undefined;
-
- if (request.headers.get("Content-Type")?.includes("application/json")) {
- const body = await request.json();
- name = body.name;
- color = body.color;
- redirectTo = body.redirectTo;
- } else {
- const formData = await request.formData();
- name = formData.get("name")?.toString();
- color = formData.get("color")?.toString();
- redirectTo = formData.get("redirectTo")?.toString();
- }
-
- if (!name) {
- return new Response("Name is required", { status: 400 });
- }
-
- const userOrg = await db
- .select()
- .from(members)
- .where(eq(members.userId, user.id))
- .get();
-
- if (!userOrg) {
- return new Response("No organization found", { status: 400 });
- }
-
- const isAdmin = userOrg.role === "owner" || userOrg.role === "admin";
- if (!isAdmin) {
- return new Response("Forbidden", { status: 403 });
- }
-
- await db
- .update(categories)
- .set({
- name,
- color: color || null,
- })
- .where(
- and(
- eq(categories.id, id!),
- eq(categories.organizationId, userOrg.organizationId),
- ),
- );
-
- if (locals.scopes) {
- return new Response(
- JSON.stringify({ success: true, id, name, color: color || null }),
- {
- status: 200,
- headers: { "Content-Type": "application/json" },
- },
- );
- }
-
- return redirect(redirectTo || "/dashboard/team/settings");
-};
diff --git a/src/pages/api/categories/create.ts b/src/pages/api/categories/create.ts
deleted file mode 100644
index 747c91a..0000000
--- a/src/pages/api/categories/create.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import type { APIRoute } from "astro";
-import { db } from "../../../db";
-import { categories, members } from "../../../db/schema";
-import { eq } from "drizzle-orm";
-import { nanoid } from "nanoid";
-
-export const POST: APIRoute = async ({ request, locals, redirect }) => {
- const user = locals.user;
- if (!user) {
- return new Response("Unauthorized", { status: 401 });
- }
-
- let name: string | undefined;
- let color: string | undefined;
- let redirectTo: string | undefined;
-
- if (request.headers.get("Content-Type")?.includes("application/json")) {
- const body = await request.json();
- name = body.name;
- color = body.color;
- redirectTo = body.redirectTo;
- } else {
- const formData = await request.formData();
- name = formData.get("name")?.toString();
- color = formData.get("color")?.toString();
- redirectTo = formData.get("redirectTo")?.toString();
- }
-
- if (!name) {
- return new Response("Name is required", { status: 400 });
- }
-
- const userOrg = await db
- .select()
- .from(members)
- .where(eq(members.userId, user.id))
- .get();
-
- if (!userOrg) {
- return new Response("No organization found", { status: 400 });
- }
-
- const id = nanoid();
- await db.insert(categories).values({
- id,
- organizationId: userOrg.organizationId,
- name,
- color: color || null,
- });
-
- if (locals.scopes) {
- return new Response(JSON.stringify({ id, name, color: color || null }), {
- status: 201,
- headers: { "Content-Type": "application/json" },
- });
- }
-
- return redirect(redirectTo || "/dashboard/team/settings");
-};
diff --git a/src/pages/api/invoices/[id]/generate.ts b/src/pages/api/invoices/[id]/generate.ts
index 09b9953..e967c6e 100644
--- a/src/pages/api/invoices/[id]/generate.ts
+++ b/src/pages/api/invoices/[id]/generate.ts
@@ -1,102 +1,100 @@
import type { APIRoute } from "astro";
-import { db } from "../../../../db";
-import {
- invoices,
- invoiceItems,
- clients,
- organizations,
- members,
-} from "../../../../db/schema";
-import { eq, and } from "drizzle-orm";
import { renderToStream } from "@ceereals/vue-pdf";
+import { db } from "../../../../db";
+import { invoices, invoiceItems, clients, organizations, members } from "../../../../db/schema";
+import { eq, and } from "drizzle-orm";
import { createInvoiceDocument } from "../../../../pdf/generateInvoicePDF";
export const GET: APIRoute = async ({ params, locals }) => {
+ const user = locals.user;
+ if (!user) {
+ return new Response("Unauthorized", { status: 401 });
+ }
+
+ const { id } = params;
+ if (!id) {
+ return new Response("Invoice ID is required", { status: 400 });
+ }
+
+ // Fetch invoice with related data
+ const invoiceResult = await db.select({
+ invoice: invoices,
+ client: clients,
+ organization: organizations,
+ })
+ .from(invoices)
+ .leftJoin(clients, eq(invoices.clientId, clients.id))
+ .innerJoin(organizations, eq(invoices.organizationId, organizations.id))
+ .where(eq(invoices.id, id))
+ .get();
+
+ if (!invoiceResult) {
+ return new Response("Invoice not found", { status: 404 });
+ }
+
+ const { invoice, client, organization } = invoiceResult;
+
+ // Verify membership
+ const membership = await db.select()
+ .from(members)
+ .where(and(
+ eq(members.userId, user.id),
+ eq(members.organizationId, invoice.organizationId)
+ ))
+ .get();
+
+ if (!membership) {
+ return new Response("Not authorized", { status: 403 });
+ }
+
+ // Fetch items
+ const items = await db.select()
+ .from(invoiceItems)
+ .where(eq(invoiceItems.invoiceId, invoice.id))
+ .all();
+
try {
- const { id } = params;
- const user = locals.user;
+ const document = createInvoiceDocument({
+ invoice: {
+ ...invoice,
+ notes: invoice.notes || null,
+ // Ensure null safety for optional fields that might be undefined in some runtimes depending on driver
+ discountValue: invoice.discountValue ?? null,
+ discountType: invoice.discountType ?? null,
+ discountAmount: invoice.discountAmount ?? null,
+ taxRate: invoice.taxRate ?? null,
+ },
+ items,
+ client: {
+ name: client?.name || "Deleted Client",
+ email: client?.email || null,
+ street: client?.street || null,
+ city: client?.city || null,
+ state: client?.state || null,
+ zip: client?.zip || null,
+ country: client?.country || null,
+ },
+ organization: {
+ name: organization.name,
+ street: organization.street || null,
+ city: organization.city || null,
+ state: organization.state || null,
+ zip: organization.zip || null,
+ country: organization.country || null,
+ logoUrl: organization.logoUrl || null,
+ }
+ });
- if (!user || !id) {
- return new Response("Unauthorized", { status: 401 });
- }
+ const stream = await renderToStream(document);
- const invoiceResult = await db
- .select({
- invoice: invoices,
- client: clients,
- organization: organizations,
- })
- .from(invoices)
- .leftJoin(clients, eq(invoices.clientId, clients.id))
- .innerJoin(organizations, eq(invoices.organizationId, organizations.id))
- .where(eq(invoices.id, id))
- .get();
-
- if (!invoiceResult) {
- return new Response("Invoice not found", { status: 404 });
- }
-
- const { invoice, client, organization } = invoiceResult;
-
- const membership = await db
- .select()
- .from(members)
- .where(
- and(
- eq(members.userId, user.id),
- eq(members.organizationId, invoice.organizationId),
- ),
- )
- .get();
-
- if (!membership) {
- return new Response("Forbidden", { status: 403 });
- }
-
- const items = await db
- .select()
- .from(invoiceItems)
- .where(eq(invoiceItems.invoiceId, invoice.id))
- .all();
-
- if (!client) {
- return new Response("Client not found", { status: 404 });
- }
-
- const originalConsoleLog = console.log;
- const originalConsoleWarn = console.warn;
- console.log = () => {};
- console.warn = () => {};
-
- try {
- const pdfDocument = createInvoiceDocument({
- invoice,
- items,
- client,
- organization,
- });
-
- const stream = await renderToStream(pdfDocument);
-
- console.log = originalConsoleLog;
- console.warn = originalConsoleWarn;
-
- const filename = `${invoice.type}_${invoice.number.replace(/[^a-zA-Z0-9]/g, "_")}.pdf`;
-
- return new Response(stream as any, {
- headers: {
- "Content-Type": "application/pdf",
- "Content-Disposition": `attachment; filename="${filename}"`,
- },
- });
- } catch (pdfError) {
- // Restore console.log on error
- console.log = originalConsoleLog;
- console.warn = originalConsoleWarn;
- throw pdfError;
- }
+ return new Response(stream, {
+ headers: {
+ "Content-Type": "application/pdf",
+ "Content-Disposition": `attachment; filename="${invoice.number}.pdf"`,
+ },
+ });
} catch (error) {
console.error("Error generating PDF:", error);
- return new Response("Error generating PDF", { status: 500 });
+ return new Response("Failed to generate PDF", { status: 500 });
}
};
diff --git a/src/pages/api/reports/export.ts b/src/pages/api/reports/export.ts
index 260e155..e9dd50a 100644
--- a/src/pages/api/reports/export.ts
+++ b/src/pages/api/reports/export.ts
@@ -1,72 +1,96 @@
-import type { APIRoute } from 'astro';
-import { db } from '../../../db';
-import { timeEntries, members, users, clients, categories } from '../../../db/schema';
-import { eq, and, gte, lte, desc } from 'drizzle-orm';
+import type { APIRoute } from "astro";
+import { db } from "../../../db";
+import {
+ timeEntries,
+ members,
+ users,
+ clients,
+ tags,
+ timeEntryTags,
+} from "../../../db/schema";
+import { eq, and, gte, lte, desc, inArray } from "drizzle-orm";
export const GET: APIRoute = async ({ request, locals, cookies }) => {
const user = locals.user;
if (!user) {
- return new Response('Unauthorized', { status: 401 });
+ return new Response("Unauthorized", { status: 401 });
}
// Get current team from cookie
- const currentTeamId = cookies.get('currentTeamId')?.value;
+ const currentTeamId = cookies.get("currentTeamId")?.value;
- const userMemberships = await db.select()
+ const userMemberships = await db
+ .select()
.from(members)
.where(eq(members.userId, user.id))
.all();
if (userMemberships.length === 0) {
- return new Response('No organization found', { status: 404 });
+ return new Response("No organization found", { status: 404 });
}
// Use current team or fallback to first membership
const userMembership = currentTeamId
- ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
+ ? userMemberships.find((m) => m.organizationId === currentTeamId) ||
+ userMemberships[0]
: userMemberships[0];
const url = new URL(request.url);
- const selectedMemberId = url.searchParams.get('member') || '';
- const selectedCategoryId = url.searchParams.get('category') || '';
- const selectedClientId = url.searchParams.get('client') || '';
- const timeRange = url.searchParams.get('range') || 'week';
- const customFrom = url.searchParams.get('from');
- const customTo = url.searchParams.get('to');
+ const selectedMemberId = url.searchParams.get("member") || "";
+ const selectedClientId = url.searchParams.get("client") || "";
+ const timeRange = url.searchParams.get("range") || "week";
+ const customFrom = url.searchParams.get("from");
+ const customTo = url.searchParams.get("to");
const now = new Date();
let startDate = new Date();
let endDate = new Date();
switch (timeRange) {
- case 'today':
+ case "today":
startDate.setHours(0, 0, 0, 0);
endDate.setHours(23, 59, 59, 999);
break;
- case 'week':
+ case "week":
startDate.setDate(now.getDate() - 7);
break;
- case 'month':
+ case "month":
startDate.setMonth(now.getMonth() - 1);
break;
- case 'mtd':
+ case "mtd":
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
break;
- case 'ytd':
+ case "ytd":
startDate = new Date(now.getFullYear(), 0, 1);
break;
- case 'last-month':
+ case "last-month":
startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
endDate = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
break;
- case 'custom':
+ case "custom":
if (customFrom) {
- const parts = customFrom.split('-');
- startDate = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 0, 0, 0, 0);
+ const parts = customFrom.split("-");
+ startDate = new Date(
+ parseInt(parts[0]),
+ parseInt(parts[1]) - 1,
+ parseInt(parts[2]),
+ 0,
+ 0,
+ 0,
+ 0,
+ );
}
if (customTo) {
- const parts = customTo.split('-');
- endDate = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 23, 59, 59, 999);
+ const parts = customTo.split("-");
+ endDate = new Date(
+ parseInt(parts[0]),
+ parseInt(parts[1]) - 1,
+ parseInt(parts[2]),
+ 23,
+ 59,
+ 59,
+ 999,
+ );
}
break;
}
@@ -81,31 +105,58 @@ export const GET: APIRoute = async ({ request, locals, cookies }) => {
conditions.push(eq(timeEntries.userId, selectedMemberId));
}
- if (selectedCategoryId) {
- conditions.push(eq(timeEntries.categoryId, selectedCategoryId));
- }
-
if (selectedClientId) {
conditions.push(eq(timeEntries.clientId, selectedClientId));
}
- const entries = await db.select({
- entry: timeEntries,
- user: users,
- client: clients,
- category: categories,
- })
+ const entries = await db
+ .select({
+ entry: timeEntries,
+ user: users,
+ client: clients,
+ })
.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 = entries.map((e) => e.entry.id);
+ const tagsMap = new Map
();
+
+ if (entryIds.length > 0) {
+ const entryTags = await db
+ .select({
+ entryId: timeEntryTags.timeEntryId,
+ tagName: tags.name,
+ })
+ .from(timeEntryTags)
+ .innerJoin(tags, eq(timeEntryTags.tagId, tags.id))
+ .where(inArray(timeEntryTags.timeEntryId, entryIds))
+ .all();
+
+ for (const tag of entryTags) {
+ if (!tagsMap.has(tag.entryId)) {
+ tagsMap.set(tag.entryId, []);
+ }
+ tagsMap.get(tag.entryId)!.push(tag.tagName);
+ }
+ }
+
// Generate CSV
- const headers = ['Date', 'Start Time', 'End Time', 'Duration (h)', 'Member', 'Client', 'Category', 'Description'];
- const rows = entries.map(e => {
+ const headers = [
+ "Date",
+ "Start Time",
+ "End Time",
+ "Duration (h)",
+ "Member",
+ "Client",
+ "Tags",
+ "Description",
+ ];
+ const rows = entries.map((e) => {
const start = e.entry.startTime;
const end = e.entry.endTime;
@@ -114,24 +165,26 @@ export const GET: APIRoute = async ({ request, locals, cookies }) => {
duration = (end.getTime() - start.getTime()) / (1000 * 60 * 60); // Hours
}
+ const tagsStr = tagsMap.get(e.entry.id)?.join("; ") || "";
+
return [
start.toLocaleDateString(),
start.toLocaleTimeString(),
- end ? end.toLocaleTimeString() : '',
- end ? duration.toFixed(2) : 'Running',
- `"${(e.user.name || '').replace(/"/g, '""')}"`,
- `"${(e.client.name || '').replace(/"/g, '""')}"`,
- `"${(e.category.name || '').replace(/"/g, '""')}"`,
- `"${(e.entry.description || '').replace(/"/g, '""')}"`
- ].join(',');
+ end ? end.toLocaleTimeString() : "",
+ end ? duration.toFixed(2) : "Running",
+ `"${(e.user.name || "").replace(/"/g, '""')}"`,
+ `"${(e.client.name || "").replace(/"/g, '""')}"`,
+ `"${tagsStr.replace(/"/g, '""')}"`,
+ `"${(e.entry.description || "").replace(/"/g, '""')}"`,
+ ].join(",");
});
- const csvContent = [headers.join(','), ...rows].join('\n');
+ const csvContent = [headers.join(","), ...rows].join("\n");
return new Response(csvContent, {
headers: {
- 'Content-Type': 'text/csv',
- 'Content-Disposition': `attachment; filename="time-entries-${startDate.toISOString().split('T')[0]}-to-${endDate.toISOString().split('T')[0]}.csv"`,
+ "Content-Type": "text/csv",
+ "Content-Disposition": `attachment; filename="time-entries-${startDate.toISOString().split("T")[0]}-to-${endDate.toISOString().split("T")[0]}.csv"`,
},
});
};
diff --git a/src/pages/api/time-entries/manual.ts b/src/pages/api/time-entries/manual.ts
index 6883b32..c74cdd7 100644
--- a/src/pages/api/time-entries/manual.ts
+++ b/src/pages/api/time-entries/manual.ts
@@ -17,7 +17,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
}
const body = await request.json();
- const { description, clientId, categoryId, startTime, endTime, tags } = body;
+ const { description, clientId, startTime, endTime, tags } = body;
// Validation
if (!clientId) {
@@ -27,13 +27,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
});
}
- if (!categoryId) {
- return new Response(JSON.stringify({ error: "Category is required" }), {
- status: 400,
- headers: { "Content-Type": "application/json" },
- });
- }
-
if (!startTime) {
return new Response(JSON.stringify({ error: "Start time is required" }), {
status: 400,
@@ -81,7 +74,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
const resourceValidation = await validateTimeEntryResources({
organizationId: member.organizationId,
clientId,
- categoryId,
tagIds: Array.isArray(tags) ? tags : undefined,
});
@@ -101,7 +93,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
userId: locals.user.id,
organizationId: member.organizationId,
clientId,
- categoryId,
startTime: startDate,
endTime: endDate,
description: description || null,
diff --git a/src/pages/api/time-entries/start.ts b/src/pages/api/time-entries/start.ts
index f59aa44..6a4320d 100644
--- a/src/pages/api/time-entries/start.ts
+++ b/src/pages/api/time-entries/start.ts
@@ -11,17 +11,12 @@ export const POST: APIRoute = async ({ request, locals }) => {
const body = await request.json();
const description = body.description || "";
const clientId = body.clientId;
- const categoryId = body.categoryId;
const tags = body.tags || [];
if (!clientId) {
return new Response("Client is required", { status: 400 });
}
- if (!categoryId) {
- return new Response("Category is required", { status: 400 });
- }
-
const runningEntry = await db
.select()
.from(timeEntries)
@@ -47,7 +42,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
const validation = await validateTimeEntryResources({
organizationId: member.organizationId,
clientId,
- categoryId,
tagIds: tags,
});
@@ -63,7 +57,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
userId: locals.user.id,
organizationId: member.organizationId,
clientId,
- categoryId,
startTime,
description,
isManual: false,
diff --git a/src/pages/dashboard/categories.astro b/src/pages/dashboard/categories.astro
deleted file mode 100644
index 5e35708..0000000
--- a/src/pages/dashboard/categories.astro
+++ /dev/null
@@ -1,62 +0,0 @@
----
-import DashboardLayout from '../../layouts/DashboardLayout.astro';
-import { db } from '../../db';
-import { categories, members } from '../../db/schema';
-import { eq } from 'drizzle-orm';
-
-const user = Astro.locals.user;
-if (!user) return Astro.redirect('/login');
-
-// Get current team from cookie
-const currentTeamId = Astro.cookies.get('currentTeamId')?.value;
-
-const userMemberships = await db.select()
- .from(members)
- .where(eq(members.userId, user.id))
- .all();
-
-if (userMemberships.length === 0) return Astro.redirect('/dashboard');
-
-// Use current team or fallback to first membership
-const userMembership = currentTeamId
- ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
- : userMemberships[0];
-
-const allCategories = await db.select()
- .from(categories)
- .where(eq(categories.organizationId, userMembership.organizationId))
- .all();
----
-
-
-
-
-
- {allCategories.map(category => (
-
-
-
- {category.color && (
-
- )}
- {category.name}
-
-
Created {category.createdAt?.toLocaleDateString() ?? 'N/A'}
-
-
-
- ))}
-
-
- {allCategories.length === 0 && (
-
-
No categories yet
-
Add Your First Category
-
- )}
-
diff --git a/src/pages/dashboard/categories/[id]/edit.astro b/src/pages/dashboard/categories/[id]/edit.astro
deleted file mode 100644
index 37ea17f..0000000
--- a/src/pages/dashboard/categories/[id]/edit.astro
+++ /dev/null
@@ -1,99 +0,0 @@
----
-import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
-import { Icon } from 'astro-icon/components';
-import { db } from '../../../../db';
-import { categories, members } from '../../../../db/schema';
-import { eq, and } from 'drizzle-orm';
-
-const user = Astro.locals.user;
-if (!user) return Astro.redirect('/login');
-
-const { id } = Astro.params;
-
-// Get current team from cookie
-const currentTeamId = Astro.cookies.get('currentTeamId')?.value;
-
-const userMemberships = await db.select()
- .from(members)
- .where(eq(members.userId, user.id))
- .all();
-
-if (userMemberships.length === 0) return Astro.redirect('/dashboard');
-
-// Use current team or fallback to first membership
-const userMembership = currentTeamId
- ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
- : userMemberships[0];
-
-const isAdmin = userMembership.role === 'owner' || userMembership.role === 'admin';
-if (!isAdmin) return Astro.redirect('/dashboard/categories');
-
-const category = await db.select()
- .from(categories)
- .where(and(
- eq(categories.id, id!),
- eq(categories.organizationId, userMembership.organizationId)
- ))
- .get();
-
-if (!category) return Astro.redirect('/dashboard/categories');
----
-
-
-
-
diff --git a/src/pages/dashboard/categories/new.astro b/src/pages/dashboard/categories/new.astro
deleted file mode 100644
index 9082b99..0000000
--- a/src/pages/dashboard/categories/new.astro
+++ /dev/null
@@ -1,54 +0,0 @@
----
-import DashboardLayout from '../../../layouts/DashboardLayout.astro';
-import { Icon } from 'astro-icon/components';
-
-const user = Astro.locals.user;
-if (!user) return Astro.redirect('/login');
----
-
-
-
-
-
-
-
-
Add New Category
-
-
-
-
-
diff --git a/src/pages/dashboard/team/settings.astro b/src/pages/dashboard/team/settings.astro
index 0eeca03..1e7d533 100644
--- a/src/pages/dashboard/team/settings.astro
+++ b/src/pages/dashboard/team/settings.astro
@@ -2,7 +2,7 @@
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import { db } from '../../../db';
-import { categories, members, organizations, tags } from '../../../db/schema';
+import { members, organizations, tags } from '../../../db/schema';
import { eq } from 'drizzle-orm';
const user = Astro.locals.user;
@@ -35,10 +35,7 @@ const organization = await db.select()
if (!organization) return Astro.redirect('/dashboard');
-const allCategories = await db.select()
- .from(categories)
- .where(eq(categories.organizationId, orgId))
- .all();
+
const allTags = await db.select()
.from(tags)
@@ -415,60 +412,6 @@ const successType = url.searchParams.get('success');
-
-
-
-
-
- Categories help organize time tracking by type of work. All team members use the same categories.
-
-
- {allCategories.length === 0 ? (
-
-
-
-
No categories yet
-
Create your first category to start organizing time entries.
-
-
- ) : (
-
- {allCategories.map(category => (
-
-
-
- {category.color && (
-
- )}
-
-
{category.name}
-
- Created {category.createdAt?.toLocaleDateString() ?? 'N/A'}
-
-
-
-
-
-
-
-
- ))}
-
- )}
-
-
diff --git a/src/pages/dashboard/team/settings/categories/[id]/edit.astro b/src/pages/dashboard/team/settings/categories/[id]/edit.astro
deleted file mode 100644
index 9fb4112..0000000
--- a/src/pages/dashboard/team/settings/categories/[id]/edit.astro
+++ /dev/null
@@ -1,93 +0,0 @@
----
-import DashboardLayout from '../../../../../../layouts/DashboardLayout.astro';
-import { Icon } from 'astro-icon/components';
-import { db } from '../../../../../../db';
-import { categories, members } from '../../../../../../db/schema';
-import { eq, and } from 'drizzle-orm';
-
-const user = Astro.locals.user;
-if (!user) return Astro.redirect('/login');
-
-const { id } = Astro.params;
-
-// Get current team from cookie
-const currentTeamId = Astro.cookies.get('currentTeamId')?.value;
-
-const userMemberships = await db.select()
- .from(members)
- .where(eq(members.userId, user.id))
- .all();
-
-if (userMemberships.length === 0) return Astro.redirect('/dashboard');
-
-// Use current team or fallback to first membership
-const userMembership = currentTeamId
- ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
- : userMemberships[0];
-
-const isAdmin = userMembership.role === 'owner' || userMembership.role === 'admin';
-if (!isAdmin) return Astro.redirect('/dashboard/team/settings');
-
-const category = await db.select()
- .from(categories)
- .where(and(
- eq(categories.id, id!),
- eq(categories.organizationId, userMembership.organizationId)
- ))
- .get();
-
-if (!category) return Astro.redirect('/dashboard/team/settings');
----
-
-
-
-
diff --git a/src/pages/dashboard/team/settings/categories/new.astro b/src/pages/dashboard/team/settings/categories/new.astro
deleted file mode 100644
index 4777d44..0000000
--- a/src/pages/dashboard/team/settings/categories/new.astro
+++ /dev/null
@@ -1,53 +0,0 @@
----
-import DashboardLayout from '../../../../../layouts/DashboardLayout.astro';
-import { Icon } from 'astro-icon/components';
-
-const user = Astro.locals.user;
-if (!user) return Astro.redirect('/login');
----
-
-
-
-
-
-
-
-
Add New Category
-
-
-
-
-
diff --git a/src/pages/dashboard/tracker.astro b/src/pages/dashboard/tracker.astro
index 5163dfb..0e29e0e 100644
--- a/src/pages/dashboard/tracker.astro
+++ b/src/pages/dashboard/tracker.astro
@@ -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);
You need to create a client before tracking time.
Add Client