Skip to content

Commit 1f8b049

Browse files
Merge pull request #4647 from OneCommunityGlobal/chirag-community-portal-report-share-icon-missing-fix
Chirag community portal report share icon missing fix
2 parents ddc667b + b9b4b7b commit 1f8b049

2 files changed

Lines changed: 421 additions & 41 deletions

File tree

src/components/CommunityPortal/Reports/Participation/NoShowInsights.jsx

Lines changed: 246 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
import { useState } from 'react';
1+
import { useState, useRef } from 'react';
22
import { useSelector } from 'react-redux';
3-
import mockEvents from './mockData'; // Import mock data
3+
import { ArrowUpDown, ArrowUp, ArrowDown, SquareArrowOutUpRight } from 'lucide-react';
4+
import html2canvas from 'html2canvas';
5+
import { jsPDF } from 'jspdf';
6+
import mockEvents from './mockData';
47
import styles from './Participation.module.css';
58

69
function NoShowInsights() {
710
const [dateFilter, setDateFilter] = useState('All');
811
const [activeTab, setActiveTab] = useState('Event type');
12+
const [sortOrder, setSortOrder] = useState('none');
913
const darkMode = useSelector(state => state.theme.darkMode);
14+
const insightsRef = useRef(null);
15+
const [isExportOpen, setIsExportOpen] = useState(false);
16+
const [exportError, setExportError] = useState('');
17+
const [isExporting, setIsExporting] = useState(false);
1018

1119
const filterByDate = events => {
1220
const today = new Date();
@@ -33,6 +41,15 @@ function NoShowInsights() {
3341
});
3442
};
3543

