From df110031a406d6997043cf7d16f7378976c490c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Diamond?=
<32074058+Andre-Diamond@users.noreply.github.com>
Date: Fri, 31 Jan 2025 09:04:01 +0200
Subject: [PATCH 1/7] feat: Add lodash and react-modal dependencies; refactor
WorkingDocs component for improved type safety and functionality
---
components/SummaryAgendaItems.tsx | 504 +++++++++------
components/SummaryMeetingInfo.tsx | 700 +++++++++++----------
components/SummaryTemplate.tsx | 615 +++++++++---------
components/WorkingDocs.tsx | 91 ++-
package-lock.json | 55 +-
package.json | 5 +-
pages/submit-meeting-summary/index.tsx | 833 ++++++++++++++++++-------
7 files changed, 1747 insertions(+), 1056 deletions(-)
diff --git a/components/SummaryAgendaItems.tsx b/components/SummaryAgendaItems.tsx
index fc0cc78..944568d 100644
--- a/components/SummaryAgendaItems.tsx
+++ b/components/SummaryAgendaItems.tsx
@@ -6,111 +6,137 @@ import { getDefaultAgendaItem } from '../utils/getDefaultAgendaItem';
import { filterFormData } from '../utils/filterFormData';
const SummaryAgendaItems = ({ onUpdate }: any) => {
- const { myVariable, setMyVariable } = useMyVariable();
- const formData = filterFormData(myVariable?.summary);
- const initialAgendaItems = formData.agendaItems?.map((item: any) => ({
- ...getDefaultAgendaItem(),
- ...item
- })) || [getDefaultAgendaItem()];
+ const { myVariable } = useMyVariable();
- const [agendaItems, setAgendaItems] = useState(initialAgendaItems);
+ // Track current `meeting_id` in local state so we only
+ // re‐initialize agenda items when the ID changes.
+ const [localMeetingId, setLocalMeetingId] = useState(myVariable.summary?.meeting_id || null);
- useEffect(() => {
- onUpdate(agendaItems);
- //console.log("agendaItems", agendaItems)
- }, [agendaItems]);
+ // Run `filterFormData` once for our initial agenda items:
+ const initialFilteredData = myVariable.summary
+ ? filterFormData(myVariable.summary)
+ : {};
- useEffect(() => {
- const agendaItemsFromVariable = formData.agendaItems?.map((item: any) => ({
+ const [agendaItems, setAgendaItems] = useState(
+ initialFilteredData.agendaItems?.map((item: any) => ({
...getDefaultAgendaItem(),
...item
- })) || [getDefaultAgendaItem()];
+ })) || [getDefaultAgendaItem()]
+ );
- setAgendaItems(agendaItemsFromVariable);
- }, [myVariable.summary?.agendaItems]);
+ /**
+ * If the meeting_id in context changes (meaning user selected
+ * a new summary or the backend assigned a new ID),
+ * then we re-run `filterFormData` and re‐init local state.
+ */
+ useEffect(() => {
+ const currentId = myVariable.summary?.meeting_id;
+ if (currentId && currentId !== localMeetingId) {
+ const newFiltered = filterFormData(myVariable.summary);
+ const newAgendaItems = newFiltered.agendaItems?.map((item: any) => ({
+ ...getDefaultAgendaItem(),
+ ...item
+ })) || [getDefaultAgendaItem()];
+ setAgendaItems(newAgendaItems);
+ setLocalMeetingId(currentId);
+ }
+ }, [myVariable.summary?.meeting_id, localMeetingId]);
+
+ /**
+ * Notify the parent component (`SummaryTemplate`) whenever
+ * local `agendaItems` change.
+ */
+ useEffect(() => {
+ onUpdate(agendaItems);
+ }, [agendaItems, onUpdate]);
+
+ // Utility to add an entire new "agenda item" section
const addAgendaItem = () => {
- setAgendaItems([...agendaItems, { agenda: "", status: "carry over", townHallUpdates: "", narrative: "", gameRules: "", leaderboard: [""], issues: [""], actionItems: [{ text: "", assignee: "", dueDate: "", status: "todo" }], decisionItems: [{ decision: "", rationale: "", opposing: "", effect: "" }], discussionPoints: [""], learningPoints: [""] }]);
+ setAgendaItems((prev) => [
+ ...prev,
+ { ...getDefaultAgendaItem() }
+ ]);
};
+ // Remove one entire "agenda item" section
const removeAgendaItem = (index: number) => {
- const newAgendaItems = [...agendaItems];
- newAgendaItems.splice(index, 1);
- setAgendaItems(newAgendaItems);
+ setAgendaItems((prev) => {
+ const updated = [...prev];
+ updated.splice(index, 1);
+ return updated;
+ });
};
+ // Add a sub‐item (actionItems, discussionPoints, etc.)
const addItem = (type: string, agendaIndex: number) => {
- const newAgendaItems: any = [...agendaItems];
- if (type === 'actionItems') {
- newAgendaItems[agendaIndex][type].push({ text: "", assignee: "", dueDate: "", status: "todo" });
- } else if (type === 'decisionItems') {
- newAgendaItems[agendaIndex][type].push({ decision: "", rationale: "", opposing: "", effect: "" });
- } else {
- newAgendaItems[agendaIndex][type].push("");
- }
- setAgendaItems(newAgendaItems);
+ setAgendaItems((prev) => {
+ const updated = [...prev];
+ if (type === 'actionItems') {
+ updated[agendaIndex][type].push({
+ text: "",
+ assignee: "",
+ dueDate: "",
+ status: "todo"
+ });
+ } else if (type === 'decisionItems') {
+ updated[agendaIndex][type].push({
+ decision: "",
+ rationale: "",
+ opposing: "",
+ effect: ""
+ });
+ } else {
+ // e.g. discussionPoints, issues, meetingTopics, leaderboard
+ updated[agendaIndex][type].push("");
+ }
+ return updated;
+ });
};
-
+ // Remove a sub‐item
const removeItem = (type: string, agendaIndex: number, itemIndex: number) => {
- const newAgendaItems: any = [...agendaItems];
- newAgendaItems[agendaIndex][type].splice(itemIndex, 1);
- setAgendaItems(newAgendaItems);
+ setAgendaItems((prev) => {
+ const updated = [...prev];
+ updated[agendaIndex][type].splice(itemIndex, 1);
+ return updated;
+ });
};
- const handleItemUpdate = (type: any, agendaIdx: any, itemIdx: any, updatedItem: any) => {
- setAgendaItems((prevAgendaItems: any) => {
- const newAgendaItems = JSON.parse(JSON.stringify(prevAgendaItems));
+ // Update a sub‐item (like one actionItem, or one discussionPoint, etc.)
+ const handleItemUpdate = (
+ type: string,
+ agendaIdx: number,
+ itemIdx: number,
+ updatedItem: any
+ ) => {
+ setAgendaItems((prev) => {
+ const cloned = JSON.parse(JSON.stringify(prev));
- // Check if the type is 'narrative'
- if (type === 'townHallUpdates' || type === 'narrative' || type === 'gameRules' || type === 'townHallSummary' || type === 'discussion') {
- if (newAgendaItems[agendaIdx]) {
- newAgendaItems[agendaIdx][type] = updatedItem[type]; // Directly set the narrative or gameRules string
- }
+ // Some fields (narrative, discussion, etc.) are single text fields
+ // stored directly rather than in an array.
+ if (
+ type === 'townHallUpdates' ||
+ type === 'narrative' ||
+ type === 'gameRules' ||
+ type === 'townHallSummary' ||
+ type === 'discussion'
+ ) {
+ cloned[agendaIdx][type] = updatedItem[type];
} else {
- // Handle other types as before
- if (newAgendaItems[agendaIdx] && newAgendaItems[agendaIdx][type]) {
- newAgendaItems[agendaIdx][type][itemIdx] = updatedItem;
- }
+ // Arrays: discussionPoints, actionItems, etc.
+ cloned[agendaIdx][type][itemIdx] = updatedItem;
}
-
- return newAgendaItems;
+ return cloned;
});
};
-
- const getHeading = (itemType: any, workgroup: any) => {
- switch (itemType) {
- // Only check this section when changing or adding new workgroup
- case "issues":
- if (workgroup === "Onboarding Workgroup") return "To carry over for next meeting";
- if (workgroup === "AI Sandbox/Think-tank") return "To carry over for next meeting";
- if (workgroup === "WG Sync Call") return "To carry over for next meeting";
- // Add more specific conditions for "issues" if needed
- return "Issues"; // Default for "issues"
-
- case "discussionPoints":
- if (workgroup === "Onboarding Workgroup") return "In this meeting we discussed";
- if (workgroup === "AI Sandbox/Think-tank") return "In this meeting we discussed";
- return "Discussion Points";
-
- case "meetingTopics":
- if (workgroup === "Research and Development Guild") return "Agenda Items";
- if (workgroup === "Education Workgroup") return "In this meeting we discussed";
- if (workgroup === "WG Sync Call") return "Agenda Items";
- return "Meeting Topics";
- // Add cases for other item types as needed
-
- default:
- return "Items";
- }
- }
-
+ // You have your "template" logic to show/hide sections:
const itemTypesConfig = [
{
type: "townHallUpdates",
- isEnabled: (template: any) => template?.townHallUpdates === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.townHallUpdates === 1,
+ render: (item: any, agendaIndex: number) => (
<>
Town Hall Updates from this meeting
@@ -119,7 +145,9 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
item={item.townHallUpdates}
agendaIndex={agendaIndex}
itemIndex={0}
- onUpdate={(agendaIdx: any, itemIdx: any, updatedItem: any) => handleItemUpdate('townHallUpdates', agendaIdx, itemIdx, updatedItem)}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("townHallUpdates", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
@@ -128,8 +156,8 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
},
{
type: "narrative",
- isEnabled: (template: any) => template?.narrative === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.narrative === 1,
+ render: (item: any, agendaIndex: number) => (
<>
Narrative
@@ -138,7 +166,9 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
item={item.narrative}
agendaIndex={agendaIndex}
itemIndex={0}
- onUpdate={(agendaIdx: any, itemIdx: any, updatedItem: any) => handleItemUpdate('narrative', agendaIdx, itemIdx, updatedItem)}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("narrative", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
@@ -147,8 +177,8 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
},
{
type: "discussion",
- isEnabled: (template: any) => template?.discussion === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.discussion === 1,
+ render: (item: any, agendaIndex: number) => (
<>
Discussion
@@ -157,7 +187,9 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
item={item.discussion}
agendaIndex={agendaIndex}
itemIndex={0}
- onUpdate={(agendaIdx: any, itemIdx: any, updatedItem: any) => handleItemUpdate('discussion', agendaIdx, itemIdx, updatedItem)}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("discussion", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
@@ -166,96 +198,128 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
},
{
type: "meetingTopics",
- isEnabled: (template: any) => template?.meetingTopics === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.meetingTopics === 1,
+ render: (item: any, agendaIndex: number) => (
<>
- {getHeading("meetingTopics", myVariable.workgroup?.preferred_template?.workgroup)}
+ Meeting Topics
- {item.meetingTopics.map((point: any, pointIndex: any) => (
+ {item.meetingTopics.map((topic: any, idx: number) => (
- handleItemUpdate('meetingTopics', agendaIdx, itemIdx, updatedItem)}
+ itemIndex={idx}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("meetingTopics", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
))}
- addItem('meetingTopics', agendaIndex)}>Add Item
+ addItem('meetingTopics', agendaIndex)}
+ >
+ Add Item
+
>
)
},
{
type: "discussionPoints",
- isEnabled: (template: any) => template?.discussionPoints === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.discussionPoints === 1,
+ render: (item: any, agendaIndex: number) => (
<>
- {getHeading("discussionPoints", myVariable.workgroup?.preferred_template?.workgroup)}
+ Discussion Points
- {item.discussionPoints.map((point: any, pointIndex: any) => (
+ {item.discussionPoints?.map((pt: any, idx: number) => (
- handleItemUpdate('discussionPoints', agendaIdx, itemIdx, updatedItem)}
+ itemIndex={idx}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("discussionPoints", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
))}
- addItem('discussionPoints', agendaIndex)}>Add Discussion Point
+ addItem('discussionPoints', agendaIndex)}
+ >
+ Add Discussion Point
+
>
)
},
{
type: "actionItems",
- isEnabled: (template: any) => template?.actionItems === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.actionItems === 1,
+ render: (item: any, agendaIndex: number) => (
<>
Action items
- {item?.actionItems?.map((action: any, actionIndex: any) => (
+ {item.actionItems?.map((action: any, actionIdx: number) => (
- handleItemUpdate('actionItems', agendaIdx, itemIdx, updatedItem)}
+ itemIndex={actionIdx}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("actionItems", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
))}
-
addItem('actionItems', agendaIndex)}>Add Action
+ addItem('actionItems', agendaIndex)}
+ >
+ Add Action
+
>
)
},
{
type: "decisionItems",
- isEnabled: (template: any) => template?.decisionItems === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.decisionItems === 1,
+ render: (item: any, agendaIndex: number) => (
<>
Decisions
- {item?.decisionItems?.map((decision: any, decisionIndex: any) => (
+ {item.decisionItems?.map((dec: any, decIdx: number) => (
- handleItemUpdate('decisionItems', agendaIdx, itemIdx, updatedItem)}
+ itemIndex={decIdx}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("decisionItems", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
))}
-
addItem('decisionItems', agendaIndex)}>Add Decision
+ addItem('decisionItems', agendaIndex)}
+ >
+ Add Decision
+
>
)
},
{
type: "gameRules",
- isEnabled: (template: any) => template?.gameRules === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.gameRules === 1,
+ render: (item: any, agendaIndex: number) => (
<>
Game Rules
@@ -264,7 +328,9 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
item={item.gameRules}
agendaIndex={agendaIndex}
itemIndex={0}
- onUpdate={(agendaIdx: any, itemIdx: any, updatedItem: any) => handleItemUpdate('gameRules', agendaIdx, itemIdx, updatedItem)}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("gameRules", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
@@ -273,8 +339,8 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
},
{
type: "townHallSummary",
- isEnabled: (template: any) => template?.townHallSummary === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.townHallSummary === 1,
+ render: (item: any, agendaIndex: number) => (
<>
Town Hall Summary (Optional)
@@ -283,7 +349,9 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
item={item.townHallSummary}
agendaIndex={agendaIndex}
itemIndex={0}
- onUpdate={(agendaIdx: any, itemIdx: any, updatedItem: any) => handleItemUpdate('townHallSummary', agendaIdx, itemIdx, updatedItem)}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("townHallSummary", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
@@ -292,125 +360,138 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
},
{
type: "leaderboard",
- isEnabled: (template: any) => template?.leaderboard === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.leaderboard === 1,
+ render: (item: any, agendaIndex: number) => (
<>
Leaderboard
- {item.leaderboard.map((point: any, pointIndex: any) => (
+ {item.leaderboard.map((lb: any, lbIdx: number) => (
- handleItemUpdate('leaderboard', agendaIdx, itemIdx, updatedItem)}
+ itemIndex={lbIdx}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("leaderboard", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
))}
- addItem('leaderboard', agendaIndex)}>Add Gamer
+ addItem('leaderboard', agendaIndex)}
+ >
+ Add Gamer
+
>
)
},
{
type: "learningPoints",
- isEnabled: (template: any) => template?.learningPoints === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.learningPoints === 1,
+ render: (item: any, agendaIndex: number) => (
<>
Learning Points
- {item.learningPoints.map((point: any, pointIndex: any) => (
+ {item.learningPoints.map((lp: any, lpIdx: number) => (
- handleItemUpdate('learningPoints', agendaIdx, itemIdx, updatedItem)}
+ itemIndex={lpIdx}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("learningPoints", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
))}
- addItem('learningPoints', agendaIndex)}>Add Learning Point
+ addItem('learningPoints', agendaIndex)}
+ >
+ Add Learning Point
+
>
)
},
{
type: "issues",
- isEnabled: (template: any) => template?.issues === 1,
- render: (item: any, agendaIndex: any) => (
+ isEnabled: (t: any) => t?.issues === 1,
+ render: (item: any, agendaIndex: number) => (
<>
- {getHeading("issues", myVariable.workgroup?.preferred_template?.workgroup)}
+ Issues
- {item?.issues?.map((issue: any, issueIndex: any) => (
+ {item.issues?.map((issue: any, issueIdx: number) => (
- handleItemUpdate('issues', agendaIdx, itemIdx, updatedItem)}
+ itemIndex={issueIdx}
+ onUpdate={(aIdx, iIdx, updated) =>
+ handleItemUpdate("issues", aIdx, iIdx, updated)
+ }
onRemove={removeItem}
/>
))}
-
addItem('issues', agendaIndex)}>Add Item
+ addItem('issues', agendaIndex)}
+ >
+ Add Item
+
>
)
},
- /*{
- type: "issues",
- isEnabled: (template: any) => template.issues === 1,
- render: (item: any, agendaIndex: any) => (
- // ... rendering logic for issues ...
- )
- },*/
];
+ // If the user’s workgroup is in `agendaItemOrder`, reorder itemTypes accordingly
+ const orderMapping = myVariable?.agendaItemOrder;
type WorkgroupKey = keyof typeof orderMapping;
-
- const orderMapping = myVariable?.agendaItemOrder;
-
- const reorderItemTypesConfig = (orderKey: WorkgroupKey) => {
+ if (
+ typeof myVariable.workgroup?.workgroup === 'string' &&
+ myVariable.workgroup?.workgroup in orderMapping
+ ) {
+ const orderKey = myVariable.workgroup.workgroup as WorkgroupKey;
const order = orderMapping[orderKey];
- if (!order) {
- console.warn("Order key not found in mapping");
- return;
+ if (order) {
+ itemTypesConfig.sort((a, b) => {
+ const indexA = order.indexOf(a.type);
+ const indexB = order.indexOf(b.type);
+ if (indexA === -1 && indexB === -1) return 0;
+ if (indexA === -1) return 1;
+ if (indexB === -1) return -1;
+ return indexA - indexB;
+ });
}
-
- itemTypesConfig.sort((a, b) => {
- const indexA = order.indexOf(a.type);
- const indexB = order.indexOf(b.type);
-
- if (indexA === -1 && indexB === -1) return 0; // Both types are not in the order list, keep their relative order
- if (indexA === -1) return 1; // Type A is not in the list, move it towards the end
- if (indexB === -1) return -1; // Type B is not in the list, move it towards the end
-
- return indexA - indexB;
- });
-
- };
-
- if (typeof myVariable.workgroup?.workgroup === 'string' && myVariable.workgroup?.workgroup in orderMapping) {
- reorderItemTypesConfig(myVariable.workgroup?.workgroup as WorkgroupKey);
} else {
- console.warn("Invalid workgroup key");
+ console.warn("Invalid or unmapped workgroup key - using default item order.");
}
return (
- {myVariable.workgroup?.preferred_template?.agendaItems[0]?.agenda == 1 && (
Agenda Items )}
- {agendaItems.map((item: any, agendaIndex: any) => (
+ {myVariable.workgroup?.preferred_template?.agendaItems[0]?.agenda === 1 && (
+
Agenda Items
+ )}
+
+ {agendaItems.map((item: any, agendaIndex: number) => (
- {myVariable.workgroup?.preferred_template?.agendaItems[0]?.agenda == 1 && (
+
+ {/* If your template says "agenda=1", show agenda title + status */}
+ {myVariable.workgroup?.preferred_template?.agendaItems[0]?.agenda === 1 && (
<>
Agenda item {agendaIndex + 1}
-
- Agenda Title
-
+ Agenda Title
{
value={item.agenda}
autoComplete="off"
onChange={(e) => {
- const newAgenda = [...agendaItems];
- newAgenda[agendaIndex].agenda = e.target.value;
- setAgendaItems(newAgenda);
+ const updated = [...agendaItems];
+ updated[agendaIndex].agenda = e.target.value;
+ setAgendaItems(updated);
}}
/>
-
- Status
-
+
Status
{
- const newAgenda = [...agendaItems];
- newAgenda[agendaIndex].status = e.target.value;
- setAgendaItems(newAgenda);
+ const updated = [...agendaItems];
+ updated[agendaIndex].status = e.target.value;
+ setAgendaItems(updated);
}}
>
Carry Over
@@ -445,20 +524,47 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
>
)}
+ {/* Render sub‐sections based on the template flags */}
{itemTypesConfig.map(({ type, isEnabled, render }) => {
if (isEnabled(myVariable.workgroup?.preferred_template?.agendaItems[0])) {
- // Add a unique key prop to each rendered element
- return {render(item, agendaIndex)}
;
+ return (
+
+ {render(item, agendaIndex)}
+
+ );
}
return null;
})}
- {myVariable.workgroup?.preferred_template?.agendaItems[0]?.agenda == 1 && item.agenda == '' && ( removeAgendaItem(agendaIndex)}>Remove Agenda )}
+ {/* If agenda=1 is enabled, let them remove this agenda if empty */}
+ {myVariable.workgroup?.preferred_template?.agendaItems[0]?.agenda === 1 &&
+ item.agenda === '' && (
+ removeAgendaItem(agendaIndex)}
+ >
+ Remove Agenda
+
+ )}
))}
- {((myVariable.workgroup?.preferred_template?.agendaItems[0]?.agenda == 0 && agendaItems.length == 0) ||
- (myVariable.workgroup?.preferred_template?.agendaItems[0]?.agenda == 1))
- && (
Add Agenda Item )}
+
+ {(
+ // Show "Add Agenda Item" if the template says agenda=1,
+ // or if there are no agenda items yet
+ (myVariable.workgroup?.preferred_template?.agendaItems[0]?.agenda === 0 &&
+ agendaItems.length === 0) ||
+ (myVariable.workgroup?.preferred_template?.agendaItems[0]?.agenda === 1)
+ ) && (
+
+ Add Agenda Item
+
+ )}
);
};
diff --git a/components/SummaryMeetingInfo.tsx b/components/SummaryMeetingInfo.tsx
index 2fc8ba5..58d403b 100644
--- a/components/SummaryMeetingInfo.tsx
+++ b/components/SummaryMeetingInfo.tsx
@@ -1,11 +1,10 @@
-// ../components/SummaryMeetingInfo.tsx
import { useState, useEffect, useRef, useCallback } from 'react';
import styles from '../styles/meetinginfo.module.css';
import { useMyVariable } from '../context/MyVariableContext';
import SelectNames from './SelectNames';
import WorkingDocs from './WorkingDocs';
import TimestampedVideo from './TimestampedVideo';
-import { MeetingTypeSelect } from '../components/meeting/MeetingTypeSelect';
+import { MeetingTypeSelect } from './meeting/MeetingTypeSelect';
type SummaryMeetingInfoProps = {
workgroup: string;
@@ -13,379 +12,422 @@ type SummaryMeetingInfoProps = {
};
const SummaryMeetingInfo: React.FC
= ({ workgroup, onUpdate }) => {
- const { myVariable, setMyVariable } = useMyVariable();
-
- const {
- name = 'Weekly',
- date = '',
- host = '',
- documenter = '',
- translator = '',
- peoplePresent = '',
- purpose = '',
- townHallNumber = '',
- googleSlides = '',
- meetingVideoLink = '',
- miroBoardLink = '',
- otherMediaLink = '',
- transcriptLink = '',
- mediaLink = '',
- workingDocs = [],
- timestampedVideo = {}
- } = myVariable?.summary?.meetingInfo || {};
+ const { myVariable } = useMyVariable();
+ // Track which meeting_id we have “locally”
+ const [localMeetingId, setLocalMeetingId] = useState(myVariable.summary?.meeting_id || null);
+
+ // Pull initial meeting info:
+ const initialInfo = myVariable.summary?.meetingInfo || {};
+ // Save the initial docs array:
+ const initialDocs = initialInfo.workingDocs || [];
+
+ // This is how many docs came from the DB initially
+ // (as opposed to newly added on the client).
+ const [originalDocsCount] = useState(initialDocs.length);
+
+ // Keep the entire meetingInfo in local state
const [meetingInfo, setMeetingInfo] = useState({
- name,
- date,
- host,
- documenter,
- translator,
- peoplePresent,
- purpose,
- townHallNumber,
- googleSlides,
- meetingVideoLink,
- miroBoardLink,
- otherMediaLink,
- transcriptLink,
- mediaLink,
- workingDocs,
- timestampedVideo
+ name: initialInfo.name || "Weekly",
+ date: initialInfo.date || "",
+ host: initialInfo.host || "",
+ documenter: initialInfo.documenter || "",
+ translator: initialInfo.translator || "",
+ peoplePresent: initialInfo.peoplePresent || "",
+ purpose: initialInfo.purpose || "",
+ townHallNumber: initialInfo.townHallNumber || "",
+ googleSlides: initialInfo.googleSlides || "",
+ meetingVideoLink: initialInfo.meetingVideoLink || "",
+ miroBoardLink: initialInfo.miroBoardLink || "",
+ otherMediaLink: initialInfo.otherMediaLink || "",
+ transcriptLink: initialInfo.transcriptLink || "",
+ mediaLink: initialInfo.mediaLink || "",
+ workingDocs: initialDocs,
+ timestampedVideo: initialInfo.timestampedVideo || { url: "", intro: "", timestamps: "" }
});
- const [displayedWorkingDocs, setDisplayedWorkingDocs] = useState(workingDocs.length > 0 ? [] : [{ title: '', link: '' }]);
- const allWorkingDocs = [...workingDocs, ...displayedWorkingDocs];
+ /**
+ * If `meeting_id` changes in context, re‐init from the updated context
+ * once (rather than every time summary updates).
+ */
+ useEffect(() => {
+ const currentId = myVariable.summary?.meeting_id;
+ if (currentId && currentId !== localMeetingId) {
+ const newInfo = myVariable.summary?.meetingInfo || {};
+ const newDocs = newInfo.workingDocs || [];
+
+ setMeetingInfo({
+ name: newInfo.name || "Weekly",
+ date: newInfo.date || "",
+ host: newInfo.host || "",
+ documenter: newInfo.documenter || "",
+ translator: newInfo.translator || "",
+ peoplePresent: newInfo.peoplePresent || "",
+ purpose: newInfo.purpose || "",
+ townHallNumber: newInfo.townHallNumber || "",
+ googleSlides: newInfo.googleSlides || "",
+ meetingVideoLink: newInfo.meetingVideoLink || "",
+ miroBoardLink: newInfo.miroBoardLink || "",
+ otherMediaLink: newInfo.otherMediaLink || "",
+ transcriptLink: newInfo.transcriptLink || "",
+ mediaLink: newInfo.mediaLink || "",
+ workingDocs: newDocs,
+ timestampedVideo: newInfo.timestampedVideo || { url: "", intro: "", timestamps: "" }
+ });
+
+ setLocalMeetingId(currentId);
+ // If you want to reset originalDocsCount whenever you load a new summary,
+ // you could do that here, but that implies re-setting state, etc.
+ // For now, we’ll just leave it as is.
+ }
+ }, [myVariable.summary?.meeting_id, localMeetingId]);
- const purposeRef = useRef(null);
+ // For auto‐resizing the Purpose textarea
+ const purposeRef = useRef(null);
const adjustTextareaHeight = (textarea: HTMLTextAreaElement | null) => {
if (textarea) {
- textarea.style.height = 'auto'; // Reset height
- textarea.style.height = `${textarea.scrollHeight}px`; // Set to scrollHeight
+ textarea.style.height = 'auto';
+ textarea.style.height = `${textarea.scrollHeight}px`;
}
};
-
useEffect(() => {
adjustTextareaHeight(purposeRef.current);
}, [meetingInfo.purpose]);
- const addNewDoc = () => {
- const newDoc = { title: '', link: '' };
- setDisplayedWorkingDocs([...displayedWorkingDocs, newDoc]);
- };
-
- const handleChange = (e: any, docIndex = null) => {
+ // Generic handler for top-level text fields
+ const handleChange = (e: React.ChangeEvent) => {
const { name, value } = e.target;
+ setMeetingInfo((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
- if (docIndex !== null) {
- setDisplayedWorkingDocs(prevDisplayedDocs => {
- const updatedDocs = [...prevDisplayedDocs];
- updatedDocs[docIndex] = { ...updatedDocs[docIndex], [name]: value };
- return updatedDocs;
- });
- } else {
- setMeetingInfo(prevMeetingInfo => {
- const updatedMeetingInfo = { ...prevMeetingInfo, [name]: value };
- return updatedMeetingInfo;
- });
- }
+ useEffect(() => {
+ onUpdate(meetingInfo);
+ }, [meetingInfo, onUpdate]);
+
+ // For Facilitator, Documenter, etc. using
+ const handleSelection = (fieldName: string, selectedNames: string) => {
+ setMeetingInfo((prev) => ({
+ ...prev,
+ [fieldName]: selectedNames
+ }));
};
- const updateMyVariable = (e: any, index: number) => {
- const updatedMyVariable = { ...myVariable };
- if (!updatedMyVariable.summary.meetingInfo.workingDocs) {
- updatedMyVariable.summary.meetingInfo.workingDocs = [];
- }
-
- if (e) {
- const { name, value } = e.target;
- updatedMyVariable.summary.meetingInfo.workingDocs[index] = { ...updatedMyVariable.summary.meetingInfo.workingDocs[index], [name]: value };
- } else {
- if (updatedMyVariable.summary.meetingInfo.workingDocs.length > 0) {
- updatedMyVariable.summary.meetingInfo.workingDocs.splice(index, 1);
- }
- }
- setMyVariable(updatedMyVariable);
- };
+ // For
+ const handleVideoDataUpdate = useCallback((newVideoData: any) => {
+ setMeetingInfo((prev) => ({
+ ...prev,
+ timestampedVideo: newVideoData
+ }));
+ }, []);
- const removeDoc = (index: number) => {
- setDisplayedWorkingDocs(prevDocs => {
- const updatedDocs = prevDocs.filter((_, i) => i !== index);
- return updatedDocs;
- });
+ // For : updates the entire array of docs
+ const handleWorkingDocsChange = (updatedDocs: any[]) => {
+ setMeetingInfo((prev) => ({
+ ...prev,
+ workingDocs: updatedDocs
+ }));
};
-
- useEffect(() => {
- onUpdate({ ...meetingInfo, workingDocs: [...workingDocs, ...displayedWorkingDocs] });
- }, [meetingInfo, displayedWorkingDocs]);
-
- const handleSelection = (name: any, selectedNames: any) => {
- const updatedInfo = { ...meetingInfo, [name]: selectedNames };
- setMeetingInfo(updatedInfo);
+
+ // Simple example: add a new doc (client-side)
+ const addNewDoc = () => {
+ setMeetingInfo((prev) => ({
+ ...prev,
+ workingDocs: [...prev.workingDocs, { title: "", link: "" }]
+ }));
};
- useEffect(() => {
- const {
- name = 'Weekly',
- date = '',
- host = '',
- documenter = '',
- translator = '',
- peoplePresent = '',
- purpose = '',
- townHallNumber = '',
- googleSlides = '',
- meetingVideoLink = '',
- miroBoardLink = '',
- otherMediaLink = '',
- transcriptLink = '',
- mediaLink = '',
- workingDocs = [{ title: '', link: '' }],
- timestampedVideo = { url: '', intro: '', timestamps: '' }
- } = myVariable?.summary?.meetingInfo || {};
-
- setMeetingInfo({
- name,
- date,
- host,
- documenter,
- translator,
- peoplePresent,
- purpose,
- townHallNumber,
- googleSlides,
- meetingVideoLink,
- miroBoardLink,
- otherMediaLink,
- transcriptLink,
- mediaLink,
- workingDocs,
- timestampedVideo
+ // Example: remove a new doc (one above originalDocsCount)
+ const removeDoc = (newDocIndex: number) => {
+ setMeetingInfo((prev) => {
+ const updated = [...prev.workingDocs];
+ // Because newDocIndex references only the newly added items
+ // after the old ones. So the actual array index is oldDocsCount + newDocIndex
+ const removeIndex = originalDocsCount + newDocIndex;
+ updated.splice(removeIndex, 1);
+ return { ...prev, workingDocs: updated };
});
- }, [myVariable.summary?.meetingInfo]);
-
- const handleVideoDataUpdate = useCallback((newVideoData: any) => {
- setMeetingInfo(prevMeetingInfo => {
- if (JSON.stringify(prevMeetingInfo.timestampedVideo) === JSON.stringify(newVideoData)) {
- return prevMeetingInfo;
- }
- return {
- ...prevMeetingInfo,
- timestampedVideo: newVideoData,
+ };
+
+ // Example: handle updating a newly added doc
+ const handleNewDocChange = (e: any, newDocIndex: number) => {
+ const { name, value } = e.target;
+ setMeetingInfo((prev) => {
+ const updated = [...prev.workingDocs];
+ // actual array index:
+ const arrayIndex = originalDocsCount + newDocIndex;
+ updated[arrayIndex] = {
+ ...updated[arrayIndex],
+ [name]: value
};
+ return { ...prev, workingDocs: updated };
});
- }, []);
- //console.log("myVariable", myVariable);
+ };
+
+ // Example: handle updating an old doc
+ const updateMyVariableForOldDoc = (e: any | null, oldDocIndex: number) => {
+ if (e === null) {
+ // Means remove
+ setMeetingInfo((prev) => {
+ const updated = [...prev.workingDocs];
+ updated.splice(oldDocIndex, 1);
+ return { ...prev, workingDocs: updated };
+ });
+ // TODO: Make an API call or store something to actually remove from DB
+ } else {
+ // It's a change event
+ const { name, value } = e.target;
+ setMeetingInfo((prev) => {
+ const updated = [...prev.workingDocs];
+ updated[oldDocIndex] = {
+ ...updated[oldDocIndex],
+ [name]: value
+ };
+ return { ...prev, workingDocs: updated };
+ });
+ // TODO: Possibly do an API call to update DB in real-time, etc.
+ }
+ };
+
return (
- <>
-
-
-
+
+
+
+
+ {/* Facilitator */}
{myVariable.workgroup?.preferred_template?.meetingInfo?.host == 1 && (
-
-
- Facilitator:
-
- handleSelection('host', selectedNames)}
- initialValue={meetingInfo.host || ""}
- />
-
)}
+
+ Facilitator:
+ handleSelection('host', selected)}
+ initialValue={meetingInfo.host || ""}
+ />
+
+ )}
+
+ {/* Documenter */}
{myVariable.workgroup?.preferred_template?.meetingInfo?.documenter == 1 && (
-
-
- Documenter:
-
- handleSelection('documenter', selectedNames)}
- initialValue={meetingInfo.documenter || ""}
- />
-
)}
+
+ Documenter:
+ handleSelection('documenter', selected)}
+ initialValue={meetingInfo.documenter || ""}
+ />
+
+ )}
+
+ {/* Translator */}
{myVariable.workgroup?.preferred_template?.meetingInfo?.translator == 1 && (
-
-
- Translator:
-
- handleSelection('translator', selectedNames)}
- initialValue={meetingInfo.translator || ""}
- />
-
)}
+
+ Translator:
+ handleSelection('translator', selected)}
+ initialValue={meetingInfo.translator || ""}
+ />
+
+ )}
+
+ {/* People present */}
-
- {myVariable.workgroup?.preferred_template?.meetingInfo?.peoplePresent == 1 && (<>
-
- Present:
-
-
handleSelection('peoplePresent', selectedNames)}
- initialValue={meetingInfo.peoplePresent || ""}
- />
- >)}
+ {myVariable.workgroup?.preferred_template?.meetingInfo?.peoplePresent == 1 && (
+
+ Present:
+ handleSelection('peoplePresent', selected)}
+ initialValue={meetingInfo.peoplePresent || ""}
+ />
+
+ )}
+
+ {/* Right‐side links column */}
+
+ {myVariable.workgroup?.preferred_template?.meetingInfo?.purpose == 1 && (
+ <>
+ Purpose:
+
-
- {myVariable.workgroup?.preferred_template?.meetingInfo?.purpose == 1 && (<>
-
- Purpose:
-
-
-
- {myVariable.workgroup?.preferred_template?.meetingInfo?.workingDocs == 1 && (
+
+ {myVariable.workgroup?.preferred_template?.meetingInfo?.workingDocs == 1 && (
- )}
- {myVariable.workgroup?.preferred_template?.meetingInfo?.googleSlides == 1 && (<>
-
- Google Slides (Optional):
-
-
- >)}
- {myVariable.workgroup?.preferred_template?.meetingInfo?.timestampedVideo == 1 && (
+ )}
+
+ {myVariable.workgroup?.preferred_template?.meetingInfo?.googleSlides == 1 && (
+ <>
+
Google Slides (Optional):
+
+ >
+ )}
+
+ {myVariable.workgroup?.preferred_template?.meetingInfo?.timestampedVideo == 1 && (
+ onUpdate={handleVideoDataUpdate}
+ initialData={meetingInfo.timestampedVideo}
+ />
)}
- >
);
};
diff --git a/components/SummaryTemplate.tsx b/components/SummaryTemplate.tsx
index 60ef6df..708d26e 100644
--- a/components/SummaryTemplate.tsx
+++ b/components/SummaryTemplate.tsx
@@ -1,5 +1,7 @@
// ../components/SummaryTemplate.tsx
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo, useRef } from "react";
+import debounce from "lodash/debounce";
+import isEqual from "lodash/isEqual";
import styles from '../styles/summarytemplate.module.css';
import { useMyVariable } from '../context/MyVariableContext';
import SummaryMeetingInfo from './SummaryMeetingInfo'
@@ -15,279 +17,231 @@ type SummaryTemplateProps = {
updateMeetings: (newMeetingSummary: any) => void;
};
-const filterKeys = (source: any, template: any) => {
- const result: any = {};
- Object.keys(template).forEach(key => {
- if (key === "type") {
- result[key] = "Custom";
- //} else if (key === "date") {
- // Explicitly setting the date to be empty
- // result[key] = "";
- } else if (source.hasOwnProperty(key)) {
- // If the key is an object, recursively filter its keys
- if (typeof source[key] === 'object' && !Array.isArray(source[key])) {
- result[key] = filterKeys(source[key], template[key]);
- } else {
- result[key] = source[key];
- }
- } else {
- result[key] = template[key];
+const defaultFormData = {
+ workgroup: "",
+ workgroup_id: "",
+ meetingInfo: {
+ name: "Weekly",
+ date: "",
+ host: "",
+ documenter: "",
+ translator: "",
+ peoplePresent: "",
+ purpose: "",
+ townHallNumber: "",
+ googleSlides: "",
+ meetingVideoLink: "",
+ miroBoardLink: "",
+ otherMediaLink: "",
+ transcriptLink: "",
+ mediaLink: "",
+ workingDocs: [{ title: "", link: "" }],
+ timestampedVideo: { url: "", intro: "", timestamps: "" }
+ },
+ agendaItems: [
+ {
+ agenda: "",
+ status: "carry over",
+ townHallUpdates: "",
+ townHallSummary: "",
+ narrative: "",
+ discussion: "",
+ gameRules: "",
+ meetingTopics:[""],
+ issues: [""],
+ actionItems: [{ text: "", assignee: "", dueDate: "", status: "todo" }],
+ decisionItems: [
+ { decision: "", rationale: "", opposing: "", effect: "affectsOnlyThisWorkgroup" }
+ ],
+ discussionPoints: [""],
+ learningPoints: [""],
}
- });
- return result;
+ ],
+ tags: { topicsCovered: "", emotions: "", other: "", gamesPlayed: "" },
+ type: "Custom",
+ noSummaryGiven: false,
+ canceledSummary: false
};
function formatTimestampForPdf(timestamp: any) {
- // Create a Date object using the timestamp
const date = new Date(timestamp);
-
date.setHours(date.getHours() + 2);
-
const day = date.getUTCDate();
const month = date.toLocaleString('en-US', { month: 'long', timeZone: 'UTC' });
const year = date.getUTCFullYear();
-
return `${day} ${month} ${year}`;
}
-
function formatTimestamp(timestamp: any) {
- // Parse the timestamp into a Date object
const date = new Date(timestamp);
-
- // Format the date and time
- // Get the YYYY-MM-DD format
const formattedDate = date.toISOString().split('T')[0];
-
- // Get the HH:MM format
const hours = date.getUTCHours().toString().padStart(2, '0');
const minutes = date.getUTCMinutes().toString().padStart(2, '0');
-
- // Combine the date and time with UTC
return `${formattedDate} ${hours}:${minutes} UTC`;
}
const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
- const [loading, setLoading] = useState
(false);
const { myVariable, setMyVariable } = useMyVariable();
- const [creatingDoc, setCreatingDoc] = useState(false);
- const today = new Date().toISOString().split('T')[0];
+
+ const [formData, setFormData] = useState(defaultFormData);
+ const [tags, setTags] = useState({
+ topicsCovered: "",
+ emotions: "",
+ other: "",
+ gamesPlayed: ""
+ });
+ const [loading, setLoading] = useState(false);
+ const [creatingDoc, setCreatingDoc] = useState(false);
+
+ // For the quarterly doc dropdown
const [selectedQuarter, setSelectedQuarter] = useState('');
const quarterOptions = getQuarterOptions();
-
- const defaultFormData = {
- workgroup: "",
- workgroup_id: "",
- meetingInfo: {
- name:"Weekly",
- date:"",
- host:"",
- documenter:"",
- translator: "",
- peoplePresent: "",
- purpose: "",
- townHallNumber: "",
- googleSlides: "",
- meetingVideoLink: "",
- miroBoardLink: "",
- otherMediaLink: "",
- transcriptLink: "",
- mediaLink: "",
- workingDocs: [{ title: "", link: "" }],
- timestampedVideo: { url: "", intro: "", timestamps: "" }
- },
- agendaItems: [
- {
- agenda: "",
- status: "carry over",
- townHallUpdates: "",
- townHallSummary: "",
- narrative: "",
- discussion: "",
- gameRules: "",
- meetingTopics:[""],
- issues: [""],
- actionItems: [{ text: "", assignee: "", dueDate: "", status: "todo" }],
- decisionItems: [{ decision: "", rationale: "", opposing: "", effect: "affectsOnlyThisWorkgroup" }],
- discussionPoints: [""],
- learningPoints: [""],
- }
- ],
- tags: { topicsCovered: "", emotions: "", other: "", gamesPlayed: "" },
- type: "Custom",
- noSummaryGiven: false,
- canceledSummary: false
- };
- const [formData, setFormData] = useState(filterFormData(filterKeys(myVariable.summary || {}, defaultFormData)));
- const [tags, setTags] = useState({ topicsCovered: "", emotions: "", other: "", gamesPlayed: "" });
- const currentOrder = myVariable.agendaItemOrder ? myVariable.agendaItemOrder[myVariable.workgroup?.workgroup] : undefined;
+ // A ref to track if we've initialized once from context:
+ const initializedRef = useRef(false);
- async function handleCreateQuarterlyDoc() {
- setCreatingDoc(true);
+ // ------------------------------------------
+ // Only set local formData from context once:
+ // ------------------------------------------
+ useEffect(() => {
+ if (!initializedRef.current && myVariable.summary) {
+ initializedRef.current = true;
+ const filtered = filterFormData(myVariable.summary);
+ setFormData(filtered);
+ if (filtered.tags) {
+ setTags(filtered.tags);
+ }
+ }
+ }, [myVariable.summary]);
- try {
- const [quarter, year] = selectedQuarter.split(' ');
- const quarterNumber = parseInt(quarter.slice(1));
- const markdown = await generateQuarterlyReport(myVariable.workgroup.workgroup_id, parseInt(year), quarterNumber, currentOrder);
- //console.log(markdown);
- const response = await axios.post('/api/createGoogleDoc', {
- markdown,
- workgroup: myVariable.workgroup.workgroup,
- date: selectedQuarter
- });
+ // Keep formData.tags in sync with local tags:
+ useEffect(() => {
+ setFormData(prev => {
+ if (isEqual(prev.tags, tags)) {
+ return prev;
+ }
+ return { ...prev, tags };
+ });
+ }, [tags]);
- window.open(response.data.link, '_blank');
- } catch (error) {
- console.error('Error creating Quarterly Google Doc:', error);
- alert('There was an error creating the Quarterly Google Doc.');
- } finally {
- setCreatingDoc(false);
+ // Debounced auto-save logic
+ const debouncedAutoSave = useMemo(
+ () => debounce(async (latestData) => {
+ await autoSave(latestData);
+ }, 1000),
+ []
+ );
+
+ useEffect(() => {
+ // Skip auto-save if date is empty or the summary is flagged noSummary/canceled
+ if (!formData.meetingInfo?.date || formData.noSummaryGiven || formData.canceledSummary) {
+ return;
}
- }
+ debouncedAutoSave(formData);
+ return () => {
+ debouncedAutoSave.cancel();
+ };
+ }, [formData, debouncedAutoSave]);
- async function handleCreateGoogleDoc() {
- setCreatingDoc(true); // Set creatingDoc to true when the button is clicked
-
+ // 1) The autoSave function
+ const autoSave = async (latestData: any) => {
try {
- let markdown = generateMarkdown(myVariable.summary, currentOrder);
+ const cleanedData = prepareFormDataForSave(latestData);
+ const result = await saveCustomAgenda(cleanedData);
- // Add a heading to the first line of the markdown
- const heading = `# Meeting Summary for ${formData.workgroup}\nDate: ${formatTimestampForPdf(formData.meetingInfo?.date)}`;
- markdown = `${heading}\n\n${markdown}`;
- //console.log("markdown", markdown);
- const response = await axios.post('/api/createGoogleDoc', {
- markdown,
- workgroup: myVariable.workgroup?.workgroup,
- date: formatTimestampForPdf(formData.meetingInfo?.date)
- });
- window.open(response.data.link, '_blank');
- } catch (error) {
- console.error('Error creating Google Doc:', error);
- alert('There was an error creating the Google Doc.');
- } finally {
- setCreatingDoc(false); // Set creatingDoc to false after completion
- }
- }
-
- useEffect(() => {
- // Set the local state whenever myVariable.summary changes
- setFormData((filterKeys(myVariable.summary || {}, defaultFormData)));
- //console.log(myVariable, generateMarkdown(myVariable.summary, currentOrder))
- }, [myVariable.summary]); // Add myVariable.summary to the dependency array
-
- useEffect(() => {
- setLoading(true);
- if (myVariable.workgroup && myVariable.workgroup.workgroup) {
- setFormData((prevState: any)=> ({ ...prevState, workgroup: myVariable.workgroup.workgroup, workgroup_id: myVariable.workgroup.workgroup_id }));
+ if (result !== false) {
+ const dbRecord = result[0];
+ // Merge new fields (IDs, timestamps) but DO NOT overwrite typed text
+ const updatedSummary = {
+ ...latestData,
+ meeting_id: dbRecord.meeting_id,
+ updated_at: dbRecord.updated_at,
+ date: dbRecord.date
+ };
+
+ // We update context so the rest of the app knows about the new ID, date, etc.
+ setMyVariable((prev) => ({
+ ...prev,
+ summary: updatedSummary
+ }));
+
+ // Keep the parent's "meetings" list updated:
+ updateMeetings(updatedSummary);
+ }
+ } catch (err) {
+ console.error("AutoSave failed:", err);
}
- setLoading(false);
- //console.log("workgroupData", myVariable)
- }, [myVariable.workgroup]);
+ };
- useEffect(() => {
- setFormData((prevState: any) => ({ ...prevState, tags }));
- //console.log("formData",formData, myVariable)
- }, [tags]);
+ // 2) Helper: remove empty strings, etc.
+ const prepareFormDataForSave = (rawData: any) => {
+ const filteredWorkingDocs = rawData.meetingInfo.workingDocs.filter(
+ (doc: any) => doc.title || doc.link
+ );
+ const newData = {
+ ...rawData,
+ meetingInfo: {
+ ...rawData.meetingInfo,
+ workingDocs: filteredWorkingDocs
+ },
+ noSummaryGiven: false,
+ canceledSummary: false
+ };
+ return removeEmptyValues(newData);
+ };
const removeEmptyValues = (obj: any) => {
+ if (typeof obj !== 'object' || !obj) return obj;
Object.keys(obj).forEach(key => {
- // If it's an object and not an array
if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
- // If the object is empty, delete the key
+ obj[key] = removeEmptyValues(obj[key]);
if (Object.keys(obj[key]).length === 0) {
delete obj[key];
- } else {
- // Recursively clean the object
- removeEmptyValues(obj[key]);
}
- }
- // If it's an array
- else if (Array.isArray(obj[key])) {
- // Clean the array items if they are objects
- obj[key] = obj[key].map((item: any) => typeof item === 'object' ? removeEmptyValues(item) : item)
- .filter((item: any) => {
- // Filter out empty strings, empty arrays, and empty objects
- return item !== '' &&
- !(Array.isArray(item) && item.length === 0) &&
- !(typeof item === 'object' && Object.keys(item).length === 0);
- });
-
- // If after processing, the array is empty, remove the key
+ } else if (Array.isArray(obj[key])) {
+ obj[key] = obj[key]
+ .map((item: any) => removeEmptyValues(item))
+ .filter((item: any) => item !== '' &&
+ !(Array.isArray(item) && item.length === 0) &&
+ !(typeof item === 'object' && Object.keys(item).length === 0)
+ );
if (obj[key].length === 0) {
delete obj[key];
}
- }
- // If it's an empty string, remove it
- else if (obj[key] === '') {
- delete obj[key];
+ } else {
+ if (obj[key] === '') {
+ delete obj[key];
+ }
}
});
return obj;
- };
-
- async function handleSubmit(e: any) {
+ };
+
+ // 3) Manual "Save"
+ const handleSubmit = async () => {
if (!formData.meetingInfo.date) {
- alert("Please select the meeting date.");
+ alert("Please select the meeting date before saving.");
return;
}
-
- // Filter out working docs with both empty title and link
- const filteredWorkingDocs = formData.meetingInfo.workingDocs.filter((doc: any) => doc.title || doc.link);
-
- // Merging new workingDocs with old ones after filtering
- let summary: any = {
- ...myVariable.summary,
- ...formData,
- meetingInfo: {
- ...formData.meetingInfo,
- workingDocs: filteredWorkingDocs // This now includes filtered docs
- },
- updated_at: new Date()
- }
-
- summary.confirmed = false;
-
- const cleanedFormData = removeEmptyValues({
- ...formData,
- meetingInfo: { ...formData.meetingInfo, workingDocs: filteredWorkingDocs },
- noSummaryGiven: false,
- canceledSummary: false
- });
setLoading(true);
-
try {
- //console.log("test", cleanedFormData)
+ const cleanedFormData = prepareFormDataForSave(formData);
const data = await saveCustomAgenda(cleanedFormData);
if (data !== false) {
- //console.log("Calling updateMeetings with:", summary, data[0].date);
- summary.date = data[0].date
- summary.meeting_id = data[0].meeting_id
- summary.updated_at = data[0].updated_at
- // Ensure myVariable.summaries is initialized
- let existingSummaries = myVariable.summaries || [];
-
- // Find index of summary with the same date
- let existingSummaryIndex = existingSummaries.findIndex((s: any) => s.date === summary.date);
-
- if (existingSummaryIndex !== -1) {
- // Replace existing summary
- existingSummaries[existingSummaryIndex] = summary;
- } else {
- // Add new summary if one with the same date doesn't exist
- existingSummaries.push(summary);
- }
- //console.log("existingSummaries", existingSummaries);
- updateMeetings(summary);
- const updatedMyVariable = {
- ...myVariable,
- summary: summary,
- summaries: existingSummaries
+ const [latest] = data;
+ // Merge only ID, date, updated_at:
+ const summary = {
+ ...formData,
+ date: latest.date,
+ meeting_id: latest.meeting_id,
+ updated_at: latest.updated_at,
+ confirmed: false
};
-
- setMyVariable(updatedMyVariable);
- } else {
- console.log("Error in saving custom agenda");
+ updateMeetings(summary);
+ setMyVariable(prev => ({
+ ...prev,
+ summary
+ }));
}
} catch (error) {
console.error("Error submitting the form:", error);
@@ -295,70 +249,173 @@ const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
} finally {
setLoading(false);
}
- }
+ };
+
+ // 4) Create Google Docs
+ const handleCreateGoogleDoc = async () => {
+ setCreatingDoc(true);
+ try {
+ const currentOrder = myVariable.agendaItemOrder
+ ? myVariable.agendaItemOrder[myVariable.workgroup?.workgroup]
+ : undefined;
+ let markdown = generateMarkdown(myVariable.summary, currentOrder);
+
+ const heading = `# Meeting Summary for ${formData.workgroup}\nDate: ${formatTimestampForPdf(formData.meetingInfo?.date)}`;
+ markdown = `${heading}\n\n${markdown}`;
+
+ const response = await axios.post('/api/createGoogleDoc', {
+ markdown,
+ workgroup: myVariable.workgroup?.workgroup,
+ date: formatTimestampForPdf(formData.meetingInfo?.date)
+ });
+ window.open(response.data.link, '_blank');
+ } catch (error) {
+ console.error('Error creating Google Doc:', error);
+ alert('There was an error creating the Google Doc.');
+ } finally {
+ setCreatingDoc(false);
+ }
+ };
+
+ // 5) Create Quarterly Doc
+ const handleCreateQuarterlyDoc = async () => {
+ setCreatingDoc(true);
+ try {
+ const [quarter, year] = selectedQuarter.split(' ');
+ const quarterNumber = parseInt(quarter.slice(1));
+
+ const currentOrder = myVariable.agendaItemOrder
+ ? myVariable.agendaItemOrder[myVariable.workgroup?.workgroup]
+ : undefined;
+
+ const markdown = await generateQuarterlyReport(
+ myVariable.workgroup.workgroup_id,
+ parseInt(year),
+ quarterNumber,
+ currentOrder
+ );
+
+ const response = await axios.post('/api/createGoogleDoc', {
+ markdown,
+ workgroup: myVariable.workgroup.workgroup,
+ date: selectedQuarter
+ });
+ window.open(response.data.link, '_blank');
+ } catch (error) {
+ console.error('Error creating Quarterly Google Doc:', error);
+ alert('There was an error creating the Quarterly Google Doc.');
+ } finally {
+ setCreatingDoc(false);
+ }
+ };
return (
<>
- {loading && (
- <>
-
- Saving summary...
+ {loading && (
+
Saving summary...
+ )}
+ {!loading && (
+
+
{formData.workgroup} {formData.meetingInfo.date}
+
+ {/* Meeting Info */}
+ {formData.meetingInfo.name && (
+
{
+ setFormData((prev) => {
+ // Compare old meetingInfo vs new
+ if (isEqual(prev.meetingInfo, info)) {
+ // If they are the same, do nothing -> no re-render
+ return prev;
+ }
+ // If different, update parent’s formData
+ return { ...prev, meetingInfo: info };
+ });
+ }}
+ />
+ )}
+
+ {/* Agenda Items */}
+ {
+ setFormData((prev: any) => {
+ // Compare old vs new
+ if (isEqual(prev.agendaItems, items)) {
+ return prev; // no change => skip re-render
+ }
+ return { ...prev, agendaItems: items };
+ });
+ }}
+ />
+
+
+ {/* Tags */}
+
+
+ {/* Manual Save Button */}
+
+ {loading ? "Loading..." : "Save"}
+
+
+ {/* Show last updated date/time if available */}
+ {myVariable.summary?.updated_at && (
+ {`(last saved ${formatTimestamp(myVariable.summary?.updated_at)})`}
+ )}
+
+ {/* Create Google Doc */}
+
+ {creatingDoc
+ ? Creating Google Doc...
+ : "Create Google Doc"
+ }
+
+
+ (The document will open in a new tab if popups are enabled for this site)
+
+
+ {/* Quarterly Report */}
+
+ setSelectedQuarter(e.target.value)}
+ className={styles.quarterSelect}
+ >
+ Select Quarter
+ {quarterOptions.map((option: string) => (
+
+ {option}
+
+ ))}
+
+
+ {creatingDoc
+ ? Creating Google Doc...
+ : "Create Quarterly Google Doc"
+ }
+
+
+
- >
- )}
- {!loading && (
-
{formData.workgroup} {formData.meetingInfo.date}
-
- {formData.meetingInfo.name && (
setFormData({...formData, meetingInfo: info})} />)}
- setFormData({...formData, agendaItems: items})} />
-
-
- {loading ? "Loading..." : "Save"}
-
- {myVariable.summary?.updated_at && ({`(last saved ${formatTimestamp(myVariable.summary?.updated_at)})`}
)}
-
- {creatingDoc ? (
- Creating Google Doc...
- ) : (
- "Create Google Doc"
- )}
-
-
- (The document will open in a new tab if popups are enabled for this site)
-
-
- setSelectedQuarter(e.target.value)}
- className={styles.quarterSelect}
- >
- Select Quarter
- {quarterOptions.map((option) => (
-
- {option}
-
- ))}
-
-
- {creatingDoc ? (
- Creating Google Doc...
- ) : (
- "Create Quarterly Google Doc"
- )}
-
-
-
-
)}
+ )}
>
);
};
diff --git a/components/WorkingDocs.tsx b/components/WorkingDocs.tsx
index 5caab31..6dbe29f 100644
--- a/components/WorkingDocs.tsx
+++ b/components/WorkingDocs.tsx
@@ -1,33 +1,91 @@
import styles from '../styles/workingDocsTable.module.css';
-const WorkingDocs = ({ handleChange, addNewDoc, docs, removeDoc, originalDocsCount, updateMyVariable }: any) => {
+type Doc = {
+ title?: string;
+ link?: string;
+};
+
+type WorkingDocsProps = {
+ /** The complete array of docs, both old + newly added. */
+ docs: Doc[];
+ /**
+ * Number of docs that existed in the DB before.
+ * If `index < originalDocsCount`, we treat it as an “existing doc”.
+ * Otherwise, it's a newly added doc.
+ */
+ originalDocsCount: number;
+
+ /** Called when a NEW doc changes (indexes >= originalDocsCount). */
+ handleChange: (e: any, newDocIndex: number) => void;
+
+ /** Called when the user clicks “Add New Working Document”. */
+ addNewDoc: () => void;
+
+ /** Called to remove a NEW doc from the parent's state. */
+ removeDoc: (newDocIndex: number) => void;
+
+ /**
+ * Called to update an OLD doc (i.e. in DB).
+ * If `e === null`, it means remove. Otherwise it’s a change event.
+ */
+ updateMyVariable: (e: any | null, oldDocIndex: number) => void;
+};
+
+const WorkingDocs = ({
+ docs,
+ originalDocsCount,
+ handleChange,
+ addNewDoc,
+ removeDoc,
+ updateMyVariable
+}: WorkingDocsProps) => {
+
+ /**
+ * Called whenever an input changes in the table row.
+ * If it's an existing doc (index < originalDocsCount),
+ * we call `updateMyVariable`. Otherwise, we call `handleChange`.
+ */
const handleDocChange = (e: any, index: number) => {
if (index < originalDocsCount) {
+ // This is an existing doc from the DB
updateMyVariable(e, index);
} else {
+ // This is a newly added doc
handleChange(e, index - originalDocsCount);
}
};
+ /**
+ * Called when the user clicks the “X” to remove a doc row.
+ * If it’s an existing doc, call `updateMyVariable(null, index)`.
+ * If it’s a new doc, call `removeDoc(...)`.
+ */
const handleDocRemove = (index: number) => {
if (index < originalDocsCount) {
+ // Remove an existing doc from DB or mark it removed
updateMyVariable(null, index);
} else {
+ // Remove a new doc from local state
removeDoc(index - originalDocsCount);
}
};
+ /**
+ * Called when the user clicks “Add New Working Document”.
+ * If we currently have 0 docs, we initialize the first doc as empty.
+ * Otherwise, we call `addNewDoc()` to push another blank doc to local.
+ */
const handleAddNewDoc = () => {
if (docs.length === 0) {
- // Add the first document
+ // Force-initialize the very first doc
handleChange({ target: { name: 'title', value: '' } }, 0);
handleChange({ target: { name: 'link', value: '' } }, 0);
} else {
- // Add a new document
addNewDoc();
}
};
+ /** Utility function to ensure links have a protocol. */
const formatUrl = (url: string) => {
if (!url?.startsWith('http://') && !url?.startsWith('https://')) {
return `http://${url}`;
@@ -47,8 +105,9 @@ const WorkingDocs = ({ handleChange, addNewDoc, docs, removeDoc, originalDocsCou
- {docs.map((doc: any, index: number) => (
+ {docs.map((doc: Doc, index: number) => (
+ {/* TITLE */}
handleDocChange(e, index)}
/>
+
+ {/* LINK */}
- {/* Change here: Always show input fields if there's only one row */}
+ {/* If there's only 1 row total, or if it's a newly added doc, show the text input. */}
{(docs.length === 1 || index >= originalDocsCount) ? (
handleDocChange(e, index)}
/>
) : (
- Link
+ // Otherwise, for older docs, show a clickable link
+
+ Link
+
)}
+
+ {/* REMOVE BUTTON */}
- handleDocRemove(index)}>X
+ handleDocRemove(index)}
+ >
+ X
+
))}
-
Add New Working Document
+
+
+ Add New Working Document
+
>
);
};
diff --git a/package-lock.json b/package-lock.json
index 9ead4ac..5d894d3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,15 +20,18 @@
"eslint-config-next": "13.4.6",
"google-auth-library": "^9.14.1",
"googleapis": "^144.0.0",
+ "lodash": "^4.17.21",
"next": "^14.2.8",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^9.0.0",
+ "react-modal": "^3.16.3",
"react-select": "^5.7.7",
"typescript": "5.1.3"
},
"devDependencies": {
- "@netlify/plugin-nextjs": "^5.9.4"
+ "@netlify/plugin-nextjs": "^5.9.4",
+ "@types/lodash": "^4.17.14"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -902,6 +905,13 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
+ "node_modules/@types/lodash": {
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/mdast": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.2.tgz",
@@ -2400,6 +2410,12 @@
"es5-ext": "~0.10.14"
}
},
+ "node_modules/exenv": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
+ "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/ext": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
@@ -3527,6 +3543,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -4577,6 +4599,12 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/react-lifecycles-compat": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
+ "license": "MIT"
+ },
"node_modules/react-markdown": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.0.tgz",
@@ -4603,6 +4631,22 @@
"react": ">=18"
}
},
+ "node_modules/react-modal": {
+ "version": "3.16.3",
+ "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz",
+ "integrity": "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==",
+ "license": "MIT",
+ "dependencies": {
+ "exenv": "^1.2.0",
+ "prop-types": "^15.7.2",
+ "react-lifecycles-compat": "^3.0.0",
+ "warning": "^4.0.3"
+ },
+ "peerDependencies": {
+ "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19",
+ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-select": {
"version": "5.7.7",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.7.tgz",
@@ -5486,6 +5530,15 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
diff --git a/package.json b/package.json
index 084e88f..9c3194a 100644
--- a/package.json
+++ b/package.json
@@ -21,14 +21,17 @@
"eslint-config-next": "13.4.6",
"google-auth-library": "^9.14.1",
"googleapis": "^144.0.0",
+ "lodash": "^4.17.21",
"next": "^14.2.8",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^9.0.0",
+ "react-modal": "^3.16.3",
"react-select": "^5.7.7",
"typescript": "5.1.3"
},
"devDependencies": {
- "@netlify/plugin-nextjs": "^5.9.4"
+ "@netlify/plugin-nextjs": "^5.9.4",
+ "@types/lodash": "^4.17.14"
}
}
diff --git a/pages/submit-meeting-summary/index.tsx b/pages/submit-meeting-summary/index.tsx
index 328d959..f4fbda1 100644
--- a/pages/submit-meeting-summary/index.tsx
+++ b/pages/submit-meeting-summary/index.tsx
@@ -1,15 +1,17 @@
+// pages/submit-meeting-summary/index.tsx
import { useState, useEffect } from "react";
import type { NextPage } from "next";
import { useRouter } from 'next/router';
+import Modal from 'react-modal';
import styles from '../../styles/meetingsummary.module.css';
import SummaryTemplate from '../../components/SummaryTemplate';
-import ArchiveSummaries from '../../components/ArchiveSummaries'
+import ArchiveSummaries from '../../components/ArchiveSummaries';
import { useMyVariable } from '../../context/MyVariableContext';
-import { getWorkgroups } from '../../utils/getWorkgroups'
-import { updateWorkgroups } from '../../utils/updateWorkgroups'
+import { getWorkgroups } from '../../utils/getWorkgroups';
+import { updateWorkgroups } from '../../utils/updateWorkgroups';
import { getSummaries } from '../../utils/getsummaries';
-import { getNames } from '../../utils/getNames'
-import { getTags } from '../../utils/getTags'
+import { getNames } from '../../utils/getNames';
+import { getTags } from '../../utils/getTags';
type Workgroup = {
workgroup_id: string;
@@ -23,59 +25,41 @@ type Names = {
label: any;
};
+// Configure ReactModal
+Modal.setAppElement('#__next');
+
+// Include new summary states in union
+type SelectionMode =
+ | "edit"
+ | "cleanNew"
+ | "prefilledNew"
+ | "noSummaryGiven"
+ | "canceledSummary"
+ | "";
+
const SubmitMeetingSummary: NextPage = () => {
const router = useRouter();
- const [activeComponent, setActiveComponent] = useState('two');
+ const { myVariable, setMyVariable } = useMyVariable();
+
+ // Local state
+ const [activeComponent, setActiveComponent] = useState('');
const [workgroups, setWorkgroups] = useState
([]);
- const [meetings, setMeetings] = useState([]);
+ const [meetings, setMeetings] = useState([]);
const [showNewWorkgroupInput, setShowNewWorkgroupInput] = useState(false);
const [newWorkgroup, setNewWorkgroup] = useState('');
- const { myVariable, setMyVariable } = useMyVariable();
const [selectedWorkgroupId, setSelectedWorkgroupId] = useState('');
const [selectedMeetingId, setSelectedMeetingId] = useState('');
const [isLoading, setIsLoading] = useState(false);
- const [names, setNames] = useState([])
- const [tags, setTags] = useState({})
- const [summaryStatus, setSummaryStatus] = useState('populatedSummary');
-
- async function getWorkgroupList() {
- setIsLoading(true);
- const workgroupList: any = await getWorkgroups();
- const names1 = await getNames();
- const tags1 = await getTags();
-
- // Sort workgroups alphabetically by workgroup name
- const sortedWorkgroups = workgroupList.sort((a: Workgroup, b: Workgroup) =>
- a.workgroup.localeCompare(b.workgroup)
- );
-
- let newNames = names1.map((value) => ({ value: value.name, label: value.name }));
-
- let otherTags = tags1
- .filter(tag => tag.type === 'other')
- .map(tag => ({ value: tag.tag, label: tag.tag }));
-
- let emotionTags = tags1
- .filter(tag => tag.type === 'emotions')
- .map(tag => ({ value: tag.tag, label: tag.tag }));
-
- let topicTags = tags1
- .filter(tag => tag.type === 'topicsCovered')
- .map(tag => ({ value: tag.tag, label: tag.tag }));
+ const [names, setNames] = useState([]);
+ const [tags, setTags] = useState({});
- let referenceTags = tags1
- .filter(tag => tag.type === 'references')
- .map(tag => ({ value: tag.tag, label: tag.tag }));
+ // Modal-related state
+ const [showModal, setShowModal] = useState(false);
+ const [selectionMode, setSelectionMode] = useState("");
+ const [selectedSummaryForEdit, setSelectedSummaryForEdit] = useState("");
+ const [newSummaryDate, setNewSummaryDate] = useState("");
- let gamesPlayedTags = tags1
- .filter(tag => tag.type === 'gamesPlayed')
- .map(tag => ({ value: tag.tag, label: tag.tag }));
-
- setWorkgroups(sortedWorkgroups);
- setNames(newNames);
- setTags({ other: otherTags, emotions: emotionTags, topicsCovered: topicTags, references: referenceTags, gamesPlayed: gamesPlayedTags });
- setIsLoading(false);
- }
+ // Agenda item order mapping
const orderMapping = {
"Gamers Guild": ["narrative", "discussionPoints", "decisionItems", "actionItems", "gameRules", "leaderboard"],
"Writers Workgroup": ["narrative", "decisionItems", "actionItems", "learningPoints"],
@@ -101,85 +85,123 @@ const SubmitMeetingSummary: NextPage = () => {
"WG Sync Call": ["meetingTopics", "discussion", "decisionItems", "actionItems", "issues"],
"AI Sandbox/Think-tank": ["townHallUpdates", "discussionPoints", "decisionItems", "actionItems", "learningPoints", "issues"],
"GitHub PBL WG": ["discussionPoints", "decisionItems", "actionItems"]
- };
- // When you add a new Workgroup you need to update this ordermapping and the Discord API with the new workgroup
- // Also check generateMarkdown and SummaryAgendaItems for potential updates
- // If you add a new AgendaItem type, you need to update the following components: Item.tsx, SummaryTemplate.tsx and AgendaItem.tsx and
- // the database template. You also need to update the generateMarkdown.js and getDefaultAgendaItem.js util functions
-
- useEffect(() => {
- async function fetchInitialData(workgroupId: string) {
- setIsLoading(true);
- const summaries: any = await getSummaries(workgroupId);
- setMeetings(summaries)
- if (summaries && summaries[0]?.type) {
- setActiveComponent('two');
- }
- setShowNewWorkgroupInput(false);
- setSelectedWorkgroupId(workgroupId);
- const selectedWorkgroup = workgroups.find(workgroup => workgroup.workgroup_id === workgroupId);
- if (selectedWorkgroup) {
- setMyVariable({ ...myVariable, workgroup: selectedWorkgroup, summaries, summary: summaries[0], names, tags, agendaItemOrder: orderMapping });
- }
- setIsLoading(false);
- }
- setSummaryStatus('populatedSummary');
- if (router.query.workgroup && workgroups.length > 0) {
- fetchInitialData(router.query.workgroup as string);
- }
- }, [router.query, workgroups]);
+ };
+ // -----------------------------------------
+ // Fetch the list of Workgroups on mount
+ // -----------------------------------------
useEffect(() => {
getWorkgroupList();
}, []);
+ async function getWorkgroupList() {
+ setIsLoading(true);
+ const workgroupList: any = await getWorkgroups();
+ const names1 = await getNames();
+ const tags1 = await getTags();
+
+ // Sort alphabetically
+ const sortedWorkgroups = workgroupList.sort((a: Workgroup, b: Workgroup) =>
+ a.workgroup.localeCompare(b.workgroup)
+ );
+
+ let newNames = names1.map((value: any) => ({ value: value.name, label: value.name }));
+
+ let otherTags = tags1
+ .filter((tag: any) => tag.type === 'other')
+ .map((tag: any) => ({ value: tag.tag, label: tag.tag }));
+
+ let emotionTags = tags1
+ .filter((tag: any) => tag.type === 'emotions')
+ .map((tag: any) => ({ value: tag.tag, label: tag.tag }));
+
+ let topicTags = tags1
+ .filter((tag: any) => tag.type === 'topicsCovered')
+ .map((tag: any) => ({ value: tag.tag, label: tag.tag }));
+
+ let referenceTags = tags1
+ .filter((tag: any) => tag.type === 'references')
+ .map((tag: any) => ({ value: tag.tag, label: tag.tag }));
+
+ let gamesPlayedTags = tags1
+ .filter((tag: any) => tag.type === 'gamesPlayed')
+ .map((tag: any) => ({ value: tag.tag, label: tag.tag }));
+
+ setWorkgroups(sortedWorkgroups);
+ setNames(newNames);
+ setTags({
+ other: otherTags,
+ emotions: emotionTags,
+ topicsCovered: topicTags,
+ references: referenceTags,
+ gamesPlayed: gamesPlayedTags
+ });
+ setIsLoading(false);
+ }
+
+ // -----------------------------------------
+ // Handle Workgroup selection -> open modal
+ // -----------------------------------------
async function handleSelectChange(e: any) {
- const selectedWorkgroupId = e.target.value;
- const summaries: any = selectedWorkgroupId != 'add_new' ? await getSummaries(selectedWorkgroupId) : null;
- setMeetings(summaries)
- if (summaries && summaries[0]?.type) {
- setActiveComponent('two');
- }
+ const selectedId = e.target.value;
- if (selectedWorkgroupId === 'add_new') {
+ if (selectedId === 'add_new') {
+ // Show new WG input
setNewWorkgroup('');
setShowNewWorkgroupInput(true);
- } else {
- setShowNewWorkgroupInput(false);
- setSelectedWorkgroupId(selectedWorkgroupId);
- const selectedWorkgroup = workgroups.find(workgroup => workgroup.workgroup_id === selectedWorkgroupId);
- if (selectedWorkgroup) {
- setMyVariable({ ...myVariable, workgroup: selectedWorkgroup, summaries, summary: summaries[0], names, tags });
- }
- }
- if (selectedWorkgroupId !== 'add_new') {
- router.push(`/submit-meeting-summary?workgroup=${selectedWorkgroupId}`, undefined, { shallow: true });
+ setSelectedWorkgroupId('');
+ setActiveComponent('');
+ router.push(`/submit-meeting-summary?workgroup=add_new`, undefined, { shallow: true });
+ return;
}
- //console.log("myVariable", myVariable );
+
+ setSelectedWorkgroupId(selectedId);
+ setShowNewWorkgroupInput(false);
+ setIsLoading(true);
+
+ // fetch existing summaries for that WG
+ const existingSummaries = await getSummaries(selectedId);
+ setMeetings(existingSummaries);
+ setIsLoading(false);
+
+ // push route to reflect selected workgroup
+ router.push(`/submit-meeting-summary?workgroup=${selectedId}`, undefined, { shallow: true });
+
+ // Open modal
+ setShowModal(true);
+ setSelectionMode("");
+ setSelectedSummaryForEdit("");
+ setNewSummaryDate("");
}
+
+ // -----------------------------------------
+ // (If used) for existing Meeting in UI
+ // -----------------------------------------
async function handleSelectChange2(e: any) {
const newSelectedMeetingId = e.target.value;
- setSelectedMeetingId(newSelectedMeetingId); // Correctly set the selectedMeetingId
+ setSelectedMeetingId(newSelectedMeetingId);
- // Find the selected summary using the new selectedMeetingId
- const selectedSummary = meetings.find((meeting: any) => meeting.meeting_id === newSelectedMeetingId);
-
- // If there's a selected summary, update the myVariable state with that summary
+ const selectedSummary = meetings.find((m: any) => m.meeting_id === newSelectedMeetingId);
if (selectedSummary) {
- setMyVariable(prevMyVariable => ({
- ...prevMyVariable,
- summary: selectedSummary // Set the selected summary here
+ setMyVariable((prev) => ({
+ ...prev,
+ summary: selectedSummary
}));
}
}
+ // -----------------------------------------
+ // Add New Workgroup logic
+ // -----------------------------------------
const handleNewWorkgroupChange = (e: any) => {
setNewWorkgroup(e.target.value);
- }
+ };
const handleRegisterNewWorkgroup = async () => {
setIsLoading(true);
- const existingWorkgroup = workgroups.find(workgroup => workgroup.workgroup.toLowerCase() === newWorkgroup.toLowerCase());
+ const existingWorkgroup = workgroups.find(
+ (w) => w.workgroup.toLowerCase() === newWorkgroup.toLowerCase()
+ );
if (existingWorkgroup) {
setSelectedWorkgroupId(existingWorkgroup.workgroup_id);
setShowNewWorkgroupInput(false);
@@ -187,67 +209,64 @@ const SubmitMeetingSummary: NextPage = () => {
await updateWorkgroups({ workgroup: newWorkgroup });
const updatedWorkgroupList: any = await getWorkgroups();
setWorkgroups(updatedWorkgroupList);
- const newWorkgroupEntry = updatedWorkgroupList.find((workgroup: any) => workgroup.workgroup.toLowerCase() === newWorkgroup.toLowerCase());
+ const newWorkgroupEntry = updatedWorkgroupList.find(
+ (w: any) => w.workgroup.toLowerCase() === newWorkgroup.toLowerCase()
+ );
if (newWorkgroupEntry) {
setSelectedWorkgroupId(newWorkgroupEntry.workgroup_id);
- setMyVariable(prev => ({ ...prev, workgroup: newWorkgroupEntry, names, tags })); // updating myVariable with new workgroup
+ setMyVariable((prev) => ({
+ ...prev,
+ workgroup: newWorkgroupEntry,
+ names,
+ tags
+ }));
}
setShowNewWorkgroupInput(false);
}
setIsLoading(false);
};
- const getComponent = () => {
- switch (activeComponent) {
- case 'two': return ;
- case 'four': return ;
- default: return Select a component
;
- }
- }
-
+ // -----------------------------------------
+ // Utility functions
+ // -----------------------------------------
function formatDate(isoString: any) {
const months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
-
const date = new Date(isoString);
const day = date.getDate();
const monthIndex = date.getMonth();
const year = date.getFullYear();
-
return `${day} ${months[monthIndex]} ${year}`;
}
const updateMeetings = (newMeetingSummary: any) => {
- //console.log("Before update, meetings:", meetings, newMeetingSummary); // Log the current state before update
-
- setMeetings(prevMeetings => {
- let updatedMeetings: any = [...prevMeetings];
- const meetingIndex: any = updatedMeetings.findIndex((meeting: any) => meeting.meeting_id === newMeetingSummary.meeting_id);
+ setMeetings((prevMeetings) => {
+ let updated = [...prevMeetings];
+ const meetingIndex = updated.findIndex((m: any) => m.meeting_id === newMeetingSummary.meeting_id);
if (meetingIndex !== -1) {
- // Replace existing summary
- updatedMeetings[meetingIndex] = newMeetingSummary;
+ updated[meetingIndex] = newMeetingSummary;
} else {
- // Add new summary
- updatedMeetings.unshift(newMeetingSummary);
+ updated.unshift(newMeetingSummary);
}
- updatedMeetings = updatedMeetings.sort((a: any, b: any) => new Date(b.date).getTime() - new Date(a.date).getTime());
+ updated = updated.sort(
+ (a: any, b: any) => new Date(b.date).getTime() - new Date(a.date).getTime()
+ );
setSelectedMeetingId(newMeetingSummary.meeting_id);
- //console.log("After update, meetings:", updatedMeetings); // Log the new state
- return updatedMeetings;
+ return updated;
});
};
const resetSummary = () => {
- setMyVariable(prevMyVariable => ({
- ...prevMyVariable,
+ setMyVariable((prev) => ({
+ ...prev,
summary: {
- ...prevMyVariable.summary,
+ ...prev.summary,
meetingInfo: {
- ...prevMyVariable.summary.meetingInfo,
- date: "",
+ ...prev.summary?.meetingInfo,
+ date: ""
},
agendaItems: [],
tags: {}
@@ -255,142 +274,478 @@ const SubmitMeetingSummary: NextPage = () => {
}));
};
- const noSummaryGiven = () => {
- setMyVariable(prevMyVariable => ({
- ...prevMyVariable,
- summary: {
- ...prevMyVariable.summary,
- meetingInfo: {
- name:"Weekly",
- date: prevMyVariable.summary.meetingInfo.date
+ // Decides which component to show in main content
+ function getComponent() {
+ switch (activeComponent) {
+ case 'two':
+ return ;
+ case 'four':
+ return ;
+ default:
+ return Select a component
;
+ }
+ }
+
+ // -----------------------------------------
+ // Confirm selection from Modal
+ // -----------------------------------------
+ const confirmModalSelection = () => {
+ setShowModal(false);
+ if (!selectedWorkgroupId) return;
+
+ const chosenWorkgroup = workgroups.find((wg) => wg.workgroup_id === selectedWorkgroupId);
+
+ if (selectionMode === "edit") {
+ // Edit existing
+ const existing = meetings.find((m) => m.meeting_id === selectedSummaryForEdit);
+ if (existing) {
+ setMyVariable((prev) => ({
+ ...prev,
+ workgroup: chosenWorkgroup,
+ summary: existing,
+ summaries: meetings,
+ names,
+ tags,
+ agendaItemOrder: orderMapping
+ }));
+ setSelectedMeetingId(existing.meeting_id);
+ }
+ setActiveComponent('two');
+ }
+ else if (selectionMode === "cleanNew") {
+ // New clean summary
+ const newClean = {
+ workgroup: chosenWorkgroup?.workgroup || "",
+ workgroup_id: selectedWorkgroupId,
+ meetingInfo: {
+ name: "Weekly",
+ date: newSummaryDate,
+ host: "",
+ documenter: "",
+ translator: "",
+ peoplePresent: "",
+ purpose: "",
+ townHallNumber: "",
+ googleSlides: "",
+ meetingVideoLink: "",
+ miroBoardLink: "",
+ otherMediaLink: "",
+ transcriptLink: "",
+ mediaLink: "",
+ workingDocs: [{ title: "", link: "" }],
+ timestampedVideo: { url: "", intro: "", timestamps: "" }
},
agendaItems: [],
tags: {},
- noSummaryGiven: true,
- canceledSummary: false
- }
- }));
- };
-
- const handleSummaryStatusChange = (e: any) => {
- const newStatus = e.target.value;
- setSummaryStatus(newStatus);
-
- // Update myVariable state based on the selection
- if (newStatus === 'noSummaryGiven') {
- noSummaryGiven(); // Your existing function to handle no summary
- } else if (newStatus === 'canceledSummary') {
- // Implement logic for canceled summary
- //console.log("Canceled Summary", myVariable);
- setMyVariable(prevMyVariable => ({
- ...prevMyVariable,
- summary: {
- ...prevMyVariable.summary,
- meetingInfo: {
- name:"Weekly",
- date: prevMyVariable.summary.meetingInfo.date
+ type: "Custom",
+ noSummaryGiven: false,
+ canceledSummary: false,
+ updated_at: new Date()
+ };
+
+ setMyVariable((prev) => ({
+ ...prev,
+ workgroup: chosenWorkgroup,
+ summary: newClean,
+ summaries: meetings,
+ names,
+ tags,
+ agendaItemOrder: orderMapping
+ }));
+ setActiveComponent('two');
+ }
+ else if (selectionMode === "prefilledNew") {
+ // New prefilled summary from last
+ if (meetings.length > 0) {
+ const lastSummary = meetings[0];
+ const newPrefilled = {
+ ...lastSummary,
+ meeting_id: undefined,
+ workgroup_id: selectedWorkgroupId,
+ workgroup: chosenWorkgroup?.workgroup || "",
+ meetingInfo: {
+ ...lastSummary.meetingInfo,
+ date: newSummaryDate
+ },
+ updated_at: new Date(),
+ confirmed: false,
+ noSummaryGiven: false,
+ canceledSummary: false
+ };
+ setMyVariable((prev) => ({
+ ...prev,
+ workgroup: chosenWorkgroup,
+ summary: newPrefilled,
+ summaries: meetings,
+ names,
+ tags,
+ agendaItemOrder: orderMapping
+ }));
+ } else {
+ // If no prior summaries, treat it like a clean summary
+ const newClean = {
+ workgroup: chosenWorkgroup?.workgroup || "",
+ workgroup_id: selectedWorkgroupId,
+ meetingInfo: {
+ name: "Weekly",
+ date: newSummaryDate,
+ host: "",
+ documenter: "",
+ translator: "",
+ peoplePresent: "",
+ purpose: "",
+ townHallNumber: "",
+ googleSlides: "",
+ meetingVideoLink: "",
+ miroBoardLink: "",
+ otherMediaLink: "",
+ transcriptLink: "",
+ mediaLink: "",
+ workingDocs: [{ title: "", link: "" }],
+ timestampedVideo: { url: "", intro: "", timestamps: "" }
},
agendaItems: [],
tags: {},
+ type: "Custom",
noSummaryGiven: false,
- canceledSummary: true
- }
+ canceledSummary: false,
+ updated_at: new Date()
+ };
+ setMyVariable((prev) => ({
+ ...prev,
+ workgroup: chosenWorkgroup,
+ summary: newClean,
+ summaries: meetings,
+ names,
+ tags,
+ agendaItemOrder: orderMapping
+ }));
+ }
+ setActiveComponent('two');
+ }
+ else if (selectionMode === "noSummaryGiven") {
+ // Mark as noSummaryGiven
+ const newNoSummary = {
+ workgroup: chosenWorkgroup?.workgroup || "",
+ workgroup_id: selectedWorkgroupId,
+ meetingInfo: {
+ name: "Weekly",
+ date: newSummaryDate
+ },
+ agendaItems: [],
+ tags: {},
+ noSummaryGiven: true,
+ canceledSummary: false,
+ updated_at: new Date()
+ };
+ setMyVariable((prev) => ({
+ ...prev,
+ workgroup: chosenWorkgroup,
+ summary: newNoSummary,
+ summaries: meetings,
+ names,
+ tags,
+ agendaItemOrder: orderMapping
}));
- } else {
- // Reset to populated summary (default state or any specific logic)
- console.log("Populated Summary");
+ setActiveComponent('four');
+ }
+ else if (selectionMode === "canceledSummary") {
+ // Mark as canceled
+ const newCanceledSummary = {
+ workgroup: chosenWorkgroup?.workgroup || "",
+ workgroup_id: selectedWorkgroupId,
+ meetingInfo: {
+ name: "Weekly",
+ date: newSummaryDate
+ },
+ agendaItems: [],
+ tags: {},
+ noSummaryGiven: false,
+ canceledSummary: true,
+ updated_at: new Date()
+ };
+ setMyVariable((prev) => ({
+ ...prev,
+ workgroup: chosenWorkgroup,
+ summary: newCanceledSummary,
+ summaries: meetings,
+ names,
+ tags,
+ agendaItemOrder: orderMapping
+ }));
+ setActiveComponent('four');
}
};
-
return (
+ {/* ---------- MODAL ---------- */}
+
setShowModal(false)}
+ contentLabel="Select Summary Option"
+ style={{
+ overlay: {
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ content: {
+ position: 'relative',
+ inset: 'unset',
+ maxWidth: '500px',
+ margin: 'auto',
+ borderRadius: '8px',
+ padding: '1rem',
+ backgroundColor: '#fff'
+ }
+ }}
+ >
+ Select how you want to proceed
+
+
+
+ Confirm
+
+
+ {/* ---------- /MODAL ---------- */}
+
{isLoading ? (
Loading...
) : (
<>
{workgroups.length > 0 && (
- <>
-
- Select Workgroup
-
- Please select Workgroup
- {workgroups.map((workgroup: any) => (
- {workgroup.workgroup}
- ))}
- {myVariable.roles?.isAdmin && (Add new WG )}
-
-
- >
- )}
- {workgroups.length > 0 && meetings?.length > 0 && (
- <>
-
- Select previous meeting data
-
- {meetings.map((meeting: any) => (
-
- {formatDate(meeting.date)} {meeting.username} {meeting.confirmed ? 'Archived' : ''}
-
- ))}
-
-
- >
+
+
+ Select Workgroup
+
+
+ Please select Workgroup
+ {workgroups.map((workgroup: any) => (
+
+ {workgroup.workgroup}
+
+ ))}
+ {myVariable.roles?.isAdmin && (
+ Add new WG
+ )}
+
+
)}
+
{showNewWorkgroupInput && (
<>
-
-
Register New Workgroup
+
+
+ Register New Workgroup
+
>
)}
>
)}
- {selectedWorkgroupId && (<>
- {myVariable.roles?.isAdmin && (
setActiveComponent('two')}>Summary )}
- {myVariable.roles?.isAdmin &&
setActiveComponent('four')}>Archive Summaries }
-
Clear Summary
-
- {myVariable.roles?.isAdmin && (
-
- Populated Summary
- No Summary Given
- Cancelled Summary
-
- )}
- >)}
+
+ {/* Buttons for navigation if a WG is selected */}
+ {selectedWorkgroupId && (
+ <>
+ {myVariable.roles?.isAdmin && (
+
setActiveComponent('two')}
+ >
+ Summary
+
+ )}
+ {myVariable.roles?.isAdmin && (
+
setActiveComponent('four')}
+ >
+ Archive Summaries
+
+ )}
+
+ Clear Summary
+
+ >
+ )}
- {myVariable.isLoggedIn && selectedWorkgroupId && (
- {getComponent()}
-
)}
- {myVariable.isLoggedIn && !selectedWorkgroupId && !isLoading && (
-
Please select workgroup
- )}
- {!myVariable.isLoggedIn && (
-
-
Please sign in with Discord
+
+ {/* Main content logic */}
+ {myVariable.isLoggedIn && selectedWorkgroupId && (
+
{getComponent()}
+ )}
+ {myVariable.isLoggedIn && !selectedWorkgroupId && !isLoading && (
+
+
Please select workgroup
+
+ )}
+ {!myVariable.isLoggedIn && (
+
+
+
Please sign in with Discord
+
-
)}
+ )}
);
};
-export default SubmitMeetingSummary;
\ No newline at end of file
+export default SubmitMeetingSummary;
From 680f5b11a562bbfc6b0cbdb2fa7282018d67460d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Diamond?=
<32074058+Andre-Diamond@users.noreply.github.com>
Date: Fri, 14 Feb 2025 08:25:22 +0200
Subject: [PATCH 2/7] refactor: Improve agenda item handling and remove unused
reset functionality
---
components/SummaryAgendaItems.tsx | 11 +++++++++--
pages/submit-meeting-summary/index.tsx | 22 ----------------------
utils/getDefaultAgendaItem.js | 1 +
3 files changed, 10 insertions(+), 24 deletions(-)
diff --git a/components/SummaryAgendaItems.tsx b/components/SummaryAgendaItems.tsx
index 944568d..d23613e 100644
--- a/components/SummaryAgendaItems.tsx
+++ b/components/SummaryAgendaItems.tsx
@@ -72,6 +72,12 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
const addItem = (type: string, agendaIndex: number) => {
setAgendaItems((prev) => {
const updated = [...prev];
+
+ // Ensure the target array exists; if not, initialize it.
+ if (!Array.isArray(updated[agendaIndex][type])) {
+ updated[agendaIndex][type] = [];
+ }
+
if (type === 'actionItems') {
updated[agendaIndex][type].push({
text: "",
@@ -87,12 +93,13 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
effect: ""
});
} else {
- // e.g. discussionPoints, issues, meetingTopics, leaderboard
+ // For discussionPoints, issues, meetingTopics, leaderboard, etc.
updated[agendaIndex][type].push("");
}
return updated;
});
};
+
// Remove a sub‐item
const removeItem = (type: string, agendaIndex: number, itemIndex: number) => {
@@ -496,7 +503,7 @@ const SummaryAgendaItems = ({ onUpdate }: any) => {
className={styles['form-select']}
type="text"
placeholder="Agenda Item"
- value={item.agenda}
+ value={item.agenda ?? ""}
autoComplete="off"
onChange={(e) => {
const updated = [...agendaItems];
diff --git a/pages/submit-meeting-summary/index.tsx b/pages/submit-meeting-summary/index.tsx
index f4fbda1..3284060 100644
--- a/pages/submit-meeting-summary/index.tsx
+++ b/pages/submit-meeting-summary/index.tsx
@@ -259,21 +259,6 @@ const SubmitMeetingSummary: NextPage = () => {
});
};
- const resetSummary = () => {
- setMyVariable((prev) => ({
- ...prev,
- summary: {
- ...prev.summary,
- meetingInfo: {
- ...prev.summary?.meetingInfo,
- date: ""
- },
- agendaItems: [],
- tags: {}
- }
- }));
- };
-
// Decides which component to show in main content
function getComponent() {
switch (activeComponent) {
@@ -717,13 +702,6 @@ const SubmitMeetingSummary: NextPage = () => {
Archive Summaries
)}
-
- Clear Summary
-
>
)}
diff --git a/utils/getDefaultAgendaItem.js b/utils/getDefaultAgendaItem.js
index a51032a..8596170 100644
--- a/utils/getDefaultAgendaItem.js
+++ b/utils/getDefaultAgendaItem.js
@@ -1,3 +1,4 @@
+// ../utils/getDefaultAgendaItem.js
export const getDefaultAgendaItem = () => ({
agenda: "",
status: "carry over",
From f2cb8fe2e982db4ab493fbc50a2c8b4876a2d96d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Diamond?=
<32074058+Andre-Diamond@users.noreply.github.com>
Date: Fri, 14 Feb 2025 08:55:48 +0200
Subject: [PATCH 3/7] feat: Implement debounced input handling in SelectTags
component and enhance saveNewTags function with conflict resolution
---
components/SelectTags.tsx | 23 +++++++++++++----------
utils/saveNewTags.js | 2 +-
2 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/components/SelectTags.tsx b/components/SelectTags.tsx
index 01bc75c..7e1426e 100644
--- a/components/SelectTags.tsx
+++ b/components/SelectTags.tsx
@@ -1,6 +1,6 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { useMyVariable } from '../context/MyVariableContext';
-import styles from '../styles/typea.module.css';
+import debounce from 'lodash/debounce';
import CreatableSelect from 'react-select/creatable';
import { saveNewTags } from '../utils/saveNewTags'
@@ -19,18 +19,21 @@ const SelectTags: React.FC = ({ onSelect, initialValue, type })
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
];
- //console.log("options", options)
- async function handleInputChange(selected: any) {
- setSelectedLabels(selected); // Update local state
- let labs: string[] = selected.map((item: any) => item.label);
- const status = await saveNewTags(labs, type);
- onSelect(labs.join(", ")); // Update parent component's state
- }
React.useEffect(() => {
let initialOptions = initialValue ? initialValue.split(", ").map((val) => ({ label: val, value: val })) : [];
setSelectedLabels(initialOptions);
}, [initialValue]);
+
+ const debouncedHandleInputChange = useMemo(
+ () => debounce(async (selected) => {
+ setSelectedLabels(selected); // Update local state
+ const labs: string[] = selected.map((item: any) => item.label);
+ const status = await saveNewTags(labs, type);
+ onSelect(labs.join(", ")); // Update parent component's state
+ }, 1000),
+ [onSelect, type]
+ );
return (
@@ -39,7 +42,7 @@ const SelectTags: React.FC
= ({ onSelect, initialValue, type })
options={options}
value={selectedLabels}
onChange={(selected) => {
- handleInputChange(selected || []);
+ debouncedHandleInputChange(selected || []);
}}
styles={{
control: (baseStyles, state) => ({
diff --git a/utils/saveNewTags.js b/utils/saveNewTags.js
index 708e8b6..4900937 100644
--- a/utils/saveNewTags.js
+++ b/utils/saveNewTags.js
@@ -27,7 +27,7 @@ export async function saveNewTags(inputTags, type) {
const { data, error } = await supabase
.from("tags")
- .upsert(updates)
+ .upsert(updates, { onConflict: ['tag', 'type'] })
.select('*');
if (error) throw error;
From be51f8a8cd7610d6a9f8641d3cd9cc817fe00af2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Diamond?=
<32074058+Andre-Diamond@users.noreply.github.com>
Date: Fri, 14 Feb 2025 09:08:32 +0200
Subject: [PATCH 4/7] refactor: Update saveNewNames function to improve naming
consistency and add conflict resolution for upsert operation
---
components/SelectNames.tsx | 1 -
utils/saveNewNames.js | 15 ++++++---------
2 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/components/SelectNames.tsx b/components/SelectNames.tsx
index 92efae2..abffaa2 100644
--- a/components/SelectNames.tsx
+++ b/components/SelectNames.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { useMyVariable } from '../context/MyVariableContext';
-import styles from '../styles/typea.module.css';
import CreatableSelect from 'react-select/creatable';
import { saveNewNames } from '../utils/saveNewNames'
diff --git a/utils/saveNewNames.js b/utils/saveNewNames.js
index a6f1211..d9eb04d 100644
--- a/utils/saveNewNames.js
+++ b/utils/saveNewNames.js
@@ -4,32 +4,29 @@ export async function saveNewNames(inputNames) {
async function updateNames(inputNames) {
let status = 'started';
- // Fetch all existing SubGroups from the database
+ // Fetch all existing names from the database
const { data: existingNames, error: fetchError } = await supabase
.from("names")
.select("name");
if (fetchError) throw fetchError;
- // Convert the existing Namess to a Set for faster lookup
+ // Convert the existing names to a Set for faster lookup
const existingNamesSet = new Set(existingNames.map(item => item.name));
- // Filter out the Namess that already exist
+ // Filter out the names that already exist
const newNames = inputNames.filter(name => !existingNamesSet.has(name));
- // Insert new labels
+ // Insert new names
for (const name of newNames) {
- const updates = {
- name,
- };
+ const updates = { name };
const { data, error } = await supabase
.from("names")
- .upsert(updates)
+ .upsert(updates, { onConflict: ['name'] }) // Specify conflict target
.select('*');
if (error) throw error;
-
if (!data) {
throw new Error("Failed to update the name");
}
From c0967e806fd6c990d28fa1a563be8943bee7909d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Diamond?=
<32074058+Andre-Diamond@users.noreply.github.com>
Date: Tue, 18 Feb 2025 08:45:59 +0200
Subject: [PATCH 5/7] refactor: Simplify data handling in SummaryTemplate and
saveCustomAgenda functions; add prepareFormDataForSave utility for cleaner
data processing
---
components/SummaryTemplate.tsx | 77 +++++---------------------
pages/submit-meeting-summary/index.tsx | 59 +++++++++++++++++++-
utils/prepareFormDataForSave.ts | 57 +++++++++++++++++++
utils/saveCustomAgenda.js | 31 ++++++-----
4 files changed, 146 insertions(+), 78 deletions(-)
create mode 100644 utils/prepareFormDataForSave.ts
diff --git a/components/SummaryTemplate.tsx b/components/SummaryTemplate.tsx
index 708d26e..68c7139 100644
--- a/components/SummaryTemplate.tsx
+++ b/components/SummaryTemplate.tsx
@@ -116,7 +116,7 @@ const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
// Keep formData.tags in sync with local tags:
useEffect(() => {
- setFormData(prev => {
+ setFormData((prev: any) => {
if (isEqual(prev.tags, tags)) {
return prev;
}
@@ -143,12 +143,10 @@ const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
};
}, [formData, debouncedAutoSave]);
- // 1) The autoSave function
+ // The autoSave function
const autoSave = async (latestData: any) => {
try {
- const cleanedData = prepareFormDataForSave(latestData);
- const result = await saveCustomAgenda(cleanedData);
-
+ const result = await saveCustomAgenda(latestData);
if (result !== false) {
const dbRecord = result[0];
// Merge new fields (IDs, timestamps) but DO NOT overwrite typed text
@@ -156,15 +154,13 @@ const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
...latestData,
meeting_id: dbRecord.meeting_id,
updated_at: dbRecord.updated_at,
- date: dbRecord.date
+ date: dbRecord.date,
};
-
- // We update context so the rest of the app knows about the new ID, date, etc.
+ // Update context so the rest of the app knows about the new ID, date, etc.
setMyVariable((prev) => ({
...prev,
- summary: updatedSummary
+ summary: updatedSummary,
}));
-
// Keep the parent's "meetings" list updated:
updateMeetings(updatedSummary);
}
@@ -173,51 +169,7 @@ const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
}
};
- // 2) Helper: remove empty strings, etc.
- const prepareFormDataForSave = (rawData: any) => {
- const filteredWorkingDocs = rawData.meetingInfo.workingDocs.filter(
- (doc: any) => doc.title || doc.link
- );
- const newData = {
- ...rawData,
- meetingInfo: {
- ...rawData.meetingInfo,
- workingDocs: filteredWorkingDocs
- },
- noSummaryGiven: false,
- canceledSummary: false
- };
- return removeEmptyValues(newData);
- };
-
- const removeEmptyValues = (obj: any) => {
- if (typeof obj !== 'object' || !obj) return obj;
- Object.keys(obj).forEach(key => {
- if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
- obj[key] = removeEmptyValues(obj[key]);
- if (Object.keys(obj[key]).length === 0) {
- delete obj[key];
- }
- } else if (Array.isArray(obj[key])) {
- obj[key] = obj[key]
- .map((item: any) => removeEmptyValues(item))
- .filter((item: any) => item !== '' &&
- !(Array.isArray(item) && item.length === 0) &&
- !(typeof item === 'object' && Object.keys(item).length === 0)
- );
- if (obj[key].length === 0) {
- delete obj[key];
- }
- } else {
- if (obj[key] === '') {
- delete obj[key];
- }
- }
- });
- return obj;
- };
-
- // 3) Manual "Save"
+ // Manual "Save"
const handleSubmit = async () => {
if (!formData.meetingInfo.date) {
alert("Please select the meeting date before saving.");
@@ -225,8 +177,7 @@ const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
}
setLoading(true);
try {
- const cleanedFormData = prepareFormDataForSave(formData);
- const data = await saveCustomAgenda(cleanedFormData);
+ const data = await saveCustomAgenda(formData);
if (data !== false) {
const [latest] = data;
// Merge only ID, date, updated_at:
@@ -235,12 +186,12 @@ const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
date: latest.date,
meeting_id: latest.meeting_id,
updated_at: latest.updated_at,
- confirmed: false
+ confirmed: false,
};
updateMeetings(summary);
- setMyVariable(prev => ({
+ setMyVariable((prev) => ({
...prev,
- summary
+ summary,
}));
}
} catch (error) {
@@ -251,7 +202,7 @@ const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
}
};
- // 4) Create Google Docs
+ // Create Google Docs
const handleCreateGoogleDoc = async () => {
setCreatingDoc(true);
try {
@@ -277,7 +228,7 @@ const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
}
};
- // 5) Create Quarterly Doc
+ // Create Quarterly Doc
const handleCreateQuarterlyDoc = async () => {
setCreatingDoc(true);
try {
@@ -323,7 +274,7 @@ const SummaryTemplate = ({ updateMeetings }: SummaryTemplateProps) => {
{
- setFormData((prev) => {
+ setFormData((prev: any) => {
// Compare old meetingInfo vs new
if (isEqual(prev.meetingInfo, info)) {
// If they are the same, do nothing -> no re-render
diff --git a/pages/submit-meeting-summary/index.tsx b/pages/submit-meeting-summary/index.tsx
index 3284060..464cd67 100644
--- a/pages/submit-meeting-summary/index.tsx
+++ b/pages/submit-meeting-summary/index.tsx
@@ -94,6 +94,38 @@ const SubmitMeetingSummary: NextPage = () => {
getWorkgroupList();
}, []);
+ // useEffect to check URL for a workgroup_id and load the modal
+ useEffect(() => {
+ if (!router.isReady) return; // wait until router is ready
+ const workgroupFromUrl = router.query.workgroup;
+ if (workgroupFromUrl) {
+ if (workgroupFromUrl === "add_new") {
+ // Handle "add new" workgroup logic if needed
+ setNewWorkgroup('');
+ setShowNewWorkgroupInput(true);
+ setSelectedWorkgroupId('');
+ setActiveComponent('');
+ } else {
+ // Set the selected workgroup id from URL
+ setSelectedWorkgroupId(workgroupFromUrl as string);
+ setShowNewWorkgroupInput(false);
+ setIsLoading(true);
+
+ // Fetch existing summaries for the workgroup from URL
+ getSummaries(workgroupFromUrl as string)
+ .then((existingSummaries) => {
+ setMeetings(existingSummaries);
+ // Open the modal after fetching summaries
+ setShowModal(true);
+ setSelectionMode("");
+ setSelectedSummaryForEdit("");
+ setNewSummaryDate("");
+ })
+ .finally(() => setIsLoading(false));
+ }
+ }
+ }, [router.isReady, router.query.workgroup]);
+
async function getWorkgroupList() {
setIsLoading(true);
const workgroupList: any = await getWorkgroups();
@@ -145,6 +177,11 @@ const SubmitMeetingSummary: NextPage = () => {
async function handleSelectChange(e: any) {
const selectedId = e.target.value;
+ // If the modal is open, close it first
+ if (showModal) {
+ setShowModal(false);
+ }
+
if (selectedId === 'add_new') {
// Show new WG input
setNewWorkgroup('');
@@ -263,13 +300,24 @@ const SubmitMeetingSummary: NextPage = () => {
function getComponent() {
switch (activeComponent) {
case 'two':
- return ;
+ // Replace this condition with your actual condition
+ if (myVariable.summary?.noSummaryGiven || myVariable.summary?.canceledSummary) {
+ window.location.reload();
+ return null;
+ }
+ return (
+
+ );
case 'four':
return ;
default:
- return Select a component
;
+ return
;
}
}
+
// -----------------------------------------
// Confirm selection from Modal
@@ -462,6 +510,8 @@ const SubmitMeetingSummary: NextPage = () => {
}
};
+ const currentWorkgroup = workgroups.find((wg) => wg.workgroup_id === selectedWorkgroupId);
+
return (
{/* ---------- MODAL ---------- */}
@@ -488,6 +538,11 @@ const SubmitMeetingSummary: NextPage = () => {
}}
>
Select how you want to proceed
+ {currentWorkgroup && (
+
+ Workgroup: {currentWorkgroup.workgroup}
+
+ )}
{/* 1. Edit existing summary */}
diff --git a/utils/prepareFormDataForSave.ts b/utils/prepareFormDataForSave.ts
new file mode 100644
index 0000000..faec8dd
--- /dev/null
+++ b/utils/prepareFormDataForSave.ts
@@ -0,0 +1,57 @@
+// ../utils/prepareFormDataForSave.ts
+
+export function prepareFormDataForSave(rawData: any) {
+ const filteredWorkingDocs = rawData.meetingInfo.workingDocs.filter(
+ (doc: any) => doc.title || doc.link
+ );
+ let newData = {
+ ...rawData,
+ meetingInfo: {
+ ...rawData.meetingInfo,
+ workingDocs: filteredWorkingDocs,
+ },
+ noSummaryGiven: false,
+ canceledSummary: false,
+ };
+
+ // Specify the root-level keys you want to remove
+ const keysToRemove = ['username', 'meeting_id','confirmed','date','updated_at'];
+
+ keysToRemove.forEach(key => {
+ if (key in newData) {
+ delete newData[key];
+ }
+ });
+
+ return removeEmptyValues(newData);
+ }
+
+ function removeEmptyValues(obj: any) {
+ if (typeof obj !== "object" || !obj) return obj;
+ Object.keys(obj).forEach((key) => {
+ if (typeof obj[key] === "object" && !Array.isArray(obj[key])) {
+ obj[key] = removeEmptyValues(obj[key]);
+ if (Object.keys(obj[key]).length === 0) {
+ delete obj[key];
+ }
+ } else if (Array.isArray(obj[key])) {
+ obj[key] = obj[key]
+ .map((item: any) => removeEmptyValues(item))
+ .filter(
+ (item: any) =>
+ item !== "" &&
+ !(Array.isArray(item) && item.length === 0) &&
+ !(typeof item === "object" && Object.keys(item).length === 0)
+ );
+ if (obj[key].length === 0) {
+ delete obj[key];
+ }
+ } else {
+ if (obj[key] === "") {
+ delete obj[key];
+ }
+ }
+ });
+ return obj;
+ }
+
\ No newline at end of file
diff --git a/utils/saveCustomAgenda.js b/utils/saveCustomAgenda.js
index 2a0f0e7..84d9d28 100644
--- a/utils/saveCustomAgenda.js
+++ b/utils/saveCustomAgenda.js
@@ -1,23 +1,28 @@
+// ../utils/saveCustomAgenda
import { supabase } from "../lib/supabaseClient";
-import { tableNames } from '../config/config';
+import { tableNames } from "../config/config";
+import { prepareFormDataForSave } from "./prepareFormDataForSave";
export async function saveCustomAgenda(agendaData) {
- let updates = {
- name: agendaData.meetingInfo.name,
- template: agendaData.type,
- date: new Date(agendaData.meetingInfo.date).toISOString(),
- workgroup_id: agendaData.workgroup_id,
- summary: agendaData,
- updated_at: new Date
- }
-
+ // Clean the data before saving
+ const cleanedData = prepareFormDataForSave(agendaData);
+
+ const updates = {
+ name: cleanedData.meetingInfo.name,
+ template: cleanedData.type,
+ date: new Date(cleanedData.meetingInfo.date).toISOString(),
+ workgroup_id: cleanedData.workgroup_id,
+ summary: cleanedData,
+ updated_at: new Date(),
+ };
+
const { data, error } = await supabase
.from(tableNames.meetingsummaries)
- .upsert(updates, { onConflict: ['name', 'date', 'workgroup_id', 'user_id'] })
- .select('date, meeting_id, updated_at');
+ .upsert(updates, { onConflict: ["name", "date", "workgroup_id", "user_id"] })
+ .select("date, meeting_id, updated_at");
if (error) {
- console.error('Error upserting data:', error);
+ console.error("Error upserting data:", error);
return false;
}
From 86112b1d93a370fb5c3c13e2e378821f3b26e36b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Diamond?=
<32074058+Andre-Diamond@users.noreply.github.com>
Date: Tue, 18 Feb 2025 09:09:29 +0200
Subject: [PATCH 6/7] refactor: Update meeting summary instructions for clarity
and add autosave feature
---
pages/index.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/pages/index.tsx b/pages/index.tsx
index 4ab14e2..2a4b7ad 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -42,10 +42,10 @@ const Home: NextPage = () => {
When you hover over input fields it will give you a tip on what is needed or how it works
- When you select your workgroup to submit a meeting summary, it will load all the data from the previous meeting
- {`Please select the date the meeting happened in the "Meeting Date:" dropdown`}
- {`Any changes you save will be saved to the date you selected in the "Meeting Date:" dropdown`}
- Please remember to click the save button when you are done
+ When you select your workgroup it will ask how you want to proceed in creating the meeting summary
+ It will give you the following options - Edit existing summary, New clean summary or New prefilled Summary
+ For each of these options you need to select a date
+ The tool now autosaves
Bottom left of the save button you will find the date and time the summary was last saved
Meeting summaries will be reviewed by an Archive member
When the Archive member approves the data, the GitBook and database will be updated
From c632f39b016426390f41530d88b0dac57a57b9ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Diamond?=
<32074058+Andre-Diamond@users.noreply.github.com>
Date: Mon, 24 Feb 2025 10:15:22 +0200
Subject: [PATCH 7/7] refactor: Prevent modal from closing on overlay click and
ESC key for improved user experience
---
pages/submit-meeting-summary/index.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/pages/submit-meeting-summary/index.tsx b/pages/submit-meeting-summary/index.tsx
index 464cd67..a6e51ad 100644
--- a/pages/submit-meeting-summary/index.tsx
+++ b/pages/submit-meeting-summary/index.tsx
@@ -518,6 +518,8 @@ const SubmitMeetingSummary: NextPage = () => {
setShowModal(false)}
+ shouldCloseOnOverlayClick={false}
+ shouldCloseOnEsc={false}
contentLabel="Select Summary Option"
style={{
overlay: {