Skip to content
22 changes: 22 additions & 0 deletions firebase/firestore.indexes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "thesisflow",
"version": "5.7.5",
"version": "5.7.6",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/components/ExpertRequests/ExpertRequestsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ export default function ExpertRequestsPage({ role, roleLabel, allowedRoles }: Ex
</Button>
</span>
</Tooltip>
{expertUid && (
{/* {expertUid && (
<SlotRequestButton
expertUid={expertUid}
expertRole={role}
Expand All @@ -701,7 +701,7 @@ export default function ExpertRequestsPage({ role, roleLabel, allowedRoles }: Ex
size="small"
fullWidth
/>
)}
)} */}
</Stack>
)}
</CardActions>
Expand Down
10 changes: 7 additions & 3 deletions src/hooks/useJobNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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':
Expand Down
17 changes: 8 additions & 9 deletions src/pages/Admin/Management/Experts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -38,14 +37,14 @@ import { formatProfileLabel } from '../../../utils/userUtils';
// Metadata
// ============================================================================

export const metadata: NavigationItem = {
group: 'management',
index: 1,
title: 'Experts',
segment: 'experts',
icon: <SchoolIcon />,
roles: ['admin', 'developer'],
};
// export const metadata: NavigationItem = {
// group: 'management',
// index: 1,
// title: 'Experts',
// segment: 'experts',
// icon: <SchoolIcon />,
// roles: ['admin', 'developer'],
// };

// ============================================================================
// Types
Expand Down
77 changes: 71 additions & 6 deletions src/pages/Student/PanelComments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export default function StudentPanelCommentsPage() {
const [requestingReview, setRequestingReview] = React.useState(false);
const [fileViewerOpen, setFileViewerOpen] = React.useState(false);
const fileInputRef = React.useRef<HTMLInputElement>(null);
/** All entries for the active stage (across all panelists) to check for revision_required */
const [allStageEntries, setAllStageEntries] = React.useState<PanelCommentEntry[]>([]);

/** Build panel comment context from group */
const panelCommentCtx: PanelCommentContext | null = React.useMemo(() => {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 && (
<Alert severity="warning">
The panel has requested revisions. You may upload a new
manuscript and request another review.
</Alert>
)}

<Stack direction="row" spacing={2} alignItems="center">
{/* Allow re-uploading when revision is required */}
{hasRevisionRequired && manuscript.reviewRequested && (
<>
<input
ref={fileInputRef}
type="file"
// eslint-disable-next-line max-len
accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
style={{ display: 'none' }}
onChange={handleFileSelect}
/>
<Button
variant="outlined"
startIcon={
uploadingManuscript
? <CircularProgress size={16} />
: <CloudUploadIcon />
}
onClick={() => fileInputRef.current?.click()}
disabled={uploadingManuscript}
>
{uploadingManuscript ? 'Uploading...' : 'Upload Revised Manuscript'}
</Button>
</>
)}
<Button
variant="contained"
color="primary"
startIcon={requestingReview ? <CircularProgress size={16} /> : <RequestReviewIcon />}
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'}
</Button>
{manuscript.reviewRequested && (
{manuscript.reviewRequested && !hasRevisionRequired && (
<Typography variant="body2" color="success.main">
✓ Review requested on{' '}
{new Date(manuscript.reviewRequestedAt || '').toLocaleDateString()}
Expand Down
13 changes: 13 additions & 0 deletions src/utils/firebase/firestore/panelComments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand Down
14 changes: 7 additions & 7 deletions src/utils/firebase/firestore/skillTemplates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,21 +309,21 @@ export interface SkillsConfigData {
max: number;
labels: Record<string, string>;
};
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 {
Expand Down Expand Up @@ -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<number> {
let created = 0;
Expand Down
10 changes: 8 additions & 2 deletions src/utils/firebase/firestore/slotRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down