StringFellow's picture
Update frontend/src/App.jsx
26df28f verified
Raw
History Blame Contribute Delete
33.2 kB
import React, { useState, useEffect, useRef } from 'react';
import {
CContainer, CRow, CCol, CCard, CCardBody, CCardHeader,
CFormInput, CButton, CTable, CNavbar, CNavbarBrand,
CNav, CNavItem, CForm, CBadge, CModal, CModalHeader,
CModalTitle, CModalBody, CModalFooter, CFormSwitch,
COffcanvas, COffcanvasHeader, COffcanvasTitle, COffcanvasBody, CCloseButton,
CTooltip
} from '@coreui/react';
// --- INLINE ICONS ---
const IconEye = () => (<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/><path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/></svg>);
const IconEdit = () => (<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>);
const IconTrash = () => (<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fillRule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>);
const IconDrag = () => (<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16"><path fillRule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/></svg>);
function App() {
const [userRole, setUserRole] = useState(null);
const [theme, setTheme] = useState('light');
// Mobile & Sidebar State
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const [sidebarVisible, setSidebarVisible] = useState(false);
const [desktopSidebarVisible, setDesktopSidebarVisible] = useState(true);
// System Alert Modal State
const [alertModalVisible, setAlertModalVisible] = useState(false);
const [alertModalTitle, setAlertModalTitle] = useState('');
const [alertModalMessage, setAlertModalMessage] = useState('');
// Login State
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
// Admin User Management State
const [users, setUsers] = useState([]);
const [selectedAdminUserId, setSelectedAdminUserId] = useState(null); // Tracks the active user being edited
const [newUsername, setNewUsername] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newReportTitle, setNewReportTitle] = useState('');
const [newDesktopUrl, setNewDesktopUrl] = useState('');
const [newMobileUrl, setNewMobileUrl] = useState('');
// Derived active user for the admin dashboard panel
const activeAdminUser = users.find(u => u.id === selectedAdminUserId) || null;
// Admin Manage Reports State
const [reportModalVisible, setReportModalVisible] = useState(false);
const [reportModalMode, setReportModalMode] = useState('add');
const [activeUserForModal, setActiveUserForModal] = useState(null);
const [activeReportId, setActiveReportId] = useState(null);
const [modalReportTitle, setModalReportTitle] = useState('');
const [modalDesktopUrl, setModalDesktopUrl] = useState('');
const [modalMobileUrl, setModalMobileUrl] = useState('');
// Admin Reset Password State
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
const [newPasswordInput, setNewPasswordInput] = useState('');
// Viewer State
const [pages, setPages] = useState([]);
const [selectedPage, setSelectedPage] = useState(null);
// Force Refresh State & Ref
const [refreshKey, setRefreshKey] = useState(0);
const [lastRefresh, setLastRefresh] = useState('');
const lastAutoRefreshHour = useRef(null);
// Drag and Drop Refs
const dragItem = useRef();
const dragOverItem = useRef();
const dragUser = useRef();
useEffect(() => {
document.documentElement.setAttribute('data-coreui-theme', theme);
}, [theme]);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Ensures there is always a selected user in the admin panel if users exist
useEffect(() => {
if (userRole === 'admin' && users.length > 0) {
if (!selectedAdminUserId || !users.find(u => u.id === selectedAdminUserId)) {
setSelectedAdminUserId(users[0].id);
}
}
}, [users, userRole, selectedAdminUserId]);
useEffect(() => {
if (selectedPage && userRole === 'user') {
setLastRefresh(new Date().toLocaleTimeString());
setRefreshKey(prev => prev + 1);
}
}, [selectedPage, userRole]);
useEffect(() => {
const checkTimeInterval = setInterval(() => {
if (!selectedPage || userRole !== 'user') return;
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
if (currentHour >= 7 && currentHour <= 15 && currentMinute === 5) {
if (lastAutoRefreshHour.current !== currentHour) {
setRefreshKey(prev => prev + 1);
setLastRefresh(`Auto-update: ${now.toLocaleTimeString()}`);
lastAutoRefreshHour.current = currentHour;
}
}
}, 30000);
return () => clearInterval(checkTimeInterval);
}, [selectedPage, userRole]);
const showSystemAlert = (title, message) => {
setAlertModalTitle(title);
setAlertModalMessage(message);
setAlertModalVisible(true);
};
useEffect(() => {
let sessionTimer;
if (userRole) {
const EIGHT_HOURS = 8 * 60 * 60 * 1000;
sessionTimer = setTimeout(() => {
handleLogout();
showSystemAlert("Session Expired", "Your 8-hour session has automatically ended. Please log in again to continue viewing reports.");
}, EIGHT_HOURS);
}
return () => clearTimeout(sessionTimer);
}, [userRole]);
const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
const handleManualRefresh = () => {
setRefreshKey(prev => prev + 1);
setLastRefresh(new Date().toLocaleTimeString());
};
const handleLogin = async (e) => {
e.preventDefault();
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (res.ok) {
const data = await res.json();
setUserRole(data.role);
if (data.role === 'admin') fetchAdminUsers();
if (data.role === 'user') loadUserPages();
} else {
showSystemAlert("Login Failed", "Invalid username or password. Please check your credentials and try again.");
}
};
const handleLogout = async () => {
await fetch('/api/logout', { method: 'POST' });
setUserRole(null);
setPages([]);
setSelectedPage(null);
setSelectedAdminUserId(null);
};
const fetchAdminUsers = async () => {
const res = await fetch('/api/admin/users');
if (res.status === 401 || res.status === 403) return handleLogout();
if (res.ok) {
const data = await res.json();
setUsers(data);
}
};
const handleAddUser = async (e) => {
e.preventDefault();
const initialPages = [];
if (newReportTitle && newDesktopUrl) {
initialPages.push({
id: 'rep_' + Date.now(),
title: newReportTitle,
desktopUrl: newDesktopUrl,
mobileUrl: newMobileUrl
});
}
const newUser = {
username: newUsername,
password: newPassword,
pages: initialPages
};
const res = await fetch('/api/admin/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser)
});
if (res.status === 401 || res.status === 403) return handleLogout();
if (res.ok) {
const createdUser = await res.json();
fetchAdminUsers();
setSelectedAdminUserId(createdUser.id); // Auto-select the newly created user
setNewUsername('');
setNewPassword('');
setNewReportTitle('');
setNewDesktopUrl('');
setNewMobileUrl('');
}
};
const handleDeleteUser = async (id) => {
const res = await fetch(`/api/admin/users/${id}`, { method: 'DELETE' });
if (res.status === 401 || res.status === 403) return handleLogout();
fetchAdminUsers(); // This will auto-select the next available user via the useEffect
};
const openReportModal = (mode, user, report = null) => {
setReportModalMode(mode);
setActiveUserForModal(user);
if (mode === 'edit' && report) {
setActiveReportId(report.id);
setModalReportTitle(report.title);
setModalDesktopUrl(report.desktopUrl || report.url || '');
setModalMobileUrl(report.mobileUrl || '');
} else {
setActiveReportId(null);
setModalReportTitle('');
setModalDesktopUrl('');
setModalMobileUrl('');
}
setReportModalVisible(true);
};
const handleSaveReport = async () => {
try {
const currentUser = users.find(u => u.id === activeUserForModal.id);
if (!currentUser) return showSystemAlert("Error", "User record not found in active state.");
let updatedPages = [...currentUser.pages];
if (reportModalMode === 'add') {
updatedPages.push({
id: 'rep_' + Date.now(),
title: modalReportTitle,
desktopUrl: modalDesktopUrl,
mobileUrl: modalMobileUrl
});
} else if (reportModalMode === 'edit') {
updatedPages = updatedPages.map(p =>
p.id === activeReportId ? { ...p, title: modalReportTitle, desktopUrl: modalDesktopUrl, mobileUrl: modalMobileUrl } : p
);
}
const res = await fetch(`/api/admin/users/${currentUser.id}/pages`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pages: updatedPages })
});
if (res.status === 401 || res.status === 403) return handleLogout();
if (res.ok) {
setReportModalVisible(false);
fetchAdminUsers();
} else {
showSystemAlert("Save Failed", "The server failed to save the report updates.");
}
} catch (error) {
showSystemAlert("Error", "An unexpected error occurred during save.");
}
};
const handleDeleteReport = async (user, reportId) => {
const updatedPages = user.pages.filter(p => p.id !== reportId);
const res = await fetch(`/api/admin/users/${user.id}/pages`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pages: updatedPages })
});
if (res.status === 401 || res.status === 403) return handleLogout();
fetchAdminUsers();
};
const openPasswordModal = (user) => {
setActiveUserForModal(user);
setNewPasswordInput('');
setPasswordModalVisible(true);
};
const handleSavePassword = async () => {
const res = await fetch(`/api/admin/users/${activeUserForModal.id}/password`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: newPasswordInput })
});
if (res.status === 401 || res.status === 403) return handleLogout();
if (res.ok) {
setPasswordModalVisible(false);
fetchAdminUsers();
}
};
const loadUserPages = async () => {
const res = await fetch('/api/user/pages');
if (res.status === 401 || res.status === 403) return handleLogout();
if (res.ok) {
const data = await res.json();
setPages(data);
if(data.length > 0) setSelectedPage(data[0]);
}
};
// --- DRAG AND DROP HANDLERS ---
const handleDragStart = (e, userId, index) => {
dragItem.current = index;
dragUser.current = userId;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => e.target.style.opacity = '0.5', 0);
};
const handleDragEnter = (e, userId, index) => {
if (dragUser.current !== userId) return;
dragOverItem.current = index;
};
const handleDragEnd = async (e, userId) => {
e.target.style.opacity = '1';
if (dragUser.current !== userId || dragItem.current === null || dragOverItem.current === null) return;
if (dragItem.current === dragOverItem.current) return;
const currentUser = users.find(u => u.id === userId);
if (!currentUser) return;
const newPages = [...currentUser.pages];
const draggedItemContent = newPages[dragItem.current];
newPages.splice(dragItem.current, 1);
newPages.splice(dragOverItem.current, 0, draggedItemContent);
// Optimistic UI Update ensures no visual lag
setUsers(users.map(u => u.id === userId ? { ...u, pages: newPages } : u));
const res = await fetch(`/api/admin/users/${userId}/pages`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pages: newPages })
});
if (res.status === 401 || res.status === 403) return handleLogout();
dragItem.current = null;
dragOverItem.current = null;
dragUser.current = null;
};
// --- DYNAMIC OVERLAY STYLES ---
const overlayStyleLeft = {
position: 'absolute', bottom: 0, left: 0,
width: '140px',
height: isMobile ? '30px' : '34px',
backgroundColor: '#eaeaea', zIndex: 10
};
const overlayStyleRight = {
position: 'absolute', bottom: 0, right: 0,
width: isMobile ? '30px' : '190px',
height: isMobile ? '30px' : '34px',
backgroundColor: '#eaeaea', zIndex: 10
};
const getActiveUrl = (page) => {
if (!page) return '';
if (isMobile && page.mobileUrl) return page.mobileUrl;
return page.desktopUrl || page.url;
};
const renderAlertModal = () => (
<CModal alignment="center" visible={alertModalVisible} onClose={() => setAlertModalVisible(false)}>
<CModalHeader onClose={() => setAlertModalVisible(false)}>
<CModalTitle>{alertModalTitle}</CModalTitle>
</CModalHeader>
<CModalBody>
{alertModalMessage}
</CModalBody>
<CModalFooter>
<CButton color="primary" onClick={() => setAlertModalVisible(false)}>OK</CButton>
</CModalFooter>
</CModal>
);
if (!userRole) {
return (
<CContainer className="d-flex align-items-center min-vh-100">
{renderAlertModal()}
<CRow className="justify-content-center w-100">
<CCol md={6} lg={4}>
<CCard className="p-4 shadow-sm">
<CCardBody>
<h3 className="text-center mb-4 d-flex justify-content-center align-items-center">
<img
src="https://upload.wikimedia.org/wikipedia/commons/c/cf/New_Power_BI_Logo.svg"
alt="Power BI"
style={{ height: '32px', marginRight: '10px' }}
/>
LOGIN
</h3>
<CForm onSubmit={handleLogin}>
<CFormInput className="mb-3" type="text" placeholder="Username" value={username} onChange={e => setUsername(e.target.value)} required />
<CFormInput className="mb-4" type="password" placeholder="Password" value={password} onChange={e => setPassword(e.target.value)} required />
<CButton color="primary" type="submit" className="w-100 mb-3">Log In</CButton>
</CForm>
<div className="d-flex justify-content-center mt-3">
<CFormSwitch
size="lg"
id="themeSwitchLogin"
label="Dark Mode"
checked={theme === 'dark'}
onChange={toggleTheme}
/>
</div>
</CCardBody>
</CCard>
</CCol>
</CRow>
</CContainer>
);
}
return (
<div className="vh-100 d-flex flex-column bg-body overflow-hidden">
{renderAlertModal()}
<CNavbar colorScheme={theme} className="bg-body-tertiary px-3 shadow-sm flex-shrink-0 z-1 border-bottom">
<CNavbarBrand className="d-flex align-items-center">
{userRole === 'user' && (
<CButton
variant="ghost"
color="secondary"
className="me-2 p-1"
onClick={() => isMobile ? setSidebarVisible(true) : setDesktopSidebarVisible(!desktopSidebarVisible)}
>
<span className="fs-5"></span>
</CButton>
)}
{userRole === 'admin' ? 'Administration Dashboard' : 'BI Viewer'}
</CNavbarBrand>
<CNav className="ms-auto align-items-center flex-row">
<CNavItem className="me-3 mt-1">
<CFormSwitch id="themeSwitchNav" label="Dark" checked={theme === 'dark'} onChange={toggleTheme} />
</CNavItem>
<CNavItem>
<CButton color="danger" size="sm" onClick={handleLogout}>Logout</CButton>
</CNavItem>
</CNav>
</CNavbar>
{/* --- ADMIN VIEW (REFACTORED TO MASTER-DETAIL LAYOUT) --- */}
{userRole === 'admin' && (
<CContainer fluid className="flex-grow-1 overflow-hidden py-4 d-flex">
<CRow className="w-100 m-0">
{/* LEFT COLUMN: Create User & User List */}
<CCol md={4} className="h-100 d-flex flex-column pe-md-3 p-0">
<CCard className="mb-4 flex-shrink-0">
<CCardHeader><strong>Create New User</strong></CCardHeader>
<CCardBody>
<CForm onSubmit={handleAddUser}>
<CFormInput className="mb-2" placeholder="New Username" value={newUsername} onChange={e => setNewUsername(e.target.value)} required />
<CFormInput className="mb-2" type="text" placeholder="Password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required />
<hr/>
<div className="mb-2 text-muted small">Optional: Assign initial report</div>
<CFormInput className="mb-2" placeholder="Report Title (e.g. Sales Q1)" value={newReportTitle} onChange={e => setNewReportTitle(e.target.value)} />
<CFormInput className="mb-2" placeholder="Desktop PowerBI URL" value={newDesktopUrl} onChange={e => setNewDesktopUrl(e.target.value)} />
<CFormInput className="mb-3" placeholder="Mobile PowerBI URL (Optional)" value={newMobileUrl} onChange={e => setNewMobileUrl(e.target.value)} />
<CButton color="success" type="submit" className="w-100">Create User</CButton>
</CForm>
</CCardBody>
</CCard>
<CCard className="flex-grow-1 overflow-hidden d-flex flex-column mb-3">
<CCardHeader><strong>Managed Users</strong></CCardHeader>
<CCardBody className="p-2 overflow-auto h-100">
{users.length === 0 && <p className="text-muted small p-2 text-center">No users created yet.</p>}
{users.map(u => (
<CButton
key={u.id}
color={selectedAdminUserId === u.id ? 'primary' : 'light'}
className="w-100 mb-2 text-start d-flex justify-content-between align-items-center"
onClick={() => setSelectedAdminUserId(u.id)}
>
<span className="text-truncate me-2">{u.username}</span>
<CBadge color={selectedAdminUserId === u.id ? 'light' : 'secondary'} textColor={selectedAdminUserId === u.id ? 'primary' : 'light'}>
{u.pages.length} {u.pages.length === 1 ? 'Report' : 'Reports'}
</CBadge>
</CButton>
))}
</CCardBody>
</CCard>
</CCol>
{/* RIGHT COLUMN: Active User Details & Previews */}
<CCol md={8} className="h-100 overflow-auto p-0 pb-4">
{activeAdminUser ? (
<CCard className="mb-4">
<CCardHeader className="d-flex justify-content-between align-items-center">
<h5 className="m-0">User: <strong>{activeAdminUser.username}</strong></h5>
<div>
<CButton size="sm" color="warning" className="me-2" onClick={() => openPasswordModal(activeAdminUser)}>Reset Pw</CButton>
<CButton size="sm" color="danger" onClick={() => handleDeleteUser(activeAdminUser.id)}>Delete User</CButton>
</div>
</CCardHeader>
<CCardBody>
<div className="mb-4 text-muted small">Current Password: <code>{activeAdminUser.password}</code></div>
<h6 className="mb-3">Assigned Reports</h6>
{activeAdminUser.pages.length === 0 && <p className="text-muted small">This user currently has no reports assigned.</p>}
{activeAdminUser.pages.map((p, index) => (
<div
key={p.id}
className="mb-2 border rounded bg-body-tertiary d-flex"
draggable
onDragStart={(e) => handleDragStart(e, activeAdminUser.id, index)}
onDragEnter={(e) => handleDragEnter(e, activeAdminUser.id, index)}
onDragEnd={(e) => handleDragEnd(e, activeAdminUser.id)}
onDragOver={(e) => e.preventDefault()}
style={{ cursor: 'grab', transition: 'opacity 0.2s' }}
>
<div className="d-flex align-items-center justify-content-center px-2 text-muted border-end" title="Drag to reorder">
<IconDrag />
</div>
<div className="p-2 flex-grow-1 overflow-hidden">
<div className="d-flex justify-content-between align-items-center mb-2">
<CBadge color="info">{p.title}</CBadge>
<div>
<CTooltip content="Edit Report Links">
<CButton size="sm" color="warning" variant="ghost" className="p-1 me-1" onClick={() => openReportModal('edit', activeAdminUser, p)}>
<IconEdit />
</CButton>
</CTooltip>
<CTooltip content="Delete Entire Report">
<CButton size="sm" color="danger" variant="ghost" className="p-1" onClick={() => handleDeleteReport(activeAdminUser, p.id)}>
<IconTrash />
</CButton>
</CTooltip>
</div>
</div>
<div className="small text-muted mb-1 d-flex align-items-center">
<strong className="me-2" style={{width: '60px'}}>Desktop:</strong>
<span className="text-truncate flex-grow-1">{p.desktopUrl || p.url || 'None'}</span>
{(p.desktopUrl || p.url) && (
<CTooltip content="Preview Desktop View">
<CButton size="sm" color="primary" variant="ghost" className="p-0 ms-2 flex-shrink-0" onClick={() => setSelectedPage({ ...p, previewMode: 'desktop' })}>
<IconEye />
</CButton>
</CTooltip>
)}
</div>
<div className="small text-muted d-flex align-items-center">
<strong className="me-2" style={{width: '60px'}}>Mobile:</strong>
<span className="text-truncate flex-grow-1">{p.mobileUrl || 'None'}</span>
{p.mobileUrl && (
<CTooltip content="Preview Mobile View">
<CButton size="sm" color="primary" variant="ghost" className="p-0 ms-2 flex-shrink-0" onClick={() => setSelectedPage({ ...p, previewMode: 'mobile' })}>
<IconEye />
</CButton>
</CTooltip>
)}
</div>
</div>
</div>
))}
<CButton size="sm" color="success" variant="outline" className="mt-2 w-100" onClick={() => openReportModal('add', activeAdminUser)}>
+ Add New Report
</CButton>
</CCardBody>
</CCard>
) : (
<CCard className="mb-4">
<CCardBody className="text-center text-muted p-5">
Select a user from the left panel to manage their reports and settings.
</CCardBody>
</CCard>
)}
{selectedPage && (
<CCard className="mt-4">
<CCardHeader className="d-flex justify-content-between align-items-center">
<strong>Preview: {selectedPage.title} ({selectedPage.previewMode === 'mobile' ? 'Mobile View' : 'Desktop View'})</strong>
<CButton color="secondary" size="sm" onClick={() => setSelectedPage(null)}>Close</CButton>
</CCardHeader>
<CCardBody className="p-0 d-flex justify-content-center bg-dark">
<div
className="position-relative bg-body"
style={{
width: selectedPage.previewMode === 'mobile' ? '375px' : '100%',
height: '600px',
boxShadow: selectedPage.previewMode === 'mobile' ? '0 0 20px rgba(0,0,0,0.5)' : 'none',
transition: 'width 0.3s ease-in-out'
}}
>
<iframe
src={selectedPage.previewMode === 'mobile' ? selectedPage.mobileUrl : (selectedPage.desktopUrl || selectedPage.url)}
style={{ width: '100%', height: '100%', border: 'none' }}
title="Preview"
allowFullScreen>
</iframe>
<div style={{ ...overlayStyleLeft, height: selectedPage.previewMode === 'mobile' ? '30px' : '34px' }}></div>
<div style={{
...overlayStyleRight,
width: selectedPage.previewMode === 'mobile' ? '30px' : '190px',
height: selectedPage.previewMode === 'mobile' ? '30px' : '34px'
}}></div>
</div>
</CCardBody>
</CCard>
)}
</CCol>
</CRow>
<CModal visible={reportModalVisible} onClose={() => setReportModalVisible(false)}>
<CModalHeader>
<CModalTitle>{reportModalMode === 'add' ? `Add Report for ${activeAdminUser?.username}` : `Edit Report`}</CModalTitle>
</CModalHeader>
<CModalBody>
<CFormInput className="mb-3" label="Report Title" value={modalReportTitle} onChange={e => setModalReportTitle(e.target.value)} />
<CFormInput className="mb-3" label="Desktop PowerBI URL" value={modalDesktopUrl} onChange={e => setModalDesktopUrl(e.target.value)} />
<CFormInput label="Mobile PowerBI URL (Optional)" value={modalMobileUrl} onChange={e => setModalMobileUrl(e.target.value)} />
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={() => setReportModalVisible(false)}>Cancel</CButton>
<CButton color="primary" onClick={handleSaveReport}>Save</CButton>
</CModalFooter>
</CModal>
<CModal visible={passwordModalVisible} onClose={() => setPasswordModalVisible(false)}>
<CModalHeader>
<CModalTitle>Reset Password for {activeUserForModal?.username}</CModalTitle>
</CModalHeader>
<CModalBody>
<CFormInput type="text" label="New Password" value={newPasswordInput} onChange={e => setNewPasswordInput(e.target.value)} required />
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={() => setPasswordModalVisible(false)}>Cancel</CButton>
<CButton color="primary" onClick={handleSavePassword}>Save Password</CButton>
</CModalFooter>
</CModal>
</CContainer>
)}
{/* --- USER VIEW --- */}
{userRole === 'user' && (
<div className="d-flex flex-grow-1 overflow-hidden">
{desktopSidebarVisible && (
<div className="d-none d-md-flex flex-column border-end bg-body-tertiary" style={{ width: '250px' }}>
<div className="p-3 overflow-auto flex-grow-1">
{pages.length === 0 && <p className="text-muted small">No reports assigned.</p>}
{pages.map(page => (
<CButton
key={page.id}
color={selectedPage?.id === page.id ? 'primary' : 'light'}
className="w-100 mb-2 text-start text-truncate"
onClick={() => setSelectedPage(page)}
>
{page.title}
</CButton>
))}
</div>
</div>
)}
<COffcanvas placement="start" visible={sidebarVisible} onHide={() => setSidebarVisible(false)}>
<COffcanvasHeader>
<COffcanvasTitle>Select Report</COffcanvasTitle>
<CCloseButton className="text-reset" onClick={() => setSidebarVisible(false)} />
</COffcanvasHeader>
<COffcanvasBody>
{pages.map(page => (
<CButton
key={page.id}
color={selectedPage?.id === page.id ? 'primary' : 'light'}
className="w-100 mb-2 text-start"
onClick={() => { setSelectedPage(page); setSidebarVisible(false); }}
>
{page.title}
</CButton>
))}
</COffcanvasBody>
</COffcanvas>
<div className="flex-grow-1 d-flex flex-column position-relative">
{selectedPage ? (
<>
<div className="bg-body px-3 py-2 d-flex justify-content-between align-items-center border-bottom shadow-sm z-1">
<h5 className="m-0 text-truncate">{selectedPage.title}</h5>
<div className="d-flex align-items-center text-muted small">
<span className="d-none d-sm-inline me-2">Last reload: {lastRefresh}</span>
<CButton size="sm" color="info" variant="outline" onClick={handleManualRefresh}>
↻ Refresh
</CButton>
</div>
</div>
<div className="position-relative w-100 flex-grow-1 bg-body">
<iframe
key={refreshKey}
src={getActiveUrl(selectedPage)}
style={{ width: '100%', height: '100%', border: 'none', display: 'block' }}
title="Report Viewer"
allowFullScreen
></iframe>
<div style={overlayStyleLeft}></div>
<div style={overlayStyleRight}></div>
</div>
</>
) : (
<div className="p-4 text-center text-muted flex-grow-1 d-flex justify-content-center align-items-center">
Select a report from the menu to begin viewing.
</div>
)}
</div>
</div>
)}
</div>
);
}
export default App;