from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for from flask_login import login_required, current_user from app.extensions import db from app.models.lv import LVPosition from app.models.contract import Contract from app.models.view_profile import ViewProfile import json lv_bp = Blueprint('lv', __name__) def _lv_berechtigt(): if current_user.is_superadmin(): return True if current_user.is_firmadmin() or current_user.darf_lv_verwalten: return True return False @lv_bp.route('/') @login_required def index(): if not _lv_berechtigt(): flash('Keine Berechtigung für LV-Verwaltung.', 'danger') return redirect(url_for('admin.dashboard')) lv_names = db.session.query(LVPosition.lv_name).filter_by( company_id=current_user.company_id ).distinct().order_by(LVPosition.lv_name).all() lv_names = [r[0] for r in lv_names] selected_lv = request.args.get('lv', lv_names[0] if lv_names else '') search = request.args.get('q', '').strip() sort_col = request.args.get('sort', '') sort_dir = request.args.get('dir', 'asc') base = LVPosition.query.filter_by( company_id=current_user.company_id, lv_name=selected_lv ) if search: like = f'%{search}%' base = base.filter( db.or_(LVPosition.pos_nr.like(like), LVPosition.kurztext.like(like)) ) sort_map = { 'pos_nr': LVPosition.pos_nr, 'text': LVPosition.kurztext, 'einheit': LVPosition.einheit, 'ep': LVPosition.einzelpreis, } if sort_col in sort_map: col = sort_map[sort_col] order = col.asc() if sort_dir == 'asc' else col.desc() positionen = base.order_by(LVPosition.favorite.desc(), order).all() else: positionen = base.order_by(LVPosition.favorite.desc(), LVPosition.order_index).all() contracts = Contract.query.filter_by(company_id=current_user.company_id).order_by(Contract.name).all() view_id = request.args.get('view_id', type=int) view_config = ViewProfile.get_default_config() if view_id: vp = ViewProfile.query.get(view_id) if vp and vp.user_id == current_user.id: view_config = vp.get_config() else: default_vp = ViewProfile.query.filter_by( user_id=current_user.id, view_type='lv', is_default=True ).first() if default_vp: view_config = default_vp.get_config() preise_sichtbar = current_user.is_firmadmin() or current_user.darf_preise_sehen return render_template('lv/index.html', lv_names=lv_names, selected_lv=selected_lv, positionen=positionen, search=search, contracts=contracts, view_config=view_config, view_config_json=json.dumps(view_config), preise_sichtbar=preise_sichtbar, titel='Leistungsverzeichnis') @lv_bp.route('/neu', methods=['POST']) @login_required def neu_lv(): if not _lv_berechtigt(): flash('Keine Berechtigung.', 'danger') return redirect(url_for('lv.index')) lv_name = request.form.get('lv_name', '').strip() if not lv_name: flash('LV-Name erforderlich.', 'danger') return redirect(url_for('lv.index')) return redirect(url_for('lv.index', lv=lv_name)) @lv_bp.route('/position/neu', methods=['POST']) @login_required def position_neu(): if not _lv_berechtigt(): flash('Keine Berechtigung.', 'danger') return redirect(url_for('lv.index')) lv_name = request.form.get('lv_name', '') if not lv_name: flash('Bitte zuerst ein LV auswählen.', 'danger') return redirect(url_for('lv.index')) max_order = db.session.query(db.func.max(LVPosition.order_index)).filter_by( company_id=current_user.company_id, lv_name=lv_name ).scalar() or 0 einzelpreis = float(request.form.get('einzelpreis', 0)) pos = LVPosition( company_id=current_user.company_id, lv_name=lv_name, pos_nr=request.form.get('pos_nr', ''), order_index=max_order + 1, kurztext=request.form.get('kurztext', ''), langtext=request.form.get('langtext', ''), einheit=request.form.get('einheit', 'ST'), einzelpreis=einzelpreis if current_user.is_firmadmin() or current_user.darf_preise_sehen else 0, gruppe=request.form.get('gruppe', ''), ) db.session.add(pos) db.session.commit() return redirect(url_for('lv.index', lv=lv_name)) @lv_bp.route('/position//bearbeiten', methods=['POST']) @login_required def position_bearbeiten(pos_id): if not _lv_berechtigt(): return 'Keine Berechtigung', 403 pos = LVPosition.query.get_or_404(pos_id) if pos.company_id != current_user.company_id: return 'Zugriff verweigert', 403 pos.pos_nr = request.form.get('pos_nr', pos.pos_nr) pos.kurztext = request.form.get('kurztext', pos.kurztext) pos.langtext = request.form.get('langtext', pos.langtext) pos.einheit = request.form.get('einheit', pos.einheit) if current_user.is_firmadmin() or current_user.darf_preise_sehen: pos.einzelpreis = float(request.form.get('einzelpreis', pos.einzelpreis)) pos.gruppe = request.form.get('gruppe', pos.gruppe) db.session.commit() return redirect(url_for('lv.index', lv=pos.lv_name)) @lv_bp.route('/position//loeschen', methods=['POST']) @login_required def position_loeschen(pos_id): if not _lv_berechtigt(): return 'Keine Berechtigung', 403 pos = LVPosition.query.get_or_404(pos_id) if pos.company_id != current_user.company_id: return 'Zugriff verweigert', 403 lv_name = pos.lv_name db.session.delete(pos) db.session.commit() return redirect(url_for('lv.index', lv=lv_name)) @lv_bp.route('/positionen/reihenfolge', methods=['POST']) @login_required def positionen_reihenfolge(): if not _lv_berechtigt(): return jsonify({'error': 'Keine Berechtigung'}), 403 data = request.get_json() if not data or 'reihenfolge' not in data: return jsonify({'error': 'Keine Daten'}), 400 for item in data['reihenfolge']: pos = LVPosition.query.get(item['id']) if pos and pos.company_id == current_user.company_id: pos.order_index = item['order_index'] db.session.commit() return jsonify({'status': 'ok'}) @lv_bp.route('/position//favorite', methods=['POST']) @login_required def position_favorite(pos_id): pos = LVPosition.query.get_or_404(pos_id) if pos.company_id != current_user.company_id: return 'Zugriff verweigert', 403 data = request.get_json() or {} pos.favorite = data.get('favorite', not pos.favorite) db.session.commit() return jsonify({'favorite': pos.favorite}) @lv_bp.route('/position//langtext') @login_required def position_langtext(pos_id): pos = LVPosition.query.get_or_404(pos_id) if pos.company_id != current_user.company_id: return 'Zugriff verweigert', 403 preise_sichtbar = current_user.is_firmadmin() or current_user.darf_preise_sehen preis_text = f'{pos.einzelpreis:.2f} €' if preise_sichtbar else 'versteckt' return f'''

{pos.pos_nr} – {pos.kurztext or ''}

Einheit: {pos.einheit} | EP: {preis_text} | Favorit: {'★' if pos.favorite else '☆'}


{pos.langtext or 'Kein Langtext vorhanden.'}
''' @lv_bp.route('/import/txt', methods=['POST']) @login_required def import_txt(): if not _lv_berechtigt(): flash('Keine Berechtigung.', 'danger') return redirect(url_for('lv.index')) lv_name = request.form.get('lv_name', '').strip() if not lv_name: flash('LV-Name erforderlich.', 'danger') return redirect(url_for('lv.index')) file = request.files.get('datei') if not file: flash('Keine Datei ausgewählt.', 'danger') return redirect(url_for('lv.index')) content = file.read().decode('utf-8', errors='ignore') max_order = db.session.query(db.func.max(LVPosition.order_index)).filter_by( company_id=current_user.company_id, lv_name=lv_name ).scalar() or 0 zeilen = content.split('\n') count = 0 preise_sichtbar = current_user.is_firmadmin() or current_user.darf_preise_sehen for zeile in zeilen: zeile = zeile.strip() if not zeile or zeile.startswith('#'): continue parts = [p.strip() for p in zeile.split('|')] if len(parts) >= 2: pos_nr = parts[0] if not LVPosition.query.filter_by( company_id=current_user.company_id, lv_name=lv_name, pos_nr=pos_nr ).first(): max_order += 1 einzelpreis = float(parts[4]) if len(parts) > 4 and parts[4] else 0 pos = LVPosition( company_id=current_user.company_id, lv_name=lv_name, pos_nr=pos_nr, order_index=max_order, kurztext=parts[1] if len(parts) > 1 else '', langtext=parts[2] if len(parts) > 2 else '', einheit=parts[3] if len(parts) > 3 else 'ST', einzelpreis=einzelpreis if preise_sichtbar else 0, ) db.session.add(pos) count += 1 db.session.commit() flash(f'{count} Positionen importiert.', 'success') return redirect(url_for('lv.index', lv=lv_name))