Spaces:
Paused
Paused
Dipika Bhadane commited on
Commit ·
df568a8
1
Parent(s): 4904300
changes file upload and login register
Browse files- app.py +197 -8
- auth.py +119 -0
- health_passport.py +258 -37
- pdf_export.py +142 -0
- requirements.txt +4 -0
- static/js/main.js +319 -6
- steps.txt +5 -1
- templates/base.html +22 -0
- templates/emergency_help.html +75 -0
- templates/emergency_view.html +96 -0
- templates/index.html +3 -0
- templates/login.html +32 -0
- templates/passport.html +99 -40
- templates/register.html +40 -0
app.py
CHANGED
|
@@ -1,20 +1,37 @@
|
|
| 1 |
-
from flask import Flask, render_template, request, jsonify, Response
|
| 2 |
import os
|
| 3 |
|
|
|
|
|
|
|
| 4 |
from verify_v2 import verify_claim
|
| 5 |
import db
|
| 6 |
import health_passport as hp
|
| 7 |
import ocr_utils
|
| 8 |
import translate_utils
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
app = Flask(__name__)
|
| 13 |
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-only-change-me")
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# Make sure tables exist before anything else runs
|
| 16 |
db.init_db()
|
| 17 |
hp.init_passport_table()
|
|
|
|
| 18 |
|
| 19 |
# --- PAGE ROUTING ---
|
| 20 |
|
|
@@ -39,8 +56,91 @@ def predictor():
|
|
| 39 |
return render_template('predictor.html')
|
| 40 |
|
| 41 |
@app.route('/passport')
|
|
|
|
| 42 |
def passport():
|
| 43 |
-
return render_template('passport.html')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
# --- API ENDPOINTS ---
|
| 46 |
|
|
@@ -153,6 +253,7 @@ def api_predict_spread():
|
|
| 153 |
# risk features (verdict, confidence, entities) are grounded in real
|
| 154 |
# evidence, not guessed independently.
|
| 155 |
verification = verify_claim(claim)
|
|
|
|
| 156 |
graph_data = predict_spread(
|
| 157 |
claim_text=claim,
|
| 158 |
verdict=verification.get("verdict", "Unverified"),
|
|
@@ -163,6 +264,7 @@ def api_predict_spread():
|
|
| 163 |
|
| 164 |
|
| 165 |
@app.route('/api/passport', methods=['GET', 'POST'])
|
|
|
|
| 166 |
def api_passport():
|
| 167 |
if request.method == 'POST':
|
| 168 |
data = request.get_json(force=True) or {}
|
|
@@ -173,23 +275,110 @@ def api_passport():
|
|
| 173 |
}
|
| 174 |
for key, default in required_defaults.items():
|
| 175 |
data.setdefault(key, default)
|
| 176 |
-
hp.save_passport(data)
|
| 177 |
return jsonify({"status": "saved"})
|
| 178 |
|
| 179 |
-
passport_data = hp.get_passport()
|
| 180 |
if not passport_data:
|
| 181 |
return jsonify(None)
|
| 182 |
return jsonify(passport_data)
|
| 183 |
|
| 184 |
|
| 185 |
@app.route('/api/passport/qr', methods=['GET'])
|
|
|
|
| 186 |
def api_passport_qr():
|
| 187 |
-
passport_data = hp.get_passport()
|
| 188 |
if not passport_data:
|
| 189 |
return jsonify({"error": "No passport saved yet."}), 404
|
| 190 |
-
png_bytes = hp.generate_qr_code(passport_data)
|
| 191 |
return Response(png_bytes, mimetype='image/png')
|
| 192 |
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
if __name__ == '__main__':
|
| 195 |
app.run(debug=True, port=5000, use_reloader=False)
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify, Response, redirect, url_for, flash, send_file
|
| 2 |
import os
|
| 3 |
|
| 4 |
+
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
| 5 |
+
|
| 6 |
from verify_v2 import verify_claim
|
| 7 |
import db
|
| 8 |
import health_passport as hp
|
| 9 |
import ocr_utils
|
| 10 |
import translate_utils
|
| 11 |
+
import auth
|
| 12 |
+
import pdf_export
|
| 13 |
+
import sys, os
|
| 14 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "gnn"))
|
| 15 |
+
from gnn_predict import predict_spread
|
| 16 |
|
| 17 |
app = Flask(__name__)
|
| 18 |
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-only-change-me")
|
| 19 |
|
| 20 |
+
login_manager = LoginManager()
|
| 21 |
+
login_manager.init_app(app)
|
| 22 |
+
login_manager.login_view = "login"
|
| 23 |
+
login_manager.login_message = "Please log in to access your Health Passport."
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@login_manager.user_loader
|
| 27 |
+
def load_user(user_id):
|
| 28 |
+
return auth.get_user_by_id(user_id)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
# Make sure tables exist before anything else runs
|
| 32 |
db.init_db()
|
| 33 |
hp.init_passport_table()
|
| 34 |
+
auth.init_users_table()
|
| 35 |
|
| 36 |
# --- PAGE ROUTING ---
|
| 37 |
|
|
|
|
| 56 |
return render_template('predictor.html')
|
| 57 |
|
| 58 |
@app.route('/passport')
|
| 59 |
+
@login_required
|
| 60 |
def passport():
|
| 61 |
+
return render_template('passport.html', user=current_user)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@app.route('/register', methods=['GET', 'POST'])
|
| 65 |
+
def register():
|
| 66 |
+
if current_user.is_authenticated:
|
| 67 |
+
return redirect(url_for('passport'))
|
| 68 |
+
|
| 69 |
+
if request.method == 'POST':
|
| 70 |
+
name = request.form.get('name', '').strip()
|
| 71 |
+
email = request.form.get('email', '').strip()
|
| 72 |
+
password = request.form.get('password', '')
|
| 73 |
+
confirm_password = request.form.get('confirm_password', '')
|
| 74 |
+
|
| 75 |
+
if not name or not email or not password:
|
| 76 |
+
flash("Please fill in all fields.", "error")
|
| 77 |
+
return render_template('register.html')
|
| 78 |
+
|
| 79 |
+
if password != confirm_password:
|
| 80 |
+
flash("Passwords do not match.", "error")
|
| 81 |
+
return render_template('register.html')
|
| 82 |
+
|
| 83 |
+
if len(password) < 8:
|
| 84 |
+
flash("Password must be at least 8 characters.", "error")
|
| 85 |
+
return render_template('register.html')
|
| 86 |
+
|
| 87 |
+
user = auth.create_user(name, email, password)
|
| 88 |
+
if user is None:
|
| 89 |
+
flash("An account with that email already exists. Try logging in instead.", "error")
|
| 90 |
+
return render_template('register.html')
|
| 91 |
+
|
| 92 |
+
login_user(user)
|
| 93 |
+
return redirect(url_for('passport'))
|
| 94 |
+
|
| 95 |
+
return render_template('register.html')
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@app.route('/login', methods=['GET', 'POST'])
|
| 99 |
+
def login():
|
| 100 |
+
if current_user.is_authenticated:
|
| 101 |
+
return redirect(url_for('passport'))
|
| 102 |
+
|
| 103 |
+
if request.method == 'POST':
|
| 104 |
+
email = request.form.get('email', '').strip()
|
| 105 |
+
password = request.form.get('password', '')
|
| 106 |
+
|
| 107 |
+
user = auth.verify_login(email, password)
|
| 108 |
+
if user is None:
|
| 109 |
+
flash("Incorrect email or password.", "error")
|
| 110 |
+
return render_template('login.html')
|
| 111 |
+
|
| 112 |
+
login_user(user)
|
| 113 |
+
next_page = request.args.get('next')
|
| 114 |
+
return redirect(next_page or url_for('passport'))
|
| 115 |
+
|
| 116 |
+
return render_template('login.html')
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@app.route('/logout')
|
| 120 |
+
@login_required
|
| 121 |
+
def logout():
|
| 122 |
+
logout_user()
|
| 123 |
+
return redirect(url_for('home'))
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@app.route('/emergency-help')
|
| 127 |
+
def emergency_help():
|
| 128 |
+
return render_template('emergency_help.html')
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@app.route('/emergency/<share_token>')
|
| 132 |
+
def emergency_view(share_token):
|
| 133 |
+
"""
|
| 134 |
+
Public, read-only emergency view -- this is what the QR code opens.
|
| 135 |
+
No login required, intentionally shows only emergency-relevant fields.
|
| 136 |
+
"""
|
| 137 |
+
passport_data = hp.get_passport_by_token(share_token)
|
| 138 |
+
if not passport_data:
|
| 139 |
+
return render_template('emergency_view.html', passport=None), 404
|
| 140 |
+
|
| 141 |
+
surgeries = hp.get_surgeries(passport_data['user_id'])
|
| 142 |
+
vaccinations = hp.get_vaccinations(passport_data['user_id'])
|
| 143 |
+
return render_template('emergency_view.html', passport=passport_data, surgeries=surgeries, vaccinations=vaccinations)
|
| 144 |
|
| 145 |
# --- API ENDPOINTS ---
|
| 146 |
|
|
|
|
| 253 |
# risk features (verdict, confidence, entities) are grounded in real
|
| 254 |
# evidence, not guessed independently.
|
| 255 |
verification = verify_claim(claim)
|
| 256 |
+
|
| 257 |
graph_data = predict_spread(
|
| 258 |
claim_text=claim,
|
| 259 |
verdict=verification.get("verdict", "Unverified"),
|
|
|
|
| 264 |
|
| 265 |
|
| 266 |
@app.route('/api/passport', methods=['GET', 'POST'])
|
| 267 |
+
@login_required
|
| 268 |
def api_passport():
|
| 269 |
if request.method == 'POST':
|
| 270 |
data = request.get_json(force=True) or {}
|
|
|
|
| 275 |
}
|
| 276 |
for key, default in required_defaults.items():
|
| 277 |
data.setdefault(key, default)
|
| 278 |
+
hp.save_passport(int(current_user.id), data)
|
| 279 |
return jsonify({"status": "saved"})
|
| 280 |
|
| 281 |
+
passport_data = hp.get_passport(int(current_user.id))
|
| 282 |
if not passport_data:
|
| 283 |
return jsonify(None)
|
| 284 |
return jsonify(passport_data)
|
| 285 |
|
| 286 |
|
| 287 |
@app.route('/api/passport/qr', methods=['GET'])
|
| 288 |
+
@login_required
|
| 289 |
def api_passport_qr():
|
| 290 |
+
passport_data = hp.get_passport(int(current_user.id))
|
| 291 |
if not passport_data:
|
| 292 |
return jsonify({"error": "No passport saved yet."}), 404
|
| 293 |
+
png_bytes = hp.generate_qr_code(passport_data['share_token'], request.host_url)
|
| 294 |
return Response(png_bytes, mimetype='image/png')
|
| 295 |
|
| 296 |
|
| 297 |
+
@app.route('/api/passport/surgeries', methods=['GET', 'POST'])
|
| 298 |
+
@login_required
|
| 299 |
+
def api_surgeries():
|
| 300 |
+
if request.method == 'POST':
|
| 301 |
+
data = request.get_json(force=True) or {}
|
| 302 |
+
hp.add_surgery(int(current_user.id), data.get('year', ''), data.get('description', ''))
|
| 303 |
+
return jsonify({"status": "added"})
|
| 304 |
+
return jsonify(hp.get_surgeries(int(current_user.id)))
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
@app.route('/api/passport/surgeries/<int:surgery_id>', methods=['DELETE'])
|
| 308 |
+
@login_required
|
| 309 |
+
def api_delete_surgery(surgery_id):
|
| 310 |
+
hp.delete_surgery(int(current_user.id), surgery_id)
|
| 311 |
+
return jsonify({"status": "deleted"})
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
@app.route('/api/passport/vaccinations', methods=['GET', 'POST'])
|
| 315 |
+
@login_required
|
| 316 |
+
def api_vaccinations():
|
| 317 |
+
if request.method == 'POST':
|
| 318 |
+
data = request.get_json(force=True) or {}
|
| 319 |
+
hp.add_vaccination(int(current_user.id), data.get('vaccine_name', ''), data.get('month', ''), data.get('year', ''))
|
| 320 |
+
return jsonify({"status": "added"})
|
| 321 |
+
return jsonify(hp.get_vaccinations(int(current_user.id)))
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
@app.route('/api/passport/vaccinations/<int:vaccination_id>', methods=['DELETE'])
|
| 325 |
+
@login_required
|
| 326 |
+
def api_delete_vaccination(vaccination_id):
|
| 327 |
+
hp.delete_vaccination(int(current_user.id), vaccination_id)
|
| 328 |
+
return jsonify({"status": "deleted"})
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
@app.route('/api/passport/reports', methods=['GET', 'POST'])
|
| 332 |
+
@login_required
|
| 333 |
+
def api_reports():
|
| 334 |
+
if request.method == 'POST':
|
| 335 |
+
if 'file' not in request.files or request.files['file'].filename == '':
|
| 336 |
+
return jsonify({"error": "No file provided."}), 400
|
| 337 |
+
file_storage = request.files['file']
|
| 338 |
+
category = request.form.get('category', 'Other')
|
| 339 |
+
month = request.form.get('month', '')
|
| 340 |
+
year = request.form.get('year', '')
|
| 341 |
+
report = hp.save_report_file(int(current_user.id), file_storage, category, month, year)
|
| 342 |
+
return jsonify(report)
|
| 343 |
+
|
| 344 |
+
return jsonify(hp.get_reports(int(current_user.id)))
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
@app.route('/api/passport/reports/<int:report_id>', methods=['DELETE'])
|
| 348 |
+
@login_required
|
| 349 |
+
def api_delete_report(report_id):
|
| 350 |
+
hp.delete_report(int(current_user.id), report_id)
|
| 351 |
+
return jsonify({"status": "deleted"})
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
@app.route('/api/passport/reports/<int:report_id>/download', methods=['GET'])
|
| 355 |
+
@login_required
|
| 356 |
+
def api_download_report(report_id):
|
| 357 |
+
report = hp.get_report_by_id(int(current_user.id), report_id)
|
| 358 |
+
if not report or not os.path.exists(report['stored_path']):
|
| 359 |
+
return jsonify({"error": "Report not found."}), 404
|
| 360 |
+
return send_file(report['stored_path'], as_attachment=True, download_name=report['filename'])
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
@app.route('/api/passport/pdf', methods=['GET'])
|
| 364 |
+
@login_required
|
| 365 |
+
def api_passport_pdf():
|
| 366 |
+
passport_data = hp.get_passport(int(current_user.id))
|
| 367 |
+
if not passport_data:
|
| 368 |
+
return jsonify({"error": "No passport saved yet."}), 404
|
| 369 |
+
|
| 370 |
+
surgeries = hp.get_surgeries(int(current_user.id))
|
| 371 |
+
vaccinations = hp.get_vaccinations(int(current_user.id))
|
| 372 |
+
qr_bytes = hp.generate_qr_code(passport_data['share_token'], request.host_url)
|
| 373 |
+
|
| 374 |
+
pdf_bytes = pdf_export.generate_passport_pdf(passport_data, surgeries, vaccinations, qr_bytes)
|
| 375 |
+
|
| 376 |
+
return Response(
|
| 377 |
+
pdf_bytes,
|
| 378 |
+
mimetype='application/pdf',
|
| 379 |
+
headers={"Content-Disposition": "attachment; filename=VeriMed_Health_Passport.pdf"},
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
|
| 383 |
if __name__ == '__main__':
|
| 384 |
app.run(debug=True, port=5000, use_reloader=False)
|
auth.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Authentication module.
|
| 3 |
+
|
| 4 |
+
Real password hashing (werkzeug's generate_password_hash / check_password_hash
|
| 5 |
+
-- salted, uses PBKDF2/scrypt depending on werkzeug version) and Flask-Login
|
| 6 |
+
session management.
|
| 7 |
+
|
| 8 |
+
NOTE ON SCOPE: this is hackathon-appropriate auth -- correct password hashing,
|
| 9 |
+
proper session cookies via Flask-Login, but no email verification, no
|
| 10 |
+
password reset flow, no rate limiting on login attempts. Be upfront about
|
| 11 |
+
that if asked -- it's a normal, expected scope cut, not a hidden flaw.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import sqlite3
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 17 |
+
from flask_login import UserMixin
|
| 18 |
+
|
| 19 |
+
DB_FILE = "verimed.db"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class User(UserMixin):
|
| 23 |
+
"""Flask-Login needs an object with .id, .is_authenticated, etc. -- UserMixin provides those."""
|
| 24 |
+
def __init__(self, id, name, email):
|
| 25 |
+
self.id = str(id)
|
| 26 |
+
self.name = name
|
| 27 |
+
self.email = email
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def init_users_table():
|
| 31 |
+
conn = sqlite3.connect(DB_FILE)
|
| 32 |
+
cur = conn.cursor()
|
| 33 |
+
cur.execute("""
|
| 34 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 35 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 36 |
+
name TEXT NOT NULL,
|
| 37 |
+
email TEXT UNIQUE NOT NULL,
|
| 38 |
+
password_hash TEXT NOT NULL,
|
| 39 |
+
created_at TEXT NOT NULL
|
| 40 |
+
)
|
| 41 |
+
""")
|
| 42 |
+
conn.commit()
|
| 43 |
+
conn.close()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def create_user(name: str, email: str, password: str) -> "User | None":
|
| 47 |
+
"""Returns the created User, or None if the email is already registered."""
|
| 48 |
+
conn = sqlite3.connect(DB_FILE)
|
| 49 |
+
cur = conn.cursor()
|
| 50 |
+
cur.execute("SELECT id FROM users WHERE email = ?", (email.lower().strip(),))
|
| 51 |
+
if cur.fetchone():
|
| 52 |
+
conn.close()
|
| 53 |
+
return None # email already taken
|
| 54 |
+
|
| 55 |
+
password_hash = generate_password_hash(password)
|
| 56 |
+
now = datetime.now().isoformat(timespec="seconds")
|
| 57 |
+
cur.execute(
|
| 58 |
+
"INSERT INTO users (name, email, password_hash, created_at) VALUES (?, ?, ?, ?)",
|
| 59 |
+
(name.strip(), email.lower().strip(), password_hash, now),
|
| 60 |
+
)
|
| 61 |
+
conn.commit()
|
| 62 |
+
user_id = cur.lastrowid
|
| 63 |
+
conn.close()
|
| 64 |
+
return User(user_id, name.strip(), email.lower().strip())
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def verify_login(email: str, password: str) -> "User | None":
|
| 68 |
+
"""Returns the User if email+password match, else None."""
|
| 69 |
+
conn = sqlite3.connect(DB_FILE)
|
| 70 |
+
conn.row_factory = sqlite3.Row
|
| 71 |
+
cur = conn.cursor()
|
| 72 |
+
cur.execute("SELECT * FROM users WHERE email = ?", (email.lower().strip(),))
|
| 73 |
+
row = cur.fetchone()
|
| 74 |
+
conn.close()
|
| 75 |
+
|
| 76 |
+
if not row:
|
| 77 |
+
return None
|
| 78 |
+
if not check_password_hash(row["password_hash"], password):
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
return User(row["id"], row["name"], row["email"])
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def get_user_by_id(user_id: str) -> "User | None":
|
| 85 |
+
"""Required by Flask-Login's user_loader callback."""
|
| 86 |
+
conn = sqlite3.connect(DB_FILE)
|
| 87 |
+
conn.row_factory = sqlite3.Row
|
| 88 |
+
cur = conn.cursor()
|
| 89 |
+
cur.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 90 |
+
row = cur.fetchone()
|
| 91 |
+
conn.close()
|
| 92 |
+
|
| 93 |
+
if not row:
|
| 94 |
+
return None
|
| 95 |
+
return User(row["id"], row["name"], row["email"])
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
if __name__ == "__main__":
|
| 99 |
+
# Quick manual test -- run: python auth.py
|
| 100 |
+
import os
|
| 101 |
+
if os.path.exists(DB_FILE):
|
| 102 |
+
os.remove(DB_FILE) # clean slate for this test only
|
| 103 |
+
|
| 104 |
+
init_users_table()
|
| 105 |
+
|
| 106 |
+
user = create_user("Test User", "test@example.com", "correct-password-123")
|
| 107 |
+
print(f"Created user: {user.name} ({user.email}), id={user.id}")
|
| 108 |
+
|
| 109 |
+
dupe = create_user("Someone Else", "test@example.com", "whatever")
|
| 110 |
+
print(f"Duplicate email rejected: {dupe is None}")
|
| 111 |
+
|
| 112 |
+
good_login = verify_login("test@example.com", "correct-password-123")
|
| 113 |
+
print(f"Correct password login: {'SUCCESS' if good_login else 'FAILED'}")
|
| 114 |
+
|
| 115 |
+
bad_login = verify_login("test@example.com", "wrong-password")
|
| 116 |
+
print(f"Wrong password login: {'correctly rejected' if bad_login is None else 'SECURITY BUG'}")
|
| 117 |
+
|
| 118 |
+
fetched = get_user_by_id(user.id)
|
| 119 |
+
print(f"Fetched by id: {fetched.name} ({fetched.email})")
|
health_passport.py
CHANGED
|
@@ -1,29 +1,40 @@
|
|
| 1 |
"""
|
| 2 |
-
Health Passport module.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
"""
|
| 12 |
|
| 13 |
import sqlite3
|
| 14 |
-
import json
|
| 15 |
-
import qrcode
|
| 16 |
import io
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
DB_FILE = "verimed.db"
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
def init_passport_table():
|
| 22 |
conn = sqlite3.connect(DB_FILE)
|
| 23 |
cur = conn.cursor()
|
|
|
|
| 24 |
cur.execute("""
|
| 25 |
CREATE TABLE IF NOT EXISTS health_passport (
|
| 26 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
| 27 |
full_name TEXT,
|
| 28 |
blood_group TEXT,
|
| 29 |
date_of_birth TEXT,
|
|
@@ -32,21 +43,59 @@ def init_passport_table():
|
|
| 32 |
current_medicines TEXT,
|
| 33 |
emergency_contact_name TEXT,
|
| 34 |
emergency_contact_phone TEXT,
|
|
|
|
| 35 |
updated_at TEXT
|
| 36 |
)
|
| 37 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
conn.commit()
|
| 39 |
conn.close()
|
|
|
|
|
|
|
| 40 |
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
def save_passport(data: dict):
|
| 43 |
-
"""Upserts the single local passport record (id=1, since this is single-user demo)."""
|
| 44 |
conn = sqlite3.connect(DB_FILE)
|
| 45 |
cur = conn.cursor()
|
| 46 |
-
cur.execute("SELECT id FROM health_passport
|
| 47 |
existing = cur.fetchone()
|
| 48 |
-
|
| 49 |
-
from datetime import datetime
|
| 50 |
now = datetime.now().isoformat(timespec="seconds")
|
| 51 |
|
| 52 |
if existing:
|
|
@@ -55,60 +104,232 @@ def save_passport(data: dict):
|
|
| 55 |
full_name=?, blood_group=?, date_of_birth=?, allergies=?,
|
| 56 |
chronic_conditions=?, current_medicines=?,
|
| 57 |
emergency_contact_name=?, emergency_contact_phone=?, updated_at=?
|
| 58 |
-
WHERE
|
| 59 |
""", (
|
| 60 |
data["full_name"], data["blood_group"], data["date_of_birth"],
|
| 61 |
data["allergies"], data["chronic_conditions"], data["current_medicines"],
|
| 62 |
data["emergency_contact_name"], data["emergency_contact_phone"], now,
|
| 63 |
-
|
| 64 |
))
|
| 65 |
else:
|
|
|
|
| 66 |
cur.execute("""
|
| 67 |
INSERT INTO health_passport
|
| 68 |
-
(full_name, blood_group, date_of_birth, allergies, chronic_conditions,
|
| 69 |
-
current_medicines, emergency_contact_name, emergency_contact_phone, updated_at)
|
| 70 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 71 |
""", (
|
| 72 |
-
data["full_name"], data["blood_group"], data["date_of_birth"],
|
| 73 |
data["allergies"], data["chronic_conditions"], data["current_medicines"],
|
| 74 |
-
data["emergency_contact_name"], data["emergency_contact_phone"], now,
|
| 75 |
))
|
| 76 |
conn.commit()
|
| 77 |
conn.close()
|
| 78 |
|
| 79 |
|
| 80 |
-
def get_passport() -> dict | None:
|
| 81 |
conn = sqlite3.connect(DB_FILE)
|
| 82 |
conn.row_factory = sqlite3.Row
|
| 83 |
cur = conn.cursor()
|
| 84 |
-
cur.execute("SELECT * FROM health_passport
|
| 85 |
row = cur.fetchone()
|
| 86 |
conn.close()
|
| 87 |
return dict(row) if row else None
|
| 88 |
|
| 89 |
|
| 90 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
"""
|
| 92 |
-
|
| 93 |
-
|
| 94 |
"""
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
qr = qrcode.QRCode(version=None, box_size=8, border=3)
|
| 108 |
-
qr.add_data(
|
| 109 |
qr.make(fit=True)
|
| 110 |
img = qr.make_image(fill_color="#0F6FFF", back_color="white")
|
| 111 |
|
| 112 |
buf = io.BytesIO()
|
| 113 |
img.save(buf, format="PNG")
|
| 114 |
return buf.getvalue()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
Health Passport module -- v2, per-user.
|
| 3 |
|
| 4 |
+
Every table now has a user_id foreign key, so each logged-in user has their
|
| 5 |
+
own private passport, surgery history, vaccination record, and uploaded
|
| 6 |
+
reports.
|
| 7 |
|
| 8 |
+
QR CODE DESIGN CHANGE FROM v1:
|
| 9 |
+
v1 encoded the passport data as raw text directly in the QR code. That only
|
| 10 |
+
works if the scanning device has software that can parse that specific text
|
| 11 |
+
format. v2 instead gives each user a random, unguessable share_token and
|
| 12 |
+
encodes a URL (e.g. http://yourhost/emergency/<token>) in the QR code. Any
|
| 13 |
+
phone camera can scan it and it opens directly in a browser -- no app
|
| 14 |
+
needed on the doctor's side. That page is intentionally read-only and shows
|
| 15 |
+
only emergency-relevant fields, not the full account or uploaded documents.
|
| 16 |
"""
|
| 17 |
|
| 18 |
import sqlite3
|
|
|
|
|
|
|
| 19 |
import io
|
| 20 |
+
import os
|
| 21 |
+
import secrets
|
| 22 |
+
from datetime import datetime
|
| 23 |
+
|
| 24 |
+
import qrcode
|
| 25 |
|
| 26 |
DB_FILE = "verimed.db"
|
| 27 |
+
UPLOAD_DIR = "uploads/medical_reports"
|
| 28 |
|
| 29 |
|
| 30 |
def init_passport_table():
|
| 31 |
conn = sqlite3.connect(DB_FILE)
|
| 32 |
cur = conn.cursor()
|
| 33 |
+
|
| 34 |
cur.execute("""
|
| 35 |
CREATE TABLE IF NOT EXISTS health_passport (
|
| 36 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 37 |
+
user_id INTEGER NOT NULL UNIQUE,
|
| 38 |
full_name TEXT,
|
| 39 |
blood_group TEXT,
|
| 40 |
date_of_birth TEXT,
|
|
|
|
| 43 |
current_medicines TEXT,
|
| 44 |
emergency_contact_name TEXT,
|
| 45 |
emergency_contact_phone TEXT,
|
| 46 |
+
share_token TEXT UNIQUE,
|
| 47 |
updated_at TEXT
|
| 48 |
)
|
| 49 |
""")
|
| 50 |
+
|
| 51 |
+
cur.execute("""
|
| 52 |
+
CREATE TABLE IF NOT EXISTS surgeries (
|
| 53 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 54 |
+
user_id INTEGER NOT NULL,
|
| 55 |
+
year TEXT,
|
| 56 |
+
description TEXT,
|
| 57 |
+
created_at TEXT
|
| 58 |
+
)
|
| 59 |
+
""")
|
| 60 |
+
|
| 61 |
+
cur.execute("""
|
| 62 |
+
CREATE TABLE IF NOT EXISTS vaccinations (
|
| 63 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 64 |
+
user_id INTEGER NOT NULL,
|
| 65 |
+
vaccine_name TEXT,
|
| 66 |
+
month TEXT,
|
| 67 |
+
year TEXT,
|
| 68 |
+
created_at TEXT
|
| 69 |
+
)
|
| 70 |
+
""")
|
| 71 |
+
|
| 72 |
+
cur.execute("""
|
| 73 |
+
CREATE TABLE IF NOT EXISTS medical_reports (
|
| 74 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 75 |
+
user_id INTEGER NOT NULL,
|
| 76 |
+
filename TEXT,
|
| 77 |
+
stored_path TEXT,
|
| 78 |
+
category TEXT,
|
| 79 |
+
month TEXT,
|
| 80 |
+
year TEXT,
|
| 81 |
+
uploaded_at TEXT
|
| 82 |
+
)
|
| 83 |
+
""")
|
| 84 |
+
|
| 85 |
conn.commit()
|
| 86 |
conn.close()
|
| 87 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 88 |
+
|
| 89 |
|
| 90 |
+
# ---------------------------------------------------------------------------
|
| 91 |
+
# Core passport (personal details)
|
| 92 |
+
# ---------------------------------------------------------------------------
|
| 93 |
|
| 94 |
+
def save_passport(user_id: int, data: dict):
|
|
|
|
| 95 |
conn = sqlite3.connect(DB_FILE)
|
| 96 |
cur = conn.cursor()
|
| 97 |
+
cur.execute("SELECT id, share_token FROM health_passport WHERE user_id = ?", (user_id,))
|
| 98 |
existing = cur.fetchone()
|
|
|
|
|
|
|
| 99 |
now = datetime.now().isoformat(timespec="seconds")
|
| 100 |
|
| 101 |
if existing:
|
|
|
|
| 104 |
full_name=?, blood_group=?, date_of_birth=?, allergies=?,
|
| 105 |
chronic_conditions=?, current_medicines=?,
|
| 106 |
emergency_contact_name=?, emergency_contact_phone=?, updated_at=?
|
| 107 |
+
WHERE user_id=?
|
| 108 |
""", (
|
| 109 |
data["full_name"], data["blood_group"], data["date_of_birth"],
|
| 110 |
data["allergies"], data["chronic_conditions"], data["current_medicines"],
|
| 111 |
data["emergency_contact_name"], data["emergency_contact_phone"], now,
|
| 112 |
+
user_id,
|
| 113 |
))
|
| 114 |
else:
|
| 115 |
+
share_token = secrets.token_urlsafe(16)
|
| 116 |
cur.execute("""
|
| 117 |
INSERT INTO health_passport
|
| 118 |
+
(user_id, full_name, blood_group, date_of_birth, allergies, chronic_conditions,
|
| 119 |
+
current_medicines, emergency_contact_name, emergency_contact_phone, share_token, updated_at)
|
| 120 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 121 |
""", (
|
| 122 |
+
user_id, data["full_name"], data["blood_group"], data["date_of_birth"],
|
| 123 |
data["allergies"], data["chronic_conditions"], data["current_medicines"],
|
| 124 |
+
data["emergency_contact_name"], data["emergency_contact_phone"], share_token, now,
|
| 125 |
))
|
| 126 |
conn.commit()
|
| 127 |
conn.close()
|
| 128 |
|
| 129 |
|
| 130 |
+
def get_passport(user_id: int) -> dict | None:
|
| 131 |
conn = sqlite3.connect(DB_FILE)
|
| 132 |
conn.row_factory = sqlite3.Row
|
| 133 |
cur = conn.cursor()
|
| 134 |
+
cur.execute("SELECT * FROM health_passport WHERE user_id = ?", (user_id,))
|
| 135 |
row = cur.fetchone()
|
| 136 |
conn.close()
|
| 137 |
return dict(row) if row else None
|
| 138 |
|
| 139 |
|
| 140 |
+
def get_passport_by_token(share_token: str) -> dict | None:
|
| 141 |
+
"""Used by the public /emergency/<token> view -- no auth required."""
|
| 142 |
+
conn = sqlite3.connect(DB_FILE)
|
| 143 |
+
conn.row_factory = sqlite3.Row
|
| 144 |
+
cur = conn.cursor()
|
| 145 |
+
cur.execute("SELECT * FROM health_passport WHERE share_token = ?", (share_token,))
|
| 146 |
+
row = cur.fetchone()
|
| 147 |
+
conn.close()
|
| 148 |
+
return dict(row) if row else None
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# ---------------------------------------------------------------------------
|
| 152 |
+
# Surgeries
|
| 153 |
+
# ---------------------------------------------------------------------------
|
| 154 |
+
|
| 155 |
+
def add_surgery(user_id: int, year: str, description: str):
|
| 156 |
+
conn = sqlite3.connect(DB_FILE)
|
| 157 |
+
cur = conn.cursor()
|
| 158 |
+
cur.execute(
|
| 159 |
+
"INSERT INTO surgeries (user_id, year, description, created_at) VALUES (?, ?, ?, ?)",
|
| 160 |
+
(user_id, year, description, datetime.now().isoformat(timespec="seconds")),
|
| 161 |
+
)
|
| 162 |
+
conn.commit()
|
| 163 |
+
conn.close()
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def get_surgeries(user_id: int) -> list[dict]:
|
| 167 |
+
conn = sqlite3.connect(DB_FILE)
|
| 168 |
+
conn.row_factory = sqlite3.Row
|
| 169 |
+
cur = conn.cursor()
|
| 170 |
+
cur.execute("SELECT * FROM surgeries WHERE user_id = ? ORDER BY year DESC", (user_id,))
|
| 171 |
+
rows = [dict(r) for r in cur.fetchall()]
|
| 172 |
+
conn.close()
|
| 173 |
+
return rows
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def delete_surgery(user_id: int, surgery_id: int):
|
| 177 |
+
conn = sqlite3.connect(DB_FILE)
|
| 178 |
+
cur = conn.cursor()
|
| 179 |
+
cur.execute("DELETE FROM surgeries WHERE id = ? AND user_id = ?", (surgery_id, user_id))
|
| 180 |
+
conn.commit()
|
| 181 |
+
conn.close()
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
# ---------------------------------------------------------------------------
|
| 185 |
+
# Vaccinations
|
| 186 |
+
# ---------------------------------------------------------------------------
|
| 187 |
+
|
| 188 |
+
def add_vaccination(user_id: int, vaccine_name: str, month: str, year: str):
|
| 189 |
+
conn = sqlite3.connect(DB_FILE)
|
| 190 |
+
cur = conn.cursor()
|
| 191 |
+
cur.execute(
|
| 192 |
+
"INSERT INTO vaccinations (user_id, vaccine_name, month, year, created_at) VALUES (?, ?, ?, ?, ?)",
|
| 193 |
+
(user_id, vaccine_name, month, year, datetime.now().isoformat(timespec="seconds")),
|
| 194 |
+
)
|
| 195 |
+
conn.commit()
|
| 196 |
+
conn.close()
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def get_vaccinations(user_id: int) -> list[dict]:
|
| 200 |
+
conn = sqlite3.connect(DB_FILE)
|
| 201 |
+
conn.row_factory = sqlite3.Row
|
| 202 |
+
cur = conn.cursor()
|
| 203 |
+
cur.execute("SELECT * FROM vaccinations WHERE user_id = ? ORDER BY year DESC", (user_id,))
|
| 204 |
+
rows = [dict(r) for r in cur.fetchall()]
|
| 205 |
+
conn.close()
|
| 206 |
+
return rows
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def delete_vaccination(user_id: int, vaccination_id: int):
|
| 210 |
+
conn = sqlite3.connect(DB_FILE)
|
| 211 |
+
cur = conn.cursor()
|
| 212 |
+
cur.execute("DELETE FROM vaccinations WHERE id = ? AND user_id = ?", (vaccination_id, user_id))
|
| 213 |
+
conn.commit()
|
| 214 |
+
conn.close()
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
# ---------------------------------------------------------------------------
|
| 218 |
+
# Medical report uploads
|
| 219 |
+
# ---------------------------------------------------------------------------
|
| 220 |
+
|
| 221 |
+
def save_report_file(user_id: int, file_storage, category: str, month: str, year: str) -> dict:
|
| 222 |
"""
|
| 223 |
+
file_storage: a Flask FileStorage object (from request.files[...]).
|
| 224 |
+
Saves the file to disk under a per-user subfolder and records metadata.
|
| 225 |
"""
|
| 226 |
+
user_folder = os.path.join(UPLOAD_DIR, str(user_id))
|
| 227 |
+
os.makedirs(user_folder, exist_ok=True)
|
| 228 |
+
|
| 229 |
+
# Prefix with a timestamp so same-named uploads don't collide
|
| 230 |
+
safe_name = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{file_storage.filename}"
|
| 231 |
+
stored_path = os.path.join(user_folder, safe_name)
|
| 232 |
+
file_storage.save(stored_path)
|
| 233 |
+
|
| 234 |
+
conn = sqlite3.connect(DB_FILE)
|
| 235 |
+
cur = conn.cursor()
|
| 236 |
+
cur.execute("""
|
| 237 |
+
INSERT INTO medical_reports (user_id, filename, stored_path, category, month, year, uploaded_at)
|
| 238 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 239 |
+
""", (
|
| 240 |
+
user_id, file_storage.filename, stored_path, category, month, year,
|
| 241 |
+
datetime.now().isoformat(timespec="seconds"),
|
| 242 |
+
))
|
| 243 |
+
conn.commit()
|
| 244 |
+
report_id = cur.lastrowid
|
| 245 |
+
conn.close()
|
| 246 |
+
|
| 247 |
+
return {
|
| 248 |
+
"id": report_id, "filename": file_storage.filename, "category": category,
|
| 249 |
+
"month": month, "year": year,
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def get_reports(user_id: int) -> list[dict]:
|
| 254 |
+
conn = sqlite3.connect(DB_FILE)
|
| 255 |
+
conn.row_factory = sqlite3.Row
|
| 256 |
+
cur = conn.cursor()
|
| 257 |
+
cur.execute(
|
| 258 |
+
"SELECT * FROM medical_reports WHERE user_id = ? ORDER BY year DESC, month DESC",
|
| 259 |
+
(user_id,),
|
| 260 |
)
|
| 261 |
+
rows = [dict(r) for r in cur.fetchall()]
|
| 262 |
+
conn.close()
|
| 263 |
+
return rows
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def get_report_by_id(user_id: int, report_id: int) -> dict | None:
|
| 267 |
+
conn = sqlite3.connect(DB_FILE)
|
| 268 |
+
conn.row_factory = sqlite3.Row
|
| 269 |
+
cur = conn.cursor()
|
| 270 |
+
cur.execute("SELECT * FROM medical_reports WHERE id = ? AND user_id = ?", (report_id, user_id))
|
| 271 |
+
row = cur.fetchone()
|
| 272 |
+
conn.close()
|
| 273 |
+
return dict(row) if row else None
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def delete_report(user_id: int, report_id: int):
|
| 277 |
+
report = get_report_by_id(user_id, report_id)
|
| 278 |
+
if report and os.path.exists(report["stored_path"]):
|
| 279 |
+
os.remove(report["stored_path"])
|
| 280 |
+
|
| 281 |
+
conn = sqlite3.connect(DB_FILE)
|
| 282 |
+
cur = conn.cursor()
|
| 283 |
+
cur.execute("DELETE FROM medical_reports WHERE id = ? AND user_id = ?", (report_id, user_id))
|
| 284 |
+
conn.commit()
|
| 285 |
+
conn.close()
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
# ---------------------------------------------------------------------------
|
| 289 |
+
# QR code -- now encodes a URL to the public emergency view, not raw text
|
| 290 |
+
# ---------------------------------------------------------------------------
|
| 291 |
+
|
| 292 |
+
def generate_qr_code(share_token: str, base_url: str) -> bytes:
|
| 293 |
+
"""
|
| 294 |
+
base_url: e.g. "http://localhost:5000" -- passed in from app.py using
|
| 295 |
+
request.host_url so it works correctly whether running locally or deployed.
|
| 296 |
+
"""
|
| 297 |
+
emergency_url = f"{base_url.rstrip('/')}/emergency/{share_token}"
|
| 298 |
|
| 299 |
qr = qrcode.QRCode(version=None, box_size=8, border=3)
|
| 300 |
+
qr.add_data(emergency_url)
|
| 301 |
qr.make(fit=True)
|
| 302 |
img = qr.make_image(fill_color="#0F6FFF", back_color="white")
|
| 303 |
|
| 304 |
buf = io.BytesIO()
|
| 305 |
img.save(buf, format="PNG")
|
| 306 |
return buf.getvalue()
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
if __name__ == "__main__":
|
| 310 |
+
# Quick manual test -- run: python health_passport.py
|
| 311 |
+
import os as _os
|
| 312 |
+
if _os.path.exists(DB_FILE):
|
| 313 |
+
_os.remove(DB_FILE)
|
| 314 |
+
|
| 315 |
+
init_passport_table()
|
| 316 |
+
|
| 317 |
+
save_passport(1, {
|
| 318 |
+
"full_name": "Jane Doe", "blood_group": "O+", "date_of_birth": "01-01-1995",
|
| 319 |
+
"allergies": "Penicillin", "chronic_conditions": "Asthma",
|
| 320 |
+
"current_medicines": "Ventolin inhaler", "emergency_contact_name": "John Doe",
|
| 321 |
+
"emergency_contact_phone": "+91-9999999999",
|
| 322 |
+
})
|
| 323 |
+
passport = get_passport(1)
|
| 324 |
+
print(f"Saved passport for: {passport['full_name']}, share_token={passport['share_token']}")
|
| 325 |
+
|
| 326 |
+
add_surgery(1, "2019", "Appendectomy")
|
| 327 |
+
add_vaccination(1, "COVID-19 Booster", "March", "2024")
|
| 328 |
+
print(f"Surgeries: {get_surgeries(1)}")
|
| 329 |
+
print(f"Vaccinations: {get_vaccinations(1)}")
|
| 330 |
+
|
| 331 |
+
by_token = get_passport_by_token(passport["share_token"])
|
| 332 |
+
print(f"Fetched via public token: {by_token['full_name']}")
|
| 333 |
+
|
| 334 |
+
qr_bytes = generate_qr_code(passport["share_token"], "http://localhost:5000")
|
| 335 |
+
print(f"QR code generated: {len(qr_bytes)} bytes")
|
pdf_export.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PDF export for the Health Passport.
|
| 3 |
+
|
| 4 |
+
Generates a clean, printable one-page PDF with all passport info, surgeries,
|
| 5 |
+
vaccinations, and the emergency QR code embedded -- so a physical printed
|
| 6 |
+
copy works exactly like the digital one for a doctor scanning it.
|
| 7 |
+
|
| 8 |
+
Uses fpdf2 (pure Python, no system dependencies like wkhtmltopdf/Chrome
|
| 9 |
+
needed -- important for easy deployment).
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import io
|
| 13 |
+
from fpdf import FPDF
|
| 14 |
+
|
| 15 |
+
PRIMARY_RGB = (15, 111, 255) # #0F6FFF
|
| 16 |
+
SECONDARY_RGB = (16, 185, 129) # #10B981
|
| 17 |
+
TEXT_RGB = (30, 41, 59)
|
| 18 |
+
MUTED_RGB = (100, 116, 139)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class PassportPDF(FPDF):
|
| 22 |
+
def header(self):
|
| 23 |
+
self.set_fill_color(*PRIMARY_RGB)
|
| 24 |
+
self.rect(0, 0, 210, 22, style="F")
|
| 25 |
+
self.set_text_color(255, 255, 255)
|
| 26 |
+
self.set_font("Helvetica", "B", 16)
|
| 27 |
+
self.set_xy(10, 6)
|
| 28 |
+
self.cell(0, 10, "VeriMed AI -- Health Passport", new_x="LMARGIN", new_y="NEXT")
|
| 29 |
+
self.set_font("Helvetica", "", 9)
|
| 30 |
+
self.set_xy(10, 15)
|
| 31 |
+
self.cell(0, 6, "Emergency Medical Information Card", new_x="LMARGIN", new_y="NEXT")
|
| 32 |
+
self.ln(8)
|
| 33 |
+
|
| 34 |
+
def footer(self):
|
| 35 |
+
self.set_y(-15)
|
| 36 |
+
self.set_font("Helvetica", "I", 7)
|
| 37 |
+
self.set_text_color(*MUTED_RGB)
|
| 38 |
+
self.cell(0, 10, "Generated by VeriMed AI. Educational project -- not a substitute for official medical records.", align="C")
|
| 39 |
+
|
| 40 |
+
def section_title(self, title: str):
|
| 41 |
+
self.ln(3)
|
| 42 |
+
self.set_font("Helvetica", "B", 12)
|
| 43 |
+
self.set_text_color(*PRIMARY_RGB)
|
| 44 |
+
self.cell(0, 8, title, new_x="LMARGIN", new_y="NEXT")
|
| 45 |
+
self.set_draw_color(*PRIMARY_RGB)
|
| 46 |
+
self.line(10, self.get_y(), 200, self.get_y())
|
| 47 |
+
self.ln(2)
|
| 48 |
+
|
| 49 |
+
def field_row(self, label: str, value: str):
|
| 50 |
+
self.set_font("Helvetica", "B", 10)
|
| 51 |
+
self.set_text_color(*MUTED_RGB)
|
| 52 |
+
self.cell(55, 7, label, new_x="RIGHT", new_y="TOP")
|
| 53 |
+
self.set_font("Helvetica", "", 10)
|
| 54 |
+
self.set_text_color(*TEXT_RGB)
|
| 55 |
+
self.multi_cell(135, 7, value or "Not provided", new_x="LMARGIN", new_y="NEXT")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def generate_passport_pdf(passport: dict, surgeries: list[dict], vaccinations: list[dict], qr_png_bytes: bytes) -> bytes:
|
| 59 |
+
pdf = PassportPDF(format="A4")
|
| 60 |
+
pdf.add_page()
|
| 61 |
+
pdf.set_auto_page_break(auto=True, margin=20)
|
| 62 |
+
|
| 63 |
+
# ---- Personal details ----
|
| 64 |
+
pdf.section_title("Personal Details")
|
| 65 |
+
pdf.field_row("Full Name:", passport.get("full_name", ""))
|
| 66 |
+
pdf.field_row("Date of Birth:", passport.get("date_of_birth", ""))
|
| 67 |
+
pdf.field_row("Blood Group:", passport.get("blood_group", ""))
|
| 68 |
+
|
| 69 |
+
# ---- Medical info ----
|
| 70 |
+
pdf.section_title("Medical Information")
|
| 71 |
+
pdf.field_row("Allergies:", passport.get("allergies") or "None listed")
|
| 72 |
+
pdf.field_row("Chronic Conditions:", passport.get("chronic_conditions") or "None listed")
|
| 73 |
+
pdf.field_row("Current Medicines:", passport.get("current_medicines") or "None listed")
|
| 74 |
+
|
| 75 |
+
# ---- Surgeries ----
|
| 76 |
+
pdf.section_title("Surgical History")
|
| 77 |
+
if surgeries:
|
| 78 |
+
for s in surgeries:
|
| 79 |
+
pdf.field_row(f"{s['year']}:", s["description"])
|
| 80 |
+
else:
|
| 81 |
+
pdf.set_font("Helvetica", "I", 10)
|
| 82 |
+
pdf.set_text_color(*MUTED_RGB)
|
| 83 |
+
pdf.cell(0, 7, "None recorded.", new_x="LMARGIN", new_y="NEXT")
|
| 84 |
+
|
| 85 |
+
# ---- Vaccinations ----
|
| 86 |
+
pdf.section_title("Vaccination Record")
|
| 87 |
+
if vaccinations:
|
| 88 |
+
for v in vaccinations:
|
| 89 |
+
pdf.field_row(f"{v['month']} {v['year']}:", v["vaccine_name"])
|
| 90 |
+
else:
|
| 91 |
+
pdf.set_font("Helvetica", "I", 10)
|
| 92 |
+
pdf.set_text_color(*MUTED_RGB)
|
| 93 |
+
pdf.cell(0, 7, "None recorded.", new_x="LMARGIN", new_y="NEXT")
|
| 94 |
+
|
| 95 |
+
# ---- Emergency contact ----
|
| 96 |
+
pdf.section_title("Emergency Contact")
|
| 97 |
+
pdf.field_row("Name:", passport.get("emergency_contact_name", ""))
|
| 98 |
+
pdf.field_row("Phone:", passport.get("emergency_contact_phone", ""))
|
| 99 |
+
|
| 100 |
+
# ---- QR code ----
|
| 101 |
+
pdf.section_title("Emergency QR Code")
|
| 102 |
+
pdf.set_font("Helvetica", "", 9)
|
| 103 |
+
pdf.set_text_color(*MUTED_RGB)
|
| 104 |
+
pdf.multi_cell(0, 6, "Scan this code to view this person's emergency medical summary instantly -- no app or login required.")
|
| 105 |
+
pdf.ln(2)
|
| 106 |
+
|
| 107 |
+
qr_stream = io.BytesIO(qr_png_bytes)
|
| 108 |
+
pdf.image(qr_stream, x=10, y=pdf.get_y(), w=35, h=35)
|
| 109 |
+
pdf.ln(40)
|
| 110 |
+
|
| 111 |
+
output = pdf.output()
|
| 112 |
+
return bytes(output)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
# Quick manual test -- run: python pdf_export.py
|
| 117 |
+
import health_passport as hp
|
| 118 |
+
import os
|
| 119 |
+
|
| 120 |
+
if os.path.exists(hp.DB_FILE):
|
| 121 |
+
os.remove(hp.DB_FILE)
|
| 122 |
+
hp.init_passport_table()
|
| 123 |
+
hp.save_passport(1, {
|
| 124 |
+
"full_name": "Jane Doe", "blood_group": "O+", "date_of_birth": "01-01-1995",
|
| 125 |
+
"allergies": "Penicillin", "chronic_conditions": "Asthma",
|
| 126 |
+
"current_medicines": "Ventolin inhaler", "emergency_contact_name": "John Doe",
|
| 127 |
+
"emergency_contact_phone": "+91-9999999999",
|
| 128 |
+
})
|
| 129 |
+
hp.add_surgery(1, "2019", "Appendectomy")
|
| 130 |
+
hp.add_vaccination(1, "COVID-19 Booster", "March", "2024")
|
| 131 |
+
|
| 132 |
+
passport = hp.get_passport(1)
|
| 133 |
+
surgeries = hp.get_surgeries(1)
|
| 134 |
+
vaccinations = hp.get_vaccinations(1)
|
| 135 |
+
qr_bytes = hp.generate_qr_code(passport["share_token"], "http://localhost:5000")
|
| 136 |
+
|
| 137 |
+
pdf_bytes = generate_passport_pdf(passport, surgeries, vaccinations, qr_bytes)
|
| 138 |
+
|
| 139 |
+
with open("test_passport_output.pdf", "wb") as f:
|
| 140 |
+
f.write(pdf_bytes)
|
| 141 |
+
|
| 142 |
+
print(f"✅ PDF generated: {len(pdf_bytes)} bytes, saved to test_passport_output.pdf")
|
requirements.txt
CHANGED
|
@@ -29,3 +29,7 @@ qrcode[pil]
|
|
| 29 |
# ==== Translation (Quick Win) ====
|
| 30 |
deep-translator
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
# ==== Translation (Quick Win) ====
|
| 30 |
deep-translator
|
| 31 |
|
| 32 |
+
# ==== Auth + PDF Export (Login/Register feature) ====
|
| 33 |
+
flask-login
|
| 34 |
+
fpdf2
|
| 35 |
+
|
static/js/main.js
CHANGED
|
@@ -16,10 +16,166 @@ function verdictColorClasses(verdict) {
|
|
| 16 |
}
|
| 17 |
}
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
// ---------- Checker page ----------
|
|
@@ -259,7 +415,9 @@ if (passportForm) {
|
|
| 259 |
if (!data) return;
|
| 260 |
document.getElementById("passName").value = data.full_name || "";
|
| 261 |
document.getElementById("passBlood").value = data.blood_group || "";
|
|
|
|
| 262 |
document.getElementById("passAllergies").value = data.allergies || "";
|
|
|
|
| 263 |
document.getElementById("passMeds").value = data.current_medicines || "";
|
| 264 |
document.getElementById("passContact").value = data.emergency_contact_name || "";
|
| 265 |
document.getElementById("passPhone").value = data.emergency_contact_phone || "";
|
|
@@ -273,9 +431,9 @@ if (passportForm) {
|
|
| 273 |
const payload = {
|
| 274 |
full_name: document.getElementById("passName").value,
|
| 275 |
blood_group: document.getElementById("passBlood").value,
|
| 276 |
-
date_of_birth: "",
|
| 277 |
allergies: document.getElementById("passAllergies").value,
|
| 278 |
-
chronic_conditions: "",
|
| 279 |
current_medicines: document.getElementById("passMeds").value,
|
| 280 |
emergency_contact_name: document.getElementById("passContact").value,
|
| 281 |
emergency_contact_phone: document.getElementById("passPhone").value,
|
|
@@ -310,6 +468,152 @@ function showQrCode() {
|
|
| 310 |
qrContainer.innerHTML = `<img src="/api/passport/qr?t=${Date.now()}" alt="Health Passport QR Code" class="w-40 h-40 object-contain" />`;
|
| 311 |
}
|
| 312 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
// ---------- Home page: Personalized Snapshot ----------
|
| 314 |
|
| 315 |
const passportSnapshotCard = document.getElementById("passportSnapshotCard");
|
|
@@ -399,4 +703,13 @@ if (latestAlertsContainer) {
|
|
| 399 |
.catch(() => {
|
| 400 |
latestAlertsContainer.innerHTML = `<div class="md:col-span-3 text-center text-red-400 text-sm py-6">Couldn't load recent alerts.</div>`;
|
| 401 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
}
|
|
|
|
| 16 |
}
|
| 17 |
}
|
| 18 |
|
| 19 |
+
// ---------- Emergency Help page ----------
|
| 20 |
+
|
| 21 |
+
const useGpsBtn = document.getElementById("useGpsBtn");
|
| 22 |
+
if (useGpsBtn) {
|
| 23 |
+
const manualSearchBtn = document.getElementById("manualSearchBtn");
|
| 24 |
+
const manualLocationInput = document.getElementById("manualLocationInput");
|
| 25 |
+
const hospitalStatus = document.getElementById("hospitalStatus");
|
| 26 |
+
const hospitalList = document.getElementById("hospitalList");
|
| 27 |
+
|
| 28 |
+
function setStatus(message, isError = false) {
|
| 29 |
+
hospitalStatus.textContent = message;
|
| 30 |
+
hospitalStatus.className = isError
|
| 31 |
+
? "text-sm text-red-500 text-center py-6"
|
| 32 |
+
: "text-sm text-slate-400 text-center py-6";
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function haversineDistanceKm(lat1, lon1, lat2, lon2) {
|
| 36 |
+
const R = 6371;
|
| 37 |
+
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
| 38 |
+
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
| 39 |
+
const a =
|
| 40 |
+
Math.sin(dLat / 2) ** 2 +
|
| 41 |
+
Math.cos((lat1 * Math.PI) / 180) *
|
| 42 |
+
Math.cos((lat2 * Math.PI) / 180) *
|
| 43 |
+
Math.sin(dLon / 2) ** 2;
|
| 44 |
+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function buildAddress(tags) {
|
| 48 |
+
const parts = [
|
| 49 |
+
tags["addr:housenumber"],
|
| 50 |
+
tags["addr:street"],
|
| 51 |
+
tags["addr:suburb"],
|
| 52 |
+
tags["addr:city"],
|
| 53 |
+
tags["addr:postcode"],
|
| 54 |
+
].filter(Boolean);
|
| 55 |
+
return parts.length ? parts.join(", ") : "Address not available";
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
async function fetchNearbyHospitals(lat, lon) {
|
| 59 |
+
hospitalList.innerHTML = "";
|
| 60 |
+
setStatus("Searching for hospitals and clinics nearby...");
|
| 61 |
+
|
| 62 |
+
// Overpass API (OpenStreetMap) -- free, no API key required
|
| 63 |
+
const query = `
|
| 64 |
+
[out:json][timeout:25];
|
| 65 |
+
(
|
| 66 |
+
node["amenity"="hospital"](around:6000,${lat},${lon});
|
| 67 |
+
way["amenity"="hospital"](around:6000,${lat},${lon});
|
| 68 |
+
node["amenity"="clinic"](around:6000,${lat},${lon});
|
| 69 |
+
way["amenity"="clinic"](around:6000,${lat},${lon});
|
| 70 |
+
);
|
| 71 |
+
out center 40;
|
| 72 |
+
`;
|
| 73 |
+
|
| 74 |
+
try {
|
| 75 |
+
const res = await fetch("https://overpass-api.de/api/interpreter", {
|
| 76 |
+
method: "POST",
|
| 77 |
+
body: "data=" + encodeURIComponent(query),
|
| 78 |
+
});
|
| 79 |
+
if (!res.ok) throw new Error("Overpass API request failed.");
|
| 80 |
+
const data = await res.json();
|
| 81 |
+
|
| 82 |
+
const results = (data.elements || [])
|
| 83 |
+
.filter((el) => el.tags && el.tags.name)
|
| 84 |
+
.map((el) => {
|
| 85 |
+
const elLat = el.lat || (el.center && el.center.lat);
|
| 86 |
+
const elLon = el.lon || (el.center && el.center.lon);
|
| 87 |
+
return {
|
| 88 |
+
name: el.tags.name,
|
| 89 |
+
type: el.tags.amenity === "hospital" ? "Hospital" : "Clinic",
|
| 90 |
+
address: buildAddress(el.tags),
|
| 91 |
+
phone: el.tags.phone || el.tags["contact:phone"] || null,
|
| 92 |
+
lat: elLat,
|
| 93 |
+
lon: elLon,
|
| 94 |
+
distanceKm: elLat && elLon ? haversineDistanceKm(lat, lon, elLat, elLon) : null,
|
| 95 |
+
};
|
| 96 |
+
})
|
| 97 |
+
.filter((h) => h.lat && h.lon)
|
| 98 |
+
.sort((a, b) => (a.distanceKm ?? 999) - (b.distanceKm ?? 999))
|
| 99 |
+
.slice(0, 20);
|
| 100 |
+
|
| 101 |
+
if (results.length === 0) {
|
| 102 |
+
setStatus("No hospitals found nearby. Try a different location.", true);
|
| 103 |
+
return;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
setStatus(`Found ${results.length} hospitals/clinics nearby, sorted by distance.`);
|
| 107 |
+
renderHospitalList(results);
|
| 108 |
+
} catch (err) {
|
| 109 |
+
setStatus("Couldn't fetch nearby hospitals. Please try again.", true);
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
function renderHospitalList(hospitals) {
|
| 114 |
+
hospitalList.innerHTML = hospitals
|
| 115 |
+
.map((h) => {
|
| 116 |
+
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${h.lat},${h.lon}`;
|
| 117 |
+
const distanceLabel = h.distanceKm !== null ? `${h.distanceKm.toFixed(1)} km away` : "";
|
| 118 |
+
return `
|
| 119 |
+
<div class="bg-slate-50 border border-slate-100 rounded-2xl p-4">
|
| 120 |
+
<div class="flex items-start justify-between gap-2">
|
| 121 |
+
<div>
|
| 122 |
+
<h4 class="font-bold text-slate-800">${escapeHtml(h.name)}</h4>
|
| 123 |
+
<span class="inline-block text-[10px] font-bold uppercase tracking-wide text-blue-600 bg-blue-50 px-2 py-0.5 rounded-full mt-1">${escapeHtml(h.type)}</span>
|
| 124 |
+
${distanceLabel ? `<span class="text-xs text-slate-400 ml-2">${distanceLabel}</span>` : ""}
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
<p class="text-xs text-slate-500 mt-2">${escapeHtml(h.address)}</p>
|
| 128 |
+
<div class="flex gap-2 mt-3">
|
| 129 |
+
${h.phone ? `<a href="tel:${escapeHtml(h.phone)}" class="flex-1 text-center bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-bold py-2 rounded-lg transition-colors"><i class="fa-solid fa-phone mr-1"></i>Call</a>` : ""}
|
| 130 |
+
<a href="${directionsUrl}" target="_blank" class="flex-1 text-center bg-blue-600 hover:bg-blue-700 text-white text-xs font-bold py-2 rounded-lg transition-colors"><i class="fa-solid fa-location-arrow mr-1"></i>Directions</a>
|
| 131 |
+
</div>
|
| 132 |
+
</div>`;
|
| 133 |
+
})
|
| 134 |
+
.join("");
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
useGpsBtn.addEventListener("click", () => {
|
| 138 |
+
if (!navigator.geolocation) {
|
| 139 |
+
setStatus("Your browser doesn't support GPS location.", true);
|
| 140 |
+
return;
|
| 141 |
+
}
|
| 142 |
+
setStatus("Requesting your location...");
|
| 143 |
+
navigator.geolocation.getCurrentPosition(
|
| 144 |
+
(position) => {
|
| 145 |
+
fetchNearbyHospitals(position.coords.latitude, position.coords.longitude);
|
| 146 |
+
},
|
| 147 |
+
() => {
|
| 148 |
+
setStatus("Location access denied. Try typing a location manually instead.", true);
|
| 149 |
+
}
|
| 150 |
+
);
|
| 151 |
+
});
|
| 152 |
+
|
| 153 |
+
async function searchManualLocation() {
|
| 154 |
+
const query = manualLocationInput.value.trim();
|
| 155 |
+
if (!query) return;
|
| 156 |
+
|
| 157 |
+
setStatus(`Looking up "${query}"...`);
|
| 158 |
+
try {
|
| 159 |
+
// Nominatim (OpenStreetMap) geocoding -- free, no API key required
|
| 160 |
+
const res = await fetch(
|
| 161 |
+
`https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(query)}`
|
| 162 |
+
);
|
| 163 |
+
const results = await res.json();
|
| 164 |
+
if (!results || results.length === 0) {
|
| 165 |
+
setStatus(`Couldn't find "${query}". Try a more specific location.`, true);
|
| 166 |
+
return;
|
| 167 |
+
}
|
| 168 |
+
const { lat, lon } = results[0];
|
| 169 |
+
fetchNearbyHospitals(parseFloat(lat), parseFloat(lon));
|
| 170 |
+
} catch (err) {
|
| 171 |
+
setStatus("Location search failed. Please try again.", true);
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
manualSearchBtn.addEventListener("click", searchManualLocation);
|
| 176 |
+
manualLocationInput.addEventListener("keypress", (e) => {
|
| 177 |
+
if (e.key === "Enter") searchManualLocation();
|
| 178 |
+
});
|
| 179 |
}
|
| 180 |
|
| 181 |
// ---------- Checker page ----------
|
|
|
|
| 415 |
if (!data) return;
|
| 416 |
document.getElementById("passName").value = data.full_name || "";
|
| 417 |
document.getElementById("passBlood").value = data.blood_group || "";
|
| 418 |
+
document.getElementById("passDob").value = data.date_of_birth || "";
|
| 419 |
document.getElementById("passAllergies").value = data.allergies || "";
|
| 420 |
+
document.getElementById("passChronic").value = data.chronic_conditions || "";
|
| 421 |
document.getElementById("passMeds").value = data.current_medicines || "";
|
| 422 |
document.getElementById("passContact").value = data.emergency_contact_name || "";
|
| 423 |
document.getElementById("passPhone").value = data.emergency_contact_phone || "";
|
|
|
|
| 431 |
const payload = {
|
| 432 |
full_name: document.getElementById("passName").value,
|
| 433 |
blood_group: document.getElementById("passBlood").value,
|
| 434 |
+
date_of_birth: document.getElementById("passDob").value,
|
| 435 |
allergies: document.getElementById("passAllergies").value,
|
| 436 |
+
chronic_conditions: document.getElementById("passChronic").value,
|
| 437 |
current_medicines: document.getElementById("passMeds").value,
|
| 438 |
emergency_contact_name: document.getElementById("passContact").value,
|
| 439 |
emergency_contact_phone: document.getElementById("passPhone").value,
|
|
|
|
| 468 |
qrContainer.innerHTML = `<img src="/api/passport/qr?t=${Date.now()}" alt="Health Passport QR Code" class="w-40 h-40 object-contain" />`;
|
| 469 |
}
|
| 470 |
|
| 471 |
+
// ---------- Surgeries ----------
|
| 472 |
+
|
| 473 |
+
const surgeryForm = document.getElementById("surgeryForm");
|
| 474 |
+
if (surgeryForm) {
|
| 475 |
+
function loadSurgeries() {
|
| 476 |
+
fetch("/api/passport/surgeries")
|
| 477 |
+
.then((res) => res.json())
|
| 478 |
+
.then((surgeries) => {
|
| 479 |
+
const list = document.getElementById("surgeryList");
|
| 480 |
+
if (!surgeries || surgeries.length === 0) {
|
| 481 |
+
list.innerHTML = `<p class="text-xs text-slate-400 text-center py-3">No surgeries recorded yet.</p>`;
|
| 482 |
+
return;
|
| 483 |
+
}
|
| 484 |
+
list.innerHTML = surgeries.map((s) => `
|
| 485 |
+
<div class="flex items-center justify-between bg-slate-50 border border-slate-100 rounded-xl p-3">
|
| 486 |
+
<div><span class="font-bold text-sm text-slate-700">${escapeHtml(s.year)}</span> <span class="text-sm text-slate-500">-- ${escapeHtml(s.description)}</span></div>
|
| 487 |
+
<button onclick="deleteSurgery(${s.id})" class="text-red-400 hover:text-red-600 text-xs"><i class="fa-solid fa-trash"></i></button>
|
| 488 |
+
</div>`).join("");
|
| 489 |
+
});
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
surgeryForm.addEventListener("submit", async (e) => {
|
| 493 |
+
e.preventDefault();
|
| 494 |
+
const year = document.getElementById("surgeryYear").value;
|
| 495 |
+
const description = document.getElementById("surgeryDescription").value;
|
| 496 |
+
await fetch("/api/passport/surgeries", {
|
| 497 |
+
method: "POST",
|
| 498 |
+
headers: { "Content-Type": "application/json" },
|
| 499 |
+
body: JSON.stringify({ year, description }),
|
| 500 |
+
});
|
| 501 |
+
surgeryForm.reset();
|
| 502 |
+
loadSurgeries();
|
| 503 |
+
});
|
| 504 |
+
|
| 505 |
+
window.deleteSurgery = async (id) => {
|
| 506 |
+
await fetch(`/api/passport/surgeries/${id}`, { method: "DELETE" });
|
| 507 |
+
loadSurgeries();
|
| 508 |
+
};
|
| 509 |
+
|
| 510 |
+
loadSurgeries();
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
// ---------- Vaccinations ----------
|
| 514 |
+
|
| 515 |
+
const vaccinationForm = document.getElementById("vaccinationForm");
|
| 516 |
+
if (vaccinationForm) {
|
| 517 |
+
function loadVaccinations() {
|
| 518 |
+
fetch("/api/passport/vaccinations")
|
| 519 |
+
.then((res) => res.json())
|
| 520 |
+
.then((vaccinations) => {
|
| 521 |
+
const list = document.getElementById("vaccinationList");
|
| 522 |
+
if (!vaccinations || vaccinations.length === 0) {
|
| 523 |
+
list.innerHTML = `<p class="text-xs text-slate-400 text-center py-3">No vaccinations recorded yet.</p>`;
|
| 524 |
+
return;
|
| 525 |
+
}
|
| 526 |
+
list.innerHTML = vaccinations.map((v) => `
|
| 527 |
+
<div class="flex items-center justify-between bg-slate-50 border border-slate-100 rounded-xl p-3">
|
| 528 |
+
<div><span class="font-bold text-sm text-slate-700">${escapeHtml(v.month)} ${escapeHtml(v.year)}</span> <span class="text-sm text-slate-500">-- ${escapeHtml(v.vaccine_name)}</span></div>
|
| 529 |
+
<button onclick="deleteVaccination(${v.id})" class="text-red-400 hover:text-red-600 text-xs"><i class="fa-solid fa-trash"></i></button>
|
| 530 |
+
</div>`).join("");
|
| 531 |
+
});
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
vaccinationForm.addEventListener("submit", async (e) => {
|
| 535 |
+
e.preventDefault();
|
| 536 |
+
const vaccine_name = document.getElementById("vaccineName").value;
|
| 537 |
+
const month = document.getElementById("vaccineMonth").value;
|
| 538 |
+
const year = document.getElementById("vaccineYear").value;
|
| 539 |
+
await fetch("/api/passport/vaccinations", {
|
| 540 |
+
method: "POST",
|
| 541 |
+
headers: { "Content-Type": "application/json" },
|
| 542 |
+
body: JSON.stringify({ vaccine_name, month, year }),
|
| 543 |
+
});
|
| 544 |
+
vaccinationForm.reset();
|
| 545 |
+
loadVaccinations();
|
| 546 |
+
});
|
| 547 |
+
|
| 548 |
+
window.deleteVaccination = async (id) => {
|
| 549 |
+
await fetch(`/api/passport/vaccinations/${id}`, { method: "DELETE" });
|
| 550 |
+
loadVaccinations();
|
| 551 |
+
};
|
| 552 |
+
|
| 553 |
+
loadVaccinations();
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
// ---------- Medical Report Uploads ----------
|
| 557 |
+
|
| 558 |
+
const reportForm = document.getElementById("reportForm");
|
| 559 |
+
if (reportForm) {
|
| 560 |
+
function loadReports() {
|
| 561 |
+
fetch("/api/passport/reports")
|
| 562 |
+
.then((res) => res.json())
|
| 563 |
+
.then((reports) => {
|
| 564 |
+
const list = document.getElementById("reportList");
|
| 565 |
+
if (!reports || reports.length === 0) {
|
| 566 |
+
list.innerHTML = `<p class="text-xs text-slate-400 text-center py-3">No reports uploaded yet.</p>`;
|
| 567 |
+
return;
|
| 568 |
+
}
|
| 569 |
+
list.innerHTML = reports.map((r) => `
|
| 570 |
+
<div class="flex items-center justify-between bg-slate-50 border border-slate-100 rounded-xl p-3">
|
| 571 |
+
<div>
|
| 572 |
+
<span class="font-bold text-sm text-slate-700">${escapeHtml(r.category)}</span>
|
| 573 |
+
<span class="text-xs text-slate-400 ml-2">${escapeHtml(r.month)} ${escapeHtml(r.year)}</span>
|
| 574 |
+
<div class="text-xs text-slate-500">${escapeHtml(r.filename)}</div>
|
| 575 |
+
</div>
|
| 576 |
+
<div class="flex items-center gap-3">
|
| 577 |
+
<a href="/api/passport/reports/${r.id}/download" class="text-blue-500 hover:text-blue-700 text-xs"><i class="fa-solid fa-download"></i></a>
|
| 578 |
+
<button onclick="deleteReport(${r.id})" class="text-red-400 hover:text-red-600 text-xs"><i class="fa-solid fa-trash"></i></button>
|
| 579 |
+
</div>
|
| 580 |
+
</div>`).join("");
|
| 581 |
+
});
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
reportForm.addEventListener("submit", async (e) => {
|
| 585 |
+
e.preventDefault();
|
| 586 |
+
const fileInput = document.getElementById("reportFile");
|
| 587 |
+
if (!fileInput.files.length) return;
|
| 588 |
+
|
| 589 |
+
const formData = new FormData();
|
| 590 |
+
formData.append("file", fileInput.files[0]);
|
| 591 |
+
formData.append("category", document.getElementById("reportCategory").value);
|
| 592 |
+
formData.append("month", document.getElementById("reportMonth").value);
|
| 593 |
+
formData.append("year", document.getElementById("reportYear").value);
|
| 594 |
+
|
| 595 |
+
const submitBtn = reportForm.querySelector("button[type='submit']");
|
| 596 |
+
submitBtn.disabled = true;
|
| 597 |
+
submitBtn.textContent = "Uploading...";
|
| 598 |
+
|
| 599 |
+
try {
|
| 600 |
+
await fetch("/api/passport/reports", { method: "POST", body: formData });
|
| 601 |
+
reportForm.reset();
|
| 602 |
+
loadReports();
|
| 603 |
+
} finally {
|
| 604 |
+
submitBtn.disabled = false;
|
| 605 |
+
submitBtn.textContent = "Upload Report";
|
| 606 |
+
}
|
| 607 |
+
});
|
| 608 |
+
|
| 609 |
+
window.deleteReport = async (id) => {
|
| 610 |
+
await fetch(`/api/passport/reports/${id}`, { method: "DELETE" });
|
| 611 |
+
loadReports();
|
| 612 |
+
};
|
| 613 |
+
|
| 614 |
+
loadReports();
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
// ---------- Home page: Personalized Snapshot ----------
|
| 618 |
|
| 619 |
const passportSnapshotCard = document.getElementById("passportSnapshotCard");
|
|
|
|
| 703 |
.catch(() => {
|
| 704 |
latestAlertsContainer.innerHTML = `<div class="md:col-span-3 text-center text-red-400 text-sm py-6">Couldn't load recent alerts.</div>`;
|
| 705 |
});
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
function escapeHtml(str) {
|
| 709 |
+
if (str === null || str === undefined) return "";
|
| 710 |
+
return String(str)
|
| 711 |
+
.replace(/&/g, "&")
|
| 712 |
+
.replace(/</g, "<")
|
| 713 |
+
.replace(/>/g, ">")
|
| 714 |
+
.replace(/"/g, """);
|
| 715 |
}
|
steps.txt
CHANGED
|
@@ -58,4 +58,8 @@ pip install deep-translator
|
|
| 58 |
|
| 59 |
____________________--
|
| 60 |
gnn
|
| 61 |
-
pip install torch-geometric networkx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
____________________--
|
| 60 |
gnn
|
| 61 |
+
pip install torch-geometric networkx
|
| 62 |
+
|
| 63 |
+
_____________________________________________________________________________________________________________________________________________________
|
| 64 |
+
passport page
|
| 65 |
+
pip install flask-login fpdf2
|
templates/base.html
CHANGED
|
@@ -28,9 +28,31 @@
|
|
| 28 |
<a href="/checker" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-magnifying-glass-shield mr-1"></i> AI Fact Checker</a>
|
| 29 |
<a href="/predictor" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-diagram-project mr-1"></i> GNN Predictor</a>
|
| 30 |
<a href="/passport" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-id-card-clip mr-1"></i> Health Passport</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
</div>
|
| 32 |
</nav>
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
<!-- Main Dynamic Content Wrapper -->
|
| 35 |
<main class="flex-1 max-w-7xl w-full mx-auto p-6">
|
| 36 |
{% block content %}{% endblock %}
|
|
|
|
| 28 |
<a href="/checker" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-magnifying-glass-shield mr-1"></i> AI Fact Checker</a>
|
| 29 |
<a href="/predictor" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-diagram-project mr-1"></i> GNN Predictor</a>
|
| 30 |
<a href="/passport" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-id-card-clip mr-1"></i> Health Passport</a>
|
| 31 |
+
<a href="/emergency-help" class="text-red-500 hover:text-red-700 transition-colors font-bold"><i class="fa-solid fa-truck-medical mr-1"></i> Emergency</a>
|
| 32 |
+
{% if current_user.is_authenticated %}
|
| 33 |
+
<span class="text-slate-400">|</span>
|
| 34 |
+
<span class="text-slate-500"><i class="fa-solid fa-circle-user mr-1"></i>{{ current_user.name }}</span>
|
| 35 |
+
<a href="/logout" class="text-red-500 hover:text-red-700 transition-colors"><i class="fa-solid fa-right-from-bracket mr-1"></i>Logout</a>
|
| 36 |
+
{% else %}
|
| 37 |
+
<span class="text-slate-400">|</span>
|
| 38 |
+
<a href="/login" class="hover:text-blue-600 transition-colors">Login</a>
|
| 39 |
+
<a href="/register" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">Register</a>
|
| 40 |
+
{% endif %}
|
| 41 |
</div>
|
| 42 |
</nav>
|
| 43 |
|
| 44 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 45 |
+
{% if messages %}
|
| 46 |
+
<div class="max-w-7xl w-full mx-auto px-6 pt-4">
|
| 47 |
+
{% for category, message in messages %}
|
| 48 |
+
<div class="p-3 rounded-xl text-sm font-semibold mb-2 {{ 'bg-red-50 text-red-700 border border-red-200' if category == 'error' else 'bg-emerald-50 text-emerald-700 border border-emerald-200' }}">
|
| 49 |
+
{{ message }}
|
| 50 |
+
</div>
|
| 51 |
+
{% endfor %}
|
| 52 |
+
</div>
|
| 53 |
+
{% endif %}
|
| 54 |
+
{% endwith %}
|
| 55 |
+
|
| 56 |
<!-- Main Dynamic Content Wrapper -->
|
| 57 |
<main class="flex-1 max-w-7xl w-full mx-auto p-6">
|
| 58 |
{% block content %}{% endblock %}
|
templates/emergency_help.html
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
|
| 4 |
+
<div class="mb-8 p-8 rounded-3xl bg-gradient-to-r from-red-600 via-rose-600 to-orange-600 text-white shadow-xl shadow-red-100">
|
| 5 |
+
<h1 class="text-3xl font-black mb-2"><i class="fa-solid fa-truck-medical mr-2"></i>Emergency Help</h1>
|
| 6 |
+
<p class="text-red-100 max-w-2xl text-sm leading-relaxed">
|
| 7 |
+
Immediate emergency contact numbers and hospitals near your current location.
|
| 8 |
+
</p>
|
| 9 |
+
</div>
|
| 10 |
+
|
| 11 |
+
<!-- Emergency Numbers -->
|
| 12 |
+
<div class="mb-8">
|
| 13 |
+
<h2 class="text-lg font-bold text-slate-800 mb-4"><i class="fa-solid fa-phone-volume text-red-600 mr-2"></i>Emergency Numbers (India)</h2>
|
| 14 |
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
| 15 |
+
<a href="tel:112" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
|
| 16 |
+
<div class="text-2xl font-black text-red-600">112</div>
|
| 17 |
+
<div class="text-xs font-bold text-slate-500 mt-1">National Emergency</div>
|
| 18 |
+
</a>
|
| 19 |
+
<a href="tel:108" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
|
| 20 |
+
<div class="text-2xl font-black text-red-600">108</div>
|
| 21 |
+
<div class="text-xs font-bold text-slate-500 mt-1">Ambulance</div>
|
| 22 |
+
</a>
|
| 23 |
+
<a href="tel:101" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
|
| 24 |
+
<div class="text-2xl font-black text-red-600">101</div>
|
| 25 |
+
<div class="text-xs font-bold text-slate-500 mt-1">Fire Brigade</div>
|
| 26 |
+
</a>
|
| 27 |
+
<a href="tel:100" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
|
| 28 |
+
<div class="text-2xl font-black text-red-600">100</div>
|
| 29 |
+
<div class="text-xs font-bold text-slate-500 mt-1">Police</div>
|
| 30 |
+
</a>
|
| 31 |
+
<a href="tel:1091" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
|
| 32 |
+
<div class="text-2xl font-black text-red-600">1091</div>
|
| 33 |
+
<div class="text-xs font-bold text-slate-500 mt-1">Women's Helpline</div>
|
| 34 |
+
</a>
|
| 35 |
+
<a href="tel:1098" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
|
| 36 |
+
<div class="text-2xl font-black text-red-600">1098</div>
|
| 37 |
+
<div class="text-xs font-bold text-slate-500 mt-1">Child Helpline</div>
|
| 38 |
+
</a>
|
| 39 |
+
<a href="tel:1075" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
|
| 40 |
+
<div class="text-2xl font-black text-red-600">1075</div>
|
| 41 |
+
<div class="text-xs font-bold text-slate-500 mt-1">Disaster Mgmt</div>
|
| 42 |
+
</a>
|
| 43 |
+
<a href="tel:1930" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
|
| 44 |
+
<div class="text-2xl font-black text-red-600">1930</div>
|
| 45 |
+
<div class="text-xs font-bold text-slate-500 mt-1">Cyber Crime</div>
|
| 46 |
+
</a>
|
| 47 |
+
</div>
|
| 48 |
+
<p class="text-xs text-slate-400 mt-3">Tap any number to call directly on mobile. Numbers shown are for India -- update for your region if deploying elsewhere.</p>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<!-- Nearby Hospitals -->
|
| 52 |
+
<div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
|
| 53 |
+
<h2 class="text-lg font-bold text-slate-800 mb-1"><i class="fa-solid fa-hospital text-blue-600 mr-2"></i>Find Nearby Hospitals</h2>
|
| 54 |
+
<p class="text-xs text-slate-400 mb-4">Data from OpenStreetMap. Use GPS for instant results, or search a location manually.</p>
|
| 55 |
+
|
| 56 |
+
<div class="flex flex-col md:flex-row gap-3 mb-6">
|
| 57 |
+
<button id="useGpsBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-5 rounded-xl text-sm flex items-center justify-center gap-2 transition-colors">
|
| 58 |
+
<i class="fa-solid fa-location-crosshairs"></i> Use My Current Location (GPS)
|
| 59 |
+
</button>
|
| 60 |
+
<div class="flex-1 flex gap-2">
|
| 61 |
+
<input type="text" id="manualLocationInput" placeholder="Or type a city / area name..." class="flex-1 bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500">
|
| 62 |
+
<button id="manualSearchBtn" class="bg-slate-700 hover:bg-slate-800 text-white font-bold py-3 px-5 rounded-xl text-sm transition-colors">
|
| 63 |
+
<i class="fa-solid fa-magnifying-glass"></i>
|
| 64 |
+
</button>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<div id="hospitalStatus" class="text-sm text-slate-400 text-center py-6">
|
| 69 |
+
Choose GPS or type a location to find nearby hospitals.
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<div id="hospitalList" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
{% endblock %}
|
templates/emergency_view.html
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Emergency Medical Info -- VeriMed AI</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 9 |
+
</head>
|
| 10 |
+
<body class="bg-slate-50 min-h-screen">
|
| 11 |
+
|
| 12 |
+
<div class="bg-red-600 text-white text-center py-3 font-bold text-sm tracking-wide">
|
| 13 |
+
<i class="fa-solid fa-triangle-exclamation mr-2"></i>EMERGENCY MEDICAL INFORMATION -- READ ONLY
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<div class="max-w-xl mx-auto p-6">
|
| 17 |
+
|
| 18 |
+
{% if not passport %}
|
| 19 |
+
<div class="bg-white p-8 rounded-3xl border border-slate-100 shadow-xl text-center mt-10">
|
| 20 |
+
<i class="fa-solid fa-circle-question text-4xl text-slate-300 mb-4"></i>
|
| 21 |
+
<h1 class="text-xl font-black text-slate-700">No Record Found</h1>
|
| 22 |
+
<p class="text-sm text-slate-500 mt-2">This QR code link is invalid or has expired.</p>
|
| 23 |
+
</div>
|
| 24 |
+
{% else %}
|
| 25 |
+
<div class="bg-white p-6 rounded-3xl border border-slate-100 shadow-xl mt-6">
|
| 26 |
+
<div class="flex items-center gap-3 mb-6 pb-4 border-b border-slate-100">
|
| 27 |
+
<div class="bg-blue-600 text-white w-12 h-12 rounded-xl flex items-center justify-center">
|
| 28 |
+
<i class="fa-solid fa-id-card-clip text-xl"></i>
|
| 29 |
+
</div>
|
| 30 |
+
<div>
|
| 31 |
+
<h1 class="text-xl font-black text-slate-800">{{ passport.full_name }}</h1>
|
| 32 |
+
<p class="text-xs text-slate-400">VeriMed AI Health Passport</p>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<div class="grid grid-cols-2 gap-4 mb-5">
|
| 37 |
+
<div class="bg-red-50 border border-red-100 p-4 rounded-2xl text-center">
|
| 38 |
+
<p class="text-[10px] uppercase font-bold text-red-400">Blood Group</p>
|
| 39 |
+
<p class="text-2xl font-black text-red-600">{{ passport.blood_group or '--' }}</p>
|
| 40 |
+
</div>
|
| 41 |
+
<div class="bg-slate-50 border border-slate-100 p-4 rounded-2xl text-center">
|
| 42 |
+
<p class="text-[10px] uppercase font-bold text-slate-400">Date of Birth</p>
|
| 43 |
+
<p class="text-lg font-bold text-slate-700 mt-1">{{ passport.date_of_birth or '--' }}</p>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
<div class="space-y-4">
|
| 48 |
+
<div class="bg-amber-50 border border-amber-100 p-4 rounded-2xl">
|
| 49 |
+
<p class="text-xs uppercase font-bold text-amber-600 mb-1"><i class="fa-solid fa-triangle-exclamation mr-1"></i>Allergies</p>
|
| 50 |
+
<p class="text-sm font-semibold text-slate-700">{{ passport.allergies or 'None listed' }}</p>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<div class="bg-purple-50 border border-purple-100 p-4 rounded-2xl">
|
| 54 |
+
<p class="text-xs uppercase font-bold text-purple-600 mb-1"><i class="fa-solid fa-heart-pulse mr-1"></i>Chronic Conditions</p>
|
| 55 |
+
<p class="text-sm font-semibold text-slate-700">{{ passport.chronic_conditions or 'None listed' }}</p>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div class="bg-blue-50 border border-blue-100 p-4 rounded-2xl">
|
| 59 |
+
<p class="text-xs uppercase font-bold text-blue-600 mb-1"><i class="fa-solid fa-pills mr-1"></i>Current Medicines</p>
|
| 60 |
+
<p class="text-sm font-semibold text-slate-700">{{ passport.current_medicines or 'None listed' }}</p>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
{% if surgeries %}
|
| 64 |
+
<div class="bg-slate-50 border border-slate-100 p-4 rounded-2xl">
|
| 65 |
+
<p class="text-xs uppercase font-bold text-slate-500 mb-2"><i class="fa-solid fa-user-doctor mr-1"></i>Surgical History</p>
|
| 66 |
+
{% for s in surgeries %}
|
| 67 |
+
<p class="text-sm text-slate-700"><span class="font-bold">{{ s.year }}:</span> {{ s.description }}</p>
|
| 68 |
+
{% endfor %}
|
| 69 |
+
</div>
|
| 70 |
+
{% endif %}
|
| 71 |
+
|
| 72 |
+
{% if vaccinations %}
|
| 73 |
+
<div class="bg-emerald-50 border border-emerald-100 p-4 rounded-2xl">
|
| 74 |
+
<p class="text-xs uppercase font-bold text-emerald-600 mb-2"><i class="fa-solid fa-syringe mr-1"></i>Vaccination Record</p>
|
| 75 |
+
{% for v in vaccinations %}
|
| 76 |
+
<p class="text-sm text-slate-700"><span class="font-bold">{{ v.month }} {{ v.year }}:</span> {{ v.vaccine_name }}</p>
|
| 77 |
+
{% endfor %}
|
| 78 |
+
</div>
|
| 79 |
+
{% endif %}
|
| 80 |
+
|
| 81 |
+
<div class="bg-red-600 text-white p-4 rounded-2xl">
|
| 82 |
+
<p class="text-xs uppercase font-bold text-red-200 mb-1"><i class="fa-solid fa-phone mr-1"></i>Emergency Contact</p>
|
| 83 |
+
<p class="text-lg font-bold">{{ passport.emergency_contact_name or '--' }}</p>
|
| 84 |
+
<p class="text-sm text-red-100">{{ passport.emergency_contact_phone or '--' }}</p>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<p class="text-center text-xs text-slate-400 mt-4">
|
| 90 |
+
Generated by VeriMed AI. Educational project -- verify critical decisions with attending medical staff.
|
| 91 |
+
</p>
|
| 92 |
+
{% endif %}
|
| 93 |
+
|
| 94 |
+
</div>
|
| 95 |
+
</body>
|
| 96 |
+
</html>
|
templates/index.html
CHANGED
|
@@ -12,6 +12,9 @@
|
|
| 12 |
<a href="/passport" class="bg-white/15 border border-white/40 text-white font-bold text-sm px-6 py-3 rounded-xl hover:bg-white/25 transition-colors inline-flex items-center gap-2">
|
| 13 |
<i class="fa-solid fa-id-card-clip"></i> Check Your Health Passport
|
| 14 |
</a>
|
|
|
|
|
|
|
|
|
|
| 15 |
</div>
|
| 16 |
</div>
|
| 17 |
|
|
|
|
| 12 |
<a href="/passport" class="bg-white/15 border border-white/40 text-white font-bold text-sm px-6 py-3 rounded-xl hover:bg-white/25 transition-colors inline-flex items-center gap-2">
|
| 13 |
<i class="fa-solid fa-id-card-clip"></i> Check Your Health Passport
|
| 14 |
</a>
|
| 15 |
+
<a href="/emergency-help" class="bg-red-600 text-white font-bold text-sm px-6 py-3 rounded-xl shadow-lg hover:bg-red-700 transition-colors inline-flex items-center gap-2">
|
| 16 |
+
<i class="fa-solid fa-truck-medical"></i> Emergency Help
|
| 17 |
+
</a>
|
| 18 |
</div>
|
| 19 |
</div>
|
| 20 |
|
templates/login.html
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<div class="min-h-[70vh] flex items-center justify-center">
|
| 4 |
+
<div class="w-full max-w-md bg-white p-8 rounded-3xl border border-slate-100 shadow-xl shadow-blue-50">
|
| 5 |
+
<div class="text-center mb-6">
|
| 6 |
+
<div class="bg-blue-600 text-white w-14 h-14 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-blue-200">
|
| 7 |
+
<i class="fa-solid fa-id-card-clip text-2xl"></i>
|
| 8 |
+
</div>
|
| 9 |
+
<h1 class="text-2xl font-black text-slate-800">Welcome Back</h1>
|
| 10 |
+
<p class="text-sm text-slate-500 mt-1">Log in to access your Health Passport.</p>
|
| 11 |
+
</div>
|
| 12 |
+
|
| 13 |
+
<form method="POST" action="/login" class="space-y-4">
|
| 14 |
+
<div>
|
| 15 |
+
<label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Email</label>
|
| 16 |
+
<input type="email" name="email" required class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="you@example.com">
|
| 17 |
+
</div>
|
| 18 |
+
<div>
|
| 19 |
+
<label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Password</label>
|
| 20 |
+
<input type="password" name="password" required class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="••••••••">
|
| 21 |
+
</div>
|
| 22 |
+
<button type="submit" class="w-full bg-blue-600 text-white font-bold py-3 rounded-xl hover:bg-blue-700 transition-colors shadow-lg shadow-blue-200">
|
| 23 |
+
<i class="fa-solid fa-right-to-bracket mr-2"></i>Log In
|
| 24 |
+
</button>
|
| 25 |
+
</form>
|
| 26 |
+
|
| 27 |
+
<p class="text-center text-sm text-slate-500 mt-6">
|
| 28 |
+
Don't have an account? <a href="/register" class="text-blue-600 font-bold hover:underline">Register here</a>
|
| 29 |
+
</p>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
{% endblock %}
|
templates/passport.html
CHANGED
|
@@ -1,55 +1,114 @@
|
|
| 1 |
{% extends 'base.html' %}
|
| 2 |
{% block content %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
<
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
</select>
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
<
|
| 21 |
-
|
| 22 |
-
</div>
|
| 23 |
-
|
| 24 |
-
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Active Prescribed Medications</label>
|
| 25 |
-
<textarea id="passMeds" rows="2" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500"></textarea>
|
| 26 |
-
</div>
|
| 27 |
-
<div>
|
| 28 |
-
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Emergency Point of Contact</label>
|
| 29 |
-
<input type="text" id="passContact" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
|
| 30 |
-
</div>
|
| 31 |
-
<div>
|
| 32 |
-
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Contact Emergency Phone Line</label>
|
| 33 |
-
<input type="tel" id="passPhone" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
|
| 34 |
-
</div>
|
| 35 |
-
|
| 36 |
-
<button type="submit" class="md:col-span-2 mt-2 bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 rounded-xl transition-colors text-xs tracking-wider uppercase">
|
| 37 |
-
Compile & Encrypt Locally
|
| 38 |
-
</button>
|
| 39 |
-
</form>
|
| 40 |
</div>
|
| 41 |
|
| 42 |
-
<!--
|
| 43 |
<div class="lg:col-span-5 flex flex-col gap-6">
|
| 44 |
-
<div class="bg-gradient-to-br from-emerald-800 to-teal-950 text-white p-6 rounded-2xl shadow-lg flex flex-col items-center justify-center text-center gap-4 min-h-[
|
| 45 |
-
<h4 class="font-bold text-sm tracking-wider uppercase text-emerald-300">
|
| 46 |
-
<!-- QR Verification Placeholder Area -->
|
| 47 |
<div class="bg-white p-4 rounded-xl shadow-inner inline-block" id="qrContainer">
|
| 48 |
<i class="fa-solid fa-qrcode text-8xl text-slate-300 p-4"></i>
|
| 49 |
</div>
|
| 50 |
<p class="text-[11px] text-emerald-200/80 max-w-xs leading-relaxed">
|
| 51 |
-
|
| 52 |
</p>
|
|
|
|
|
|
|
|
|
|
| 53 |
</div>
|
| 54 |
</div>
|
| 55 |
</div>
|
|
|
|
| 1 |
{% extends 'base.html' %}
|
| 2 |
{% block content %}
|
| 3 |
+
<div class="mb-6">
|
| 4 |
+
<h1 class="text-2xl font-black text-slate-800">Your Health Passport</h1>
|
| 5 |
+
<p class="text-sm text-slate-500 mt-1">Logged in as <span class="font-bold">{{ user.name }}</span> ({{ user.email }})</p>
|
| 6 |
+
</div>
|
| 7 |
+
|
| 8 |
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
| 9 |
+
|
| 10 |
+
<!-- LEFT COLUMN: forms -->
|
| 11 |
+
<div class="lg:col-span-7 flex flex-col gap-6">
|
| 12 |
+
|
| 13 |
+
<!-- Personal + Medical Details -->
|
| 14 |
+
<div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
|
| 15 |
+
<h2 class="text-lg font-bold text-slate-800 mb-4"><i class="fa-solid fa-id-card-clip text-emerald-600 mr-2"></i>Personal & Medical Details</h2>
|
| 16 |
+
<form id="passportForm" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 17 |
+
<div>
|
| 18 |
+
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Full Name</label>
|
| 19 |
+
<input type="text" id="passName" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" required>
|
| 20 |
+
</div>
|
| 21 |
+
<div>
|
| 22 |
+
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Blood Group</label>
|
| 23 |
+
<select id="passBlood" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
|
| 24 |
+
<option>A+</option><option>O+</option><option>B+</option><option>AB+</option>
|
| 25 |
+
<option>A-</option><option>O-</option><option>B-</option><option>AB-</option>
|
| 26 |
+
</select>
|
| 27 |
+
</div>
|
| 28 |
+
<div class="md:col-span-2">
|
| 29 |
+
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Date of Birth</label>
|
| 30 |
+
<input type="text" id="passDob" placeholder="DD-MM-YYYY" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
|
| 31 |
+
</div>
|
| 32 |
+
<div class="md:col-span-2">
|
| 33 |
+
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Allergies</label>
|
| 34 |
+
<input type="text" id="passAllergies" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" placeholder="e.g. Penicillin, Sulfa drugs, Nuts">
|
| 35 |
+
</div>
|
| 36 |
+
<div class="md:col-span-2">
|
| 37 |
+
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Chronic Diseases</label>
|
| 38 |
+
<input type="text" id="passChronic" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" placeholder="e.g. Diabetes, Asthma, Hypertension">
|
| 39 |
+
</div>
|
| 40 |
+
<div class="md:col-span-2">
|
| 41 |
+
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Current Medicines</label>
|
| 42 |
+
<textarea id="passMeds" rows="2" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500"></textarea>
|
| 43 |
+
</div>
|
| 44 |
+
<div>
|
| 45 |
+
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Emergency Contact Name</label>
|
| 46 |
+
<input type="text" id="passContact" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
|
| 47 |
+
</div>
|
| 48 |
+
<div>
|
| 49 |
+
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Emergency Contact Phone</label>
|
| 50 |
+
<input type="tel" id="passPhone" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
|
| 51 |
+
</div>
|
| 52 |
+
<button type="submit" class="md:col-span-2 mt-2 bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 rounded-xl transition-colors text-xs tracking-wider uppercase">
|
| 53 |
+
Save Passport Details
|
| 54 |
+
</button>
|
| 55 |
+
</form>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<!-- Surgeries -->
|
| 59 |
+
<div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
|
| 60 |
+
<h2 class="text-lg font-bold text-slate-800 mb-4"><i class="fa-solid fa-user-doctor text-blue-600 mr-2"></i>Surgical History</h2>
|
| 61 |
+
<form id="surgeryForm" class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
|
| 62 |
+
<input type="text" id="surgeryYear" placeholder="Year" class="md:col-span-1 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-blue-500" required>
|
| 63 |
+
<input type="text" id="surgeryDescription" placeholder="e.g. Appendectomy -- brief description" class="md:col-span-2 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-blue-500" required>
|
| 64 |
+
<button type="submit" class="md:col-span-1 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2.5 rounded-xl text-xs uppercase tracking-wider">Add</button>
|
| 65 |
+
</form>
|
| 66 |
+
<div id="surgeryList" class="flex flex-col gap-2"></div>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<!-- Vaccinations -->
|
| 70 |
+
<div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
|
| 71 |
+
<h2 class="text-lg font-bold text-slate-800 mb-4"><i class="fa-solid fa-syringe text-emerald-600 mr-2"></i>Vaccination Record</h2>
|
| 72 |
+
<form id="vaccinationForm" class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
|
| 73 |
+
<input type="text" id="vaccineName" placeholder="Vaccine name" class="md:col-span-2 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" required>
|
| 74 |
+
<input type="text" id="vaccineMonth" placeholder="Month" class="md:col-span-1 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" required>
|
| 75 |
+
<input type="text" id="vaccineYear" placeholder="Year" class="md:col-span-1 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" required>
|
| 76 |
+
<button type="submit" class="md:col-span-4 bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-2.5 rounded-xl text-xs uppercase tracking-wider">Add Vaccination</button>
|
| 77 |
+
</form>
|
| 78 |
+
<div id="vaccinationList" class="flex flex-col gap-2"></div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<!-- Medical Report Uploads -->
|
| 82 |
+
<div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
|
| 83 |
+
<h2 class="text-lg font-bold text-slate-800 mb-4"><i class="fa-solid fa-file-medical text-purple-600 mr-2"></i>Medical Report Documents</h2>
|
| 84 |
+
<form id="reportForm" class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4" enctype="multipart/form-data">
|
| 85 |
+
<input type="file" id="reportFile" class="md:col-span-4 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-purple-500" required>
|
| 86 |
+
<select id="reportCategory" class="md:col-span-2 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-purple-500">
|
| 87 |
+
<option>Blood Report</option><option>MRI</option><option>X-Ray</option>
|
| 88 |
+
<option>ECG</option><option>Prescription</option><option>CT Scan</option>
|
| 89 |
+
<option>Discharge Summary</option><option>Other</option>
|
| 90 |
</select>
|
| 91 |
+
<input type="text" id="reportMonth" placeholder="Month" class="md:col-span-1 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-purple-500">
|
| 92 |
+
<input type="text" id="reportYear" placeholder="Year" class="md:col-span-1 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-purple-500">
|
| 93 |
+
<button type="submit" class="md:col-span-4 bg-purple-600 hover:bg-purple-700 text-white font-bold py-2.5 rounded-xl text-xs uppercase tracking-wider">Upload Report</button>
|
| 94 |
+
</form>
|
| 95 |
+
<div id="reportList" class="flex flex-col gap-2"></div>
|
| 96 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
</div>
|
| 98 |
|
| 99 |
+
<!-- RIGHT COLUMN: QR + PDF -->
|
| 100 |
<div class="lg:col-span-5 flex flex-col gap-6">
|
| 101 |
+
<div class="bg-gradient-to-br from-emerald-800 to-teal-950 text-white p-6 rounded-2xl shadow-lg flex flex-col items-center justify-center text-center gap-4 min-h-[300px] sticky top-24">
|
| 102 |
+
<h4 class="font-bold text-sm tracking-wider uppercase text-emerald-300">Emergency QR Code</h4>
|
|
|
|
| 103 |
<div class="bg-white p-4 rounded-xl shadow-inner inline-block" id="qrContainer">
|
| 104 |
<i class="fa-solid fa-qrcode text-8xl text-slate-300 p-4"></i>
|
| 105 |
</div>
|
| 106 |
<p class="text-[11px] text-emerald-200/80 max-w-xs leading-relaxed">
|
| 107 |
+
Any phone camera can scan this -- opens a read-only emergency summary instantly. No app or login needed for whoever scans it.
|
| 108 |
</p>
|
| 109 |
+
<a href="/api/passport/pdf" id="downloadPdfBtn" class="w-full bg-white text-emerald-800 font-bold py-3 rounded-xl hover:bg-emerald-50 transition-colors text-xs uppercase tracking-wider">
|
| 110 |
+
<i class="fa-solid fa-file-pdf mr-2"></i>Download Printable PDF
|
| 111 |
+
</a>
|
| 112 |
</div>
|
| 113 |
</div>
|
| 114 |
</div>
|
templates/register.html
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<div class="min-h-[70vh] flex items-center justify-center">
|
| 4 |
+
<div class="w-full max-w-md bg-white p-8 rounded-3xl border border-slate-100 shadow-xl shadow-blue-50">
|
| 5 |
+
<div class="text-center mb-6">
|
| 6 |
+
<div class="bg-emerald-600 text-white w-14 h-14 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-emerald-200">
|
| 7 |
+
<i class="fa-solid fa-user-plus text-2xl"></i>
|
| 8 |
+
</div>
|
| 9 |
+
<h1 class="text-2xl font-black text-slate-800">Create Your Account</h1>
|
| 10 |
+
<p class="text-sm text-slate-500 mt-1">Set up your personal Health Passport in a minute.</p>
|
| 11 |
+
</div>
|
| 12 |
+
|
| 13 |
+
<form method="POST" action="/register" class="space-y-4">
|
| 14 |
+
<div>
|
| 15 |
+
<label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Full Name</label>
|
| 16 |
+
<input type="text" name="name" required class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="Jane Doe">
|
| 17 |
+
</div>
|
| 18 |
+
<div>
|
| 19 |
+
<label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Email</label>
|
| 20 |
+
<input type="email" name="email" required class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="you@example.com">
|
| 21 |
+
</div>
|
| 22 |
+
<div>
|
| 23 |
+
<label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Password</label>
|
| 24 |
+
<input type="password" name="password" required minlength="8" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="At least 8 characters">
|
| 25 |
+
</div>
|
| 26 |
+
<div>
|
| 27 |
+
<label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Confirm Password</label>
|
| 28 |
+
<input type="password" name="confirm_password" required minlength="8" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="Re-enter password">
|
| 29 |
+
</div>
|
| 30 |
+
<button type="submit" class="w-full bg-emerald-600 text-white font-bold py-3 rounded-xl hover:bg-emerald-700 transition-colors shadow-lg shadow-emerald-200">
|
| 31 |
+
<i class="fa-solid fa-user-plus mr-2"></i>Create Account
|
| 32 |
+
</button>
|
| 33 |
+
</form>
|
| 34 |
+
|
| 35 |
+
<p class="text-center text-sm text-slate-500 mt-6">
|
| 36 |
+
Already have an account? <a href="/login" class="text-blue-600 font-bold hover:underline">Log in here</a>
|
| 37 |
+
</p>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
{% endblock %}
|