diff --git a/apps/scoop-portal/server/api/journal/route.js b/apps/scoop-portal/server/api/journal/route.js
index 91aaf59a..774324ee 100644
--- a/apps/scoop-portal/server/api/journal/route.js
+++ b/apps/scoop-portal/server/api/journal/route.js
@@ -7,7 +7,7 @@ const prisma = new PrismaClient();
* GET all journal entries
*
* @param {Object} req - The request object
- * @param {Object} res - The response object to send all jurnal entries or an error
+ * @param {Object} res - The response object to send all journal entries or an error
*/
router.get("/", async (req, res) => {
try {
@@ -15,10 +15,7 @@ router.get("/", async (req, res) => {
res.status(200).json(entries);
} catch (error) {
console.error("Error fetching journal entries:", error);
- res.status(500).json({
- message: "Error fetching journal entries",
- error: error.message,
- });
+ res.status(500).json({ message: "Error fetching journal entries", error: error.message });
}
});
@@ -50,48 +47,39 @@ router.post("/", async (req, res) => {
sender_id,
topic_id,
semester_GroupId: semester_GroupId ? Number(semester_GroupId) : null,
- previous_entryid: previous_entryid,
- entry_type: entry_type,
- visibility_level: visibility_level,
- privacy_level: privacy_level,
+ previous_entryid,
+ entry_type,
+ visibility_level,
+ privacy_level,
},
- include:{
+ include: {
sender: true,
recipients: true,
topic: true,
- next_entries: {
- where:{
- OR:[
- { privacy_level: "PUBLIC" },
- { sender_id: sender_id }
- ],
- },
- include:{
- sender: true,
- recipients: true,
- topic: true,
- },
- },
+ next_entries: {
+ where: {
+ OR: [
+ { privacy_level: "PUBLIC" },
+ { sender_id: sender_id }
+ ],
+ },
+ include: {
+ sender: true,
+ recipients: true,
+ topic: true,
+ },
+ },
},
});
notifyStatus({ userId: "zim1902", context: { journalEntryId: newEntry.id, notes: newEntry.notes } })
- .then(summary => {
- console.log('Notification sent:', summary)
- })
- .catch(error => {
- console.error('Error sending notification:', error);
- });
+ .then(summary => console.log('Notification sent:', summary))
+ .catch(error => console.error('Error sending notification:', error));
- res
- .status(200)
- .json({ message: "New journal entry created", entry: newEntry });
+ res.status(200).json({ message: "New journal entry created", entry: newEntry });
} catch (error) {
console.error("Error creating journal entry:", error);
- res.status(500).json({
- message: "Error creating journal entry",
- error: error.message,
- });
+ res.status(500).json({ message: "Error creating journal entry", error: error.message });
}
});
@@ -112,72 +100,57 @@ router.put("/:id", async (req, res) => {
res.status(200).json(updatedEntry);
} catch (error) {
console.error("Error updating journal entry:", error);
- res.status(500).json({
- message: "Error updating journal entry",
- error: error.message,
- });
+ res.status(500).json({ message: "Error updating journal entry", error: error.message });
}
});
/**
* GET journal entries for scoopdinator
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object to send scoopdinator journal entries or an error
*/
router.get("/scoopdinator", async (req, res) => {
- // Get query parameters from URL
const { semester_GroupId, contactee_fname, contactee_lname } = req.query;
-
- // Develop whereClause conditionally for fitlering
const whereClause = { journal_owner_type: "scoopdinator" };
- if (semester_GroupId) {
- whereClause.semester_GroupId = Number(semester_GroupId);
- }
- if (contactee_fname) {
- whereClause.contactee_fname = contactee_fname;
- }
- if (contactee_lname) {
- whereClause.contactee_lname = contactee_lname;
- }
-
- // Develop orderByClause for ordering
- const orderByClause = { date: "desc" };
-
- // Fetch journal entries based on the two clauses
+ if (semester_GroupId) whereClause.semester_GroupId = Number(semester_GroupId);
+ if (contactee_fname) whereClause.contactee_fname = contactee_fname;
+ if (contactee_lname) whereClause.contactee_lname = contactee_lname;
try {
const scoopdinatorEntries = await prisma.journalEntry.findMany({
where: whereClause,
- orderBy: orderByClause,
+ orderBy: { date: "desc" },
});
res.status(200).json(scoopdinatorEntries);
} catch (error) {
console.error("Error fetching the scoopdinator journal entries: ", error);
- res.status(500).json({
- message: "Error fetching scoopdinator journal entries",
- error: error.message,
- });
+ res.status(500).json({ message: "Error fetching scoopdinator journal entries", error: error.message });
}
});
/**
* GET journal entries for scoopervisor
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object to send scoopervisor journal entries or an error
*/
router.get("/scoopervisor", async (req, res) => {
- const whereClause = { journal_owner_type: "scoopervisor" };
try {
const scoopervisorEntries = await prisma.journalEntry.findMany({
- where: whereClause,
+ where: { journal_owner_type: "scoopervisor" },
});
res.status(200).json(scoopervisorEntries);
} catch (error) {
console.error("Error fetching the scoopervisor journal entries: ", error);
- res.status(500).json({
- message: "Error fetching scoopervisor journal entries",
- error: error.message,
- });
+ res.status(500).json({ message: "Error fetching scoopervisor journal entries", error: error.message });
}
});
/**
* GET journal entries for scooployee
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object to send scooployee journal entries or an error
*/
router.get("/scooployee", async (req, res) => {
try {
@@ -187,244 +160,238 @@ router.get("/scooployee", async (req, res) => {
res.status(200).json(scooployeeEntries);
} catch (error) {
console.error("Error fetching the scooployee journal entries: ", error);
- res.status(500).json({
- message: "Error fetching scooployee journal entries",
- error: error.message,
- });
+ res.status(500).json({ message: "Error fetching scooployee journal entries", error: error.message });
}
});
/**
* GET journal entries for user with id
+ *
+ * @param {Object} req - The request object
+ * @param {Object} res - The response object to send user's journal entries or an error
*/
router.get("/:id", async (req, res) => {
- const {id} = req.params;
-
- try {
- const user = await prisma.users.findUnique({
- where: { id: id },
- });
+ const { id } = req.params;
+ try {
+ const user = await prisma.users.findUnique({ where: { id } });
let entries = [];
- if(user.type == "scooployee"){
- const scooployeeEntries = await prisma.journalEntry.findMany({
+ if (user.type == "scooployee") {
+ // Direct recipients always see the entry regardless of visibility_level
+ const recipientEntries = await prisma.journalEntry.findMany({
+ where: {
+ recipients: { some: { id } },
+ privacy_level: "PUBLIC",
+ },
+ include: {
+ sender: true,
+ recipients: true,
+ topic: true,
+ next_entries: {
+ where: {
+ OR: [{ privacy_level: "PUBLIC" }, { sender_id: id }],
+ },
+ include: { sender: true, recipients: true, topic: true },
+ },
+ },
+ });
+
+ const generalEntries = await prisma.journalEntry.findMany({
where: {
OR: [
- {sender_id: id},
- { recipients: {
- some: {
- id: id,
- },},},
+ { sender_id: id },
{ topic_id: id },
],
privacy_level: "PUBLIC",
- visibility_level: {
- lt: 2
- },
+ visibility_level: { lt: 2 },
},
include: {
sender: true,
recipients: true,
topic: true,
next_entries: {
- where:{
- OR:[
- { privacy_level: "PUBLIC" },
- { sender_id: id }
- ],
- visibility_level: {
- lt: 2
- },
- },
- include:{
- sender: true,
- recipients: true,
- topic: true,
+ where: {
+ OR: [{ privacy_level: "PUBLIC" }, { sender_id: id }],
+ visibility_level: { lt: 2 },
},
+ include: { sender: true, recipients: true, topic: true },
},
},
- })
- //res.status(200).json(scooployeeEntries);
- entries = entries.concat(scooployeeEntries);
- }
- else if(user.type == "scoopdinator"){
+ });
+
+ entries = entries.concat(recipientEntries, generalEntries);
+
+ } else if (user.type == "scoopdinator") {
const dinatorEntries = await prisma.journalEntry.findMany({
- where:{
+ where: {
privacy_level: "PUBLIC",
- visibility_level: {
- lt: 5
- },
+ visibility_level: { lt: 5 },
},
include: {
sender: true,
recipients: true,
topic: true,
next_entries: {
- where:{
- OR:[
- { privacy_level: "PUBLIC" },
- { sender_id: id }
- ],
- visibility_level: {
- lt: 5
- },
- },
- include: {
- sender: true,
- recipients: true,
- topic: true,
+ where: {
+ OR: [{ privacy_level: "PUBLIC" }, { sender_id: id }],
+ visibility_level: { lt: 5 },
},
+ include: { sender: true, recipients: true, topic: true },
},
},
});
- //res.status(200).json(dinatorEntries);
entries = entries.concat(dinatorEntries);
- }
- else if(user.type == "scoopervisor"){
- const scoopervisorTeams = await prisma.teams.findMany({
+
+ } else if (user.type == "scoopervisor") {
+ // Direct recipients always see the entry regardless of visibility_level
+ const recipientEntries = await prisma.journalEntry.findMany({
where: {
- scoopervisorId: user.id,
+ recipients: { some: { id } },
+ privacy_level: "PUBLIC",
},
- include: {
- members: true,
- }
- });
-
- const memberSet = new Set();
-
- for (const team of scoopervisorTeams) {
- for (const member of team.members) {
- memberSet.add(member.id)
- }
- }
- const memberArray = Array.from(memberSet);
- //this currently allows Scoopervisors to see entries in which they are the topic
- const scoopervisorEntries = await prisma.journalEntry.findMany({
+ include: {
+ sender: true,
+ recipients: true,
+ topic: true,
+ next_entries: {
where: {
- OR: memberArray.flatMap(memberId => [
- { sender_id: memberId },
- { recipients: {
- some: {
- id: memberId,
- },},},
- { topic_id: memberId },
- ]),
- privacy_level: "PUBLIC",
- visibility_level: {
- lt: 4
- },
- },
- include: {
- sender: true,
- recipients: true,
- topic: true,
- next_entries: {
- where:{
- OR:[
- { privacy_level: "PUBLIC" },
- { sender_id: id }
- ],
- visibility_level: {
- lt: 4
- },
- },
- include:{
- sender: true,
- recipients: true,
- topic: true,
- },
- },
+ OR: [{ privacy_level: "PUBLIC" }, { sender_id: id }],
},
- });
- //res.status(200).json(visorEntries);
- entries = entries.concat(scoopervisorEntries);
+ include: { sender: true, recipients: true, topic: true },
+ },
+ },
+ });
+
+ const scoopervisorTeams = await prisma.teams.findMany({
+ where: { scoopervisorId: user.id },
+ include: { members: true },
+ });
+
+ const memberSet = new Set();
+ for (const team of scoopervisorTeams) {
+ for (const member of team.members) {
+ memberSet.add(member.id);
+ }
}
- else if(user.type == "advisor"){
- const advisorEntries = await prisma.journalEntry.findMany({
+ const memberArray = Array.from(memberSet);
+
+ const teamEntries = await prisma.journalEntry.findMany({
where: {
+ OR: memberArray.flatMap(memberId => [
+ { sender_id: memberId },
+ { recipients: { some: { id: memberId } } },
+ { topic_id: memberId },
+ ]),
privacy_level: "PUBLIC",
- visibility_level: {
- lt: 3
- },
+ visibility_level: { lt: 4 },
},
include: {
sender: true,
recipients: true,
topic: true,
next_entries: {
- where:{
- OR:[
- { privacy_level: "PUBLIC" },
- { sender_id: id }
- ],
- visibility_level: {
- lt: 3
- },
- },
- include:{
- sender: true,
- recipients: true,
- topic: true,
+ where: {
+ OR: [{ privacy_level: "PUBLIC" }, { sender_id: id }],
+ visibility_level: { lt: 4 },
},
+ include: { sender: true, recipients: true, topic: true },
},
},
- })
- entries = entries.concat(advisorEntries);
- }
- const privateEntries = await prisma.journalEntry.findMany({
- where:{
- privacy_level: "PERSONAL",
- sender_id: id,
+ });
+ entries = entries.concat(recipientEntries, teamEntries);
+
+ } else if (user.type == "advisor") {
+ // Direct recipients always see the entry regardless of visibility_level
+ const recipientEntries = await prisma.journalEntry.findMany({
+ where: {
+ recipients: { some: { id } },
+ privacy_level: "PUBLIC",
},
include: {
sender: true,
recipients: true,
topic: true,
next_entries: {
- where:{
- OR:[
- { privacy_level: "PUBLIC" },
- { sender_id: id }
- ],
+ where: {
+ OR: [{ privacy_level: "PUBLIC" }, { sender_id: id }],
},
- include: {
- sender: true,
- recipients: true,
- topic: true,
+ include: { sender: true, recipients: true, topic: true },
+ },
+ },
+ });
+
+ const advisorEntries = await prisma.journalEntry.findMany({
+ where: {
+ privacy_level: "PUBLIC",
+ visibility_level: { lt: 3 },
+ },
+ include: {
+ sender: true,
+ recipients: true,
+ topic: true,
+ next_entries: {
+ where: {
+ OR: [{ privacy_level: "PUBLIC" }, { sender_id: id }],
+ visibility_level: { lt: 3 },
},
+ include: { sender: true, recipients: true, topic: true },
},
},
});
- entries = entries.concat(privateEntries);
- res.status(200).json(entries);
+ entries = entries.concat(recipientEntries, advisorEntries);
+ }
+
+ const privateEntries = await prisma.journalEntry.findMany({
+ where: {
+ privacy_level: "PERSONAL",
+ sender_id: id,
+ },
+ include: {
+ sender: true,
+ recipients: true,
+ topic: true,
+ next_entries: {
+ where: {
+ OR: [{ privacy_level: "PUBLIC" }, { sender_id: id }],
+ },
+ include: { sender: true, recipients: true, topic: true },
+ },
+ },
+ });
+ entries = entries.concat(privateEntries);
+
+ // Deduplicate entries
+ const seen = new Set();
+ const deduped = entries.filter(entry => {
+ if (seen.has(entry.id)) return false;
+ seen.add(entry.id);
+ return true;
+ });
+
+ res.status(200).json(deduped);
} catch (error) {
console.error("Error fetching the users journal entries: ", error);
- res.status(500).json({
- message: "Error fetching users journal entries",
- error: error.message,
- });
+ res.status(500).json({ message: "Error fetching users journal entries", error: error.message });
}
-})
+});
+
+const NOTIFY_BASE = process.env.NOTIFY_BASE || 'http://localhost:4000/api/notifications';
+const APP_ID = process.env.APP_ID || 'scoop';
-const NOTIFY_BASE = process.env.NOTIFY_BASE || 'http://localhost:4000/api/notifications'
-const APP_ID = process.env.APP_ID || 'scoop'
export async function notifyStatus({ userId, context }) {
const res = await fetch(`${NOTIFY_BASE}/dispatch/${APP_ID}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- userId: userId,
+ userId,
subject: "New Journal Entry Created",
message: context.notes || "A new journal entry has been created.",
- })
- })
-
-
- const data = await res.json()
- if (!res.ok) throw new Error(`Dispatch failed: ${res.status} ${JSON.stringify(data)}`)
- return data.summary
+ }),
+ });
+ const data = await res.json();
+ if (!res.ok) throw new Error(`Dispatch failed: ${res.status} ${JSON.stringify(data)}`);
+ return data.summary;
}
-
-export default router;
+export default router;
\ No newline at end of file
diff --git a/apps/scoop-portal/ui/src/app/_components/journal/JournalHeader.js b/apps/scoop-portal/ui/src/app/_components/journal/JournalHeader.js
index 35db65a5..9bc86959 100644
--- a/apps/scoop-portal/ui/src/app/_components/journal/JournalHeader.js
+++ b/apps/scoop-portal/ui/src/app/_components/journal/JournalHeader.js
@@ -6,7 +6,6 @@ export default function JournalHeader({
setFilterDialogOpen,
setNewEntryOpen,
handleJSONDownload
-
}) {
const handleOpenFilterDialog = () => setFilterDialogOpen(true);
diff --git a/apps/scoop-portal/ui/src/app/bubbles/layout.js b/apps/scoop-portal/ui/src/app/bubbles/layout.js
new file mode 100644
index 00000000..0cb1b0f9
--- /dev/null
+++ b/apps/scoop-portal/ui/src/app/bubbles/layout.js
@@ -0,0 +1,12 @@
+import ProtectedRoute from "../utils/ProtectedRoute";
+import { Box } from "@mui/material";
+
+export default function bubblesLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/scoop-portal/ui/src/app/dashboard/layout.js b/apps/scoop-portal/ui/src/app/dashboard/layout.js
new file mode 100644
index 00000000..b2313bc1
--- /dev/null
+++ b/apps/scoop-portal/ui/src/app/dashboard/layout.js
@@ -0,0 +1,12 @@
+import ProtectedRoute from "../utils/ProtectedRoute";
+import { Box } from "@mui/material";
+
+export default function dashboardLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/scoop-portal/ui/src/app/journal/layout.js b/apps/scoop-portal/ui/src/app/journal/layout.js
new file mode 100644
index 00000000..c2370e44
--- /dev/null
+++ b/apps/scoop-portal/ui/src/app/journal/layout.js
@@ -0,0 +1,12 @@
+import ProtectedRoute from "../utils/ProtectedRoute";
+import { Box } from "@mui/material";
+
+export default function journalLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/scoop-portal/ui/src/app/journal/page.js b/apps/scoop-portal/ui/src/app/journal/page.js
index 341ec3a5..f661334b 100644
--- a/apps/scoop-portal/ui/src/app/journal/page.js
+++ b/apps/scoop-portal/ui/src/app/journal/page.js
@@ -4,7 +4,6 @@ import React, { useEffect, useState } from "react";
import FilterDialog from "@components/FilterDialog";
import Header from "@components/Header";
import JournalHeader from "@components/journal/JournalHeader";
-import EditNoteIcon from "@mui/icons-material/EditNote";
import { useUser } from "../utils/user-context/page";
import {
Autocomplete,
@@ -17,8 +16,6 @@ import {
DialogActions,
DialogContent,
DialogTitle,
- FormControl,
- IconButton,
MenuItem,
Paper,
Select,
@@ -27,10 +24,9 @@ import {
Tooltip,
Typography,
useTheme,
- alpha
+ alpha
} from "@mui/material";
-// MUI Lab imports for Timeline
import Timeline from "@mui/lab/Timeline";
import TimelineItem from "@mui/lab/TimelineItem";
import TimelineSeparator from "@mui/lab/TimelineSeparator";
@@ -41,23 +37,19 @@ import TimelineOppositeContent, { timelineOppositeContentClasses } from "@mui/la
import toast, { Toaster } from "react-hot-toast";
-/**
- * Renders the content for the Journal Page
- * @returns {JSX.Element}
- */
export default function Journal() {
const theme = useTheme();
-
- // -- State variables --
+
const [journalEntries, setJournalEntries] = useState([]);
const [filteredJournalEntries, setFilteredJournalEntries] = useState([]);
const [users, setUsers] = useState({});
+ const [allUsers, setAllUsers] = useState([]);
+ const [scooployeeUsers, setScooployeeUsers] = useState({});
const [semesterGroups, setSemesterGroups] = useState({});
const [newEntryOpen, setNewEntryOpen] = useState(false);
const [filterDialogOpen, setFilterDialogOpen] = useState(false);
-
- // Filter States
+
const [filterSemesterValue, setFilterSemesterValue] = useState("");
const [filterSenderValue, setFilterSenderValue] = useState("");
const [filterRecipientValue, setFilterRecipientValue] = useState("");
@@ -65,27 +57,27 @@ export default function Journal() {
const [filterEntryTypeValue, setFilterEntryTypeValue] = useState("");
const [filterTimeValue, setFilterTimeValue] = useState("newest_first");
- // New Entry States
+ const [newEntryNotes, setNewEntryNotes] = useState("");
const [newEntrySemester, setNewEntrySemester] = useState("");
const [newEntryRecipientIds, setNewEntryRecipientIds] = useState([]);
+ const [newEntryRecipientObjects, setNewEntryRecipientObjects] = useState([]);
const [newEntryTopicId, setNewEntryTopicId] = useState("");
const [newEntryPreviousId, setNewEntryPreviousId] = useState(null);
const [newEntryVisibilityLevel, setNewEntryVisibilityLevel] = useState("");
const [newEntryIsComment, setNewEntryIsComment] = useState(false);
-
- // Edit States
+
const [editingEntry, setEditingEntry] = useState(null);
const [editValue, setEditValue] = useState("");
const [replyEntry, setReplyEntry] = useState(null);
const { user } = useUser();
-
+
useEffect(() => {
- async function fetchEntries () {
+ async function fetchEntries() {
if (user == null || user.id == null) return;
try {
const [entriesRes, usersRes, semestersRes] = await Promise.all([
- fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/journal/${user.id}`),
+ fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/journal/${user.id}`),
fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/users`),
fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/semestergroup`),
]);
@@ -100,10 +92,16 @@ export default function Journal() {
handleApplyFilter(Array.isArray(entries) ? entries : []);
const contacteeMap = {};
+ const scooployeeMap = {};
loadedUsers.forEach((loadedUser) => {
contacteeMap[loadedUser.id] = `${loadedUser.fname} ${loadedUser.lname}`;
+ if (loadedUser.type === "scooployee") {
+ scooployeeMap[loadedUser.id] = `${loadedUser.fname} ${loadedUser.lname}`;
+ }
});
setUsers(contacteeMap);
+ setAllUsers(loadedUsers);
+ setScooployeeUsers(scooployeeMap);
const semesterGroupMap = {};
semesterGroupsData.forEach((group) => {
@@ -118,23 +116,19 @@ export default function Journal() {
fetchEntries();
}, [user]);
- //This is how the JSON download of all of the Journal entresi for a person will be done
-
- const handleJSONDownload = () =>{
- const JSONString = JSON.stringify(journalEntries,null,2);
- const blob = new Blob([JSONString],{type:"application/json"});
+ const handleJSONDownload = () => {
+ const JSONString = JSON.stringify(journalEntries, null, 2);
+ const blob = new Blob([JSONString], { type: "application/json" });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `journal_entries_${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(link);
link.click();
-
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
- // -- Filters --
const handleFilterSemesterChange = (e) => setFilterSemesterValue(e.target.value || "");
const handleFilterRecipientChange = (e) => setFilterRecipientValue(e.target.value || "");
const handleFilterSenderChange = (e) => setFilterSenderValue(e.target.value || "");
@@ -142,49 +136,68 @@ export default function Journal() {
const handleFilterEntryTypeChange = (e) => setFilterEntryTypeValue(e.target.value || "");
const handleFilterTimeChange = (e) => setFilterTimeValue(e.target.value || "");
- const handleApplyFilter = async (baseArray=null) => {
- if(baseArray == null) baseArray = Array.from(journalEntries);
- if(filterTopicValue) baseArray = baseArray.filter((entry) => entry.topic_id == filterTopicValue);
+ const handleApplyFilter = (baseArray = null) => {
+ if (baseArray == null) baseArray = Array.from(journalEntries);
+ if (filterTopicValue) baseArray = baseArray.filter((entry) => entry.topic_id == filterTopicValue);
if (filterSemesterValue) baseArray = baseArray.filter((entry) => entry.semester_GroupId == filterSemesterValue);
- if(filterRecipientValue) baseArray = baseArray.filter(entry => entry.recipients.some(recipient => recipient.id === filterRecipientValue));
- if(filterSenderValue) baseArray = baseArray.filter((entry) => entry.sender_id == filterSenderValue);
- if(filterEntryTypeValue) baseArray = baseArray.filter((entry) => entry.entry_type == filterEntryTypeValue);
-
- if(filterTimeValue === "oldest_first") baseArray.sort((a,b) => new Date(a.date) - new Date(b.date));
- if(filterTimeValue === "newest_first") baseArray.sort((a,b) => new Date(b.date) - new Date(a.date));
-
- setFilteredJournalEntries(baseArray)
+ if (filterRecipientValue) baseArray = baseArray.filter(entry => entry.recipients.some(recipient => recipient.id === filterRecipientValue));
+ if (filterSenderValue) baseArray = baseArray.filter((entry) => entry.sender_id == filterSenderValue);
+ if (filterEntryTypeValue) baseArray = baseArray.filter((entry) => entry.entry_type == filterEntryTypeValue);
+ if (filterTimeValue === "oldest_first") baseArray.sort((a, b) => new Date(a.date) - new Date(b.date));
+ if (filterTimeValue === "newest_first") baseArray.sort((a, b) => new Date(b.date) - new Date(a.date));
+ setFilteredJournalEntries(baseArray);
+ };
+
+ const handleClearFilter = () => {
+ setFilterSemesterValue("");
+ setFilterSenderValue("");
+ setFilterRecipientValue("");
+ setFilterTopicValue("");
+ setFilterEntryTypeValue("");
+ setFilterTimeValue("newest_first");
+ const fullArray = Array.from(journalEntries).sort((a, b) => new Date(b.date) - new Date(a.date));
+ setFilteredJournalEntries(fullArray);
};
const getVisibilityOptions = () => {
- let options = {"PERSONAL": "Private Note"};
- let options_map = {"scooployee": 1, "advisor": 2, "scoopervisor": 3, "scoopdinator": 4};
- if (!user?.id) return options;
- switch(options_map[user.type]){
- case 4: options["4"] = "Scoopdinators only";
- case 3: options["3"] = "Scoopervisors and higher";
- case 2: options["2"] = "Advisors and higher";
- case 1: options["1"] = "Scooployees and higher";
- break;
- }
+ const roleRank = { scooployee: 1, advisor: 2, scoopervisor: 3, scoopdinator: 4 };
+ const rank = user?.type ? (roleRank[user.type] ?? 0) : 0;
+ const options = [{ value: "PERSONAL", label: "Private Note" }];
+ if (rank >= 2) options.push({ value: "1", label: "Everyone" });
+ if (rank >= 2) options.push({ value: "2", label: "Advisors and higher" });
+ if (rank >= 3) options.push({ value: "3", label: "Scoopervisors and higher" });
+ if (rank >= 4) options.push({ value: "4", label: "Scoopdinators only" });
return options;
};
- // -- Edit Logic --
+ const isPrivateEntry = newEntryVisibilityLevel === "PERSONAL";
+
+ const getEligibleRecipients = () => {
+ const roleRank = { scooployee: 1, advisor: 2, scoopervisor: 3, scoopdinator: 4 };
+ const visibilityRank = parseInt(newEntryVisibilityLevel) || 0;
+ return Object.entries(users)
+ .filter(([id]) => {
+ const u = allUsers.find(u => u.id === id);
+ if (!u) return true;
+ return (roleRank[u.type] ?? 0) >= visibilityRank;
+ })
+ .filter(([id]) => !newEntryRecipientIds.includes(id))
+ .map(([id, name]) => ({ label: name, value: id }));
+ };
+
const handleEditClick = (entry) => {
setEditingEntry(entry);
setEditValue(entry.notes);
};
const handleCancelEdit = () => setEditingEntry(null);
-
+
const saveEntryNotes = async (entry) => {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/journal/${entry.id}`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ notes: editValue }),
- }
- );
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ notes: editValue }),
+ });
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
setJournalEntries((prev) => prev.map((e) => (e.id === entry.id ? { ...e, notes: editValue } : e)));
setFilteredJournalEntries((prev) => prev.map((e) => (e.id === entry.id ? { ...e, notes: editValue } : e)));
@@ -198,48 +211,46 @@ export default function Journal() {
};
const handleSaveEdit = (entry) => {
- toast.promise(saveEntryNotes(entry), { loading: "Saving...", success: "Notes saved!", error: "Failed to save notes."});
+ toast.promise(saveEntryNotes(entry), { loading: "Saving...", success: "Notes saved!", error: "Failed to save notes." });
setEditValue("");
};
- // -- New Entry Logic --
const postNewEntry = async () => {
- let privacy_level = newEntryVisibilityLevel === "PERSONAL" ? "PERSONAL" : "PUBLIC";
- let visibility_level = newEntryVisibilityLevel !== "PERSONAL" ? parseInt(newEntryVisibilityLevel) : 1;
-
+ const privacy_level = newEntryVisibilityLevel === "PERSONAL" ? "PERSONAL" : "PUBLIC";
+ const visibility_level = newEntryVisibilityLevel !== "PERSONAL" ? parseInt(newEntryVisibilityLevel) : 1;
+
const entry = {
date: new Date().toISOString(),
sender_id: user.id,
- notes: editValue,
- recipient_ids: newEntryRecipientIds,
+ notes: newEntryNotes,
+ recipient_ids: isPrivateEntry ? [] : newEntryRecipientIds,
topic_id: newEntryTopicId,
semester_GroupId: Number(newEntrySemester),
previous_entryid: parseInt(newEntryPreviousId) || null,
entry_type: "MANUAL",
- visibility_level: visibility_level,
- privacy_level: privacy_level,
+ visibility_level,
+ privacy_level,
};
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/journal`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(entry),
- }
- );
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(entry),
+ });
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
if (data.entry) {
let updatedEntries = Array.from(journalEntries);
- if(!newEntryIsComment){
- updatedEntries = [...updatedEntries, data.entry];
+ if (!newEntryIsComment) {
+ updatedEntries = [...updatedEntries, data.entry];
} else {
updatedEntries = updatedEntries.map((check_entry) => {
if (check_entry.id == parseInt(newEntryPreviousId)) {
- return { ...check_entry, next_entries: [...check_entry.next_entries, data.entry]};
+ return { ...check_entry, next_entries: [...check_entry.next_entries, data.entry] };
}
- return check_entry;
+ return check_entry;
});
}
setJournalEntries(updatedEntries);
@@ -253,49 +264,45 @@ export default function Journal() {
}
};
+ const handleOpenNewEntry = () => {
+ const visibilityOptions = getVisibilityOptions();
+ if (visibilityOptions.length === 1 && visibilityOptions[0].value === "PERSONAL") {
+ setNewEntryVisibilityLevel("PERSONAL");
+ }
+ setNewEntryOpen(true);
+ };
+
const handleCancelNewEntry = () => {
- setNewEntryOpen(false);
- setEditValue("");
+ setNewEntryOpen(false);
+ setTimeout(() => {
+ setNewEntryNotes("");
setNewEntrySemester("");
setNewEntryRecipientIds([]);
+ setNewEntryRecipientObjects([]);
setNewEntryTopicId("");
setNewEntryPreviousId(null);
setNewEntryVisibilityLevel("");
- setNewEntryIsComment(false);
- }
+ setNewEntryIsComment(false);
+ }, 200);
+ };
+
const handleCreateNewEntry = () => {
- if (!newEntrySemester || !newEntryRecipientIds || !newEntryTopicId || !newEntryVisibilityLevel) {
+ const missingRecipients = !isPrivateEntry && !newEntryRecipientIds.length;
+ if (!newEntrySemester || missingRecipients || !newEntryTopicId || !newEntryVisibilityLevel || !newEntryNotes.trim()) {
toast.error("Please fill out all fields.");
return;
}
- toast.promise(postNewEntry(), { loading: "Creating new journal entry...", success: "Journal entry created!", error: "Failed to create a new journal entry."});
+ toast.promise(postNewEntry(), { loading: "Creating new journal entry...", success: "Journal entry created!", error: "Failed to create a new journal entry." });
};
const getInitials = (fname, lname) => {
return `${fname ? fname[0] : ""}${lname ? lname[0] : ""}`.toUpperCase();
};
- //This function will set the filter states back to their base, effectivily clearing the filter
-const handleClearFilter = () =>{
- setFilterSemesterValue("");
- setFilterSenderValue("");
- setFilterRecipientValue("");
- setFilterTopicValue("");
- setFilterEntryTypeValue("");
- setFilterTimeValue("newest_first");
-
- const fullArray = Array.from(journalEntries).sort((a, b) => new Date(b.date) - new Date(a.date));
- setFilteredJournalEntries(fullArray);
-}
-
- /**
- * Component: EntriesList
- * Renders the Timeline with dark-mode support and readable avatars
- */
- function EntriesList({entries, commentView = false} ){
+ function EntriesList({ entries, commentView = false }) {
const isDarkMode = theme.palette.mode === 'dark';
- if(entries == null || entries.length === 0){
+ if (entries == null || entries.length === 0) {
return (
No journal entries found.
@@ -315,15 +322,11 @@ const handleClearFilter = () =>{
{entries.map((entry) => {
const isSender = entry.sender_id === user.id;
const entryDate = new Date(entry.date);
-
- // Determine color for the Avatar bubble
const avatarBgColor = isSender ? theme.palette.primary.main : theme.palette.secondary.main;
- // Determine text color based on contrast to background (fixes black-on-black)
const avatarTextColor = theme.palette.getContrastText(avatarBgColor);
return (
- {/* Left Side: Date */}
{entryDate.toLocaleDateString("en-US", { month: "short", day: "numeric" })}
@@ -336,22 +339,21 @@ const handleClearFilter = () =>{
- {/* Center: Dot/Avatar */}
-
-
{getInitials(entry.sender.fname, entry.sender.lname)}
@@ -360,17 +362,15 @@ const handleClearFilter = () =>{
- {/* Right Side: Content */}
- {
{entry.sender.fname} {entry.sender.lname}
- To: {entry.recipients.map(rec => rec.fname + " " + rec.lname).join(", ")}
+ To: {entry.recipients.length > 0 ? entry.recipients.map(rec => `${rec.fname} ${rec.lname}`).join(", ") : "No recipients"}
-
{isSender && (
- handleEditClick(entry)} sx={{ color: '#FF6A00' }}>
- {/* */}
+
+
)}
- {/* Metadata Chips */}
-
- {semesterGroups[entry.semester_GroupId] && (
-
- )}
-
-
+
+ {semesterGroups[entry.semester_GroupId] && (
+
+ )}
+
+
- {/* The Note Body */}
{
color: 'text.primary'
}}
>
- {
- {/* Footer Actions */}
{commentView && (