- {!hasEntitlement('project.create') ?
- 'Project creation not available on your plan'
- : `Project limit reached (${projectCount()}/${isUnlimitedQuota(quotas()['projects.max']) ? '∞' : quotas()['projects.max']})`
- }
+
+
}
>
diff --git a/packages/web/src/components/project-ui/overview-tab/ChartSection.jsx b/packages/web/src/components/project-ui/overview-tab/ChartSection.jsx
index 19f084114..6a4ab1b15 100644
--- a/packages/web/src/components/project-ui/overview-tab/ChartSection.jsx
+++ b/packages/web/src/components/project-ui/overview-tab/ChartSection.jsx
@@ -241,11 +241,18 @@ export default function ChartSection(props) {
}
>
- {/* Settings button */}
-
+ {/* Section Header with Description and Settings */}
+
+
+
+ Visual representation of AMSTAR-2 quality assessment ratings across completed
+ checklists. Use the settings to customize chart appearance, labels, and export
+ options.
+
+
setShowSettingsModal(true)}
- class='inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900'
+ class='inline-flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900'
title='Chart Settings'
>
diff --git a/packages/web/src/components/project-ui/overview-tab/CircularProgress.jsx b/packages/web/src/components/project-ui/overview-tab/CircularProgress.jsx
new file mode 100644
index 000000000..cd6bf4af4
--- /dev/null
+++ b/packages/web/src/components/project-ui/overview-tab/CircularProgress.jsx
@@ -0,0 +1,146 @@
+/**
+ * CircularProgress - Circular progress indicator using D3
+ */
+
+import { createEffect } from 'solid-js';
+import * as d3 from 'd3';
+
+export default function CircularProgress(props) {
+ let svgRef = null;
+
+ const value = () => props.value ?? 0;
+ const label = () => props.label ?? '';
+ const showValue = () => props.showValue ?? false;
+ const variant = () => props.variant ?? 'default';
+ const size = () => props.size ?? 120; // Default size in pixels
+
+ const getVariantColor = () => {
+ switch (variant()) {
+ case 'success':
+ return '#10b981'; // green-500
+ case 'warning':
+ return '#eab308'; // yellow-500
+ case 'error':
+ return '#ef4444'; // red-500
+ default:
+ return '#3b82f6'; // blue-500
+ }
+ };
+
+ const clampedValue = () => Math.max(0, Math.min(100, value()));
+
+ createEffect(() => {
+ if (!svgRef) return;
+
+ const radius = size() / 2 - 8; // Leave some padding
+ const centerX = size() / 2;
+ const centerY = size() / 2;
+ const strokeWidth = 8;
+
+ // Clear previous content
+ d3.select(svgRef).selectAll('*').remove();
+
+ const svg = d3.select(svgRef).attr('width', size()).attr('height', size());
+
+ // Create arc generator
+ const arc = d3
+ .arc()
+ .innerRadius(radius - strokeWidth / 2)
+ .outerRadius(radius + strokeWidth / 2)
+ .cornerRadius(4);
+
+ const group = svg.append('g').attr('transform', `translate(${centerX}, ${centerY})`);
+
+ // Background circle (full 360 degrees)
+ const backgroundArc = arc({
+ startAngle: 0,
+ endAngle: 2 * Math.PI,
+ });
+
+ group.append('path').attr('d', backgroundArc).attr('fill', '#e5e7eb'); // gray-200
+
+ // Progress arc
+ const progressAngle = (clampedValue() / 100) * 2 * Math.PI;
+ const progressArcData = {
+ startAngle: -Math.PI / 2, // Start from top
+ endAngle: -Math.PI / 2 + progressAngle,
+ };
+
+ // Initialize with 0 angle for smooth animation
+ const progressPath = group
+ .append('path')
+ .datum({
+ startAngle: -Math.PI / 2,
+ endAngle: -Math.PI / 2,
+ })
+ .attr('d', arc)
+ .attr('fill', getVariantColor());
+
+ // Animate to actual progress
+ progressPath
+ .transition()
+ .duration(800)
+ .ease(d3.easeCubicOut)
+ .attrTween('d', function (d) {
+ const interpolate = d3.interpolate(d.endAngle, progressArcData.endAngle);
+ return function (t) {
+ d.endAngle = interpolate(t);
+ return arc(d);
+ };
+ });
+
+ // Center text - percentage
+ if (showValue()) {
+ svg
+ .append('text')
+ .attr('x', centerX)
+ .attr('y', centerY)
+ .attr('text-anchor', 'middle')
+ .attr('dominant-baseline', 'middle')
+ .attr('font-size', '24px')
+ .attr('font-weight', 'bold')
+ .attr('fill', '#111827') // gray-900
+ .text(`${Math.round(clampedValue())}%`)
+ .attr('opacity', 0)
+ .transition()
+ .duration(800)
+ .ease(d3.easeCubicOut)
+ .attr('opacity', 1);
+ }
+
+ // Center text - label (if provided, show below value)
+ if (label()) {
+ const currentLabel = label();
+ const currentSize = size();
+ const labelY = showValue() ? centerY + 28 : centerY;
+ svg
+ .append('text')
+ .attr('x', centerX)
+ .attr('y', labelY)
+ .attr('text-anchor', 'middle')
+ .attr('dominant-baseline', 'middle')
+ .attr('font-size', '11px')
+ .attr('fill', '#6b7280') // gray-500
+ .text(currentLabel)
+ .attr('opacity', 0)
+ .transition()
+ .duration(800)
+ .ease(d3.easeCubicOut)
+ .attr('opacity', 1)
+ .call(text => {
+ // Truncate text if too long
+ const maxLength = Math.floor((currentSize * 0.75) / 6); // Approximate chars that fit
+ const textElement = text.node();
+ if (textElement && currentLabel.length > maxLength) {
+ textElement.textContent = currentLabel.substring(0, maxLength - 3) + '...';
+ }
+ });
+ }
+ });
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/web/src/components/project-ui/overview-tab/OverviewTab.jsx b/packages/web/src/components/project-ui/overview-tab/OverviewTab.jsx
index 716d30478..aa2094641 100644
--- a/packages/web/src/components/project-ui/overview-tab/OverviewTab.jsx
+++ b/packages/web/src/components/project-ui/overview-tab/OverviewTab.jsx
@@ -1,6 +1,9 @@
-import { For, Show, createSignal } from 'solid-js';
+import { For, Show, createSignal, createMemo } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { FiPlus, FiTrash2 } from 'solid-icons/fi';
+import { AiOutlineBook } from 'solid-icons/ai';
+import { BiRegularCheckCircle, BiRegularTimeFive } from 'solid-icons/bi';
+import { CgArrowsExchange } from 'solid-icons/cg';
import ChartSection from './ChartSection.jsx';
import AddMemberModal from './AddMemberModal.jsx';
import ReviewerAssignment from './ReviewerAssignment.jsx';
@@ -8,10 +11,11 @@ import projectStore from '@/stores/projectStore.js';
import projectActionsStore from '@/stores/projectActionsStore';
import { useBetterAuth } from '@api/better-auth-store.js';
import { useProjectContext } from '../ProjectContext.jsx';
-import { Avatar, useConfirmDialog, showToast } from '@corates/ui';
+import { Avatar, useConfirmDialog, showToast, Progress, Collapsible } from '@corates/ui';
import { API_BASE } from '@config/api.js';
import { CHECKLIST_STATUS } from '@/constants/checklist-status.js';
import { shouldShowInTab } from '@/lib/checklist-domain.js';
+import CircularProgress from './CircularProgress.jsx';
/**
* OverviewTab - Project overview with stats, settings, and members
@@ -19,6 +23,7 @@ import { shouldShowInTab } from '@/lib/checklist-domain.js';
*/
export default function OverviewTab() {
const [showAddMemberModal, setShowAddMemberModal] = createSignal(false);
+ const [chartsExpanded, setChartsExpanded] = createSignal(false);
const { user } = useBetterAuth();
const { projectId, isOwner } = useProjectContext();
@@ -47,6 +52,59 @@ export default function OverviewTab() {
const completedStudies = () =>
studies().filter(s => shouldShowInTab(s, 'completed', null)).length;
+ // Calculate overall progress (completed studies / total studies)
+ const overallProgress = createMemo(() => {
+ const total = studies().length;
+ if (total === 0) return 0;
+ const completed = completedStudies();
+ return Math.round((completed / total) * 100);
+ });
+
+ // Calculate user progress for all users (memoized for performance)
+ const userProgressMap = createMemo(() => {
+ const progressMap = new Map();
+
+ studies().forEach(study => {
+ const checklists = study.checklists || [];
+ checklists.forEach(checklist => {
+ const userId = checklist.assignedTo;
+ if (!userId) return;
+
+ if (!progressMap.has(userId)) {
+ progressMap.set(userId, { completed: 0, total: 0 });
+ }
+
+ const progress = progressMap.get(userId);
+ progress.total++;
+ if (
+ checklist.status === CHECKLIST_STATUS.AWAITING_RECONCILE ||
+ checklist.status === CHECKLIST_STATUS.COMPLETED
+ ) {
+ progress.completed++;
+ }
+ });
+ });
+
+ // Convert to percentage
+ const result = new Map();
+ progressMap.forEach((progress, userId) => {
+ result.set(userId, {
+ percentage:
+ progress.total === 0 ? 0 : Math.round((progress.completed / progress.total) * 100),
+ completed: progress.completed,
+ total: progress.total,
+ });
+ });
+
+ return result;
+ });
+
+ // Helper to get user progress
+ const getUserProgress = userId => {
+ if (!userId) return { percentage: 0, completed: 0, total: 0 };
+ return userProgressMap().get(userId) || { percentage: 0, completed: 0, total: 0 };
+ };
+
// Handlers (use active project - no projectId needed)
const handleUpdateStudy = (studyId, updates) => {
projectActionsStore.study.update(studyId, updates);
@@ -87,70 +145,105 @@ export default function OverviewTab() {
return projectActionsStore.checklist.getData(studyId, checklistId);
};
+ // Calculate unassigned studies for Reviewer Assignment visibility
+ const unassignedStudies = createMemo(() => studies().filter(s => !s.reviewer1 && !s.reviewer2));
+
+ // Determine if Reviewer Assignment should be shown
+ const shouldShowReviewerAssignment = () =>
+ isOwner() && studies().length > 0 && unassignedStudies().length > 0;
+
return (
<>
- {/* Stats Summary */}
-
-
-
{studies().length}
-
Total Studies
-
-
-
{inProgressStudies()}
-
In Progress
-
-
-
{readyToReconcile()}
-
Ready to Reconcile
-
-
-
{completedStudies()}
-
Completed
-
-
+ {/* Section 1: Project Progress - Hero Section */}
+
+
Project Progress
- {/* Two-column layout for better space utilization */}
-
- {/* Left Column */}
-
- {/* Reviewer Assignment Section */}
-
0}>
-
+ {/* Overall Progress - Circular */}
+
+ = 50 ?
+ 'default'
+ : 'warning'
+ }
+ size={160}
/>
-
-
+
+ {completedStudies()} of {studies().length} studies completed
+
+
- {/* Right Column */}
-
- {/* Members Section */}
-
-
-
Project Members
-
- setShowAddMemberModal(true)}
- class='inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700'
- >
-
- Add Member
-
-
+ {/* Enhanced Stats Grid */}
+
+
+
+
{studies().length}
+
Total Studies
+
+
+
+
+
+
{inProgressStudies()}
+
In Progress
-
0}>
-
-
- {member => {
- const isSelf = currentUserId() === member.userId;
- const canRemove = isOwner() || isSelf;
- const isLastOwner =
- member.role === 'owner' &&
- members().filter(m => m.role === 'owner').length <= 1;
-
- return (
-
+
+
+
+
+
{readyToReconcile()}
+
Ready to Reconcile
+
+
+
+
+
+
{completedStudies()}
+
Completed
+
+
+
+
+
+ {/* Section 2: Team & Collaboration */}
+
+
Team & Collaboration
+
+ {/* Members Section - Full Width */}
+
+
+
Project Members
+
+ setShowAddMemberModal(true)}
+ class='inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700'
+ >
+
+ Add Member
+
+
+
+
0}>
+
+
+ {member => {
+ const isSelf = currentUserId() === member.userId;
+ const canRemove = isOwner() || isSelf;
+ const isLastOwner =
+ member.role === 'owner' &&
+ members().filter(m => m.role === 'owner').length <= 1;
+
+ const userProgress = () => getUserProgress(member.userId);
+
+ return (
+
-
-
+ 0}>
+
+
= 50 ?
+ 'default'
+ : 'warning'
+ }
+ />
+
+
+
+ );
+ }}
+
+
+
+
+ {/* Reviewer Assignment - Below Members, Collapsed by Default */}
+
+
+
- {/* Charts Section - Full width */}
-
-
Quality Assessment Figures
-
+ {/* Section 3: Quality Assessment - Collapsible */}
+
+
+
setChartsExpanded(open)}
+ trigger={
+
+
Quality Assessment
+
+ {chartsExpanded() ? 'Click to collapse' : 'Click to expand charts'}
+
+
+ }
+ >
+
+
+
+
+
{
});
it('should add PDF metadata to study', async () => {
- createRoot(async dispose => {
- cleanup = dispose;
- const project = useProject('local-test');
+ await new Promise(resolveTest => {
+ createRoot(async dispose => {
+ cleanup = dispose;
+ const project = useProject('local-test');
- const studyId = project.createStudy('Test Study');
- projectStore.setProjectData.mockClear();
-
- const pdfId = project.addPdfToStudy(studyId, {
- fileName: 'test.pdf',
- key: 'r2-storage-key',
- size: 123456,
- uploadedBy: 'user-1',
- uploadedAt: Date.now(),
- });
+ await new Promise(resolve => setTimeout(resolve, 10));
- expect(pdfId).toBeTruthy();
- expect(projectStore.setProjectData).toHaveBeenCalled();
+ const studyId = project.createStudy('Test Study');
+ projectStore.setProjectData.mockClear();
+
+ const pdfId = project.addPdfToStudy(studyId, {
+ fileName: 'test.pdf',
+ key: 'r2-storage-key',
+ size: 123456,
+ uploadedBy: 'user-1',
+ uploadedAt: Date.now(),
+ });
+
+ expect(pdfId).toBeTruthy();
+
+ // Wait for Y.js update event to trigger sync
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ expect(projectStore.setProjectData).toHaveBeenCalled();
+ resolveTest();
+ });
});
});
it('should remove PDF metadata from study', async () => {
- createRoot(async dispose => {
- cleanup = dispose;
- const project = useProject('local-test');
+ await new Promise(resolveTest => {
+ createRoot(async dispose => {
+ cleanup = dispose;
+ const project = useProject('local-test');
- const studyId = project.createStudy('Test Study');
+ await new Promise(resolve => setTimeout(resolve, 10));
- project.addPdfToStudy(studyId, {
- fileName: 'test.pdf',
- key: 'r2-key',
- size: 12345,
- uploadedBy: 'user-1',
- });
+ const studyId = project.createStudy('Test Study');
- projectStore.setProjectData.mockClear();
+ project.addPdfToStudy(studyId, {
+ fileName: 'test.pdf',
+ key: 'r2-key',
+ size: 12345,
+ uploadedBy: 'user-1',
+ });
- project.removePdfFromStudy(studyId, 'test.pdf');
+ // Wait for initial add to sync
+ await new Promise(resolve => setTimeout(resolve, 10));
- expect(projectStore.setProjectData).toHaveBeenCalled();
+ projectStore.setProjectData.mockClear();
+
+ project.removePdfFromStudy(studyId, 'test.pdf');
+
+ // Wait for Y.js update event to trigger sync
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ expect(projectStore.setProjectData).toHaveBeenCalled();
+ resolveTest();
+ });
});
});
});
@@ -341,62 +361,80 @@ describe('useProject - Checklist Operations', () => {
});
it('should update checklist', async () => {
- createRoot(async dispose => {
- cleanup = dispose;
- const project = useProject('local-test');
+ await new Promise(resolveTest => {
+ createRoot(async dispose => {
+ cleanup = dispose;
+ const project = useProject('local-test');
- await new Promise(resolve => setTimeout(resolve, 10));
+ await new Promise(resolve => setTimeout(resolve, 10));
- const studyId = project.createStudy('Test Study');
- const checklistId = project.createChecklist(studyId);
+ const studyId = project.createStudy('Test Study');
+ const checklistId = project.createChecklist(studyId);
- projectStore.setProjectData.mockClear();
+ projectStore.setProjectData.mockClear();
- project.updateChecklist(studyId, checklistId, {
- status: 'completed',
- assignedTo: 'user-2',
- });
+ project.updateChecklist(studyId, checklistId, {
+ status: 'completed',
+ assignedTo: 'user-2',
+ });
- expect(projectStore.setProjectData).toHaveBeenCalled();
+ // Wait for Y.js update event to trigger sync
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ expect(projectStore.setProjectData).toHaveBeenCalled();
+ resolveTest();
+ });
});
});
it('should delete checklist', async () => {
- createRoot(async dispose => {
- cleanup = dispose;
- const project = useProject('local-test');
+ await new Promise(resolveTest => {
+ createRoot(async dispose => {
+ cleanup = dispose;
+ const project = useProject('local-test');
- await new Promise(resolve => setTimeout(resolve, 10));
+ await new Promise(resolve => setTimeout(resolve, 10));
- const studyId = project.createStudy('Test Study');
- const checklistId = project.createChecklist(studyId);
+ const studyId = project.createStudy('Test Study');
+ const checklistId = project.createChecklist(studyId);
- projectStore.setProjectData.mockClear();
+ projectStore.setProjectData.mockClear();
- project.deleteChecklist(studyId, checklistId);
+ project.deleteChecklist(studyId, checklistId);
- expect(projectStore.setProjectData).toHaveBeenCalled();
+ // Wait for Y.js update event to trigger sync
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ expect(projectStore.setProjectData).toHaveBeenCalled();
+ resolveTest();
+ });
});
});
it('should update checklist answer', async () => {
- createRoot(async dispose => {
- cleanup = dispose;
- const project = useProject('local-test');
+ await new Promise(resolveTest => {
+ createRoot(async dispose => {
+ cleanup = dispose;
+ const project = useProject('local-test');
- await new Promise(resolve => setTimeout(resolve, 10));
+ await new Promise(resolve => setTimeout(resolve, 10));
- const studyId = project.createStudy('Test Study');
- const checklistId = project.createChecklist(studyId);
+ const studyId = project.createStudy('Test Study');
+ const checklistId = project.createChecklist(studyId);
- projectStore.setProjectData.mockClear();
+ projectStore.setProjectData.mockClear();
- project.updateChecklistAnswer(studyId, checklistId, 'q1', {
- answers: [[true, false, false, false]],
- critical: true,
- });
+ project.updateChecklistAnswer(studyId, checklistId, 'q1', {
+ answers: [[true, false, false, false]],
+ critical: true,
+ });
- expect(projectStore.setProjectData).toHaveBeenCalled();
+ // Wait for Y.js update event to trigger sync
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ expect(projectStore.setProjectData).toHaveBeenCalled();
+ resolveTest();
+ });
});
});
@@ -455,52 +493,64 @@ describe('useProject - Reconciliation Operations', () => {
});
it('should save reconciliation progress', async () => {
- createRoot(async dispose => {
- cleanup = dispose;
- const project = useProject('local-test');
+ await new Promise(resolveTest => {
+ createRoot(async dispose => {
+ cleanup = dispose;
+ const project = useProject('local-test');
- await new Promise(resolve => setTimeout(resolve, 10));
+ await new Promise(resolve => setTimeout(resolve, 10));
- const studyId = project.createStudy('Test Study');
- projectStore.setProjectData.mockClear();
+ const studyId = project.createStudy('Test Study');
+ projectStore.setProjectData.mockClear();
- project.saveReconciliationProgress(studyId, {
- checklist1Id: 'checklist-1',
- checklist2Id: 'checklist-2',
- currentPage: 2,
- viewMode: 'questions',
- finalAnswers: { q1: { selection: 'reviewer1' } },
- });
+ project.saveReconciliationProgress(studyId, {
+ checklist1Id: 'checklist-1',
+ checklist2Id: 'checklist-2',
+ currentPage: 2,
+ viewMode: 'questions',
+ finalAnswers: { q1: { selection: 'reviewer1' } },
+ });
- expect(projectStore.setProjectData).toHaveBeenCalled();
+ // Wait for Y.js update event to trigger sync
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ expect(projectStore.setProjectData).toHaveBeenCalled();
+ resolveTest();
+ });
});
});
it('should get reconciliation progress', async () => {
- createRoot(async dispose => {
- cleanup = dispose;
- const project = useProject('local-test');
+ await new Promise(resolveTest => {
+ createRoot(async dispose => {
+ cleanup = dispose;
+ const project = useProject('local-test');
- await new Promise(resolve => setTimeout(resolve, 10));
+ await new Promise(resolve => setTimeout(resolve, 10));
- const studyId = project.createStudy('Test Study');
+ const studyId = project.createStudy('Test Study');
- project.saveReconciliationProgress(studyId, {
- checklist1Id: 'checklist-1',
- checklist2Id: 'checklist-2',
- currentPage: 3,
- viewMode: 'questions',
- finalAnswers: { q1: 'answer' },
- });
+ project.saveReconciliationProgress(studyId, {
+ checklist1Id: 'checklist-1',
+ checklist2Id: 'checklist-2',
+ currentPage: 3,
+ viewMode: 'questions',
+ finalAnswers: { q1: 'answer' },
+ });
- const progress = project.getReconciliationProgress(studyId);
+ // Wait for Y.js update to complete
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ const progress = project.getReconciliationProgress(studyId);
- expect(progress).toBeDefined();
- expect(progress.checklist1Id).toBe('checklist-1');
- expect(progress.checklist2Id).toBe('checklist-2');
- expect(progress.currentPage).toBe(3);
- expect(progress.viewMode).toBe('questions');
- expect(progress.finalAnswers).toEqual({ q1: 'answer' });
+ expect(progress).toBeDefined();
+ expect(progress.checklist1Id).toBe('checklist-1');
+ expect(progress.checklist2Id).toBe('checklist-2');
+ expect(progress.currentPage).toBe(3);
+ expect(progress.viewMode).toBe('questions');
+ // Note: finalAnswers are stored in the reconciled checklist, not in progress
+ resolveTest();
+ });
});
});
@@ -558,20 +608,26 @@ describe('useProject - Project Settings', () => {
});
it('should update project settings', async () => {
- createRoot(async dispose => {
- cleanup = dispose;
- const project = useProject('local-test');
+ await new Promise(resolveTest => {
+ createRoot(async dispose => {
+ cleanup = dispose;
+ const project = useProject('local-test');
- await new Promise(resolve => setTimeout(resolve, 10));
+ await new Promise(resolve => setTimeout(resolve, 10));
- projectStore.setProjectData.mockClear();
+ projectStore.setProjectData.mockClear();
- project.updateProjectSettings({
- namingConvention: 'lastNameYear',
- defaultChecklistType: 'AMSTAR2',
- });
+ project.updateProjectSettings({
+ namingConvention: 'lastNameYear',
+ defaultChecklistType: 'AMSTAR2',
+ });
- expect(projectStore.setProjectData).toHaveBeenCalled();
+ // Wait for Y.js update event to trigger sync
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ expect(projectStore.setProjectData).toHaveBeenCalled();
+ resolveTest();
+ });
});
});
});
diff --git a/packages/web/src/primitives/useProject/checklists.js b/packages/web/src/primitives/useProject/checklists.js
index ab1a101db..c4b3d1adb 100644
--- a/packages/web/src/primitives/useProject/checklists.js
+++ b/packages/web/src/primitives/useProject/checklists.js
@@ -13,7 +13,7 @@ import { CHECKLIST_STATUS } from '@/constants/checklist-status.js';
* @param {Function} isSynced - Function that returns sync status
* @returns {Object} Checklist operations
*/
-export function createChecklistOperations(projectId, getYDoc, isSynced) {
+export function createChecklistOperations(projectId, getYDoc, _isSynced) {
/**
* Create a checklist in a study
* @param {string} studyId - The study ID
diff --git a/packages/web/src/primitives/useProject/pdfs.js b/packages/web/src/primitives/useProject/pdfs.js
index f6eab7110..8c0f2c6a1 100644
--- a/packages/web/src/primitives/useProject/pdfs.js
+++ b/packages/web/src/primitives/useProject/pdfs.js
@@ -16,7 +16,7 @@ import * as Y from 'yjs';
* @param {Function} isSynced - Function that returns sync status
* @returns {Object} PDF operations
*/
-export function createPdfOperations(projectId, getYDoc, isSynced) {
+export function createPdfOperations(projectId, getYDoc, _isSynced) {
/**
* Get the pdfs Y.Map for a study, creating it if needed
* @private
diff --git a/packages/web/src/primitives/useProject/reconciliation.js b/packages/web/src/primitives/useProject/reconciliation.js
index 47935b271..7e4be39d7 100644
--- a/packages/web/src/primitives/useProject/reconciliation.js
+++ b/packages/web/src/primitives/useProject/reconciliation.js
@@ -15,7 +15,7 @@ import * as Y from 'yjs';
* @param {Function} isSynced - Function that returns sync status
* @returns {Object} Reconciliation operations
*/
-export function createReconciliationOperations(projectId, getYDoc, isSynced) {
+export function createReconciliationOperations(projectId, getYDoc, _isSynced) {
/**
* Save reconciliation progress for a study
* Stores only metadata references - finalAnswers are in the reconciled checklist
@@ -43,6 +43,12 @@ export function createReconciliationOperations(projectId, getYDoc, isSynced) {
if (progressData.reconciledChecklistId) {
reconciliationMap.set('reconciledChecklistId', progressData.reconciledChecklistId);
}
+ if (progressData.currentPage !== undefined) {
+ reconciliationMap.set('currentPage', progressData.currentPage);
+ }
+ if (progressData.viewMode !== undefined) {
+ reconciliationMap.set('viewMode', progressData.viewMode);
+ }
reconciliationMap.set('updatedAt', Date.now());
studyYMap.set('updatedAt', Date.now());
@@ -72,6 +78,8 @@ export function createReconciliationOperations(projectId, getYDoc, isSynced) {
checklist1Id,
checklist2Id,
reconciledChecklistId: reconciliationMap.get('reconciledChecklistId') || null,
+ currentPage: reconciliationMap.get('currentPage'),
+ viewMode: reconciliationMap.get('viewMode'),
updatedAt: reconciliationMap.get('updatedAt'),
};
}