Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/components/BMDashboard/UtilizationChart/ExportReportButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useState } from 'react';
import PropTypes from 'prop-types';
import axios from 'axios';
import { toast } from 'react-toastify';
import { ENDPOINTS } from '../../../utils/URL';
import { EXPORT_FORMATS } from './constants';
import styles from './UtilizationChart.module.css';

function ExportReportButton({ tool, project, startDate, endDate }) {
const [exportingFormat, setExportingFormat] = useState(null);

const handleExport = async format => {
setExportingFormat(format);
try {
const params = {
format,
tool,
project,
...(startDate && { startDate: startDate.toISOString() }),
...(endDate && { endDate: endDate.toISOString() }),
};
const response = await axios.get(ENDPOINTS.BM_TOOL_UTILIZATION_EXPORT, {
params,
headers: { Authorization: localStorage.getItem('token') },
responseType: 'blob',
});

const contentDisposition = response.headers['content-disposition'] || '';
const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
const filename = filenameMatch ? filenameMatch[1] : `tool-utilization-report.${format}`;

const url = globalThis.URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
globalThis.URL.revokeObjectURL(url);
} catch {
toast.error(`Failed to export ${format.toUpperCase()} report.`);
} finally {
setExportingFormat(null);
}
};

return (
<div className={styles.exportSection}>
<span className={styles.exportLabel}>Export Report:</span>
<button
type="button"
className={styles.exportButton}
onClick={() => handleExport(EXPORT_FORMATS.PDF)}
disabled={exportingFormat !== null}
aria-label="Export as PDF"
>
{exportingFormat === EXPORT_FORMATS.PDF ? 'Exporting...' : 'PDF'}
</button>
<button
type="button"
className={styles.exportButton}
onClick={() => handleExport(EXPORT_FORMATS.CSV)}
disabled={exportingFormat !== null}
aria-label="Export as CSV"
>
{exportingFormat === EXPORT_FORMATS.CSV ? 'Exporting...' : 'CSV'}
</button>
</div>
);
}

ExportReportButton.propTypes = {
tool: PropTypes.string,
project: PropTypes.string,
startDate: PropTypes.shape({ toISOString: PropTypes.func }),
endDate: PropTypes.shape({ toISOString: PropTypes.func }),
};

ExportReportButton.defaultProps = {
tool: 'ALL',
project: 'ALL',
startDate: null,
endDate: null,
};

export default ExportReportButton;
57 changes: 57 additions & 0 deletions src/components/BMDashboard/UtilizationChart/ForecastModeToggle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useRef } from 'react';
import { FORECAST_MODE_LABELS } from './constants';
import styles from './UtilizationChart.module.css';
import PropTypes from 'prop-types';

const MODES = Object.entries(FORECAST_MODE_LABELS);

function ForecastModeToggle({ value, onChange }) {
const buttonRefs = useRef([]);

const handleKeyDown = (e, index) => {
const count = MODES.length;
let nextIndex = -1;

if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
nextIndex = (index + 1) % count;
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
nextIndex = (index - 1 + count) % count;
}

if (nextIndex !== -1) {
onChange(MODES[nextIndex][0]);
buttonRefs.current[nextIndex]?.focus();
}
};

return (
<div className={styles.forecastToggle} role="radiogroup" aria-label="Forecast mode">
{MODES.map(([mode, label], index) => (
<button
key={mode}
ref={el => {
buttonRefs.current[index] = el;
}}
type="button"
role="radio"
aria-checked={value === mode}
tabIndex={value === mode ? 0 : -1}
className={`${styles.toggleButton} ${value === mode ? styles.toggleButtonActive : ''}`}
onClick={() => onChange(mode)}
onKeyDown={e => handleKeyDown(e, index)}
>
{label}
</button>
))}
</div>
);
}

ForecastModeToggle.propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};

export default ForecastModeToggle;
47 changes: 47 additions & 0 deletions src/components/BMDashboard/UtilizationChart/InsightsSummaryBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import styles from './UtilizationChart.module.css';
import PropTypes from 'prop-types';

function InsightsSummaryBar({ summary }) {
if (!summary) return null;

return (
<section className={styles.summaryBar} aria-label="Utilization summary">
<div className={styles.summaryCard}>
<span className={styles.summaryValue}>{summary.totalToolTypes}</span>
<span className={styles.summaryLabel}>Tool Types</span>
</div>
<div className={styles.summaryCard}>
<span className={styles.summaryValue}>{summary.averageUtilization}%</span>
<span className={styles.summaryLabel}>Avg Utilization</span>
</div>
<div className={`${styles.summaryCard} ${styles.summaryCardGreen}`}>
<span className={styles.summaryValue}>{summary.normal}</span>
<span className={styles.summaryLabel}>Normal</span>
</div>
<div className={`${styles.summaryCard} ${styles.summaryCardYellow}`}>
<span className={styles.summaryValue}>{summary.underUtilized}</span>
<span className={styles.summaryLabel}>Under-utilized</span>
</div>
<div className={`${styles.summaryCard} ${styles.summaryCardRed}`}>
<span className={styles.summaryValue}>{summary.overUtilized}</span>
<span className={styles.summaryLabel}>Over-utilized</span>
</div>
</section>
);
}

