mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 10:33:01 +00:00
more stripe changes on frontend
This commit is contained in:
@@ -4,5 +4,6 @@ export * from "./organisations";
|
|||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
export * from "./sessions";
|
export * from "./sessions";
|
||||||
export * from "./sprints";
|
export * from "./sprints";
|
||||||
|
export * from "./subscriptions";
|
||||||
export * from "./timed-sessions";
|
export * from "./timed-sessions";
|
||||||
export * from "./users";
|
export * from "./users";
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ export default async function organisationMemberTimeTracking(req: AuthedRequest)
|
|||||||
|
|
||||||
const { organisationId, fromDate } = parsed.data;
|
const { organisationId, fromDate } = parsed.data;
|
||||||
|
|
||||||
// Check organisation exists
|
// check organisation exists
|
||||||
const organisation = await getOrganisationById(organisationId);
|
const organisation = await getOrganisationById(organisationId);
|
||||||
if (!organisation) {
|
if (!organisation) {
|
||||||
return errorResponse(`organisation with id ${organisationId} not found`, "ORG_NOT_FOUND", 404);
|
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);
|
const memberRole = await getOrganisationMemberRole(organisationId, req.userId);
|
||||||
if (!memberRole) {
|
if (!memberRole) {
|
||||||
return errorResponse("you are not a member of this organisation", "NOT_MEMBER", 403);
|
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);
|
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);
|
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 enriched = sessions.map((session) => {
|
||||||
const timestamps = session.timestamps.map((t) => new Date(t));
|
const timestamps = session.timestamps.map((t) => new Date(t));
|
||||||
return {
|
return {
|
||||||
@@ -51,7 +47,7 @@ export default async function organisationMemberTimeTracking(req: AuthedRequest)
|
|||||||
issueId: session.issueId,
|
issueId: session.issueId,
|
||||||
issueNumber: session.issueNumber,
|
issueNumber: session.issueNumber,
|
||||||
projectKey: session.projectKey,
|
projectKey: session.projectKey,
|
||||||
timestamps: session.timestamps, // Return original strings for JSON serialization
|
timestamps: session.timestamps,
|
||||||
endedAt: session.endedAt,
|
endedAt: session.endedAt,
|
||||||
createdAt: session.createdAt,
|
createdAt: session.createdAt,
|
||||||
workTimeMs: calculateWorkTimeMs(timestamps),
|
workTimeMs: calculateWorkTimeMs(timestamps),
|
||||||
|
|||||||
@@ -126,42 +126,33 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
});
|
});
|
||||||
}, [membersData]);
|
}, [membersData]);
|
||||||
|
|
||||||
// Calculate total time per member and sort by time (greatest to smallest)
|
|
||||||
const membersWithTimeTracking = useMemo(() => {
|
const membersWithTimeTracking = useMemo(() => {
|
||||||
// Calculate total time per user
|
|
||||||
const timePerUser = new Map<number, number>();
|
const timePerUser = new Map<number, number>();
|
||||||
for (const session of timeTrackingData) {
|
for (const session of timeTrackingData) {
|
||||||
const current = timePerUser.get(session.userId) ?? 0;
|
const current = timePerUser.get(session.userId) ?? 0;
|
||||||
timePerUser.set(session.userId, current + (session.workTimeMs ?? 0));
|
timePerUser.set(session.userId, current + (session.workTimeMs ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map members with their total time
|
|
||||||
const membersWithTime = members.map((member) => ({
|
const membersWithTime = members.map((member) => ({
|
||||||
...member,
|
...member,
|
||||||
totalTimeMs: timePerUser.get(member.User.id) ?? 0,
|
totalTimeMs: timePerUser.get(member.User.id) ?? 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Sort by total time (greatest to smallest), then by role, then by name
|
|
||||||
const roleOrder: Record<string, number> = { owner: 0, admin: 1, member: 2 };
|
const roleOrder: Record<string, number> = { owner: 0, admin: 1, member: 2 };
|
||||||
return membersWithTime.sort((a, b) => {
|
return membersWithTime.sort((a, b) => {
|
||||||
// First sort by total time (descending)
|
|
||||||
if (b.totalTimeMs !== a.totalTimeMs) {
|
if (b.totalTimeMs !== a.totalTimeMs) {
|
||||||
return b.totalTimeMs - a.totalTimeMs;
|
return b.totalTimeMs - a.totalTimeMs;
|
||||||
}
|
}
|
||||||
// Then by role
|
|
||||||
const roleA = roleOrder[a.OrganisationMember.role] ?? 3;
|
const roleA = roleOrder[a.OrganisationMember.role] ?? 3;
|
||||||
const roleB = roleOrder[b.OrganisationMember.role] ?? 3;
|
const roleB = roleOrder[b.OrganisationMember.role] ?? 3;
|
||||||
if (roleA !== roleB) return roleA - roleB;
|
if (roleA !== roleB) return roleA - roleB;
|
||||||
// Finally by name
|
|
||||||
return a.User.name.localeCompare(b.User.name);
|
return a.User.name.localeCompare(b.User.name);
|
||||||
});
|
});
|
||||||
}, [members, timeTrackingData]);
|
}, [members, timeTrackingData]);
|
||||||
|
|
||||||
// Download time tracking data as CSV or JSON
|
|
||||||
const downloadTimeTrackingData = (format: "csv" | "json") => {
|
const downloadTimeTrackingData = (format: "csv" | "json") => {
|
||||||
if (!selectedOrganisation) return;
|
if (!selectedOrganisation) return;
|
||||||
|
|
||||||
// Aggregate data per user
|
|
||||||
const userData = new Map<
|
const userData = new Map<
|
||||||
number,
|
number,
|
||||||
{
|
{
|
||||||
@@ -193,8 +184,8 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
|
|
||||||
const data = Array.from(userData.values()).sort((a, b) => b.totalTimeMs - a.totalTimeMs);
|
const data = Array.from(userData.values()).sort((a, b) => b.totalTimeMs - a.totalTimeMs);
|
||||||
|
|
||||||
|
// generate CSV or JSON
|
||||||
if (format === "csv") {
|
if (format === "csv") {
|
||||||
// Generate CSV
|
|
||||||
const headers = ["User ID", "Name", "Username", "Total Time (ms)", "Total Time (formatted)"];
|
const headers = ["User ID", "Name", "Username", "Total Time (ms)", "Total Time (formatted)"];
|
||||||
const rows = data.map((user) => [
|
const rows = data.map((user) => [
|
||||||
user.userId,
|
user.userId,
|
||||||
@@ -207,6 +198,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
"\n",
|
"\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// download
|
||||||
const blob = new Blob([csv], { type: "text/csv" });
|
const blob = new Blob([csv], { type: "text/csv" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
@@ -217,7 +209,6 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} else {
|
} else {
|
||||||
// Generate JSON
|
|
||||||
const json = JSON.stringify(
|
const json = JSON.stringify(
|
||||||
{
|
{
|
||||||
organisation: selectedOrganisation.Organisation.name,
|
organisation: selectedOrganisation.Organisation.name,
|
||||||
@@ -232,6 +223,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// download
|
||||||
const blob = new Blob([json], { type: "application/json" });
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
@@ -894,7 +886,6 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
<Icon icon="calendar" className="size-4 mr-1" />
|
|
||||||
From: {fromDate.toLocaleDateString()}
|
From: {fromDate.toLocaleDateString()}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -903,24 +894,21 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
mode="single"
|
mode="single"
|
||||||
selected={fromDate}
|
selected={fromDate}
|
||||||
onSelect={(date) => date && setFromDate(date)}
|
onSelect={(date) => date && setFromDate(date)}
|
||||||
initialFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
<Icon icon="download" className="size-4 mr-1" />
|
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onSelect={() => downloadTimeTrackingData("csv")}>
|
<DropdownMenuItem onSelect={() => downloadTimeTrackingData("csv")}>
|
||||||
<Icon icon="fileSpreadsheet" className="size-4 mr-2" />
|
|
||||||
Download CSV
|
Download CSV
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onSelect={() => downloadTimeTrackingData("json")}>
|
<DropdownMenuItem onSelect={() => downloadTimeTrackingData("json")}>
|
||||||
<Icon icon="fileJson" className="size-4 mr-2" />
|
|
||||||
Download JSON
|
Download JSON
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -419,6 +419,7 @@ export const UserResponseSchema = z.object({
|
|||||||
username: z.string(),
|
username: z.string(),
|
||||||
avatarURL: z.string().nullable(),
|
avatarURL: z.string().nullable(),
|
||||||
iconPreference: z.enum(["lucide", "pixel", "phosphor"]),
|
iconPreference: z.enum(["lucide", "pixel", "phosphor"]),
|
||||||
|
plan: z.string().nullable().optional(),
|
||||||
createdAt: z.string().nullable().optional(),
|
createdAt: z.string().nullable().optional(),
|
||||||
updatedAt: z.string().nullable().optional(),
|
updatedAt: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user