Adding manual entries + UI cleanup

This commit is contained in:
2026-01-16 16:28:06 -07:00
parent 38fe0ea9ce
commit 15b903f1af
12 changed files with 817 additions and 135 deletions

View File

@@ -0,0 +1,195 @@
import type { APIRoute } from 'astro';
import { db } from '../../../db';
import { timeEntries, members, timeEntryTags, categories, clients } from '../../../db/schema';
import { eq, and } from 'drizzle-orm';
import { nanoid } from 'nanoid';
export const POST: APIRoute = async ({ request, locals }) => {
if (!locals.user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' }
}
);
}
const body = await request.json();
const { description, clientId, categoryId, startTime, endTime, tags } = body;
// Validation
if (!clientId) {
return new Response(
JSON.stringify({ error: 'Client is required' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
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,
headers: { 'Content-Type': 'application/json' }
}
);
}
if (!endTime) {
return new Response(
JSON.stringify({ error: 'End time is required' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
const startDate = new Date(startTime);
const endDate = new Date(endTime);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return new Response(
JSON.stringify({ error: 'Invalid date format' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
if (endDate <= startDate) {
return new Response(
JSON.stringify({ error: 'End time must be after start time' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
// Get user's organization
const member = await db
.select()
.from(members)
.where(eq(members.userId, locals.user.id))
.limit(1)
.get();
if (!member) {
return new Response(
JSON.stringify({ error: 'No organization found' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
// Verify category belongs to organization
const category = await db
.select()
.from(categories)
.where(
and(
eq(categories.id, categoryId),
eq(categories.organizationId, member.organizationId)
)
)
.get();
if (!category) {
return new Response(
JSON.stringify({ error: 'Invalid category' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
// Verify client belongs to organization
const client = await db
.select()
.from(clients)
.where(
and(
eq(clients.id, clientId),
eq(clients.organizationId, member.organizationId)
)
)
.get();
if (!client) {
return new Response(
JSON.stringify({ error: 'Invalid client' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
const id = nanoid();
try {
// Insert the manual time entry
await db.insert(timeEntries).values({
id,
userId: locals.user.id,
organizationId: member.organizationId,
clientId,
categoryId,
startTime: startDate,
endTime: endDate,
description: description || null,
isManual: true,
});
// Insert tags if provided
if (tags && Array.isArray(tags) && tags.length > 0) {
await db.insert(timeEntryTags).values(
tags.map((tagId: string) => ({
timeEntryId: id,
tagId,
}))
);
}
return new Response(
JSON.stringify({
success: true,
id,
startTime: startDate.toISOString(),
endTime: endDate.toISOString(),
}),
{
status: 201,
headers: { 'Content-Type': 'application/json' }
}
);
} catch (error) {
console.error('Error creating manual time entry:', error);
return new Response(
JSON.stringify({ error: 'Failed to create time entry' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
);
}
};

View File

@@ -1,51 +1,66 @@
import type { APIRoute } from 'astro';
import { db } from '../../../db';
import { timeEntries, members, timeEntryTags, categories } from '../../../db/schema';
import { eq, and, isNull } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import type { APIRoute } from "astro";
import { db } from "../../../db";
import {
timeEntries,
members,
timeEntryTags,
categories,
} from "../../../db/schema";
import { eq, and, isNull } from "drizzle-orm";
import { nanoid } from "nanoid";
export const POST: APIRoute = async ({ request, locals }) => {
if (!locals.user) return new Response('Unauthorized', { status: 401 });
if (!locals.user) return new Response("Unauthorized", { status: 401 });
const body = await request.json();
const description = body.description || '';
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 });
return new Response("Client is required", { status: 400 });
}
if (!categoryId) {
return new Response('Category is required', { status: 400 });
return new Response("Category is required", { status: 400 });
}
const runningEntry = await db.select().from(timeEntries).where(
and(
eq(timeEntries.userId, locals.user.id),
isNull(timeEntries.endTime)
const runningEntry = await db
.select()
.from(timeEntries)
.where(
and(eq(timeEntries.userId, locals.user.id), isNull(timeEntries.endTime)),
)
).get();
.get();
if (runningEntry) {
return new Response('Timer already running', { status: 400 });
return new Response("Timer already running", { status: 400 });
}
const member = await db.select().from(members).where(eq(members.userId, locals.user.id)).limit(1).get();
const member = await db
.select()
.from(members)
.where(eq(members.userId, locals.user.id))
.limit(1)
.get();
if (!member) {
return new Response('No organization found', { status: 400 });
return new Response("No organization found", { status: 400 });
}
const category = await db.select().from(categories).where(
and(
eq(categories.id, categoryId),
eq(categories.organizationId, member.organizationId)
const category = await db
.select()
.from(categories)
.where(
and(
eq(categories.id, categoryId),
eq(categories.organizationId, member.organizationId),
),
)
).get();
.get();
if (!category) {
return new Response('Invalid category', { status: 400 });
return new Response("Invalid category", { status: 400 });
}
const startTime = new Date();
@@ -59,6 +74,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
categoryId,
startTime,
description,
isManual: false,
});
if (tags.length > 0) {
@@ -66,7 +82,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
tags.map((tagId: string) => ({
timeEntryId: id,
tagId,
}))
})),
);
}