Skip to content

Commit 541810a

Browse files
committed
Merge branch 'development' of https://github.com/OneCommunityGlobal/HighestGoodNetworkApp into Swathi_PR_Review_Team_Usability
2 parents 3a3f54a + d1a8d3d commit 541810a

15 files changed

Lines changed: 1709 additions & 281 deletions

src/components/BMDashboard/Issues/IssueDashboard.jsx

Lines changed: 171 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@ import {
66
FiTrash2,
77
FiCopy,
88
FiEdit,
9+
FiDownload,
910
} from 'react-icons/fi';
1011
import 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';
1221
import { useDispatch, useSelector } from 'react-redux';
1322
import {
1423
copyIssue,
@@ -17,6 +26,8 @@ import {
1726
renameIssue,
1827
} from '~/actions/bmdashboard/issueActions';
1928
import IssueHeader from './IssueHeader';
29+
import { toast } from 'react-toastify';
30+
import { jsPDF } from 'jspdf';
2031

2132
export 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"

src/components/BMDashboard/Issues/IssueDashboard.module.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@
5656
background-color: #2f4157;
5757
}
5858

59+
.exportDropdownMenuDark {
60+
background-color: #1c2541;
61+
border: 1px solid #3a506b;
62+
}
63+
64+
.exportDropdownItemDark {
65+
color: #ffffff !important;
66+
}
67+
68+
.exportDropdownItemDark:hover,
69+
.exportDropdownItemDark:focus {
70+
background-color: #2d3b66 !important;
71+
color: #ffffff !important;
72+
}
73+
5974
.issueDashboardDropdownItemDark.textDanger {
6075
color: #ff6b6b;
6176
}

src/components/BMDashboard/Issues/issueChart.module.css

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,51 @@
4848
}
4949

5050
.issueChartSelect {
51-
padding: 2px 10px;
52-
font-size: 16px;
53-
border-radius: 4px;
54-
border: 1px solid #ccc;
55-
background-color: #fff;
56-
color: #333;
57-
cursor: pointer;
58-
transition: border-color 0.3s ease;
59-
outline: none;
51+
padding: 0;
52+
border: none;
53+
background: transparent;
6054
margin-bottom: 20px;
6155
}
6256

63-
.issueChartSelectDark {
64-
border-color: #3d444d;
65-
background-color: #22272e;
57+
.filterMenuActions {
58+
display: flex;
59+
justify-content: space-between;
60+
gap: 8px;
61+
padding: 6px 10px 8px;
62+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
63+
position: static;
64+
background: inherit;
65+
}
66+
67+
.filterMenuButton {
68+
background: transparent;
69+
border: 1px solid #4caf50;
70+
color: #4caf50;
71+
padding: 4px 8px;
72+
border-radius: 6px;
73+
font-size: 12px;
74+
cursor: pointer;
75+
}
76+
77+
.activeFilterSummary {
78+
width: 100%;
79+
text-align: center;
80+
font-size: 13px;
81+
color: #5b6470;
82+
margin-bottom: 6px;
83+
font-weight: 600;
84+
}
85+
86+
.issueChartEventContainerDark .activeFilterSummary {
6687
color: #cfd7e3;
6788
}
6889

90+
.issueChartSelectDark {
91+
border: none;
92+
background: transparent;
93+
color: inherit;
94+
}
95+
6996
/* Spacing between year groups within each issue type */
7097
.issueChartYearGroup {
7198
margin-bottom: 24px;
@@ -248,4 +275,4 @@
248275
border-style: dashed;
249276
opacity: 0.7;
250277
}
251-
}
278+
}

0 commit comments

Comments
 (0)