Files

518 lines
19 KiB
Python

from flask import Flask, render_template, request, jsonify, session
from flask_cors import CORS
import json
from datetime import datetime
from data_manager import DataManager
import os
from functools import wraps
app = Flask(__name__, static_folder='static', template_folder='templates')
CORS(app)
app.secret_key = os.environ.get('SECRET_KEY', 'rfid-detection-secret-key-change-in-production')
# Create data directory if it doesn't exist
os.makedirs('data', exist_ok=True)
# Global variables
data_manager = DataManager("data/participants.json", "data/scan_logs.json")
last_scan_result = None
# Admin credentials (change this to your desired password)
ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD', 'Pleterski')
def require_admin(f):
"""Decorator to require admin authentication"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'admin' not in session or not session['admin']:
return jsonify({'success': False, 'message': 'Unauthorized'}), 401
return f(*args, **kwargs)
return decorated_function
def process_card(uid):
"""Process scanned card or key"""
global last_scan_result
participant = data_manager.get_participant_by_card(uid)
# Get card and key UIDs from participant
card_uid = participant.get('card_uid') if participant else None
key_uid = participant.get('key_uid') if participant else None
if participant and not data_manager.is_card_expired(participant):
data_manager.log_scan(uid, "PASS", participant['name'], card_uid=card_uid, key_uid=key_uid)
last_scan_result = {
'uid': uid,
'name': participant['name'],
'status': 'PASS',
'color': 'green',
'timestamp': datetime.now().isoformat()
}
else:
# FAIL if participant not found or card is expired
name = participant['name'] if participant else "Unknown"
reason = "Expired" if participant and data_manager.is_card_expired(participant) else "Unknown"
# For FAIL scans, log the scanned UID as card_uid
data_manager.log_scan(uid, "FAIL", name, reason, card_uid=uid, key_uid=None)
last_scan_result = {
'uid': uid,
'name': name if participant else 'Unknown',
'status': 'FAIL',
'color': 'red',
'timestamp': datetime.now().isoformat()
}
return last_scan_result
# ===== AUTHENTICATION ROUTES =====
@app.route('/api/login', methods=['POST'])
def login():
"""Admin login"""
try:
data = request.json
password = data.get('password', '').strip()
if password == ADMIN_PASSWORD:
session['admin'] = True
return jsonify({'success': True, 'message': 'Logged in successfully'})
else:
return jsonify({'success': False, 'message': 'Invalid password'}), 401
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/api/logout', methods=['POST'])
def logout():
"""Admin logout"""
session.pop('admin', None)
return jsonify({'success': True, 'message': 'Logged out successfully'})
@app.route('/api/check-admin', methods=['GET'])
def check_admin():
"""Check if user is logged in as admin"""
is_admin = 'admin' in session and session['admin']
return jsonify({'is_admin': is_admin})
# ===== API Routes =====
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/scan', methods=['POST'])
def receive_wifi_scan():
"""Receive card scan from WiFi-enabled ESP8266"""
try:
data = request.json
uid = data.get('uid', '').strip()
if not uid:
return jsonify({'success': False, 'message': 'No UID provided'}), 400
print(f"[WiFi Scan] Card scanned via WiFi: {uid}")
result = process_card(uid)
print(f"[WiFi Scan] Result: {result}")
return jsonify({'success': True, 'result': result})
except Exception as e:
print(f"[WiFi Scan Error] {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/scan', methods=['POST'])
def receive_wifi_scan_root():
"""Fallback route for /scan (without /api prefix)"""
try:
data = request.json
uid = data.get('uid', '').strip()
if not uid:
return jsonify({'success': False, 'message': 'No UID provided'}), 400
print(f"[WiFi Scan Root] Card scanned via WiFi: {uid}")
result = process_card(uid)
print(f"[WiFi Scan Root] Result: {result}")
return jsonify({'success': True, 'result': result})
except Exception as e:
print(f"[WiFi Scan Root Error] {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/api/participants', methods=['GET'])
def get_participants():
"""Get all participants"""
participants = data_manager.get_all_participants()
return jsonify(participants)
@app.route('/api/participants', methods=['POST'])
@require_admin
def add_participant():
"""Add a new participant (admin only)"""
try:
data = request.json
if not data:
return jsonify({'success': False, 'message': 'Invalid JSON'}), 400
name = (data.get('name') or '').strip()
card_uid = (data.get('card_uid') or '').strip() or None
key_uid = (data.get('key_uid') or '').strip() or None
expiration_date = (data.get('expiration_date') or '').strip() or None
if not name:
return jsonify({'success': False, 'message': 'Name is required'}), 400
if not card_uid and not key_uid:
return jsonify({'success': False, 'message': 'At least one of card UID or key UID is required'}), 400
if data_manager.add_participant(name, card_uid, expiration_date, key_uid):
return jsonify({'success': True, 'message': 'Participant added'})
else:
return jsonify({'success': False, 'message': 'Duplicate UID or error'}), 400
except Exception as e:
print(f"Error adding participant: {e}")
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/api/participants/<int:participant_id>', methods=['PUT'])
@require_admin
def update_participant(participant_id):
"""Update a participant (admin only)"""
try:
data = request.json
if not data:
return jsonify({'success': False, 'message': 'Invalid JSON'}), 400
name = (data.get('name') or '').strip()
card_uid = (data.get('card_uid') or '').strip() or None
key_uid = (data.get('key_uid') or '').strip() or None
expiration_date = (data.get('expiration_date') or '').strip() or None
if not name:
return jsonify({'success': False, 'message': 'Name is required'}), 400
if not card_uid and not key_uid:
return jsonify({'success': False, 'message': 'At least one of card UID or key UID is required'}), 400
if data_manager.update_participant(participant_id, name, card_uid, expiration_date, key_uid):
return jsonify({'success': True, 'message': 'Participant updated'})
else:
return jsonify({'success': False, 'message': 'Update failed'}), 400
except Exception as e:
print(f"Error updating participant: {e}")
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/api/participants/<int:participant_id>', methods=['DELETE'])
@require_admin
def delete_participant(participant_id):
"""Delete a participant (admin only)"""
if data_manager.delete_participant(participant_id):
return jsonify({'success': True, 'message': 'Participant deleted'})
else:
return jsonify({'success': False, 'message': 'Delete failed'}), 400
@app.route('/api/logs', methods=['GET'])
def get_logs():
"""Get scan logs"""
filter_type = request.args.get('filter', 'All')
logs = data_manager.get_scan_history(1000)
if filter_type == 'PASS':
logs = [log for log in logs if log['scan_result'] == 'PASS']
elif filter_type == 'FAIL':
logs = [log for log in logs if log['scan_result'] == 'FAIL']
return jsonify(logs)
@app.route('/api/logs/unregistered', methods=['POST'])
def add_unregistered_log():
"""Add unregistered user to logs"""
try:
data = request.json
if not data:
return jsonify({'success': False, 'message': 'Invalid JSON'}), 400
name = (data.get('name') or '').strip()
timestamp = (data.get('timestamp') or '').strip() or None
if not name:
return jsonify({'success': False, 'message': 'Name is required'}), 400
if data_manager.log_unregistered_user(name, timestamp):
return jsonify({'success': True, 'message': 'Unregistered user logged'})
else:
return jsonify({'success': False, 'message': 'Error logging user'}), 400
except Exception as e:
print(f"Error adding unregistered log: {e}")
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/api/logs/<int:log_id>', methods=['DELETE'])
@require_admin
def delete_log(log_id):
"""Delete a log entry (admin only)"""
try:
if data_manager.delete_log(log_id):
return jsonify({'success': True, 'message': 'Log deleted'})
else:
return jsonify({'success': False, 'message': 'Log not found'}), 404
except Exception as e:
print(f"Error deleting log: {e}")
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/api/export', methods=['GET'])
def export_logs():
"""Export logs to Excel - returns file data for client-side saving"""
try:
import openpyxl
from openpyxl.styles import PatternFill, Font, Alignment
import io
import base64
from datetime import datetime as dt
logs = data_manager.get_scan_history(10000)
# Apply filters from query parameters
filter_type = request.args.get('filter', 'All')
participant_filter = request.args.get('participant', '')
start_date = request.args.get('startDate', '')
end_date = request.args.get('endDate', '')
# Filter by result type
if filter_type == 'PASS':
logs = [log for log in logs if log['scan_result'] == 'PASS']
elif filter_type == 'FAIL':
logs = [log for log in logs if log['scan_result'] == 'FAIL']
# Filter by participant
if participant_filter:
logs = [log for log in logs if log.get('name') == participant_filter]
# Filter by date range
if start_date:
start_dt = dt.strptime(start_date, '%Y-%m-%d')
logs = [log for log in logs if dt.fromisoformat(log['timestamp'].replace('Z', '+00:00')) >= start_dt]
if end_date:
end_dt = dt.strptime(end_date, '%Y-%m-%d')
end_dt = end_dt.replace(hour=23, minute=59, second=59)
logs = [log for log in logs if dt.fromisoformat(log['timestamp'].replace('Z', '+00:00')) <= end_dt]
if not logs:
return jsonify({'success': False, 'message': 'No logs to export'}), 400
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Scan Logs"
headers = ["ID", "Name", "Card UID", "Key UID", "Result", "Date", "Time"]
ws.append(headers)
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF")
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal="center", vertical="center")
for log in logs:
timestamp = log['timestamp']
date_part = timestamp[:10]
time_part = timestamp[11:19]
ws.append([
log['id'],
log['name'],
log['card_uid'],
log.get('key_uid', ''),
log['scan_result'],
date_part,
time_part
])
pass_fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
pass_font = Font(color="006100")
fail_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
fail_font = Font(color="9C0006")
for idx, row in enumerate(ws.iter_rows(min_row=2, max_row=ws.max_row), start=2):
result_cell = row[4] # Result is now column 5 (index 4)
if result_cell.value == "PASS":
result_cell.fill = pass_fill
result_cell.font = pass_font
else:
result_cell.fill = fail_fill
result_cell.font = fail_font
ws.column_dimensions['A'].width = 8
ws.column_dimensions['B'].width = 20
ws.column_dimensions['C'].width = 20
ws.column_dimensions['D'].width = 20
ws.column_dimensions['E'].width = 10
ws.column_dimensions['F'].width = 12
ws.column_dimensions['G'].width = 10
# Generate filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"scan_logs_{timestamp}.xlsx"
# Save to bytes buffer
buffer = io.BytesIO()
wb.save(buffer)
buffer.seek(0)
# Encode to base64 for transfer
file_data = base64.b64encode(buffer.getvalue()).decode('utf-8')
return jsonify({
'success': True,
'filename': filename,
'data': file_data
})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
# ===== STORE ROUTES =====
@app.route('/api/store/items', methods=['GET'])
def get_store_items():
return jsonify(data_manager.get_store_items())
@app.route('/api/store/items', methods=['POST'])
@require_admin
def add_store_item():
try:
data = request.json
name = (data.get('name') or '').strip()
item_type = data.get('type', 'product')
stock = 0 if item_type == 'service' else int(data.get('stock', 0))
image = data.get('image') or None
pm = data.get('price_member')
pnm = data.get('price_non_member')
price_member = float(pm) if pm is not None else None
price_non_member = float(pnm) if pnm is not None else None
price = price_member if price_member is not None else float(data.get('price', 0))
if not name:
return jsonify({'success': False, 'message': 'Name required'}), 400
if price < 0:
return jsonify({'success': False, 'message': 'Price cannot be negative'}), 400
if item_type != 'service' and stock < 0:
return jsonify({'success': False, 'message': 'Stock cannot be negative'}), 400
if data_manager.add_store_item(name, price, stock, image, item_type, price_member, price_non_member):
return jsonify({'success': True})
return jsonify({'success': False, 'message': 'Error adding item'}), 400
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/api/store/items/<int:item_id>', methods=['PUT'])
@require_admin
def update_store_item(item_id):
try:
data = request.json
name = (data.get('name') or '').strip()
item_type = data.get('type', 'product')
stock = 0 if item_type == 'service' else int(data.get('stock', 0))
image = data.get('image') or None
pm = data.get('price_member')
pnm = data.get('price_non_member')
price_member = float(pm) if pm is not None else None
price_non_member = float(pnm) if pnm is not None else None
price = price_member if price_member is not None else float(data.get('price', 0))
if not name:
return jsonify({'success': False, 'message': 'Name required'}), 400
if data_manager.update_store_item(item_id, name, price, stock, image, item_type, price_member, price_non_member):
return jsonify({'success': True})
return jsonify({'success': False, 'message': 'Item not found'}), 404
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/api/store/items/<int:item_id>', methods=['DELETE'])
@require_admin
def delete_store_item(item_id):
if data_manager.delete_store_item(item_id):
return jsonify({'success': True})
return jsonify({'success': False, 'message': 'Item not found'}), 404
@app.route('/api/store/payments', methods=['GET'])
def get_payments():
return jsonify(data_manager.get_payments())
@app.route('/api/store/payments', methods=['POST'])
def log_payment():
try:
data = request.json
items = data.get('items', [])
total = float(data.get('total', 0))
balance_before = float(data.get('balance_before', 0))
balance_after = float(data.get('balance_after', 0))
seller = (data.get('seller') or '').strip()
cart_items = data.get('cart_items', []) # [{id, name, price, qty, image}]
# Deduct stock for each sold product (services have no stock)
for ci in cart_items:
item_id = ci.get('id')
new_stock = ci.get('new_stock')
item_type = ci.get('type', 'product')
name = ci.get('name', '')
price = ci.get('price', 0)
image = ci.get('image')
if item_id is not None and new_stock is not None and item_type != 'service':
data_manager.update_store_item(item_id, name, float(price), int(new_stock), image, item_type)
# Update balance atomically with payment log
data_manager.set_balance(balance_after)
if data_manager.log_payment(items, total, balance_before, balance_after, seller):
return jsonify({'success': True})
return jsonify({'success': False, 'message': 'Error logging payment'}), 400
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/api/store/payments', methods=['DELETE'])
@require_admin
def clear_payments():
if data_manager.clear_payments():
return jsonify({'success': True})
return jsonify({'success': False, 'message': 'Error clearing payments'}), 400
@app.route('/api/store/balance', methods=['GET'])
def get_balance():
return jsonify({'balance': data_manager.get_balance()})
@app.route('/api/store/balance', methods=['PUT'])
@require_admin
def set_balance():
try:
data = request.json
amount = float(data.get('balance', 0))
if data_manager.set_balance(amount):
return jsonify({'success': True, 'balance': round(amount, 2)})
return jsonify({'success': False, 'message': 'Error setting balance'}), 400
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 400
@app.route('/api/stats', methods=['GET'])
def get_stats():
"""Get statistics"""
stats = data_manager.get_statistics()
return jsonify(stats)
@app.route('/api/documents', methods=['GET'])
def get_documents():
"""List PDF files in static/PDF folder"""
pdf_dir = os.path.join(app.static_folder, 'PDF')
os.makedirs(pdf_dir, exist_ok=True)
files = [f for f in os.listdir(pdf_dir) if f.lower().endswith('.pdf')]
files.sort()
return jsonify(files)
@app.route('/api/last-scan', methods=['GET'])
def get_last_scan():
"""Get last scan result"""
return jsonify(last_scan_result if last_scan_result else {
'uid': '-',
'name': '-',
'status': '-',
'color': 'gray'
})
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)