- Removed `package_type` from the Smart Package creation process in the backend. - Updated SQL queries and data handling in `smartPackageManagementService.js` to reflect the removal of `package_type`. - Simplified the Smart Package form component by removing unnecessary navigation hooks and parameters. - Adjusted the Smart Packages main component to handle editing and creating packages without direct navigation. - Enhanced the Smart Packages menu to use callback functions for section changes instead of routing. - Implemented a new script to create Smart Packages from Ordrestyring cases, including detailed logging and error handling. - Added a test script to explore cases from Ordrestyring, providing insights into available cases and their details. - Updated last_sync_status.json with new sync data.
352 lines
12 KiB
JavaScript
352 lines
12 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import axios from 'axios';
|
|
import { SnackbarProvider } from 'notistack';
|
|
import './App.css';
|
|
import ProjectFlow from './components/ProjectFlow';
|
|
import MaterialsList from './MaterialsList';
|
|
import PlanningDashboard from './components/PlanningDashboard';
|
|
import AnalyticsDashboard from './components/AnalyticsDashboard';
|
|
import SmartPackagesRoutes from './routes/SmartPackagesRoutes';
|
|
import './components/ProjectReview.css';
|
|
|
|
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:4031';
|
|
|
|
function App() {
|
|
const [currentView, setCurrentView] = useState('projects'); // 'projects', 'planning', 'materials', 'openai-usage'
|
|
|
|
// Authentication state
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [authError, setAuthError] = useState('');
|
|
|
|
// OpenAI usage state
|
|
const [openaiUsage, setOpenaiUsage] = useState(null);
|
|
|
|
|
|
|
|
// Check authentication on mount
|
|
useEffect(() => {
|
|
const authStatus = localStorage.getItem('isAuthenticated');
|
|
if (authStatus === 'true') {
|
|
setIsAuthenticated(true);
|
|
// Load AI usage data when user is already authenticated
|
|
loadOpenaiUsage();
|
|
}
|
|
|
|
// Set up global AI usage refresh callback
|
|
window.refreshAIUsage = loadOpenaiUsage;
|
|
|
|
// Cleanup on unmount
|
|
return () => {
|
|
delete window.refreshAIUsage;
|
|
};
|
|
}, []);
|
|
|
|
const handleLogin = async (e) => {
|
|
e.preventDefault();
|
|
setAuthError('');
|
|
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/auth/login`, {
|
|
username,
|
|
password
|
|
});
|
|
|
|
if (response.data.success) {
|
|
setIsAuthenticated(true);
|
|
localStorage.setItem('isAuthenticated', 'true');
|
|
setUsername('');
|
|
setPassword('');
|
|
// Load AI usage data after successful login
|
|
loadOpenaiUsage();
|
|
} else {
|
|
setAuthError('Forkert brugernavn eller password');
|
|
}
|
|
} catch (error) {
|
|
setAuthError('Forkert brugernavn eller password');
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
setIsAuthenticated(false);
|
|
localStorage.removeItem('isAuthenticated');
|
|
};
|
|
|
|
const loadOpenaiUsage = async () => {
|
|
try {
|
|
const response = await axios.get(`${API_BASE_URL}/api/quotes/openai/stats`);
|
|
// Transform the data to match expected format
|
|
const data = response.data;
|
|
if (data.tokenUsage) {
|
|
setOpenaiUsage({
|
|
current_cost: data.tokenUsage.currentCost,
|
|
request_count: data.tokenUsage.requestCount,
|
|
total_tokens: data.tokenUsage.totalTokens,
|
|
last_updated: data.tokenUsage.actualUsage?.lastUpdated || data.timestamp
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading OpenAI usage:', error);
|
|
}
|
|
};
|
|
|
|
if (!isAuthenticated) {
|
|
return (
|
|
<SnackbarProvider
|
|
maxSnack={3}
|
|
anchorOrigin={{
|
|
vertical: 'top',
|
|
horizontal: 'right',
|
|
}}
|
|
autoHideDuration={4000}
|
|
>
|
|
<div className="login-container">
|
|
<div className="login-form">
|
|
<h1>🏗️ Tilbudgivern</h1>
|
|
<h2>Log ind</h2>
|
|
<form onSubmit={handleLogin}>
|
|
<div className="form-group">
|
|
<label htmlFor="username">Brugernavn</label>
|
|
<input
|
|
type="text"
|
|
id="username"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="password">Password</label>
|
|
<input
|
|
type="password"
|
|
id="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
{authError && <div className="error">{authError}</div>}
|
|
<button type="submit" className="btn btn-primary">
|
|
Log ind
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</SnackbarProvider>
|
|
);
|
|
}
|
|
|
|
const navigateToCompletedQuotes = () => {
|
|
setCurrentView('planning');
|
|
};
|
|
|
|
return (
|
|
<SnackbarProvider
|
|
maxSnack={3}
|
|
anchorOrigin={{
|
|
vertical: 'top',
|
|
horizontal: 'right',
|
|
}}
|
|
autoHideDuration={4000}
|
|
>
|
|
<div className={`App ${currentView === 'materials' ? 'materials-mode' : ''}`}>
|
|
<header className="header">
|
|
<h1>🏗️ Tilbudgivern</h1>
|
|
|
|
{isAuthenticated && (
|
|
<div className="nav-buttons">
|
|
<button
|
|
className={`nav-btn ${currentView === 'projects' ? 'active' : ''}`}
|
|
onClick={() => setCurrentView('projects')}
|
|
>
|
|
🏗️ Projekt Flow
|
|
</button>
|
|
<button
|
|
className={`nav-btn ${currentView === 'planning' ? 'active' : ''}`}
|
|
onClick={() => setCurrentView('planning')}
|
|
>
|
|
📋 Planlægning
|
|
</button>
|
|
<button
|
|
className={`nav-btn ${currentView === 'materials' ? 'active' : ''}`}
|
|
onClick={() => {
|
|
setCurrentView('materials');
|
|
}}
|
|
>
|
|
🧱 Materialer
|
|
</button>
|
|
|
|
<button
|
|
className={`nav-btn ${currentView === 'smart-packages' ? 'active' : ''}`}
|
|
onClick={() => {
|
|
setCurrentView('smart-packages');
|
|
}}
|
|
>
|
|
📦 Smart Pakker
|
|
</button>
|
|
|
|
<button
|
|
className={`nav-btn ${currentView === 'noegletal' ? 'active' : ''}`}
|
|
onClick={() => setCurrentView('noegletal')}
|
|
>
|
|
📊 Nøgletal
|
|
</button>
|
|
|
|
|
|
<button
|
|
className={`nav-btn ${currentView === 'openai-usage' ? 'active' : ''}`}
|
|
onClick={() => {
|
|
setCurrentView('openai-usage');
|
|
loadOpenaiUsage();
|
|
}}
|
|
>
|
|
🤖 AI Budget
|
|
</button>
|
|
</div>
|
|
)}
|
|
{isAuthenticated && (
|
|
<button className="logout-btn" onClick={handleLogout}>
|
|
Log ud
|
|
</button>
|
|
)}
|
|
</header>
|
|
|
|
<main className="main-content">
|
|
{currentView === 'projects' ? (
|
|
// Structured project flow
|
|
<ProjectFlow
|
|
apiBaseUrl={API_BASE_URL}
|
|
onNavigateToCompletedQuotes={navigateToCompletedQuotes}
|
|
/>
|
|
) : currentView === 'materials' ? (
|
|
// Materials Management with new design
|
|
<MaterialsList apiBaseUrl={API_BASE_URL} />
|
|
) : currentView === 'planning' ? (
|
|
// Planning Dashboard with orders and calendar
|
|
<PlanningDashboard />
|
|
) : currentView === 'noegletal' ? (
|
|
// Analytics Dashboard with business intelligence
|
|
<AnalyticsDashboard apiBaseUrl={API_BASE_URL} />
|
|
) : currentView === 'smart-packages' ? (
|
|
// Smart Packages Management
|
|
<SmartPackagesRoutes />
|
|
) : currentView === 'openai-usage' ? (
|
|
// OpenAI Usage section
|
|
<div className="openai-usage-view">
|
|
<div className="usage-section">
|
|
<h2>🤖 OpenAI Budget</h2>
|
|
<p>Oversigt over AI forbrug og omkostninger</p>
|
|
|
|
{openaiUsage ? (
|
|
<div className="usage-stats-grid">
|
|
<div className="stat-card primary-stat">
|
|
<div className="stat-icon">💰</div>
|
|
<div className="stat-content">
|
|
<h3>Aktuelle omkostninger</h3>
|
|
<p className="stat-value large">
|
|
${(() => {
|
|
const cost = openaiUsage.current_cost || 0;
|
|
// Smart formatting: remove trailing zeros
|
|
return cost.toFixed(4).replace(/\.?0+$/, '') || '0';
|
|
})()}
|
|
</p>
|
|
<span className="stat-label">USD total</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="stat-card">
|
|
<div className="stat-icon">📊</div>
|
|
<div className="stat-content">
|
|
<h3>Antal requests</h3>
|
|
<p className="stat-value">{openaiUsage.request_count || 0}</p>
|
|
<span className="stat-label">API kald</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="stat-card">
|
|
<div className="stat-icon">🔢</div>
|
|
<div className="stat-content">
|
|
<h3>Total tokens</h3>
|
|
<p className="stat-value">{openaiUsage.total_tokens?.toLocaleString() || 0}</p>
|
|
<span className="stat-label">tokens brugt</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="stat-card">
|
|
<div className="stat-icon">⏰</div>
|
|
<div className="stat-content">
|
|
<h3>Sidst opdateret</h3>
|
|
<p className="stat-value small">
|
|
{openaiUsage.last_updated ?
|
|
new Date(openaiUsage.last_updated).toLocaleString('da-DK') :
|
|
'Ingen data'
|
|
}
|
|
</p>
|
|
<span className="stat-label">timestamp</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Additional cost breakdown if available */}
|
|
{openaiUsage.tokenUsage && (
|
|
<div className="stat-card wide-stat">
|
|
<div className="stat-icon">📈</div>
|
|
<div className="stat-content">
|
|
<h3>Token fordeling</h3>
|
|
<div className="token-breakdown">
|
|
<div className="token-item">
|
|
<span>Input tokens:</span>
|
|
<strong>{openaiUsage.tokenUsage.promptTokens?.toLocaleString() || 0}</strong>
|
|
</div>
|
|
<div className="token-item">
|
|
<span>Output tokens:</span>
|
|
<strong>{openaiUsage.tokenUsage.completionTokens?.toLocaleString() || 0}</strong>
|
|
</div>
|
|
<div className="token-item total">
|
|
<span>Total:</span>
|
|
<strong>{openaiUsage.tokenUsage.totalTokens?.toLocaleString() || 0}</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Cost in DKK approximation */}
|
|
<div className="stat-card secondary-stat">
|
|
<div className="stat-icon">🇩🇰</div>
|
|
<div className="stat-content">
|
|
<h3>Omkostninger (ca.)</h3>
|
|
<p className="stat-value medium">
|
|
{(() => {
|
|
const dkkCost = (openaiUsage.current_cost || 0) * 6.8;
|
|
// Smart formatting for DKK: show more decimals for small amounts
|
|
if (dkkCost < 0.01) {
|
|
return dkkCost.toFixed(4).replace(/\.?0+$/, '') + ' kr';
|
|
} else if (dkkCost < 1) {
|
|
return dkkCost.toFixed(3).replace(/\.?0+$/, '') + ' kr';
|
|
} else {
|
|
return dkkCost.toFixed(2) + ' kr';
|
|
}
|
|
})()}
|
|
</p>
|
|
<span className="stat-label">Omregnet til DKK</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="loading-state">
|
|
<div className="loading-spinner"></div>
|
|
<p>Indlæser AI usage data...</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</main>
|
|
</div>
|
|
</SnackbarProvider>
|
|
);
|
|
}
|
|
|
|
export default App;
|