@@ -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) {
381384export 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+
922948async 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