1288 lines
51 KiB
JavaScript
1288 lines
51 KiB
JavaScript
// API Routes für Nebenkostenabrechnung (Objekte, Mieter, Mietverträge, Kosten, Vorauszahlungen, Abrechnungen)
|
|
const { Pool } = require('pg');
|
|
|
|
const pool = new Pool({
|
|
host: process.env.DB_HOST || 'buchhaltung-db',
|
|
port: process.env.DB_PORT || 5432,
|
|
database: process.env.DB_NAME || 'buchhaltung',
|
|
user: process.env.DB_USER || 'postgres',
|
|
password: process.env.DB_PASSWORD || 'postgres',
|
|
});
|
|
|
|
module.exports = (app) => {
|
|
|
|
// ========== OBJEKTE ==========
|
|
|
|
// Alle Objekte
|
|
app.get('/api/objekte', async (req, res) => {
|
|
try {
|
|
const result = await pool.query('SELECT * FROM objekte ORDER BY name');
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Einzelnes Objekt
|
|
app.get('/api/objekte/:id', async (req, res) => {
|
|
try {
|
|
const result = await pool.query('SELECT * FROM objekte WHERE id = $1', [req.params.id]);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Objekt nicht gefunden' });
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Objekt erstellen
|
|
app.post('/api/objekte', async (req, res) => {
|
|
try {
|
|
const { name, adresse, plz, ort, wohnflaeche_qm, bemerkung } = req.body;
|
|
const result = await pool.query(
|
|
'INSERT INTO objekte (name, adresse, plz, ort, wohnflaeche_qm, bemerkung) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *',
|
|
[name, adresse, plz, ort, wohnflaeche_qm || 0, bemerkung]
|
|
);
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Objekt aktualisieren
|
|
app.put('/api/objekte/:id', async (req, res) => {
|
|
try {
|
|
const { name, adresse, plz, ort, wohnflaeche_qm, bemerkung } = req.body;
|
|
const result = await pool.query(
|
|
'UPDATE objekte SET name = $1, adresse = $2, plz = $3, ort = $4, wohnflaeche_qm = $5, bemerkung = $6 WHERE id = $7 RETURNING *',
|
|
[name, adresse, plz, ort, wohnflaeche_qm, bemerkung, req.params.id]
|
|
);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Objekt nicht gefunden' });
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Objekt löschen
|
|
app.delete('/api/objekte/:id', async (req, res) => {
|
|
try {
|
|
const result = await pool.query('DELETE FROM objekte WHERE id = $1 RETURNING *', [req.params.id]);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Objekt nicht gefunden' });
|
|
res.json({ success: true, deleted: result.rows[0] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ========== OBJEKTKOSTEN ==========
|
|
|
|
// Kosten für ein Objekt laden (optional gefiltert nach Jahr)
|
|
app.get('/api/objekte/:id/kosten', async (req, res) => {
|
|
try {
|
|
const { jahr } = req.query;
|
|
let query = 'SELECT * FROM objektkosten WHERE objekt_id = $1';
|
|
const params = [req.params.id];
|
|
|
|
if (jahr) {
|
|
query += ' AND jahr = $2';
|
|
params.push(jahr);
|
|
}
|
|
|
|
query += ' ORDER BY jahr DESC, kategorie ASC';
|
|
|
|
const result = await pool.query(query, params);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Kosten hinzufügen
|
|
app.post('/api/objekte/:id/kosten', async (req, res) => {
|
|
try {
|
|
const { kategorie, betrag, jahr } = req.body;
|
|
const result = await pool.query(
|
|
'INSERT INTO objektkosten (objekt_id, kategorie, betrag, jahr) VALUES ($1, $2, $3, $4) RETURNING *',
|
|
[req.params.id, kategorie, betrag, jahr]
|
|
);
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Kosten löschen
|
|
app.delete('/api/objekte/:id/kosten/:kostenId', async (req, res) => {
|
|
try {
|
|
await pool.query('DELETE FROM objektkosten WHERE id = $1 AND objekt_id = $2', [req.params.kostenId, req.params.id]);
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Mieter eines Objekts
|
|
app.get('/api/objekte/:id/mieter', async (req, res) => {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT m.*, mv.id as mietvertrag_id, mv.wohnflaeche_qm, mv.kaltmiete,
|
|
mv.nebenkosten_vorauszahlung, mv.vertragsbeginn, mv.vertragsende, mv.ist_aktuell
|
|
FROM mieter m
|
|
JOIN mietvertraege mv ON mv.mieter_id = m.id
|
|
WHERE mv.objekt_id = $1
|
|
ORDER BY m.name`,
|
|
[req.params.id]
|
|
);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ========== MIETER ==========
|
|
|
|
app.get('/api/mieter', async (req, res) => {
|
|
try {
|
|
const result = await pool.query('SELECT * FROM mieter ORDER BY name');
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/mieter/:id', async (req, res) => {
|
|
try {
|
|
const result = await pool.query('SELECT * FROM mieter WHERE id = $1', [req.params.id]);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Mieter nicht gefunden' });
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.post('/api/mieter', async (req, res) => {
|
|
try {
|
|
const { name, email, telefon, adresse } = req.body;
|
|
const result = await pool.query(
|
|
'INSERT INTO mieter (name, email, telefon, adresse) VALUES ($1, $2, $3, $4) RETURNING *',
|
|
[name, email, telefon, adresse]
|
|
);
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.put('/api/mieter/:id', async (req, res) => {
|
|
try {
|
|
const { name, email, telefon, adresse } = req.body;
|
|
const result = await pool.query(
|
|
'UPDATE mieter SET name = $1, email = $2, telefon = $3, adresse = $4 WHERE id = $5 RETURNING *',
|
|
[name, email, telefon, adresse, req.params.id]
|
|
);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Mieter nicht gefunden' });
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.delete('/api/mieter/:id', async (req, res) => {
|
|
try {
|
|
const result = await pool.query('DELETE FROM mieter WHERE id = $1 RETURNING *', [req.params.id]);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Mieter nicht gefunden' });
|
|
res.json({ success: true, deleted: result.rows[0] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Mieter mit Mietvertrag erstellen (Combined)
|
|
app.post('/api/mieter-mit-vertrag', async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const { name, email, telefon, adresse, objekt_id, wohnflaeche_qm, kaltmiete, nebenkosten_vorauszahlung, vertragsbeginn, vertragsende } = req.body;
|
|
|
|
// Mieter erstellen
|
|
const mieterResult = await client.query(
|
|
'INSERT INTO mieter (name, email, telefon, adresse) VALUES ($1, $2, $3, $4) RETURNING *',
|
|
[name, email, telefon, adresse]
|
|
);
|
|
const mieter = mieterResult.rows[0];
|
|
|
|
// Mietvertrag erstellen
|
|
const vertragResult = await client.query(
|
|
`INSERT INTO mietvertraege (objekt_id, mieter_id, wohnflaeche_qm, kaltmiete, nebenkosten_vorauszahlung, vertragsbeginn, vertragsende, ist_aktuell)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
|
|
[objekt_id, mieter.id, wohnflaeche_qm || 0, kaltmiete || 0, nebenkosten_vorauszahlung || 0, vertragsbeginn, vertragsende || null, !vertragsende]
|
|
);
|
|
|
|
await client.query('COMMIT');
|
|
res.json({ mieter, mietvertrag: vertragResult.rows[0] });
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
res.status(500).json({ error: error.message });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
// Mieter mit Mietvertrag aktualisieren (Combined)
|
|
app.put('/api/mieter/:id/mit-vertrag', async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const { name, email, telefon, adresse, objekt_id, kaltmiete, vorauszahlung, vertragsbeginn, vertragsende } = req.body;
|
|
|
|
// ist_aktuell automatisch berechnen: true wenn kein vertragsende oder vertragsende in Zukunft
|
|
const istAktuell = !vertragsende || new Date(vertragsende) >= new Date();
|
|
|
|
// Mieter aktualisieren
|
|
const mieterResult = await client.query(
|
|
'UPDATE mieter SET name = $1, email = $2, telefon = $3, adresse = $4 WHERE id = $5 RETURNING *',
|
|
[name, email, telefon, adresse, req.params.id]
|
|
);
|
|
if (mieterResult.rows.length === 0) {
|
|
await client.query('ROLLBACK');
|
|
return res.status(404).json({ error: 'Mieter nicht gefunden' });
|
|
}
|
|
|
|
// Aktiven Mietvertrag finden und aktualisieren
|
|
const vertragResult = await client.query(
|
|
`UPDATE mietvertraege SET
|
|
objekt_id = $1, kaltmiete = $2, nebenkosten_vorauszahlung = $3,
|
|
vertragsbeginn = $4, vertragsende = $5, ist_aktuell = $6
|
|
WHERE mieter_id = $7 AND (ist_aktuell = true OR id = (
|
|
SELECT id FROM mietvertraege WHERE mieter_id = $7 ORDER BY vertragsbeginn DESC LIMIT 1
|
|
))
|
|
RETURNING *`,
|
|
[objekt_id, kaltmiete, vorauszahlung, vertragsbeginn, vertragsende || null, istAktuell, req.params.id]
|
|
);
|
|
|
|
await client.query('COMMIT');
|
|
res.json({ mieter: mieterResult.rows[0], mietvertrag: vertragResult.rows[0] || null });
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
res.status(500).json({ error: error.message });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
// Mietverträge eines Mieters
|
|
app.get('/api/mieter/:id/mietvertraege', async (req, res) => {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT mv.*, o.name as objekt_name
|
|
FROM mietvertraege mv
|
|
JOIN objekte o ON o.id = mv.objekt_id
|
|
WHERE mv.mieter_id = $1
|
|
ORDER BY mv.vertragsbeginn DESC`,
|
|
[req.params.id]
|
|
);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ========== MIETVERTRÄGE ==========
|
|
|
|
app.get('/api/mietvertraege', async (req, res) => {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT mv.*, m.name as mieter_name, o.name as objekt_name
|
|
FROM mietvertraege mv
|
|
JOIN mieter m ON m.id = mv.mieter_id
|
|
JOIN objekte o ON o.id = mv.objekt_id
|
|
ORDER BY mv.vertragsbeginn DESC`
|
|
);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ========== MIETVERTRÄGE ==========
|
|
|
|
app.get('/api/mietvertraege', async (req, res) => {
|
|
try {
|
|
const { objekt_id, ist_aktuell } = req.query;
|
|
let query = `SELECT mv.*, o.name as objekt_name, o.adresse, o.plz, o.ort,
|
|
m.name as mieter_name, m.email as mieter_email
|
|
FROM mietvertraege mv
|
|
JOIN objekte o ON o.id = mv.objekt_id
|
|
JOIN mieter m ON m.id = mv.mieter_id`;
|
|
const values = [];
|
|
const conditions = [];
|
|
|
|
if (objekt_id) {
|
|
conditions.push(`mv.objekt_id = $${values.length + 1}`);
|
|
values.push(objekt_id);
|
|
}
|
|
if (ist_aktuell !== undefined) {
|
|
conditions.push(`mv.ist_aktuell = $${values.length + 1}`);
|
|
values.push(ist_aktuell === 'true');
|
|
}
|
|
|
|
if (conditions.length > 0) {
|
|
query += ' WHERE ' + conditions.join(' AND ');
|
|
}
|
|
|
|
query += ' ORDER BY mv.vertragsbeginn DESC';
|
|
|
|
const result = await pool.query(query, values);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.post('/api/mietvertraege', async (req, res) => {
|
|
try {
|
|
const { objekt_id, mieter_id, wohnflaeche_qm, kaltmiete, nebenkosten_vorauszahlung, vertragsbeginn } = req.body;
|
|
const result = await pool.query(
|
|
`INSERT INTO mietvertraege (objekt_id, mieter_id, wohnflaeche_qm, kaltmiete, nebenkosten_vorauszahlung, vertragsbeginn)
|
|
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
|
|
[objekt_id, mieter_id, wohnflaeche_qm, kaltmiete, nebenkosten_vorauszahlung || 0, vertragsbeginn]
|
|
);
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.put('/api/mietvertraege/:id', async (req, res) => {
|
|
try {
|
|
const { wohnflaeche_qm, kaltmiete, nebenkosten_vorauszahlung, vertragsende, ist_aktuell } = req.body;
|
|
const result = await pool.query(
|
|
`UPDATE mietvertraege SET wohnflaeche_qm = $1, kaltmiete = $2, nebenkosten_vorauszahlung = $3,
|
|
vertragsende = $4, ist_aktuell = $5 WHERE id = $6 RETURNING *`,
|
|
[wohnflaeche_qm, kaltmiete, nebenkosten_vorauszahlung, vertragsende, ist_aktuell !== undefined ? ist_aktuell : true, req.params.id]
|
|
);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Mietvertrag nicht gefunden' });
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.post('/api/mietvertraege/:id/beenden', async (req, res) => {
|
|
try {
|
|
const { vertragsende } = req.body;
|
|
const result = await pool.query(
|
|
`UPDATE mietvertraege SET vertragsende = $1, ist_aktuell = false WHERE id = $2 RETURNING *`,
|
|
[vertragsende, req.params.id]
|
|
);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Mietvertrag nicht gefunden' });
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.delete('/api/mietvertraege/:id', async (req, res) => {
|
|
try {
|
|
const result = await pool.query('DELETE FROM mietvertraege WHERE id = $1 RETURNING *', [req.params.id]);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Mietvertrag nicht gefunden' });
|
|
res.json({ success: true, deleted: result.rows[0] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ========== OBJEKTKOSTEN ==========
|
|
|
|
app.get('/api/objektkosten', async (req, res) => {
|
|
try {
|
|
const { objekt_id, jahr } = req.query;
|
|
let query = 'SELECT ok.*, o.name as objekt_name FROM objektkosten ok JOIN objekte o ON o.id = ok.objekt_id';
|
|
const values = [];
|
|
const conditions = [];
|
|
|
|
if (objekt_id) {
|
|
conditions.push(`ok.objekt_id = $${values.length + 1}`);
|
|
values.push(objekt_id);
|
|
}
|
|
if (jahr) {
|
|
conditions.push(`ok.jahr = $${values.length + 1}`);
|
|
values.push(parseInt(jahr));
|
|
}
|
|
|
|
if (conditions.length > 0) {
|
|
query += ' WHERE ' + conditions.join(' AND ');
|
|
}
|
|
|
|
query += ' ORDER BY ok.jahr DESC, ok.kategorie, ok.datum';
|
|
|
|
const result = await pool.query(query, values);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.post('/api/objektkosten', async (req, res) => {
|
|
try {
|
|
const { objekt_id, kategorie, bezeichnung, betrag, datum, jahr, verteilung, bemerkung } = req.body;
|
|
const result = await pool.query(
|
|
`INSERT INTO objektkosten (objekt_id, kategorie, bezeichnung, betrag, datum, jahr, verteilung, bemerkung)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
|
|
[objekt_id, kategorie, bezeichnung, betrag, datum, jahr || new Date().getFullYear(), verteilung || 'qm', bemerkung]
|
|
);
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.put('/api/objektkosten/:id', async (req, res) => {
|
|
try {
|
|
const { kategorie, bezeichnung, betrag, datum, jahr, verteilung, bemerkung } = req.body;
|
|
const result = await pool.query(
|
|
`UPDATE objektkosten SET kategorie = $1, bezeichnung = $2, betrag = $3, datum = $4, jahr = $5, verteilung = $6, bemerkung = $7
|
|
WHERE id = $8 RETURNING *`,
|
|
[kategorie, bezeichnung, betrag, datum, jahr, verteilung, bemerkung, req.params.id]
|
|
);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Kosten nicht gefunden' });
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.delete('/api/objektkosten/:id', async (req, res) => {
|
|
try {
|
|
const result = await pool.query('DELETE FROM objektkosten WHERE id = $1 RETURNING *', [req.params.id]);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Kosten nicht gefunden' });
|
|
res.json({ success: true, deleted: result.rows[0] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ========== VORAUSZAHLUNGEN ==========
|
|
|
|
app.get('/api/vorauszahlungen', async (req, res) => {
|
|
try {
|
|
const { objekt_id, mieter_id, jahr } = req.query;
|
|
let query = `SELECT v.*, o.name as objekt_name, m.name as mieter_name
|
|
FROM vorauszahlungen v
|
|
JOIN objekte o ON o.id = v.objekt_id
|
|
JOIN mieter m ON m.id = v.mieter_id`;
|
|
const values = [];
|
|
const conditions = [];
|
|
|
|
if (objekt_id) {
|
|
conditions.push(`v.objekt_id = $${values.length + 1}`);
|
|
values.push(objekt_id);
|
|
}
|
|
if (mieter_id) {
|
|
conditions.push(`v.mieter_id = $${values.length + 1}`);
|
|
values.push(mieter_id);
|
|
}
|
|
if (jahr) {
|
|
conditions.push(`v.jahr = $${values.length + 1}`);
|
|
values.push(parseInt(jahr));
|
|
}
|
|
|
|
if (conditions.length > 0) {
|
|
query += ' WHERE ' + conditions.join(' AND ');
|
|
}
|
|
|
|
query += ' ORDER BY v.jahr DESC, v.monat';
|
|
|
|
const result = await pool.query(query, values);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.post('/api/vorauszahlungen', async (req, res) => {
|
|
try {
|
|
const { objekt_id, mieter_id, jahr, monat, betrag, bezahlt_am, bemerkung } = req.body;
|
|
const result = await pool.query(
|
|
`INSERT INTO vorauszahlungen (objekt_id, mieter_id, jahr, monat, betrag, bezahlt_am, bemerkung)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
|
[objekt_id, mieter_id, jahr, monat, betrag, bezahlt_am, bemerkung]
|
|
);
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Bulk: Vorauszahlungen für Jahr erstellen
|
|
app.post('/api/vorauszahlungen/bulk', async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
const { objektId, mieterId, jahr, monatlicherBetrag } = req.body;
|
|
const results = [];
|
|
|
|
for (let monat = 1; monat <= 12; monat++) {
|
|
const result = await client.query(
|
|
`INSERT INTO vorauszahlungen (objekt_id, mieter_id, jahr, monat, betrag)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (objekt_id, mieter_id, jahr, monat)
|
|
DO UPDATE SET betrag = $5
|
|
RETURNING *`,
|
|
[objektId, mieterId, jahr, monat, monatlicherBetrag]
|
|
);
|
|
results.push(result.rows[0]);
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
res.json({ success: true, created: results.length, vorauszahlungen: results });
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
res.status(500).json({ error: error.message });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
app.put('/api/vorauszahlungen/:id', async (req, res) => {
|
|
try {
|
|
const { betrag, bezahlt_am, bemerkung } = req.body;
|
|
const result = await pool.query(
|
|
`UPDATE vorauszahlungen SET betrag = $1, bezahlt_am = $2, bemerkung = $3 WHERE id = $4 RETURNING *`,
|
|
[betrag, bezahlt_am, bemerkung, req.params.id]
|
|
);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Vorauszahlung nicht gefunden' });
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.delete('/api/vorauszahlungen/:id', async (req, res) => {
|
|
try {
|
|
const result = await pool.query('DELETE FROM vorauszahlungen WHERE id = $1 RETURNING *', [req.params.id]);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Vorauszahlung nicht gefunden' });
|
|
res.json({ success: true, deleted: result.rows[0] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ========== VERMIETUNGSBILANZ ==========
|
|
|
|
// Hilfsfunktion: Berechne überlappende Monate zwischen zwei Zeiträumen
|
|
function calculateOverlappingMonths(startA, endA, startB, endB) {
|
|
const start = new Date(Math.max(startA.getTime(), startB.getTime()));
|
|
const end = new Date(Math.min(endA.getTime(), endB.getTime()));
|
|
|
|
if (start > end) return 0;
|
|
|
|
// Monate berechnen (inklusive Start- und Endmonat)
|
|
const months = (end.getFullYear() - start.getFullYear()) * 12 +
|
|
(end.getMonth() - start.getMonth()) + 1;
|
|
return Math.max(0, months);
|
|
}
|
|
|
|
// Einnahmen und Ausgaben pro Objekt für Vermietungs-Bilanz
|
|
app.get('/api/vermietungsbilanz', async (req, res) => {
|
|
try {
|
|
const { jahr = new Date().getFullYear() } = req.query;
|
|
|
|
const yearStart = new Date(jahr, 0, 1); // 1. Januar
|
|
const yearEnd = new Date(jahr, 11, 31); // 31. Dezember
|
|
|
|
// Alle Objekte laden
|
|
const objekteResult = await pool.query('SELECT id, name FROM objekte ORDER BY name');
|
|
|
|
const bilanzDaten = [];
|
|
let gesamtEinnahmen = 0;
|
|
let gesamtAusgaben = 0;
|
|
|
|
for (const objekt of objekteResult.rows) {
|
|
// === EINNAHMEN: Alle Mietverträge mit Zeitraumberechnung ===
|
|
// Mietverträge laden die im Jahr aktiv waren
|
|
const vertraegeResult = await pool.query(`
|
|
SELECT mv.*, m.name as mieter_name
|
|
FROM mietvertraege mv
|
|
JOIN mieter m ON m.id = mv.mieter_id
|
|
WHERE mv.objekt_id = $1
|
|
AND mv.vertragsbeginn <= $2
|
|
AND (mv.vertragsende IS NULL OR mv.vertragsende >= $3)
|
|
`, [objekt.id, yearEnd, yearStart]);
|
|
|
|
// Einnahmen pro Vertrag berechnen
|
|
let objektEinnahmen = 0;
|
|
const einnahmenDetails = [];
|
|
|
|
for (const vertrag of vertraegeResult.rows) {
|
|
const vertragsBeginn = new Date(vertrag.vertragsbeginn);
|
|
const vertragsEnde = vertrag.vertragsende ? new Date(vertrag.vertragsende) : null;
|
|
|
|
// Überlappende Monate berechnen
|
|
const effectiveStart = vertragsBeginn > yearStart ? vertragsBeginn : yearStart;
|
|
const effectiveEnd = (vertragsEnde && vertragsEnde < yearEnd) ? vertragsEnde : yearEnd;
|
|
|
|
const months = calculateOverlappingMonths(
|
|
vertragsBeginn, vertragsEnde || new Date(2099, 11, 31),
|
|
yearStart, yearEnd
|
|
);
|
|
|
|
const kaltmiete = parseFloat(vertrag.kaltmiete) || 0;
|
|
const vertragEinnahmen = kaltmiete * months;
|
|
objektEinnahmen += vertragEinnahmen;
|
|
|
|
einnahmenDetails.push({
|
|
mieter_id: vertrag.mieter_id,
|
|
mieter_name: vertrag.mieter_name,
|
|
vertragsbeginn: vertrag.vertragsbeginn,
|
|
vertragsende: vertrag.vertragsende,
|
|
kaltmiete_monat: kaltmiete,
|
|
monate: months,
|
|
summe: Math.round(vertragEinnahmen * 100) / 100
|
|
});
|
|
}
|
|
|
|
// === AUSGABEN: Nebenkosten für das Jahr ===
|
|
const ausgabenResult = await pool.query(`
|
|
SELECT COALESCE(SUM(betrag), 0) as nebenkosten
|
|
FROM objektkosten
|
|
WHERE objekt_id = $1 AND jahr = $2
|
|
`, [objekt.id, jahr]);
|
|
const nebenkosten = parseFloat(ausgabenResult.rows[0].nebenkosten);
|
|
|
|
// Detaillierte Ausgaben nach Kategorie
|
|
const kategorienResult = await pool.query(`
|
|
SELECT kategorie, SUM(betrag) as betrag
|
|
FROM objektkosten
|
|
WHERE objekt_id = $1 AND jahr = $2
|
|
GROUP BY kategorie
|
|
ORDER BY SUM(betrag) DESC
|
|
`, [objekt.id, jahr]);
|
|
|
|
// Detaillierte Ausgaben für Modal
|
|
const ausgabenDetailsResult = await pool.query(`
|
|
SELECT id, kategorie, bezeichnung, betrag, datum
|
|
FROM objektkosten
|
|
WHERE objekt_id = $1 AND jahr = $2
|
|
ORDER BY betrag DESC
|
|
`, [objekt.id, jahr]);
|
|
|
|
const bilanz = objektEinnahmen - nebenkosten;
|
|
gesamtEinnahmen += objektEinnahmen;
|
|
gesamtAusgaben += nebenkosten;
|
|
|
|
// Anzahl aktiver Mieter
|
|
const aktiveMieter = einnahmenDetails.length;
|
|
|
|
bilanzDaten.push({
|
|
objekt_id: objekt.id,
|
|
objekt_name: objekt.name,
|
|
mieteinnahmen: Math.round(objektEinnahmen * 100) / 100,
|
|
nebenkosten: Math.round(nebenkosten * 100) / 100,
|
|
bilanz: Math.round(bilanz * 100) / 100,
|
|
anzahl_mieter: aktiveMieter,
|
|
kategorien: kategorienResult.rows,
|
|
// Details für Drill-down
|
|
einnahmen_details: einnahmenDetails,
|
|
ausgaben_details: ausgabenDetailsResult.rows
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
jahr: parseInt(jahr),
|
|
objekte: bilanzDaten,
|
|
gesamt_einnahmen: Math.round(gesamtEinnahmen * 100) / 100,
|
|
gesamt_ausgaben: Math.round(gesamtAusgaben * 100) / 100,
|
|
gesamt_bilanz: Math.round((gesamtEinnahmen - gesamtAusgaben) * 100) / 100
|
|
});
|
|
} catch (error) {
|
|
console.error('Vermietungsbilanz Error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Alias für Frontend-Kompatibilität mit DETAILS
|
|
app.get('/api/vermietung/bilanz', async (req, res) => {
|
|
try {
|
|
const { jahr = new Date().getFullYear() } = req.query;
|
|
const yearStart = new Date(jahr, 0, 1);
|
|
const yearEnd = new Date(jahr, 11, 31);
|
|
|
|
// Alle Objekte laden
|
|
const objekteResult = await pool.query('SELECT id, name, adresse FROM objekte ORDER BY name');
|
|
|
|
const bilanzDaten = [];
|
|
let gesamtEinnahmen = 0;
|
|
let gesamtAusgaben = 0;
|
|
|
|
for (const objekt of objekteResult.rows) {
|
|
// === EINNAHMEN: Detaillierte Abfrage ===
|
|
const vertraegeResult = await pool.query(`
|
|
SELECT mv.*, m.name as mieter_name
|
|
FROM mietvertraege mv
|
|
JOIN mieter m ON m.id = mv.mieter_id
|
|
WHERE mv.objekt_id = $1
|
|
AND mv.vertragsbeginn <= $2
|
|
AND (mv.vertragsende IS NULL OR mv.vertragsende >= $3)
|
|
ORDER BY m.name
|
|
`, [objekt.id, yearEnd, yearStart]);
|
|
|
|
// Einnahmen berechnen mit Detail-Tracking
|
|
let objektEinnahmen = 0;
|
|
const einnahmenDetails = [];
|
|
|
|
for (const vertrag of vertraegeResult.rows) {
|
|
const vertragsBeginn = new Date(vertrag.vertragsbeginn);
|
|
const vertragsEnde = vertrag.vertragsende ? new Date(vertrag.vertragsende) : null;
|
|
|
|
// Überlappende Monate berechnen
|
|
const effectiveStart = vertragsBeginn > yearStart ? vertragsBeginn : yearStart;
|
|
const effectiveEnd = (vertragsEnde && vertragsEnde < yearEnd) ? vertragsEnde : yearEnd;
|
|
|
|
let monate = 0;
|
|
if (effectiveStart <= effectiveEnd) {
|
|
monate = (effectiveEnd.getFullYear() - effectiveStart.getFullYear()) * 12 +
|
|
(effectiveEnd.getMonth() - effectiveStart.getMonth()) + 1;
|
|
}
|
|
|
|
const kaltmiete = parseFloat(vertrag.kaltmiete) || 0;
|
|
const vertragEinnahmen = kaltmiete * monate;
|
|
objektEinnahmen += vertragEinnahmen;
|
|
|
|
// Formatierter Zeitraum
|
|
const zeitraumVon = effectiveStart.toLocaleDateString('de-DE', { month: 'short' });
|
|
const zeitraumBis = effectiveEnd.toLocaleDateString('de-DE', { month: 'short' });
|
|
const zeitraum = monate === 12 ? 'Jan-Dez' : `${zeitraumVon}-${zeitraumBis}`;
|
|
|
|
einnahmenDetails.push({
|
|
mieter: vertrag.mieter_name,
|
|
zeitraum: zeitraum,
|
|
monate: monate,
|
|
kaltmiete: kaltmiete,
|
|
summe: Math.round(vertragEinnahmen * 100) / 100
|
|
});
|
|
}
|
|
|
|
// === AUSGABEN: Detaillierte Abfrage ===
|
|
const kostenResult = await pool.query(`
|
|
SELECT kategorie, bezeichnung, betrag, datum
|
|
FROM objektkosten
|
|
WHERE objekt_id = $1 AND jahr = $2
|
|
ORDER BY kategorie, bezeichnung
|
|
`, [objekt.id, jahr]);
|
|
|
|
const ausgabenDetails = kostenResult.rows.map(k => ({
|
|
kategorie: k.kategorie,
|
|
bezeichnung: k.bezeichnung,
|
|
betrag: parseFloat(k.betrag) || 0,
|
|
datum: k.datum
|
|
}));
|
|
|
|
const objektAusgaben = ausgabenDetails.reduce((sum, k) => sum + k.betrag, 0);
|
|
const bilanz = objektEinnahmen - objektAusgaben;
|
|
const rendite = objektEinnahmen > 0 ? ((bilanz / objektEinnahmen) * 100) : 0;
|
|
|
|
gesamtEinnahmen += objektEinnahmen;
|
|
gesamtAusgaben += objektAusgaben;
|
|
|
|
bilanzDaten.push({
|
|
id: objekt.id,
|
|
name: objekt.name,
|
|
adresse: objekt.adresse,
|
|
einnahmen: Math.round(objektEinnahmen * 100) / 100,
|
|
ausgaben: Math.round(objektAusgaben * 100) / 100,
|
|
bilanz: Math.round(bilanz * 100) / 100,
|
|
rendite: Math.round(rendite * 10) / 10,
|
|
anzahl_mieter: einnahmenDetails.length,
|
|
details: {
|
|
einnahmen: einnahmenDetails,
|
|
ausgaben: ausgabenDetails
|
|
}
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
jahr: parseInt(jahr),
|
|
objekte: bilanzDaten,
|
|
gesamt: {
|
|
einnahmen: Math.round(gesamtEinnahmen * 100) / 100,
|
|
ausgaben: Math.round(gesamtAusgaben * 100) / 100,
|
|
bilanz: Math.round((gesamtEinnahmen - gesamtAusgaben) * 100) / 100,
|
|
rendite: gesamtEinnahmen > 0 ? Math.round(((gesamtEinnahmen - gesamtAusgaben) / gesamtEinnahmen) * 100 * 10) / 10 : 0
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Vermietung Bilanz Error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ========== NEBENKOSTENABRECHNUNG ==========
|
|
|
|
// Übersicht
|
|
app.get('/api/nebenkostenabrechnung/uebersicht', async (req, res) => {
|
|
try {
|
|
const { jahr = new Date().getFullYear() } = req.query;
|
|
|
|
// Alle Objekte mit Summen
|
|
const result = await pool.query(`
|
|
SELECT o.*,
|
|
COALESCE(k.gesamt_kosten, 0) as gesamt_kosten,
|
|
COALESCE(v.gesamt_vorauszahlungen, 0) as gesamt_vorauszahlungen,
|
|
COALESCE(m.anzahl_mieter, 0) as anzahl_mieter
|
|
FROM objekte o
|
|
LEFT JOIN (
|
|
SELECT objekt_id, SUM(betrag) as gesamt_kosten
|
|
FROM objektkosten WHERE jahr = $1 GROUP BY objekt_id
|
|
) k ON k.objekt_id = o.id
|
|
LEFT JOIN (
|
|
SELECT objekt_id, SUM(betrag) as gesamt_vorauszahlungen
|
|
FROM vorauszahlungen WHERE jahr = $1 GROUP BY objekt_id
|
|
) v ON v.objekt_id = o.id
|
|
LEFT JOIN (
|
|
SELECT objekt_id, COUNT(*) as anzahl_mieter
|
|
FROM mietvertraege WHERE ist_aktuell = true GROUP BY objekt_id
|
|
) m ON m.objekt_id = o.id
|
|
ORDER BY o.name
|
|
`, [jahr]);
|
|
|
|
res.json({ jahr: parseInt(jahr), objekte: result.rows });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Abrechnung Vorschau (Berechnung)
|
|
app.post('/api/nebenkostenabrechnung/vorschau', async (req, res) => {
|
|
try {
|
|
const { objektId, jahr, zeitraumVon, zeitraumBis } = req.body;
|
|
|
|
// Objekt-Daten
|
|
const objektResult = await pool.query('SELECT * FROM objekte WHERE id = $1', [objektId]);
|
|
if (objektResult.rows.length === 0) return res.status(404).json({ error: 'Objekt nicht gefunden' });
|
|
const objekt = objektResult.rows[0];
|
|
|
|
// Alle Kosten des Objekts im Jahr
|
|
const kostenResult = await pool.query(
|
|
'SELECT * FROM objektkosten WHERE objekt_id = $1 AND jahr = $2 ORDER BY kategorie',
|
|
[objektId, jahr]
|
|
);
|
|
|
|
// Aktuelle Mieter mit Mietverträgen
|
|
const mieterResult = await pool.query(`
|
|
SELECT mv.*, m.name as mieter_name, m.email as mieter_email
|
|
FROM mietvertraege mv
|
|
JOIN mieter m ON m.id = mv.mieter_id
|
|
WHERE mv.objekt_id = $1 AND mv.ist_aktuell = true
|
|
`, [objektId]);
|
|
|
|
// Berechnung
|
|
const gesamtKosten = kostenResult.rows.reduce((sum, k) => sum + parseFloat(k.betrag), 0);
|
|
const gesamtFlaeche = mieterResult.rows.reduce((sum, m) => sum + parseFloat(m.wohnflaeche_qm), 0);
|
|
|
|
// Tage im Abrechnungszeitraum
|
|
const vonDatum = new Date(zeitraumVon);
|
|
const bisDatum = new Date(zeitraumBis);
|
|
const tageGesamt = Math.ceil((bisDatum - vonDatum) / (1000 * 60 * 60 * 24)) + 1;
|
|
|
|
// Pro Mieter berechnen
|
|
const berechnungen = [];
|
|
|
|
for (const mietvertrag of mieterResult.rows) {
|
|
// Vorauszahlungen dieses Mieters
|
|
const vorauszahlungenResult = await pool.query(
|
|
'SELECT SUM(betrag) as summe FROM vorauszahlungen WHERE objekt_id = $1 AND mieter_id = $2 AND jahr = $3',
|
|
[objektId, mietvertrag.mieter_id, jahr]
|
|
);
|
|
const summeVorauszahlungen = parseFloat(vorauszahlungenResult.rows[0]?.summe || 0);
|
|
|
|
// Anteil berechnen (pro-rata bei Mieterwechsel möglich)
|
|
const anteilQm = parseFloat(mietvertrag.wohnflaeche_qm);
|
|
const anteilTage = tageGesamt; // Hier könnte differenzierte Logik für Ein-/Auszug stehen
|
|
const anteilKosten = gesamtKosten * (anteilQm / gesamtFlaeche);
|
|
|
|
const ergebnis = anteilKosten - summeVorauszahlungen;
|
|
|
|
berechnungen.push({
|
|
mietvertrag_id: mietvertrag.id,
|
|
mieter_id: mietvertrag.mieter_id,
|
|
mieter_name: mietvertrag.mieter_name,
|
|
anteil_qm: anteilQm,
|
|
anteil_tage: anteilTage,
|
|
anteil_kosten: Math.round(anteilKosten * 100) / 100,
|
|
summe_vorauszahlungen: summeVorauszahlungen,
|
|
ergebnis: Math.round(ergebnis * 100) / 100,
|
|
ist_nachzahlung: ergebnis > 0,
|
|
betrag_nachzahlung: ergebnis > 0 ? ergebnis : 0,
|
|
betrag_gutschrift: ergebnis < 0 ? Math.abs(ergebnis) : 0
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
objekt,
|
|
jahr,
|
|
zeitraum_von: zeitraumVon,
|
|
zeitraum_bis: zeitraumBis,
|
|
tage_gesamt: tageGesamt,
|
|
gesamt_kosten: gesamtKosten,
|
|
gesamt_flaeche: gesamtFlaeche,
|
|
kosten: kostenResult.rows,
|
|
mieter: mieterResult.rows,
|
|
berechnungen
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Abrechnung speichern
|
|
app.post('/api/nebenkostenabrechnung', async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const { objekt_id, jahr, zeitraum_von, zeitraum_bis, ist_entwurf, berechnungen } = req.body;
|
|
|
|
// Bestehende Abrechnung prüfen/löschen
|
|
await client.query(
|
|
'DELETE FROM nebenkostenabrechnungen WHERE objekt_id = $1 AND jahr = $2',
|
|
[objekt_id, jahr]
|
|
);
|
|
|
|
// Neue Abrechnung erstellen
|
|
const abrechnungResult = await client.query(
|
|
`INSERT INTO nebenkostenabrechnungen (objekt_id, jahr, zeitraum_von, zeitraum_bis, ist_entwurf)
|
|
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
|
|
[objekt_id, jahr, zeitraum_von, zeitraum_bis, ist_entwurf !== false]
|
|
);
|
|
const abrechnung = abrechnungResult.rows[0];
|
|
|
|
// Positionen speichern
|
|
for (const pos of berechnungen) {
|
|
await client.query(
|
|
`INSERT INTO abrechnungspositionen
|
|
(abrechnung_id, mieter_id, mietvertrag_id, anteil_qm, anteil_tage, anteil_kosten, summe_vorauszahlungen, ergebnis)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
[abrechnung.id, pos.mieter_id, pos.mietvertrag_id, pos.anteil_qm, pos.anteil_tage,
|
|
pos.anteil_kosten, pos.summe_vorauszahlungen, pos.ergebnis]
|
|
);
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
res.json({ success: true, abrechnung: abrechnungResult.rows[0] });
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
res.status(500).json({ error: error.message });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
// Alle Abrechnungen
|
|
app.get('/api/nebenkostenabrechnung', async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT na.*, o.name as objekt_name, o.adresse, o.plz, o.ort
|
|
FROM nebenkostenabrechnungen na
|
|
JOIN objekte o ON o.id = na.objekt_id
|
|
ORDER BY na.jahr DESC, o.name
|
|
`);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Alias für Frontend (Plural)
|
|
app.get('/api/nebenkostenabrechnungen', async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT na.*, o.name as objekt_name, o.adresse, o.plz, o.ort
|
|
FROM nebenkostenabrechnungen na
|
|
JOIN objekte o ON o.id = na.objekt_id
|
|
ORDER BY na.jahr DESC, o.name
|
|
`);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Einzelne Abrechnung mit Positionen
|
|
app.get('/api/nebenkostenabrechnung/:id', async (req, res) => {
|
|
try {
|
|
const abrechnungResult = await pool.query(`
|
|
SELECT na.*, o.name as objekt_name, o.adresse, o.plz, o.ort
|
|
FROM nebenkostenabrechnungen na
|
|
JOIN objekte o ON o.id = na.objekt_id
|
|
WHERE na.id = $1
|
|
`, [req.params.id]);
|
|
|
|
if (abrechnungResult.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Abrechnung nicht gefunden' });
|
|
}
|
|
|
|
const positionenResult = await pool.query(`
|
|
SELECT ap.*, m.name as mieter_name, m.email as mieter_email
|
|
FROM abrechnungspositionen ap
|
|
JOIN mieter m ON m.id = ap.mieter_id
|
|
WHERE ap.abrechnung_id = $1
|
|
`, [req.params.id]);
|
|
|
|
const kostenResult = await pool.query(`
|
|
SELECT * FROM objektkosten
|
|
WHERE objekt_id = $1 AND jahr = $2
|
|
`, [abrechnungResult.rows[0].objekt_id, abrechnungResult.rows[0].jahr]);
|
|
|
|
res.json({
|
|
abrechnung: abrechnungResult.rows[0],
|
|
positionen: positionenResult.rows,
|
|
kosten: kostenResult.rows
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Status aktualisieren (Entwurf/Final)
|
|
app.put('/api/nebenkostenabrechnung/:id/status', async (req, res) => {
|
|
try {
|
|
const { ist_entwurf } = req.body;
|
|
const result = await pool.query(
|
|
'UPDATE nebenkostenabrechnungen SET ist_entwurf = $1 WHERE id = $2 RETURNING *',
|
|
[ist_entwurf, req.params.id]
|
|
);
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Abrechnung nicht gefunden' });
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// PDF Export - Privat (René Täger) - EINE PDF pro Mieter
|
|
app.post('/api/nebenkostenabrechnung/:id/pdf', async (req, res) => {
|
|
try {
|
|
const { PDFDocument, rgb, StandardFonts } = require('pdf-lib');
|
|
|
|
const abrechnungResult = await pool.query(`
|
|
SELECT na.*, o.name as objekt_name, o.adresse, o.plz, o.ort, o.wohnflaeche_qm as objekt_flaeche
|
|
FROM nebenkostenabrechnungen na
|
|
JOIN objekte o ON o.id = na.objekt_id
|
|
WHERE na.id = $1
|
|
`, [req.params.id]);
|
|
|
|
if (abrechnungResult.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Abrechnung nicht gefunden' });
|
|
}
|
|
|
|
const abrechnung = abrechnungResult.rows[0];
|
|
|
|
// ALLE Positionen laden
|
|
const positionenResult = await pool.query(`
|
|
SELECT ap.*, m.name as mieter_name, m.email as mieter_email, m.adresse as mieter_adresse
|
|
FROM abrechnungspositionen ap
|
|
JOIN mieter m ON m.id = ap.mieter_id
|
|
WHERE ap.abrechnung_id = $1
|
|
`, [req.params.id]);
|
|
|
|
const kostenResult = await pool.query(`
|
|
SELECT * FROM objektkosten
|
|
WHERE objekt_id = $1 AND jahr = $2 ORDER BY kategorie
|
|
`, [abrechnung.objekt_id, abrechnung.jahr]);
|
|
|
|
// Berechnungsgrundlagen
|
|
const gesamtKosten = kostenResult.rows.reduce((sum, k) => sum + parseFloat(k.betrag), 0);
|
|
const gesamtFlaeche = parseFloat(abrechnung.objekt_flaeche || 95);
|
|
const kostenProQm = gesamtKosten / gesamtFlaeche;
|
|
|
|
// PDF erstellen
|
|
const pdfDoc = await PDFDocument.create();
|
|
const width = 595.28;
|
|
const height = 841.89;
|
|
const margin = 50;
|
|
|
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
|
|
|
const istEntwurf = req.query.entwurf === 'true';
|
|
const heute = new Date().toLocaleDateString('de-DE');
|
|
|
|
// Hilfsfunktion für Trennlinie
|
|
function drawLine(page, yPos) {
|
|
page.drawLine({
|
|
start: { x: margin, y: yPos },
|
|
end: { x: width - margin, y: yPos },
|
|
thickness: 0.5,
|
|
color: rgb(0.8, 0.8, 0.8)
|
|
});
|
|
}
|
|
|
|
// ========== FÜR JEDEN MIETER EINE SEITE ==========
|
|
for (const pos of positionenResult.rows) {
|
|
const mieterName = pos.mieter_name;
|
|
const mieterAdresse = pos.mieter_adresse || '';
|
|
const mieterFlaeche = parseFloat(pos.anteil_qm);
|
|
const anteilKosten = parseFloat(pos.anteil_kosten);
|
|
const vorauszahlungen = parseFloat(pos.summe_vorauszahlungen);
|
|
const ergebnis = parseFloat(pos.ergebnis);
|
|
|
|
// Neue Seite für diesen Mieter
|
|
const page = pdfDoc.addPage([width, height]);
|
|
let y = height - 50;
|
|
|
|
// ========== ABSENDER (oben links) ==========
|
|
page.drawText('René Täger', { x: margin, y, size: 10, font: fontBold });
|
|
y -= 14;
|
|
page.drawText('Zingelstraße 7', { x: margin, y, size: 9, font });
|
|
y -= 14;
|
|
page.drawText('25554 Wilster', { x: margin, y, size: 9, font });
|
|
y -= 30;
|
|
|
|
// Trennlinie
|
|
drawLine(page, y);
|
|
y -= 30;
|
|
|
|
// ========== EMPFÄNGER (nur dieser Mieter) ==========
|
|
page.drawText(mieterName, { x: margin, y, size: 11, font: fontBold });
|
|
y -= 16;
|
|
if (mieterAdresse) {
|
|
const adressZeilen = mieterAdresse.split('\n');
|
|
for (const zeile of adressZeilen) {
|
|
page.drawText(zeile, { x: margin, y, size: 10, font });
|
|
y -= 14;
|
|
}
|
|
}
|
|
y -= 40;
|
|
|
|
// ========== DATUM + BETREFF ==========
|
|
page.drawText(`Wilster, den ${heute}`, { x: width - margin - 150, y: height - 120, size: 9, font });
|
|
|
|
page.drawText('Nebenkostenabrechnung', { x: margin, y, size: 16, font: fontBold, color: rgb(0.2, 0.4, 0.6) });
|
|
y -= 25;
|
|
|
|
if (istEntwurf) {
|
|
page.drawText('ENTWURF', { x: width - 140, y: y + 15, size: 20, font: fontBold, color: rgb(0.8, 0.3, 0.3) });
|
|
}
|
|
|
|
page.drawText(`für das Jahr ${abrechnung.jahr}`, { x: margin, y, size: 12, font });
|
|
y -= 20;
|
|
page.drawText(`Objekt: ${abrechnung.objekt_name}`, { x: margin, y, size: 11, font: fontBold });
|
|
y -= 16;
|
|
page.drawText(`${abrechnung.adresse}, ${abrechnung.plz} ${abrechnung.ort}`, { x: margin, y, size: 10, font });
|
|
y -= 16;
|
|
page.drawText(`Abrechnungszeitraum: ${new Date(abrechnung.zeitraum_von).toLocaleDateString('de-DE')} - ${new Date(abrechnung.zeitraum_bis).toLocaleDateString('de-DE')}`, { x: margin, y, size: 10, font });
|
|
y -= 35;
|
|
|
|
// ========== BEGLEITTEXT ==========
|
|
page.drawText('Sehr geehrte(r) Mieter(in),', { x: margin, y, size: 10, font: fontBold });
|
|
y -= 18;
|
|
page.drawText('im Folgenden erhalten Sie Ihre persönliche Nebenkostenabrechnung.', { x: margin, y, size: 9, font });
|
|
y -= 14;
|
|
page.drawText('Die Kosten wurden nach Ihrem im Mietvertrag vereinbarten Anteil umgelegt.', { x: margin, y, size: 9, font });
|
|
y -= 30;
|
|
|
|
// ========== 1. KOSTENÜBERSICHT (alle Kosten des Objekts) ==========
|
|
page.drawText('1. Kostenübersicht des Objekts:', { x: margin, y, size: 12, font: fontBold, color: rgb(0.2, 0.4, 0.6) });
|
|
y -= 20;
|
|
|
|
// Tabellenkopf
|
|
page.drawText('Kategorie', { x: margin, y, size: 9, font: fontBold });
|
|
page.drawText('Bezeichnung', { x: margin + 100, y, size: 9, font: fontBold });
|
|
page.drawText('Betrag', { x: margin + 350, y, size: 9, font: fontBold });
|
|
y -= 12;
|
|
drawLine(page, y + 8);
|
|
y -= 5;
|
|
|
|
for (const k of kostenResult.rows) {
|
|
const betrag = parseFloat(k.betrag);
|
|
page.drawText(k.kategorie, { x: margin, y, size: 9, font });
|
|
page.drawText(k.bezeichnung, { x: margin + 100, y, size: 9, font });
|
|
page.drawText(`${betrag.toFixed(2)} €`, { x: margin + 350, y, size: 9, font });
|
|
y -= 14;
|
|
}
|
|
|
|
y -= 5;
|
|
drawLine(page, y + 8);
|
|
y -= 5;
|
|
page.drawText('Gesamtkosten:', { x: margin + 100, y, size: 10, font: fontBold });
|
|
page.drawText(`${gesamtKosten.toFixed(2)} €`, { x: margin + 350, y, size: 10, font: fontBold });
|
|
y -= 30;
|
|
|
|
// ========== 2. BERECHNUNGSGRUNDLAGE ==========
|
|
page.drawText('2. Berechnungsgrundlage:', { x: margin, y, size: 12, font: fontBold, color: rgb(0.2, 0.4, 0.6) });
|
|
y -= 20;
|
|
|
|
page.drawText(`Gesamtkosten:`, { x: margin + 20, y, size: 9, font });
|
|
page.drawText(`${gesamtKosten.toFixed(2)} €`, { x: margin + 250, y, size: 9, font });
|
|
y -= 14;
|
|
page.drawText(`Gesamtwohnfläche:`, { x: margin + 20, y, size: 9, font });
|
|
page.drawText(`${gesamtFlaeche.toFixed(2)} qm`, { x: margin + 250, y, size: 9, font });
|
|
y -= 14;
|
|
page.drawText(`Kosten pro qm:`, { x: margin + 20, y, size: 9, font: fontBold });
|
|
page.drawText(`${gesamtKosten.toFixed(2)} € / ${gesamtFlaeche.toFixed(2)} qm = ${kostenProQm.toFixed(2)} €/qm`, { x: margin + 250, y, size: 9, font: fontBold });
|
|
y -= 30;
|
|
|
|
// ========== 3. IHRE PERSÖNLICHE BERECHNUNG ==========
|
|
page.drawText('3. Ihre persönliche Berechnung:', { x: margin, y, size: 12, font: fontBold, color: rgb(0.2, 0.4, 0.6) });
|
|
y -= 20;
|
|
|
|
page.drawText(`Ihr Name:`, { x: margin + 20, y, size: 9, font });
|
|
page.drawText(mieterName, { x: margin + 250, y, size: 9, font: fontBold });
|
|
y -= 14;
|
|
page.drawText(`Ihre Wohnfläche:`, { x: margin + 20, y, size: 9, font });
|
|
page.drawText(`${mieterFlaeche.toFixed(2)} qm`, { x: margin + 250, y, size: 9, font });
|
|
y -= 14;
|
|
page.drawText(`Ihr Anteil an den Kosten:`, { x: margin + 20, y, size: 9, font });
|
|
page.drawText(`${kostenProQm.toFixed(2)} €/qm Ã ${mieterFlaeche.toFixed(2)} qm = ${anteilKosten.toFixed(2)} €`, { x: margin + 250, y, size: 9, font });
|
|
y -= 14;
|
|
page.drawText(`Geleistete Vorauszahlungen:`, { x: margin + 20, y, size: 9, font });
|
|
page.drawText(`${vorauszahlungen.toFixed(2)} €`, { x: margin + 250, y, size: 9, font });
|
|
y -= 25;
|
|
|
|
// Ergebnis hervorgehoben
|
|
drawLine(page, y + 8);
|
|
y -= 5;
|
|
|
|
if (ergebnis > 0) {
|
|
page.drawText(`Nachzahlung:`, { x: margin + 20, y, size: 11, font: fontBold });
|
|
page.drawText(`${anteilKosten.toFixed(2)} € - ${vorauszahlungen.toFixed(2)} € = ${ergebnis.toFixed(2)} €`, { x: margin + 250, y, size: 11, font: fontBold });
|
|
y -= 20;
|
|
page.drawText(`Bitte überweisen Sie den Betrag innerhalb von 14 Tagen.`, { x: margin + 20, y, size: 9, font, color: rgb(0.8, 0.2, 0.2) });
|
|
} else {
|
|
page.drawText(`Gutschrift:`, { x: margin + 20, y, size: 11, font: fontBold });
|
|
page.drawText(`${anteilKosten.toFixed(2)} € - ${vorauszahlungen.toFixed(2)} € = ${Math.abs(ergebnis).toFixed(2)} €`, { x: margin + 250, y, size: 11, font: fontBold });
|
|
y -= 20;
|
|
page.drawText(`Der Betrag wird mit der nächsten Miete verrechnet.`, { x: margin + 20, y, size: 9, font, color: rgb(0.2, 0.6, 0.2) });
|
|
}
|
|
y -= 25;
|
|
|
|
// ========== FUSSZEILE ==========
|
|
y -= 20;
|
|
drawLine(page, y);
|
|
y -= 20;
|
|
page.drawText('Diese Abrechnung wurde maschinell erstellt und ist ohne Unterschrift gültig.', { x: margin, y, size: 8, font, color: rgb(0.5, 0.5, 0.5) });
|
|
y -= 12;
|
|
page.drawText('Bei Rückfragen: René Täger | Tel: +49 15563 717612 | E-Mail: info@taeger-it.de', { x: margin, y, size: 8, font, color: rgb(0.5, 0.5, 0.5) });
|
|
}
|
|
|
|
// PDF speichern
|
|
const pdfBytes = await pdfDoc.save();
|
|
const pdfBuffer = Buffer.from(pdfBytes.buffer || pdfBytes);
|
|
|
|
res.setHeader('Content-Type', 'application/pdf');
|
|
res.setHeader('Content-Length', pdfBuffer.length);
|
|
res.setHeader('Content-Disposition', `attachment; filename="nebenkostenabrechnung-${abrechnung.jahr}-${abrechnung.objekt_name.replace(/\s+/g, '_')}.pdf"`);
|
|
res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');
|
|
res.end(pdfBuffer);
|
|
} catch (error) {
|
|
console.error('PDF Error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
|
|
};
|
|
|