InsightsSummaryBar.propTypes = {
summary: PropTypes.shape({
totalToolTypes: PropTypes.number,
averageUtilization: PropTypes.number,
normal: PropTypes.number,
underUtilized: PropTypes.number,
overUtilized: PropTypes.number,
}),
};

InsightsSummaryBar.defaultProps = {
summary: null,
};

export default InsightsSummaryBar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { URGENCY_STYLES } from './constants';
import styles from './UtilizationChart.module.css';
import PropTypes from 'prop-types';

function MaintenanceAlertPanel({ alerts }) {
const sortedAlerts = [...alerts].sort((a, b) =>
a.urgency === 'high' && b.urgency !== 'high' ? -1 : 1,
);

return (
<section className={styles.insightsPanel} aria-labelledby="maintenance-heading">
<h3 id="maintenance-heading" className={styles.panelTitle}>
Maintenance Alerts
</h3>
{sortedAlerts.length === 0 ? (
<p className={styles.emptyPanel}>No maintenance alerts.</p>
) : (
<ul className={styles.alertList}>
{sortedAlerts.map((alert, index) => (
<li
key={`${alert.toolName}-${alert.alertType}-${index}`}
className={styles.alertItem}
data-urgency={alert.urgency}
>
<div className={styles.alertHeader}>
<span
className={styles.urgencyBadge}
style={{ backgroundColor: URGENCY_STYLES[alert.urgency]?.color }}
>
{URGENCY_STYLES[alert.urgency]?.label}
</span>
<strong>{alert.toolName}</strong>
</div>
<p className={styles.alertMessage}>{alert.message}</p>
</li>
))}
</ul>
)}
</section>
);
}

MaintenanceAlertPanel.propTypes = {
alerts: PropTypes.arrayOf(
PropTypes.shape({
toolName: PropTypes.string,
alertType: PropTypes.string,
urgency: PropTypes.string,
message: PropTypes.string,
}),
).isRequired,
};

export default MaintenanceAlertPanel;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { TRAFFIC_LIGHT_COLORS } from './constants';
import styles from './UtilizationChart.module.css';
import PropTypes from 'prop-types';

function RecommendationPanel({ recommendations }) {
return (
<section className={styles.insightsPanel} aria-labelledby="recommendations-heading">
<h3 id="recommendations-heading" className={styles.panelTitle}>
Utilization Recommendations
</h3>
{recommendations.length === 0 ? (
<p className={styles.emptyPanel}>No recommendations available.</p>
) : (
<ul className={styles.recommendationList}>
{recommendations.map(rec => (
<li key={rec.toolName} className={styles.recommendationItem}>
<span
className={styles.trafficDot}
style={{ backgroundColor: TRAFFIC_LIGHT_COLORS[rec.trafficLight] }}
aria-hidden="true"
/>
<div>
<strong className={styles.toolName}>{rec.toolName}</strong>
<span className={styles.classificationBadge} data-level={rec.trafficLight}>
{rec.label}
</span>
<p className={styles.actionText}>{rec.action}</p>
<span className={styles.rateText}>{rec.utilizationRate}% utilization</span>
</div>
</li>
))}
</ul>
)}
</section>
);
}

RecommendationPanel.propTypes = {
recommendations: PropTypes.arrayOf(
PropTypes.shape({
toolName: PropTypes.string,
trafficLight: PropTypes.string,
label: PropTypes.string,
action: PropTypes.string,
utilizationRate: PropTypes.number,
}),
).isRequired,
};

export default RecommendationPanel;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import styles from './UtilizationChart.module.css';
import PropTypes from 'prop-types';

function ResourceBalancingPanel({ suggestions }) {
return (
<section className={styles.insightsPanel} aria-labelledby="balancing-heading">
<h3 id="balancing-heading" className={styles.panelTitle}>
Resource Balancing
</h3>
{suggestions.length === 0 ? (
<p className={styles.emptyPanel}>Resources are balanced. No action needed.</p>
) : (
<ul className={styles.balancingList}>
{suggestions.map(item => (
<li key={item.suggestion} className={styles.balancingItem}>
<p className={styles.suggestionText}>{item.suggestion}</p>
<div className={styles.balancingDetail}>
<span className={styles.fromTool}>
From: <strong>{item.fromTool}</strong>
</span>
{item.toTool && (
<span className={styles.toTool}>
&rarr; To: <strong>{item.toTool}</strong>
</span>
)}
</div>
<p className={styles.rationaleText}>{item.rationale}</p>
</li>
))}
</ul>
)}
</section>
);
}

ResourceBalancingPanel.propTypes = {
suggestions: PropTypes.arrayOf(
PropTypes.shape({
suggestion: PropTypes.string,
fromTool: PropTypes.string,
toTool: PropTypes.string,
rationale: PropTypes.string,
}),
).isRequired,
};

export default ResourceBalancingPanel;
Loading
Loading