1- import { useState } from 'react' ;
1+ import { useState , useRef } from 'react' ;
22import { 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' ;
47import styles from './Participation.module.css' ;
58
69function 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