diff --git a/packages/web/src/components/checklist/ChecklistYjsWrapper.jsx b/packages/web/src/components/checklist/ChecklistYjsWrapper.jsx index 8984305ee..4ac8f26c1 100644 --- a/packages/web/src/components/checklist/ChecklistYjsWrapper.jsx +++ b/packages/web/src/components/checklist/ChecklistYjsWrapper.jsx @@ -258,8 +258,8 @@ export default function ChecklistYjsWrapper() { const study = currentStudy(); if (!checklist || !study) return; - // If already completed, don't allow toggle back (checklist is locked) - if (checklist.status === CHECKLIST_STATUS.COMPLETED) { + // If already finalized, don't allow toggle back (checklist is locked) + if (checklist.status === CHECKLIST_STATUS.FINALIZED) { showToast.info('Checklist Locked', 'Completed checklists cannot be edited.'); return; } @@ -290,7 +290,7 @@ export default function ChecklistYjsWrapper() { updateChecklist(params.studyId, params.checklistId, { status: nextStatus }); const statusLabel = - nextStatus === CHECKLIST_STATUS.COMPLETED ? 'completed' : 'awaiting reconciliation'; + nextStatus === CHECKLIST_STATUS.FINALIZED ? 'completed' : 'awaiting reconciliation'; showToast.success( 'Checklist Completed', `This checklist has been marked as ${statusLabel} and is now locked.`, @@ -362,12 +362,12 @@ export default function ChecklistYjsWrapper() { fallback={ - {currentChecklist()?.status === CHECKLIST_STATUS.COMPLETED ? + {currentChecklist()?.status === CHECKLIST_STATUS.FINALIZED ? 'Completed' : 'Read-only'} @@ -382,13 +382,13 @@ export default function ChecklistYjsWrapper() { : undefined } class={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${ - currentChecklist()?.status === CHECKLIST_STATUS.COMPLETED ? + currentChecklist()?.status === CHECKLIST_STATUS.FINALIZED ? 'bg-green-100 text-green-700 hover:bg-green-200' : !isChecklistValid() ? 'cursor-not-allowed bg-gray-300 text-gray-500 opacity-60' : 'bg-blue-600 text-white hover:bg-blue-700' }`} > - {currentChecklist()?.status === CHECKLIST_STATUS.COMPLETED ? + {currentChecklist()?.status === CHECKLIST_STATUS.FINALIZED ? 'Completed' : 'Mark Complete'} diff --git a/packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx b/packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx index 15dbf3fd1..192eecb06 100644 --- a/packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx +++ b/packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx @@ -236,9 +236,9 @@ export default function ReconciliationWrapper() { progress.checklist2Id === params.checklist2Id && progress.reconciledChecklistId ) { - // Verify it still exists and is not completed + // Verify it still exists and is not finalized const existingChecklist = study.checklists?.find( - c => c.id === progress.reconciledChecklistId && c.status !== CHECKLIST_STATUS.COMPLETED, + c => c.id === progress.reconciledChecklistId && c.status !== CHECKLIST_STATUS.FINALIZED, ); if (existingChecklist) { setReconciledChecklistId(progress.reconciledChecklistId); @@ -249,7 +249,7 @@ export default function ReconciliationWrapper() { // Check if a reconciled checklist already exists (another client may have created it) const existingReconciled = findReconciledChecklist(study); - if (existingReconciled && existingReconciled.status !== CHECKLIST_STATUS.COMPLETED) { + if (existingReconciled && existingReconciled.status !== CHECKLIST_STATUS.FINALIZED) { // Found existing - save reference in progress and use it saveReconciliationProgress(params.studyId, { checklist1Id: params.checklist1Id, @@ -273,9 +273,9 @@ export default function ReconciliationWrapper() { return; } - // Mark it as in-progress (reconciled checklist starts as in-progress) + // Mark it as reconciling (reconciled checklist starts as reconciling) updateChecklist(params.studyId, newChecklistId, { - status: CHECKLIST_STATUS.IN_PROGRESS, + status: CHECKLIST_STATUS.RECONCILING, title: 'Reconciled Checklist', }); @@ -365,19 +365,14 @@ export default function ReconciliationWrapper() { throw new Error('No reconciled checklist found'); } - // Mark the reconciled checklist as completed + // Mark the reconciled checklist as finalized updateChecklist(params.studyId, id, { - status: CHECKLIST_STATUS.COMPLETED, + status: CHECKLIST_STATUS.FINALIZED, title: reconciledName || 'Reconciled Checklist', }); - // Mark the individual reviewer checklists as completed - updateChecklist(params.studyId, params.checklist1Id, { - status: CHECKLIST_STATUS.COMPLETED, - }); - updateChecklist(params.studyId, params.checklist2Id, { - status: CHECKLIST_STATUS.COMPLETED, - }); + // Individual reviewer checklists remain as REVIEWER_COMPLETED (they were already set when reviewers completed them) + // No need to update them - they're historical records // Keep reconciliation progress (checklist1Id and checklist2Id) so users can view previous reviewers // The progress data is needed for the "View Previous" button in the completed tab diff --git a/packages/web/src/components/project/overview-tab/AMSTAR2ResultsTable.jsx b/packages/web/src/components/project/overview-tab/AMSTAR2ResultsTable.jsx index 1ce6f2c61..90425207d 100644 --- a/packages/web/src/components/project/overview-tab/AMSTAR2ResultsTable.jsx +++ b/packages/web/src/components/project/overview-tab/AMSTAR2ResultsTable.jsx @@ -29,15 +29,15 @@ export default function AMSTAR2ResultsTable(props) { const reconciledChecklist = checklists.find( c => c.id === study.reconciliation.reconciledChecklistId && c.type === 'AMSTAR2', ); - if (reconciledChecklist && reconciledChecklist.status === CHECKLIST_STATUS.COMPLETED) { + if (reconciledChecklist && reconciledChecklist.status === CHECKLIST_STATUS.FINALIZED) { checklistToScore = reconciledChecklist; } } - // If no reconciled checklist, use first completed AMSTAR2 checklist + // If no reconciled checklist, use first finalized AMSTAR2 checklist if (!checklistToScore) { checklistToScore = checklists.find( - c => c.type === 'AMSTAR2' && c.status === CHECKLIST_STATUS.COMPLETED, + c => c.type === 'AMSTAR2' && c.status === CHECKLIST_STATUS.FINALIZED, ); } diff --git a/packages/web/src/components/project/overview-tab/ChartSection.jsx b/packages/web/src/components/project/overview-tab/ChartSection.jsx index 842918d04..c31f2dfbe 100644 --- a/packages/web/src/components/project/overview-tab/ChartSection.jsx +++ b/packages/web/src/components/project/overview-tab/ChartSection.jsx @@ -158,8 +158,8 @@ export default function ChartSection(props) { if (checklists.length === 0) continue; for (const checklist of checklists) { - // Only include completed AMSTAR2 checklists - if (checklist.status !== CHECKLIST_STATUS.COMPLETED) continue; + // Only include finalized AMSTAR2 checklists + if (checklist.status !== CHECKLIST_STATUS.FINALIZED) continue; if (checklist.type !== 'AMSTAR2') continue; // Answers are pre-computed during sync and stored on the checklist diff --git a/packages/web/src/components/project/overview-tab/OverviewTab.jsx b/packages/web/src/components/project/overview-tab/OverviewTab.jsx index d7ee3310d..4960bfd38 100644 --- a/packages/web/src/components/project/overview-tab/OverviewTab.jsx +++ b/packages/web/src/components/project/overview-tab/OverviewTab.jsx @@ -46,9 +46,11 @@ export default function OverviewTab() { const readyToReconcile = () => studies().filter(s => { const checklists = s.checklists || []; - const completedChecklists = checklists.filter(c => c.status === CHECKLIST_STATUS.COMPLETED); + const completedChecklists = checklists.filter( + c => c.status === CHECKLIST_STATUS.REVIEWER_COMPLETED, + ); return completedChecklists.length === 2; - }).length; + }).length / 2; // Divide by 2 because we need to count the number of studies that have both reviewers completed, not the number of checklists const completedStudies = () => studies().filter(s => shouldShowInTab(s, 'completed', null)).length; @@ -84,8 +86,8 @@ export default function OverviewTab() { const userChecklists = checklists.filter(c => c.assignedTo === userId); const hasCompleted = userChecklists.some( c => - c.status === CHECKLIST_STATUS.COMPLETED || - c.status === CHECKLIST_STATUS.AWAITING_RECONCILE, + c.status === CHECKLIST_STATUS.FINALIZED || + c.status === CHECKLIST_STATUS.REVIEWER_COMPLETED, ); if (hasCompleted) { diff --git a/packages/web/src/components/project/reconcile-tab/ReconcileStatusTag.jsx b/packages/web/src/components/project/reconcile-tab/ReconcileStatusTag.jsx index 2439a739e..866b0be7e 100644 --- a/packages/web/src/components/project/reconcile-tab/ReconcileStatusTag.jsx +++ b/packages/web/src/components/project/reconcile-tab/ReconcileStatusTag.jsx @@ -12,7 +12,7 @@ import { isReconciledChecklist } from '@/lib/checklist-domain.js'; export default function ReconcileStatusTag(props) { const awaitingReconcileChecklists = () => (props.study.checklists || []).filter( - c => !isReconciledChecklist(c) && c.status === CHECKLIST_STATUS.AWAITING_RECONCILE, + c => !isReconciledChecklist(c) && c.status === CHECKLIST_STATUS.REVIEWER_COMPLETED, ); const isReady = () => awaitingReconcileChecklists().length === 2; diff --git a/packages/web/src/components/project/reconcile-tab/ReconcileStudyRow.jsx b/packages/web/src/components/project/reconcile-tab/ReconcileStudyRow.jsx index 49f9978e4..0b385c5e1 100644 --- a/packages/web/src/components/project/reconcile-tab/ReconcileStudyRow.jsx +++ b/packages/web/src/components/project/reconcile-tab/ReconcileStudyRow.jsx @@ -52,7 +52,7 @@ export default function ReconcileStudyRow(props) { // Get the individual reviewer checklists awaiting reconciliation const awaitingReconcileChecklists = () => { return (study().checklists || []).filter( - c => !isReconciledChecklist(c) && c.status === CHECKLIST_STATUS.AWAITING_RECONCILE, + c => !isReconciledChecklist(c) && c.status === CHECKLIST_STATUS.REVIEWER_COMPLETED, ); }; diff --git a/packages/web/src/constants/checklist-status.js b/packages/web/src/constants/checklist-status.js index 2e2fdb0d9..72e348c9d 100644 --- a/packages/web/src/constants/checklist-status.js +++ b/packages/web/src/constants/checklist-status.js @@ -8,8 +8,9 @@ export const CHECKLIST_STATUS = { PENDING: 'pending', IN_PROGRESS: 'in-progress', - COMPLETED: 'completed', - AWAITING_RECONCILE: 'awaiting-reconcile', + REVIEWER_COMPLETED: 'reviewer-completed', + RECONCILING: 'reconciling', + FINALIZED: 'finalized', }; /** @@ -18,7 +19,11 @@ export const CHECKLIST_STATUS = { * @returns {boolean} True if the checklist can be edited */ export function isEditable(status) { - return status !== CHECKLIST_STATUS.COMPLETED && status !== CHECKLIST_STATUS.AWAITING_RECONCILE; + return ( + status !== CHECKLIST_STATUS.FINALIZED && + status !== CHECKLIST_STATUS.REVIEWER_COMPLETED && + status !== CHECKLIST_STATUS.RECONCILING + ); } /** @@ -32,10 +37,12 @@ export function getStatusLabel(status) { return 'Pending'; case CHECKLIST_STATUS.IN_PROGRESS: return 'In Progress'; - case CHECKLIST_STATUS.COMPLETED: - return 'Completed'; - case CHECKLIST_STATUS.AWAITING_RECONCILE: - return 'Awaiting Reconcile'; + case CHECKLIST_STATUS.REVIEWER_COMPLETED: + return 'Reviewer Completed'; + case CHECKLIST_STATUS.RECONCILING: + return 'Reconciling'; + case CHECKLIST_STATUS.FINALIZED: + return 'Finalized'; default: return status || 'Pending'; } @@ -48,12 +55,14 @@ export function getStatusLabel(status) { */ export function getStatusStyle(status) { switch (status) { - case CHECKLIST_STATUS.COMPLETED: + case CHECKLIST_STATUS.FINALIZED: return 'bg-green-100 text-green-800'; case CHECKLIST_STATUS.IN_PROGRESS: return 'bg-yellow-100 text-yellow-800'; - case CHECKLIST_STATUS.AWAITING_RECONCILE: + case CHECKLIST_STATUS.REVIEWER_COMPLETED: return 'bg-blue-100 text-blue-800'; + case CHECKLIST_STATUS.RECONCILING: + return 'bg-purple-100 text-purple-800'; case CHECKLIST_STATUS.PENDING: default: return 'bg-gray-100 text-gray-800'; @@ -75,23 +84,23 @@ export function canTransitionTo(currentStatus, newStatus) { return true; } - // Can transition from in-progress to completed or awaiting-reconcile + // Can transition from in-progress to reviewer-completed or finalized if (currentStatus === CHECKLIST_STATUS.IN_PROGRESS) { return ( - newStatus === CHECKLIST_STATUS.COMPLETED || newStatus === CHECKLIST_STATUS.AWAITING_RECONCILE + newStatus === CHECKLIST_STATUS.REVIEWER_COMPLETED || newStatus === CHECKLIST_STATUS.FINALIZED ); } - // Can transition from awaiting-reconcile to completed (after reconciliation) - if ( - currentStatus === CHECKLIST_STATUS.AWAITING_RECONCILE && - newStatus === CHECKLIST_STATUS.COMPLETED - ) { + // Can transition from reconciling to finalized (after reconciliation is complete) + if (currentStatus === CHECKLIST_STATUS.RECONCILING && newStatus === CHECKLIST_STATUS.FINALIZED) { return true; } - // Cannot transition from completed to anything else (locked) - if (currentStatus === CHECKLIST_STATUS.COMPLETED) { + // Cannot transition from finalized or reviewer-completed to anything else (locked) + if ( + currentStatus === CHECKLIST_STATUS.FINALIZED || + currentStatus === CHECKLIST_STATUS.REVIEWER_COMPLETED + ) { return false; } diff --git a/packages/web/src/lib/checklist-domain.js b/packages/web/src/lib/checklist-domain.js index a5709f08a..77c407886 100644 --- a/packages/web/src/lib/checklist-domain.js +++ b/packages/web/src/lib/checklist-domain.js @@ -20,7 +20,7 @@ export function isReconciledChecklist(checklist) { } /** - * Gets checklists for the todo tab (assigned to user, not completed/awaiting-reconcile) + * Gets checklists for the todo tab (assigned to user, not finalized or reviewer-completed) * @param {Object} study - The study object * @param {string} userId - The current user ID * @returns {Array} Array of checklists for todo tab @@ -31,32 +31,51 @@ export function getTodoChecklists(study, userId) { return checklists.filter( c => c.assignedTo === userId && - c.status !== CHECKLIST_STATUS.COMPLETED && - c.status !== CHECKLIST_STATUS.AWAITING_RECONCILE, + c.status !== CHECKLIST_STATUS.FINALIZED && + c.status !== CHECKLIST_STATUS.REVIEWER_COMPLETED, ); } /** - * Gets checklists for the completed tab + * Gets checklists for the completed tab (finalized checklists) * @param {Object} study - The study object * @returns {Array} Array of checklists for completed tab */ export function getCompletedChecklists(study) { if (!study) return []; const checklists = study.checklists || []; - return checklists.filter(c => c.status === CHECKLIST_STATUS.COMPLETED); + return checklists.filter(c => c.status === CHECKLIST_STATUS.FINALIZED); } /** - * Gets checklists in the reconciliation workflow + * Gets the finalized checklist for a study (the authoritative version for tables/charts) + * @param {Object} study - The study object + * @returns {Object|null} The finalized checklist or null if not found + */ +export function getFinalizedChecklist(study) { + if (!study || !study.checklists) return null; + const checklists = study.checklists || []; + // Prefer reconciled checklist if it's finalized + const reconciled = checklists.find( + c => isReconciledChecklist(c) && c.status === CHECKLIST_STATUS.FINALIZED, + ); + if (reconciled) return reconciled; + // Otherwise, find any finalized checklist + return checklists.find(c => c.status === CHECKLIST_STATUS.FINALIZED) || null; +} + +/** + * Gets checklists in the reconciliation workflow (individual reviewer checklists that are completed) * @param {Object} study - The study object * @returns {Array} Array of checklists in reconciliation workflow */ export function getReconciliationChecklists(study) { if (!study) return []; const checklists = study.checklists || []; - // Return checklists that are awaiting reconciliation - return checklists.filter(c => c.status === CHECKLIST_STATUS.AWAITING_RECONCILE); + // Return individual reviewer checklists that are completed (awaiting reconciliation) + return checklists.filter( + c => !isReconciledChecklist(c) && c.status === CHECKLIST_STATUS.REVIEWER_COMPLETED, + ); } /** @@ -76,13 +95,13 @@ export function shouldShowInTab(study, tab, userId) { if (study.reviewer1 !== userId && study.reviewer2 !== userId) return false; const checklists = study.checklists || []; const userChecklists = checklists.filter(c => c.assignedTo === userId); - // Show if user has no checklist yet OR has a non-completed/awaiting-reconcile checklist + // Show if user has no checklist yet OR has a non-finalized/reviewer-completed checklist return ( userChecklists.length === 0 || userChecklists.some( c => - c.status !== CHECKLIST_STATUS.COMPLETED && - c.status !== CHECKLIST_STATUS.AWAITING_RECONCILE, + c.status !== CHECKLIST_STATUS.FINALIZED && + c.status !== CHECKLIST_STATUS.REVIEWER_COMPLETED, ) ); } @@ -93,10 +112,16 @@ export function shouldShowInTab(study, tab, userId) { const checklists = study.checklists || []; + // If there's a finalized reconciled checklist, reconciliation is complete - don't show in reconcile tab + const hasFinalizedReconciled = checklists.some( + c => isReconciledChecklist(c) && c.status === CHECKLIST_STATUS.FINALIZED, + ); + if (hasFinalizedReconciled) return false; + // Check for individual reviewer checklists awaiting reconciliation // (not reconciled checklists - those are identified by assignedTo === null) const awaitingReconcile = checklists.filter( - c => !isReconciledChecklist(c) && c.status === CHECKLIST_STATUS.AWAITING_RECONCILE, + c => !isReconciledChecklist(c) && c.status === CHECKLIST_STATUS.REVIEWER_COMPLETED, ); // Show if there are 1 or 2 individual checklists awaiting reconciliation @@ -105,7 +130,7 @@ export function shouldShowInTab(study, tab, userId) { case 'completed': { const checklists = study.checklists || []; - return checklists.some(c => c.status === CHECKLIST_STATUS.COMPLETED); + return checklists.some(c => c.status === CHECKLIST_STATUS.FINALIZED); } default: @@ -159,19 +184,19 @@ export function getChecklistCount(studies, tab, userId) { /** * Determines the next status when a reviewer marks their checklist as complete * @param {Object} study - The study object - * @returns {string} The status to set (COMPLETED or AWAITING_RECONCILE) + * @returns {string} The status to set (FINALIZED for single reviewer, REVIEWER_COMPLETED for dual reviewer) */ export function getNextStatusForCompletion(study) { - if (!study) return CHECKLIST_STATUS.COMPLETED; + if (!study) return CHECKLIST_STATUS.FINALIZED; const isSingleReviewer = study.reviewer1 && !study.reviewer2; if (isSingleReviewer) { - // Single reviewer: goes directly to completed - return CHECKLIST_STATUS.COMPLETED; + // Single reviewer: goes directly to finalized + return CHECKLIST_STATUS.FINALIZED; } - // Dual reviewer: goes to awaiting-reconcile - return CHECKLIST_STATUS.AWAITING_RECONCILE; + // Dual reviewer: goes to reviewer-completed (awaiting reconciliation) + return CHECKLIST_STATUS.REVIEWER_COMPLETED; } /** @@ -191,15 +216,15 @@ export function findReconciledChecklist(study, excludeId = null) { } /** - * Gets all reconciled checklists for a study that are not yet completed + * Gets all reconciled checklists for a study that are not yet finalized * @param {Object} study - The study object - * @returns {Array} Array of in-progress reconciled checklists + * @returns {Array} Array of in-progress or reconciling checklists */ export function getInProgressReconciledChecklists(study) { if (!study || !study.checklists) return []; return study.checklists.filter( - c => isReconciledChecklist(c) && c.status !== CHECKLIST_STATUS.COMPLETED, + c => isReconciledChecklist(c) && c.status !== CHECKLIST_STATUS.FINALIZED, ); } diff --git a/packages/web/src/lib/inter-rater-reliability.js b/packages/web/src/lib/inter-rater-reliability.js index 471378679..9dd49a5c4 100644 --- a/packages/web/src/lib/inter-rater-reliability.js +++ b/packages/web/src/lib/inter-rater-reliability.js @@ -44,9 +44,9 @@ export function calculateInterRaterReliability(studies, getChecklistData) { for (const study of dualReviewerStudies) { const checklists = study.checklists || []; - // Find 2 completed AMSTAR2 checklists (one per reviewer) + // Find 2 reviewer-completed AMSTAR2 checklists (one per reviewer) const completedChecklists = checklists.filter( - c => c.status === CHECKLIST_STATUS.COMPLETED && c.type === 'AMSTAR2', + c => c.status === CHECKLIST_STATUS.REVIEWER_COMPLETED && c.type === 'AMSTAR2', ); // Must have exactly 2 completed checklists diff --git a/packages/web/src/primitives/__tests__/useProject.test.js b/packages/web/src/primitives/__tests__/useProject.test.js index a9bcfaf51..708e7604c 100644 --- a/packages/web/src/primitives/__tests__/useProject.test.js +++ b/packages/web/src/primitives/__tests__/useProject.test.js @@ -8,6 +8,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { createRoot } from 'solid-js'; import { useProject } from '../useProject/index.js'; +import { CHECKLIST_STATUS } from '@/constants/checklist-status.js'; import * as Y from 'yjs'; // Mock dependencies @@ -374,7 +375,7 @@ describe('useProject - Checklist Operations', () => { projectStore.setProjectData.mockClear(); project.updateChecklist(studyId, checklistId, { - status: 'completed', + status: CHECKLIST_STATUS.FINALIZED, assignedTo: 'user-2', }); diff --git a/packages/web/src/primitives/useProject/sync.js b/packages/web/src/primitives/useProject/sync.js index 523b2fd81..b5fd0c03f 100644 --- a/packages/web/src/primitives/useProject/sync.js +++ b/packages/web/src/primitives/useProject/sync.js @@ -110,8 +110,8 @@ function buildStudyFromYMap(studyId, studyData, studyYMap) { answers: null, }; - // Extract answers and compute score for completed checklists - if (status === CHECKLIST_STATUS.COMPLETED) { + // Extract answers and compute score for finalized checklists + if (status === CHECKLIST_STATUS.FINALIZED) { const answersMap = checklistYMap.get('answers'); if (answersMap && typeof answersMap.entries === 'function') { const answers = extractAnswersFromYMap(answersMap, checklistType);