new features
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
CreateAusruestungData,
|
||||
UpdateAusruestungData,
|
||||
CreateAusruestungWartungslogData,
|
||||
UpdateAusruestungWartungslogData,
|
||||
AusruestungStatus,
|
||||
EquipmentStats,
|
||||
VehicleEquipmentWarning,
|
||||
@@ -330,6 +331,15 @@ class EquipmentService {
|
||||
|
||||
const entry = result.rows[0] as AusruestungWartungslog;
|
||||
logger.info('Equipment wartungslog entry added', { entryId: entry.id, equipmentId, by: createdBy });
|
||||
|
||||
// Auto-update next inspection date on the equipment when result is 'bestanden'
|
||||
if (data.ergebnis === 'bestanden' && data.naechste_pruefung_am) {
|
||||
await pool.query(
|
||||
`UPDATE ausruestung SET naechste_pruefung_am = $1, letzte_pruefung_am = $2 WHERE id = $3`,
|
||||
[data.naechste_pruefung_am, data.datum, equipmentId]
|
||||
);
|
||||
}
|
||||
|
||||
return entry;
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.addWartungslog failed', { error, equipmentId });
|
||||
@@ -461,6 +471,70 @@ class EquipmentService {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// WARTUNGSLOG UPDATE
|
||||
// =========================================================================
|
||||
|
||||
async updateWartungslog(
|
||||
equipmentId: string,
|
||||
wartungId: number,
|
||||
data: UpdateAusruestungWartungslogData,
|
||||
updatedBy: string
|
||||
): Promise<AusruestungWartungslog> {
|
||||
try {
|
||||
// Verify the wartung entry belongs to this equipment
|
||||
const check = await pool.query(
|
||||
`SELECT id FROM ausruestung_wartungslog WHERE id = $1 AND ausruestung_id = $2`,
|
||||
[wartungId, equipmentId]
|
||||
);
|
||||
if (check.rows.length === 0) {
|
||||
throw new Error('Wartungseintrag nicht gefunden');
|
||||
}
|
||||
|
||||
const fields: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
let p = 1;
|
||||
|
||||
const addField = (col: string, value: unknown) => {
|
||||
fields.push(`${col} = $${p++}`);
|
||||
values.push(value);
|
||||
};
|
||||
|
||||
if (data.datum !== undefined) addField('datum', data.datum);
|
||||
if (data.art !== undefined) addField('art', data.art);
|
||||
if (data.beschreibung !== undefined) addField('beschreibung', data.beschreibung);
|
||||
if (data.ergebnis !== undefined) addField('ergebnis', data.ergebnis);
|
||||
if (data.kosten !== undefined) addField('kosten', data.kosten);
|
||||
if (data.pruefende_stelle !== undefined) addField('pruefende_stelle', data.pruefende_stelle);
|
||||
|
||||
if (fields.length === 0) {
|
||||
throw new Error('No fields to update');
|
||||
}
|
||||
|
||||
values.push(wartungId);
|
||||
const result = await pool.query(
|
||||
`UPDATE ausruestung_wartungslog SET ${fields.join(', ')} WHERE id = $${p} RETURNING *`,
|
||||
values
|
||||
);
|
||||
|
||||
const entry = result.rows[0] as AusruestungWartungslog;
|
||||
|
||||
// Auto-update next inspection date on the equipment when result is 'bestanden'
|
||||
if (data.ergebnis === 'bestanden' && data.naechste_pruefung_am) {
|
||||
await pool.query(
|
||||
`UPDATE ausruestung SET naechste_pruefung_am = $1, letzte_pruefung_am = $2 WHERE id = $3`,
|
||||
[data.naechste_pruefung_am, data.datum ?? entry.datum, equipmentId]
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('Equipment wartungslog entry updated', { wartungId, equipmentId, by: updatedBy });
|
||||
return entry;
|
||||
} catch (error) {
|
||||
logger.error('EquipmentService.updateWartungslog failed', { error, wartungId, equipmentId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// WARTUNGSLOG FILE UPLOAD
|
||||
// =========================================================================
|
||||
|
||||
@@ -367,6 +367,7 @@ class EventsService {
|
||||
data.alle_gruppen,
|
||||
data.max_teilnehmer ?? null,
|
||||
data.anmeldung_erforderlich,
|
||||
data.anmeldung_bis ?? null,
|
||||
userId,
|
||||
]);
|
||||
}
|
||||
@@ -376,8 +377,8 @@ class EventsService {
|
||||
`INSERT INTO veranstaltungen (
|
||||
wiederholung_parent_id, titel, beschreibung, ort, ort_url, kategorie_id,
|
||||
datum_von, datum_bis, ganztaegig, zielgruppen, alle_gruppen,
|
||||
max_teilnehmer, anmeldung_erforderlich, erstellt_von
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,
|
||||
max_teilnehmer, anmeldung_erforderlich, anmeldung_bis, erstellt_von
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)`,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
162
backend/src/services/issue.service.ts
Normal file
162
backend/src/services/issue.service.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import pool from '../config/database';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
async function getIssues(userId: string, canViewAll: boolean) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT i.*,
|
||||
u1.name AS erstellt_von_name,
|
||||
u2.name AS zugewiesen_an_name
|
||||
FROM issues i
|
||||
LEFT JOIN users u1 ON u1.id = i.erstellt_von
|
||||
LEFT JOIN users u2 ON u2.id = i.zugewiesen_an
|
||||
${canViewAll ? '' : 'WHERE i.erstellt_von = $1'}
|
||||
ORDER BY i.created_at DESC
|
||||
`;
|
||||
const result = canViewAll
|
||||
? await pool.query(query)
|
||||
: await pool.query(query, [userId]);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('IssueService.getIssues failed', { error });
|
||||
throw new Error('Issues konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function getIssueById(id: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT i.*,
|
||||
u1.name AS erstellt_von_name,
|
||||
u2.name AS zugewiesen_an_name
|
||||
FROM issues i
|
||||
LEFT JOIN users u1 ON u1.id = i.erstellt_von
|
||||
LEFT JOIN users u2 ON u2.id = i.zugewiesen_an
|
||||
WHERE i.id = $1`,
|
||||
[id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('IssueService.getIssueById failed', { error, id });
|
||||
throw new Error('Issue konnte nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function createIssue(
|
||||
data: { titel: string; beschreibung?: string; typ?: string; prioritaet?: string },
|
||||
userId: string
|
||||
) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO issues (titel, beschreibung, typ, prioritaet, erstellt_von)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *`,
|
||||
[
|
||||
data.titel,
|
||||
data.beschreibung || null,
|
||||
data.typ || 'sonstiges',
|
||||
data.prioritaet || 'mittel',
|
||||
userId,
|
||||
]
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('IssueService.createIssue failed', { error });
|
||||
throw new Error('Issue konnte nicht erstellt werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateIssue(
|
||||
id: number,
|
||||
data: {
|
||||
titel?: string;
|
||||
beschreibung?: string;
|
||||
typ?: string;
|
||||
prioritaet?: string;
|
||||
status?: string;
|
||||
zugewiesen_an?: string | null;
|
||||
}
|
||||
) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE issues
|
||||
SET titel = COALESCE($1, titel),
|
||||
beschreibung = COALESCE($2, beschreibung),
|
||||
typ = COALESCE($3, typ),
|
||||
prioritaet = COALESCE($4, prioritaet),
|
||||
status = COALESCE($5, status),
|
||||
zugewiesen_an = COALESCE($6, zugewiesen_an),
|
||||
updated_at = NOW()
|
||||
WHERE id = $7
|
||||
RETURNING *`,
|
||||
[
|
||||
data.titel,
|
||||
data.beschreibung,
|
||||
data.typ,
|
||||
data.prioritaet,
|
||||
data.status,
|
||||
data.zugewiesen_an,
|
||||
id,
|
||||
]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('IssueService.updateIssue failed', { error, id });
|
||||
throw new Error('Issue konnte nicht aktualisiert werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteIssue(id: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`DELETE FROM issues WHERE id = $1 RETURNING id`,
|
||||
[id]
|
||||
);
|
||||
return result.rows.length > 0;
|
||||
} catch (error) {
|
||||
logger.error('IssueService.deleteIssue failed', { error, id });
|
||||
throw new Error('Issue konnte nicht gelöscht werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function getComments(issueId: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT c.*, u.name AS autor_name
|
||||
FROM issue_kommentare c
|
||||
LEFT JOIN users u ON u.id = c.autor_id
|
||||
WHERE c.issue_id = $1
|
||||
ORDER BY c.created_at ASC`,
|
||||
[issueId]
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('IssueService.getComments failed', { error, issueId });
|
||||
throw new Error('Kommentare konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function addComment(issueId: number, autorId: string, inhalt: string) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO issue_kommentare (issue_id, autor_id, inhalt)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *`,
|
||||
[issueId, autorId, inhalt]
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('IssueService.addComment failed', { error, issueId });
|
||||
throw new Error('Kommentar konnte nicht erstellt werden');
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getIssues,
|
||||
getIssueById,
|
||||
createIssue,
|
||||
updateIssue,
|
||||
deleteIssue,
|
||||
getComments,
|
||||
addComment,
|
||||
};
|
||||
@@ -202,11 +202,21 @@ async function createRequest(
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Get next bestell_nummer for the current year
|
||||
const currentYear = new Date().getFullYear();
|
||||
const maxResult = await client.query(
|
||||
`SELECT COALESCE(MAX(bestell_nummer), 0) + 1 AS next_nr
|
||||
FROM shop_anfragen
|
||||
WHERE bestell_jahr = $1`,
|
||||
[currentYear],
|
||||
);
|
||||
const nextNr = maxResult.rows[0].next_nr;
|
||||
|
||||
const anfrageResult = await client.query(
|
||||
`INSERT INTO shop_anfragen (anfrager_id, notizen)
|
||||
VALUES ($1, $2)
|
||||
`INSERT INTO shop_anfragen (anfrager_id, notizen, bestell_nummer, bestell_jahr)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *`,
|
||||
[userId, notizen || null],
|
||||
[userId, notizen || null, nextNr, currentYear],
|
||||
);
|
||||
const anfrage = anfrageResult.rows[0];
|
||||
|
||||
@@ -296,6 +306,42 @@ async function getLinkedOrders(anfrageId: number) {
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Overview (aggregated)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getOverview() {
|
||||
const aggregated = await pool.query(
|
||||
`SELECT p.bezeichnung,
|
||||
SUM(p.menge)::int AS total_menge,
|
||||
COUNT(DISTINCT p.anfrage_id)::int AS anfrage_count
|
||||
FROM shop_anfrage_positionen p
|
||||
JOIN shop_anfragen a ON a.id = p.anfrage_id
|
||||
WHERE a.status IN ('offen', 'genehmigt')
|
||||
GROUP BY p.bezeichnung
|
||||
ORDER BY total_menge DESC, p.bezeichnung`,
|
||||
);
|
||||
|
||||
const counts = await pool.query(
|
||||
`SELECT
|
||||
COUNT(*) FILTER (WHERE status = 'offen')::int AS pending_count,
|
||||
COUNT(*) FILTER (WHERE status = 'genehmigt')::int AS approved_count,
|
||||
COALESCE(SUM(sub.total), 0)::int AS total_items
|
||||
FROM shop_anfragen a
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT SUM(p.menge) AS total
|
||||
FROM shop_anfrage_positionen p
|
||||
WHERE p.anfrage_id = a.id
|
||||
) sub ON true
|
||||
WHERE a.status IN ('offen', 'genehmigt')`,
|
||||
);
|
||||
|
||||
return {
|
||||
items: aggregated.rows,
|
||||
...counts.rows[0],
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
getItems,
|
||||
getItemById,
|
||||
@@ -312,4 +358,5 @@ export default {
|
||||
linkToOrder,
|
||||
unlinkFromOrder,
|
||||
getLinkedOrders,
|
||||
getOverview,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CreateFahrzeugData,
|
||||
UpdateFahrzeugData,
|
||||
CreateWartungslogData,
|
||||
UpdateWartungslogData,
|
||||
FahrzeugStatus,
|
||||
VehicleStats,
|
||||
InspectionAlert,
|
||||
@@ -372,8 +373,9 @@ class VehicleService {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO fahrzeug_wartungslog (
|
||||
fahrzeug_id, datum, art, beschreibung,
|
||||
km_stand, kraftstoff_liter, kosten, externe_werkstatt, erfasst_von
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||
km_stand, kraftstoff_liter, kosten, externe_werkstatt,
|
||||
ergebnis, naechste_faelligkeit, erfasst_von
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
|
||||
RETURNING *`,
|
||||
[
|
||||
fahrzeugId,
|
||||
@@ -384,11 +386,21 @@ class VehicleService {
|
||||
data.kraftstoff_liter ?? null,
|
||||
data.kosten ?? null,
|
||||
data.externe_werkstatt ?? null,
|
||||
data.ergebnis ?? null,
|
||||
data.naechste_faelligkeit ?? null,
|
||||
createdBy,
|
||||
]
|
||||
);
|
||||
|
||||
const entry = result.rows[0] as FahrzeugWartungslog;
|
||||
|
||||
// Auto-update next service date on the vehicle when result is 'bestanden'
|
||||
if (data.ergebnis === 'bestanden' && data.naechste_faelligkeit) {
|
||||
await pool.query(
|
||||
`UPDATE fahrzeuge SET naechste_wartung_am = $1 WHERE id = $2`,
|
||||
[data.naechste_faelligkeit, fahrzeugId]
|
||||
);
|
||||
}
|
||||
logger.info('Wartungslog entry added', { entryId: entry.id, fahrzeugId, by: createdBy });
|
||||
return entry;
|
||||
} catch (error) {
|
||||
@@ -397,6 +409,54 @@ class VehicleService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateWartungslog(
|
||||
wartungId: string,
|
||||
fahrzeugId: string,
|
||||
data: UpdateWartungslogData,
|
||||
updatedBy: string
|
||||
): Promise<FahrzeugWartungslog> {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE fahrzeug_wartungslog
|
||||
SET datum = $1, art = $2, beschreibung = $3, km_stand = $4,
|
||||
externe_werkstatt = $5, ergebnis = $6, naechste_faelligkeit = $7
|
||||
WHERE id = $8 AND fahrzeug_id = $9
|
||||
RETURNING *`,
|
||||
[
|
||||
data.datum,
|
||||
data.art ?? null,
|
||||
data.beschreibung,
|
||||
data.km_stand ?? null,
|
||||
data.externe_werkstatt ?? null,
|
||||
data.ergebnis ?? null,
|
||||
data.naechste_faelligkeit ?? null,
|
||||
wartungId,
|
||||
fahrzeugId,
|
||||
]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Wartungseintrag nicht gefunden');
|
||||
}
|
||||
|
||||
const entry = result.rows[0] as FahrzeugWartungslog;
|
||||
|
||||
// Auto-update next service date on the vehicle when result is 'bestanden'
|
||||
if (data.ergebnis === 'bestanden' && data.naechste_faelligkeit) {
|
||||
await pool.query(
|
||||
`UPDATE fahrzeuge SET naechste_wartung_am = $1 WHERE id = $2`,
|
||||
[data.naechste_faelligkeit, fahrzeugId]
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('Wartungslog entry updated', { wartungId, fahrzeugId, by: updatedBy });
|
||||
return entry;
|
||||
} catch (error) {
|
||||
logger.error('VehicleService.updateWartungslog failed', { error, wartungId, fahrzeugId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getWartungslogForVehicle(fahrzeugId: string): Promise<FahrzeugWartungslog[]> {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
|
||||
Reference in New Issue
Block a user