@@ -6,9 +6,18 @@ import {
66 FiTrash2 ,
77 FiCopy ,
88 FiEdit ,
9+ FiDownload ,
910} from 'react-icons/fi' ;
1011import styles from './IssueDashboard.module.css' ;
11- import { Col , Row , Table } from 'reactstrap' ;
12+ import {
13+ Col ,
14+ Row ,
15+ Table ,
16+ UncontrolledDropdown ,
17+ DropdownToggle ,
18+ DropdownMenu ,
19+ DropdownItem ,
20+ } from 'reactstrap' ;
1221import { useDispatch , useSelector } from 'react-redux' ;
1322import {
1423 copyIssue ,
@@ -17,6 +26,8 @@ import {
1726 renameIssue ,
1827} from '~/actions/bmdashboard/issueActions' ;
1928import IssueHeader from './IssueHeader' ;
29+ import { toast } from 'react-toastify' ;
30+ import { jsPDF } from 'jspdf' ;
2031
2132export default function IssueDashboard ( ) {
2233 const dispatch = useDispatch ( ) ;
@@ -26,14 +37,18 @@ export default function IssueDashboard() {
2637 const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
2738 const [ menuOpen , setMenuOpen ] = useState ( null ) ;
2839 const itemsPerPage = 5 ;
29- const totalPages = Math . ceil ( issues . length / itemsPerPage ) ;
40+ const displayIssues = issues ;
41+ const totalPages = Math . ceil ( displayIssues . length / itemsPerPage ) ;
3042 const [ showDeleteModal , setShowDeleteModal ] = useState ( false ) ;
3143 const [ showRenameModal , setShowRenameModal ] = useState ( false ) ;
3244 const [ showCopyModal , setShowCopyModal ] = useState ( false ) ;
3345 const [ selectedIssue , setSelectedIssue ] = useState ( null ) ;
3446 const [ renameValue , setRenameValue ] = useState ( '' ) ;
3547
36- const currentItems = issues . slice ( ( currentPage - 1 ) * itemsPerPage , currentPage * itemsPerPage ) ;
48+ const currentItems = displayIssues . slice (
49+ ( currentPage - 1 ) * itemsPerPage ,
50+ currentPage * itemsPerPage ,
51+ ) ;
3752
3853 const toggleMenu = id => setMenuOpen ( open => ( open === id ? null : id ) ) ;
3954
@@ -77,6 +92,137 @@ export default function IssueDashboard() {
7792 dispatch ( fetchAllIssues ( ) ) ;
7893 } , [ dispatch ] ) ;
7994
95+ const buildExportRows = sourceIssues => {
96+ return ( sourceIssues || [ ] ) . map ( issue => {
97+ const assignedUser = issue . assignedTo
98+ ? `${ issue . assignedTo . firstName || '' } ${ issue . assignedTo . lastName || '' } ` . trim ( )
99+ : issue . assignedToName || issue . assignee || 'Unassigned' ;
100+
101+ const formatDate = value => {
102+ if ( ! value ) return '-' ;
103+ const date = new Date ( value ) ;
104+ if ( Number . isNaN ( date . getTime ( ) ) ) return `${ value } ` ;
105+ return date . toLocaleDateString ( ) ;
106+ } ;
107+
108+ return {
109+ issueName : issue . name || issue . issueName || '-' ,
110+ status : issue . status || issue . state || issue . issueStatus || '-' ,
111+ priority : issue . priority || issue . severity || issue . issuePriority || '-' ,
112+ assignedUser : assignedUser || '-' ,
113+ createdDate : formatDate (
114+ issue . createdDate || issue . createdAt || issue . openDate || issue . dateCreated ,
115+ ) ,
116+ lastUpdated : formatDate ( issue . updatedDate || issue . updatedAt || issue . lastUpdated ) ,
117+ } ;
118+ } ) ;
119+ } ;
120+
121+ const exportHeaders = [
122+ 'Issue Name' ,
123+ 'Status' ,
124+ 'Priority' ,
125+ 'Assigned User' ,
126+ 'Created Date' ,
127+ 'Last Updated' ,
128+ ] ;
129+
130+ const downloadBlob = ( blob , filename ) => {
131+ const url = window . URL . createObjectURL ( blob ) ;
132+ const link = document . createElement ( 'a' ) ;
133+ link . href = url ;
134+ link . download = filename ;
135+ document . body . appendChild ( link ) ;
136+ link . click ( ) ;
137+ link . remove ( ) ;
138+ window . URL . revokeObjectURL ( url ) ;
139+ } ;
140+
141+ const handleExportCsv = ( ) => {
142+ const exportRows = buildExportRows ( displayIssues ) ;
143+ if ( exportRows . length === 0 ) {
144+ toast . info ( 'No issues available to export.' ) ;
145+ return ;
146+ }
147+ const escapeCsv = value => `"${ String ( value ?? '' ) . replace ( / " / g, '""' ) } "` ;
148+ const rows = [
149+ exportHeaders ,
150+ ...exportRows . map ( row => [
151+ row . issueName || '-' ,
152+ row . status || '-' ,
153+ row . priority || '-' ,
154+ row . assignedUser || '-' ,
155+ row . createdDate || '-' ,
156+ row . lastUpdated || '-' ,
157+ ] ) ,
158+ ] ;
159+
160+ const csvContent = rows . map ( row => row . map ( escapeCsv ) . join ( ',' ) ) . join ( '\n' ) ;
161+ const blob = new Blob ( [ csvContent ] , { type : 'text/csv;charset=utf-8;' } ) ;
162+ downloadBlob ( blob , `issues-export-${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } .csv` ) ;
163+ toast . success ( 'Issue export generated (CSV).' ) ;
164+ } ;
165+
166+ const handleExportPdf = ( ) => {
167+ const exportRows = buildExportRows ( displayIssues ) ;
168+ if ( exportRows . length === 0 ) {
169+ toast . info ( 'No issues available to export.' ) ;
170+ return ;
171+ }
172+ const doc = new jsPDF ( { orientation : 'portrait' , unit : 'pt' , format : 'a4' } ) ;
173+ const pageWidth = doc . internal . pageSize . getWidth ( ) ;
174+ const startX = 40 ;
175+ const startY = 50 ;
176+ const rowHeight = 18 ;
177+ const colWidths = [ 160 , 70 , 70 , 110 , 80 , 80 ] ;
178+ const truncate = ( text , maxWidth ) => {
179+ if ( doc . getTextWidth ( text ) <= maxWidth ) return text ;
180+ let truncated = text ;
181+ while ( truncated . length > 0 && doc . getTextWidth ( `${ truncated } …` ) > maxWidth ) {
182+ truncated = truncated . slice ( 0 , - 1 ) ;
183+ }
184+ return `${ truncated } …` ;
185+ } ;
186+
187+ doc . setFontSize ( 12 ) ;
188+ doc . text ( 'Issue Export' , startX , startY - 20 ) ;
189+ doc . setFontSize ( 9 ) ;
190+
191+ let x = startX ;
192+ exportHeaders . forEach ( ( header , index ) => {
193+ const width = colWidths [ index ] ;
194+ doc . text ( truncate ( header , width - 4 ) , x , startY ) ;
195+ x += width ;
196+ } ) ;
197+
198+ let y = startY + rowHeight ;
199+ exportRows . forEach ( row => {
200+ if ( y > doc . internal . pageSize . getHeight ( ) - 40 ) {
201+ doc . addPage ( ) ;
202+ y = 50 ;
203+ }
204+ const values = [
205+ row . issueName || '-' ,
206+ row . status || '-' ,
207+ row . priority || '-' ,
208+ row . assignedUser || '-' ,
209+ row . createdDate || '-' ,
210+ row . lastUpdated || '-' ,
211+ ] ;
212+ let colX = startX ;
213+ values . forEach ( ( value , index ) => {
214+ const width = colWidths [ index ] ;
215+ doc . text ( truncate ( String ( value ?? '' ) , width - 4 ) , colX , y ) ;
216+ colX += width ;
217+ } ) ;
218+ y += rowHeight ;
219+ } ) ;
220+
221+ const filename = `issues-export-${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } .pdf` ;
222+ doc . save ( filename ) ;
223+ toast . success ( 'Issue export generated (PDF).' ) ;
224+ } ;
225+
80226 function getTimeSince ( dateStr ) {
81227 const date = new Date ( dateStr ) ;
82228 const now = new Date ( ) ;
@@ -108,6 +254,28 @@ export default function IssueDashboard() {
108254 < Col >
109255 < h4 className = { `fw-semibold ${ darkMode ? 'text-light' : '' } ` } > Issue Dashboard</ h4 >
110256 </ Col >
257+ < Col className = "d-flex justify-content-end" >
258+ < UncontrolledDropdown >
259+ < DropdownToggle tag = "button" className = "btn btn-sm btn-primary" type = "button" >
260+ < FiDownload className = "me-2" />
261+ Export
262+ </ DropdownToggle >
263+ < DropdownMenu end className = { `${ darkMode ? styles . exportDropdownMenuDark : '' } ` } >
264+ < DropdownItem
265+ onClick = { handleExportCsv }
266+ className = { `${ darkMode ? styles . exportDropdownItemDark : '' } ` }
267+ >
268+ Export as CSV
269+ </ DropdownItem >
270+ < DropdownItem
271+ onClick = { handleExportPdf }
272+ className = { `${ darkMode ? styles . exportDropdownItemDark : '' } ` }
273+ >
274+ Export as PDF
275+ </ DropdownItem >
276+ </ DropdownMenu >
277+ </ UncontrolledDropdown >
278+ </ Col >
111279 </ Row >
112280
113281 < div className = { `${ styles . issuesTableResponsive } ` } >
@@ -239,31 +407,6 @@ export default function IssueDashboard() {
239407 </ button >
240408 </ li >
241409
242- { Array . from ( { length : totalPages } , ( _ , i ) => {
243- const isActive = currentPage === i + 1 ;
244- let buttonClass = 'page-link' ;
245-
246- if ( darkMode ) {
247- buttonClass += ' bg-dark text-light border-secondary' ;
248- }
249-
250- if ( isActive ) {
251- buttonClass += darkMode ? ' bg-secondary' : ' bg-primary' ;
252- }
253-
254- return (
255- < li key = { i + 1 } className = { `page-item ${ isActive ? 'active' : '' } ` } >
256- < button
257- type = "button"
258- className = { buttonClass }
259- onClick = { ( ) => setCurrentPage ( i + 1 ) }
260- >
261- { i + 1 }
262- </ button >
263- </ li >
264- ) ;
265- } ) }
266-
267410 < li className = { `page-item ${ currentPage === totalPages ? 'disabled' : '' } ` } >
268411 < button
269412 type = "button"
0 commit comments