Skip to content

338 multiple appraisals per study per user#352

Merged
InfinityBowman merged 7 commits into
mainfrom
338-multiple-appraisals-per-study-per-user
Feb 2, 2026
Merged

338 multiple appraisals per study per user#352
InfinityBowman merged 7 commits into
mainfrom
338-multiple-appraisals-per-study-per-user

Conversation

@InfinityBowman
Copy link
Copy Markdown
Owner

@InfinityBowman InfinityBowman commented Feb 2, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced outcome management system to organize and track project-specific outcomes with create, edit, and delete functionality.
    • Enabled multi-outcome support for ROB2 and ROBINS-I checklists, allowing outcome-specific tracking and management.
    • Added support for multiple checklists per study, improving workflow flexibility and outcome organization.
    • Implemented automatic saving of calculated checklist judgement values.
  • Chores

    • Updated project dependencies for enhanced stability and security.

@InfinityBowman InfinityBowman linked an issue Feb 2, 2026 that may be closed by this pull request
@cursor
Copy link
Copy Markdown

cursor Bot commented Feb 2, 2026

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on March 26.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

This PR introduces outcome management to the project review system, enabling checklists to be associated with specific outcomes (for ROB2 and ROBINS_I types). It adds outcome CRUD operations, updates data models and domain logic to group checklists by outcome, and extends UI across multiple tabs (todo, reconcile, completed) to support multi-outcome workflows for creating, reviewing, and completing outcome-specific checklists.

Changes

