Spaces:
Paused
Paused
| 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) | |
| def load_user(user_id): | |
| return User.query.get(int(user_id)) | |
| def not_found(e): | |
| return render_template('errors/404.html'), 404 | |
| def forbidden(e): | |
| return render_template('errors/403.html'), 403 | |
| def server_error(e): | |
| return render_template('errors/500.html'), 500 | |
| 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) | |
| 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) | |