44+
const handleSortClick = () => {
45+
setSortOrder(prev => {
46+
if (prev === 'none' || prev === 'desc') return 'asc';
47+
if (prev === 'asc') return 'desc';
48+
return 'none';
49+
});
50+
};
51+
const SortIcon = sortOrder === 'none' ? ArrowUpDown : sortOrder === 'asc' ? ArrowUp : ArrowDown;
52+
3653
const calculateStats = filteredEvents => {
3754
const statsMap = new Map();
3855

@@ -64,10 +81,16 @@ function NoShowInsights() {
6481
const renderStats = () => {
6582
const filteredEvents = filterByDate(mockEvents);
6683
const stats = calculateStats(filteredEvents);
84+
const finalStats =
85+
sortOrder === 'none'
86+
? stats
87+
: [...stats].sort((a, b) =>
88+
sortOrder === 'asc' ? a.percentage - b.percentage : b.percentage - a.percentage,
89+
);
6790

68-
return stats.map(item => (
91+
return finalStats.map(item => (
6992
<div key={item.label} className={styles.insightItem}>
70-
<div className={`${styles.insightsLabel} ${darkMode ? styles.insightsLabelDark : ''}`}>
93+
<div className={`${styles.insightLabel} ${darkMode ? styles.insightLabelDark : ''}`}>
7194
{item.label}
7295
</div>
7396
<div className={`${styles.insightBar}`}>
@@ -84,39 +107,229 @@ function NoShowInsights() {
84107
));
85108
};
86109

110+
const buildPdfFromView = async () => {
111+
try {
112+
if (typeof jsPDF === 'undefined' || typeof html2canvas === 'undefined') {
113+
return;
114+
}
115+
if (!insightsRef.current) return;
116+
117+
const canvas = await html2canvas(insightsRef.current, {
118+
scale: 2,
119+
useCORS: true,
120+
backgroundColor: darkMode ? '#1C2541' : null,
121+
});
122+
123+
const imgData = canvas.toDataURL('image/png');
124+
const pdf = new jsPDF('p', 'pt', 'a4');
125+
126+
const pageWidth = pdf.internal.pageSize.getWidth();
127+
const pageHeight = pdf.internal.pageSize.getHeight();
128+
129+
const imgWidth = pageWidth;
130+
const imgHeight = (canvas.height * imgWidth) / canvas.width;
131+
132+
let y = 0;
133+
134+
let remainingHeight = imgHeight;
135+
while (remainingHeight > 0) {
136+
pdf.addImage(imgData, 'PNG', 0, y, imgWidth, imgHeight);
137+
remainingHeight -= pageHeight;
138+
139+
if (remainingHeight > 0) {
140+
pdf.addPage();
141+
y -= pageHeight;
142+
}
143+
}
144+
return pdf;
145+
} catch (pdfError) {
146+
setExportError(pdfError?.message || 'Failed to share PDF.');
147+
} finally {
148+
setIsExporting(false);
149+
}
150+
};
151+
152+
const getPdfFilename = () => {
153+
const now = new Date();
154+
const localDate = now.toLocaleDateString('en-CA');
155+
const filename = `no-show-insights_${dateFilter}_${activeTab}_${localDate}.pdf`;
156+
return filename.replace(/\s+/g, '_').toLowerCase();
157+
};
158+
159+
const handleDownloadPdf = async () => {
160+
try {
161+
setIsExporting(true);
162+
setExportError('');
163+
const pdf = await buildPdfFromView();
164+
pdf.save(getPdfFilename());
165+
setIsExportOpen(false);
166+
} catch (e) {
167+
setExportError(e?.message || 'Failed to download PDF.');
168+
} finally {
169+
setIsExporting(false);
170+
}
171+
};
172+
173+
const handleSharePdf = async () => {
174+
try {
175+
setIsExporting(true);
176+
setExportError('');
177+
178+
const pdf = await buildPdfFromView();
179+
const blob = pdf.output('blob');
180+
const file = new File([blob], getPdfFilename(), { type: 'application/pdf' });
181+
182+
if (!navigator.share || !navigator.canShare?.({ files: [file] })) {
183+
setExportError(
184+
'Sharing is not supported in this browser. Please download the PDF instead.',
185+
);
186+
return;
187+
}
188+
189+
await navigator.share({
190+
title: 'No-show rate insights',
191+
text: `Insights (${dateFilter}, ${activeTab})`,
192+
files: [file],
193+
});
194+
195+
setIsExportOpen(false);
196+
} catch (e) {
197+
setExportError(e?.message || 'Failed to share PDF.');
198+
} finally {
199+
setIsExporting(false);
200+
}
201+
};
202+
87203
return (
88-
<div className={`${styles.insights} ${darkMode ? styles.insightsDark : ''}`}>
89-
<div className={`${styles.insightsHeader} ${darkMode ? styles.insightsHeaderDark : ''}`}>
90-
<h3>No-show rate insights</h3>
91-
<div className={`${styles.insightsFilters} ${darkMode ? styles.insightsFiltersDark : ''}`}>
92-
<select value={dateFilter} onChange={e => setDateFilter(e.target.value)}>
93-
<option value="All">All Time</option>
94-
<option value="Today">Today</option>
95-
<option value="This Week">This Week</option>
96-
<option value="This Month">This Month</option>
97-
</select>
98-
</div>
99-
</div>
204+
<>
205+
{isExportOpen && (
206+
<div
207+
className={styles.modalOverlay}
208+
onClick={() => !isExporting && setIsExportOpen(false)}
209+
onKeyDown={() => !isExporting && setIsExportOpen(false)}
210+
role="button"
211+
tabIndex={0}
212+
>
213+
<div
214+
className={`${styles.modal} ${darkMode ? styles.modalDark : ''}`}
215+
onClick={e => e.stopPropagation()}
216+
onKeyDown={e => e.stopPropagation()}
217+
role="button"
218+
tabIndex={0}
219+
>
220+
<div className={styles.modalHeader}>
221+
<h4 className={styles.modalTitle}>Export No-show Insights</h4>
222+
<button
223+
type="button"
224+
className={styles.modalClose}
225+
onClick={() => !isExporting && setIsExportOpen(false)}
226+
aria-label="Close export modal"
227+
>
228+
×
229+
</button>
230+
</div>
231+
232+
<div className={styles.modalBody}>
233+
<div className={styles.modalMeta}>
234+
<div>
235+
<strong>Filter:</strong> {dateFilter}
236+
</div>
237+
<div>
238+
<strong>View:</strong> {activeTab}
239+
</div>
240+
</div>
241+
242+
{exportError && <div className={styles.modalError}>{exportError}</div>}
100243

101-
<div className={`${styles.insightsTabs} ${darkMode ? styles.insightsTabsDarkMode : ''}`}>
102-
{['Event type', 'Time', 'Location'].map(tab => (
103-
<button
104-
key={tab}
105-
type="button"
106-
className={`
107-
${styles.insightsTab}
108-
${darkMode ? styles.insightsTabDarkMode : ''}
109-
${activeTab === tab ? (darkMode ? styles.activeTabDarkMode : styles.activeTab) : ''}
110-
`}
111-
onClick={() => setActiveTab(tab)}
244+
<div className={styles.modalActions}>
245+
<button
246+
type="button"
247+
className={`${
248+
darkMode ? styles.exportOptionsButtonsDark : styles.exportOptionsButtons
249+
}`}
250+
onClick={handleDownloadPdf}
251+
disabled={isExporting}
252+
>
253+
{isExporting ? 'Working…' : 'Download PDF'}
254+
</button>
255+
256+
<button
257+
type="button"
258+
className={`${
259+
darkMode ? styles.exportOptionsButtonsDark : styles.exportOptionsButtons
260+
}`}
261+
onClick={handleSharePdf}
262+
disabled={isExporting}
263+
>
264+
{isExporting ? 'Working…' : 'Share PDF'}
265+
</button>
266+
</div>
267+
</div>
268+
</div>
269+
</div>
270+
)}
271+
<div
272+
ref={insightsRef}
273+
className={`${styles.insights} ${darkMode ? styles.insightsDark : ''}`}
274+
>
275+
<div className={`${styles.insightsHeader} ${darkMode ? styles.insightsHeaderDark : ''}`}>
276+
<h3>No-show rate insights</h3>
277+
<div
278+
className={`${styles.insightsFilters} ${darkMode ? styles.insightsFiltersDark : ''}`}
112279
>
113-
{tab}
114-
</button>
115-
))}
116-
</div>
280+
<select value={dateFilter} onChange={e => setDateFilter(e.target.value)}>
281+
<option value="All">All Time</option>
282+
<option value="Today">Today</option>
283+
<option value="This Week">This Week</option>
284+
<option value="This Month">This Month</option>
285+
</select>
286+
</div>
287+
</div>
117288

118-
<div className={styles.insightsContent}>{renderStats()}</div>
119-
</div>
289+
<div className={styles.insightsTabsContainer}>
290+
<div className={`${styles.insightsTabs} ${darkMode ? styles.insightsTabsDarkMode : ''}`}>
291+
{['Event type', 'Time', 'Location'].map(tab => (
292+
<button
293+
key={tab}
294+
type="button"
295+
className={`
296+
${styles.insightsTab}
297+
${darkMode ? styles.insightsTabDarkMode : ''}
298+
${
299+
activeTab === tab ? (darkMode ? styles.activeTabDarkMode : styles.activeTab) : ''
300+
}`}
301+
onClick={() => setActiveTab(tab)}
302+
>
303+
{tab}
304+
</button>
305+
))}
306+
</div>
307+
<div className={styles.icons}>
308+
<div className={styles.tooltipWrapper}>
309+
<SortIcon onClick={handleSortClick} className={styles.sortIcon} />
310+
<span className={styles.tooltip}>
311+
{sortOrder === 'none'
312+
? 'Default'
313+
: sortOrder === 'asc'
314+
? 'Low → High'
315+
: 'High → Low'}
316+
</span>
317+
</div>
318+
<div className={styles.tooltipWrapper}>
319+
<SquareArrowOutUpRight
320+
onClick={() => {
321+
setExportError('');
322+
setIsExportOpen(true);
323+
}}
324+
/>
325+
<span className={styles.tooltip}>Export Data</span>
326+
</div>
327+
</div>
328+
</div>
329+
330+
<div className={styles.insightsContent}>{renderStats()}</div>
331+
</div>
332+
</>
120333
);
121334
}
122335

0 commit comments

Comments
 (0)