Cohort / File(s) Summary
Dependency Updates
package.json, packages/mcp-memory/package.json, packages/mcp/package.json, packages/web/package.json, packages/workers/package.json
Minor version bumps to turbo, wrangler, @types/node, @sentry/solid, better-auth, dexie, preact, and related dependencies.
Type Definitions & Validation
packages/shared/src/checklists/types.ts
Introduced Outcome interface with id, name, createdAt, createdBy; extended ChecklistMetadata with optional outcomeId; added requiresOutcome() helper for ROB2 and ROBINS_I types.
Outcome Management Infrastructure
packages/web/src/primitives/useProject/outcomes.js, packages/web/src/stores/projectActionsStore/outcomes.js
New modules providing outcome CRUD operations (create, read, update, delete, isInUse) with referential integrity checks and integration into the project actions store.
Checklist Lifecycle
packages/web/src/primitives/useProject/checklists/index.js, packages/web/src/stores/projectActionsStore/checklists.js
Extended checklist creation to accept and validate outcomeId; added outcome-specific validation and duplicate checks; propagated outcomeId through checklist data and ROBINS-I auto-fill logic.
Reconciliation Storage & Retrieval
packages/web/src/primitives/useProject/reconciliation.js, packages/web/src/primitives/useProject/index.js
Replaced single reconciliation map with outcome-keyed nested structure; updated save/get/clear functions to accept outcomeId and type; added getAllReconciliationProgress aggregator; integrated outcomes module into project connection lifecycle.
Domain Logic for Grouping & Lookup
packages/web/src/lib/checklist-domain.js
Added outcome-aware grouping functions (getReconciliationChecklistsByOutcome, getCompletedChecklistsByOutcome, getReadyReconciliationPairs); added outcome-scoped lookup (findReconciledChecklistForOutcome); added getOutcomeKey helper; updated shouldShowInTab reconciliation logic to group by outcome.
Outcome UI Component
packages/web/src/components/project/outcomes/OutcomeManager.jsx, packages/web/src/components/project/outcomes/index.js
New collapsible outcome manager component with add, edit, delete functionality; inline forms; keyboard support; success/error handling; permission-based visibility for project owners.
Checklist Form & Type Selection
packages/web/src/components/project/todo-tab/ChecklistForm.jsx
Added outcome selector conditionally rendered for ROB2 and ROBINS_I types; extended validation to enforce outcome selection when required; added availableOutcomes filtering to prevent duplicate assignments.
Todo Tab & Study Row Expansion
packages/web/src/components/project/todo-tab/ToDoTab.jsx, packages/web/src/components/project/todo-tab/TodoStudyRow.jsx
Introduced expanded state tracking per study; extended checklist lifecycle to support add/delete operations; updated UI to show multi-checklist lists with outcome badges; integrated outcome-specific form in collapsible sections; added deletion confirmation dialogs.
Reconciliation Tabs & Multi-Outcome Grouping
packages/web/src/components/project/reconcile-tab/ReconcileTab.jsx, packages/web/src/components/project/reconcile-tab/ReconcileStudyRow.jsx, packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.jsx
Added outcome-aware reconciliation grouping; extended ReconcileStudyRow to display per-outcome reconciliation pairs with badges and status; updated ReconciliationWrapper to scope progress tracking and checklist creation by outcome and type; added multi-outcome UI sections with collapsible outcome lists.
Completed Tab & Outcome Row Components
packages/web/src/components/project/completed-tab/CompletedTab.jsx, packages/web/src/components/project/completed-tab/CompletedStudyRow.jsx, packages/web/src/components/project/completed-tab/CompletedOutcomeRow.jsx
Introduced CompletedOutcomeRow component for per-outcome result display; refactored CompletedStudyRow to group outcomes and render them in expandable sections; added getOutcomeName prop threading; extended completion tracking to be outcome-scoped via reconciliation progress lookups.
Auto-Save Judgement Logic
packages/web/src/components/checklist/ROB2Checklist/OverallSection.jsx, packages/web/src/components/checklist/ROBINSIChecklist/OverallSection.jsx
Added reactive createEffect with on watchers to auto-persist calculated judgements when they differ from stored values and section is not disabled; imports extended with createEffect and on utilities.
Project Outcome Integration
packages/web/src/components/project/all-studies-tab/AllStudiesTab.jsx, packages/web/src/primitives/useProject/sync.js
Added conditional rendering of OutcomeManager in AllStudiesTab guarded by data availability; extended sync.js to extract and surface outcomes metadata in buildStudyFromYMap; enriched checklist data with outcomeId and outcomes list.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant OutcomeUI as Outcome Manager<br/>(UI)
    participant ProjectStore as Project Store<br/>(Outcomes Ops)
    participant ChecklistUI as Checklist Form<br/>(UI)
    participant ChecklistOps as Checklist Ops<br/>(Domain)
    participant ReconciliationOps as Reconciliation Ops<br/>(Domain)

    User->>OutcomeUI: Create Outcome (name)
    OutcomeUI->>ProjectStore: createOutcome(name, userId)
    ProjectStore->>ProjectStore: Generate UUID, store in meta.outcomes
    ProjectStore-->>OutcomeUI: Return outcomeId
    OutcomeUI-->>User: Display outcome in list

    User->>ChecklistUI: Select type (ROB2/ROBINS_I)
    ChecklistUI->>ProjectStore: getOutcomes()
    ProjectStore-->>ChecklistUI: Filter available outcomes
    ChecklistUI->>ChecklistUI: Show outcome selector
    User->>ChecklistUI: Select outcome & assign reviewer
    ChecklistUI->>ChecklistOps: createChecklist(studyId, type, reviewer, outcomeId)
    ChecklistOps->>ChecklistOps: Validate outcomeId required
    ChecklistOps->>ProjectStore: Store checklist with outcomeId
    ChecklistOps-->>ChecklistUI: Return checklistId
    ChecklistUI-->>User: Checklist created

    User->>ReconciliationOps: Open Reconcile tab
    ReconciliationOps->>ReconciliationOps: Group checklists by outcome
    ReconciliationOps->>ReconciliationOps: Filter pairs with 2 checklists
    ReconciliationOps-->>User: Display outcomes needing reconciliation
    User->>ReconciliationOps: Start reconciliation for outcome
    ReconciliationOps->>ReconciliationOps: saveReconciliationProgress(studyId, outcomeId, type, data)
    ReconciliationOps->>ProjectStore: Store in reconciliations[outcomeKey]
    ReconciliationOps-->>User: Track reconciliation progress
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #138: Modifies packages/web/src/lib/checklist-domain.js with overlapping reconciliation and completed-checklist grouping logic to support outcome and type-based keying.
  • PR #275: Updates packages/shared/src/checklists/types.ts to add ROB2 checklist type support, directly complementing the Outcome and requiresOutcome additions in this PR.
  • PR #50: Extends ROBINS-I checklist component functionality and related domain logic, indicating shared feature scope with outcome-aware ROBINS-I handling introduced here.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title '338 multiple appraisals per study per user' directly reflects the main feature added: outcome-based multi-checklist support (multiple appraisals) per study with per-user assignment.
