diff --git a/components/ArchiveSummaries.tsx b/components/ArchiveSummaries.tsx index eb0a188..0ccfc05 100644 --- a/components/ArchiveSummaries.tsx +++ b/components/ArchiveSummaries.tsx @@ -56,7 +56,7 @@ const ArchiveSummaries = () => { if (type === "checkbox") { const checked = (e.target as HTMLInputElement).checked; name === "commitToGitBook" ? setCommitToGitBook(checked) : setSendToDiscord(checked); - } else if (name == 'date' && myVariable.summary.noSummaryGiven == true) { + } else if (name == 'date' && (myVariable.summary?.noSummaryGiven == true || myVariable.summary?.canceledSummary == true)) { console.log(myVariable, formData, value) setFormData({ ...formData, [name]: value, confirmed: false }); setSendToDiscord(false); @@ -116,7 +116,7 @@ const ArchiveSummaries = () => { await sendDiscordMessage(myVariable, renderedMarkdown); } } else { - if (myVariable.summary.noSummaryGiven == true) { + if (myVariable.summary.noSummaryGiven == true || myVariable.summary.canceledSummary == true) { alert('Select a date') } else { alert('Summary already archived'); diff --git a/components/nav.tsx b/components/nav.tsx index ec3b46c..d890ed2 100644 --- a/components/nav.tsx +++ b/components/nav.tsx @@ -128,9 +128,15 @@ const Nav = () => { Issues - {roleData?.appRole == "admin" && ( - Summaries - )} + {roleData?.appRole == "admin" && (<> + + Summaries + + + Admin Tools + + + )}
{latestTag}
diff --git a/pages/admin-tools.tsx b/pages/admin-tools.tsx new file mode 100644 index 0000000..f9f5d9f --- /dev/null +++ b/pages/admin-tools.tsx @@ -0,0 +1,61 @@ +import { useState } from "react"; +import type { NextPage } from "next"; +import styles from '../styles/admintools.module.css'; +import { exportTags, exportUsers } from '../utils/exportUtils'; + +const AdminTools: NextPage = () => { + const [loading, setLoading] = useState(false); + + const handleExport = async (entity: 'tags' | 'users', format: 'csv' | 'pdf' | 'json') => { + setLoading(true); + if (entity === 'tags') { + await exportTags(format); + } else if (entity === 'users') { + await exportUsers(format); + } + setLoading(false); + }; + + // Simplified and more scalable structure for export operations + const exportOperations = [ + { + entity: 'Tags', + formats: ['csv', 'pdf', 'json'], + }, + { + entity: 'Users', + formats: ['csv', 'pdf', 'json'], + }, + // Example of how to add a new entity + // { + // entity: 'NewEntity', + // formats: ['csv', 'pdf'], + // }, + ]; + + return ( +
+ {!loading && ( +
+

Admin Tools

+ {exportOperations.map((operation: any) => ( +
+

{operation.entity}

+ {operation.formats.map((format: any) => ( + + ))} +
+ ))} +
+ )} +
+ ); +}; + +export default AdminTools; diff --git a/pages/api/convertToPdf.js b/pages/api/convertToPdf.js index 2f0e4b3..f5819e2 100644 --- a/pages/api/convertToPdf.js +++ b/pages/api/convertToPdf.js @@ -1,3 +1,4 @@ +//convertToPdf.js import axios from 'axios'; export default async function handler(req, res) { diff --git a/pages/issues.tsx b/pages/issues.tsx index 038173f..01b882f 100644 --- a/pages/issues.tsx +++ b/pages/issues.tsx @@ -20,7 +20,7 @@ const Issues: NextPage = () => { async function getIssues() { setLoading(true); const issues = await fetchIssues(); - console.log(issues) + //console.log(issues) const open = issues.filter((issue: Issue) => issue.state === 'open'); const closed = issues.filter((issue: Issue) => issue.state === 'closed'); setOpenIssues(open); diff --git a/pages/submit-meeting-summary/index.tsx b/pages/submit-meeting-summary/index.tsx index 5cc43d3..64c5f50 100644 --- a/pages/submit-meeting-summary/index.tsx +++ b/pages/submit-meeting-summary/index.tsx @@ -36,12 +36,13 @@ const SubmitMeetingSummary: NextPage = () => { 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(); + const names1 = await getNames(); + const tags1 = await getTags(); let newNames = names1.map((value) => ({ value: value.name, label: value.name })); @@ -60,11 +61,11 @@ const SubmitMeetingSummary: NextPage = () => { let referenceTags = tags1 .filter(tag => tag.type === 'references') .map(tag => ({ value: tag.tag, label: tag.tag })); - + let gamesPlayedTags = tags1 .filter(tag => tag.type === 'gamesPlayed') .map(tag => ({ value: tag.tag, label: tag.tag })); - + setWorkgroups(workgroupList); setNames(newNames); setTags({ other: otherTags, emotions: emotionTags, topicsCovered: topicTags, references: referenceTags, gamesPlayed: gamesPlayedTags }); @@ -87,28 +88,28 @@ const SubmitMeetingSummary: NextPage = () => { "Deep Funding Town Hall": ["townHallSummary"], "One-off Event": ["Narative"] }; - -useEffect(() => { - async function fetchInitialData(workgroupId: string) { - setIsLoading(true); - const summaries: any = await getSummaries(workgroupId); - setMeetings(summaries) - if (summaries && summaries[0]?.type) { - setActiveComponent('two'); - } + + 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); + 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}); + setMyVariable({ ...myVariable, workgroup: selectedWorkgroup, summaries, summary: summaries[0], names, tags, agendaItemOrder: orderMapping }); } setIsLoading(false); - } + } - if (router.query.workgroup && workgroups.length > 0) { - fetchInitialData(router.query.workgroup as string); - } -}, [router.query, workgroups]); + if (router.query.workgroup && workgroups.length > 0) { + fetchInitialData(router.query.workgroup as string); + } + }, [router.query, workgroups]); useEffect(() => { getWorkgroupList(); @@ -127,7 +128,7 @@ useEffect(() => { setShowNewWorkgroupInput(true); } else { setShowNewWorkgroupInput(false); - setSelectedWorkgroupId(selectedWorkgroupId); + setSelectedWorkgroupId(selectedWorkgroupId); const selectedWorkgroup = workgroups.find(workgroup => workgroup.workgroup_id === selectedWorkgroupId); if (selectedWorkgroup) { setMyVariable({ ...myVariable, workgroup: selectedWorkgroup, summaries, summary: summaries[0], names, tags }); @@ -137,14 +138,14 @@ useEffect(() => { router.push(`/submit-meeting-summary?workgroup=${selectedWorkgroupId}`, undefined, { shallow: true }); } //console.log("myVariable", myVariable ); - } + } async function handleSelectChange2(e: any) { const newSelectedMeetingId = e.target.value; setSelectedMeetingId(newSelectedMeetingId); // Correctly set the selectedMeetingId // 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 if (selectedSummary) { setMyVariable(prevMyVariable => ({ @@ -176,7 +177,7 @@ useEffect(() => { setShowNewWorkgroupInput(false); } setIsLoading(false); - }; + }; const getComponent = () => { switch (activeComponent) { @@ -184,29 +185,29 @@ useEffect(() => { case 'four': return ; default: return
Select a component
; } - } + } 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); - + if (meetingIndex !== -1) { // Replace existing summary updatedMeetings[meetingIndex] = newMeetingSummary; @@ -220,36 +221,64 @@ useEffect(() => { return updatedMeetings; }); }; - + const resetSummary = () => { setMyVariable(prevMyVariable => ({ - ...prevMyVariable, + ...prevMyVariable, summary: { - ...prevMyVariable.summary, - meetingInfo: {}, + ...prevMyVariable.summary, + meetingInfo: {}, agendaItems: [], - tags: {} + tags: {} } })); - }; + }; const noSummaryGiven = () => { setMyVariable(prevMyVariable => ({ - ...prevMyVariable, + ...prevMyVariable, summary: { - ...prevMyVariable.summary, - meetingInfo: {}, + ...prevMyVariable.summary, + meetingInfo: {}, agendaItems: [], tags: {}, - noSummaryGiven: true + 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 + setMyVariable(prevMyVariable => ({ + ...prevMyVariable, + summary: { + ...prevMyVariable.summary, + meetingInfo: {}, + agendaItems: [], + tags: {}, + noSummaryGiven: false, + canceledSummary: true + } + })); + } else { + // Reset to populated summary (default state or any specific logic) + console.log("Populated Summary"); + } + }; + return (
- {isLoading ? ( + {isLoading ? (

Loading...

) : ( <> @@ -258,8 +287,8 @@ useEffect(() => {
- {meetings.map((meeting: any) => ( - + ))} + +
)} {showNewWorkgroupInput && ( @@ -300,24 +329,28 @@ useEffect(() => { )} )} - {selectedWorkgroupId && (<> - {myVariable.roles?.isAdmin && ()} - {myVariable.roles?.isAdmin && } - )} + {myVariable.roles?.isAdmin && } + - {myVariable.roles?.isAdmin && activeComponent == 'four' && ()} + {myVariable.roles?.isAdmin && activeComponent == 'four' && ( + + )} )}
- {myVariable.isLoggedIn && selectedWorkgroupId && (
+ {myVariable.isLoggedIn && selectedWorkgroupId && (
{getComponent()}
)} {myVariable.isLoggedIn && !selectedWorkgroupId && !isLoading && (
diff --git a/styles/admintools.module.css b/styles/admintools.module.css new file mode 100644 index 0000000..b818018 --- /dev/null +++ b/styles/admintools.module.css @@ -0,0 +1,74 @@ +/* admintools.module.css */ +.container { + display: flex; + flex-direction: column; + margin-top: 4.5em; /* Leaving space for the top fixed navbar */ + height: calc(100vh - 4.5em); + justify-content: center; /* Centers content vertically */ + align-items: center; /* Centers content horizontally */ +} + +@media (max-width: 600px) { + .container { + display: flex; + flex-direction: column; + margin-top: 120px; + align-items: center; + } +} + +.heading { + font-size: 2rem; + font-weight: bold; + margin-bottom: 1rem; +} + +.subheading { + font-size: 1.5rem; + margin-bottom: 2rem; +} + +.buttonContainer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1rem; +} + +.button { + background-color: #0070f3; + color: white; + border: none; + padding: 0.75rem 1.5rem; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.3s ease; + margin: 5px; +} + +.button:hover { + background-color: #0056b3; +} + +.column-flex { + display: flex; + flex-direction: column; +} + +.row-flex-space-between { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.column { + display: flex; + flex-direction: column; + align-items: center; /* Aligns buttons and headings in the center of the column */ + gap: 0.5rem; /* Adjusts the space between items in the column */ +} + +.columnHeading { + font-size: 1.25rem; /* Adjust size as needed */ + margin-bottom: 1rem; /* Adds some space between the heading and the first button */ +} diff --git a/utils/exportUtils.ts b/utils/exportUtils.ts new file mode 100644 index 0000000..f01b690 --- /dev/null +++ b/utils/exportUtils.ts @@ -0,0 +1,113 @@ +// exportUtils.ts +import { supabase } from "../lib/supabaseClient"; +import axios from 'axios'; + +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + const day = date.getDate(); + const months = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"]; + const month = months[date.getMonth()]; + const year = date.getFullYear(); + + return `${day} ${month} ${year}`; +}; + +const convertToMarkdown = (data: any[]): string => { + let markdown = ''; + const headers = Object.keys(data[0]).join(' | '); + markdown += `| ${headers} |\n| ${new Array(headers.length).fill('-').join(' | ')} |\n`; + + data.forEach((item) => { + const formattedItem = Object.entries(item).map(([key, value]) => { + if (key === 'created_at') { + return formatDate(value as string); + } + return value; + }); + const values = formattedItem.join(' | '); + markdown += `| ${values} |\n`; + }); + + return markdown; +}; + +const convertToCsv = (data: any[]): string => { + const headers = Object.keys(data[0]).join(','); + const rows = data.map((item) => + Object.entries(item).map(([key, value]) => { + if (key === 'created_at') { + return `"${formatDate(value as string)}"`; + } + return `"${value}"`; + }).join(',') + ); + return `${headers}\n${rows.join('\n')}`; +}; + +const exportAsCsv = async (data: any[]) => { + const csvData = convertToCsv(data); + const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = 'Export.csv'; // Consider making the filename dynamic + link.click(); +}; + +const exportAsPdf = async (data: any[]) => { + const markdown = convertToMarkdown(data); + const pdfResponse = await axios.post('/api/convertToPdf', { markdown }, { + responseType: 'blob', + }); + const url = window.URL.createObjectURL(new Blob([pdfResponse.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', 'Export.pdf'); // Consider making the filename dynamic + document.body.appendChild(link); + link.click(); + link.remove(); +}; + +const exportAsJson = async (data: any[]) => { + console.log("Json", data) +}; + +const exportStrategies = { + csv: exportAsCsv, + pdf: exportAsPdf, + json: exportAsJson, +}; + +const exportData = async (format: 'csv' | 'pdf' | 'json', data: any[]) => { + const exportStrategy = exportStrategies[format]; + if (!exportStrategy) { + throw new Error(`Export format '${format}' is not supported.`); + } + await exportStrategy(data); +}; + +export const exportTags = async (format: 'csv' | 'pdf' | 'json') => { + try { + const { data, error } = await supabase.from('tags').select('created_at, type, tag'); + if (error) { + console.error('Error exporting tags:', error); + return; + } + await exportData(format, data); + } catch (error) { + console.error('Error exporting tags:', error); + } +}; + +export const exportUsers = async (format: 'csv' | 'pdf' | 'json') => { + try { + const { data, error } = await supabase.from('users').select('created_at, global_name'); + if (error) { + console.error('Error exporting users:', error); + return; + } + await exportData(format, data); + } catch (error) { + console.error('Error exporting users:', error); + } +}; diff --git a/utils/generateMarkdown.js b/utils/generateMarkdown.js index 779245c..82c3aa4 100644 --- a/utils/generateMarkdown.js +++ b/utils/generateMarkdown.js @@ -237,5 +237,9 @@ export function generateMarkdown(summary, order) { if (summary.noSummaryGiven == true) { markdown += `No Summary Given \n`; } + //canceledSummary + if (summary.canceledSummary == true) { + markdown += `Meeting was canceled \n`; + } return markdown; };