from flask import Flask, jsonify, request, send_from_directory, send_file, session, redirect from flask_cors import CORS from flask_login import login_required, current_user from flask_mail import Mail from models import db, Vehicle, ServiceRecord, FuelRecord, Reminder, Todo, User, RecurringExpense from auth import auth_bp, login_manager from config import Config import os from datetime import datetime, timedelta import pandas as pd from werkzeug.utils import secure_filename import io import shutil import zipfile import tempfile import secrets import re app = Flask(__name__, static_folder='../frontend/static', template_folder='../frontend/templates') app.config.from_object(Config) app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=12) app.config['SESSION_COOKIE_SECURE'] = False app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' CORS(app, supports_credentials=True, resources={r"/api/*": {"origins": "*"}}) db.init_app(app) login_manager.init_app(app) mail = Mail(app) app.register_blueprint(auth_bp) ALLOWED_EXTENSIONS = {'pdf', 'png', 'jpg', 'jpeg', 'gif', 'csv'} ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} ALLOWED_ATTACHMENT_EXTENSIONS = {'pdf', 'png', 'jpg', 'jpeg', 'txt', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'odt', 'ods'} def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def allowed_image(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_IMAGE_EXTENSIONS def allowed_attachment(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_ATTACHMENT_EXTENSIONS def validate_email(email): pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return re.match(pattern, email) is not None @app.before_request def check_session(): session.permanent = True if 'csrf_token' not in session: session['csrf_token'] = secrets.token_hex(16) @app.route('/') def index(): return send_from_directory(app.template_folder, 'index.html') @app.route('/login') def login_page(): return send_from_directory(app.template_folder, 'login.html') @app.route('/register') def register_page(): return send_from_directory(app.template_folder, 'register.html') @app.route('/first-login') @login_required def first_login_page(): if not current_user.must_change_credentials: return redirect('/dashboard') return send_from_directory(app.template_folder, 'first_login.html') @app.route('/dashboard') @login_required def dashboard(): return send_from_directory(app.template_folder, 'dashboard.html') @app.route('/settings') @login_required def settings_page(): return send_from_directory(app.template_folder, 'settings.html') @app.route('/vehicles') @login_required def vehicles_page(): return send_from_directory(app.template_folder, 'vehicles.html') @app.route('/vehicle-detail') @login_required def vehicle_detail_page(): return send_from_directory(app.template_folder, 'vehicle_detail.html') @app.route('/service-records') @login_required def service_records_page(): return send_from_directory(app.template_folder, 'service_records.html') @app.route('/repairs') @login_required def repairs_page(): return send_from_directory(app.template_folder, 'repairs.html') @app.route('/fuel') @login_required def fuel_page(): return send_from_directory(app.template_folder, 'fuel.html') @app.route('/taxes') @login_required def taxes_page(): return send_from_directory(app.template_folder, 'taxes.html') @app.route('/notes') @login_required def notes_page(): return send_from_directory(app.template_folder, 'notes.html') @app.route('/reminders') @login_required def reminders_page(): return send_from_directory(app.template_folder, 'reminders.html') @app.route('/planner') @login_required def planner_page(): return send_from_directory(app.template_folder, 'planner.html') @app.route('/uploads/') def serve_upload(filename): return send_from_directory('/app/uploads', filename) @app.route('/api/settings', methods=['GET']) @login_required def get_settings(): return jsonify({ 'username': current_user.username, 'email': current_user.email, 'language': current_user.language, 'unit_system': current_user.unit_system, 'currency': current_user.currency, 'theme': current_user.theme, 'photo': current_user.photo }), 200 @app.route('/api/settings/language', methods=['POST']) @login_required def update_language(): data = request.get_json() current_user.language = data.get('language', 'en') db.session.commit() return jsonify({'message': 'Language updated successfully'}), 200 @app.route('/api/settings/units', methods=['POST']) @login_required def update_units(): data = request.get_json() current_user.unit_system = data.get('unit_system', 'metric') current_user.currency = data.get('currency', 'USD') db.session.commit() return jsonify({'message': 'Units and currency updated successfully'}), 200 @app.route('/api/settings/theme', methods=['POST']) @login_required def update_theme(): data = request.get_json() current_user.theme = data['theme'] db.session.commit() return jsonify({'message': 'Theme updated successfully'}), 200 @app.route('/api/user/update-profile', methods=['POST']) @login_required def update_profile(): data = request.get_json() if 'username' in data and data['username'] != current_user.username: existing = User.query.filter_by(username=data['username']).first() if existing: return jsonify({'error': 'Username already taken'}), 400 current_user.username = data['username'] if 'email' in data and data['email'] != current_user.email: if not validate_email(data['email']): return jsonify({'error': 'Invalid email format'}), 400 existing = User.query.filter_by(email=data['email']).first() if existing: return jsonify({'error': 'Email already registered'}), 400 current_user.email = data['email'] if 'photo' in data: current_user.photo = data['photo'] db.session.commit() return jsonify({'message': 'Profile updated successfully'}), 200 @app.route('/api/backup/create', methods=['GET']) @login_required def create_backup(): timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') backup_filename = f'masina_dock_backup_{timestamp}.zip' temp_dir = tempfile.mkdtemp() backup_path = os.path.join(temp_dir, backup_filename) try: with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf: db_path = '/app/data/masina_dock.db' if os.path.exists(db_path): zipf.write(db_path, 'masina_dock.db') uploads_dir = '/app/uploads' if os.path.exists(uploads_dir): for root, dirs, files in os.walk(uploads_dir): for file in files: file_path = os.path.join(root, file) arcname = os.path.relpath(file_path, '/app') zipf.write(file_path, arcname) return send_file( backup_path, mimetype='application/zip', as_attachment=True, download_name=backup_filename ) except Exception as e: if os.path.exists(temp_dir): shutil.rmtree(temp_dir) return jsonify({'error': str(e)}), 500 @app.route('/api/backup/restore', methods=['POST']) @login_required def restore_backup(): if 'backup' not in request.files: return jsonify({'error': 'No backup file provided'}), 400 file = request.files['backup'] if file.filename == '': return jsonify({'error': 'No file selected'}), 400 if not file.filename.endswith('.zip'): return jsonify({'error': 'Invalid file type. Please upload a .zip file.'}), 400 temp_dir = tempfile.mkdtemp() backup_path = os.path.join(temp_dir, secure_filename(file.filename)) try: file.save(backup_path) with zipfile.ZipFile(backup_path, 'r') as zipf: zipf.extractall(temp_dir) extracted_db = os.path.join(temp_dir, 'masina_dock.db') if os.path.exists(extracted_db): target_db = '/app/data/masina_dock.db' backup_db = '/app/data/masina_dock.db.backup' if os.path.exists(target_db): shutil.copy2(target_db, backup_db) os.remove(target_db) shutil.copy2(extracted_db, target_db) else: raise Exception('Database file not found in backup') extracted_uploads = os.path.join(temp_dir, 'uploads') if os.path.exists(extracted_uploads): target_uploads = '/app/uploads' for item in os.listdir(target_uploads): item_path = os.path.join(target_uploads, item) try: if os.path.isfile(item_path): os.remove(item_path) elif os.path.isdir(item_path): shutil.rmtree(item_path) except Exception as e: print(f"Error removing {item_path}: {e}") for root, dirs, files in os.walk(extracted_uploads): rel_path = os.path.relpath(root, extracted_uploads) target_path = os.path.join(target_uploads, rel_path) if rel_path != '.' else target_uploads os.makedirs(target_path, exist_ok=True) for file in files: src_file = os.path.join(root, file) dst_file = os.path.join(target_path, file) shutil.copy2(src_file, dst_file) os.chmod(dst_file, 0o644) if os.path.exists(temp_dir): shutil.rmtree(temp_dir) return jsonify({'message': 'Backup restored successfully'}), 200 except zipfile.BadZipFile: if os.path.exists(temp_dir): shutil.rmtree(temp_dir) return jsonify({'error': 'Invalid or corrupted backup file'}), 400 except Exception as e: if os.path.exists(temp_dir): shutil.rmtree(temp_dir) return jsonify({'error': f'Restore failed: {str(e)}'}), 500 @app.route('/api/vehicles', methods=['GET', 'POST']) @login_required def vehicles(): if request.method == 'GET': vehicles = Vehicle.query.filter_by(user_id=current_user.id).all() return jsonify([{ 'id': v.id, 'year': v.year, 'make': v.make, 'model': v.model, 'vin': v.vin, 'license_plate': v.license_plate, 'odometer': v.odometer, 'photo': v.photo, 'status': v.status } for v in vehicles]), 200 elif request.method == 'POST': data = request.get_json() vehicle = Vehicle( user_id=current_user.id, year=data['year'], make=data['make'], model=data['model'], vin=data.get('vin'), license_plate=data.get('license_plate'), odometer=data.get('odometer', 0), photo=data.get('photo') ) db.session.add(vehicle) db.session.commit() return jsonify({'id': vehicle.id, 'message': 'Vehicle added successfully'}), 201 @app.route('/api/upload/photo', methods=['POST']) @login_required def upload_photo(): if 'photo' not in request.files: return jsonify({'error': 'No photo provided'}), 400 file = request.files['photo'] if file.filename == '': return jsonify({'error': 'No file selected'}), 400 if file and allowed_image(file.filename): filename = secure_filename(file.filename) timestamp = datetime.now().strftime('%Y%m%d%H%M%S') random_suffix = secrets.token_hex(4) filename = f"{timestamp}_{random_suffix}_{filename}" uploads_dir = '/app/uploads' os.makedirs(uploads_dir, exist_ok=True) filepath = os.path.join(uploads_dir, filename) file.save(filepath) os.chmod(filepath, 0o644) return jsonify({'photo_url': f'/uploads/{filename}'}), 200 return jsonify({'error': 'Invalid file type. Only images are allowed.'}), 400 @app.route('/api/upload/attachment', methods=['POST']) @login_required def upload_attachment(): if 'attachment' not in request.files: return jsonify({'error': 'No attachment provided'}), 400 file = request.files['attachment'] if file.filename == '': return jsonify({'error': 'No file selected'}), 400 if file and allowed_attachment(file.filename): filename = secure_filename(file.filename) timestamp = datetime.now().strftime('%Y%m%d%H%M%S') random_suffix = secrets.token_hex(4) filename = f"{timestamp}_{random_suffix}_{filename}" attachments_dir = '/app/uploads/attachments' os.makedirs(attachments_dir, exist_ok=True) filepath = os.path.join(attachments_dir, filename) file.save(filepath) os.chmod(filepath, 0o644) relative_path = f'attachments/{filename}' return jsonify({'file_path': relative_path}), 200 return jsonify({'error': 'Invalid file type'}), 400 @app.route('/api/attachments/download', methods=['GET']) @login_required def download_attachment(): file_path = request.args.get('path') if not file_path: return jsonify({'error': 'No file path provided'}), 400 # Normalize the file path to prevent directory traversal file_path = os.path.normpath(file_path) # Ensure the path doesn't try to escape the uploads directory if '..' in file_path or file_path.startswith('/'): return jsonify({'error': 'Invalid file path'}), 403 full_path = os.path.normpath(os.path.join('/app/uploads', file_path)) # Double-check the resolved path is within the uploads directory if not full_path.startswith('/app/uploads/'): return jsonify({'error': 'Invalid file path'}), 403 # Check if the file exists and is actually a file (not a directory) if not os.path.isfile(full_path): return jsonify({'error': 'File not found'}), 404 directory = os.path.dirname(full_path) filename = os.path.basename(full_path) return send_from_directory(directory, filename, as_attachment=True) @app.route('/api/attachments/delete', methods=['DELETE']) @login_required def delete_attachment(): """Delete an attachment file. Only the file owner can delete it.""" file_path = request.args.get('path') if not file_path: return jsonify({'error': 'No file path provided'}), 400 # Normalize the file path to prevent directory traversal file_path = os.path.normpath(file_path) # Ensure the path doesn't try to escape the uploads directory if '..' in file_path or file_path.startswith('/'): return jsonify({'error': 'Invalid file path'}), 403 full_path = os.path.normpath(os.path.join('/app/uploads', file_path)) # Double-check the resolved path is within the uploads directory if not full_path.startswith('/app/uploads/'): return jsonify({'error': 'Invalid file path'}), 403 # Check existence without revealing path details if not os.path.isfile(full_path): return jsonify({'error': 'File not found'}), 404 try: os.remove(full_path) return jsonify({'message': 'Attachment deleted successfully'}), 200 except OSError: # Don't expose internal error details return jsonify({'error': 'Failed to delete attachment'}), 500 @app.route('/api/vehicles/', methods=['GET', 'PUT', 'DELETE']) @login_required def vehicle_detail(vehicle_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() if request.method == 'GET': return jsonify({ 'id': vehicle.id, 'year': vehicle.year, 'make': vehicle.make, 'model': vehicle.model, 'vin': vehicle.vin, 'license_plate': vehicle.license_plate, 'odometer': vehicle.odometer, 'photo': vehicle.photo, 'status': vehicle.status }), 200 elif request.method == 'PUT': data = request.get_json() vehicle.year = data.get('year', vehicle.year) vehicle.make = data.get('make', vehicle.make) vehicle.model = data.get('model', vehicle.model) vehicle.vin = data.get('vin', vehicle.vin) vehicle.license_plate = data.get('license_plate', vehicle.license_plate) vehicle.odometer = data.get('odometer', vehicle.odometer) vehicle.status = data.get('status', vehicle.status) vehicle.photo = data.get('photo', vehicle.photo) db.session.commit() return jsonify({'message': 'Vehicle updated successfully'}), 200 elif request.method == 'DELETE': db.session.delete(vehicle) db.session.commit() return jsonify({'message': 'Vehicle deleted successfully'}), 200 @app.route('/api/vehicles//service-records', methods=['GET', 'POST']) @login_required def service_records(vehicle_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() if request.method == 'GET': records = ServiceRecord.query.filter_by(vehicle_id=vehicle_id).order_by(ServiceRecord.date.desc()).all() return jsonify([{ 'id': r.id, 'date': r.date.isoformat(), 'odometer': r.odometer, 'description': r.description, 'cost': r.cost, 'notes': r.notes, 'category': r.category, 'document_path': r.document_path } for r in records]), 200 elif request.method == 'POST': data = request.get_json() record = ServiceRecord( vehicle_id=vehicle_id, date=datetime.fromisoformat(data['date']), odometer=data['odometer'], description=data['description'], cost=data.get('cost', 0.0), notes=data.get('notes'), category=data.get('category'), document_path=data.get('document_path') ) db.session.add(record) db.session.commit() return jsonify({'id': record.id, 'message': 'Service record added successfully'}), 201 @app.route('/api/vehicles//fuel-records', methods=['GET', 'POST']) @login_required def fuel_records(vehicle_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() if request.method == 'GET': records = FuelRecord.query.filter_by(vehicle_id=vehicle_id).order_by(FuelRecord.date.desc()).all() return jsonify([{ 'id': r.id, 'date': r.date.isoformat(), 'odometer': r.odometer, 'fuel_amount': r.fuel_amount, 'cost': r.cost, 'unit_cost': r.unit_cost, 'distance': r.distance, 'fuel_economy': r.fuel_economy, 'unit': r.unit, 'notes': r.notes, 'document_path': r.document_path } for r in records]), 200 elif request.method == 'POST': data = request.get_json() last_record = FuelRecord.query.filter_by(vehicle_id=vehicle_id).order_by(FuelRecord.odometer.desc()).first() distance = data['odometer'] - last_record.odometer if last_record else 0 fuel_economy = None if distance > 0 and data['fuel_amount'] > 0: if data.get('unit', 'MPG') == 'MPG': fuel_economy = distance / data['fuel_amount'] elif data.get('unit') == 'L/100KM': fuel_economy = (data['fuel_amount'] / distance) * 100 elif data.get('unit') == 'KM/L': fuel_economy = distance / data['fuel_amount'] record = FuelRecord( vehicle_id=vehicle_id, date=datetime.fromisoformat(data['date']), odometer=data['odometer'], fuel_amount=data['fuel_amount'], cost=data.get('cost', 0.0), unit_cost=data.get('unit_cost'), distance=distance, fuel_economy=fuel_economy, unit=data.get('unit', 'MPG'), notes=data.get('notes'), document_path=data.get('document_path') ) db.session.add(record) db.session.commit() return jsonify({'id': record.id, 'message': 'Fuel record added successfully', 'fuel_economy': fuel_economy}), 201 @app.route('/api/vehicles//reminders', methods=['GET', 'POST']) @login_required def reminders(vehicle_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() if request.method == 'GET': reminders = Reminder.query.filter_by(vehicle_id=vehicle_id, completed=False).all() return jsonify([{ 'id': r.id, 'description': r.description, 'urgency': r.urgency, 'due_date': r.due_date.isoformat() if r.due_date else None, 'due_odometer': r.due_odometer, 'metric': r.metric, 'recurring': r.recurring, 'interval_type': r.interval_type, 'interval_value': r.interval_value, 'notes': r.notes } for r in reminders]), 200 elif request.method == 'POST': data = request.get_json() reminder = Reminder( vehicle_id=vehicle_id, description=data['description'], urgency=data.get('urgency', 'not_urgent'), due_date=datetime.fromisoformat(data['due_date']) if data.get('due_date') else None, due_odometer=data.get('due_odometer'), metric=data.get('metric'), recurring=data.get('recurring', False), interval_type=data.get('interval_type'), interval_value=data.get('interval_value'), notes=data.get('notes') ) db.session.add(reminder) db.session.commit() return jsonify({'id': reminder.id, 'message': 'Reminder added successfully'}), 201 @app.route('/api/vehicles//todos', methods=['GET', 'POST']) @login_required def todos(vehicle_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() if request.method == 'GET': todos = Todo.query.filter_by(vehicle_id=vehicle_id).all() return jsonify([{ 'id': t.id, 'description': t.description, 'cost': t.cost, 'priority': t.priority, 'status': t.status, 'type': t.type, 'notes': t.notes } for t in todos]), 200 elif request.method == 'POST': data = request.get_json() todo = Todo( vehicle_id=vehicle_id, description=data['description'], cost=data.get('cost', 0.0), priority=data.get('priority', 'medium'), status=data.get('status', 'planned'), type=data.get('type'), notes=data.get('notes') ) db.session.add(todo) db.session.commit() return jsonify({'id': todo.id, 'message': 'Todo added successfully'}), 201 @app.route('/api/vehicles//recurring-expenses', methods=['GET', 'POST']) @login_required def recurring_expenses(vehicle_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() if request.method == 'GET': expenses = RecurringExpense.query.filter_by(vehicle_id=vehicle_id, is_active=True).all() return jsonify([{ 'id': e.id, 'expense_type': e.expense_type, 'description': e.description, 'amount': e.amount, 'frequency': e.frequency, 'start_date': e.start_date.isoformat(), 'next_due_date': e.next_due_date.isoformat(), 'is_active': e.is_active, 'notes': e.notes, 'document_path': e.document_path } for e in expenses]), 200 elif request.method == 'POST': data = request.get_json() start_date = datetime.fromisoformat(data['start_date']).date() frequency = data['frequency'] if frequency == 'monthly': next_due = start_date + timedelta(days=30) elif frequency == 'quarterly': next_due = start_date + timedelta(days=90) elif frequency == 'yearly': next_due = start_date + timedelta(days=365) else: next_due = start_date expense = RecurringExpense( vehicle_id=vehicle_id, expense_type=data['expense_type'], description=data['description'], amount=data['amount'], frequency=frequency, start_date=start_date, next_due_date=next_due, notes=data.get('notes'), document_path=data.get('document_path') ) db.session.add(expense) db.session.commit() return jsonify({ 'id': expense.id, 'message': 'Recurring expense added successfully', 'next_due_date': next_due.isoformat() }), 201 @app.route('/api/vehicles//recurring-expenses/', methods=['DELETE']) @login_required def cancel_recurring_expense(vehicle_id, expense_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() expense = RecurringExpense.query.filter_by(id=expense_id, vehicle_id=vehicle_id).first_or_404() expense.is_active = False db.session.commit() return jsonify({'message': 'Recurring expense cancelled successfully'}), 200 @app.route('/api/vehicles//export-all', methods=['GET']) @login_required def export_all_vehicle_data(vehicle_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() service_records = ServiceRecord.query.filter_by(vehicle_id=vehicle_id).order_by(ServiceRecord.date.desc()).all() fuel_records = FuelRecord.query.filter_by(vehicle_id=vehicle_id).order_by(FuelRecord.date.desc()).all() reminders = Reminder.query.filter_by(vehicle_id=vehicle_id).all() todos = Todo.query.filter_by(vehicle_id=vehicle_id).all() output = io.BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: vehicle_data = pd.DataFrame([{ 'Year': vehicle.year, 'Make': vehicle.make, 'Model': vehicle.model, 'VIN': vehicle.vin or '', 'License Plate': vehicle.license_plate or '', 'Current Odometer': vehicle.odometer, 'Status': vehicle.status, 'Photo URL': vehicle.photo or '' }]) vehicle_data.to_excel(writer, sheet_name='Vehicle Info', index=False) if service_records: service_df = pd.DataFrame([{ 'Date': r.date.strftime('%Y-%m-%d'), 'Odometer': r.odometer, 'Description': r.description, 'Category': r.category or '', 'Cost': r.cost, 'Notes': r.notes or '', 'Document': r.document_path or '' } for r in service_records]) service_df.to_excel(writer, sheet_name='Service Records', index=False) if fuel_records: fuel_df = pd.DataFrame([{ 'Date': r.date.strftime('%Y-%m-%d'), 'Odometer': r.odometer, 'Fuel Amount': r.fuel_amount, 'Unit': r.unit, 'Cost': r.cost, 'Unit Cost': r.unit_cost or 0, 'Distance': r.distance or 0, 'Fuel Economy': r.fuel_economy or 0, 'Notes': r.notes or '' } for r in fuel_records]) fuel_df.to_excel(writer, sheet_name='Fuel Records', index=False) if reminders: reminders_df = pd.DataFrame([{ 'Description': r.description, 'Urgency': r.urgency, 'Due Date': r.due_date.strftime('%Y-%m-%d') if r.due_date else '', 'Due Odometer': r.due_odometer or '', 'Recurring': r.recurring, 'Interval Type': r.interval_type or '', 'Interval Value': r.interval_value or '', 'Notes': r.notes or '', 'Completed': r.completed } for r in reminders]) reminders_df.to_excel(writer, sheet_name='Reminders', index=False) if todos: todos_df = pd.DataFrame([{ 'Description': t.description, 'Type': t.type or '', 'Priority': t.priority, 'Status': t.status, 'Cost': t.cost, 'Notes': t.notes or '' } for t in todos]) todos_df.to_excel(writer, sheet_name='Todos', index=False) output.seek(0) filename = f"{vehicle.year}_{vehicle.make}_{vehicle.model}_Complete_Data_{datetime.now().strftime('%Y%m%d')}.xlsx" return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=filename ) @app.route('/api/export/', methods=['GET']) @login_required def export_data(data_type): vehicle_id = request.args.get('vehicle_id') if data_type == 'service_records': records = ServiceRecord.query.filter_by(vehicle_id=vehicle_id).all() df = pd.DataFrame([{ 'Date': r.date, 'Odometer': r.odometer, 'Description': r.description, 'Cost': r.cost, 'Category': r.category, 'Notes': r.notes } for r in records]) elif data_type == 'fuel_records': records = FuelRecord.query.filter_by(vehicle_id=vehicle_id).all() df = pd.DataFrame([{ 'Date': r.date, 'Odometer': r.odometer, 'Fuel Amount': r.fuel_amount, 'Cost': r.cost, 'Fuel Economy': r.fuel_economy, 'Unit': r.unit } for r in records]) else: return jsonify({'error': 'Invalid data type'}), 400 output_file = f'/tmp/{data_type}_{vehicle_id}.csv' df.to_csv(output_file, index=False) return send_from_directory('/tmp', f'{data_type}_{vehicle_id}.csv', as_attachment=True) # Edit/Delete operations for Service Records @app.route('/api/vehicles//service-records/', methods=['GET', 'PUT', 'DELETE']) @login_required def service_record_operations(vehicle_id, record_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() record = ServiceRecord.query.filter_by(id=record_id, vehicle_id=vehicle_id).first_or_404() if request.method == 'GET': return jsonify({ 'id': record.id, 'date': record.date.isoformat(), 'odometer': record.odometer, 'description': record.description, 'category': record.category, 'cost': record.cost, 'notes': record.notes or '', 'document_path': record.document_path or '' }) elif request.method == 'PUT': data = request.get_json() record.date = datetime.fromisoformat(data['date']).date() if 'date' in data else record.date record.odometer = data.get('odometer', record.odometer) record.description = data.get('description', record.description) record.category = data.get('category', record.category) record.cost = data.get('cost', record.cost) record.notes = data.get('notes', record.notes) record.document_path = data.get('document_path', record.document_path) db.session.commit() return jsonify({'message': 'Service record updated successfully'}) elif request.method == 'DELETE': # Clean up attachment if it exists if record.document_path: try: full_path = os.path.join('/app/uploads', record.document_path) if os.path.exists(full_path): os.remove(full_path) except Exception as e: print(f"Failed to delete attachment: {e}") db.session.delete(record) db.session.commit() return jsonify({'message': 'Service record deleted successfully'}) # Edit/Delete operations for Fuel Records @app.route('/api/vehicles//fuel-records/', methods=['GET', 'PUT', 'DELETE']) @login_required def fuel_record_operations(vehicle_id, record_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() record = FuelRecord.query.filter_by(id=record_id, vehicle_id=vehicle_id).first_or_404() if request.method == 'GET': return jsonify({ 'id': record.id, 'date': record.date.isoformat(), 'odometer': record.odometer, 'fuel_amount': record.fuel_amount, 'cost': record.cost, 'notes': record.notes or '', 'document_path': record.document_path or '' }) elif request.method == 'PUT': data = request.get_json() record.date = datetime.fromisoformat(data['date']).date() if 'date' in data else record.date record.odometer = data.get('odometer', record.odometer) record.fuel_amount = data.get('fuel_amount', record.fuel_amount) record.cost = data.get('cost', record.cost) record.notes = data.get('notes', record.notes) record.document_path = data.get('document_path', record.document_path) db.session.commit() return jsonify({'message': 'Fuel record updated successfully'}) elif request.method == 'DELETE': # Clean up attachment if it exists if record.document_path: try: full_path = os.path.join('/app/uploads', record.document_path) if os.path.exists(full_path): os.remove(full_path) except Exception as e: print(f"Failed to delete attachment: {e}") db.session.delete(record) db.session.commit() return jsonify({'message': 'Fuel record deleted successfully'}) # Edit/Delete operations for Reminders @app.route('/api/vehicles//reminders/', methods=['GET', 'PUT', 'DELETE']) @login_required def reminder_operations(vehicle_id, reminder_id): vehicle = Vehicle.query.filter_by(id=vehicle_id, user_id=current_user.id).first_or_404() reminder = Reminder.query.filter_by(id=reminder_id, vehicle_id=vehicle_id).first_or_404() if request.method == 'GET': return jsonify({ 'id': reminder.id, 'description': reminder.description, 'urgency': reminder.urgency, 'due_date': reminder.due_date.isoformat() if reminder.due_date else None, 'due_odometer': reminder.due_odometer, 'notes': reminder.notes or '' }) elif request.method == 'PUT': data = request.get_json() reminder.description = data.get('description', reminder.description) reminder.urgency = data.get('urgency', reminder.urgency) if 'due_date' in data and data['due_date']: reminder.due_date = datetime.fromisoformat(data['due_date']).date() reminder.due_odometer = data.get('due_odometer', reminder.due_odometer) reminder.notes = data.get('notes', reminder.notes) db.session.commit() return jsonify({'message': 'Reminder updated successfully'}) elif request.method == 'DELETE': db.session.delete(reminder) db.session.commit() return jsonify({'message': 'Reminder deleted successfully'}) if __name__ == '__main__': with app.app_context(): db.create_all() app.run(host='0.0.0.0', port=5000, debug=False)