from flask import Flask, render_template, request, jsonify, Response import sqlite3, pandas as pd, os, io, csv, hashlib, json, re from datetime import datetime from zoneinfo import ZoneInfo from werkzeug.utils import secure_filename app = Flask(__name__) app.config['UPLOAD_FOLDER'] = 'uploads' DB_PATH = 'queue_stats.db' IRAN_TZ = ZoneInfo('Asia/Tehran') SHORT_ABANDON = 5 os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) # ─────────────────────── DB ─────────────────────── def init_db(): conn = sqlite3.connect(DB_PATH) conn.execute(''' CREATE TABLE IF NOT EXISTS queue_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, callid TEXT, queuename TEXT, agent TEXT, event TEXT, data1 TEXT, data2 TEXT, data3 TEXT, file_source TEXT, imported_at TEXT ) ''') conn.execute(''' CREATE TABLE IF NOT EXISTS stats_cache ( cache_key TEXT PRIMARY KEY, result TEXT, created_at TEXT ) ''') for idx, col in [('ts','timestamp'),('queue','queuename'), ('agent','agent'),('event','event'),('callid','callid')]: conn.execute(f'CREATE INDEX IF NOT EXISTS idx_{idx} ON queue_events({col})') conn.commit() conn.close() # ─────────────────────── Cache ─────────────────────── def get_cache(key): conn = sqlite3.connect(DB_PATH) row = conn.execute("SELECT result FROM stats_cache WHERE cache_key=?", (key,)).fetchone() conn.close() return json.loads(row[0]) if row else None def set_cache(key, data): conn = sqlite3.connect(DB_PATH) conn.execute( "INSERT OR REPLACE INTO stats_cache (cache_key, result, created_at) VALUES (?,?,?)", (key, json.dumps(data, ensure_ascii=False), datetime.now().isoformat()) ) conn.commit() conn.close() def clear_cache(): conn = sqlite3.connect(DB_PATH) conn.execute("DELETE FROM stats_cache") conn.commit() conn.close() def make_cache_key(*args): return hashlib.md5('|'.join(str(a) for a in args).encode()).hexdigest() # ─────────────────────── Phone ─────────────────────── def normalize_phone(number): if not number or not isinstance(number, str): return None n = re.sub(r'\D', '', number.strip()) if not n or len(n) < 7: return None if n in ('0', '00'): return None if n.startswith('0098'): n = n[4:] elif n.startswith('98') and len(n) == 12: n = n[2:] elif n.startswith('0') and len(n) == 11: n = n[1:] return n if len(n) >= 7 else None # ─────────────────────── Parse / Import ─────────────────────── def parse_line(line): parts = line.strip().split('|') if len(parts) < 5: return None try: return { 'timestamp': int(parts[0]), 'callid' : parts[1], 'queuename': parts[2], 'agent' : parts[3], 'event' : parts[4], 'data1' : parts[5] if len(parts) > 5 else None, 'data2' : parts[6] if len(parts) > 6 else None, 'data3' : parts[7] if len(parts) > 7 else None, } except: return None def process_file(filepath, filename): conn = sqlite3.connect(DB_PATH) existing = conn.execute( "SELECT COUNT(*) FROM queue_events WHERE file_source=?", (filename,) ).fetchone()[0] if existing > 0: conn.close() return -1 rows = [] with open(filepath, 'r', errors='ignore') as f: for line in f: r = parse_line(line) if r: rows.append(( r['timestamp'], r['callid'], r['queuename'], r['agent'], r['event'], r['data1'], r['data2'], r['data3'], filename, datetime.now().isoformat() )) conn.executemany(''' INSERT INTO queue_events (timestamp,callid,queuename,agent,event,data1,data2,data3,file_source,imported_at) VALUES (?,?,?,?,?,?,?,?,?,?) ''', rows) conn.commit() conn.close() return len(rows) # ─────────────────────── Build Calls ─────────────────────── def ts_to_dt(t): try: return datetime.fromtimestamp(int(t), tz=IRAN_TZ) except: return None def build_calls(df): enter = df[df['event'] == 'ENTERQUEUE'].groupby('callid').first() # ✅ آخرین CONNECT (در ringall چندتا داریم، آخری که جواب داده مهمه) connect = df[df['event'] == 'CONNECT'].groupby('callid').last() # ✅ COMPLETE = منبع اصلی — answered فقط با COMPLETE تعیین میشه comp = df[df['event'].isin(['COMPLETEAGENT', 'COMPLETECALLER'])]\ .groupby('callid').first() abandon = df[df['event'] == 'ABANDON'].groupby('callid').first() timeout = df[df['event'] == 'EXITWITHTIMEOUT'].groupby('callid').first() rna_cnt = df[df['event'] == 'RINGNOANSWER'].groupby('callid').size().rename('rna_count') all_ids = pd.DataFrame(index=df['callid'].unique()) # ✅ outcome: answered فقط اگه COMPLETEAGENT یا COMPLETECALLER داشته باشه # نه صرفاً CONNECT — چون در ringall چند CONNECT داریم all_ids['outcome'] = 'other' all_ids.loc[all_ids.index.isin(abandon.index), 'outcome'] = 'abandoned' all_ids.loc[all_ids.index.isin(timeout.index), 'outcome'] = 'timeout' all_ids.loc[all_ids.index.isin(comp.index), 'outcome'] = 'answered' # زمان‌ها: اول از COMPLETE (دقیق‌ترین)، بعد از آخرین CONNECT all_ids['wait_time'] = pd.to_numeric(comp['data1'], errors='coerce').reindex(all_ids.index) all_ids['talk_time'] = pd.to_numeric(comp['data2'], errors='coerce').reindex(all_ids.index) all_ids['position'] = pd.to_numeric(comp['data3'], errors='coerce').reindex(all_ids.index) connect_wait = pd.to_numeric(connect['data1'], errors='coerce').reindex(all_ids.index) all_ids['wait_time'] = all_ids['wait_time'].fillna(connect_wait) enter_pos = pd.to_numeric(enter['data3'], errors='coerce').reindex(all_ids.index) all_ids['position'] = all_ids['position'].fillna(enter_pos) all_ids['abandon_wait'] = pd.to_numeric(abandon['data3'], errors='coerce').reindex(all_ids.index) all_ids['timeout_wait'] = pd.to_numeric(timeout['data3'], errors='coerce').reindex(all_ids.index) all_ids['is_short_abandon'] = ( (all_ids['outcome'] == 'abandoned') & (all_ids['abandon_wait'].notna()) & (all_ids['abandon_wait'] < SHORT_ABANDON) ) all_ids['aht'] = all_ids['wait_time'] + all_ids['talk_time'] # agent: اول از COMPLETE، بعد از آخرین CONNECT comp_agent = comp['agent'].reindex(all_ids.index) connect_agent = connect['agent'].reindex(all_ids.index) all_ids['agent'] = comp_agent.where( comp_agent.notna() & ~comp_agent.isin(['NONE', 'none', '']), connect_agent ) all_ids['agent'] = all_ids['agent'].where( all_ids['agent'].notna() & ~all_ids['agent'].isin(['NONE', 'none', '']), None ) raw_caller = enter['data2'].reindex(all_ids.index) raw_caller = raw_caller.where( raw_caller.notna() & ~raw_caller.isin(['NONE', 'none', '', '', 'anonymous']), None ) all_ids['caller_id'] = raw_caller.apply(normalize_phone) all_ids['queue'] = enter['queuename'].reindex(all_ids.index).fillna( df.groupby('callid')['queuename'].first() ) ts = df.groupby('callid')['timestamp'].min() all_ids['timestamp'] = ts.reindex(all_ids.index) def apply_dt(t, attr): dt = ts_to_dt(t) if dt is None: return None return {'hour': dt.hour, 'time': dt.strftime('%H:%M'), 'date': dt.strftime('%Y-%m-%d')}[attr] all_ids['hour'] = all_ids['timestamp'].apply(lambda t: apply_dt(t, 'hour')) all_ids['time'] = all_ids['timestamp'].apply(lambda t: apply_dt(t, 'time')) all_ids['date'] = all_ids['timestamp'].apply(lambda t: apply_dt(t, 'date')) comp_event = df[df['event'].isin(['COMPLETEAGENT', 'COMPLETECALLER'])]\ .groupby('callid')['event'].first() all_ids['complete_type'] = comp_event.map( {'COMPLETEAGENT': 'agent', 'COMPLETECALLER': 'caller'} ).reindex(all_ids.index) all_ids['rna_count'] = rna_cnt.reindex(all_ids.index).fillna(0).astype(int) all_ids.index.name = 'callid' return all_ids.reset_index() # ─────────────────────── Shared calls cache ─────────────────────── def get_calls_cached(queue, agent_filter, file_filter): cache_key = make_cache_key('calls_df', queue, agent_filter, file_filter) cached = get_cache(cache_key) if cached is not None: if not cached: return pd.DataFrame() df = pd.DataFrame(cached) df['is_short_abandon'] = df['is_short_abandon'].astype(bool) for col in ['wait_time', 'talk_time', 'aht', 'abandon_wait', 'timeout_wait', 'position', 'hour', 'rna_count']: if col in df.columns: df[col] = pd.to_numeric(df[col], errors='coerce') return df raw = _build_stats_df(queue, file_filter) if raw.empty: set_cache(cache_key, []) return pd.DataFrame() calls = build_calls(raw) if calls.empty: set_cache(cache_key, []) return pd.DataFrame() if agent_filter: agent_callids = calls[ calls['agent'].notna() & calls['agent'].str.contains(agent_filter, case=False, na=False) ]['callid'] calls = calls[calls['callid'].isin(agent_callids)] calls_save = calls.copy() calls_save['timestamp'] = calls_save['timestamp'].astype(str) calls_save['is_short_abandon'] = calls_save['is_short_abandon'].astype(bool) set_cache(cache_key, calls_save.where(pd.notnull(calls_save), None).to_dict('records')) return calls # ─────────────────────── Stats ─────────────────────── def _build_stats_df(queue='', file_filter=''): conn = sqlite3.connect(DB_PATH) q = "SELECT * FROM queue_events WHERE 1=1" params = [] if queue: q += " AND queuename=?" params.append(queue) if file_filter: q += " AND file_source=?" params.append(file_filter) df = pd.read_sql_query(q, conn, params=params) conn.close() return df def _calculate_stats(calls): answered = calls[calls['outcome'] == 'answered'] abandoned_all = calls[calls['outcome'] == 'abandoned'] short_abandons= abandoned_all[abandoned_all['is_short_abandon'] == True] real_abandons = abandoned_all[abandoned_all['is_short_abandon'] == False] timeouts = calls[calls['outcome'] == 'timeout'] total_calls = len(calls) total_ans = len(answered) total_abn_real = len(real_abandons) total_abn_short = len(short_abandons) total_to = len(timeouts) wait_times = answered['wait_time'].dropna() abn_waits = real_abandons['abandon_wait'].dropna() to_waits = timeouts['timeout_wait'].dropna() talk_times = answered['talk_time'].dropna() aht_values = answered['aht'].dropna() effective_total = total_calls - total_abn_short if total_calls else 0 answer_rate = round(total_ans / effective_total * 100, 1) if effective_total else 0 abandon_rate = round(total_abn_real / effective_total * 100, 1) if effective_total else 0 timeout_rate = round(total_to / effective_total * 100, 1) if effective_total else 0 pos_answered = answered['position'].dropna() pos_abandoned = real_abandons['position'].dropna() avg_pos_ans = round(float(pos_answered.mean()), 1) if len(pos_answered) > 0 else 0 avg_pos_abn = round(float(pos_abandoned.mean()), 1) if len(pos_abandoned) > 0 else 0 avg_to_wait = round(float(to_waits.mean()), 1) if len(to_waits) > 0 else 0 med_to_wait = round(float(to_waits.median()), 1) if len(to_waits) > 0 else 0 timeout_hourly = [ {'hour': h, 'count': int(len(timeouts[timeouts['hour'] == h]))} for h in range(24) ] fcr_data = calls[calls['caller_id'].notna() & (calls['caller_id'] != '')] caller_counts = fcr_data.groupby('caller_id')['callid'].nunique() if not fcr_data.empty else pd.Series(dtype=int) unique_callers = int(len(caller_counts)) repeat_callers = int((caller_counts > 1).sum()) if unique_callers > 0 else 0 repeat_rate = round(repeat_callers / unique_callers * 100, 1) if unique_callers > 0 else 0 repeat_calls = int(caller_counts[caller_counts > 1].sum()) - repeat_callers if unique_callers > 0 else 0 repeat_calls_rate = round(repeat_calls / total_calls * 100, 1) if total_calls > 0 else 0 hourly = [] for h in range(24): hc = calls[calls['hour'] == h] ha = hc[hc['outcome'] == 'answered'] habn = hc[(hc['outcome'] == 'abandoned') & (hc['is_short_abandon'] == False)] hto = hc[hc['outcome'] == 'timeout'] hw = ha['wait_time'].dropna() eff_h= len(hc) - int(hc['is_short_abandon'].sum()) hourly.append({ 'hour' : h, 'total' : int(len(hc)), 'answered' : int(len(ha)), 'abandoned' : int(len(habn)), 'timeout' : int(len(hto)), 'abandon_rate': round(len(habn) / eff_h * 100, 1) if eff_h > 0 else 0, 'avg_wait' : round(float(hw.mean()), 1) if len(hw) > 0 else 0, 'avg_aht' : round(float(ha['aht'].dropna().mean()), 1) if len(ha) > 0 else 0, }) team_avg_aht = round(float(aht_values.mean()), 1) if len(aht_values) > 0 else 0 team_med_wait = round(float(wait_times.median()), 1) if len(wait_times) > 0 else 0 agent_stats = [] agent_calls = answered[answered['agent'].notna() & (answered['agent'] != '')] for ag, ag_calls in agent_calls.groupby('agent'): ag_waits = ag_calls['wait_time'].dropna() ag_talks = ag_calls['talk_time'].dropna() ag_aht = ag_calls['aht'].dropna() ag_ans = len(ag_calls) share_of_team = round(ag_ans / total_ans * 100, 1) if total_ans > 0 else 0 comp_agent_cnt = int((ag_calls['complete_type'] == 'agent').sum()) comp_caller_cnt = int((ag_calls['complete_type'] == 'caller').sum()) ag_callers = ag_calls[ag_calls['caller_id'].notna()] ag_caller_ids = set(ag_callers['caller_id'].values) ag_repeat = sum(1 for cid in ag_caller_ids if caller_counts.get(cid, 1) > 1) if unique_callers > 0 else 0 ag_repeat_rate= round(ag_repeat / len(ag_caller_ids) * 100, 1) if len(ag_caller_ids) > 0 else 0 ag_pos = ag_calls['position'].dropna() avg_ag_pos = round(float(ag_pos.mean()), 1) if len(ag_pos) > 0 else 0 aht_vs_team = round( (float(ag_aht.mean()) - team_avg_aht) / team_avg_aht * 100, 1 ) if (len(ag_aht) > 0 and team_avg_aht > 0) else 0 agent_stats.append({ 'agent' : ag, 'answered' : ag_ans, 'share_of_team' : share_of_team, 'complete_agent' : comp_agent_cnt, 'complete_caller': comp_caller_cnt, 'avg_wait' : round(float(ag_waits.mean()), 1) if len(ag_waits) > 0 else 0, 'median_wait' : round(float(ag_waits.median()), 1) if len(ag_waits) > 0 else 0, 'avg_talk' : round(float(ag_talks.mean()), 1) if len(ag_talks) > 0 else 0, 'median_talk' : round(float(ag_talks.median()), 1) if len(ag_talks) > 0 else 0, 'avg_aht' : round(float(ag_aht.mean()), 1) if len(ag_aht) > 0 else 0, 'aht_vs_team' : aht_vs_team, 'repeat_rate' : ag_repeat_rate, 'avg_position' : avg_ag_pos, }) agent_stats.sort(key=lambda x: x['answered'], reverse=True) histogram = { '0-20s' : int((wait_times < 20).sum()), '20-40s' : int(((wait_times >= 20) & (wait_times < 40)).sum()), '40-60s' : int(((wait_times >= 40) & (wait_times < 60)).sum()), '60-120s': int(((wait_times >= 60) & (wait_times < 120)).sum()), '120s+' : int((wait_times >= 120).sum()), } return { 'total_calls' : int(total_calls), 'total_answered' : int(total_ans), 'total_abandoned' : int(total_abn_real), 'total_short_abandon': int(total_abn_short), 'total_timeout' : int(total_to), 'answer_rate' : answer_rate, 'abandon_rate' : abandon_rate, 'timeout_rate' : timeout_rate, 'avg_wait' : round(float(wait_times.mean()), 1) if len(wait_times) > 0 else 0, 'median_wait' : round(float(wait_times.median()), 1) if len(wait_times) > 0 else 0, 'avg_abandon_wait' : round(float(abn_waits.mean()), 1) if len(abn_waits) > 0 else 0, 'median_abandon_wait': round(float(abn_waits.median()), 1) if len(abn_waits) > 0 else 0, 'avg_timeout_wait' : avg_to_wait, 'median_timeout_wait': med_to_wait, 'avg_talk' : round(float(talk_times.mean()), 1) if len(talk_times) > 0 else 0, 'median_talk' : round(float(talk_times.median()), 1) if len(talk_times) > 0 else 0, 'avg_aht' : round(float(aht_values.mean()), 1) if len(aht_values) > 0 else 0, 'median_aht' : round(float(aht_values.median()), 1) if len(aht_values) > 0 else 0, 'team_avg_aht' : team_avg_aht, 'team_med_wait' : team_med_wait, 'avg_pos_answered' : avg_pos_ans, 'avg_pos_abandoned' : avg_pos_abn, 'unique_callers' : unique_callers, 'repeat_callers' : repeat_callers, 'repeat_rate' : repeat_rate, 'repeat_calls_rate' : repeat_calls_rate, 'timeout_hourly' : timeout_hourly, 'hourly' : hourly, 'histogram' : histogram, 'agents' : agent_stats, } # ─────────────────────── Routes ─────────────────────── @app.route('/queue/') @app.route('/queue') def index(): return render_template('index.html') @app.route('/queue/upload', methods=['POST']) def upload(): f = request.files.get('file') if not f: return jsonify({'error': 'فایل پیدا نشد'}) filename = secure_filename(f.filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) f.save(filepath) count = process_file(filepath, filename) if count == -1: return jsonify({'warning': True, 'message': f'فایل {filename} قبلاً وارد شده!'}) clear_cache() return jsonify({'success': True, 'message': f'{count} رکورد از {filename} وارد شد'}) @app.route('/queue/files') def get_files(): conn = sqlite3.connect(DB_PATH) files = pd.read_sql_query( "SELECT file_source, COUNT(*) as count, MIN(imported_at) as imported " "FROM queue_events GROUP BY file_source ORDER BY imported DESC", conn ) conn.close() return jsonify(files.to_dict('records')) @app.route('/queue/queues') def get_queues(): conn = sqlite3.connect(DB_PATH) queues = pd.read_sql_query( "SELECT DISTINCT queuename FROM queue_events ORDER BY queuename", conn ) conn.close() return jsonify(queues['queuename'].tolist()) @app.route('/queue/agents') def get_agents(): queue = request.args.get('queue', '') conn = sqlite3.connect(DB_PATH) q = "SELECT DISTINCT agent FROM queue_events WHERE agent NOT IN ('NONE','none','') AND agent IS NOT NULL" params = [] if queue: q += " AND queuename=?" params.append(queue) q += " ORDER BY agent" agents = pd.read_sql_query(q, conn, params=params) conn.close() return jsonify(agents['agent'].tolist()) @app.route('/queue/delete_file', methods=['POST']) def delete_file(): filename = request.json.get('filename') if not filename: return jsonify({'error': 'نام فایل وارد نشد'}) conn = sqlite3.connect(DB_PATH) conn.execute("DELETE FROM queue_events WHERE file_source=?", (filename,)) conn.commit() conn.close() fp = os.path.join(app.config['UPLOAD_FOLDER'], filename) if os.path.exists(fp): os.remove(fp) clear_cache() return jsonify({'success': True, 'message': f'فایل {filename} حذف شد'}) @app.route('/queue/clear_cache', methods=['POST']) def api_clear_cache(): clear_cache() return jsonify({'success': True, 'message': 'cache پاک شد'}) @app.route('/queue/stats') def stats(): queue = request.args.get('queue', '') agent_filter = request.args.get('agent', '') file_filter = request.args.get('file_filter', '') cache_key = make_cache_key('stats', queue, agent_filter, file_filter) cached = get_cache(cache_key) if cached: cached['from_cache'] = True return jsonify(cached) calls = get_calls_cached(queue, agent_filter, file_filter) if calls.empty: return jsonify({'error': 'تماسی پیدا نشد'}) result = _calculate_stats(calls) set_cache(cache_key, result) result['from_cache'] = False return jsonify(result) @app.route('/queue/call_details') def call_details(): queue = request.args.get('queue', '') agent_filter = request.args.get('agent', '') file_filter = request.args.get('file_filter', '') page = int(request.args.get('page', 1)) per_page = int(request.args.get('per_page', 50)) search = request.args.get('search', '').strip() cache_key = make_cache_key('details', queue, agent_filter, file_filter) cached_calls = get_cache(cache_key) if cached_calls is None: calls = get_calls_cached(queue, agent_filter, file_filter) if calls.empty: return jsonify({'error': 'تماسی پیدا نشد', 'calls': [], 'total': 0}) calls = calls.sort_values('timestamp', ascending=False) all_calls = [] for _, row in calls.iterrows(): all_calls.append({ 'callid' : str(row.get('callid', '')), 'caller_id': str(row.get('caller_id', '') or '—'), 'agent' : str(row.get('agent', '') or '—'), 'queue' : str(row.get('queue', '')), 'outcome' : str(row.get('outcome', '')), 'wait_time': round(float(row['wait_time']), 1) if pd.notna(row.get('wait_time')) else '—', 'talk_time': round(float(row['talk_time']), 1) if pd.notna(row.get('talk_time')) else '—', 'aht' : round(float(row['aht']), 1) if pd.notna(row.get('aht')) else '—', 'date' : str(row.get('date', '')), 'time' : str(row.get('time', '') or '—'), 'position' : int(row['position']) if pd.notna(row.get('position')) else '—', }) set_cache(cache_key, all_calls) cached_calls = all_calls filtered = [c for c in cached_calls if search in c.get('caller_id', '')] if search else cached_calls total = len(filtered) start = (page - 1) * per_page page_calls = filtered[start: start + per_page] return jsonify({ 'calls' : page_calls, 'total' : total, 'page' : page, 'per_page': per_page, 'pages' : (total + per_page - 1) // per_page, }) @app.route('/queue/repeat_callers') def repeat_callers(): queue = request.args.get('queue', '') agent_filter = request.args.get('agent', '') file_filter = request.args.get('file_filter', '') page = int(request.args.get('page', 1)) per_page = int(request.args.get('per_page', 50)) search = request.args.get('search', '').strip() cache_key = make_cache_key('repeats', queue, agent_filter, file_filter) cached_list = get_cache(cache_key) if cached_list is None: calls = get_calls_cached(queue, agent_filter, file_filter) if calls.empty: return jsonify({'callers': [], 'total': 0, 'pages': 0}) calls_with_id = calls[calls['caller_id'].notna() & (calls['caller_id'] != '')] result = [] for cid, grp in calls_with_id.groupby('caller_id'): if len(grp) < 2: continue answered = grp[grp['outcome'] == 'answered'] abandoned = grp[(grp['outcome'] == 'abandoned') & (grp['is_short_abandon'] == False)] dates = sorted(grp['date'].dropna().unique().tolist()) agents_used = [a for a in grp['agent'].dropna().unique().tolist() if a not in ('NONE', 'none', '')] max_daily = int(grp.groupby('date')['callid'].count().max()) result.append({ 'caller_id' : str(cid), 'call_count': len(grp), 'answered' : int(len(answered)), 'abandoned' : int(len(abandoned)), 'first_call': dates[0] if dates else '—', 'last_call' : dates[-1] if dates else '—', 'days_span' : len(dates), 'agents' : ', '.join(agents_used[:3]) + ( f' +{len(agents_used)-3}' if len(agents_used) > 3 else ''), 'avg_wait' : round(float(answered['wait_time'].dropna().mean()), 1) if len(answered) > 0 and answered['wait_time'].notna().any() else '—', 'max_daily' : max_daily, 'is_shared' : max_daily >= 5, }) result.sort(key=lambda x: x['call_count'], reverse=True) set_cache(cache_key, result) cached_list = result filtered = [r for r in cached_list if search in r['caller_id']] if search else cached_list total = len(filtered) start = (page - 1) * per_page paged = filtered[start: start + per_page] return jsonify({ 'callers' : paged, 'total' : total, 'page' : page, 'per_page': per_page, 'pages' : (total + per_page - 1) // per_page, }) @app.route('/queue/export_csv') def export_csv(): queue = request.args.get('queue', '') file_filter = request.args.get('file_filter', '') calls = get_calls_cached(queue, '', file_filter) if calls.empty: return jsonify({'error': 'داده‌ای پیدا نشد'}) output = io.StringIO() writer = csv.writer(output) writer.writerow(['callid', 'queue', 'agent', 'caller_id', 'outcome', 'wait_time', 'talk_time', 'aht', 'abandon_wait', 'position', 'date', 'time']) for _, row in calls.iterrows(): writer.writerow([ row.get('callid', ''), row.get('queue', ''), row.get('agent', ''), row.get('caller_id', ''), row.get('outcome', ''), row.get('wait_time', ''), row.get('talk_time', ''), row.get('aht', ''), row.get('abandon_wait', ''), row.get('position', ''), row.get('date', ''), row.get('time', ''), ]) output.seek(0) return Response( '\ufeff' + output.getvalue(), mimetype='text/csv; charset=utf-8', headers={'Content-Disposition': 'attachment; filename=queue_report.csv'} ) if __name__ == '__main__': init_db() app.run(host='0.0.0.0', port=5000)