diff --git a/client/src/components/ProjectsPage.jsx b/client/src/components/ProjectsPage.jsx index 9274b64..e94b600 100644 --- a/client/src/components/ProjectsPage.jsx +++ b/client/src/components/ProjectsPage.jsx @@ -117,6 +117,22 @@ function getShipLockReason(project) { return missing.length ? `Locked: ${missing.join(", ")}.` : ""; } +function formatHours(value) { + return Number(value ?? 0).toFixed(2); +} + +function getLoggedHours(project) { + return Number(project.combinedHours ?? project.totalHours ?? project.journalHours ?? 0); +} + +function getApprovedBankedHours(project) { + return Math.max(Number(project.approvedHours ?? 0), Number(project.pastApprovedHours ?? 0)); +} + +function getUnshippedHours(project) { + return Math.max(0, Number((getLoggedHours(project) - getApprovedBankedHours(project)).toFixed(2))); +} + export function ProjectsPage() { const { user } = useAuth(); const [projects, setProjects] = useState([]); @@ -490,6 +506,7 @@ export function ProjectsPage() { function ProjectDetailsModal({ project, onClose, onEdit, onJournal, onShip, onDelete }) { const shipLockReason = getShipLockReason(project); + const unshippedHours = getUnshippedHours(project); return (
@@ -518,19 +535,23 @@ function ProjectDetailsModal({ project, onClose, onEdit, onJournal, onShip, onDe
Hours logged (combined) - {project.combinedHours ?? project.totalHours ?? 0} + {formatHours(getLoggedHours(project))}
Hours approved - {project.approvedHours || 0} + {formatHours(getApprovedBankedHours(project))} +
+
+ Unshipped hours + {formatHours(unshippedHours)}
Journal hours - {project.journalHours || 0} + {formatHours(project.journalHours)}
Hackatime hours - {project.hackatimeHours || 0} + {formatHours(project.hackatimeHours)}
Hackatime projects diff --git a/server/airtableYsws.js b/server/airtableYsws.js index 2d6e0f6..df75edd 100644 --- a/server/airtableYsws.js +++ b/server/airtableYsws.js @@ -129,8 +129,12 @@ function totalLoggedHours(project) { return roundedHours(numberOrZero(project.total_hours) + numberOrZero(project.hackatime_hours)); } +function approvedBankedHours(project) { + return roundedHours(Math.max(numberOrZero(project.past_approved_hours), numberOrZero(project.approved_hours))); +} + function pendingHours(project) { - return Math.max(0, roundedHours(totalLoggedHours(project) - numberOrZero(project.past_approved_hours))); + return Math.max(0, roundedHours(totalLoggedHours(project) - approvedBankedHours(project))); } function yswsStatusForProject(project) { diff --git a/server/projects.js b/server/projects.js index d33b836..05a50f3 100644 --- a/server/projects.js +++ b/server/projects.js @@ -86,6 +86,11 @@ export async function ensureProjectsTable() { await pool.query("ALTER TABLE projects DROP COLUMN IF EXISTS github_username"); await pool.query(`UPDATE projects SET reviewed = FALSE WHERE reviewed IS NULL`); await pool.query(`UPDATE projects SET past_approved_hours = COALESCE(approved_hours, 0) WHERE past_approved_hours IS NULL`); + await pool.query(` + UPDATE projects + SET past_approved_hours = GREATEST(COALESCE(past_approved_hours, 0), COALESCE(approved_hours, 0)) + WHERE COALESCE(past_approved_hours, 0) < COALESCE(approved_hours, 0) + `); await pool.query(`UPDATE projects SET bricks_earned = 0 WHERE bricks_earned IS NULL`); await pool.query(`UPDATE projects SET fraud_flag = FALSE WHERE fraud_flag IS NULL`); await pool.query(`UPDATE projects SET ship_kind = 'initial' WHERE ship_kind IS NULL`); @@ -404,7 +409,7 @@ export async function approveAdminReviewProject(adminId, projectId, input = {}) if (!project.shipped) throw new Error("Project is not in the review queue."); if (project.reviewed && project.status === "approved") throw new Error("Project is already approved."); - const bankedHours = roundedHours(project.past_approved_hours); + const bankedHours = approvedBankedHours(project); const totalLoggedHours = loggedHours(project); const pendingCap = pendingReviewHours(project); if (approvedHours > pendingCap) { @@ -941,8 +946,12 @@ function loggedHours(row) { return roundedHours(numberOrZero(row.journal_hours ?? row.total_hours) + numberOrZero(row.hackatime_hours)); } +function approvedBankedHours(row) { + return roundedHours(Math.max(numberOrZero(row.past_approved_hours), numberOrZero(row.approved_hours))); +} + function pendingReviewHours(row) { - return Math.max(0, roundedHours(loggedHours(row) - numberOrZero(row.past_approved_hours))); + return Math.max(0, roundedHours(loggedHours(row) - approvedBankedHours(row))); } async function getProjectRowForAirtableSync(projectId) { @@ -1034,7 +1043,7 @@ function toPublicProject(row) { combinedHours: Number((journalHours + hackatimeHours).toFixed(2)), baselineHours: row.baseline_hours != null ? Number(row.baseline_hours) : null, approvedHours: Number(row.approved_hours ?? 0), - pastApprovedHours: Number(row.past_approved_hours ?? 0), + pastApprovedHours: approvedBankedHours(row), bricksEarned: Number(row.bricks_earned ?? 0), adminFeedback: row.admin_feedback, hourJustification: row.hour_justification,