Adding manual entries + UI cleanup
This commit is contained in:
195
src/pages/api/time-entries/manual.ts
Normal file
195
src/pages/api/time-entries/manual.ts
Normal 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' }
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user