This commit is contained in:
sebseb7
2025-08-01 09:26:47 +02:00
parent be7a928ce2
commit 1ec1e1e5f6
7 changed files with 763 additions and 24 deletions

View File

@@ -0,0 +1,288 @@
import React, { Component } from 'react';
import {
Select,
MenuItem,
FormControl,
InputLabel,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Box,
Typography,
Alert,
CircularProgress
} from '@mui/material';
import { Add as AddIcon } from '@mui/icons-material';
import KreditorService from '../services/KreditorService';
class KreditorSelector extends Component {
constructor(props) {
super(props);
this.state = {
kreditors: [],
selectedKreditorId: props.selectedKreditorId || '',
loading: false,
createDialogOpen: false,
newKreditor: {
iban: '',
name: '',
kreditorId: ''
},
validationErrors: [],
error: null,
creating: false
};
this.kreditorService = new KreditorService();
}
componentDidMount() {
this.loadKreditors();
}
componentDidUpdate(prevProps) {
if (prevProps.selectedKreditorId !== this.props.selectedKreditorId) {
this.setState({ selectedKreditorId: this.props.selectedKreditorId || '' });
}
}
loadKreditors = async () => {
this.setState({ loading: true, error: null });
try {
const kreditors = await this.kreditorService.getAllKreditors();
this.setState({ kreditors, loading: false });
} catch (error) {
console.error('Error loading kreditors:', error);
this.setState({
error: error.message,
loading: false
});
}
};
handleKreditorChange = (event) => {
const selectedKreditorId = event.target.value;
if (selectedKreditorId === 'create_new') {
this.setState({ createDialogOpen: true });
return;
}
this.setState({ selectedKreditorId });
if (this.props.onKreditorChange) {
const selectedKreditor = this.state.kreditors.find(k => k.id === selectedKreditorId);
this.props.onKreditorChange(selectedKreditor);
}
};
handleCreateDialogClose = () => {
this.setState({
createDialogOpen: false,
newKreditor: { iban: '', name: '', kreditorId: '' },
validationErrors: [],
error: null
});
};
handleNewKreditorChange = (field, value) => {
this.setState({
newKreditor: {
...this.state.newKreditor,
[field]: value
},
validationErrors: [] // Clear validation errors when user types
});
};
generateKreditorId = () => {
// Generate a kreditorId starting with 70 followed by random digits
const randomDigits = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
const kreditorId = `70${randomDigits}`;
this.setState({
newKreditor: {
...this.state.newKreditor,
kreditorId
}
});
};
handleCreateKreditor = async () => {
const { newKreditor } = this.state;
// Validate the data
const validationErrors = this.kreditorService.validateKreditorData(newKreditor);
if (validationErrors.length > 0) {
this.setState({ validationErrors });
return;
}
this.setState({ creating: true, error: null });
try {
const createdKreditor = await this.kreditorService.createKreditor(newKreditor);
// Add the new kreditor to the list and select it
const updatedKreditors = [...this.state.kreditors, createdKreditor];
this.setState({
kreditors: updatedKreditors,
selectedKreditorId: createdKreditor.id,
creating: false
});
// Notify parent component
if (this.props.onKreditorChange) {
this.props.onKreditorChange(createdKreditor);
}
this.handleCreateDialogClose();
} catch (error) {
console.error('Error creating kreditor:', error);
this.setState({
error: error.message,
creating: false
});
}
};
render() {
const {
kreditors,
selectedKreditorId,
loading,
createDialogOpen,
newKreditor,
validationErrors,
error,
creating
} = this.state;
const { label = "Kreditor", disabled = false, fullWidth = true } = this.props;
return (
<>
<FormControl fullWidth={fullWidth} disabled={disabled || loading}>
<InputLabel>{label}</InputLabel>
<Select
value={selectedKreditorId}
onChange={this.handleKreditorChange}
label={label}
>
<MenuItem value="">
<em>Keinen Kreditor auswählen</em>
</MenuItem>
{kreditors.map((kreditor) => (
<MenuItem key={kreditor.id} value={kreditor.id}>
{kreditor.name} ({kreditor.kreditorId}) - {kreditor.iban}
</MenuItem>
))}
<MenuItem value="create_new" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
<AddIcon sx={{ mr: 1 }} />
Neuen Kreditor erstellen
</MenuItem>
</Select>
</FormControl>
{error && (
<Alert severity="error" sx={{ mt: 1 }}>
{error}
</Alert>
)}
{/* Create Kreditor Dialog */}
<Dialog
open={createDialogOpen}
onClose={this.handleCreateDialogClose}
maxWidth="sm"
fullWidth
>
<DialogTitle>Neuen Kreditor erstellen</DialogTitle>
<DialogContent>
<Box sx={{ pt: 1 }}>
{validationErrors.length > 0 && (
<Alert severity="error" sx={{ mb: 2 }}>
<ul style={{ margin: 0, paddingLeft: '20px' }}>
{validationErrors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<TextField
label="Name"
value={newKreditor.name}
onChange={(e) => this.handleNewKreditorChange('name', e.target.value)}
fullWidth
margin="normal"
required
/>
<TextField
label="IBAN"
value={newKreditor.iban}
onChange={(e) => this.handleNewKreditorChange('iban', e.target.value.toUpperCase())}
fullWidth
margin="normal"
required
placeholder="DE89 3704 0044 0532 0130 00"
/>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
<TextField
label="Kreditor-ID"
value={newKreditor.kreditorId}
onChange={(e) => this.handleNewKreditorChange('kreditorId', e.target.value)}
margin="normal"
required
placeholder="70001"
sx={{ flexGrow: 1 }}
/>
<Button
onClick={this.generateKreditorId}
variant="outlined"
sx={{ mb: 1 }}
>
Generieren
</Button>
</Box>
<Typography variant="caption" color="textSecondary" sx={{ mt: 1, display: 'block' }}>
Die Kreditor-ID muss mit "70" beginnen, gefolgt von mindestens 3 Ziffern.
</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button
onClick={this.handleCreateDialogClose}
disabled={creating}
>
Abbrechen
</Button>
<Button
onClick={this.handleCreateKreditor}
variant="contained"
disabled={creating}
startIcon={creating ? <CircularProgress size={16} /> : null}
>
{creating ? 'Erstellen...' : 'Erstellen'}
</Button>
</DialogActions>
</Dialog>
</>
);
}
}
export default KreditorSelector;

View File

@@ -25,6 +25,7 @@ import {
ContentCopy as CopyIcon
} from '@mui/icons-material';
import { AgGridReact } from 'ag-grid-react';
import KreditorSelector from '../KreditorSelector';
const DocumentRenderer = (params) => {
// Check for pdfs and links regardless of transaction source
@@ -145,7 +146,10 @@ const DocumentRenderer = (params) => {
currency: extractionData.currency || 'EUR',
invoiceNumber: extractionData.invoice_number || '',
date: extractionData.date || '',
sender: extractionData.sender || ''
sender: extractionData.sender || '',
kreditorId: extractionData.kreditor_id || null,
kreditorName: extractionData.kreditor_name || '',
kreditorCode: extractionData.kreditor_code || ''
});
});
}
@@ -184,6 +188,34 @@ const DocumentRenderer = (params) => {
width: 150,
tooltipField: 'sender'
},
{
headerName: 'Kreditor',
field: 'kreditor',
width: 200,
cellRenderer: (params) => {
return (
<Box sx={{ height: '100%', display: 'flex', alignItems: 'center' }}>
<KreditorSelector
selectedKreditorId={params.data.kreditorId}
onKreditorChange={(kreditor) => {
// Update the line item with the selected kreditor
if (params.data && params.api) {
params.data.kreditorId = kreditor ? kreditor.id : null;
params.data.kreditorName = kreditor ? kreditor.name : '';
params.data.kreditorCode = kreditor ? kreditor.kreditorId : '';
params.api.refreshCells({ rowNodes: [params.node] });
}
}}
label=""
fullWidth={false}
/>
</Box>
);
},
editable: false,
sortable: false,
filter: false
},
{
headerName: 'Netto',
field: 'netAmount',
@@ -371,7 +403,7 @@ const DocumentRenderer = (params) => {
)}
{tabValue === 1 && (
<Box sx={{ p: 2, height: 400 }}>
<Box sx={{ p: 2, height: 500 }}>
{lineItems.length > 0 ? (
<div style={{ height: '100%', width: '100%' }}>
<AgGridReact
@@ -379,7 +411,7 @@ const DocumentRenderer = (params) => {
rowData={lineItems}
defaultColDef={defaultColDef}
suppressRowTransform={true}
rowHeight={35}
rowHeight={50}
headerHeight={35}
domLayout="normal"
/>

View File

@@ -0,0 +1,178 @@
class KreditorService {
constructor() {
this.baseURL = '/api';
}
async getAuthHeaders() {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
};
}
async handleResponse(response) {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.log('Server error response:', errorData);
// Handle different types of errors with clearer messages
if (response.status === 502 || response.status === 503) {
throw new Error('FibDash Service nicht verfügbar - Bitte versuchen Sie es später erneut');
} else if (response.status === 500) {
throw new Error('FibDash Server Fehler - Bitte kontaktieren Sie den Administrator');
} else if (response.status === 403) {
const message = errorData.message || 'Zugriff verweigert';
throw new Error(message);
} else if (response.status === 401) {
throw new Error('Authentifizierung fehlgeschlagen - Bitte melden Sie sich erneut an');
} else if (response.status === 404) {
throw new Error('Kreditor nicht gefunden');
} else if (response.status === 409) {
const message = errorData.error || 'Kreditor bereits vorhanden';
throw new Error(message);
} else if (response.status === 400) {
const message = errorData.error || 'Ungültige Daten';
throw new Error(message);
} else {
const errorMessage = errorData.error || errorData.message || `HTTP ${response.status}: Unbekannter Fehler`;
throw new Error(errorMessage);
}
}
return await response.json();
}
async getAllKreditors() {
try {
const response = await fetch(`${this.baseURL}/data/kreditors`, {
method: 'GET',
headers: await this.getAuthHeaders(),
});
return await this.handleResponse(response);
} catch (error) {
console.error('Error fetching kreditors:', error);
// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut');
}
throw error;
}
}
async getKreditorById(id) {
try {
const response = await fetch(`${this.baseURL}/data/kreditors/${id}`, {
method: 'GET',
headers: await this.getAuthHeaders(),
});
return await this.handleResponse(response);
} catch (error) {
console.error('Error fetching kreditor:', error);
// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut');
}
throw error;
}
}
async createKreditor(kreditorData) {
try {
const response = await fetch(`${this.baseURL}/data/kreditors`, {
method: 'POST',
headers: await this.getAuthHeaders(),
body: JSON.stringify(kreditorData),
});
return await this.handleResponse(response);
} catch (error) {
console.error('Error creating kreditor:', error);
// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut');
}
throw error;
}
}
async updateKreditor(id, kreditorData) {
try {
const response = await fetch(`${this.baseURL}/data/kreditors/${id}`, {
method: 'PUT',
headers: await this.getAuthHeaders(),
body: JSON.stringify(kreditorData),
});
return await this.handleResponse(response);
} catch (error) {
console.error('Error updating kreditor:', error);
// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut');
}
throw error;
}
}
async deleteKreditor(id) {
try {
const response = await fetch(`${this.baseURL}/data/kreditors/${id}`, {
method: 'DELETE',
headers: await this.getAuthHeaders(),
});
return await this.handleResponse(response);
} catch (error) {
console.error('Error deleting kreditor:', error);
// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut');
}
throw error;
}
}
// Utility method to validate kreditor data
validateKreditorData(kreditorData) {
const errors = [];
if (!kreditorData.iban || kreditorData.iban.trim() === '') {
errors.push('IBAN ist erforderlich');
}
if (!kreditorData.name || kreditorData.name.trim() === '') {
errors.push('Name ist erforderlich');
}
if (!kreditorData.kreditorId || kreditorData.kreditorId.trim() === '') {
errors.push('Kreditor-ID ist erforderlich');
}
// Basic IBAN format validation (simplified)
if (kreditorData.iban && !/^[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}$/i.test(kreditorData.iban.replace(/\s/g, ''))) {
errors.push('IBAN Format ist ungültig');
}
// Validate kreditorId format (should start with 70xxx)
if (kreditorData.kreditorId && !/^70\d{3,}$/.test(kreditorData.kreditorId)) {
errors.push('Kreditor-ID muss mit 70 beginnen gefolgt von mindestens 3 Ziffern');
}
return errors;
}
}
export default KreditorService;