feat(buchhaltung): recurring job, budget alerts, audit endpoint, konto-typen CRUD
This commit is contained in:
@@ -115,6 +115,45 @@ class BuchhaltungController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createKontoTyp(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const data = await buchhaltungService.createKontoTyp(req.body);
|
||||||
|
res.status(201).json({ success: true, data });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('BuchhaltungController.createKontoTyp', { error });
|
||||||
|
res.status(500).json({ success: false, message: 'Kontotyp konnte nicht erstellt werden' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateKontoTyp(req: Request, res: Response): Promise<void> {
|
||||||
|
const id = parseInt(param(req, 'id'), 10);
|
||||||
|
if (isNaN(id)) { res.status(400).json({ success: false, message: 'Ungültige ID' }); return; }
|
||||||
|
try {
|
||||||
|
const data = await buchhaltungService.updateKontoTyp(id, req.body);
|
||||||
|
res.json({ success: true, data });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('BuchhaltungController.updateKontoTyp', { error });
|
||||||
|
res.status(500).json({ success: false, message: 'Kontotyp konnte nicht aktualisiert werden' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteKontoTyp(req: Request, res: Response): Promise<void> {
|
||||||
|
const id = parseInt(param(req, 'id'), 10);
|
||||||
|
if (isNaN(id)) { res.status(400).json({ success: false, message: 'Ungültige ID' }); return; }
|
||||||
|
try {
|
||||||
|
await buchhaltungService.deleteKontoTyp(id);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
const statusCode = (error as any).statusCode;
|
||||||
|
if (statusCode === 409) {
|
||||||
|
res.status(409).json({ success: false, message: (error as Error).message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.error('BuchhaltungController.deleteKontoTyp', { error });
|
||||||
|
res.status(500).json({ success: false, message: 'Kontotyp konnte nicht gelöscht werden' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Bankkonten ───────────────────────────────────────────────────────────────
|
// ── Bankkonten ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async listBankkonten(_req: Request, res: Response): Promise<void> {
|
async listBankkonten(_req: Request, res: Response): Promise<void> {
|
||||||
@@ -486,6 +525,20 @@ class BuchhaltungController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Audit ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async getAudit(req: Request, res: Response): Promise<void> {
|
||||||
|
const id = parseInt(param(req, 'transaktionId'), 10);
|
||||||
|
if (isNaN(id)) { res.status(400).json({ success: false, message: 'Ungültige ID' }); return; }
|
||||||
|
try {
|
||||||
|
const data = await buchhaltungService.getAuditByTransaktion(id);
|
||||||
|
res.json({ success: true, data });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('BuchhaltungController.getAudit', { error });
|
||||||
|
res.status(500).json({ success: false, message: 'Audit konnte nicht geladen werden' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Erstattungen ────────────────────────────────────────────────────────────
|
// ── Erstattungen ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async createErstattung(req: Request, res: Response): Promise<void> {
|
async createErstattung(req: Request, res: Response): Promise<void> {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Migration 081: Add alert_threshold to buchhaltung_konten + default setting
|
||||||
|
|
||||||
|
ALTER TABLE buchhaltung_konten
|
||||||
|
ADD COLUMN IF NOT EXISTS alert_threshold INT CHECK (alert_threshold BETWEEN 0 AND 100);
|
||||||
|
|
||||||
|
INSERT INTO buchhaltung_einstellungen (key, value)
|
||||||
|
VALUES ('default_alert_threshold', '"80"')
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
126
backend/src/jobs/buchhaltung-recurring.job.ts
Normal file
126
backend/src/jobs/buchhaltung-recurring.job.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import pool from '../config/database';
|
||||||
|
import buchhaltungService from '../services/buchhaltung.service';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
const INTERVAL_MS = 60 * 60 * 1000; // hourly
|
||||||
|
let jobInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let isRunning = false;
|
||||||
|
|
||||||
|
/** Advance a date by N months, then apply ausfuehrungstag. */
|
||||||
|
function advanceDate(base: Date, months: number, ausfuehrungstag: 'erster' | 'mitte' | 'letzter'): Date {
|
||||||
|
const d = new Date(base);
|
||||||
|
d.setMonth(d.getMonth() + months);
|
||||||
|
if (ausfuehrungstag === 'erster') {
|
||||||
|
d.setDate(1);
|
||||||
|
} else if (ausfuehrungstag === 'mitte') {
|
||||||
|
d.setDate(15);
|
||||||
|
} else {
|
||||||
|
// last day of the month
|
||||||
|
d.setMonth(d.getMonth() + 1, 0);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runRecurringCheck(): Promise<void> {
|
||||||
|
if (isRunning) {
|
||||||
|
logger.warn('BuchhaltungRecurringJob: previous run still in progress — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRunning = true;
|
||||||
|
try {
|
||||||
|
const dueResult = await pool.query(
|
||||||
|
`SELECT * FROM buchhaltung_wiederkehrend WHERE aktiv = TRUE AND naechste_ausfuehrung <= CURRENT_DATE`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dueResult.rows.length === 0) {
|
||||||
|
isRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const haushaltsjahr = await buchhaltungService.getCurrentHaushaltsjahr();
|
||||||
|
if (!haushaltsjahr) {
|
||||||
|
logger.warn('BuchhaltungRecurringJob: no open fiscal year — skipping');
|
||||||
|
isRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let processed = 0;
|
||||||
|
for (const template of dueResult.rows) {
|
||||||
|
try {
|
||||||
|
// Create transaction directly to set wiederkehrend_id
|
||||||
|
const txResult = await pool.query(
|
||||||
|
`INSERT INTO buchhaltung_transaktionen
|
||||||
|
(haushaltsjahr_id, konto_id, bankkonto_id, typ, betrag, datum, beschreibung,
|
||||||
|
empfaenger_auftraggeber, erstellt_von, wiederkehrend_id, status)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'entwurf')
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
haushaltsjahr.id,
|
||||||
|
template.konto_id,
|
||||||
|
template.bankkonto_id,
|
||||||
|
template.typ,
|
||||||
|
template.betrag,
|
||||||
|
template.naechste_ausfuehrung,
|
||||||
|
template.beschreibung,
|
||||||
|
template.empfaenger_auftraggeber,
|
||||||
|
template.erstellt_von,
|
||||||
|
template.id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
const tx = txResult.rows[0];
|
||||||
|
if (tx) {
|
||||||
|
await buchhaltungService.logAudit(tx.id, 'erstellt_wiederkehrend', { wiederkehrend_id: template.id }, template.erstellt_von);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance naechste_ausfuehrung
|
||||||
|
const monthsMap: Record<string, number> = {
|
||||||
|
monatlich: 1,
|
||||||
|
quartalsweise: 3,
|
||||||
|
halbjaehrlich: 6,
|
||||||
|
jaehrlich: 12,
|
||||||
|
};
|
||||||
|
const months = monthsMap[template.intervall] ?? 1;
|
||||||
|
const nextDate = advanceDate(new Date(template.naechste_ausfuehrung), months, template.ausfuehrungstag);
|
||||||
|
const nextDateStr = nextDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE buchhaltung_wiederkehrend SET naechste_ausfuehrung = $1 WHERE id = $2`,
|
||||||
|
[nextDateStr, template.id]
|
||||||
|
);
|
||||||
|
processed++;
|
||||||
|
} catch (templateErr) {
|
||||||
|
logger.error('BuchhaltungRecurringJob: failed to process template', {
|
||||||
|
id: template.id,
|
||||||
|
error: templateErr instanceof Error ? templateErr.message : String(templateErr),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`BuchhaltungRecurringJob: processed ${processed} recurring transactions`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('BuchhaltungRecurringJob: unexpected error', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startBuchhaltungRecurringJob(): void {
|
||||||
|
if (jobInterval !== null) {
|
||||||
|
logger.warn('BuchhaltungRecurringJob: already running — skipping duplicate start');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Delay initial run to let migrations settle
|
||||||
|
setTimeout(() => runRecurringCheck(), 60_000);
|
||||||
|
jobInterval = setInterval(() => runRecurringCheck(), INTERVAL_MS);
|
||||||
|
logger.info('Buchhaltung recurring job scheduled (hourly)');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopBuchhaltungRecurringJob(): void {
|
||||||
|
if (jobInterval !== null) {
|
||||||
|
clearInterval(jobInterval);
|
||||||
|
jobInterval = null;
|
||||||
|
}
|
||||||
|
logger.info('Buchhaltung recurring job stopped');
|
||||||
|
}
|
||||||
@@ -24,6 +24,9 @@ router.post('/haushaltsjahre/:id/close', authenticate, requirePermission('buchha
|
|||||||
|
|
||||||
// ── Konto-Typen ───────────────────────────────────────────────────────────────
|
// ── Konto-Typen ───────────────────────────────────────────────────────────────
|
||||||
router.get('/konto-typen', authenticate, requirePermission('buchhaltung:view'), buchhaltungController.listKontoTypen.bind(buchhaltungController));
|
router.get('/konto-typen', authenticate, requirePermission('buchhaltung:view'), buchhaltungController.listKontoTypen.bind(buchhaltungController));
|
||||||
|
router.post('/konto-typen', authenticate, requirePermission('buchhaltung:manage_settings'), buchhaltungController.createKontoTyp.bind(buchhaltungController));
|
||||||
|
router.patch('/konto-typen/:id', authenticate, requirePermission('buchhaltung:manage_settings'), buchhaltungController.updateKontoTyp.bind(buchhaltungController));
|
||||||
|
router.delete('/konto-typen/:id', authenticate, requirePermission('buchhaltung:manage_settings'), buchhaltungController.deleteKontoTyp.bind(buchhaltungController));
|
||||||
|
|
||||||
// ── Bankkonten ────────────────────────────────────────────────────────────────
|
// ── Bankkonten ────────────────────────────────────────────────────────────────
|
||||||
router.get('/bankkonten', authenticate, requirePermission('buchhaltung:view'), buchhaltungController.listBankkonten.bind(buchhaltungController));
|
router.get('/bankkonten', authenticate, requirePermission('buchhaltung:view'), buchhaltungController.listBankkonten.bind(buchhaltungController));
|
||||||
@@ -57,6 +60,9 @@ router.delete('/wiederkehrend/:id', authenticate, requirePermission('buchhaltung
|
|||||||
router.post('/erstattungen', authenticate, requirePermission('buchhaltung:create'), buchhaltungController.createErstattung.bind(buchhaltungController));
|
router.post('/erstattungen', authenticate, requirePermission('buchhaltung:create'), buchhaltungController.createErstattung.bind(buchhaltungController));
|
||||||
router.get('/transaktionen/:id/erstattung-links', authenticate, requirePermission('buchhaltung:view'), buchhaltungController.getErstattungLinks.bind(buchhaltungController));
|
router.get('/transaktionen/:id/erstattung-links', authenticate, requirePermission('buchhaltung:view'), buchhaltungController.getErstattungLinks.bind(buchhaltungController));
|
||||||
|
|
||||||
|
// ── Audit ──────────────────────────────────────────────────────────────────────
|
||||||
|
router.get('/audit/:transaktionId', authenticate, requirePermission('buchhaltung:view'), buchhaltungController.getAudit.bind(buchhaltungController));
|
||||||
|
|
||||||
// ── CSV Export ─────────────────────────────────────────────────────────────────
|
// ── CSV Export ─────────────────────────────────────────────────────────────────
|
||||||
router.get('/export/csv', authenticate, requirePermission('buchhaltung:export'), buchhaltungController.exportCsv.bind(buchhaltungController));
|
router.get('/export/csv', authenticate, requirePermission('buchhaltung:export'), buchhaltungController.exportCsv.bind(buchhaltungController));
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { startNotificationJob, stopNotificationJob } from './jobs/notification-g
|
|||||||
import { startReminderJob, stopReminderJob } from './jobs/reminder.job';
|
import { startReminderJob, stopReminderJob } from './jobs/reminder.job';
|
||||||
import { startIssueReminderJob, stopIssueReminderJob } from './jobs/issue-reminder.job';
|
import { startIssueReminderJob, stopIssueReminderJob } from './jobs/issue-reminder.job';
|
||||||
import { startChecklistReminderJob, stopChecklistReminderJob } from './jobs/checklist-reminder.job';
|
import { startChecklistReminderJob, stopChecklistReminderJob } from './jobs/checklist-reminder.job';
|
||||||
|
import { startBuchhaltungRecurringJob, stopBuchhaltungRecurringJob } from './jobs/buchhaltung-recurring.job';
|
||||||
import { permissionService } from './services/permission.service';
|
import { permissionService } from './services/permission.service';
|
||||||
|
|
||||||
const startServer = async (): Promise<void> => {
|
const startServer = async (): Promise<void> => {
|
||||||
@@ -40,6 +41,9 @@ const startServer = async (): Promise<void> => {
|
|||||||
// Start the checklist reminder job
|
// Start the checklist reminder job
|
||||||
startChecklistReminderJob();
|
startChecklistReminderJob();
|
||||||
|
|
||||||
|
// Start the buchhaltung recurring transaction job
|
||||||
|
startBuchhaltungRecurringJob();
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
const server = app.listen(environment.port, () => {
|
const server = app.listen(environment.port, () => {
|
||||||
logger.info('Server started successfully', {
|
logger.info('Server started successfully', {
|
||||||
@@ -66,6 +70,7 @@ const startServer = async (): Promise<void> => {
|
|||||||
stopReminderJob();
|
stopReminderJob();
|
||||||
stopIssueReminderJob();
|
stopIssueReminderJob();
|
||||||
stopChecklistReminderJob();
|
stopChecklistReminderJob();
|
||||||
|
stopBuchhaltungRecurringJob();
|
||||||
|
|
||||||
server.close(async () => {
|
server.close(async () => {
|
||||||
logger.info('HTTP server closed');
|
logger.info('HTTP server closed');
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
import pool from '../config/database';
|
import pool from '../config/database';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { permissionService } from './permission.service';
|
||||||
|
import notificationService from './notification.service';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Kategorien (Categories)
|
// Kategorien (Categories)
|
||||||
@@ -197,6 +199,58 @@ async function getAllKontoTypen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createKontoTyp(data: { bezeichnung: string; art: string; sort_order?: number }) {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO buchhaltung_konto_typen (bezeichnung, art, sort_order) VALUES ($1, $2, $3) RETURNING *`,
|
||||||
|
[data.bezeichnung, data.art, data.sort_order ?? 0]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('BuchhaltungService.createKontoTyp failed', { error });
|
||||||
|
throw new Error('Kontotyp konnte nicht erstellt werden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateKontoTyp(id: number, data: { bezeichnung?: string; art?: string; sort_order?: number }) {
|
||||||
|
try {
|
||||||
|
const fields: string[] = [];
|
||||||
|
const values: unknown[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
if (data.bezeichnung !== undefined) { fields.push(`bezeichnung = $${idx++}`); values.push(data.bezeichnung); }
|
||||||
|
if (data.art !== undefined) { fields.push(`art = $${idx++}`); values.push(data.art); }
|
||||||
|
if (data.sort_order !== undefined) { fields.push(`sort_order = $${idx++}`); values.push(data.sort_order); }
|
||||||
|
if (fields.length === 0) throw new Error('Keine Felder zum Aktualisieren');
|
||||||
|
values.push(id);
|
||||||
|
const result = await pool.query(
|
||||||
|
`UPDATE buchhaltung_konto_typen SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('BuchhaltungService.updateKontoTyp failed', { error, id });
|
||||||
|
throw new Error('Kontotyp konnte nicht aktualisiert werden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteKontoTyp(id: number) {
|
||||||
|
try {
|
||||||
|
const check = await pool.query(
|
||||||
|
`SELECT COUNT(*) FROM buchhaltung_bankkonten WHERE konto_typ_id = $1`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
if (parseInt(check.rows[0].count) > 0) {
|
||||||
|
const err = new Error('Kontotyp wird noch verwendet');
|
||||||
|
(err as any).statusCode = 409;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await pool.query(`DELETE FROM buchhaltung_konto_typen WHERE id = $1`, [id]);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('BuchhaltungService.deleteKontoTyp failed', { error, id });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Bankkonten (Bank Accounts)
|
// Bankkonten (Bank Accounts)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -785,6 +839,38 @@ async function bookTransaktion(id: number, userId: string) {
|
|||||||
[id, userId]
|
[id, userId]
|
||||||
);
|
);
|
||||||
if (result.rows[0]) await logAudit(id, 'gebucht', {}, userId);
|
if (result.rows[0]) await logAudit(id, 'gebucht', {}, userId);
|
||||||
|
|
||||||
|
// Budget alert check (non-fatal)
|
||||||
|
try {
|
||||||
|
const tx = result.rows[0];
|
||||||
|
if (tx && tx.konto_id) {
|
||||||
|
const budget = await getBudgetUtilisation(tx.konto_id);
|
||||||
|
if (budget && budget.auslastung_prozent > 0) {
|
||||||
|
const settings = await getEinstellungen();
|
||||||
|
const globalThreshold = parseInt(settings.default_alert_threshold as string) || 80;
|
||||||
|
const threshold = budget.alert_threshold ?? globalThreshold;
|
||||||
|
|
||||||
|
if (budget.auslastung_prozent >= threshold) {
|
||||||
|
const users = await permissionService.getUsersWithPermission('buchhaltung:manage_accounts');
|
||||||
|
for (const user of users) {
|
||||||
|
await notificationService.createNotification({
|
||||||
|
user_id: user.id,
|
||||||
|
typ: 'buchhaltung_budget_warnung',
|
||||||
|
titel: 'Budget-Warnung',
|
||||||
|
nachricht: `${budget.bezeichnung}: ${budget.auslastung_prozent}% ausgelastet`,
|
||||||
|
schwere: 'warnung',
|
||||||
|
link: `/buchhaltung/konto/${tx.konto_id}`,
|
||||||
|
quell_id: `budget-alert-${tx.konto_id}`,
|
||||||
|
quell_typ: 'buchhaltung_budget_warnung',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (alertErr) {
|
||||||
|
logger.warn('BuchhaltungService.bookTransaktion budget alert failed (non-fatal)', { alertErr });
|
||||||
|
}
|
||||||
|
|
||||||
return result.rows[0] || null;
|
return result.rows[0] || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('BuchhaltungService.bookTransaktion failed', { error, id });
|
logger.error('BuchhaltungService.bookTransaktion failed', { error, id });
|
||||||
@@ -1365,6 +1451,9 @@ const buchhaltungService = {
|
|||||||
updateHaushaltsjahr,
|
updateHaushaltsjahr,
|
||||||
closeHaushaltsjahr,
|
closeHaushaltsjahr,
|
||||||
getAllKontoTypen,
|
getAllKontoTypen,
|
||||||
|
createKontoTyp,
|
||||||
|
updateKontoTyp,
|
||||||
|
deleteKontoTyp,
|
||||||
getAllBankkonten,
|
getAllBankkonten,
|
||||||
getBankkontoById,
|
getBankkontoById,
|
||||||
createBankkonto,
|
createBankkonto,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
Freigabe,
|
Freigabe,
|
||||||
Kategorie,
|
Kategorie,
|
||||||
ErstattungFormData, ErstattungLinks,
|
ErstattungFormData, ErstattungLinks,
|
||||||
|
BuchhaltungAudit,
|
||||||
} from '../types/buchhaltung.types';
|
} from '../types/buchhaltung.types';
|
||||||
|
|
||||||
export const buchhaltungApi = {
|
export const buchhaltungApi = {
|
||||||
@@ -38,6 +39,17 @@ export const buchhaltungApi = {
|
|||||||
const r = await api.get('/api/buchhaltung/konto-typen');
|
const r = await api.get('/api/buchhaltung/konto-typen');
|
||||||
return r.data.data;
|
return r.data.data;
|
||||||
},
|
},
|
||||||
|
createKontoTyp: async (data: { bezeichnung: string; art: string; sort_order?: number }): Promise<KontoTyp> => {
|
||||||
|
const r = await api.post('/api/buchhaltung/konto-typen', data);
|
||||||
|
return r.data.data;
|
||||||
|
},
|
||||||
|
updateKontoTyp: async (id: number, data: Partial<{ bezeichnung: string; art: string; sort_order: number }>): Promise<KontoTyp> => {
|
||||||
|
const r = await api.patch(`/api/buchhaltung/konto-typen/${id}`, data);
|
||||||
|
return r.data.data;
|
||||||
|
},
|
||||||
|
deleteKontoTyp: async (id: number): Promise<void> => {
|
||||||
|
await api.delete(`/api/buchhaltung/konto-typen/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
// ── Bankkonten ───────────────────────────────────────────────────────────────
|
// ── Bankkonten ───────────────────────────────────────────────────────────────
|
||||||
getBankkonten: async (): Promise<Bankkonto[]> => {
|
getBankkonten: async (): Promise<Bankkonto[]> => {
|
||||||
@@ -207,4 +219,10 @@ export const buchhaltungApi = {
|
|||||||
const r = await api.get(`/api/buchhaltung/transaktionen/${transaktionId}/erstattung-links`);
|
const r = await api.get(`/api/buchhaltung/transaktionen/${transaktionId}/erstattung-links`);
|
||||||
return r.data.data;
|
return r.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Audit ─────────────────────────────────────────────────────────────────
|
||||||
|
getAudit: async (transaktionId: number): Promise<BuchhaltungAudit[]> => {
|
||||||
|
const r = await api.get(`/api/buchhaltung/audit/${transaktionId}`);
|
||||||
|
return r.data.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user