Initial commit: Masina-Dock Vehicle Management System

This commit is contained in:
Iulian 2025-10-19 11:10:11 +01:00
commit ae923e2c41
4999 changed files with 1607266 additions and 0 deletions

293
AppDir/backend/app.py Normal file
View file

@ -0,0 +1,293 @@
from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS
from flask_login import login_required, current_user
from models import db, Vehicle, ServiceRecord, FuelRecord, Reminder, Todo
from auth import auth_bp, login_manager
from config import Config
import os
from datetime import datetime
import pandas as pd
from werkzeug.utils import secure_filename
app = Flask(__name__, static_folder='../frontend/static', template_folder='../frontend/templates')
app.config.from_object(Config)
CORS(app, supports_credentials=True)
db.init_app(app)
login_manager.init_app(app)
app.register_blueprint(auth_bp)
ALLOWED_EXTENSIONS = {'pdf', 'png', 'jpg', 'jpeg', 'gif', 'csv'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/')
def index():
return send_from_directory(app.template_folder, 'index.html')
@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)
)
db.session.add(vehicle)
db.session.commit()
return jsonify({'id': vehicle.id, 'message': 'Vehicle added successfully'}), 201
@app.route('/api/vehicles/<int:vehicle_id>', 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)
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/<int:vehicle_id>/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')
)
db.session.add(record)
db.session.commit()
return jsonify({'id': record.id, 'message': 'Service record added successfully'}), 201
@app.route('/api/vehicles/<int:vehicle_id>/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
} 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')
)
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/<int:vehicle_id>/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/<int:vehicle_id>/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/export/<data_type>', 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)
@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
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(host='0.0.0.0', port=5000, debug=False)

107
AppDir/backend/auth.py Normal file
View file

