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,