u
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
---
|
||||
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 app -P 'readonly' -d eazybusiness -W
|
||||
|
||||
sqlcmd -C -S tcp:192.168.56.1,1497 -U sa -P 'sa_tekno23' -d eazybusiness -W
|
||||
288
client/src/components/KreditorSelector.js
Normal file
288
client/src/components/KreditorSelector.js
Normal 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;
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
178
client/src/services/KreditorService.js
Normal file
178
client/src/services/KreditorService.js
Normal 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;
|
||||
@@ -1,34 +1,94 @@
|
||||
-- FibDash Database Schema
|
||||
-- Run these commands in your MSSQL database
|
||||
|
||||
-- Create Users table
|
||||
CREATE TABLE Users (
|
||||
-- Create Kreditor table
|
||||
CREATE TABLE Kreditor (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
google_id NVARCHAR(255) UNIQUE NOT NULL,
|
||||
email NVARCHAR(255) UNIQUE NOT NULL,
|
||||
iban NVARCHAR(34) NOT NULL,
|
||||
name NVARCHAR(255) NOT NULL,
|
||||
picture NVARCHAR(500),
|
||||
created_at DATETIME2 DEFAULT GETDATE(),
|
||||
last_login DATETIME2,
|
||||
is_active BIT DEFAULT 1
|
||||
kreditorId NVARCHAR(50) NOT NULL
|
||||
);
|
||||
|
||||
-- Create UserPreferences table
|
||||
CREATE TABLE UserPreferences (
|
||||
-- Ensure kreditorId is unique to support FK references
|
||||
ALTER TABLE Kreditor
|
||||
ADD CONSTRAINT UQ_Kreditor_kreditorId UNIQUE (kreditorId);
|
||||
|
||||
-- Create AccountingItems table
|
||||
-- Based on CSV structure: umsatz brutto, soll/haben kz, konto, gegenkonto, bu, buchungsdatum, rechnungsnummer, buchungstext, beleglink
|
||||
CREATE TABLE AccountingItems (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
theme NVARCHAR(50) DEFAULT 'light',
|
||||
language NVARCHAR(10) DEFAULT 'en',
|
||||
notifications_enabled BIT DEFAULT 1,
|
||||
created_at DATETIME2 DEFAULT GETDATE(),
|
||||
updated_at DATETIME2 DEFAULT GETDATE(),
|
||||
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE
|
||||
umsatz_brutto DECIMAL(15,2) NOT NULL, -- gross turnover amount
|
||||
soll_haben_kz CHAR(1) NOT NULL CHECK (soll_haben_kz IN ('S', 'H')), -- S = eingang (debit), H = ausgang (credit)
|
||||
konto NVARCHAR(10) NOT NULL, -- account (e.g. 5400 = wareneingang 19%)
|
||||
gegenkonto NVARCHAR(50) NOT NULL, -- counter account references Kreditor(kreditorId)
|
||||
bu NVARCHAR(10), -- tax code (9 = 19%vst, 8 = 7%vst, 506 = dienstleistung aus EU, 511 = dienstleistung ausserhalb EU)
|
||||
buchungsdatum DATE NOT NULL, -- booking date
|
||||
rechnungsnummer NVARCHAR(100), -- invoice number (belegfeld 1)
|
||||
buchungstext NVARCHAR(500), -- booking text (supplier/customer name + purpose)
|
||||
beleglink NVARCHAR(500) -- document link
|
||||
);
|
||||
|
||||
-- Create Konto table
|
||||
CREATE TABLE Konto (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
konto NVARCHAR(10) NOT NULL,
|
||||
name NVARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
-- Create BU table
|
||||
CREATE TABLE BU (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
bu NVARCHAR(10) NOT NULL
|
||||
name NVARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
/*
|
||||
CSV
|
||||
umsatz brutto ,
|
||||
soll / haben kz ( S = eingang, H = ausgang),
|
||||
,,,
|
||||
konto (XXXX , z.b. 5400 = wareneingang 19%),
|
||||
gegenkonto (70XXX),
|
||||
bu (9 = 19%vst , 8 = 7%vst, 506 = dienstleistung aus EU, 511 = dienstleistung ausserhalb EU),
|
||||
buchungsdatum, (MDD)
|
||||
rechnungsnummer (belegfeld 1),
|
||||
,,
|
||||
buchungstext (lierferantenname / kundenname , + verwendungszweck)
|
||||
,,,,,
|
||||
beleglink
|
||||
|
||||
|
||||
--
|
||||
nicht abziehbare vorstreuer buchen auf 5600
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IX_Users_Email ON Users(email);
|
||||
CREATE INDEX IX_Users_GoogleId ON Users(google_id);
|
||||
CREATE INDEX IX_UserPreferences_UserId ON UserPreferences(user_id);
|
||||
CREATE INDEX IX_Kreditor_IBAN ON Kreditor(iban);
|
||||
CREATE INDEX IX_Kreditor_KreditorId ON Kreditor(kreditorId);
|
||||
CREATE INDEX IX_AccountingItems_Buchungsdatum ON AccountingItems(buchungsdatum);
|
||||
CREATE INDEX IX_AccountingItems_Konto ON AccountingItems(konto);
|
||||
CREATE INDEX IX_AccountingItems_Rechnungsnummer ON AccountingItems(rechnungsnummer);
|
||||
CREATE INDEX IX_AccountingItems_SollHabenKz ON AccountingItems(soll_haben_kz);
|
||||
|
||||
-- Add FK from AccountingItems.bu -> BU(bu)
|
||||
ALTER TABLE AccountingItems
|
||||
ADD CONSTRAINT FK_AccountingItems_BU_BU
|
||||
FOREIGN KEY (bu) REFERENCES BU(bu);
|
||||
|
||||
-- Add FK from AccountingItems.gegenkonto -> Kreditor(kreditorId)
|
||||
ALTER TABLE AccountingItems
|
||||
ADD CONSTRAINT FK_AccountingItems_Gegenkonto_Kreditor
|
||||
FOREIGN KEY (gegenkonto) REFERENCES Kreditor(kreditorId);
|
||||
|
||||
-- Add FK from AccountingItems.konto -> Konto(konto)
|
||||
ALTER TABLE AccountingItems
|
||||
ADD CONSTRAINT FK_AccountingItems_Konto_Konto
|
||||
FOREIGN KEY (konto) REFERENCES Konto(konto);
|
||||
|
||||
-- Insert sample data (optional)
|
||||
-- Note: This will only work after you have real Google user data
|
||||
|
||||
@@ -558,4 +558,183 @@ router.get('/pdf/pdfobject/:kPdfObjekt', authenticateToken, async (req, res) =>
|
||||
}
|
||||
});
|
||||
|
||||
// Kreditor API endpoints
|
||||
|
||||
// Get all kreditors
|
||||
router.get('/kreditors', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { executeQuery } = require('../config/database');
|
||||
const query = `
|
||||
SELECT id, iban, name, kreditorId, created_at, updated_at
|
||||
FROM Kreditor
|
||||
WHERE is_active = 1
|
||||
ORDER BY name ASC
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query);
|
||||
res.json(result.recordset || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching kreditors:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch kreditors' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get kreditor by ID
|
||||
router.get('/kreditors/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { executeQuery } = require('../config/database');
|
||||
const { id } = req.params;
|
||||
|
||||
const query = `
|
||||
SELECT id, iban, name, kreditorId, created_at, updated_at
|
||||
FROM Kreditor
|
||||
WHERE id = @id AND is_active = 1
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query, [
|
||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
||||
]);
|
||||
|
||||
if (result.recordset.length === 0) {
|
||||
return res.status(404).json({ error: 'Kreditor not found' });
|
||||
}
|
||||
|
||||
res.json(result.recordset[0]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching kreditor:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch kreditor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create new kreditor
|
||||
router.post('/kreditors', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { executeQuery } = require('../config/database');
|
||||
const { iban, name, kreditorId } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!iban || !name || !kreditorId) {
|
||||
return res.status(400).json({ error: 'IBAN, name, and kreditorId are required' });
|
||||
}
|
||||
|
||||
// Check if kreditor with same IBAN or kreditorId already exists
|
||||
const checkQuery = `
|
||||
SELECT id FROM Kreditor
|
||||
WHERE (iban = @iban OR kreditorId = @kreditorId) AND is_active = 1
|
||||
`;
|
||||
|
||||
const checkResult = await executeQuery(checkQuery, [
|
||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId }
|
||||
]);
|
||||
|
||||
if (checkResult.recordset.length > 0) {
|
||||
return res.status(409).json({ error: 'Kreditor with this IBAN or kreditorId already exists' });
|
||||
}
|
||||
|
||||
const insertQuery = `
|
||||
INSERT INTO Kreditor (iban, name, kreditorId, created_at, updated_at)
|
||||
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.created_at, INSERTED.updated_at
|
||||
VALUES (@iban, @name, @kreditorId, GETDATE(), GETDATE())
|
||||
`;
|
||||
|
||||
const result = await executeQuery(insertQuery, [
|
||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
||||
{ name: 'name', type: 'nvarchar', value: name },
|
||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId }
|
||||
]);
|
||||
|
||||
res.status(201).json(result.recordset[0]);
|
||||
} catch (error) {
|
||||
console.error('Error creating kreditor:', error);
|
||||
res.status(500).json({ error: 'Failed to create kreditor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update kreditor
|
||||
router.put('/kreditors/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { executeQuery } = require('../config/database');
|
||||
const { id } = req.params;
|
||||
const { iban, name, kreditorId } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!iban || !name || !kreditorId) {
|
||||
return res.status(400).json({ error: 'IBAN, name, and kreditorId are required' });
|
||||
}
|
||||
|
||||
// Check if kreditor exists
|
||||
const checkQuery = `SELECT id FROM Kreditor WHERE id = @id AND is_active = 1`;
|
||||
const checkResult = await executeQuery(checkQuery, [
|
||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
||||
]);
|
||||
|
||||
if (checkResult.recordset.length === 0) {
|
||||
return res.status(404).json({ error: 'Kreditor not found' });
|
||||
}
|
||||
|
||||
// Check for conflicts with other kreditors
|
||||
const conflictQuery = `
|
||||
SELECT id FROM Kreditor
|
||||
WHERE (iban = @iban OR kreditorId = @kreditorId) AND id != @id AND is_active = 1
|
||||
`;
|
||||
|
||||
const conflictResult = await executeQuery(conflictQuery, [
|
||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId },
|
||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
||||
]);
|
||||
|
||||
if (conflictResult.recordset.length > 0) {
|
||||
return res.status(409).json({ error: 'Another kreditor with this IBAN or kreditorId already exists' });
|
||||
}
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE Kreditor
|
||||
SET iban = @iban, name = @name, kreditorId = @kreditorId, updated_at = GETDATE()
|
||||
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.created_at, INSERTED.updated_at
|
||||
WHERE id = @id
|
||||
`;
|
||||
|
||||
const result = await executeQuery(updateQuery, [
|
||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
||||
{ name: 'name', type: 'nvarchar', value: name },
|
||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId },
|
||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
||||
]);
|
||||
|
||||
res.json(result.recordset[0]);
|
||||
} catch (error) {
|
||||
console.error('Error updating kreditor:', error);
|
||||
res.status(500).json({ error: 'Failed to update kreditor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete kreditor (soft delete)
|
||||
router.delete('/kreditors/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { executeQuery } = require('../config/database');
|
||||
const { id } = req.params;
|
||||
|
||||
const query = `
|
||||
UPDATE Kreditor
|
||||
SET is_active = 0, updated_at = GETDATE()
|
||||
WHERE id = @id AND is_active = 1
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query, [
|
||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
||||
]);
|
||||
|
||||
if (result.rowsAffected[0] === 0) {
|
||||
return res.status(404).json({ error: 'Kreditor not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Kreditor deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting kreditor:', error);
|
||||
res.status(500).json({ error: 'Failed to delete kreditor' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -45,7 +45,7 @@ module.exports = {
|
||||
REACT_APP_GOOGLE_CLIENT_ID: JSON.stringify(process.env.GOOGLE_CLIENT_ID),
|
||||
},
|
||||
}),
|
||||
new ReactRefreshWebpackPlugin(),
|
||||
...(process.env.NODE_ENV === 'development' ? [new ReactRefreshWebpackPlugin()] : []),
|
||||
],
|
||||
devServer: {
|
||||
static: {
|
||||
|
||||
Reference in New Issue
Block a user