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/upload-icon": withGlobalAuthed(withAuth(withCSRF(routes.organisationUploadIcon))),
"/organisation/add-member": withGlobalAuthed(withAuth(withCSRF(routes.organisationAddMember))), "/organisation/add-member": withGlobalAuthed(withAuth(withCSRF(routes.organisationAddMember))),
"/organisation/members": withGlobalAuthed(withAuth(routes.organisationMembers)), "/organisation/members": withGlobalAuthed(withAuth(routes.organisationMembers)),
"/organisation/member-time-tracking": withGlobalAuthed(
withAuth(routes.organisationMemberTimeTracking),
),
"/organisation/remove-member": withGlobalAuthed( "/organisation/remove-member": withGlobalAuthed(
withAuth(withCSRF(routes.organisationRemoveMember)), withAuth(withCSRF(routes.organisationRemoveMember)),
), ),
@@ -97,6 +100,16 @@ const main = async () => {
"/timer/get": withGlobalAuthed(withAuth(withCSRF(routes.timerGet))), "/timer/get": withGlobalAuthed(withAuth(withCSRF(routes.timerGet))),
"/timer/get-inactive": withGlobalAuthed(withAuth(withCSRF(routes.timerGetInactive))), "/timer/get-inactive": withGlobalAuthed(withAuth(withCSRF(routes.timerGetInactive))),
"/timers": withGlobalAuthed(withAuth(withCSRF(routes.timers))), "/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 organisationsByUser from "./organisation/by-user";
import organisationCreate from "./organisation/create"; import organisationCreate from "./organisation/create";
import organisationDelete from "./organisation/delete"; import organisationDelete from "./organisation/delete";
import organisationMemberTimeTracking from "./organisation/member-time-tracking";
import organisationMembers from "./organisation/members"; import organisationMembers from "./organisation/members";
import organisationRemoveMember from "./organisation/remove-member"; import organisationRemoveMember from "./organisation/remove-member";
import organisationUpdate from "./organisation/update"; import organisationUpdate from "./organisation/update";
@@ -37,6 +38,10 @@ import sprintCreate from "./sprint/create";
import sprintDelete from "./sprint/delete"; import sprintDelete from "./sprint/delete";
import sprintUpdate from "./sprint/update"; import sprintUpdate from "./sprint/update";
import sprintsByProject from "./sprints/by-project"; 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 timerEnd from "./timer/end";
import timerGet from "./timer/get"; import timerGet from "./timer/get";
import timerGetInactive from "./timer/get-inactive"; import timerGetInactive from "./timer/get-inactive";
@@ -77,6 +82,7 @@ export const routes = {
organisationUpdate, organisationUpdate,
organisationDelete, organisationDelete,
organisationAddMember, organisationAddMember,
organisationMemberTimeTracking,
organisationMembers, organisationMembers,
organisationRemoveMember, organisationRemoveMember,
organisationUpdateMemberRole, organisationUpdateMemberRole,
@@ -104,4 +110,9 @@ export const routes = {
timerGetInactive, timerGetInactive,
timerEnd, timerEnd,
timers, 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);
}

View File

@@ -227,6 +227,13 @@ export const OrgMembersQuerySchema = z.object({
export type OrgMembersQuery = z.infer<typeof OrgMembersQuerySchema>; export type OrgMembersQuery = z.infer<typeof OrgMembersQuerySchema>;
export const OrgMemberTimeTrackingQuerySchema = z.object({
organisationId: z.coerce.number().int().positive("organisationId must be a positive integer"),
fromDate: z.coerce.date().optional(),
});
export type OrgMemberTimeTrackingQuery = z.infer<typeof OrgMemberTimeTrackingQuerySchema>;
export const OrgAddMemberRequestSchema = z.object({ export const OrgAddMemberRequestSchema = z.object({
organisationId: z.number().int().positive("organisationId must be a positive integer"), organisationId: z.number().int().positive("organisationId must be a positive integer"),
userId: z.number().int().positive("userId must be a positive integer"), userId: z.number().int().positive("userId must be a positive integer"),

View File

@@ -29,6 +29,7 @@ import {
OrgCreateRequestSchema, OrgCreateRequestSchema,
OrgDeleteRequestSchema, OrgDeleteRequestSchema,
OrgMembersQuerySchema, OrgMembersQuerySchema,
OrgMemberTimeTrackingQuerySchema,
OrgRemoveMemberRequestSchema, OrgRemoveMemberRequestSchema,
OrgUpdateMemberRoleRequestSchema, OrgUpdateMemberRoleRequestSchema,
OrgUpdateRequestSchema, OrgUpdateRequestSchema,
@@ -379,6 +380,30 @@ export const apiContract = c.router({
200: z.array(OrganisationMemberResponseSchema), 200: z.array(OrganisationMemberResponseSchema),
}, },
}, },
organisationMemberTimeTracking: {
method: "GET",
path: "/organisation/member-time-tracking",
query: OrgMemberTimeTrackingQuerySchema,
responses: {
200: z.array(
z.object({
id: z.number(),
userId: z.number(),
issueId: z.number(),
issueNumber: z.number(),
projectKey: z.string(),
timestamps: z.array(z.string()),
endedAt: z.string().nullable(),
createdAt: z.string().nullable(),
workTimeMs: z.number(),
breakTimeMs: z.number(),
isRunning: z.boolean(),
}),
),
403: ApiErrorSchema,
404: ApiErrorSchema,
},
},
organisationRemoveMember: { organisationRemoveMember: {
method: "POST", method: "POST",
path: "/organisation/remove-member", path: "/organisation/remove-member",

View File

@@ -25,6 +25,7 @@ export type {
OrgCreateRequest, OrgCreateRequest,
OrgDeleteRequest, OrgDeleteRequest,
OrgMembersQuery, OrgMembersQuery,
OrgMemberTimeTrackingQuery,
OrgRemoveMemberRequest, OrgRemoveMemberRequest,
OrgUpdateMemberRoleRequest, OrgUpdateMemberRoleRequest,
OrgUpdateRequest, OrgUpdateRequest,
@@ -87,6 +88,7 @@ export {
OrgCreateRequestSchema, OrgCreateRequestSchema,
OrgDeleteRequestSchema, OrgDeleteRequestSchema,
OrgMembersQuerySchema, OrgMembersQuerySchema,
OrgMemberTimeTrackingQuerySchema,
OrgRemoveMemberRequestSchema, OrgRemoveMemberRequestSchema,
OrgUpdateMemberRoleRequestSchema, OrgUpdateMemberRoleRequestSchema,
OrgUpdateRequestSchema, OrgUpdateRequestSchema,
@@ -153,6 +155,8 @@ export type {
OrganisationMemberResponse as OrganisationMemberResponseRecord, OrganisationMemberResponse as OrganisationMemberResponseRecord,
OrganisationRecord, OrganisationRecord,
OrganisationResponse as OrganisationResponseRecord, OrganisationResponse as OrganisationResponseRecord,
PaymentInsert,
PaymentRecord,
ProjectInsert, ProjectInsert,
ProjectRecord, ProjectRecord,
ProjectResponse as ProjectResponseRecord, ProjectResponse as ProjectResponseRecord,
@@ -160,6 +164,8 @@ export type {
SessionRecord, SessionRecord,
SprintInsert, SprintInsert,
SprintRecord, SprintRecord,
SubscriptionInsert,
SubscriptionRecord,
TimedSessionInsert, TimedSessionInsert,
TimedSessionRecord, TimedSessionRecord,
TimerState, TimerState,
@@ -188,6 +194,9 @@ export {
OrganisationMemberInsertSchema, OrganisationMemberInsertSchema,
OrganisationMemberSelectSchema, OrganisationMemberSelectSchema,
OrganisationSelectSchema, OrganisationSelectSchema,
Payment,
PaymentInsertSchema,
PaymentSelectSchema,
Project, Project,
ProjectInsertSchema, ProjectInsertSchema,
ProjectSelectSchema, ProjectSelectSchema,
@@ -197,6 +206,9 @@ export {
Sprint, Sprint,
SprintInsertSchema, SprintInsertSchema,
SprintSelectSchema, SprintSelectSchema,
Subscription,
SubscriptionInsertSchema,
SubscriptionSelectSchema,
TimedSession, TimedSession,
TimedSessionInsertSchema, TimedSessionInsertSchema,
TimedSessionSelectSchema, TimedSessionSelectSchema,