import pool from '../config/database'; import logger from '../utils/logger'; // --------------------------------------------------------------------------- // Catalog Items (shop_artikel) // --------------------------------------------------------------------------- async function getItems(filters?: { kategorie?: string; aktiv?: boolean }) { const conditions: string[] = []; const params: unknown[] = []; if (filters?.kategorie) { params.push(filters.kategorie); conditions.push(`kategorie = $${params.length}`); } if (filters?.aktiv !== undefined) { params.push(filters.aktiv); conditions.push(`aktiv = $${params.length}`); } const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const result = await pool.query( `SELECT * FROM shop_artikel ${where} ORDER BY kategorie, bezeichnung`, params, ); return result.rows; } async function getItemById(id: number) { const result = await pool.query('SELECT * FROM shop_artikel WHERE id = $1', [id]); return result.rows[0] || null; } async function createItem( data: { bezeichnung: string; beschreibung?: string; kategorie?: string; geschaetzter_preis?: number; aktiv?: boolean; }, userId: string, ) { const result = await pool.query( `INSERT INTO shop_artikel (bezeichnung, beschreibung, kategorie, geschaetzter_preis, aktiv, erstellt_von) VALUES ($1, $2, $3, $4, COALESCE($5, true), $6) RETURNING *`, [data.bezeichnung, data.beschreibung || null, data.kategorie || null, data.geschaetzter_preis || null, data.aktiv ?? true, userId], ); return result.rows[0]; } async function updateItem( id: number, data: { bezeichnung?: string; beschreibung?: string; kategorie?: string; geschaetzter_preis?: number; aktiv?: boolean; }, _userId: string, ) { const fields: string[] = []; const params: unknown[] = []; if (data.bezeichnung !== undefined) { params.push(data.bezeichnung); fields.push(`bezeichnung = $${params.length}`); } if (data.beschreibung !== undefined) { params.push(data.beschreibung); fields.push(`beschreibung = $${params.length}`); } if (data.kategorie !== undefined) { params.push(data.kategorie); fields.push(`kategorie = $${params.length}`); } if (data.geschaetzter_preis !== undefined) { params.push(data.geschaetzter_preis); fields.push(`geschaetzter_preis = $${params.length}`); } if (data.aktiv !== undefined) { params.push(data.aktiv); fields.push(`aktiv = $${params.length}`); } if (fields.length === 0) { return getItemById(id); } params.push(new Date()); fields.push(`aktualisiert_am = $${params.length}`); params.push(id); const result = await pool.query( `UPDATE shop_artikel SET ${fields.join(', ')} WHERE id = $${params.length} RETURNING *`, params, ); return result.rows[0] || null; } async function deleteItem(id: number) { await pool.query('DELETE FROM shop_artikel WHERE id = $1', [id]); } async function getCategories() { const result = await pool.query( 'SELECT DISTINCT kategorie FROM shop_artikel WHERE kategorie IS NOT NULL ORDER BY kategorie', ); return result.rows.map((r: { kategorie: string }) => r.kategorie); } // --------------------------------------------------------------------------- // Requests (shop_anfragen) // --------------------------------------------------------------------------- async function getRequests(filters?: { status?: string; anfrager_id?: string }) { const conditions: string[] = []; const params: unknown[] = []; if (filters?.status) { params.push(filters.status); conditions.push(`a.status = $${params.length}`); } if (filters?.anfrager_id) { params.push(filters.anfrager_id); conditions.push(`a.anfrager_id = $${params.length}`); } const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const result = await pool.query( `SELECT a.*, u.vorname || ' ' || u.nachname AS anfrager_name, u2.vorname || ' ' || u2.nachname AS bearbeitet_von_name, (SELECT COUNT(*)::int FROM shop_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count FROM shop_anfragen a LEFT JOIN users u ON u.id = a.anfrager_id LEFT JOIN users u2 ON u2.id = a.bearbeitet_von ${where} ORDER BY a.erstellt_am DESC`, params, ); return result.rows; } async function getMyRequests(userId: string) { const result = await pool.query( `SELECT a.*, (SELECT COUNT(*)::int FROM shop_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count FROM shop_anfragen a WHERE a.anfrager_id = $1 ORDER BY a.erstellt_am DESC`, [userId], ); return result.rows; } async function getRequestById(id: number) { const reqResult = await pool.query( `SELECT a.*, u.vorname || ' ' || u.nachname AS anfrager_name, u2.vorname || ' ' || u2.nachname AS bearbeitet_von_name FROM shop_anfragen a LEFT JOIN users u ON u.id = a.anfrager_id LEFT JOIN users u2 ON u2.id = a.bearbeitet_von WHERE a.id = $1`, [id], ); if (reqResult.rows.length === 0) return null; const positionen = await pool.query( `SELECT p.*, sa.bezeichnung AS artikel_bezeichnung, sa.kategorie AS artikel_kategorie FROM shop_anfrage_positionen p LEFT JOIN shop_artikel sa ON sa.id = p.artikel_id WHERE p.anfrage_id = $1 ORDER BY p.id`, [id], ); const bestellungen = await pool.query( `SELECT b.* FROM shop_anfrage_bestellung ab JOIN bestellungen b ON b.id = ab.bestellung_id WHERE ab.anfrage_id = $1`, [id], ); return { ...reqResult.rows[0], positionen: positionen.rows, bestellungen: bestellungen.rows, }; } async function createRequest( userId: string, items: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string }[], notizen?: string, ) { const client = await pool.connect(); 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, bestell_nummer, bestell_jahr) VALUES ($1, $2, $3, $4) RETURNING *`, [userId, notizen || null, nextNr, currentYear], ); const anfrage = anfrageResult.rows[0]; for (const item of items) { let bezeichnung = item.bezeichnung; // If artikel_id is provided, copy bezeichnung from catalog if (item.artikel_id) { const artikelResult = await client.query( 'SELECT bezeichnung FROM shop_artikel WHERE id = $1', [item.artikel_id], ); if (artikelResult.rows.length > 0) { bezeichnung = artikelResult.rows[0].bezeichnung; } } await client.query( `INSERT INTO shop_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen) VALUES ($1, $2, $3, $4, $5)`, [anfrage.id, item.artikel_id || null, bezeichnung, item.menge, item.notizen || null], ); } await client.query('COMMIT'); return getRequestById(anfrage.id); } catch (error) { await client.query('ROLLBACK'); logger.error('shopService.createRequest failed', { error }); throw error; } finally { client.release(); } } async function updateRequestStatus( id: number, status: string, adminNotizen?: string, bearbeitetVon?: string, ) { const result = await pool.query( `UPDATE shop_anfragen SET status = $1, admin_notizen = COALESCE($2, admin_notizen), bearbeitet_von = COALESCE($3, bearbeitet_von), bearbeitet_am = NOW() WHERE id = $4 RETURNING *`, [status, adminNotizen || null, bearbeitetVon || null, id], ); return result.rows[0] || null; } async function deleteRequest(id: number) { await pool.query('DELETE FROM shop_anfragen WHERE id = $1', [id]); } // --------------------------------------------------------------------------- // Linking (shop_anfrage_bestellung) // --------------------------------------------------------------------------- async function linkToOrder(anfrageId: number, bestellungId: number) { await pool.query( `INSERT INTO shop_anfrage_bestellung (anfrage_id, bestellung_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [anfrageId, bestellungId], ); } async function unlinkFromOrder(anfrageId: number, bestellungId: number) { await pool.query( 'DELETE FROM shop_anfrage_bestellung WHERE anfrage_id = $1 AND bestellung_id = $2', [anfrageId, bestellungId], ); } async function getLinkedOrders(anfrageId: number) { const result = await pool.query( `SELECT b.* FROM shop_anfrage_bestellung ab JOIN bestellungen b ON b.id = ab.bestellung_id WHERE ab.anfrage_id = $1`, [anfrageId], ); 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, createItem, updateItem, deleteItem, getCategories, getRequests, getMyRequests, getRequestById, createRequest, updateRequestStatus, deleteRequest, linkToOrder, unlinkFromOrder, getLinkedOrders, getOverview, };