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/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),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 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"),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user