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 && (
+ <>
+
+
+ :
+ }
+ onClick={() => fileInputRef.current?.click()}
+ disabled={uploadingManuscript}
+ >
+ {uploadingManuscript ? 'Uploading...' : 'Upload Revised Manuscript'}
+
+ >
+ )}
: }
onClick={handleRequestReview}
- disabled={requestingReview || manuscript.reviewRequested}
+ disabled={requestingReview || (manuscript.reviewRequested && !hasRevisionRequired)}
>
- {manuscript.reviewRequested
+ {manuscript.reviewRequested && !hasRevisionRequired
? 'Review Requested'
- : 'Request Panel Review'}
+ : hasRevisionRequired
+ ? 'Request Re-Review'
+ : 'Request Panel Review'}
- {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(),