import os import logging from flask import Flask, render_template from config import Config from extensions import db, login_manager, csrf from models import User, KpiDefinition logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def create_app(): app = Flask(__name__) app.config.from_object(Config) os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) db.init_app(app) login_manager.init_app(app) csrf.init_app(app) login_manager.login_view = 'auth.login' login_manager.login_message = 'Пожалуйста, войдите в систему.' login_manager.login_message_category = 'info' from routes.auth import auth_bp from routes.main import main_bp from routes.admin import admin_bp from routes.api import api_bp app.register_blueprint(auth_bp) app.register_blueprint(main_bp) app.register_blueprint(admin_bp) app.register_blueprint(api_bp) @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) @app.errorhandler(404) def not_found(e): return render_template('errors/404.html'), 404 @app.errorhandler(403) def forbidden(e): return render_template('errors/403.html'), 403 @app.errorhandler(500) def server_error(e): return render_template('errors/500.html'), 500 @app.context_processor def utility_processor(): from models import Notification unread_count = 0 try: from flask_login import current_user if current_user.is_authenticated: unread_count = Notification.query.filter_by( user_id=current_user.id, is_read=False ).count() except Exception: pass return dict(unread_notifications_count=unread_count) with app.app_context(): init_database() from services.scheduler import init_scheduler init_scheduler(app) @app.cli.command('sync-facts') def sync_facts_command(): from services.platform_fetcher import fetch_all_facts result = fetch_all_facts() saved = result.get('values_saved', 0) errors = result.get('errors', []) print(f'Sync complete: {saved} values saved, {len(errors)} errors') for err in errors: print(f' ERROR: {err}') return app def init_database(): seed_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'seed') # Restore from seed if running in cloud and target DB is missing seed_db = os.path.join(seed_dir, 'kpi_dashboard.db') if Config._cloud and os.path.exists(seed_db): db_uri = Config.SQLALCHEMY_DATABASE_URI if db_uri.startswith('sqlite:///'): db_file = db_uri.replace('sqlite:///', '', 1) if not os.path.exists(db_file) or os.path.getsize(db_file) == 0: os.makedirs(os.path.dirname(db_file), exist_ok=True) import shutil shutil.copy2(seed_db, db_file) logger.info('Restored seed database to %s', db_file) db.session.remove() db.engine.dispose() db.create_all() # Restore seed uploads in cloud mode seed_uploads = os.path.join(seed_dir, 'uploads') if Config._cloud and os.path.isdir(seed_uploads): from flask import current_app upload_dir = current_app.config['UPLOAD_FOLDER'] os.makedirs(upload_dir, exist_ok=True) import shutil for fname in os.listdir(seed_uploads): src = os.path.join(seed_uploads, fname) dst = os.path.join(upload_dir, fname) if os.path.isfile(src) and not os.path.exists(dst): shutil.copy2(src, dst) logger.info('Restored upload: %s', fname) # Ensure egor (SM) exists sm = User.query.filter_by(username='egor').first() if not sm: sm = User( username='egor', full_name='Егор', role='sm', grade='SM', position='руководитель', is_active=True ) sm.set_password('egor') db.session.add(sm) db.session.commit() logger.info('SM created: egor / egor') # Ensure polina (guest viewer) exists polina = User.query.filter_by(username='polina').first() if not polina: polina = User( username='polina', full_name='Полина', role='guest', is_active=True, email='polina@kpi.local' ) polina.set_password('polina') db.session.add(polina) db.session.commit() logger.info('Guest viewer created: polina / polina') # Migration: add training file columns for existing databases try: import sqlalchemy as sa insp = sa.inspect(db.engine) cols = [c['name'] for c in insp.get_columns('trainings')] if 'original_name' not in cols: db.session.execute(sa.text('ALTER TABLE trainings ADD COLUMN original_name VARCHAR(300)')) if 'file_size' not in cols: db.session.execute(sa.text('ALTER TABLE trainings ADD COLUMN file_size INTEGER DEFAULT 0')) if 'uploaded_by' not in cols: db.session.execute(sa.text('ALTER TABLE trainings ADD COLUMN uploaded_by INTEGER REFERENCES users(id)')) if 'upload_date' not in cols: db.session.execute(sa.text('ALTER TABLE trainings ADD COLUMN upload_date DATETIME')) db.session.commit() except Exception: pass db.session.commit() demo_kpis = [ ('Объем продаж', 'Сумма продаж за период', 'руб.', 'Продажи', 1000000), ('Выполнение плана', 'Процент выполнения плана продаж', '%', 'Продажи', 100), ('Средний чек', 'Средняя сумма покупки', 'руб.', 'Продажи', 5000), ('Кол-во клиентов', 'Количество обслуженных клиентов', 'шт.', 'Сервис', 100), ('Индекс лояльности NPS', 'Готовность рекомендовать бутик', '%', 'Сервис', 80), ('Возвраты', 'Процент возвратов товара', '%', 'Сервис', 5), ('Обучение', 'Пройдено обучений за период', 'шт.', 'Развитие', 3), ('Скорость обслуживания', 'Среднее время на клиента', 'мин', 'Сервис', 15), ('Доп. продажи', 'Количество дополнительных продаж', 'шт.', 'Продажи', 30), ('Выполнение KPI', 'Общий процент выполнения KPI', '%', 'Итог', 90), ('Работа с возражениями', 'Успешное закрытие возражений', '%', 'Сервис', 70), ('Знание продукта', 'Оценка знания ассортимента', 'балл', 'Развитие', 85), ] for name, desc, unit, category, target in demo_kpis: existing = KpiDefinition.query.filter_by(name=name).first() if not existing: kpi = KpiDefinition( name=name, description=desc, unit=unit, category=category, target_value=target, is_active=True ) db.session.add(kpi) db.session.commit() app = create_app() if __name__ == '__main__': port = int(os.environ.get('PORT', 5000)) debug = os.environ.get('FLASK_ENV', 'production') == 'development' app.run(host='0.0.0.0', port=port, debug=debug)