diff --git a/firebase/firestore.indexes.json b/firebase/firestore.indexes.json index a750193..a50f591 100644 --- a/firebase/firestore.indexes.json +++ b/firebase/firestore.indexes.json @@ -241,6 +241,28 @@ "order": "DESCENDING" } ] + }, + { + "collectionGroup": "slotRequests", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "expertUid", + "order": "ASCENDING" + }, + { + "fieldPath": "status", + "order": "ASCENDING" + }, + { + "fieldPath": "createdAt", + "order": "DESCENDING" + }, + { + "fieldPath": "__name__", + "order": "DESCENDING" + } + ] } ], "fieldOverrides": [ diff --git a/package.json b/package.json index f246fba..4430353 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "thesisflow", - "version": "5.7.5", + "version": "5.7.6", "type": "module", "scripts": { "dev": "vite", @@ -42,7 +42,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "globals": "^16.5.0", - "googleapis": "^168.0.0", + "googleapis": "^169.0.0", "jiti": "^2.6.1", "typescript": "^5.9.3", "typescript-eslint": "^8.49.0", diff --git a/src/components/ExpertRequests/ExpertRequestsPage.tsx b/src/components/ExpertRequests/ExpertRequestsPage.tsx index df083da..3a4d598 100644 --- a/src/components/ExpertRequests/ExpertRequestsPage.tsx +++ b/src/components/ExpertRequests/ExpertRequestsPage.tsx @@ -692,7 +692,7 @@ export default function ExpertRequestsPage({ role, roleLabel, allowedRoles }: Ex - {expertUid && ( + {/* {expertUid && ( - )} + )} */} )} diff --git a/src/hooks/useJobNotifications.ts b/src/hooks/useJobNotifications.ts index e301ca6..fae6895 100644 --- a/src/hooks/useJobNotifications.ts +++ b/src/hooks/useJobNotifications.ts @@ -138,8 +138,12 @@ export function useJobNotifications() { } }); - // Update previous jobs reference - previousJobsRef.current = [...jobs]; + // Update previous jobs reference with deep copies to avoid reference issues + // (jobs array contains references to mutable objects that get updated in place) + previousJobsRef.current = jobs.map(j => ({ + ...j, + metadata: j.metadata ? { ...j.metadata } : undefined, + })); }); return () => { @@ -161,7 +165,7 @@ function getJobMessage(job: BackgroundJob): string { case 'pending': return 'Waiting to start...'; case 'running': - return `Processing... ${job.progress}%`; + return 'Processing...'; case 'completed': return 'Completed successfully'; case 'failed': diff --git a/src/pages/Admin/Management/Experts.tsx b/src/pages/Admin/Management/Experts.tsx index fecbc5d..a712ce5 100644 --- a/src/pages/Admin/Management/Experts.tsx +++ b/src/pages/Admin/Management/Experts.tsx @@ -17,7 +17,6 @@ import { AnimatedPage, GrowTransition } from '../../../components/Animate'; import ProfileView from '../../../components/Profile/ProfileView'; import { SkillRatingForm } from '../../../components/SkillRating'; import { useSnackbar } from '../../../contexts/SnackbarContext'; -import type { NavigationItem } from '../../../types/navigation'; import type { Session } from '../../../types/session'; import type { UserProfile, UserRole } from '../../../types/profile'; import type { ThesisGroup } from '../../../types/group'; @@ -38,14 +37,14 @@ import { formatProfileLabel } from '../../../utils/userUtils'; // Metadata // ============================================================================ -export const metadata: NavigationItem = { - group: 'management', - index: 1, - title: 'Experts', - segment: 'experts', - icon: , - roles: ['admin', 'developer'], -}; +// export const metadata: NavigationItem = { +// group: 'management', +// index: 1, +// title: 'Experts', +// segment: 'experts', +// icon: , +// roles: ['admin', 'developer'], +// }; // ============================================================================ // Types diff --git a/src/pages/Student/PanelComments.tsx b/src/pages/Student/PanelComments.tsx index 5d99518..f9682d1 100644 --- a/src/pages/Student/PanelComments.tsx +++ b/src/pages/Student/PanelComments.tsx @@ -85,6 +85,8 @@ export default function StudentPanelCommentsPage() { const [requestingReview, setRequestingReview] = React.useState(false); const [fileViewerOpen, setFileViewerOpen] = React.useState(false); const fileInputRef = React.useRef(null); + /** All entries for the active stage (across all panelists) to check for revision_required */ + const [allStageEntries, setAllStageEntries] = React.useState([]); /** Build panel comment context from group */ const panelCommentCtx: PanelCommentContext | null = React.useMemo(() => { @@ -229,6 +231,30 @@ export default function StudentPanelCommentsPage() { return () => unsubscribe(); }, [panelCommentCtx, activeStage]); + // Listen to ALL entries for the active stage (across all panelists) to detect revision_required + React.useEffect(() => { + if (!panelCommentCtx) { + setAllStageEntries([]); + return; + } + const unsubscribe = listenPanelCommentEntries(panelCommentCtx, activeStage, { + onData: (next) => setAllStageEntries(next), + onError: (error) => { + console.error('All entries listener error:', error); + setAllStageEntries([]); + }, + }); + return () => unsubscribe(); + }, [panelCommentCtx, activeStage]); + + /** + * Check if any panel comment entry has revision_required status. + * When true, students should be able to re-upload their manuscript. + */ + const hasRevisionRequired = React.useMemo(() => { + return allStageEntries.some((entry) => entry.approvalStatus === 'revision_required'); + }, [allStageEntries]); + React.useEffect(() => { let isMounted = true; async function loadPanelists() { @@ -661,25 +687,64 @@ export default function StudentPanelCommentsPage() { metaLabel={`Uploaded ${new Date(manuscript.uploadedAt).toLocaleDateString()}`} onClick={() => setFileViewerOpen(true)} onDownload={() => window.open(manuscript.url, '_blank', 'noopener,noreferrer')} - onDelete={!manuscript.reviewRequested ? handleDeleteManuscript : undefined} + onDelete={ + (!manuscript.reviewRequested || hasRevisionRequired) + ? handleDeleteManuscript + : undefined + } showDownloadButton - showDeleteButton={!manuscript.reviewRequested} + showDeleteButton={!manuscript.reviewRequested || hasRevisionRequired} disabled={uploadingManuscript} /> + {/* Show revision required alert */} + {hasRevisionRequired && manuscript.reviewRequested && ( + + The panel has requested revisions. You may upload a new + manuscript and request another review. + + )} + + {/* Allow re-uploading when revision is required */} + {hasRevisionRequired && manuscript.reviewRequested && ( + <> + + + + )} - {manuscript.reviewRequested && ( + {manuscript.reviewRequested && !hasRevisionRequired && ( ✓ Review requested on{' '} {new Date(manuscript.reviewRequestedAt || '').toLocaleDateString()} diff --git a/src/utils/firebase/firestore/panelComments.ts b/src/utils/firebase/firestore/panelComments.ts index d1ab563..ebcc2c0 100644 --- a/src/utils/firebase/firestore/panelComments.ts +++ b/src/utils/firebase/firestore/panelComments.ts @@ -639,6 +639,7 @@ function generateManuscriptStoragePath( /** * Upload a manuscript file for panel review. + * If a manuscript already exists, the old file is deleted from storage first. * @param ctx - Panel comment context * @param stage - The panel comment stage * @param file - The file to upload @@ -655,6 +656,18 @@ export async function uploadPanelManuscript( throw new Error('Group ID is required to upload manuscript.'); } + // Delete existing manuscript from storage if it exists (to avoid orphaned files) + const existingManuscript = await getPanelManuscript(ctx, stage); + if (existingManuscript?.storagePath) { + try { + const oldStorageRef = ref(firebaseStorage, existingManuscript.storagePath); + await deleteObject(oldStorageRef); + } catch (storageError) { + // Log but don't fail - file may already be deleted or not exist + console.warn('Failed to delete old manuscript from storage:', storageError); + } + } + const storagePath = generateManuscriptStoragePath(ctx, stage, file.name, userUid); const storageRef = ref(firebaseStorage, storagePath); diff --git a/src/utils/firebase/firestore/skillTemplates.ts b/src/utils/firebase/firestore/skillTemplates.ts index 6de739a..8093a4f 100644 --- a/src/utils/firebase/firestore/skillTemplates.ts +++ b/src/utils/firebase/firestore/skillTemplates.ts @@ -309,21 +309,21 @@ export interface SkillsConfigData { max: number; labels: Record; }; - defaultSkills: Array<{ + defaultSkills: { name: string; description?: string; category?: string; keywords?: string[]; - }>; - departmentTemplates: Array<{ + }[]; + departmentTemplates: { department: string; - skills: Array<{ + skills: { name: string; description?: string; category?: string; keywords?: string[]; - }>; - }>; + }[]; + }[]; } export interface SeedSkillsResult { @@ -354,7 +354,7 @@ export async function departmentHasSkills( export async function seedDepartmentSkills( year: string, department: string, - skills: Array<{ name: string; description?: string; category?: string; keywords?: string[] }>, + skills: { name: string; description?: string; category?: string; keywords?: string[] }[], creatorUid?: string ): Promise { let created = 0; diff --git a/src/utils/firebase/firestore/slotRequests.ts b/src/utils/firebase/firestore/slotRequests.ts index dc0b1d3..af9403d 100644 --- a/src/utils/firebase/firestore/slotRequests.ts +++ b/src/utils/firebase/firestore/slotRequests.ts @@ -140,13 +140,19 @@ export async function createSlotRequest( expertRole, currentSlots, requestedSlots, - reason: reason || undefined, status: 'pending', createdAt: now, updatedAt: now, - department, }; + // Only include optional fields if they have values (Firestore doesn't accept undefined) + if (reason) { + requestData.reason = reason; + } + if (department) { + requestData.department = department; + } + await setDoc(docRef, { ...requestData, createdAt: serverTimestamp(),