Enhancements

This commit is contained in:
2026-06-25 11:19:08 +02:00
parent 8804d21c32
commit 9343657d20
62 changed files with 10210 additions and 976 deletions
@@ -0,0 +1,835 @@
import React, { useState, useEffect } from 'react';
import {
Server,
Database,
Users,
Activity,
RefreshCw,
Plus,
Edit3,
Trash2,
Eye,
AlertCircle,
CheckCircle,
XCircle
} from 'lucide-react';
import { useEffect } from 'react';
function App() {
useEffect(() => {
const ws = new WebSocket('ws://localhost:8000/ws/logs');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'log') {
console.log(`[Backend Log]: ${data.message}`);
}
};
ws.onclose = () => {
console.log('WebSocket connection closed');
};
return () => {
ws.close();
};
}, []);
return <div>Your React App</div>;
}
const Dashboard = () => {
const [apiUrl, setApiUrl] = useState('http://127.0.0.1:8000');
const [connectionStatus, setConnectionStatus] = useState('disconnected');
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState('dashboard');
const [members, setMembers] = useState([]);
const [totalMembers, setTotalMembers] = useState(0);
const [activeMembersCount, setActiveMembersCount] = useState(0);
const [inactiveMembersCount, setInactiveMembersCount] = useState(0);
const [deactivatedMembersCount, setDeactivatedMembersCount] = useState(0);
const [loadingData, setLoadingData] = useState(false);
const testConnection = async () => {
setIsLoading(true);
try {
console.log('Testing connection to:', apiUrl);
// Test with a simple endpoint first - try /member/1 since /member/100 works
const response = await fetch(`${apiUrl}/member/100`);
if (response.ok) {
setConnectionStatus('connected');
console.log('Connection successful!');
// Load members data when connection is successful
await loadMembersCount();
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
console.error('Connection error:', error);
setConnectionStatus('error');
}
setIsLoading(false);
};
const loadMembersCount = async () => {
if (connectionStatus !== 'connected' && !isLoading) return;
setLoadingData(true);
console.log('Attempting to load active members count...');
// const fetchActiveMembersCount = async () => {
try {
const response = await fetch(`${apiUrl}/activememberscount`);
if (response.ok) {
const count = await response.json(); // Single number return
setTotalMembers(count);
console.log('Total active members count:', count);
} else {
console.log('Failed to fetch active members count:', response.statusText);
setTotalMembers(0);
}
} catch (error) {
console.log('Error fetching active members count:', error);
setMembers([]);
setTotalMembers(0);
}
//}
setLoadingData(false);
};
// Fetch member counts
const fetchMemberCounts = async () => {
if (connectionStatus !== 'connected' && !isLoading) return;
try {
// Fetch active members count
const activeResponse = await fetch(`${apiUrl}/activememberscount`, {
headers: { 'Content-Type': 'application/json' }
});
if (activeResponse.ok) {
const activeCount = await activeResponse.json();
setActiveMembersCount(activeCount);
console.log('activememberscount: ', activeCount);
}
// Fetch inactive members count
const inactiveResponse = await fetch(`${apiUrl}/inactivememberscount`, {
headers: { 'Content-Type': 'application/json' }
});
if (inactiveResponse.ok) {
const inactiveCount = await inactiveResponse.json();
setInactiveMembersCount(inactiveCount);
}
// Fetch deactivated members count
const deactivatedResponse = await fetch(`${apiUrl}/deactivatedmemberscount`, {
headers: { 'Content-Type': 'application/json' }
});
if (deactivatedResponse.ok) {
const deactivatedCount = await deactivatedResponse.json();
setDeactivatedMembersCount(deactivatedCount);
}
} catch (err) {
console.error('Error fetching member counts:', err);
}
};
// Fetch data from API
const fetchData = async (endpoint = 'items') => {
if (connectionStatus !== 'connected' && !isLoading) return;
setLoadingData(true);
try {
const response = await fetch(`${apiUrl}/${endpoint}`, {
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
const result = await response.json();
setData(Array.isArray(result) ? result : [result]);
} else {
throw new Error(`Failed to fetch data: ${response.statusText}`);
}
} catch (err) {
setError(err.message);
} finally {
setLoadingData(false);
}
};
const loadMembers = async () => {
if (connectionStatus !== 'connected' && !isLoading) return;
setLoadingData(true);
try {
// Since /members/ gives 500 error, let's try different approaches
console.log('Attempting to load active members count...');
// Option 1: Try individual member requests (if you know IDs)
// For now, let's try to get a few members by ID
const memberIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // Adjust these IDs as needed
const memberPromises = memberIds.map(async (id) => {
try {
const response = await fetch(`${apiUrl}/member/${id}`);
if (response.ok) {
return await response.json();
}
return null;
} catch (error) {
console.log(`Member ${id} not found or error:`, error);
return null;
}
});
const results = await Promise.all(memberPromises);
const validMembers = results.filter(member => member !== null);
console.log('Found members:', validMembers);
setMembers(validMembers);
setTotalMembers(validMembers.length);
if (validMembers.length === 0) {
console.warn('No members found with IDs 1-10. You may need to adjust the ID range or fix the /members/ endpoint.');
}
} catch (error) {
console.error('Error loading members:', error);
setMembers([]);
setTotalMembers(0);
}
setLoadingData(false);
};
useEffect(() => {
if (connectionStatus === 'connected') {
// loadMembersCount();
fetchMemberCounts();
if (activeTab === 'data') {
fetchData();
}
}
}, [connectionStatus]);
const getStatusColor = () => {
switch (connectionStatus) {
case 'connected': return '#10b981';
case 'error': return '#ef4444';
default: return '#f59e0b';
}
};
const getStatusIcon = () => {
switch (connectionStatus) {
case 'connected': return <CheckCircle size={16} />;
case 'error': return <XCircle size={16} />;
default: return <AlertCircle size={16} />;
}
};
const getStatusText = () => {
switch (connectionStatus) {
case 'connected': return 'Connected';
case 'error': return 'Connection Error';
default: return 'Disconnected';
}
};
const getFullName = (member) => {
const firstName = member.member_firstname || '';
const lastName = member.member_surname || '';
const memberSuffix = member.member_suffix || '';
const fullName = `${firstName} ${lastName} ${memberSuffix}`.trim();
return fullName || 'N/A';
};
const MEMBER_STATE_COLORS = {
'A': '#10B981', // Green - Active
'I': '#F59E0B', // Amber/Orange - Inactive
'D': '#EF4444' // Red - Deactivated
};
const getMemberStateColor = (stateCode) => {
return MEMBER_STATE_COLORS[stateCode] || '#6B7280'; // Gray as fallback
};
const styles = {
container: {
minHeight: '100vh',
backgroundColor: '#f9fafb',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
},
header: {
backgroundColor: 'white',
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
borderBottom: '1px solid #e5e7eb'
},
headerContent: {
maxWidth: '1280px',
margin: '0 auto',
padding: '0 24px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: '24px',
paddingBottom: '24px'
},
title: {
fontSize: '30px',
fontWeight: 'bold',
color: '#111827',
margin: 0
},
subtitle: {
color: '#6b7280',
marginTop: '4px',
fontSize: '16px'
},
statusContainer: {
display: 'flex',
alignItems: 'center',
gap: '8px',
color: getStatusColor(),
fontWeight: '500'
},
mainContent: {
maxWidth: '1280px',
margin: '0 auto',
padding: '32px 24px'
},
card: {
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
border: '1px solid #e5e7eb',
padding: '24px',
marginBottom: '32px'
},
cardTitle: {
fontSize: '18px',
fontWeight: '600',
color: '#111827',
marginBottom: '16px'
},
connectionForm: {
display: 'flex',
alignItems: 'center',
gap: '16px'
},
input: {
flex: 1,
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '6px',
fontSize: '14px',
outline: 'none'
},
button: {
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
padding: '8px 16px',
backgroundColor: '#2563eb',
color: 'white',
border: 'none',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'background-color 0.2s'
},
buttonDisabled: {
opacity: 0.5,
cursor: 'not-allowed'
},
errorAlert: {
marginTop: '16px',
padding: '16px',
backgroundColor: '#fef2f2',
border: '1px solid #fecaca',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
gap: '8px'
},
errorText: {
color: '#991b1b'
},
tabs: {
borderBottom: '1px solid #e5e7eb',
marginBottom: '32px'
},
tabsNav: {
display: 'flex',
gap: '32px'
},
tab: {
padding: '8px 4px',
fontSize: '14px',
fontWeight: '500',
border: 'none',
background: 'none',
cursor: 'pointer',
borderBottom: '2px solid transparent',
transition: 'all 0.2s'
},
tabActive: {
borderBottom: '2px solid #2563eb',
color: '#2563eb'
},
tabInactive: {
color: '#6b7280'
},
statsGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '24px',
marginBottom: '32px'
},
statCard: {
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
border: '1px solid #e5e7eb',
padding: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
},
statContent: {
flex: 1
},
statLabel: {
fontSize: '14px',
fontWeight: '500',
color: '#6b7280'
},
statValue: {
fontSize: '24px',
fontWeight: 'bold',
color: '#111827',
marginTop: '8px'
},
statIcon: {
padding: '12px',
borderRadius: '50%'
},
iconOrange: {
backgroundColor: '#fed7aa',
color: '#ea580c'
},
iconBlue: {
backgroundColor: '#dbeafe',
color: '#2563eb'
},
iconGreen: {
backgroundColor: '#dcfce7',
color: '#16a34a'
},
iconPurple: {
backgroundColor: '#e9d5ff',
color: '#9333ea'
},
actionButtons: {
display: 'flex',
flexWrap: 'wrap',
gap: '16px'
},
actionButton: {
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
padding: '8px 16px',
border: 'none',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'background-color 0.2s'
},
actionButtonGray: {
backgroundColor: '#f3f4f6',
color: '#374151'
},
actionButtonGreen: {
backgroundColor: '#dcfce7',
color: '#166534'
},
actionButtonBlue: {
backgroundColor: '#dbeafe',
color: '#1e40af'
},
emptyState: {
textAlign: 'center',
padding: '32px'
},
emptyStateIcon: {
margin: '0 auto 16px',
color: '#9ca3af'
},
emptyStateText: {
color: '#6b7280'
},
cardHeader: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px'
},
refreshButton: {
display: 'flex',
alignItems: 'center',
padding: '8px 16px',
backgroundColor: '#3B82F6',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500'
},
loadingState: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '40px',
color: '#6B7280'
},
loadingIcon: {
marginBottom: '10px',
color: '#3B82F6'
},
dataContainer: {
marginTop: '20px'
},
dataHeader: {
marginBottom: '16px',
padding: '12px',
backgroundColor: '#F3F4F6',
borderRadius: '6px'
},
dataCount: {
margin: 0,
fontWeight: '600',
color: '#374151'
},
tableContainer: {
overflowX: 'auto',
border: '1px solid #E5E7EB',
borderRadius: '8px'
},
table: {
width: '100%',
borderCollapse: 'collapse'
},
tableHeaderRow: {
backgroundColor: '#F9FAFB'
},
tableHeader: {
padding: '12px',
textAlign: 'left',
fontWeight: '600',
color: '#374151',
borderBottom: '1px solid #E5E7EB'
},
tableRow: {
borderBottom: '1px solid #F3F4F6'
},
tableCell: {
padding: '12px',
color: '#6B7280'
},
statusBadge: {
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: '500'
},
};
return (
<div style={styles.container}>
{/* Header */}
<div style={styles.header}>
<div style={styles.headerContent}>
<div>
<h1 style={styles.title}>FastAPI Dashboard</h1>
<p style={styles.subtitle}>Manage your backend data and monitor API status</p>
</div>
<div style={styles.statusContainer}>
{getStatusIcon()}
<span>{getStatusText()}</span>
</div>
</div>
</div>
{/* Main Content */}
<div style={styles.mainContent}>
{/* Connection Section */}
<div style={styles.card}>
<h2 style={styles.cardTitle}>API Connection</h2>
<div style={styles.connectionForm}>
<input
type="text"
value={apiUrl}
onChange={(e) => setApiUrl(e.target.value)}
style={styles.input}
placeholder="Enter API URL"
/>
<button
onClick={testConnection}
disabled={isLoading}
style={{
...styles.button,
...(isLoading ? styles.buttonDisabled : {}),
':hover': { backgroundColor: '#1d4ed8' }
}}
onMouseEnter={(e) => {
if (!isLoading) e.target.style.backgroundColor = '#1d4ed8';
}}
onMouseLeave={(e) => {
if (!isLoading) e.target.style.backgroundColor = '#2563eb';
}}
>
<RefreshCw size={16} style={isLoading ? { animation: 'spin 1s linear infinite' } : {}} />
Test Connection
</button>
</div>
{connectionStatus === 'error' && (
<div style={styles.errorAlert}>
<XCircle size={20} style={{ color: '#dc2626' }} />
<span style={styles.errorText}>NetworkError when attempting to fetch resource.</span>
</div>
)}
</div>
{/* Navigation Tabs */}
<div style={styles.tabs}>
<div style={styles.tabsNav}>
<button
onClick={() => setActiveTab('dashboard')}
style={{
...styles.tab,
...(activeTab === 'dashboard' ? styles.tabActive : styles.tabInactive)
}}
>
Dashboard
</button>
<button
onClick={() => setActiveTab('data')}
style={{
...styles.tab,
...(activeTab === 'data' ? styles.tabActive : styles.tabInactive)
}}
>
Data
</button>
</div>
</div>
{/* Stats Grid */}
<div style={styles.statsGrid}>
{/* API Status */}
<div style={styles.statCard}>
<div style={styles.statContent}>
<p style={styles.statLabel}>API Status</p>
<p style={styles.statValue}>
{connectionStatus === 'connected' ? 'Online' : 'Offline'}
</p>
</div>
<div style={{...styles.statIcon, ...styles.iconOrange}}>
<Server size={24} />
</div>
</div>
{/* Active Users */}
<div style={styles.statCard}>
<div style={styles.statContent}>
<p style={styles.statLabel}>Active Users</p>
<p style={styles.statValue}>1</p>
</div>
<div style={{...styles.statIcon, ...styles.iconGreen}}>
<Users size={24} />
</div>
</div>
{/* Activity */}
<div style={styles.statCard}>
<div style={styles.statContent}>
<p style={styles.statLabel}>Activity</p>
<p style={styles.statValue}>Live</p>
</div>
<div style={{...styles.statIcon, ...styles.iconPurple}}>
<Activity size={24} />
</div>
</div>
<div className="bg-gradient-to-r from-green-500 to-green-600 p-6 rounded-xl text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-green-100">Active Members</p>
<p className="text-2xl font-bold">{activeMembersCount}</p>
</div>
<Users className="w-8 h-8 text-green-200" />
</div>
</div>
<div className="bg-gradient-to-r from-yellow-500 to-yellow-600 p-6 rounded-xl text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-yellow-100">Inactive Members</p>
<p className="text-2xl font-bold">{inactiveMembersCount}</p>
</div>
<Users className="w-8 h-8 text-yellow-200" />
</div>
</div>
<div className="bg-gradient-to-r from-red-500 to-red-600 p-6 rounded-xl text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-red-100">Deactivated Members</p>
<p className="text-2xl font-bold">{deactivatedMembersCount}</p>
</div>
<Users className="w-8 h-8 text-red-200" />
</div>
</div>
</div>
{/* Quick Actions */}
<div style={styles.card}>
<h2 style={styles.cardTitle}>Quick Actions</h2>
<div style={styles.actionButtons}>
<button
style={{...styles.actionButton, ...styles.actionButtonGray}}
onMouseEnter={(e) => e.target.style.backgroundColor = '#e5e7eb'}
onMouseLeave={(e) => e.target.style.backgroundColor = '#f3f4f6'}
>
<Eye size={16} />
View Data
</button>
<button
style={{...styles.actionButton, ...styles.actionButtonGreen}}
onMouseEnter={(e) => e.target.style.backgroundColor = '#bbf7d0'}
onMouseLeave={(e) => e.target.style.backgroundColor = '#dcfce7'}
>
<Plus size={16} />
Add Item
</button>
<button
style={{...styles.actionButton, ...styles.actionButtonBlue}}
onMouseEnter={(e) => e.target.style.backgroundColor = '#bfdbfe'}
onMouseLeave={(e) => e.target.style.backgroundColor = '#dbeafe'}
>
<RefreshCw size={16} />
Refresh
</button>
</div>
</div>
{/* Content based on active tab */}
{activeTab === 'data' && (
<div style={styles.card}>
<div style={styles.cardHeader}>
<h2 style={styles.cardTitle}>Data Management</h2>
<button
onClick={loadMembers}
style={styles.refreshButton}
disabled={isLoading}
>
<RefreshCw size={16} style={{ marginRight: '8px' }} />
{isLoading ? 'Loading...' : 'Load Members'}
</button>
</div>
{isLoading ? (
<div style={styles.loadingState}>
<RefreshCw size={24} style={{ ...styles.loadingIcon, animation: 'spin 1s linear infinite' }} />
<p>Loading members...</p>
</div>
) : members.length === 0 ? (
<div style={styles.emptyState}>
<Database size={48} style={styles.emptyStateIcon} />
<p style={styles.emptyStateText}>No members found. Click "Load Members" to fetch data.</p>
</div>
) : (
<div style={styles.dataContainer}>
<div style={styles.dataHeader}>
<p style={styles.dataCount}>Total Members: {members.length}</p>
</div>
<div style={styles.tableContainer}>
<table style={styles.table}>
<thead>
<tr style={styles.tableHeaderRow}>
<th style={styles.tableHeader}>ID</th>
<th style={styles.tableHeader}>Name</th>
<th style={styles.tableHeader}>Email</th>
<th style={styles.tableHeader}>Active</th>
<th style={styles.tableHeader}>Actions</th>
</tr>
</thead>
<tbody>
{members.map((member, index) => (
<tr key={member.id || index} style={styles.tableRow}>
<td style={styles.tableCell}>{member.id}</td>
<td style={styles.tableCell}>{getFullName(member)}</td>
<td style={styles.tableCell}>{member.member_email || 'N/A'}</td>
<td style={styles.tableCell}>
<span style={{
...styles.statusBadge,
backgroundColor: getMemberStateColor(member.current_state.state_code),
color: 'white'
}}>
{member.current_state.state_name}
</span>
</td>
<td style={styles.tableCell}>
<button style={styles.actionButton}>
<Edit3 size={14} />
</button>
<button style={styles.actionButton}>
<Trash2 size={14} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
)}
</div>
<style>
{`
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`}
</style>
</div>
);
};
export default Dashboard;