Enhance Accounting Items Management with JTL Kontierung Integration

- Added a new API route to fetch JTL Kontierung data based on transaction ID.
- Implemented loading of JTL Kontierung data in the AccountingItemsManager component.
- Updated UI to display JTL Kontierung data for debugging purposes.
- Enhanced user feedback during processing tasks in the App component with tooltips and progress indicators.
This commit is contained in:
sebseb7
2025-08-08 11:32:57 +02:00
parent bcd7eea1b4
commit fee9f02faa
3 changed files with 160 additions and 67 deletions

View File

@@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles'; import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import { Container, AppBar, Toolbar, Typography, Button, Box, Tabs, Tab, Badge, Chip, Divider, Snackbar, Alert } from '@mui/material'; import { Container, AppBar, Toolbar, Typography, Button, Box, Tabs, Tab, Badge, Chip, Divider, Snackbar, Alert, LinearProgress, Tooltip, CircularProgress } from '@mui/material';
import LoginIcon from '@mui/icons-material/Login'; import LoginIcon from '@mui/icons-material/Login';
import DashboardIcon from '@mui/icons-material/Dashboard'; import DashboardIcon from '@mui/icons-material/Dashboard';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
@@ -302,72 +302,87 @@ class App extends Component {
</Tabs> </Tabs>
<Divider orientation="vertical" flexItem sx={{ mx: 2, backgroundColor: 'rgba(255, 255, 255, 0.3)' }} /> <Divider orientation="vertical" flexItem sx={{ mx: 2, backgroundColor: 'rgba(255, 255, 255, 0.3)' }} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button <Tooltip title={processingStatus.markdown ? 'Running markdown conversion… this can take a while' : 'Process markdown conversion'} arrow>
color="inherit" <span>
size="small" <Button
onClick={() => this.handleProcessing('markdown')} color="inherit"
disabled={processingStatus.markdown || !documentStatus} size="small"
sx={{ onClick={() => this.handleProcessing('markdown')}
minWidth: 'auto', disabled={processingStatus.markdown || !documentStatus}
px: 1, sx={{
'&:hover': { backgroundColor: 'rgba(255, 255, 255, 0.1)' } minWidth: 'auto',
}} px: 1,
title="Process markdown conversion" '&:hover': { backgroundColor: 'rgba(255, 255, 255, 0.1)' }
> }}
<Badge >
badgeContent={documentStatus?.needMarkdown || 0} <Badge
color={documentStatus?.needMarkdown > 0 ? "error" : "default"} badgeContent={documentStatus?.needMarkdown || 0}
max={999999} color={documentStatus?.needMarkdown > 0 ? "error" : "default"}
sx={{ mr: 0.5 }} max={999999}
> sx={{ mr: 0.5 }}
<DocumentScannerIcon fontSize="small" /> >
</Badge> <DocumentScannerIcon fontSize="small" />
{processingStatus.markdown && <PlayArrowIcon fontSize="small" />} </Badge>
</Button> {processingStatus.markdown && (
<Button <CircularProgress size={14} color="inherit" />
color="inherit" )}
size="small" </Button>
onClick={() => this.handleProcessing('extraction')} </span>
disabled={processingStatus.extraction || !documentStatus} </Tooltip>
sx={{ <Tooltip title={processingStatus.extraction ? 'Running data extraction… this can take a while' : 'Process data extraction'} arrow>
minWidth: 'auto', <span>
px: 1, <Button
'&:hover': { backgroundColor: 'rgba(255, 255, 255, 0.1)' } color="inherit"
}} size="small"
title="Process data extraction" onClick={() => this.handleProcessing('extraction')}
> disabled={processingStatus.extraction || !documentStatus}
<Badge sx={{
badgeContent={documentStatus?.needExtraction || 0} minWidth: 'auto',
color={documentStatus?.needExtraction > 0 ? "warning" : "default"} px: 1,
max={999999} '&:hover': { backgroundColor: 'rgba(255, 255, 255, 0.1)' }
sx={{ mr: 0.5 }} }}
> >
<ExtractIcon fontSize="small" /> <Badge
</Badge> badgeContent={documentStatus?.needExtraction || 0}
{processingStatus.extraction && <PlayArrowIcon fontSize="small" />} color={documentStatus?.needExtraction > 0 ? "warning" : "default"}
</Button> max={999999}
<Button sx={{ mr: 0.5 }}
color="inherit" >
size="small" <ExtractIcon fontSize="small" />
onClick={() => this.handleProcessing('datev-sync')} </Badge>
disabled={processingStatus.datevSync || !documentStatus} {processingStatus.extraction && (
sx={{ <CircularProgress size={14} color="inherit" />
minWidth: 'auto', )}
px: 1, </Button>
'&:hover': { backgroundColor: 'rgba(255, 255, 255, 0.1)' } </span>
}} </Tooltip>
title="Process Datev sync" <Tooltip title={processingStatus.datevSync ? 'Running DATEV sync… this can take a while' : 'Process Datev sync'} arrow>
> <span>
<Badge <Button
badgeContent={documentStatus?.needDatevSync || 0} color="inherit"
color={documentStatus?.needDatevSync > 0 ? "info" : "default"} size="small"
max={999999} onClick={() => this.handleProcessing('datev-sync')}
sx={{ mr: 0.5 }} disabled={processingStatus.datevSync || !documentStatus}
> sx={{
<EmailIcon fontSize="small" /> minWidth: 'auto',
</Badge> px: 1,
{processingStatus.datevSync && <PlayArrowIcon fontSize="small" />} '&:hover': { backgroundColor: 'rgba(255, 255, 255, 0.1)' }
</Button> }}
>
<Badge
badgeContent={documentStatus?.needDatevSync || 0}
color={documentStatus?.needDatevSync > 0 ? "info" : "default"}
max={999999}
sx={{ mr: 0.5 }}
>
<EmailIcon fontSize="small" />
</Badge>
{processingStatus.datevSync && (
<CircularProgress size={14} color="inherit" />
)}
</Button>
</span>
</Tooltip>
<Button <Button
color="inherit" color="inherit"
size="small" size="small"
@@ -426,6 +441,9 @@ class App extends Component {
</> </>
)} )}
</Toolbar> </Toolbar>
{(processingStatus.markdown || processingStatus.extraction || processingStatus.datevSync) && (
<LinearProgress color="secondary" />
)}
</AppBar> </AppBar>
<Box sx={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}> <Box sx={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}>

