diff --git a/packages/backend/src/db/queries/index.ts b/packages/backend/src/db/queries/index.ts index 99988cb..8b5b9e2 100644 --- a/packages/backend/src/db/queries/index.ts +++ b/packages/backend/src/db/queries/index.ts @@ -4,5 +4,6 @@ export * from "./organisations"; export * from "./projects"; export * from "./sessions"; export * from "./sprints"; +export * from "./subscriptions"; export * from "./timed-sessions"; export * from "./users"; diff --git a/packages/backend/src/routes/organisation/member-time-tracking.ts b/packages/backend/src/routes/organisation/member-time-tracking.ts index 410376d..c5813ba 100644 --- a/packages/backend/src/routes/organisation/member-time-tracking.ts +++ b/packages/backend/src/routes/organisation/member-time-tracking.ts @@ -21,13 +21,12 @@ export default async function organisationMemberTimeTracking(req: AuthedRequest) const { organisationId, fromDate } = parsed.data; - // Check organisation exists + // 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); @@ -38,11 +37,8 @@ export default async function organisationMemberTimeTracking(req: AuthedRequest) 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 { @@ -51,7 +47,7 @@ export default async function organisationMemberTimeTracking(req: AuthedRequest) issueId: session.issueId, issueNumber: session.issueNumber, projectKey: session.projectKey, - timestamps: session.timestamps, // Return original strings for JSON serialization + timestamps: session.timestamps, endedAt: session.endedAt, createdAt: session.createdAt, workTimeMs: calculateWorkTimeMs(timestamps), diff --git a/packages/frontend/src/components/organisations.tsx b/packages/frontend/src/components/organisations.tsx index 6eda4e7..fac2d29 100644 --- a/packages/frontend/src/components/organisations.tsx +++ b/packages/frontend/src/components/organisations.tsx @@ -126,42 +126,33 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { }); }, [membersData]); - // Calculate total time per member and sort by time (greatest to smallest) const membersWithTimeTracking = useMemo(() => { - // Calculate total time per user const timePerUser = new Map(); for (const session of timeTrackingData) { const current = timePerUser.get(session.userId) ?? 0; timePerUser.set(session.userId, current + (session.workTimeMs ?? 0)); } - // Map members with their total time const membersWithTime = members.map((member) => ({ ...member, totalTimeMs: timePerUser.get(member.User.id) ?? 0, })); - // Sort by total time (greatest to smallest), then by role, then by name const roleOrder: Record = { owner: 0, admin: 1, member: 2 }; return membersWithTime.sort((a, b) => { - // First sort by total time (descending) if (b.totalTimeMs !== a.totalTimeMs) { return b.totalTimeMs - a.totalTimeMs; } - // Then by role const roleA = roleOrder[a.OrganisationMember.role] ?? 3; const roleB = roleOrder[b.OrganisationMember.role] ?? 3; if (roleA !== roleB) return roleA - roleB; - // Finally by name return a.User.name.localeCompare(b.User.name); }); }, [members, timeTrackingData]); - // Download time tracking data as CSV or JSON const downloadTimeTrackingData = (format: "csv" | "json") => { if (!selectedOrganisation) return; - // Aggregate data per user const userData = new Map< number, { @@ -193,8 +184,8 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { const data = Array.from(userData.values()).sort((a, b) => b.totalTimeMs - a.totalTimeMs); + // generate CSV or JSON if (format === "csv") { - // Generate CSV const headers = ["User ID", "Name", "Username", "Total Time (ms)", "Total Time (formatted)"]; const rows = data.map((user) => [ user.userId, @@ -207,6 +198,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { "\n", ); + // download const blob = new Blob([csv], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); @@ -217,7 +209,6 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { document.body.removeChild(a); URL.revokeObjectURL(url); } else { - // Generate JSON const json = JSON.stringify( { organisation: selectedOrganisation.Organisation.name, @@ -232,6 +223,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { 2, ); + // download const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); @@ -894,7 +886,6 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { @@ -903,24 +894,21 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { mode="single" selected={fromDate} onSelect={(date) => date && setFromDate(date)} - initialFocus + autoFocus /> downloadTimeTrackingData("csv")}> - Download CSV downloadTimeTrackingData("json")}> - Download JSON diff --git a/packages/shared/src/api-schemas.ts b/packages/shared/src/api-schemas.ts index 029a7cf..e6ac4d0 100644 --- a/packages/shared/src/api-schemas.ts +++ b/packages/shared/src/api-schemas.ts @@ -419,6 +419,7 @@ export const UserResponseSchema = z.object({ username: z.string(), avatarURL: z.string().nullable(), iconPreference: z.enum(["lucide", "pixel", "phosphor"]), + plan: z.string().nullable().optional(), createdAt: z.string().nullable().optional(), updatedAt: z.string().nullable().optional(), });