added member-time-tracking route

This commit is contained in:
2026-01-28 17:23:20 +00:00
parent 1aabc463d4
commit c2f5a9abdb
6 changed files with 132 additions and 0 deletions

View File

@@ -68,6 +68,9 @@ const main = async () => {
"/organisation/upload-icon": withGlobalAuthed(withAuth(withCSRF(routes.organisationUploadIcon))),
"/organisation/add-member": withGlobalAuthed(withAuth(withCSRF(routes.organisationAddMember))),
"/organisation/members": withGlobalAuthed(withAuth(routes.organisationMembers)),
"/organisation/member-time-tracking": withGlobalAuthed(
withAuth(routes.organisationMemberTimeTracking),
),
"/organisation/remove-member": withGlobalAuthed(
withAuth(withCSRF(routes.organisationRemoveMember)),
),
@@ -97,6 +100,16 @@ const main = async () => {
"/timer/get": withGlobalAuthed(withAuth(withCSRF(routes.timerGet))),
"/timer/get-inactive": withGlobalAuthed(withAuth(withCSRF(routes.timerGetInactive))),
"/timers": withGlobalAuthed(withAuth(withCSRF(routes.timers))),
// subscription routes - webhook has no auth
"/subscription/create-checkout-session": withGlobalAuthed(
withAuth(withCSRF(routes.subscriptionCreateCheckoutSession)),
),
"/subscription/create-portal-session": withGlobalAuthed(
withAuth(withCSRF(routes.subscriptionCreatePortalSession)),
),
"/subscription/get": withGlobalAuthed(withAuth(routes.subscriptionGet)),
"/subscription/webhook": withGlobal(routes.subscriptionWebhook),
},
});

View File

@@ -20,6 +20,7 @@ import organisationById from "./organisation/by-id";
import organisationsByUser from "./organisation/by-user";
import organisationCreate from "./organisation/create";
import organisationDelete from "./organisation/delete";
import organisationMemberTimeTracking from "./organisation/member-time-tracking";
import organisationMembers from "./organisation/members";
import organisationRemoveMember from "./organisation/remove-member";
import organisationUpdate from "./organisation/update";
@@ -37,6 +38,10 @@ import sprintCreate from "./sprint/create";
import sprintDelete from "./sprint/delete";
import sprintUpdate from "./sprint/update";
import sprintsByProject from "./sprints/by-project";
import subscriptionCreateCheckoutSession from "./subscription/create-checkout-session";
import subscriptionCreatePortalSession from "./subscription/create-portal-session";
import subscriptionGet from "./subscription/get";
import subscriptionWebhook from "./subscription/webhook";
import timerEnd from "./timer/end";
import timerGet from "./timer/get";
import timerGetInactive from "./timer/get-inactive";
@@ -77,6 +82,7 @@ export const routes = {
organisationUpdate,
organisationDelete,
organisationAddMember,
organisationMemberTimeTracking,
organisationMembers,
organisationRemoveMember,
organisationUpdateMemberRole,
@@ -104,4 +110,9 @@ export const routes = {
timerGetInactive,
timerEnd,
timers,
subscriptionCreateCheckoutSession,
subscriptionCreatePortalSession,
subscriptionGet,
subscriptionWebhook,
};

View File

@@ -0,0 +1,64 @@
import { calculateBreakTimeMs, calculateWorkTimeMs, isTimerRunning } from "@sprint/shared";
import { z } from "zod";
import type { AuthedRequest } from "../../auth/middleware";
import {
getOrganisationById,
getOrganisationMemberRole,
getOrganisationMemberTimedSessions,
} from "../../db/queries";
import { errorResponse, parseQueryParams } from "../../validation";
const OrgMemberTimeTrackingQuerySchema = z.object({
organisationId: z.coerce.number().int().positive("organisationId must be a positive integer"),
fromDate: z.coerce.date().optional(),
});
// GET /organisation/member-time-tracking?organisationId=123&fromDate=2024-01-01
export default async function organisationMemberTimeTracking(req: AuthedRequest) {
const url = new URL(req.url);
const parsed = parseQueryParams(url, OrgMemberTimeTrackingQuerySchema);
if ("error" in parsed) return parsed.error;
const { organisationId, fromDate } = parsed.data;
// Check organisation exists
const organisation = await getOrganisationById(organisationId);
if (!organisation) {
return errorResponse(`organisation with id ${organisationId} not found`, "ORG_NOT_FOUND", 404);
}
// Check user is admin or owner of the organisation
const memberRole = await getOrganisationMemberRole(organisationId, req.userId);
if (!memberRole) {
return errorResponse("you are not a member of this organisation", "NOT_MEMBER", 403);
}
const role = memberRole.role;
if (role !== "owner" && role !== "admin") {
return errorResponse("you must be an owner or admin to view member time tracking", "FORBIDDEN", 403);
}
// Get timed sessions for all organisation members
const sessions = await getOrganisationMemberTimedSessions(organisationId, fromDate);
// Enrich with calculated times
// timestamps come from the database as strings, need to convert to Date objects for calculation
const enriched = sessions.map((session) => {
const timestamps = session.timestamps.map((t) => new Date(t));
return {
id: session.id,
userId: session.userId,
issueId: session.issueId,
issueNumber: session.issueNumber,
projectKey: session.projectKey,
timestamps: session.timestamps, // Return original strings for JSON serialization
endedAt: session.endedAt,
createdAt: session.createdAt,
workTimeMs: calculateWorkTimeMs(timestamps),
breakTimeMs: calculateBreakTimeMs(timestamps),
isRunning: session.endedAt === null && isTimerRunning(timestamps),
};
});
return Response.json(enriched);
}