diff --git a/src/components/CommunityPortal/Reports/Participation/NoShowInsights.jsx b/src/components/CommunityPortal/Reports/Participation/NoShowInsights.jsx index ab635e8ad0..7fce559d4a 100644 --- a/src/components/CommunityPortal/Reports/Participation/NoShowInsights.jsx +++ b/src/components/CommunityPortal/Reports/Participation/NoShowInsights.jsx @@ -1,12 +1,20 @@ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import { useSelector } from 'react-redux'; +import { ArrowUpDown, ArrowUp, ArrowDown, SquareArrowOutUpRight } from 'lucide-react'; +import html2canvas from 'html2canvas'; +import { jsPDF } from 'jspdf'; import mockEvents from './mockData'; import styles from './Participation.module.css'; function NoShowInsights() { const [dateFilter, setDateFilter] = useState('All'); const [activeTab, setActiveTab] = useState('Event type'); + const [sortOrder, setSortOrder] = useState('none'); const darkMode = useSelector(state => state.theme.darkMode); + const insightsRef = useRef(null); + const [isExportOpen, setIsExportOpen] = useState(false); + const [exportError, setExportError] = useState(''); + const [isExporting, setIsExporting] = useState(false); const filterByDate = events => { const today = new Date(); @@ -33,6 +41,15 @@ function NoShowInsights() { }); }; + const handleSortClick = () => { + setSortOrder(prev => { + if (prev === 'none' || prev === 'desc') return 'asc'; + if (prev === 'asc') return 'desc'; + return 'none'; + }); + }; + const SortIcon = sortOrder === 'none' ? ArrowUpDown : sortOrder === 'asc' ? ArrowUp : ArrowDown; + const calculateStats = filteredEvents => { const statsMap = new Map(); @@ -64,8 +81,14 @@ function NoShowInsights() { const renderStats = () => { const filteredEvents = filterByDate(mockEvents); const stats = calculateStats(filteredEvents); + const finalStats = + sortOrder === 'none' + ? stats + : [...stats].sort((a, b) => + sortOrder === 'asc' ? a.percentage - b.percentage : b.percentage - a.percentage, + ); - return stats.map(item => ( + return finalStats.map(item => (
{item.label} @@ -84,35 +107,230 @@ function NoShowInsights() { )); }; + const buildPdfFromView = async () => { + try { + if (typeof jsPDF === 'undefined' || typeof html2canvas === 'undefined') { + return; + } + if (!insightsRef.current) return; + + const canvas = await html2canvas(insightsRef.current, { + scale: 2, + useCORS: true, + backgroundColor: darkMode ? '#1C2541' : null, + }); + + const imgData = canvas.toDataURL('image/png'); + const pdf = new jsPDF('p', 'pt', 'a4'); + + const pageWidth = pdf.internal.pageSize.getWidth(); + const pageHeight = pdf.internal.pageSize.getHeight(); + + const imgWidth = pageWidth; + const imgHeight = (canvas.height * imgWidth) / canvas.width; + + let y = 0; + + let remainingHeight = imgHeight; + while (remainingHeight > 0) { + pdf.addImage(imgData, 'PNG', 0, y, imgWidth, imgHeight); + remainingHeight -= pageHeight; + + if (remainingHeight > 0) { + pdf.addPage(); + y -= pageHeight; + } + } + return pdf; + } catch (pdfError) { + setExportError(pdfError?.message || 'Failed to share PDF.'); + } finally { + setIsExporting(false); + } + }; + + const getPdfFilename = () => { + const now = new Date(); + const localDate = now.toLocaleDateString('en-CA'); + const filename = `no-show-insights_${dateFilter}_${activeTab}_${localDate}.pdf`; + return filename.replace(/\s+/g, '_').toLowerCase(); + }; + + const handleDownloadPdf = async () => { + try { + setIsExporting(true); + setExportError(''); + const pdf = await buildPdfFromView(); + pdf.save(getPdfFilename()); + setIsExportOpen(false); + } catch (e) { + setExportError(e?.message || 'Failed to download PDF.'); + } finally { + setIsExporting(false); + } + }; + + const handleSharePdf = async () => { + try { + setIsExporting(true); + setExportError(''); + + const pdf = await buildPdfFromView(); + const blob = pdf.output('blob'); + const file = new File([blob], getPdfFilename(), { type: 'application/pdf' }); + + if (!navigator.share || !navigator.canShare?.({ files: [file] })) { + setExportError( + 'Sharing is not supported in this browser. Please download the PDF instead.', + ); + return; + } + + await navigator.share({ + title: 'No-show rate insights', + text: `Insights (${dateFilter}, ${activeTab})`, + files: [file], + }); + + setIsExportOpen(false); + } catch (e) { + setExportError(e?.message || 'Failed to share PDF.'); + } finally { + setIsExporting(false); + } + }; + return ( -
-
-

No-show rate insights

-
- + <> + {isExportOpen && ( +
!isExporting && setIsExportOpen(false)} + onKeyDown={() => !isExporting && setIsExportOpen(false)} + role="button" + tabIndex={0} + > +
e.stopPropagation()} + onKeyDown={e => e.stopPropagation()} + role="button" + tabIndex={0} + > +
+

Export No-show Insights

+ +
+ +
+
+
+ Filter: {dateFilter} +
+
+ View: {activeTab} +
+
+ + {exportError &&
{exportError}
} + +
+ + + +
+
+
+
+ )} +
+
+

No-show rate insights

+
+ +
-
-
- {['Event type', 'Time', 'Location'].map(tab => ( - - ))} -
+ {['Event type', 'Time', 'Location'].map(tab => ( + + ))} +
+
+
+ + + {sortOrder === 'none' + ? 'Default' + : sortOrder === 'asc' + ? 'Low → High' + : 'High → Low'} + +
+
+ { + setExportError(''); + setIsExportOpen(true); + }} + /> + Export Data +
+
+
-
{renderStats()}
-
+
{renderStats()}
+
+ ); } diff --git a/src/components/CommunityPortal/Reports/Participation/Participation.module.css b/src/components/CommunityPortal/Reports/Participation/Participation.module.css index f82ec50d11..d011f0abf1 100644 --- a/src/components/CommunityPortal/Reports/Participation/Participation.module.css +++ b/src/components/CommunityPortal/Reports/Participation/Participation.module.css @@ -12,7 +12,7 @@ } .participationLandingPageDark { - max-width: 100%; + max-width: 90%; margin: 0 auto; background-color: #1B2A41; color: #ffffff; @@ -358,7 +358,13 @@ border-radius: 5px; } +.insightsFiltersDark select{ + color: #f8f9fa; + background-color: #1C2541; +} + .insightsTabs { + flex: 1; display: flex; border: 1px solid #ccc; border-radius: 5px; @@ -366,18 +372,46 @@ max-width: 60%; } +.insightsTabsContainer{ + display: flex; + align-items: center; + justify-content: space-between; +} + +.icons{ + cursor: pointer; + display: flex; + gap: 8px; +} + .insightsTab { flex: 1; text-align: center; padding: 10px 0; font-size: 0.9rem; - color: #555; - background-color: #f8f9fa; border: none; cursor: pointer; transition: background-color 0.3s, color 0.3s; } +.insightsTabLight{ + color: #555; + background-color: #f8f9fa; +} + +.insightsTabDark{ + color: #f8f9fa; + background-color: #1C2541; +} + +.insightsTabDark:hover { + background-color: #3A506B; +} + +.insightsTabLight:hover { + background-color: #e0e0e0; +} + .insightsTab:not(:last-child) { border-right: 1px solid #ccc; } @@ -389,10 +423,6 @@ font-weight: bold; } -.insightsTab:hover { - background-color: #e0e0e0; -} - .insightsContent { display: flex; flex-direction: column; @@ -447,6 +477,136 @@ color: #333; } +.insightsPercentageDark { + color: #ffffff; + } + +.tooltipWrapper { + position: relative; + display: inline-flex; + align-items: center; +} + +.tooltip { + position: absolute; + bottom: 125%; + left: 50%; + transform: translateX(-50%); + padding: 6px 10px; + font-size: 12px; + background-color: #111; + color: #fff; + border-radius: 6px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + z-index: 10; +} + +.tooltipWrapper:hover .tooltip { + opacity: 1; +} + +/* ---------- Export PDF modal ---------- */ +.modalOverlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + display: flex; + justify-content: center; + align-items: center; + z-index: 999; +} + +.modal { + width: 420px; + max-width: calc(100vw - 32px); + background: #fff; + border-radius: 12px; + padding: 16px; +} + +.modalDark { + background: #1C2541; +} + +.modalHeader { + display: flex; + align-items: center; + justify-content: space-between; +} + +.modalTitle { + margin: 0; +} + +.modalClose { + border: none; + background: transparent; + font-size: 22px; + cursor: pointer; +} + +.modalBody { + margin-top: 12px; +} + +.modalMeta { + font-size: 16px; + opacity: 0.85; + display: grid; + gap: 6px; + margin-bottom: 12px; +} + +.modalError { + margin-bottom: 12px; + font-size: 13px; + color: #b00020; +} + +.modalActions { + display: flex; + gap: 10px; +} + +.exportOptionsButtons, +.exportOptionsButtonsDark { + flex: 1; + padding: 10px 12px; + border-radius: 10px; + cursor: pointer; +} + +.exportOptionsButtons{ + background: transparent; + border: 1px solid #ccc; + color: #555; +} + +.exportOptionsButtons:hover{ + color: #555; + background-color: #e0e0e0; +} + +.exportOptionsButtonsDark{ + border: 1px solid #ccc; + color: #111; +} + +.exportOptionsButtonsDark:hover { + background: #3A506B; +} + +.exportOptionsButtons:disabled, +.exportOptionsButtonsDark:disabled { + opacity: 0.6; + cursor: not-allowed; +} + + + /* ---------- PDF/Print helpers ---------- */ .pageBreakBefore { break-before: page; page-break-before: always; } .pageBreakAfter { break-after: page; page-break-after: always; }