mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
timer routes
This commit is contained in:
@@ -2,4 +2,5 @@ export * from "./issues";
|
||||
export * from "./organisations";
|
||||
export * from "./projects";
|
||||
export * from "./sessions";
|
||||
export * from "./timed-sessions";
|
||||
export * from "./users";
|
||||
|
||||
83
packages/backend/src/db/queries/timed-sessions.ts
Normal file
83
packages/backend/src/db/queries/timed-sessions.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { TimedSession } from "@issue/shared";
|
||||
import { and, desc, eq, isNotNull, isNull } from "drizzle-orm";
|
||||
import { db } from "../client";
|
||||
|
||||
export async function createTimedSession(userId: number, issueId: number) {
|
||||
const [timedSession] = await db
|
||||
.insert(TimedSession)
|
||||
.values({ userId, issueId, timestamps: [new Date()] })
|
||||
.returning();
|
||||
return timedSession;
|
||||
}
|
||||
|
||||
export async function getActiveTimedSession(userId: number, issueId: number) {
|
||||
const [timedSession] = await db
|
||||
.select()
|
||||
.from(TimedSession)
|
||||
.where(
|
||||
and(
|
||||
eq(TimedSession.userId, userId),
|
||||
eq(TimedSession.issueId, issueId),
|
||||
isNull(TimedSession.endedAt),
|
||||
),
|
||||
);
|
||||
return timedSession ?? null;
|
||||
}
|
||||
|
||||
export async function getTimedSessionById(id: number) {
|
||||
const [timedSession] = await db.select().from(TimedSession).where(eq(TimedSession.id, id));
|
||||
return timedSession ?? null;
|
||||
}
|
||||
|
||||
export async function appendTimestamp(timedSessionId: number, currentTimestamps: Date[]) {
|
||||
const now = new Date();
|
||||
const updatedTimestamps = [...currentTimestamps, now];
|
||||
|
||||
const [updatedTimedSession] = await db
|
||||
.update(TimedSession)
|
||||
.set({ timestamps: updatedTimestamps })
|
||||
.where(eq(TimedSession.id, timedSessionId))
|
||||
.returning();
|
||||
|
||||
return updatedTimedSession;
|
||||
}
|
||||
|
||||
export async function endTimedSession(timedSessionId: number, currentTimestamps: Date[]) {
|
||||
const now = new Date();
|
||||
let finalTimestamps = [...currentTimestamps];
|
||||
|
||||
// if timer is running (odd timestamps), add final timestamp
|
||||
if (finalTimestamps.length % 2 === 1) {
|
||||
finalTimestamps = [...finalTimestamps, now];
|
||||
}
|
||||
|
||||
const [endedTimedSession] = await db
|
||||
.update(TimedSession)
|
||||
.set({ timestamps: finalTimestamps, endedAt: now })
|
||||
.where(eq(TimedSession.id, timedSessionId))
|
||||
.returning();
|
||||
|
||||
return endedTimedSession;
|
||||
}
|
||||
|
||||
export async function getUserTimedSessions(userId: number, limit = 50, offset = 0) {
|
||||
const timedSessions = await db
|
||||
.select()
|
||||
.from(TimedSession)
|
||||
.where(eq(TimedSession.userId, userId))
|
||||
.orderBy(desc(TimedSession.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
return timedSessions;
|
||||
}
|
||||
|
||||
export async function getCompletedTimedSessions(userId: number, limit = 50, offset = 0) {
|
||||
const timedSessions = await db
|
||||
.select()
|
||||
.from(TimedSession)
|
||||
.where(and(eq(TimedSession.userId, userId), isNotNull(TimedSession.endedAt)))
|
||||
.orderBy(desc(TimedSession.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
return timedSessions;
|
||||
}
|
||||
@@ -66,6 +66,11 @@ const main = async () => {
|
||||
"/projects/by-organisation": withCors(withAuth(routes.projectsByOrganisation)),
|
||||
"/projects/all": withCors(withAuth(routes.projectsAll)),
|
||||
"/projects/with-creators": withCors(withAuth(routes.projectsWithCreators)),
|
||||
|
||||
"/timer/toggle": withCors(withAuth(withCSRF(routes.timerToggle))),
|
||||
"/timer/end": withCors(withAuth(withCSRF(routes.timerEnd))),
|
||||
"/timer/get": withCors(withAuth(withCSRF(routes.timerGet))),
|
||||
"/timers": withCors(withAuth(withCSRF(routes.timers))),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ import projectDelete from "./project/delete";
|
||||
import projectUpdate from "./project/update";
|
||||
import projectWithCreator from "./project/with-creator";
|
||||
import projectsWithCreators from "./project/with-creators";
|
||||
import timerEnd from "./timer/end";
|
||||
import timerGet from "./timer/get";
|
||||
import timerToggle from "./timer/toggle";
|
||||
import timers from "./timers";
|
||||
import userByUsername from "./user/by-username";
|
||||
import userUpdate from "./user/update";
|
||||
import userUploadAvatar from "./user/upload-avatar";
|
||||
@@ -65,4 +69,9 @@ export const routes = {
|
||||
projectsByOrganisation,
|
||||
projectsAll,
|
||||
projectsWithCreators,
|
||||
|
||||
timerToggle,
|
||||
timerGet,
|
||||
timerEnd,
|
||||
timers,
|
||||
};
|
||||
|
||||
39
packages/backend/src/routes/timer/end.ts
Normal file
39
packages/backend/src/routes/timer/end.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { calculateBreakTimeMs, calculateWorkTimeMs } from "@issue/shared";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { endTimedSession, getActiveTimedSession } from "../../db/queries";
|
||||
|
||||
// POST /timer/end
|
||||
export default async function timerEnd(req: AuthedRequest) {
|
||||
const url = new URL(req.url);
|
||||
const issueId = url.searchParams.get("issueId");
|
||||
if (!issueId || Number.isNaN(Number(issueId))) {
|
||||
return new Response("missing issue id", { status: 400 });
|
||||
}
|
||||
const activeSession = await getActiveTimedSession(req.userId, Number(issueId));
|
||||
|
||||
if (!activeSession) {
|
||||
return new Response("no active timer", { status: 400 });
|
||||
}
|
||||
|
||||
// already ended - return existing without modification
|
||||
if (activeSession.endedAt) {
|
||||
return Response.json({
|
||||
...activeSession,
|
||||
workTimeMs: calculateWorkTimeMs(activeSession.timestamps),
|
||||
breakTimeMs: calculateBreakTimeMs(activeSession.timestamps),
|
||||
isRunning: false,
|
||||
});
|
||||
}
|
||||
|
||||
const ended = await endTimedSession(activeSession.id, activeSession.timestamps);
|
||||
if (!ended) {
|
||||
return new Response("failed to end timer", { status: 500 });
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
...ended,
|
||||
workTimeMs: calculateWorkTimeMs(ended.timestamps),
|
||||
breakTimeMs: calculateBreakTimeMs(ended.timestamps),
|
||||
isRunning: false,
|
||||
});
|
||||
}
|
||||
26
packages/backend/src/routes/timer/get.ts
Normal file
26
packages/backend/src/routes/timer/get.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { calculateBreakTimeMs, calculateWorkTimeMs, isTimerRunning } from "@issue/shared";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { getActiveTimedSession } from "../../db/queries";
|
||||
|
||||
// GET /timer?issueId=123
|
||||
export default async function timerGet(req: AuthedRequest) {
|
||||
const url = new URL(req.url);
|
||||
const issueId = url.searchParams.get("issueId");
|
||||
if (!issueId || Number.isNaN(Number(issueId))) {
|
||||
return new Response("missing issue id", { status: 400 });
|
||||
}
|
||||
const activeSession = await getActiveTimedSession(req.userId, Number(issueId));
|
||||
|
||||
if (!activeSession) {
|
||||
return Response.json(null);
|
||||
}
|
||||
|
||||
const running = isTimerRunning(activeSession.timestamps);
|
||||
|
||||
return Response.json({
|
||||
...activeSession,
|
||||
workTimeMs: calculateWorkTimeMs(activeSession.timestamps),
|
||||
breakTimeMs: calculateBreakTimeMs(activeSession.timestamps),
|
||||
isRunning: running,
|
||||
});
|
||||
}
|
||||
40
packages/backend/src/routes/timer/toggle.ts
Normal file
40
packages/backend/src/routes/timer/toggle.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { calculateBreakTimeMs, calculateWorkTimeMs, isTimerRunning } from "@issue/shared";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { appendTimestamp, createTimedSession, getActiveTimedSession } from "../../db/queries";
|
||||
|
||||
// POST /timer/toggle?issueId=123
|
||||
export default async function timerToggle(req: AuthedRequest) {
|
||||
const url = new URL(req.url);
|
||||
const issueId = url.searchParams.get("issueId");
|
||||
if (!issueId || Number.isNaN(Number(issueId))) {
|
||||
return new Response("missing issue id", { status: 400 });
|
||||
}
|
||||
|
||||
const activeSession = await getActiveTimedSession(req.userId, Number(issueId));
|
||||
|
||||
if (!activeSession) {
|
||||
// no active session, create new one with first timestamp
|
||||
const newSession = await createTimedSession(req.userId, Number(issueId));
|
||||
return Response.json({
|
||||
...newSession,
|
||||
workTimeMs: 0,
|
||||
breakTimeMs: 0,
|
||||
isRunning: true,
|
||||
});
|
||||
}
|
||||
|
||||
// active session exists, append timestamp (toggle)
|
||||
const updated = await appendTimestamp(activeSession.id, activeSession.timestamps);
|
||||
if (!updated) {
|
||||
return new Response("failed to update timer", { status: 500 });
|
||||
}
|
||||
|
||||
const running = isTimerRunning(updated.timestamps);
|
||||
|
||||
return Response.json({
|
||||
...updated,
|
||||
workTimeMs: calculateWorkTimeMs(updated.timestamps),
|
||||
breakTimeMs: calculateBreakTimeMs(updated.timestamps),
|
||||
isRunning: running,
|
||||
});
|
||||
}
|
||||
24
packages/backend/src/routes/timers.ts
Normal file
24
packages/backend/src/routes/timers.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { calculateBreakTimeMs, calculateWorkTimeMs, isTimerRunning } from "@issue/shared";
|
||||
import type { AuthedRequest } from "../auth/middleware";
|
||||
import { getUserTimedSessions } from "../db/queries";
|
||||
|
||||
// GET /timers?limit=50&offset=0
|
||||
export default async function timers(req: AuthedRequest) {
|
||||
const url = new URL(req.url);
|
||||
const limitParam = url.searchParams.get("limit");
|
||||
const offsetParam = url.searchParams.get("offset");
|
||||
|
||||
const limit = limitParam ? Number(limitParam) : 50;
|
||||
const offset = offsetParam ? Number(offsetParam) : 0;
|
||||
|
||||
const sessions = await getUserTimedSessions(req.userId, limit, offset);
|
||||
|
||||
const enriched = sessions.map((session) => ({
|
||||
...session,
|
||||
workTimeMs: calculateWorkTimeMs(session.timestamps),
|
||||
breakTimeMs: calculateBreakTimeMs(session.timestamps),
|
||||
isRunning: session.endedAt === null && isTimerRunning(session.timestamps),
|
||||
}));
|
||||
|
||||
return Response.json(enriched);
|
||||
}
|
||||
Reference in New Issue
Block a user