File size: 7,871 Bytes
90756b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4bf4e39
 
5c5d757
4bf4e39
 
90756b2
 
 
5c5d757
90756b2
 
4bf4e39
90756b2
 
 
 
 
 
4bf4e39
 
 
 
 
 
 
 
 
 
 
 
 
 
90756b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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)