diff --git a/modules/issue_tracker/css/issue_card.css b/modules/issue_tracker/css/issue_card.css
new file mode 100644
index 00000000000..e059ff246e0
--- /dev/null
+++ b/modules/issue_tracker/css/issue_card.css
@@ -0,0 +1,142 @@
+.issue-card {
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 16px;
+ display: flex;
+ flex-direction: column;
+ background-color: #fff;
+}
+
+.issue-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 16px;
+}
+
+.issue-title-section {
+ flex: 1;
+}
+
+.issue-dates {
+ flex: 1;
+ text-align: right;
+ font-size: 0.9em;
+ color: #555;
+}
+
+.issue-form {
+ width: 100%;
+}
+
+.issue-controls {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+.issue-content {
+ display: flex;
+ gap: 16px;
+ flex: 1;
+}
+
+.description-section {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.comments-section {
+ flex: 1;
+ border-left: 1px solid #eee;
+ padding-left: 16px;
+ display: flex;
+ flex-direction: column;
+}
+
+.description-container {
+ flex: 1;
+ max-height: 200px;
+ overflow-y: auto;
+ margin-top: 8px;
+ padding-right: 8px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+}
+
+.comments-container {
+ flex: 1;
+ max-height: 200px;
+ overflow-y: auto;
+ margin-top: 8px;
+ padding-right: 8px;
+}
+
+.description-text {
+ margin: 8px 0;
+ white-space: pre-wrap;
+}
+
+.comment {
+ margin-bottom: 12px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.comment-text {
+ margin: 4px 0;
+}
+
+.comment-meta {
+ font-size: 0.8em;
+ color: #777;
+}
+
+.no-comments {
+ font-style: italic;
+ color: #777;
+}
+
+.issue-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.title-input {
+ width: 100%;
+ padding: 4px;
+ font-size: 1em;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+
+.textarea {
+ width: 100%;
+ height: 200px;
+ resize: none;
+ padding: 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+
+@media (max-width: 600px) {
+ .issue-content {
+ flex-direction: column;
+ }
+
+ .comments-section {
+ border-left: none;
+ padding-left: 0;
+ margin-top: 16px;
+ }
+
+ .description-container,
+ .comments-container {
+ max-height: 150px;
+ }
+}
diff --git a/modules/issue_tracker/css/issue_tracker_batchmode.css b/modules/issue_tracker/css/issue_tracker_batchmode.css
new file mode 100644
index 00000000000..727795e7b2c
--- /dev/null
+++ b/modules/issue_tracker/css/issue_tracker_batchmode.css
@@ -0,0 +1,205 @@
+.issue-tracker-batch-mode {
+ display: flex;
+ flex-direction: column;
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.filter-tabs {
+ display: flex;
+ margin-bottom: 20px;
+ border-bottom: 1px solid #ddd;
+}
+
+.filter-tabs button {
+ padding: 10px 20px;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all 0.3s ease;
+}
+
+.filter-tabs button.active {
+ border-bottom: 2px solid #074785;
+ color: #074785;
+}
+
+.filter-section {
+ margin-bottom: 20px;
+}
+
+.filter-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.filter-list label {
+ display: flex;
+ align-items: center;
+ margin-bottom: 0;
+ font-weight: normal;
+}
+
+.filter-list label span {
+ margin-left: 3px;
+}
+
+.issues-list {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.issue-card {
+ border: 1px solid #ddd;
+ padding: 20px;
+ border-radius: 8px;
+ background-color: #fff;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ width: 100%;
+}
+
+.issue-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.issue-title-section {
+ display: flex;
+ align-items: center;
+}
+
+.issue-header h3 {
+ margin: 0;
+ display: flex;
+ align-items: baseline;
+ font-size: 1.5em;
+}
+
+.issue-header a {
+ color: #074785;
+ text-decoration: none;
+ display: flex;
+ align-items: baseline;
+}
+
+.issue-id {
+ font-size: 0.8em;
+ color: #666;
+ background-color: #f0f0f0;
+ padding: 2px 6px;
+ border-radius: 12px;
+ margin-right: 8px;
+}
+
+.issue-dates {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ font-size: 0.8em;
+ color: #666;
+}
+
+.issue-controls {
+ display: flex;
+ gap: 15px;
+ margin-bottom: 15px;
+}
+
+.issue-controls select {
+ padding: 5px 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background-color: #f8f8f8;
+}
+
+.issue-description {
+ margin-top: 15px;
+ padding-top: 15px;
+ border-top: 1px solid #eee;
+}
+
+.issue-metadata {
+ margin-top: 15px;
+ font-size: 0.9em;
+ color: #666;
+}
+
+.issue-actions {
+ margin-top: 15px;
+ display: flex;
+ justify-content: flex-start;
+ gap: 10px;
+}
+
+.issue-card a {
+ color: #074785;
+ text-decoration: none;
+}
+
+.issue-card a:hover {
+ text-decoration: underline;
+}
+
+.issue-card input[type="text"], .issue-card textarea {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ margin-bottom: 10px;
+}
+
+.title-input {
+ font-size: 1em;
+ width: calc(100% - 40px);
+ margin-left: 8px;
+}
+
+.checkbox {
+ margin-right: 8px;
+}
+
+.no-results-message {
+ text-align: center;
+ padding: 20px;
+ font-size: 1.2em;
+ color: #666;
+ background-color: #f8f8f8;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ margin-top: 20px;
+}
+
+.pagination-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+.pagination-controls {
+ margin-left: auto;
+}
+
+.perPage {
+ margin-left: 5px;
+ margin-right: 5px;
+}
+
+.panel-title-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+}
+
+.filter-reset-button {
+ margin-left: auto;
+ margin-right: 10px;
+}
diff --git a/modules/issue_tracker/jsx/IssueCard.js b/modules/issue_tracker/jsx/IssueCard.js
new file mode 100644
index 00000000000..8804f5ba482
--- /dev/null
+++ b/modules/issue_tracker/jsx/IssueCard.js
@@ -0,0 +1,439 @@
+import React, {useState} from 'react';
+import PropTypes from 'prop-types';
+import swal from 'sweetalert2';
+import Modal from 'jsx/Modal';
+import '../css/issue_card.css';
+
+const IssueCard = React.memo(function IssueCard({
+ issue,
+ onUpdate,
+ statuses,
+ priorities,
+ categories,
+ sites,
+}) {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editedIssue, setEditedIssue] = useState({...issue});
+ const [tempEditedIssue, setTempEditedIssue] = useState({...issue});
+
+ // State variables for adding comments
+ const [showAddCommentModal, setShowAddCommentModal] = useState(false);
+ const [newComment, setNewComment] = useState('');
+ const [isSubmittingComment, setIsSubmittingComment] = useState(false);
+
+ const handleInputChange = (field, value) => {
+ setTempEditedIssue((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+
+ if (!tempEditedIssue.title.trim()) {
+ showAlertMessage('error', 'Title cannot be empty');
+ return;
+ }
+
+ const formData = new FormData();
+
+ Object.entries(tempEditedIssue).forEach(([key, value]) => {
+ formData.append(key, value === null ? 'null' : value);
+ });
+
+ const hasChanges = Object.entries(tempEditedIssue).some(([key, value]) =>
+ value !== issue[key]
+ );
+
+ if (!hasChanges) {
+ showAlertMessage('info', 'No changes were made');
+ return;
+ }
+
+ fetch(`${loris.BaseURL}/issue_tracker/Edit/`, {
+ method: 'POST',
+ body: formData,
+ }).then((response) => {
+ if (!response.ok) {
+ return response.json().then((data) => {
+ throw new Error(data.error || 'Network response was not ok');
+ });
+ }
+ return response.json();
+ }).then((data) => {
+ showAlertMessage('success', 'Issue updated successfully');
+ setEditedIssue(tempEditedIssue);
+ onUpdate();
+ setIsEditing(false);
+ }).catch((error) => {
+ console.error('Error:', error);
+ showAlertMessage('error', error.message || 'Failed to update issue');
+ setTempEditedIssue({...editedIssue});
+ });
+ };
+
+ const showAlertMessage = (msgType, message) => {
+ let type = 'success';
+ let title = 'Issue updated!';
+ let text = message || '';
+ let timer = null;
+ let confirmation = true;
+
+ if (msgType === 'error') {
+ type = 'error';
+ title = 'Error!';
+ } else if (msgType === 'info') {
+ type = 'info';
+ title = 'Information';
+ }
+
+ swal.fire({
+ title: title,
+ type: type,
+ text: text,
+ timer: timer,
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ showConfirmButton: confirmation,
+ });
+ };
+
+ const handleOpenAddCommentModal = () => {
+ setShowAddCommentModal(true);
+ };
+
+ const handleCloseAddCommentModal = () => {
+ setShowAddCommentModal(false);
+ setNewComment('');
+ };
+
+ const handleAddCommentChange = (e) => {
+ setNewComment(e.target.value);
+ };
+
+ const handleAddCommentSubmit = (e) => {
+ e.preventDefault();
+
+ if (!newComment.trim()) {
+ showAlertMessage('error', 'Comment cannot be empty');
+ return;
+ }
+
+ setIsSubmittingComment(true);
+
+ const formData = new FormData();
+
+ // Prefill all existing issue fields to prevent NULL updates
+ Object.entries(tempEditedIssue).forEach(([key, value]) => {
+ formData.append(key, value === null ? 'null' : value);
+ });
+
+ formData.append('comment', newComment.trim());
+
+ fetch(`${loris.BaseURL}/issue_tracker/Edit/`, {
+ method: 'POST',
+ body: formData,
+ })
+ .then((response) => {
+ setIsSubmittingComment(false);
+ if (!response.ok) {
+ return response.json().then((data) => {
+ throw new Error(data.error || 'Network response was not ok');
+ });
+ }
+ return response.json();
+ })
+ .then((data) => {
+ showAlertMessage('success', 'Comment added successfully');
+ handleCloseAddCommentModal();
+ onUpdate();
+ })
+ .catch((error) => {
+ console.error('Error:', error);
+ showAlertMessage('error', error.message || 'Failed to add comment');
+ });
+ };
+
+ const description = editedIssue.description || '';
+
+ return (
+
+
+
+
+
+
+
+ Created: {issue.dateCreated}
+ Last Updated: {issue.lastUpdate}
+ Assignee: {issue.assignee}
+
+ Site: {issue.centerID
+ ? sites[String(issue.centerID)]
+ : 'No Site'}
+
+
+
+
+
+ );
+});
+
+IssueCard.propTypes = {
+ issue: PropTypes.shape({
+ issueID: PropTypes.number.isRequired,
+ title: PropTypes.string.isRequired,
+ reporter: PropTypes.string.isRequired,
+ assignee: PropTypes.string,
+ status: PropTypes.string.isRequired,
+ priority: PropTypes.string.isRequired,
+ module: PropTypes.number,
+ dateCreated: PropTypes.string.isRequired,
+ lastUpdate: PropTypes.string,
+ lastUpdatedBy: PropTypes.string,
+ sessionID: PropTypes.number,
+ centerID: PropTypes.number,
+ candID: PropTypes.number,
+ category: PropTypes.string,
+ instrument: PropTypes.string,
+ description: PropTypes.string,
+ PSCID: PropTypes.string,
+ visitLabel: PropTypes.string,
+ topComments: PropTypes.arrayOf(
+ PropTypes.shape({
+ issueComment: PropTypes.string.isRequired,
+ dateAdded: PropTypes.string.isRequired,
+ addedBy: PropTypes.string.isRequired,
+ })
+ ).isRequired,
+ }).isRequired,
+ onUpdate: PropTypes.func.isRequired,
+ statuses: PropTypes.object.isRequired,
+ priorities: PropTypes.object.isRequired,
+ categories: PropTypes.object.isRequired,
+ sites: PropTypes.object.isRequired,
+};
+
+export default IssueCard;
diff --git a/modules/issue_tracker/jsx/IssueTrackerBatchMode.js b/modules/issue_tracker/jsx/IssueTrackerBatchMode.js
new file mode 100644
index 00000000000..986b1d2d39e
--- /dev/null
+++ b/modules/issue_tracker/jsx/IssueTrackerBatchMode.js
@@ -0,0 +1,376 @@
+import React, {useState, useEffect} from 'react';
+import PropTypes from 'prop-types';
+import IssueCard from './IssueCard';
+import Loader from 'Loader';
+import PaginationLinks from 'jsx/PaginationLinks';
+import Panel from 'jsx/Panel';
+import {Tabs, TabPane} from 'jsx/Tabs';
+import '../css/issue_tracker_batchmode.css';
+
+/**
+ * IssueTrackerBatchMode component
+ *
+ * @param {object} props - The component props
+ * @param {object} props.options - The options for the IssueTrackerBatchMode
+ */
+function IssueTrackerBatchMode({options}) {
+ const [issues, setIssues] = useState([]);
+ const [selectedCategories, setSelectedCategories] = useState([]);
+ const [selectedPriorities, setSelectedPriorities] = useState([]);
+ const [selectedStatuses, setSelectedStatuses] = useState([]);
+ const [selectedSites, setSelectedSites] = useState([]);
+ const [filteredIssues, setFilteredIssues] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Pagination state
+ const [page, setPage] = useState({
+ number: 1,
+ rows: 10,
+ });
+
+ const priorities = options.priorities || {};
+ const statuses = options.statuses || {};
+ const categories = options.categories || {};
+ const sites = options.sites || {};
+
+ useEffect(() => {
+ fetchIssues();
+ }, []);
+
+ useEffect(() => {
+ filterIssues();
+ }, [
+ selectedCategories,
+ selectedPriorities,
+ selectedStatuses,
+ selectedSites,
+ issues,
+ ]);
+
+ /**
+ * Fetches issues from the server
+ */
+ async function fetchIssues() {
+ try {
+ const response = await fetch(
+ `${loris.BaseURL}/issue_tracker/Edit/?batch=true`,
+ {
+ credentials: 'include', // This ensures cookies are sent with the request
+ }
+ );
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ const data = await response.json();
+ setIssues(data);
+ setIsLoading(false);
+ } catch (error) {
+ console.error('Error fetching issues:', error);
+ setError('Failed to fetch issues. Please try again later.');
+ setIsLoading(false);
+ }
+ }
+
+ /**
+ * Filters issues based on selected categories, priorities, and statuses
+ */
+ function filterIssues() {
+ setFilteredIssues(issues.filter((issue) =>
+ (selectedCategories.length === 0 ||
+ selectedCategories.includes(issue.category)) &&
+ (selectedPriorities.length === 0 ||
+ selectedPriorities.includes(issue.priority)) &&
+ (selectedStatuses.length === 0 ||
+ selectedStatuses.includes(issue.status)) &&
+ (selectedSites.length === 0 ||
+ selectedSites.includes(String(issue.centerID)))
+ ));
+ }
+
+ /**
+ * Toggles a filter item in the given array
+ *
+ * @param {Array} array - The current array of selected items
+ * @param {Function} setArray - The state setter function for the array
+ * @param {*} item - The item to toggle in the array
+ */
+ function toggleFilter(array, setArray, item) {
+ setArray((prev) =>
+ prev.includes(item)
+ ? prev.filter((i) => i !== item)
+ : [...prev, item]
+ );
+ }
+
+ /**
+ * Resets all selected filters
+ */
+ function resetFilters() {
+ setSelectedCategories([]);
+ setSelectedPriorities([]);
+ setSelectedStatuses([]);
+ setSelectedSites([]);
+ }
+
+ /**
+ * Handles updating an issue
+ */
+ function handleIssueUpdate() {
+ fetchIssues();
+ }
+
+ // Pagination functions
+ function changePage(pageNumber) {
+ setPage((prevPage) => ({...prevPage, number: pageNumber}));
+ }
+
+ function updatePageRows(e) {
+ const newRowsPerPage = parseInt(e.target.value, 10);
+ setPage({number: 1, rows: newRowsPerPage});
+ }
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (error) {
+ return {error}
;
+ }
+
+ // Calculate pagination
+ const startIndex = (page.number - 1) * page.rows;
+ const endIndex = startIndex + page.rows;
+ const paginatedIssues = filteredIssues.slice(startIndex, endIndex);
+
+ const tabList = [
+ {
+ id: 'category',
+ label: (
+
+ Category{' '}
+ {selectedCategories.length}
+
+ ),
+ },
+ {
+ id: 'priority',
+ label: (
+
+ Priority{' '}
+ {selectedPriorities.length}
+
+ ),
+ },
+ {
+ id: 'status',
+ label: (
+
+ Status{' '}
+ {selectedStatuses.length}
+
+ ),
+ },
+ {
+ id: 'site', // Added site tab
+ label: (
+
+ Site{' '}
+ {selectedSites.length}
+
+ ),
+ },
+ ];
+
+ const panelTitle = (
+
+ Filters
+
+
+ );
+
+ return (
+
+
+ {}}
+ updateURL={false}
+ >
+
+
+ {Object.entries(categories).map(([value, label]) => (
+
+ ))}
+
+
+
+
+ {Object.entries(priorities).map(([value, label]) => (
+
+ ))}
+
+
+
+
+ {Object.entries(statuses).map(([value, label]) => (
+
+ ))}
+
+
+
+
+ {Object.entries(sites).map(([value, label]) => (
+
+ ))}
+
+
+
+
+
+
+
+ {paginatedIssues.length} issues displayed of {filteredIssues.length}.
+ (Maximum issues per page:
+
+ )
+
+
+
+
+ {paginatedIssues.length > 0 ? (
+ paginatedIssues.map((issue) => (
+
+ ))
+ ) : (
+
+ No issues match the selected filters.
+
+ )}
+
+
+
+ {paginatedIssues.length} issues displayed of {filteredIssues.length}.
+ (Maximum issues per page:
+
+ )
+
+
+
+
+ );
+}
+
+IssueTrackerBatchMode.propTypes = {
+ options: PropTypes.shape({
+ priorities: PropTypes.object,
+ statuses: PropTypes.object,
+ categories: PropTypes.object,
+ sites: PropTypes.object,
+ }).isRequired,
+};
+
+export default IssueTrackerBatchMode;
diff --git a/modules/issue_tracker/jsx/issueTrackerIndex.js b/modules/issue_tracker/jsx/issueTrackerIndex.js
index 80e8dd88ae5..3d9edaadbbf 100644
--- a/modules/issue_tracker/jsx/issueTrackerIndex.js
+++ b/modules/issue_tracker/jsx/issueTrackerIndex.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import Loader from 'Loader';
import FilterableDataTable from 'FilterableDataTable';
+import IssueTrackerBatchMode from './IssueTrackerBatchMode';
/**
* Issue Tracker Index component
@@ -20,10 +21,12 @@ class IssueTrackerIndex extends Component {
data: {},
error: false,
isLoaded: false,
+ view: 'normal', // 'normal' for FilterableDataTable, 'batch' for IssueTrackerBatchMode
};
this.fetchData = this.fetchData.bind(this);
this.formatColumn = this.formatColumn.bind(this);
+ this.toggleView = this.toggleView.bind(this);
}
/**
@@ -51,6 +54,16 @@ class IssueTrackerIndex extends Component {
});
}
+ /**
+ * Toggle between normal and batch mode
+ */
+ toggleView() {
+ this.setState((prevState) => ({
+ view: prevState.view === 'normal' ? 'batch' : 'normal',
+ }));
+ this.fetchData(); // Fetch fresh data when toggling views
+ }
+
/**
* Modify behaviour of specified column cells in the Data Table component
*
@@ -263,14 +276,37 @@ class IssueTrackerIndex extends Component {
];
return (
-
+
+
+
+ {this.state.view === 'normal' ? (
+
+ ) : (
+
+ )}
+
);
}
}
diff --git a/modules/issue_tracker/php/edit.class.inc b/modules/issue_tracker/php/edit.class.inc
index 42deda63c70..bf958e48acb 100644
--- a/modules/issue_tracker/php/edit.class.inc
+++ b/modules/issue_tracker/php/edit.class.inc
@@ -58,6 +58,15 @@ class Edit extends \NDB_Page implements ETagCalculator
$user = $request->getAttribute('user');
$db = $this->loris->getDatabaseConnection();
+ // Check if batch mode is enabled
+ $batch_mode = filter_input(INPUT_GET, 'batch', FILTER_VALIDATE_BOOLEAN);
+
+ if ($batch_mode) {
+ // Fetch all issues
+ $allIssues = $this->_getAllIssues($user);
+ return new \LORIS\Http\Response\JsonResponse($allIssues);
+ }
+
// get field options
$sites = Issue_Tracker::getSites(false, true);
@@ -275,6 +284,85 @@ class Edit extends \NDB_Page implements ETagCalculator
);
}
+ /**
+ * Fetches all issues and includes top 3 comments per issue.
+ *
+ * @param \User $user The current user.
+ *
+ * @return array List of issues with top comments.
+ */
+ private function _getAllIssues(\User $user): array
+ {
+ $db = $this->loris->getDatabaseConnection();
+
+ $query = "SELECT i.*, c.PSCID, s.Visit_label AS visitLabel
+ FROM issues AS i
+ LEFT JOIN candidate c ON (i.candID = c.CandID)
+ LEFT JOIN session s ON (i.sessionID = s.ID)";
+
+ // Add permission check if needed
+ if (!$user->hasPermission('access_all_profiles')) {
+ $query .= " WHERE i.centerID IN (" .
+ implode(',', $user->getCenterIDs()) . ")";
+ }
+
+ $query .= " ORDER BY i.issueID DESC";
+
+ $issues = $db->pselect($query, []);
+
+ // Ensure $issues is an array
+ if (!is_array($issues)) {
+ $issues = iterator_to_array($issues);
+ }
+
+ // Format the issues data
+ foreach ($issues as &$issue) {
+ $issue['reporter'] = $this->formatUserInformation(
+ $issue['reporter']
+ );
+ $issue['lastUpdatedBy'] = $this->formatUserInformation(
+ $issue['lastUpdatedBy']
+ );
+ $issue['topComments'] = $this->getTopComments(
+ (int)$issue['issueID']
+ );
+ }
+
+ return $issues;
+ }
+
+ /**
+ * Fetches the top 3 most recent comments for an issue.
+ *
+ * @param int $issueID The ID of the issue.
+ *
+ * @return array Top 3 comments with comment text, date, and author.
+ */
+ function getTopComments(int $issueID): array
+ {
+ $db = $this->loris->getDatabaseConnection();
+
+ $comments = $db->pselect(
+ "SELECT issueComment, dateAdded, addedBy
+ FROM issues_comments
+ WHERE issueID = :issueID
+ ORDER BY dateAdded DESC
+ LIMIT 3",
+ ['issueID' => $issueID]
+ );
+
+ $topComments = [];
+ foreach ($comments as $comment) {
+ $topComments[] = [
+ 'issueComment' => $comment['issueComment'],
+ 'dateAdded' => $comment['dateAdded'],
+ 'addedBy' => $this->formatUserInformation($comment['addedBy']),
+ ];
+ }
+
+ return $topComments;
+ }
+
/**
* If issueID is passed retrieves issue data from database,
* otherwise return empty issue data object