Skip to content

Commit 832e6fb

Browse files
committed
FIX - shipped hours overlap bug
1 parent aa57911 commit 832e6fb

2 files changed

Lines changed: 65 additions & 14 deletions

File tree

client/src/components/AdminReviewPage.jsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ function slackJoeToolUrl(slackId) {
2222
return `https://joe.fraud.hackclub.com/profile/${encodeURIComponent(id)}`;
2323
}
2424

25+
function clampApprovalHours(value, maxHours) {
26+
if (value === "") return "";
27+
const numeric = Number.parseFloat(value);
28+
if (!Number.isFinite(numeric)) return value;
29+
if (numeric < 0) return "0";
30+
if (Number.isFinite(maxHours) && numeric > maxHours) return maxHours.toFixed(2);
31+
return value;
32+
}
33+
2534
export function AdminReviewPage({ projectId }) {
2635
if (projectId) return <AdminReviewDetail projectId={projectId} />;
2736
return <AdminReviewIndex />;
@@ -114,6 +123,7 @@ function AdminReviewIndex() {
114123
<option value="any">Any status</option>
115124
<option value="draft">Draft</option>
116125
<option value="in-review">In review</option>
126+
<option value="pending-reship">Pending reship</option>
117127
<option value="approved">Approved</option>
118128
<option value="rejected">Rejected</option>
119129
</select>
@@ -232,6 +242,13 @@ function AdminReviewDetail({ projectId }) {
232242
return;
233243
}
234244

245+
const newHoursMax = Number(project.pendingReviewHours ?? 0);
246+
const requestedApprovedHours = Number.parseFloat(approvedHours) || 0;
247+
if (selectedAction === "approve" && requestedApprovedHours > newHoursMax) {
248+
setMessage(`Approval cannot exceed ${formatHours(newHoursMax)} new hours.`);
249+
return;
250+
}
251+
235252
const endpoint =
236253
selectedAction === "reject"
237254
? `/api/admin/review/projects/${project.id}/reject`
@@ -303,6 +320,8 @@ function AdminReviewDetail({ projectId }) {
303320
const isAlreadyReviewed = project.reviewed || project.status === "approved";
304321
const joeUrl = slackJoeToolUrl(project.user?.slackId);
305322
const isSuperadmin = user?.role === "superadmin";
323+
const newHoursMax = Number(project.pendingReviewHours ?? 0);
324+
const newHoursPlaceholder = formatHours(newHoursMax);
306325

307326
return (
308327
<main className="admin-review-page">
@@ -384,16 +403,22 @@ function AdminReviewDetail({ projectId }) {
384403
<span>Previously banked</span>
385404
<strong>{formatHours(project.pastApprovedHours)} h</strong>
386405
</div>
406+
<div>
407+
<span>New hours</span>
408+
<strong>{newHoursPlaceholder} h</strong>
409+
</div>
387410
<div>
388411
<span>New hours to approve</span>
389412
<input
390413
className="admin-review-hours-input"
391414
type="number"
392415
min="0"
416+
max={newHoursPlaceholder}
393417
step="0.25"
418+
placeholder={newHoursPlaceholder}
394419
value={approvedHours}
395420
disabled={isAlreadyReviewed}
396-
onChange={(event) => setApprovedHours(event.target.value)}
421+
onChange={(event) => setApprovedHours(clampApprovalHours(event.target.value, newHoursMax))}
397422
/>
398423
</div>
399424
<div>

server/projects.js

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,7 @@ export async function shipProjectForUser(userId, projectId) {
254254

255255
const isReship = project.status === "approved" && project.reviewed;
256256
if (isReship) {
257-
const totalLoggedHours = Number(project.total_hours ?? 0) + Number(project.hackatime_hours ?? 0);
258-
const bankedHours = Number(project.past_approved_hours ?? 0);
259-
const newHours = totalLoggedHours - bankedHours;
257+
const newHours = pendingReviewHours(project);
260258
if (newHours <= 0) {
261259
throw new Error("No new hours to ship. Add more hours before re-shipping.");
262260
}
@@ -343,7 +341,12 @@ export async function listAdminReviewProjects({ shipSort = "oldest" } = {}) {
343341
const projects = result.rows.map(toAdminReviewProject);
344342
return {
345343
projects,
346-
pendingProjects: projects.filter((project) => project.shipped && project.status === "in-review" && !project.reviewed),
344+
pendingProjects: projects.filter(
345+
(project) =>
346+
project.shipped &&
347+
(project.status === "in-review" || project.status === "pending-reship") &&
348+
!project.reviewed
349+
),
347350
};
348351
}
349352

@@ -381,11 +384,12 @@ export async function getAdminReviewProject(projectId) {
381384
export async function approveAdminReviewProject(adminId, projectId, input = {}) {
382385
if (!pool) throw new Error("DATABASE_URL is not set.");
383386

384-
const approvedHours = Number.parseFloat(input.approvedHours ?? input.approved_hours ?? 0);
387+
const requestedApprovedHours = Number.parseFloat(input.approvedHours ?? input.approved_hours ?? 0);
388+
const approvedHours = roundedHours(requestedApprovedHours);
385389
const feedback = textOrNull(input.feedback);
386390
const hourJustification = textOrNull(input.hourJustification ?? input.hour_justification);
387391

388-
if (!Number.isFinite(approvedHours) || approvedHours <= 0) {
392+
if (!Number.isFinite(requestedApprovedHours) || approvedHours <= 0) {
389393
throw new Error("Enter a positive number of new hours to approve for this submission.");
390394
}
391395

@@ -400,16 +404,20 @@ export async function approveAdminReviewProject(adminId, projectId, input = {})
400404
if (!project.shipped) throw new Error("Project is not in the review queue.");
401405
if (project.reviewed && project.status === "approved") throw new Error("Project is already approved.");
402406

403-
const bankedHours = Number(project.past_approved_hours ?? 0);
404-
const totalLoggedHours = Number(project.total_hours ?? 0) + Number(project.hackatime_hours ?? 0);
405-
const pendingCap = Math.max(0, totalLoggedHours - bankedHours);
406-
if (approvedHours > pendingCap + 0.02) {
407+
const bankedHours = roundedHours(project.past_approved_hours);
408+
const totalLoggedHours = loggedHours(project);
409+
const pendingCap = pendingReviewHours(project);
410+
if (approvedHours > pendingCap) {
407411
throw new Error(`Cannot approve more new hours than the participant has logged beyond prior approvals (${pendingCap.toFixed(2)} h max).`);
408412
}
409413

410-
const newTotalApprovedHours = Number((bankedHours + approvedHours).toFixed(2));
414+
const newTotalApprovedHours = roundedHours(bankedHours + approvedHours);
415+
if (newTotalApprovedHours > totalLoggedHours) {
416+
throw new Error(`Cannot approve ${newTotalApprovedHours.toFixed(2)} total hours because only ${totalLoggedHours.toFixed(2)} hours are logged.`);
417+
}
418+
411419
const bricksDelta = Math.ceil(approvedHours * 10);
412-
const reductionHours = Math.max(0, Number((totalLoggedHours - newTotalApprovedHours).toFixed(2)));
420+
const reductionHours = Math.max(0, roundedHours(totalLoggedHours - newTotalApprovedHours));
413421
const newCumulativeBricks = Number(project.bricks_earned ?? 0) + bricksDelta;
414422

415423
const reviewerResult = await client.query("SELECT name, email FROM users WHERE id = $1", [adminId]);
@@ -919,6 +927,24 @@ function textOrNull(value) {
919927
return text ? text : null;
920928
}
921929

930+
function numberOrZero(value) {
931+
const numeric = Number(value);
932+
return Number.isFinite(numeric) ? numeric : 0;
933+
}
934+
935+
function roundedHours(value) {
936+
const numeric = Number(value);
937+
return Number.isFinite(numeric) ? Number(numeric.toFixed(2)) : 0;
938+
}
939+
940+
function loggedHours(row) {
941+
return roundedHours(numberOrZero(row.journal_hours ?? row.total_hours) + numberOrZero(row.hackatime_hours));
942+
}
943+
944+
function pendingReviewHours(row) {
945+
return Math.max(0, roundedHours(loggedHours(row) - numberOrZero(row.past_approved_hours)));
946+
}
947+
922948
async function getProjectRowForAirtableSync(projectId) {
923949
if (!pool) return null;
924950

@@ -1036,7 +1062,7 @@ function toAdminReviewProject(row) {
10361062
profileImageUrl: row.user_profile_image_url,
10371063
slackId: row.user_slack_id,
10381064
},
1039-
pendingReviewHours: Math.max(0, Number(row.total_hours ?? 0) - Number(row.past_approved_hours ?? 0)),
1065+
pendingReviewHours: pendingReviewHours(row),
10401066
};
10411067
}
10421068

0 commit comments

Comments
 (0)