View File

@@ -44,6 +44,7 @@ class AccountingItemsManager extends Component {
editingItem: null, editingItem: null,
showCreateDialog: false, showCreateDialog: false,
showCreateKontoDialog: false, showCreateKontoDialog: false,
jtlKontierung: null,
newItem: { newItem: {
umsatz_brutto: '', umsatz_brutto: '',
soll_haben_kz: 'S', soll_haben_kz: 'S',
@@ -65,6 +66,7 @@ class AccountingItemsManager extends Component {
componentDidMount() { componentDidMount() {
this.loadData(); this.loadData();
this.loadJtlKontierung();
} }
loadData = async () => { loadData = async () => {
@@ -88,6 +90,31 @@ class AccountingItemsManager extends Component {
} }
}; };
loadJtlKontierung = async () => {
try {
const { transaction } = this.props;
if (!transaction || !transaction.jtlId) {
this.setState({ jtlKontierung: undefined });
return;
}
const response = await this.authService.apiCall(`/data/jtl-kontierung/${transaction.jtlId}`);
if (!response) return;
if (response.ok) {
const data = await response.json();
this.setState({ jtlKontierung: data });
} else {
const err = await response.json();
console.error('Failed to load JTL Kontierung:', err);
this.setState({ jtlKontierung: undefined });
}
} catch (e) {
console.error('Error loading JTL Kontierung:', e);
this.setState({ jtlKontierung: undefined });
}
}
loadAccountingItems = async () => { loadAccountingItems = async () => {
const { transaction } = this.props; const { transaction } = this.props;
if (!transaction?.id) return; if (!transaction?.id) return;
@@ -319,6 +346,21 @@ class AccountingItemsManager extends Component {
/> />
</Box> </Box>
{transaction?.jtlId && (
<Box sx={{ mb: 2, p: 2, border: '1px dashed #999', borderRadius: 1 }}>
<Typography variant="subtitle2">Debug: tUmsatzKontierung.data</Typography>
<Typography variant="caption" component="div" sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{this.state.jtlKontierung === undefined
? 'undefined'
: this.state.jtlKontierung === null
? 'null'
: typeof this.state.jtlKontierung === 'object'
? JSON.stringify(this.state.jtlKontierung, null, 2)
: String(this.state.jtlKontierung)}
</Typography>
</Box>
)}
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table size="small"> <Table size="small">
<TableHead> <TableHead>

View File

@@ -3,6 +3,39 @@ const { authenticateToken } = require('../../middleware/auth');
const router = express.Router(); const router = express.Router();
// Debug: Get JTL Kontierung data for a specific JTL Umsatz (by kZahlungsabgleichUmsatz)
router.get('/jtl-kontierung/:jtlId', authenticateToken, async (req, res) => {
try {
const { executeQuery } = require('../../config/database');
const { jtlId } = req.params;
const query = `
SELECT
uk.data
FROM eazybusiness.dbo.tZahlungsabgleichUmsatz z
LEFT JOIN eazybusiness.dbo.tUmsatzKontierung uk
ON uk.kZahlungsabgleichUmsatz = z.kZahlungsabgleichUmsatz
WHERE z.kZahlungsabgleichUmsatz = @jtlId
`;
const result = await executeQuery(query, { jtlId: parseInt(jtlId, 10) });
// Return undefined when no data found (do not lie with empty array/string)
if (!result.recordset || result.recordset.length === 0) {
return res.json({ data: undefined });
}
// If multiple rows exist, return all; otherwise single object
const rows = result.recordset.map(r => ({ data: r.data }));
if (rows.length === 1) {
return res.json(rows[0]);
}
return res.json(rows);
} catch (error) {
console.error('Error fetching JTL Kontierung data:', error);
res.status(500).json({ error: 'Failed to fetch JTL Kontierung data' });
}
});
// Get accounting items for a specific transaction // Get accounting items for a specific transaction
router.get('/accounting-items/:transactionId', authenticateToken, async (req, res) => { router.get('/accounting-items/:transactionId', authenticateToken, async (req, res) => {
try { try {