diff --git a/.cursor/rules/devdatabase.mdc b/.cursor/rules/devdatabase.mdc index 10772ae..cd35dd4 100644 --- a/.cursor/rules/devdatabase.mdc +++ b/.cursor/rules/devdatabase.mdc @@ -1,6 +1,4 @@ --- alwaysApply: true --- -sqlcmd -C -S tcp:192.168.56.1,1497 -U app -P 'readonly' -d eazybusiness -W - sqlcmd -C -S tcp:192.168.56.1,1497 -U sa -P 'sa_tekno23' -d eazybusiness -W \ No newline at end of file diff --git a/.cursor/rules/devserver.mdc b/.cursor/rules/devserver.mdc new file mode 100644 index 0000000..dfa1f62 --- /dev/null +++ b/.cursor/rules/devserver.mdc @@ -0,0 +1,7 @@ +--- +alwaysApply: true +--- +pm2 restart 10 -> restart backend (configured as "npm run dev:backend") +pm2 restart 11 -> restart backend (configured as "npm run dev:frontend") + +(both should rarely neer restart because in dev mode HMR for frontend, and nodemon for backend should already do that) diff --git a/.env.example b/.env.example index 90db9ea..4db51bc 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,9 @@ REACT_APP_GOOGLE_CLIENT_ID=your_google_client_id_here # JWT Secret JWT_SECRET=your_jwt_secret_here +# OpenAI API Configuration +OPENAI_API_KEY=your_openai_api_key_here + # Authorized Email Addresses (comma-separated) AUTHORIZED_EMAILS=admin@example.com,user1@example.com,user2@example.com diff --git a/client/src/App.js b/client/src/App.js index 7db0e2b..c1e18b2 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,11 +1,16 @@ import React, { Component } from 'react'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; -import { Container, AppBar, Toolbar, Typography, Button, Box, Tabs, Tab } from '@mui/material'; +import { Container, AppBar, Toolbar, Typography, Button, Box, Tabs, Tab, Badge, Chip, Divider, Snackbar, Alert } from '@mui/material'; import LoginIcon from '@mui/icons-material/Login'; import DashboardIcon from '@mui/icons-material/Dashboard'; import DownloadIcon from '@mui/icons-material/Download'; import TableChart from '@mui/icons-material/TableChart'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import DocumentScannerIcon from '@mui/icons-material/DocumentScanner'; +import ExtractIcon from '@mui/icons-material/TextSnippet'; +import EmailIcon from '@mui/icons-material/Email'; +import UploadIcon from '@mui/icons-material/Upload'; import AuthService from './services/AuthService'; import DataViewer from './components/DataViewer'; import Login from './components/Login'; @@ -31,6 +36,18 @@ class App extends Component { loading: true, exportData: null, // { selectedMonth, canExport, onExport } currentView: 'dashboard', // 'dashboard' or 'tables' + documentStatus: null, + processingStatus: { + markdown: false, + extraction: false, + datevSync: false, + datevUpload: false + }, + snackbar: { + open: false, + message: '', + severity: 'info' // 'success', 'error', 'warning', 'info' + } }; this.authService = new AuthService(); } @@ -39,6 +56,15 @@ class App extends Component { this.checkAuthStatus(); } + componentDidUpdate(prevState) { + // Clear targetTab after navigation is complete + if (this.state.targetTab && prevState.currentView !== this.state.currentView) { + setTimeout(() => { + this.setState({ targetTab: null }); + }, 100); // Small delay to ensure navigation completes + } + } + checkAuthStatus = async () => { try { const token = localStorage.getItem('token'); @@ -46,6 +72,7 @@ class App extends Component { const user = await this.authService.verifyToken(token); if (user) { this.setState({ isAuthenticated: true, user, loading: false }); + this.fetchDocumentStatus(); return; } } @@ -62,6 +89,7 @@ class App extends Component { if (result.success) { localStorage.setItem('token', result.token); this.setState({ isAuthenticated: true, user: result.user }); + this.fetchDocumentStatus(); } } catch (error) { console.error('Login failed:', error); @@ -83,8 +111,131 @@ class App extends Component { this.setState({ currentView: newValue }); }; + showSnackbar = (message, severity = 'info') => { + this.setState({ + snackbar: { + open: true, + message, + severity + } + }); + }; + + handleSnackbarClose = (event, reason) => { + if (reason === 'clickaway') { + return; + } + this.setState({ + snackbar: { + ...this.state.snackbar, + open: false + } + }); + }; + + fetchDocumentStatus = async () => { + try { + const token = localStorage.getItem('token'); + if (!token) { + console.log('No token found for document status'); + return; + } + + console.log('Fetching document status...'); + const response = await fetch('/api/data/document-status', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + const status = await response.json(); + console.log('Document status received:', status); + this.setState({ documentStatus: status }); + } else { + console.error('Failed to fetch document status:', response.status, await response.text()); + } + } catch (error) { + console.error('Error fetching document status:', error); + } + }; + + handleProcessing = async (processType) => { + if (this.state.processingStatus[processType]) { + return; // Already processing + } + + // Handle datev upload navigation + if (processType === 'datev-upload') { + this.setState({ + currentView: 'tables', + targetTab: { + level1: 3, // CSV Import tab + level2: 'DATEV_LINKS' // DATEV Beleglinks tab + } + }); + return; + } + + // Check if there are documents to process + const statusKey = processType === 'datev-sync' ? 'needDatevSync' : + processType === 'extraction' ? 'needExtraction' : 'needMarkdown'; + + if (!this.state.documentStatus || this.state.documentStatus[statusKey] === 0) { + this.showSnackbar(`No documents need ${processType} processing at this time.`, 'info'); + return; + } + + this.setState(prevState => ({ + processingStatus: { + ...prevState.processingStatus, + [processType]: true + } + })); + + try { + const token = localStorage.getItem('token'); + if (!token) return; + + const response = await fetch(`/api/data/process-${processType}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + const result = await response.json(); + console.log(`${processType} processing result:`, result); + this.showSnackbar(`${processType} processing completed successfully!`, 'success'); + // Refresh document status after successful processing + await this.fetchDocumentStatus(); + } else { + const error = await response.json(); + console.error(`Failed to process ${processType}:`, error); + this.showSnackbar(`Failed to process ${processType}: ${error.error || response.status}`, 'error'); + } + } catch (error) { + console.error(`Error processing ${processType}:`, error); + this.showSnackbar(`Error processing ${processType}: ${error.message}`, 'error'); + } finally { + this.setState(prevState => ({ + processingStatus: { + ...prevState.processingStatus, + [processType]: false + } + })); + } + }; + render() { - const { isAuthenticated, user, loading, currentView } = this.state; + const { isAuthenticated, user, loading, currentView, documentStatus, processingStatus, snackbar } = this.state; + + // Debug logging + console.log('App render - documentStatus:', documentStatus); + console.log('App render - isAuthenticated:', isAuthenticated); if (loading) { return ( @@ -149,6 +300,97 @@ class App extends Component { sx={{ minHeight: 48 }} /> + + + + + + + {this.state.exportData && (