@ -0,0 +1,107 @@
from flask import Blueprint, request, jsonify
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from models import db, User
from functools import wraps
auth_bp = Blueprint('auth', __name__)
login_manager = LoginManager()
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
def admin_required(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
return jsonify({'error': 'Admin access required'}), 403
return f(*args, **kwargs)
return decorated_function
@auth_bp.route('/api/auth/register', methods=['POST'])
def register():
data = request.get_json()
if User.query.filter_by(username=data['username']).first():
return jsonify({'error': 'Username already exists'}), 400
if User.query.filter_by(email=data['email']).first():
return jsonify({'error': 'Email already exists'}), 400
user = User(
username=data['username'],
email=data['email'],
is_admin=User.query.count() == 0
)
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
return jsonify({'message': 'User created successfully', 'user_id': user.id}), 201
@auth_bp.route('/api/auth/login', methods=['POST'])
def login():
data = request.get_json()
user = User.query.filter_by(username=data['username']).first()
if user and user.check_password(data['password']):
login_user(user, remember=True)
return jsonify({
'message': 'Login successful',
'user': {
'id': user.id,
'username': user.username,
'email': user.email,
'is_admin': user.is_admin,
'theme': user.theme,
'first_login': user.first_login
}
}), 200
return jsonify({'error': 'Invalid credentials'}), 401
@auth_bp.route('/api/auth/logout', methods=['POST'])
@login_required
def logout():
logout_user()
return jsonify({'message': 'Logged out successfully'}), 200
@auth_bp.route('/api/auth/change-password', methods=['POST'])
@login_required
def change_password():
data = request.get_json()
if not current_user.check_password(data['current_password']):
return jsonify({'error': 'Current password is incorrect'}), 400
current_user.set_password(data['new_password'])
current_user.first_login = False
db.session.commit()
return jsonify({'message': 'Password changed successfully'}), 200
@auth_bp.route('/api/auth/users', methods=['GET'])
@admin_required
def get_users():
users = User.query.all()
return jsonify([{
'id': u.id,
'username': u.username,
'email': u.email,
'is_admin': u.is_admin,
'created_at': u.created_at.isoformat()
} for u in users]), 200
@auth_bp.route('/api/auth/users/<int:user_id>', methods=['DELETE'])
@admin_required
def delete_user(user_id):
if user_id == current_user.id:
return jsonify({'error': 'Cannot delete your own account'}), 400
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return jsonify({'message': 'User deleted successfully'}), 200

13
AppDir/backend/config.py Normal file
View file

@ -0,0 +1,13 @@
import os
from datetime import timedelta
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'masina-dock-secret-key-change-in-production'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///masina_dock.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
MAX_CONTENT_LENGTH = 16 * 1024 * 1024
UPLOAD_FOLDER = 'uploads'

92
AppDir/backend/models.py Normal file
View file

@ -0,0 +1,92 @@
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
db = SQLAlchemy()
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
theme = db.Column(db.String(20), default='dark')
first_login = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
vehicles = db.relationship('Vehicle', backref='owner', lazy=True, cascade='all, delete-orphan')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Vehicle(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
year = db.Column(db.Integer, nullable=False)
make = db.Column(db.String(100), nullable=False)
model = db.Column(db.String(100), nullable=False)
vin = db.Column(db.String(17))
license_plate = db.Column(db.String(20))
odometer = db.Column(db.Integer, default=0)
photo = db.Column(db.String(255))
status = db.Column(db.String(20), default='active')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
service_records = db.relationship('ServiceRecord', backref='vehicle', lazy=True, cascade='all, delete-orphan')
fuel_records = db.relationship('FuelRecord', backref='vehicle', lazy=True, cascade='all, delete-orphan')
reminders = db.relationship('Reminder', backref='vehicle', lazy=True, cascade='all, delete-orphan')
todos = db.relationship('Todo', backref='vehicle', lazy=True, cascade='all, delete-orphan')
class ServiceRecord(db.Model):
id = db.Column(db.Integer, primary_key=True)
vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id'), nullable=False)
date = db.Column(db.DateTime, nullable=False)
odometer = db.Column(db.Integer, nullable=False)
description = db.Column(db.Text, nullable=False)
cost = db.Column(db.Float, default=0.0)
notes = db.Column(db.Text)
category = db.Column(db.String(50))
document_path = db.Column(db.String(255))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class FuelRecord(db.Model):
id = db.Column(db.Integer, primary_key=True)
vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id'), nullable=False)
date = db.Column(db.DateTime, nullable=False)
odometer = db.Column(db.Integer, nullable=False)
fuel_amount = db.Column(db.Float, nullable=False)
cost = db.Column(db.Float, default=0.0)
unit_cost = db.Column(db.Float)
distance = db.Column(db.Integer)
fuel_economy = db.Column(db.Float)
unit = db.Column(db.String(20), default='MPG')
notes = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class Reminder(db.Model):
id = db.Column(db.Integer, primary_key=True)
vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id'), nullable=False)
description = db.Column(db.String(255), nullable=False)
urgency = db.Column(db.String(20), default='not_urgent')
due_date = db.Column(db.DateTime)
due_odometer = db.Column(db.Integer)
metric = db.Column(db.Integer)
recurring = db.Column(db.Boolean, default=False)
interval_type = db.Column(db.String(20))
interval_value = db.Column(db.Integer)
notes = db.Column(db.Text)
completed = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class Todo(db.Model):
id = db.Column(db.Integer, primary_key=True)
vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id'), nullable=False)
description = db.Column(db.String(255), nullable=False)
cost = db.Column(db.Float, default=0.0)
priority = db.Column(db.String(20), default='medium')
status = db.Column(db.String(20), default='planned')
type = db.Column(db.String(50))
notes = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)

View file

@ -0,0 +1,9 @@
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Flask-Login==0.6.3
Flask-CORS==4.0.0
Werkzeug==3.0.1
python-dotenv==1.0.0
gunicorn==21.2.0
pandas==2.1.4
openpyxl==3.1.2