SakibAhmed commited on
Commit
7cfe2d7
·
verified ·
1 Parent(s): f74d61c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +261 -261
app.py CHANGED
@@ -1,261 +1,261 @@
1
- import os
2
- import requests
3
- import csv
4
- from flask import Flask, jsonify, render_template, request, redirect, url_for, session
5
- from flask_socketio import SocketIO, emit
6
- from dotenv import load_dotenv
7
- from datetime import datetime
8
- import logging
9
-
10
- # --- App Configuration ---
11
- load_dotenv()
12
- app = Flask(__name__)
13
- app.secret_key = os.getenv('FLASK_SECRET_KEY', 'a-very-secret-key-change-in-production')
14
- socketio = SocketIO(app)
15
-
16
- # Configure logging
17
- logging.basicConfig(level=logging.INFO)
18
- logger = logging.getLogger(__name__)
19
-
20
- # --- File & Airtable Configuration ---
21
- DATA_DIR = 'data'
22
- USERS_FILE = os.path.join(DATA_DIR, 'users.csv')
23
- AIRTABLE_API_KEY = os.getenv('AIRTABLE_API_KEY')
24
- AIRTABLE_BASE_ID = os.getenv('AIRTABLE_BASE_ID')
25
- APPOINTMENTS_TABLE = os.getenv('AIRTABLE_APPOINTMENTS_TABLE', 'Appointments')
26
- CALLS_TABLE = os.getenv('AIRTABLE_CALLS_TABLE', 'CallRecording')
27
- AIRTABLE_API_URL = f"https://api.airtable.com/v0/{AIRTABLE_BASE_ID}"
28
- RESULTS_PER_PAGE = int(os.getenv('RESULT_LIMIT', 50))
29
- N8N_WEBHOOK_SECRET = os.getenv('N8N_WEBHOOK_SECRET') # Optional secret for webhook security
30
-
31
- # --- Helper Functions ---
32
- def airtable_request(table_name, record_id=None, method='GET', params=None, json_data=None):
33
- """Generic function to make requests to the Airtable API."""
34
- url = f"{AIRTABLE_API_URL}/{table_name}"
35
- if record_id:
36
- url += f"/{record_id}"
37
-
38
- if not AIRTABLE_API_KEY or not AIRTABLE_BASE_ID:
39
- logger.error("Airtable credentials are not set in the .env file.")
40
- return None
41
-
42
- headers = {
43
- 'Authorization': f'Bearer {AIRTABLE_API_KEY}',
44
- 'Content-Type': 'application/json'
45
- }
46
-
47
- try:
48
- response = requests.request(method, url, headers=headers, params=params, json=json_data, timeout=10)
49
- response.raise_for_status()
50
- return response.json()
51
- except requests.exceptions.Timeout:
52
- logger.error(f"Airtable API timeout for {table_name}")
53
- return None
54
- except requests.exceptions.RequestException as e:
55
- logger.error(f"Airtable API Error: {e}")
56
- if hasattr(e, 'response') and e.response is not None:
57
- logger.error(f"Response content: {e.response.text}")
58
- return None
59
-
60
- def flatten_airtable_records(records):
61
- """Flatten Airtable records for easier use in frontend."""
62
- if not records:
63
- return []
64
- return [dict(id=rec.get('id'), **rec.get('fields', {})) for rec in records]
65
-
66
- def get_all_records(table_name):
67
- """Fetch all records from an Airtable table, handling pagination."""
68
- all_records = []
69
- params = {'pageSize': 100}
70
-
71
- while True:
72
- data = airtable_request(table_name, params=params)
73
- if not data:
74
- break
75
-
76
- records = data.get('records', [])
77
- all_records.extend(records)
78
-
79
- # Check for more pages
80
- offset = data.get('offset')
81
- if not offset:
82
- break
83
- params['offset'] = offset
84
-
85
- return all_records
86
-
87
- def validate_user_credentials(username, password):
88
- """Validate user credentials from CSV file."""
89
- try:
90
- if not os.path.exists(USERS_FILE):
91
- logger.error(f"Users file not found: {USERS_FILE}")
92
- return False
93
-
94
- with open(USERS_FILE, mode='r', encoding='utf-8') as infile:
95
- reader = csv.DictReader(infile)
96
- for row in reader:
97
- if row.get('username') == username and row.get('password') == password:
98
- return True
99
- return False
100
- except Exception as e:
101
- logger.error(f"Error validating credentials: {e}")
102
- return False
103
-
104
- def create_default_users_file():
105
- """Create a default users.csv file if it doesn't exist."""
106
- if not os.path.exists(USERS_FILE):
107
- os.makedirs(DATA_DIR, exist_ok=True)
108
- with open(USERS_FILE, 'w', newline='', encoding='utf-8') as f:
109
- writer = csv.writer(f)
110
- writer.writerow(['username', 'password'])
111
- writer.writerow(['admin', 'admin123']) # Default credentials
112
- logger.info(f"Created default users file at {USERS_FILE}")
113
-
114
- # --- Authentication Routes ---
115
- @app.route('/login', methods=['GET', 'POST'])
116
- def login():
117
- error = None
118
- if request.method == 'POST':
119
- username = request.form.get('username', '').strip()
120
- password = request.form.get('password', '').strip()
121
-
122
- if not username or not password:
123
- error = 'Please enter both username and password.'
124
- elif validate_user_credentials(username, password):
125
- session['logged_in'] = True
126
- session['username'] = username
127
- session.permanent = True # Make session persistent
128
- logger.info(f"Successful login for user: {username}")
129
- return redirect(url_for('dashboard'))
130
- else:
131
- error = 'Invalid username or password.'
132
- logger.warning(f"Failed login attempt for user: {username}")
133
-
134
- return render_template('login.html', error=error)
135
-
136
- @app.route('/logout')
137
- def logout():
138
- username = session.get('username', 'Unknown')
139
- session.clear()
140
- logger.info(f"User logged out: {username}")
141
- return redirect(url_for('login'))
142
-
143
- # --- Main Application Routes ---
144
- @app.route('/')
145
- def dashboard():
146
- if not session.get('logged_in'):
147
- return redirect(url_for('login'))
148
- return render_template(
149
- 'dashboard.html',
150
- username=session.get('username', 'User'),
151
- results_per_page=RESULTS_PER_PAGE
152
- )
153
-
154
- # --- API Endpoints ---
155
- @app.route('/api/data')
156
- def get_data():
157
- """Fetch all data from Airtable tables."""
158
- if not session.get('logged_in'):
159
- return jsonify({'error': 'Unauthorized'}), 401
160
-
161
- try:
162
- if not AIRTABLE_API_KEY or not AIRTABLE_BASE_ID:
163
- logger.warning("Airtable not configured, returning empty data")
164
- return jsonify({
165
- 'appointments': [],
166
- 'calls': [],
167
- 'error': 'Airtable not configured. Please check your .env file.'
168
- })
169
-
170
- appointments_records = get_all_records(APPOINTMENTS_TABLE)
171
- calls_records = get_all_records(CALLS_TABLE)
172
-
173
- return jsonify({
174
- 'appointments': flatten_airtable_records(appointments_records),
175
- 'calls': flatten_airtable_records(calls_records)
176
- })
177
- except Exception as e:
178
- logger.error(f"Error fetching data: {e}")
179
- return jsonify({'error': str(e)}), 500
180
-
181
- @app.route('/api/webhook/refresh', methods=['POST'])
182
- def webhook_refresh():
183
- """Webhook endpoint for n8n to trigger a refresh."""
184
- # Optional: Secure webhook with a secret key
185
- if N8N_WEBHOOK_SECRET:
186
- webhook_secret = request.headers.get('X-N8N-Webhook-Secret')
187
- if webhook_secret != N8N_WEBHOOK_SECRET:
188
- logger.warning("Unauthorized webhook attempt.")
189
- return jsonify({'error': 'Unauthorized'}), 401
190
-
191
- logger.info("Webhook received. Triggering client refresh via Socket.IO.")
192
- socketio.emit('refresh_data', {'message': 'Airtable data has changed.'})
193
- return jsonify({'success': True, 'message': 'Refresh event emitted to all clients.'}), 200
194
-
195
- @app.route('/api/appointments/update_status/<record_id>', methods=['PATCH'])
196
- def update_appointment_status(record_id):
197
- """Update the status of an appointment."""
198
- if not session.get('logged_in'):
199
- return jsonify({'error': 'Unauthorized'}), 401
200
-
201
- try:
202
- data = request.json
203
- new_status = data.get('Status')
204
-
205
- if not new_status:
206
- return jsonify({'error': 'Status not provided'}), 400
207
-
208
- valid_statuses = ['incomplete', 'complete', 'missed']
209
- if new_status.lower() not in valid_statuses:
210
- return jsonify({'error': f'Invalid status. Must be one of: {", ".join(valid_statuses)}'}), 400
211
-
212
- payload = {'fields': {'Status': new_status}}
213
- response = airtable_request(APPOINTMENTS_TABLE, record_id=record_id, method='PATCH', json_data=payload)
214
-
215
- if response:
216
- logger.info(f"Updated appointment {record_id} status to {new_status}")
217
- return jsonify({
218
- 'success': True,
219
- 'record': flatten_airtable_records([response])[0]
220
- }), 200
221
- else:
222
- return jsonify({'error': 'Failed to update status in Airtable'}), 500
223
-
224
- except Exception as e:
225
- logger.error(f"Error updating appointment status: {e}")
226
- return jsonify({'error': str(e)}), 500
227
-
228
- # --- Error Handlers ---
229
- @app.errorhandler(404)
230
- def not_found(error):
231
- if request.path.startswith('/api/'):
232
- return jsonify({'error': 'Endpoint not found'}), 404
233
- return render_template('404.html'), 404
234
-
235
- @app.errorhandler(500)
236
- def internal_error(error):
237
- logger.error(f"Internal server error: {error}")
238
- if request.path.startswith('/api/'):
239
- return jsonify({'error': 'Internal server error'}), 500
240
- return render_template('500.html'), 500
241
-
242
- # --- Main Execution ---
243
- if __name__ == '__main__':
244
- os.makedirs(DATA_DIR, exist_ok=True)
245
- create_default_users_file()
246
-
247
- print("=" * 50)
248
- print(" DENTAL ADMIN APPLICATION")
249
- print("=" * 50)
250
-
251
- if not (AIRTABLE_API_KEY and AIRTABLE_BASE_ID):
252
- print("⚠️ WARNING: Airtable credentials not configured!")
253
- else:
254
- print("✅ Airtable configuration loaded")
255
-
256
- print(f"🚀 Starting server on http://localhost:{port}")
257
- print(" Default login: admin / admin123")
258
- print("=" * 50)
259
-
260
- # Use allow_unsafe_werkzeug=True for production deployment
261
- socketio.run(app, debug=False, host=0.0.0.0, port=7860, allow_unsafe_werkzeug=True)
 
