improved timer system and overlay

This commit is contained in:
2026-01-26 19:19:46 +00:00
parent 72835324e1
commit 11c808ab69
20 changed files with 445 additions and 19 deletions

View File

@@ -97,6 +97,35 @@ export async function getIssueByID(id: number) {
return issue;
}
export async function getIssueWithUsersById(issueId: number): Promise<IssueResponse | null> {
const Creator = aliasedTable(User, "Creator");
const [issueWithCreator] = await db
.select({
Issue: Issue,
Creator: Creator,
})
.from(Issue)
.where(eq(Issue.id, issueId))
.innerJoin(Creator, eq(Issue.creatorId, Creator.id));
if (!issueWithCreator) return null;
const assigneesData = await db
.select({
User: User,
})
.from(IssueAssignee)
.innerJoin(User, eq(IssueAssignee.userId, User.id))
.where(eq(IssueAssignee.issueId, issueId));
return {
Issue: issueWithCreator.Issue,
Creator: issueWithCreator.Creator,
Assignees: assigneesData.map((row) => row.User),
};
}
export async function getIssueByNumber(projectId: number, number: number) {
const [issue] = await db
.select()

View File

@@ -1,4 +1,4 @@
import { TimedSession } from "@sprint/shared";
import { Issue, Project, TimedSession } from "@sprint/shared";
import { and, desc, eq, isNotNull, isNull } from "drizzle-orm";
import { db } from "../client";
@@ -80,6 +80,26 @@ export async function getUserTimedSessions(userId: number, limit = 50, offset =
return timedSessions;
}
export async function getActiveTimedSessionsWithIssue(userId: number) {
const timedSessions = await db
.select({
id: TimedSession.id,
userId: TimedSession.userId,
issueId: TimedSession.issueId,
timestamps: TimedSession.timestamps,
endedAt: TimedSession.endedAt,
createdAt: TimedSession.createdAt,
issueNumber: Issue.number,
projectKey: Project.key,
})
.from(TimedSession)
.innerJoin(Issue, eq(TimedSession.issueId, Issue.id))
.innerJoin(Project, eq(Issue.projectId, Project.id))
.where(and(eq(TimedSession.userId, userId), isNull(TimedSession.endedAt)))
.orderBy(desc(TimedSession.createdAt));
return timedSessions;
}
export async function getCompletedTimedSessions(userId: number, limit = 50, offset = 0) {
const timedSessions = await db
.select()

View File

@@ -43,6 +43,7 @@ const main = async () => {
"/user/upload-avatar": withGlobal(withAuth(withCSRF(routes.userUploadAvatar))),
"/issue/create": withGlobal(withAuth(withCSRF(routes.issueCreate))),
"/issue/by-id": withGlobal(withAuth(routes.issueById)),
"/issue/update": withGlobal(withAuth(withCSRF(routes.issueUpdate))),
"/issue/delete": withGlobal(withAuth(withCSRF(routes.issueDelete))),
"/issue-comment/create": withGlobal(withAuth(withCSRF(routes.issueCommentCreate))),

View File

@@ -2,6 +2,7 @@ import authLogin from "./auth/login";
import authLogout from "./auth/logout";
import authMe from "./auth/me";
import authRegister from "./auth/register";
import issueById from "./issue/by-id";
import issueCreate from "./issue/create";
import issueDelete from "./issue/delete";
import issueUpdate from "./issue/update";
@@ -56,6 +57,7 @@ export const routes = {
userUploadAvatar,
issueCreate,
issueById,
issueDelete,
issueUpdate,

View File

@@ -0,0 +1,29 @@
import { IssueByIdQuerySchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import { getIssueOrganisationId, getIssueWithUsersById, getOrganisationMemberRole } from "../../db/queries";
import { errorResponse, parseQueryParams } from "../../validation";
export default async function issueById(req: AuthedRequest) {
const url = new URL(req.url);
const parsed = parseQueryParams(url, IssueByIdQuerySchema);
if ("error" in parsed) return parsed.error;
const { issueId } = parsed.data;
const organisationId = await getIssueOrganisationId(issueId);
if (!organisationId) {
return errorResponse(`organisation not found for issue ${issueId}`, "ORG_NOT_FOUND", 404);
}
const member = await getOrganisationMemberRole(organisationId, req.userId);
if (!member) {
return errorResponse("forbidden", "FORBIDDEN", 403);
}
const issue = await getIssueWithUsersById(issueId);
if (!issue) {
return errorResponse(`issue not found: ${issueId}`, "ISSUE_NOT_FOUND", 404);
}
return Response.json(issue);
}

View File

@@ -1,15 +1,34 @@
import { calculateBreakTimeMs, calculateWorkTimeMs, isTimerRunning } from "@sprint/shared";
import type { AuthedRequest } from "../auth/middleware";
import { getUserTimedSessions } from "../db/queries";
import { getActiveTimedSessionsWithIssue, 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 activeOnlyParam = url.searchParams.get("activeOnly");
const limit = limitParam ? Number(limitParam) : 50;
const offset = offsetParam ? Number(offsetParam) : 0;
const activeOnly = activeOnlyParam === "true" || activeOnlyParam === "1";
if (activeOnly) {
const sessions = await getActiveTimedSessionsWithIssue(req.userId);
const enriched = sessions.map((session) => ({
id: session.id,
issueId: session.issueId,
issueNumber: session.issueNumber,
projectKey: session.projectKey,
timestamps: session.timestamps,
endedAt: session.endedAt,
workTimeMs: calculateWorkTimeMs(session.timestamps),
breakTimeMs: calculateBreakTimeMs(session.timestamps),
isRunning: isTimerRunning(session.timestamps),
}));
return Response.json(enriched);
}
const sessions = await getUserTimedSessions(req.userId, limit, offset);