mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
added member-time-tracking route
This commit is contained in:
@@ -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),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -227,6 +227,13 @@ export const OrgMembersQuerySchema = z.object({
|
||||
|
||||
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({
|
||||
organisationId: z.number().int().positive("organisationId must be a positive integer"),
|
||||
userId: z.number().int().positive("userId must be a positive integer"),
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
OrgCreateRequestSchema,
|
||||
OrgDeleteRequestSchema,
|
||||
OrgMembersQuerySchema,
|
||||
OrgMemberTimeTrackingQuerySchema,
|
||||
OrgRemoveMemberRequestSchema,
|
||||
OrgUpdateMemberRoleRequestSchema,
|
||||
OrgUpdateRequestSchema,
|
||||
@@ -379,6 +380,30 @@ export const apiContract = c.router({
|
||||
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: {
|
||||
method: "POST",
|
||||
path: "/organisation/remove-member",
|
||||
|
||||
@@ -25,6 +25,7 @@ export type {
|
||||
OrgCreateRequest,
|
||||
OrgDeleteRequest,
|
||||
OrgMembersQuery,
|
||||
OrgMemberTimeTrackingQuery,
|
||||
OrgRemoveMemberRequest,
|
||||
OrgUpdateMemberRoleRequest,
|
||||
OrgUpdateRequest,
|
||||
@@ -87,6 +88,7 @@ export {
|
||||
OrgCreateRequestSchema,
|
||||
OrgDeleteRequestSchema,
|
||||
OrgMembersQuerySchema,
|
||||
OrgMemberTimeTrackingQuerySchema,
|
||||
OrgRemoveMemberRequestSchema,
|
||||
OrgUpdateMemberRoleRequestSchema,
|
||||
OrgUpdateRequestSchema,
|
||||
@@ -153,6 +155,8 @@ export type {
|
||||
OrganisationMemberResponse as OrganisationMemberResponseRecord,
|
||||
OrganisationRecord,
|
||||
OrganisationResponse as OrganisationResponseRecord,
|
||||
PaymentInsert,
|
||||
PaymentRecord,
|
||||
ProjectInsert,
|
||||
ProjectRecord,
|
||||
ProjectResponse as ProjectResponseRecord,
|
||||
@@ -160,6 +164,8 @@ export type {
|
||||
SessionRecord,
|
||||
SprintInsert,
|
||||
SprintRecord,
|
||||
SubscriptionInsert,
|
||||
SubscriptionRecord,
|
||||
TimedSessionInsert,
|
||||
TimedSessionRecord,
|
||||
TimerState,
|
||||
@@ -188,6 +194,9 @@ export {
|
||||
OrganisationMemberInsertSchema,
|
||||
OrganisationMemberSelectSchema,
|
||||
OrganisationSelectSchema,
|
||||
Payment,
|
||||
PaymentInsertSchema,
|
||||
PaymentSelectSchema,
|
||||
Project,
|
||||
ProjectInsertSchema,
|
||||
ProjectSelectSchema,
|
||||
@@ -197,6 +206,9 @@ export {
|
||||
Sprint,
|
||||
SprintInsertSchema,
|
||||
SprintSelectSchema,
|
||||
Subscription,
|
||||
SubscriptionInsertSchema,
|
||||
SubscriptionSelectSchema,
|
||||
TimedSession,
|
||||
TimedSessionInsertSchema,
|
||||
TimedSessionSelectSchema,
|
||||
|
||||
Reference in New Issue
Block a user