All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m57s
134 lines
3.4 KiB
TypeScript
134 lines
3.4 KiB
TypeScript
import type { APIRoute } from "astro";
|
|
import { db } from "../../../db";
|
|
import { timeEntries, members } from "../../../db/schema";
|
|
import { eq } from "drizzle-orm";
|
|
import { nanoid } from "nanoid";
|
|
import {
|
|
validateTimeEntryResources,
|
|
validateTimeRange,
|
|
MAX_LENGTHS,
|
|
} from "../../../lib/validation";
|
|
|
|
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, startTime, endTime, tagId } = body;
|
|
|
|
// Validation
|
|
if (!clientId) {
|
|
return new Response(JSON.stringify({ error: "Client is required" }), {
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
if (description && description.length > MAX_LENGTHS.description) {
|
|
return new Response(
|
|
JSON.stringify({ error: `Description must be ${MAX_LENGTHS.description} characters or fewer` }),
|
|
{ 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 timeValidation = validateTimeRange(startTime, endTime);
|
|
|
|
if (
|
|
!timeValidation.valid ||
|
|
!timeValidation.startDate ||
|
|
!timeValidation.endDate
|
|
) {
|
|
return new Response(JSON.stringify({ error: timeValidation.error }), {
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
const { startDate, endDate } = timeValidation;
|
|
|
|
// 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" },
|
|
});
|
|
}
|
|
|
|
const resourceValidation = await validateTimeEntryResources({
|
|
organizationId: member.organizationId,
|
|
clientId,
|
|
tagId: tagId || null,
|
|
});
|
|
|
|
if (!resourceValidation.valid) {
|
|
return new Response(JSON.stringify({ error: resourceValidation.error }), {
|
|
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,
|
|
tagId: tagId || null,
|
|
startTime: startDate,
|
|
endTime: endDate,
|
|
description: description || null,
|
|
isManual: true,
|
|
});
|
|
|
|
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" },
|
|
},
|
|
);
|
|
}
|
|
};
|