1
+ import os
2
+ import requests
3
+ import csv
4
+ from flask import Flask, jsonify, render_template, request, redirect, url_for, session
5
+ from flask_socketio import SocketIO, emit
6
+ from dotenv import load_dotenv
7
+ from datetime import datetime
8
+ import logging
9
+
10
+ # --- App Configuration ---
11
+ load_dotenv()
12
+ app = Flask(__name__)
13
+ app.secret_key = os.getenv('FLASK_SECRET_KEY', 'a-very-secret-key-change-in-production')
14
+ socketio = SocketIO(app)
15
+
16
+ # Configure logging
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # --- File & Airtable Configuration ---
21
+ DATA_DIR = 'data'
22
+ USERS_FILE = os.path.join(DATA_DIR, 'users.csv')
23
+ AIRTABLE_API_KEY = os.getenv('AIRTABLE_API_KEY')
24
+ AIRTABLE_BASE_ID = os.getenv('AIRTABLE_BASE_ID')
25
+ APPOINTMENTS_TABLE = os.getenv('AIRTABLE_APPOINTMENTS_TABLE', 'Appointments')
26
+ CALLS_TABLE = os.getenv('AIRTABLE_CALLS_TABLE', 'CallRecording')
27
+ AIRTABLE_API_URL = f"https://api.airtable.com/v0/{AIRTABLE_BASE_ID}"
28
+ RESULTS_PER_PAGE = int(os.getenv('RESULT_LIMIT', 50))
29
+ N8N_WEBHOOK_SECRET = os.getenv('N8N_WEBHOOK_SECRET') # Optional secret for webhook security
30
+
31
+ # --- Helper Functions ---
32
+ def airtable_request(table_name, record_id=None, method='GET', params=None, json_data=None):
33
+ """Generic function to make requests to the Airtable API."""
34
+ url = f"{AIRTABLE_API_URL}/{table_name}"
35
+ if record_id:
36
+ url += f"/{record_id}"
37
+
38
+ if not AIRTABLE_API_KEY or not AIRTABLE_BASE_ID:
39
+ logger.error("Airtable credentials are not set in the .env file.")
40
+ return None
41
+
42
+ headers = {
43
+ 'Authorization': f'Bearer {AIRTABLE_API_KEY}',
44
+ 'Content-Type': 'application/json'
45
+ }
46
+
47
+ try:
48
+ response = requests.request(method, url, headers=headers, params=params, json=json_data, timeout=10)
49
+ response.raise_for_status()
50
+ return response.json()
51
+ except requests.exceptions.Timeout:
52
+ logger.error(f"Airtable API timeout for {table_name}")
53
+ return None
54
+ except requests.exceptions.RequestException as e:
55
+ logger.error(f"Airtable API Error: {e}")
56
+ if hasattr(e, 'response') and e.response is not None:
57
+ logger.error(f"Response content: {e.response.text}")
58
+ return None
59
+
60
+ def flatten_airtable_records(records):
61
+ """Flatten Airtable records for easier use in frontend."""
62
+ if not records:
63
+ return []
64
+ return [dict(id=rec.get('id'), **rec.get('fields', {})) for rec in records]
65
+
66
+ def get_all_records(table_name):
67
+ """Fetch all records from an Airtable table, handling pagination."""
68
+ all_records = []
69
+ params = {'pageSize': 100}
70
+
71
+ while True:
72
+ data = airtable_request(table_name, params=params)
73
+ if not data:
74
+ break
75
+
76
+ records = data.get('records', [])
77
+ all_records.extend(records)
78
+
79
+ # Check for more pages
80
+ offset = data.get('offset')
81
+ if not offset:
82
+ break
83
+ params['offset'] = offset
84
+
85
+ return all_records
86
+
87
+ def validate_user_credentials(username, password):
88
+ """Validate user credentials from CSV file."""
89
+ try:
90
+ if not os.path.exists(USERS_FILE):
91
+ logger.error(f"Users file not found: {USERS_FILE}")
92
+ return False
93
+
94
+ with open(USERS_FILE, mode='r', encoding='utf-8') as infile:
95
+ reader = csv.DictReader(infile)
96
+ for row in reader:
97
+ if row.get('username') == username and row.get('password') == password:
98
+ return True
99
+ return False
100
+ except Exception as e:
101
+ logger.error(f"Error validating credentials: {e}")
102
+ return False
103
+
104
+ def create_default_users_file():
105
+ """Create a default users.csv file if it doesn't exist."""
106
+ if not os.path.exists(USERS_FILE):
107
+ os.makedirs(DATA_DIR, exist_ok=True)
108
+ with open(USERS_FILE, 'w', newline='', encoding='utf-8') as f:
109
+ writer = csv.writer(f)
110
+ writer.writerow(['username', 'password'])
111
+ writer.writerow(['admin', 'admin123']) # Default credentials
112
+ logger.info(f"Created default users file at {USERS_FILE}")
113
+
114
+ # --- Authentication Routes ---
115
+ @app.route('/login', methods=['GET', 'POST'])
116
+ def login():
117
+ error = None
118
+ if request.method == 'POST':
119
+ username = request.form.get('username', '').strip()
120
+ password = request.form.get('password', '').strip()
121
+
122
+ if not username or not password:
123
+ error = 'Please enter both username and password.'
124
+ elif validate_user_credentials(username, password):
125
+ session['logged_in'] = True
126
+ session['username'] = username
127
+ session.permanent = True # Make session persistent
128
+ logger.info(f"Successful login for user: {username}")
129
+ return redirect(url_for('dashboard'))
130
+ else:
131
+ error = 'Invalid username or password.'
132
+ logger.warning(f"Failed login attempt for user: {username}")
133
+
134
+ return render_template('login.html', error=error)
135
+
136
+ @app.route('/logout')
137
+ def logout():
138
+ username = session.get('username', 'Unknown')
139
+ session.clear()
140
+ logger.info(f"User logged out: {username}")
141
+ return redirect(url_for('login'))
142
+
143
+ # --- Main Application Routes ---
144
+ @app.route('/')
145
+ def dashboard():
146
+ if not session.get('logged_in'):
147
+ return redirect(url_for('login'))
148
+ return render_template(
149
+ 'dashboard.html',
150
+ username=session.get('username', 'User'),
151
+ results_per_page=RESULTS_PER_PAGE
152
+ )
153
+
154
+ # --- API Endpoints ---
155
+ @app.route('/api/data')
156
+ def get_data():
157
+ """Fetch all data from Airtable tables."""
158
+ if not session.get('logged_in'):
159
+ return jsonify({'error': 'Unauthorized'}), 401
160
+
161
+ try:
162
+ if not AIRTABLE_API_KEY or not AIRTABLE_BASE_ID:
163
+ logger.warning("Airtable not configured, returning empty data")
164
+ return jsonify({
165
+ 'appointments': [],
166
+ 'calls': [],
167
+ 'error': 'Airtable not configured. Please check your .env file.'
168
+ })
169
+
170
+ appointments_records = get_all_records(APPOINTMENTS_TABLE)
171
+ calls_records = get_all_records(CALLS_TABLE)
172
+
173
+ return jsonify({
174
+ 'appointments': flatten_airtable_records(appointments_records),
175
+ 'calls': flatten_airtable_records(calls_records)
176
+ })
177
+ except Exception as e:
178
+ logger.error(f"Error fetching data: {e}")
179
+ return jsonify({'error': str(e)}), 500
180
+
181
+ @app.route('/api/webhook/refresh', methods=['POST'])
182
+ def webhook_refresh():
183
+ """Webhook endpoint for n8n to trigger a refresh."""
184
+ # Optional: Secure webhook with a secret key
185
+ if N8N_WEBHOOK_SECRET:
186
+ webhook_secret = request.headers.get('X-N8N-Webhook-Secret')
187
+ if webhook_secret != N8N_WEBHOOK_SECRET:
188
+ logger.warning("Unauthorized webhook attempt.")
189
+ return jsonify({'error': 'Unauthorized'}), 401
190
+
191
+ logger.info("Webhook received. Triggering client refresh via Socket.IO.")
192
+ socketio.emit('refresh_data', {'message': 'Airtable data has changed.'})
193
+ return jsonify({'success': True, 'message': 'Refresh event emitted to all clients.'}), 200
194
+
195
+ @app.route('/api/appointments/update_status/<record_id>', methods=['PATCH'])
196
+ def update_appointment_status(record_id):
197
+ """Update the status of an appointment."""
198
+ if not session.get('logged_in'):
199
+ return jsonify({'error': 'Unauthorized'}), 401
200
+
201
+ try:
202
+ data = request.json
203
+ new_status = data.get('Status')
204
+
205
+ if not new_status:
206
+ return jsonify({'error': 'Status not provided'}), 400
207
+
208
+ valid_statuses = ['incomplete', 'complete', 'missed']
209
+ if new_status.lower() not in valid_statuses:
210
+ return jsonify({'error': f'Invalid status. Must be one of: {", ".join(valid_statuses)}'}), 400
211
+
212
+ payload = {'fields': {'Status': new_status}}
213
+ response = airtable_request(APPOINTMENTS_TABLE, record_id=record_id, method='PATCH', json_data=payload)
214
+
215
+ if response:
216
+ logger.info(f"Updated appointment {record_id} status to {new_status}")
217
+ return jsonify({
218
+ 'success': True,
219
+ 'record': flatten_airtable_records([response])[0]
220
+ }), 200
221
+ else:
222
+ return jsonify({'error': 'Failed to update status in Airtable'}), 500
223
+
224
+ except Exception as e:
225
+ logger.error(f"Error updating appointment status: {e}")
226
+ return jsonify({'error': str(e)}), 500
227
+
228
+ # --- Error Handlers ---
229
+ @app.errorhandler(404)
230
+ def not_found(error):
231
+ if request.path.startswith('/api/'):
232
+ return jsonify({'error': 'Endpoint not found'}), 404
233
+ return render_template('404.html'), 404
234
+
235
+ @app.errorhandler(500)
236
+ def internal_error(error):
237
+ logger.error(f"Internal server error: {error}")
238
+ if request.path.startswith('/api/'):
239
+ return jsonify({'error': 'Internal server error'}), 500
240
+ return render_template('500.html'), 500
241
+
242
+ # --- Main Execution ---
243
+ if __name__ == '__main__':
244
+ os.makedirs(DATA_DIR, exist_ok=True)
245
+ create_default_users_file()
246
+
247
+ print("=" * 50)
248
+ print(" DENTAL ADMIN APPLICATION")
249
+ print("=" * 50)
250
+
251
+ if not (AIRTABLE_API_KEY and AIRTABLE_BASE_ID):
252
+ print("⚠️ WARNING: Airtable credentials not configured!")
253
+ else:
254
+ print("✅ Airtable configuration loaded")
255
+
256
+ print(f"🚀 Starting server on http://localhost:{port}")
257
+ print(" Default login: admin / admin123")
258
+ print("=" * 50)
259
+
260
+ # Use allow_unsafe_werkzeug=True for production deployment
261
+ socketio.run(app, debug=False, host="0.0.0.0", port=7860, allow_unsafe_werkzeug=True)