Docstring Coverage ✅ Passed Docstring coverage is 81.63% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 338-multiple-appraisals-per-study-per-user

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
corates 3f9bf44 Commit Preview URL Feb 02 2026, 04:54 PM

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@packages/web/src/components/project/completed-tab/CompletedOutcomeRow.jsx`:
- Around line 25-87: The Open button can call props.onOpenChecklist with
undefined when there is no finalized checklist; guard it by checking
finalizedChecklist()?.id before enabling or invoking the action. Update the Open
button rendering/handler in CompletedOutcomeRow.jsx to (a) compute const
checklistId = finalizedChecklist()?.id and use that for both the button disabled
state and the onClick only if truthy, or (b) hide the button when checklistId is
falsy; adjust the onClick to call props.onOpenChecklist(checklistId) only when
checklistId exists so onOpenChecklist never receives undefined.

In `@packages/web/src/components/project/completed-tab/CompletedStudyRow.jsx`:
- Around line 189-200: The button assumes a checklist exists but
firstGroup().checklists[0] can be undefined; update the render guard and click
handler in CompletedStudyRow.jsx to ensure a checklist exists before calling
props.onOpenChecklist: change the Show condition to require firstGroup() &&
firstGroup().checklists?.length > 0 (in addition to !hasMultipleOutcomes()), and
use optional chaining when reading the id (e.g., get the firstChecklist =
firstGroup().checklists?.[0] and only call
props.onOpenChecklist(firstChecklist?.id) if present) so the button neither
throws nor calls with undefined.

In `@packages/web/src/components/project/outcomes/OutcomeManager.jsx`:
- Around line 81-88: In OutcomeManager.jsx the delete failure branch in
handleDelete calls showToast.error twice with the same message, causing
duplicate toasts; remove the redundant showToast.error call so only a single
showToast.error('Cannot delete outcome', result?.error || 'Outcome is in use by
checklists') is executed (locate the duplicate lines inside the handleDelete
function and delete the extra invocation).

In `@packages/web/src/components/project/todo-tab/TodoStudyRow.jsx`:
- Around line 69-98: The canAddMore memo currently inspects all checklists in
the study; change it to consider only checklists assigned to the current user by
filtering checklists() by assignedTo === currentUserId before computing
hasAmstar2 and usedOutcomesByType. Specifically, in the canAddMore callback
filter const userChecklists = checklists().filter(c => c.assignedTo ===
currentUserId) (or equivalent) and then compute hasAmstar2 and build
usedOutcomesByType from that filtered array while leaving outcomes() unchanged;
retain the existing checks for AMSTAR2 and ROB2/ROBINS_I but scoped to the
filtered userChecklists.
🧹 Nitpick comments (12)
packages/web/src/components/checklist/ROB2Checklist/OverallSection.jsx (1)

28-31: Consider removing redundant memo.

effectiveJudgement simply returns calculatedDisplayJudgement() without any transformation. This could be inlined at usage sites (lines 126, 132, 146) to reduce indirection.

However, keeping it may be intentional for symmetry with the ROBINS-I version where effectiveJudgement contains conditional logic for manual mode.

packages/shared/src/checklists/types.ts (1)

36-38: Consider importing requiresOutcome from shared types instead of duplicating.

This function is duplicated in packages/web/src/primitives/useProject/checklists/index.js (lines 51-53) with identical logic. The frontend ChecklistForm.jsx also has similar inline logic. Consider having all consumers import this helper from the shared package to ensure consistency if the list of outcome-requiring types changes.

♻️ Suggested approach

In packages/web/src/primitives/useProject/checklists/index.js, import and use the shared helper:

+import { requiresOutcome } from '@corates/shared';
 import { createChecklistOfType, CHECKLIST_TYPES } from '@/checklist-registry';

Then remove the local requiresOutcome function (lines 46-53) and use the imported version.

packages/web/src/primitives/useProject/checklists/index.js (2)

46-53: Duplicate helper - consider importing from shared package.

As noted in the types.ts review, this requiresOutcome function duplicates the one in @corates/shared. Import from the shared package for single-source-of-truth.


172-191: Consider validating outcome existence before checklist creation.

The ROBINS-I auto-fill silently handles missing outcome data, but if outcomeId references a non-existent outcome, the checklist is still created with an invalid outcomeId. Consider validating outcome existence early (around line 72) and returning an error if the outcome doesn't exist.

♻️ Optional early validation
      // Validate outcome requirement for ROB2 and ROBINS_I
      if (requiresOutcome(type) && !outcomeId) {
        console.error(`[createChecklist] ${type} requires an outcomeId`);
        return null;
      }

+     // Validate outcome exists if provided
+     if (outcomeId) {
+       const metaMap = ydoc.getMap('meta');
+       const outcomesMap = metaMap.get('outcomes');
+       if (!outcomesMap || !outcomesMap.has(outcomeId)) {
+         console.error('[createChecklist] Outcome not found:', outcomeId);
+         return null;
+       }
+     }
packages/web/src/stores/projectActionsStore/checklists.js (1)

29-43: Import and reuse requiresOutcome from shared types.

The logic on line 31 (type === 'ROB2' || type === 'ROBINS_I') duplicates the requiresOutcome helper already defined in packages/shared/src/checklists/types.ts. Reusing the shared helper ensures consistency if the list of outcome-requiring types changes.

Proposed fix
+import { requiresOutcome } from '@shared/checklists/types';
 
 // In the create function:
       if (!checklistId) {
-        // Could be duplicate or missing outcome
-        const requiresOutcome = type === 'ROB2' || type === 'ROBINS_I';
-        if (requiresOutcome && !outcomeId) {
+        if (requiresOutcome(type) && !outcomeId) {
           showToast.error('Addition Failed', `${type} requires an outcome to be selected`);
-        } else if (requiresOutcome) {
+        } else if (requiresOutcome(type)) {
           showToast.error(
packages/web/src/stores/projectActionsStore/outcomes.js (1)

17-20: Consider reusing getCurrentUserId from the parent store.

The getCurrentUserId helper duplicates the same logic found in packages/web/src/stores/projectActionsStore/index.js (lines 107-110). Consider passing getCurrentUserId as a parameter to createOutcomeActions (similar to how createStudyActions receives it) to avoid duplication and ensure consistency.

Proposed refactor
-export function createOutcomeActions(getActiveConnection) {
-  /**
-   * Get current user ID from auth store
-   */
-  function getCurrentUserId() {
-    const auth = useBetterAuth();
-    return auth.user()?.id || null;
-  }
+export function createOutcomeActions(getActiveConnection, getCurrentUserId) {

Then in index.js:

-  const outcome = createOutcomeActions(getActiveConnection);
+  const outcome = createOutcomeActions(getActiveConnection, getCurrentUserId);
packages/web/src/components/project/all-studies-tab/AllStudiesTab.jsx (1)

147-152: Minor: Comment is slightly misleading.

The comment says "Always visible for owners" but OutcomeManager is rendered for all users when hasData() is true. The ownership check happens internally within OutcomeManager (per the component's canEdit() logic). Consider clarifying the comment.

Suggested comment update
-      {/* Outcome Management - Always visible for owners */}
+      {/* Outcome Management - Visible when data loaded; editing restricted to owners internally */}
       <Show when={hasData()}>
packages/web/src/components/project/outcomes/OutcomeManager.jsx (1)

145-153: Avoid inline ternary rendering for the outcome count label.
Consider deriving a label via createMemo (or Show blocks) to keep conditional rendering consistent and easier to scan. As per coding guidelines: Use Solid's Show component for conditional rendering instead of ternary operators.

Proposed refactor
   const outcomes = createMemo(() => meta()?.outcomes || []);
+  const outcomeCountLabel = createMemo(() => {
+    const count = outcomes().length;
+    if (count === 0) return 'None defined';
+    if (count === 1) return '1 outcome';
+    return `${count} outcomes`;
+  });
@@
           <div class='min-w-0 flex-1'>
             <span class='text-foreground font-medium'>Outcomes</span>
             <span class='text-muted-foreground ml-2 text-sm'>
-              {outcomes().length === 0 ?
-                'None defined'
-              : outcomes().length === 1 ?
-                '1 outcome'
-              : `${outcomes().length} outcomes`}
+              {outcomeCountLabel()}
             </span>
           </div>
packages/web/src/components/project/todo-tab/TodoStudyRow.jsx (1)

32-46: Consider reducing the prop surface for TodoStudyRow.
The component now accepts many props and callbacks; moving shared state and actions into a store or context would reduce prop drilling. As per coding guidelines: Components should receive at most 1-5 props for local configuration only.

packages/web/src/lib/checklist-domain.js (1)

109-160: Consider extracting shared grouping logic to reduce duplication.

The grouping logic in shouldShowInTab (lines 123-134) duplicates the pattern in getReconciliationChecklistsByOutcome (lines 311-327). This could lead to divergence if the grouping criteria change.

♻️ Suggested refactor
 case 'reconcile': {
   // Must have both reviewers assigned (single reviewer studies never go to reconcile tab)
   if (!study.reviewer1 || !study.reviewer2) return false;

-  const checklists = study.checklists || [];
-
-  // Get reviewer-completed checklists grouped by outcome
-  const awaitingReconcile = checklists.filter(
-    c => !isReconciledChecklist(c) && c.status === CHECKLIST_STATUS.REVIEWER_COMPLETED,
-  );
-
-  if (awaitingReconcile.length === 0) return false;
-
-  // Group by outcomeId (or type for AMSTAR2)
-  const groups = new Map();
-  for (const checklist of awaitingReconcile) {
-    const groupKey = checklist.outcomeId || `type:${checklist.type}`;
-    if (!groups.has(groupKey)) {
-      groups.set(groupKey, {
-        checklists: [],
-        outcomeId: checklist.outcomeId,
-        type: checklist.type,
-      });
-    }
-    groups.get(groupKey).checklists.push(checklist);
-  }
+  const groups = getReconciliationChecklistsByOutcome(study);
+  if (groups.length === 0) return false;
+  
+  const checklists = study.checklists || [];

   // Check if any group has a pair ready for reconciliation...
packages/web/src/primitives/useProject/outcomes.js (1)

151-190: Consider reusing isOutcomeInUse in deleteOutcome to reduce duplication.

The referential integrity check in deleteOutcome (lines 157-171) duplicates the logic in isOutcomeInUse (lines 197-214).

♻️ Suggested refactor
 function deleteOutcome(outcomeId) {
   try {
     const ydoc = getYDoc();
     if (!ydoc) return { success: false, error: 'No connection' };

-    // Check if outcome is in use by any checklist
-    const studiesMap = ydoc.getMap('reviews');
-    for (const [, studyYMap] of studiesMap.entries()) {
-      const checklistsMap = studyYMap.get('checklists');
-      if (!checklistsMap) continue;
-
-      for (const [, checklistYMap] of checklistsMap.entries()) {
-        const checklistOutcomeId = checklistYMap.get('outcomeId');
-        if (checklistOutcomeId === outcomeId) {
-          return {
-            success: false,
-            error: 'Cannot delete outcome that is in use by checklists',
-          };
-        }
-      }
-    }
+    if (isOutcomeInUse(outcomeId)) {
+      return {
+        success: false,
+        error: 'Cannot delete outcome that is in use by checklists',
+      };
+    }

     const metaMap = ydoc.getMap('meta');
packages/web/src/primitives/useProject/reconciliation.js (1)

222-226: Clarify the legacy clearing condition logic.

The condition if (!outcomeId || type === 'AMSTAR2') clears the legacy map when either:

  1. outcomeId is null/undefined, OR
  2. type is 'AMSTAR2'

This means clearing a non-AMSTAR2 outcome with a specific outcomeId won't touch legacy data, which is correct. However, clearing any AMSTAR2 outcome (even with an outcomeId) will also clear legacy data, which may be unexpected if AMSTAR2 starts supporting outcomes.

Consider making this more explicit:

♻️ Suggested clarification
-    // Also clear legacy format if clearing the default outcome
-    if (!outcomeId || type === 'AMSTAR2') {
+    // Also clear legacy format if this is the default AMSTAR2 case (no outcomeId)
+    // Legacy data only existed for AMSTAR2 without outcomes
+    if (!outcomeId && type === 'AMSTAR2') {
       studyYMap.delete('reconciliation');
     }

Comment on lines +25 to +87
// Get the first finalized checklist (the reconciled one)
const finalizedChecklist = () => outcomeGroup().checklists[0];

// Get outcome name for display
const outcomeName = () => {
const outcomeId = outcomeGroup().outcomeId;
if (!outcomeId) return null;
return props.getOutcomeName?.(outcomeId) || 'Unknown Outcome';
};

// Get reconciliation progress for this outcome
const reconciliationProgress = () => {
return props.getReconciliationProgress?.(outcomeGroup().outcomeId, outcomeGroup().type);
};

// Check if we have previous reviewers to show
const hasPreviousReviewers = () => {
const progress = reconciliationProgress();
return !!(progress?.checklist1Id && progress?.checklist2Id);
};

return (
<>
<div class='bg-muted/50 flex items-center justify-between rounded-lg p-3'>
<div class='flex items-center gap-3'>
{/* Outcome badge */}
<Show when={outcomeName()}>
<span class='bg-secondary text-secondary-foreground rounded-full px-2 py-0.5 text-xs font-medium'>
{outcomeName()}
</span>
</Show>

{/* Checklist type */}
<span class='text-foreground text-sm font-medium'>
{getChecklistMetadata(outcomeGroup().type)?.name || outcomeGroup().type}
</span>

{/* Status badge */}
<span
class={`rounded-full px-2 py-0.5 text-xs font-medium ${getStatusStyle(finalizedChecklist()?.status)}`}
>
{getStatusLabel(finalizedChecklist()?.status)}
</span>
</div>

<div class='flex items-center gap-2'>
{/* View Previous button (for dual-reviewer studies) */}
<Show when={hasPreviousReviewers()}>
<button
onClick={() => setShowPreviousReviewers(true)}
class='bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors'
>
View Previous
</button>
</Show>

{/* Open button */}
<button
onClick={() => props.onOpenChecklist?.(finalizedChecklist()?.id)}
class='bg-primary hover:bg-primary/90 focus:ring-primary rounded-lg px-3 py-1.5 text-sm font-medium text-white transition-colors focus:ring-2 focus:outline-none'
>
Open
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard the Open action when no finalized checklist is available.
If outcomeGroup.checklists is empty during sync or data inconsistencies, onOpenChecklist will receive undefined. Consider disabling or hiding the button when there is no checklist id.

Proposed fix
-  const finalizedChecklist = () => outcomeGroup().checklists[0];
+  const finalizedChecklist = () => outcomeGroup().checklists[0];
+  const finalizedChecklistId = () => finalizedChecklist()?.id;
@@
-          <button
-            onClick={() => props.onOpenChecklist?.(finalizedChecklist()?.id)}
+          <button
+            onClick={() => finalizedChecklistId() && props.onOpenChecklist?.(finalizedChecklistId())}
+            disabled={!finalizedChecklistId()}
             class='bg-primary hover:bg-primary/90 focus:ring-primary rounded-lg px-3 py-1.5 text-sm font-medium text-white transition-colors focus:ring-2 focus:outline-none'
           >
             Open
           </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Get the first finalized checklist (the reconciled one)
const finalizedChecklist = () => outcomeGroup().checklists[0];
// Get outcome name for display
const outcomeName = () => {
const outcomeId = outcomeGroup().outcomeId;
if (!outcomeId) return null;
return props.getOutcomeName?.(outcomeId) || 'Unknown Outcome';
};
// Get reconciliation progress for this outcome
const reconciliationProgress = () => {
return props.getReconciliationProgress?.(outcomeGroup().outcomeId, outcomeGroup().type);
};
// Check if we have previous reviewers to show
const hasPreviousReviewers = () => {
const progress = reconciliationProgress();
return !!(progress?.checklist1Id && progress?.checklist2Id);
};
return (
<>
<div class='bg-muted/50 flex items-center justify-between rounded-lg p-3'>
<div class='flex items-center gap-3'>
{/* Outcome badge */}
<Show when={outcomeName()}>
<span class='bg-secondary text-secondary-foreground rounded-full px-2 py-0.5 text-xs font-medium'>
{outcomeName()}
</span>
</Show>
{/* Checklist type */}
<span class='text-foreground text-sm font-medium'>
{getChecklistMetadata(outcomeGroup().type)?.name || outcomeGroup().type}
</span>
{/* Status badge */}
<span
class={`rounded-full px-2 py-0.5 text-xs font-medium ${getStatusStyle(finalizedChecklist()?.status)}`}
>
{getStatusLabel(finalizedChecklist()?.status)}
</span>
</div>
<div class='flex items-center gap-2'>
{/* View Previous button (for dual-reviewer studies) */}
<Show when={hasPreviousReviewers()}>
<button
onClick={() => setShowPreviousReviewers(true)}
class='bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors'
>
View Previous
</button>
</Show>
{/* Open button */}
<button
onClick={() => props.onOpenChecklist?.(finalizedChecklist()?.id)}
class='bg-primary hover:bg-primary/90 focus:ring-primary rounded-lg px-3 py-1.5 text-sm font-medium text-white transition-colors focus:ring-2 focus:outline-none'
>
Open
</button>
// Get the first finalized checklist (the reconciled one)
const finalizedChecklist = () => outcomeGroup().checklists[0];
const finalizedChecklistId = () => finalizedChecklist()?.id;
// Get outcome name for display
const outcomeName = () => {
const outcomeId = outcomeGroup().outcomeId;
if (!outcomeId) return null;
return props.getOutcomeName?.(outcomeId) || 'Unknown Outcome';
};
// Get reconciliation progress for this outcome
const reconciliationProgress = () => {
return props.getReconciliationProgress?.(outcomeGroup().outcomeId, outcomeGroup().type);
};
// Check if we have previous reviewers to show
const hasPreviousReviewers = () => {
const progress = reconciliationProgress();
return !!(progress?.checklist1Id && progress?.checklist2Id);
};
return (
<>
<div class='bg-muted/50 flex items-center justify-between rounded-lg p-3'>
<div class='flex items-center gap-3'>
{/* Outcome badge */}
<Show when={outcomeName()}>
<span class='bg-secondary text-secondary-foreground rounded-full px-2 py-0.5 text-xs font-medium'>
{outcomeName()}
</span>
</Show>
{/* Checklist type */}
<span class='text-foreground text-sm font-medium'>
{getChecklistMetadata(outcomeGroup().type)?.name || outcomeGroup().type}
</span>
{/* Status badge */}
<span
class={`rounded-full px-2 py-0.5 text-xs font-medium ${getStatusStyle(finalizedChecklist()?.status)}`}
>
{getStatusLabel(finalizedChecklist()?.status)}
</span>
</div>
<div class='flex items-center gap-2'>
{/* View Previous button (for dual-reviewer studies) */}
<Show when={hasPreviousReviewers()}>
<button
onClick={() => setShowPreviousReviewers(true)}
class='bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors'
>
View Previous
</button>
</Show>
{/* Open button */}
<button
onClick={() => finalizedChecklistId() && props.onOpenChecklist?.(finalizedChecklistId())}
disabled={!finalizedChecklistId()}
class='bg-primary hover:bg-primary/90 focus:ring-primary rounded-lg px-3 py-1.5 text-sm font-medium text-white transition-colors focus:ring-2 focus:outline-none'
>
Open
</button>
🤖 Prompt for AI Agents
In `@packages/web/src/components/project/completed-tab/CompletedOutcomeRow.jsx`
around lines 25 - 87, The Open button can call props.onOpenChecklist with
undefined when there is no finalized checklist; guard it by checking
finalizedChecklist()?.id before enabling or invoking the action. Update the Open
button rendering/handler in CompletedOutcomeRow.jsx to (a) compute const
checklistId = finalizedChecklist()?.id and use that for both the button disabled
state and the onClick only if truthy, or (b) hide the button when checklistId is
falsy; adjust the onClick to call props.onOpenChecklist(checklistId) only when
checklistId exists so onOpenChecklist never receives undefined.

Comment on lines +189 to 200
{/* Open checklist button (single outcome mode) */}
<Show when={!hasMultipleOutcomes() && firstGroup()}>
<button
onClick={e => {
e.stopPropagation();
props.onOpenChecklist?.(completedChecklists()[0].id);
props.onOpenChecklist?.(firstGroup().checklists[0].id);
}}
class='bg-primary hover:bg-primary/90 focus:ring-primary shrink-0 rounded-lg px-4 py-1.5 text-sm font-medium text-white transition-colors focus:ring-2 focus:outline-none'
>
Open
</button>
</Show>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential issue: firstGroup().checklists[0] may be undefined if the group has no checklists.

The Show when guard ensures firstGroup() exists, but doesn't guarantee checklists[0] is defined. While getCompletedChecklistsByOutcome only creates groups for completed checklists, adding a defensive check would be safer.

🛡️ Suggested defensive check
-            <Show when={!hasMultipleOutcomes() && firstGroup()}>
+            <Show when={!hasMultipleOutcomes() && firstGroup()?.checklists?.[0]}>
               <button
                 onClick={e => {
                   e.stopPropagation();
                   props.onOpenChecklist?.(firstGroup().checklists[0].id);
                 }}
🤖 Prompt for AI Agents
In `@packages/web/src/components/project/completed-tab/CompletedStudyRow.jsx`
around lines 189 - 200, The button assumes a checklist exists but
firstGroup().checklists[0] can be undefined; update the render guard and click
handler in CompletedStudyRow.jsx to ensure a checklist exists before calling
props.onOpenChecklist: change the Show condition to require firstGroup() &&
firstGroup().checklists?.length > 0 (in addition to !hasMultipleOutcomes()), and
use optional chaining when reading the id (e.g., get the firstChecklist =
firstGroup().checklists?.[0] and only call
props.onOpenChecklist(firstChecklist?.id) if present) so the button neither
throws nor calls with undefined.

Comment on lines +81 to +88
showToast.error(
'Cannot delete outcome',
result?.error || 'Outcome is in use by checklists',
);
showToast.error(
'Cannot delete outcome',
result?.error || 'Outcome is in use by checklists',
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate error toast on delete failure.
handleDelete currently shows the same error twice, which will duplicate the toast.

Proposed fix
       } else {
         showToast.error(
           'Cannot delete outcome',
           result?.error || 'Outcome is in use by checklists',
         );
-        showToast.error(
-          'Cannot delete outcome',
-          result?.error || 'Outcome is in use by checklists',
-        );
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
showToast.error(
'Cannot delete outcome',
result?.error || 'Outcome is in use by checklists',
);
showToast.error(
'Cannot delete outcome',
result?.error || 'Outcome is in use by checklists',
);
} else {
showToast.error(
'Cannot delete outcome',
result?.error || 'Outcome is in use by checklists',
);
}
🤖 Prompt for AI Agents
In `@packages/web/src/components/project/outcomes/OutcomeManager.jsx` around lines
81 - 88, In OutcomeManager.jsx the delete failure branch in handleDelete calls
showToast.error twice with the same message, causing duplicate toasts; remove
the redundant showToast.error call so only a single showToast.error('Cannot
delete outcome', result?.error || 'Outcome is in use by checklists') is executed
(locate the duplicate lines inside the handleDelete function and delete the
extra invocation).

Comment on lines +69 to +98
// Check if user can add more checklists (has unused outcomes for ROB2/ROBINS_I, or no AMSTAR2 yet)
const canAddMore = createMemo(() => {
const userChecklists = checklists();
const projectOutcomes = outcomes();

// For AMSTAR2: can add if user doesn't have one yet
const hasAmstar2 = userChecklists.some(c => c.type === CHECKLIST_TYPES.AMSTAR2);
if (!hasAmstar2) return true;

// For ROB2/ROBINS_I: can add if there are unused outcomes
if (projectOutcomes.length === 0) return false;

const usedOutcomesByType = {};
for (const checklist of userChecklists) {
if (checklist.outcomeId) {
if (!usedOutcomesByType[checklist.type]) {
usedOutcomesByType[checklist.type] = new Set();
}
usedOutcomesByType[checklist.type].add(checklist.outcomeId);
}
}

// Check if any outcome is available for ROB2 or ROBINS_I
for (const outcome of projectOutcomes) {
const rob2Used = usedOutcomesByType[CHECKLIST_TYPES.ROB2]?.has(outcome.id);
const robinsUsed = usedOutcomesByType[CHECKLIST_TYPES.ROBINS_I]?.has(outcome.id);
if (!rob2Used || !robinsUsed) return true;
}

return false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scope canAddMore to the current user to avoid blocking other reviewers.
The current logic considers all checklists in the study, so another reviewer can consume all outcomes or add AMSTAR2 and prevent the current user from adding their own checklist. Filter by assignedTo/currentUserId when computing hasAmstar2 and usedOutcomesByType.

Proposed fix
   const canAddMore = createMemo(() => {
-    const userChecklists = checklists();
+    const userId = props.currentUserId;
+    if (!userId) return false;
+    const userChecklists = checklists().filter(c => c.assignedTo === userId);
     const projectOutcomes = outcomes();
 
     // For AMSTAR2: can add if user doesn't have one yet
     const hasAmstar2 = userChecklists.some(c => c.type === CHECKLIST_TYPES.AMSTAR2);
     if (!hasAmstar2) return true;
@@
-    for (const checklist of userChecklists) {
+    for (const checklist of userChecklists) {
       if (checklist.outcomeId) {
         if (!usedOutcomesByType[checklist.type]) {
           usedOutcomesByType[checklist.type] = new Set();
         }
         usedOutcomesByType[checklist.type].add(checklist.outcomeId);
       }
     }
🤖 Prompt for AI Agents
In `@packages/web/src/components/project/todo-tab/TodoStudyRow.jsx` around lines
69 - 98, The canAddMore memo currently inspects all checklists in the study;
change it to consider only checklists assigned to the current user by filtering
checklists() by assignedTo === currentUserId before computing hasAmstar2 and
usedOutcomesByType. Specifically, in the canAddMore callback filter const
userChecklists = checklists().filter(c => c.assignedTo === currentUserId) (or
equivalent) and then compute hasAmstar2 and build usedOutcomesByType from that
filtered array while leaving outcomes() unchanged; retain the existing checks
for AMSTAR2 and ROB2/ROBINS_I but scoped to the filtered userChecklists.

@InfinityBowman InfinityBowman merged commit 78bee8a into main Feb 2, 2026
3 checks passed
@InfinityBowman InfinityBowman deleted the 338-multiple-appraisals-per-study-per-user branch February 2, 2026 17:09
@coderabbitai coderabbitai Bot mentioned this pull request May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple appraisals per study per user

2 participants