From c0f98761847521f1bd1fa8b411fa23bab7bfb2b1 Mon Sep 17 00:00:00 2001 From: cdez Date: Tue, 3 Jun 2025 16:50:03 -0700 Subject: [PATCH] Create the template for the Flask framework --- Dockerfile | 12 ++++++++ app/__init__.py | 35 +++++++++++++++++++++ app/forms.py | 0 app/models.py | 67 +++++++++++++++++++++++++++++++++++++++++ app/routes/audit.py | 11 +++++++ app/routes/customer.py | 11 +++++++ app/routes/repair.py | 38 +++++++++++++++++++++++ app/templates/base.html | 26 ++++++++++++++++ app/utils.py | 0 app/utils/barcode.py | 16 ++++++++++ config.py | 7 +++++ docker-compose.yml | 27 +++++++++++++++++ requirements.txt | 10 ++++++ run.py | 6 ++++ 14 files changed, 266 insertions(+) create mode 100644 Dockerfile create mode 100644 app/__init__.py create mode 100644 app/forms.py create mode 100644 app/models.py create mode 100644 app/routes/audit.py create mode 100644 app/routes/customer.py create mode 100644 app/routes/repair.py create mode 100644 app/templates/base.html create mode 100644 app/utils.py create mode 100644 app/utils/barcode.py create mode 100644 config.py create mode 100644 docker-compose.yml create mode 100644 requirements.txt create mode 100644 run.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3b1dfc7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . /app + +ENV FLASK_APP=run.py +ENV FLASK_ENV=production + +CMD ["flask", "run", "--host=0.0.0.0"] \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..0a19175 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,35 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_migrate import Migrate +from config import Config + +db = SQLAlchemy() +login_manager = LoginManager() +migrate = Migrate() + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + db.init_app(app) + login_manager.init_app(app) + migrate.init_app(app, db) + + # Register blueprints for modular routes + from .routes.admin import admin_bp + from .routes.auth import auth_bp + from .routes.customer import customer_bp + from .routes.repair import repair_bp + from .routes.inventory import inventory_bp + from .routes.technician import technician_bp + from .routes.audit import audit_bp + app.register_blueprint(admin_bp) + app.register_blueprint(auth_bp) + app.register_blueprint(customer_bp) + app.register_blueprint(repair_bp) + app.register_blueprint(inventory_bp) + app.register_blueprint(technician_bp) + app.register_blueprint(audit_bp) + + return app \ No newline at end of file diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..1e03b14 --- /dev/null +++ b/app/models.py @@ -0,0 +1,67 @@ +from . import db +from flask_login import UserMixin +from datetime import datetime + +# User roles: admin, technician, customer +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + password_hash = db.Column(db.String(128), nullable=False) + role = db.Column(db.String(20), default='customer') + customer = db.relationship('Customer', uselist=False, backref='user') + +class Customer(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120)) + email = db.Column(db.String(120), unique=True) + phone = db.Column(db.String(20)) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + repairs = db.relationship('RepairJob', backref='customer') + +class Technician(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120)) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + repairs = db.relationship('RepairJob', backref='technician') + +class RepairJob(db.Model): + id = db.Column(db.Integer, primary_key=True) + tracking_number = db.Column(db.String(50), unique=True) + device = db.Column(db.String(120)) + description = db.Column(db.Text) + status = db.Column(db.String(50), default='New') + customer_id = db.Column(db.Integer, db.ForeignKey('customer.id')) + technician_id = db.Column(db.Integer, db.ForeignKey('technician.id')) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + cost_estimate = db.Column(db.Float) + final_cost = db.Column(db.Float) + parts_used = db.relationship('RepairPart', backref='repair') + barcode_path = db.Column(db.String(256)) + notes = db.relationship('RepairNote', backref='repair') + +class RepairNote(db.Model): + id = db.Column(db.Integer, primary_key=True) + repair_id = db.Column(db.Integer, db.ForeignKey('repair_job.id')) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + note = db.Column(db.Text) + timestamp = db.Column(db.DateTime, default=datetime.utcnow) + +class RepairPart(db.Model): + id = db.Column(db.Integer, primary_key=True) + repair_id = db.Column(db.Integer, db.ForeignKey('repair_job.id')) + part_id = db.Column(db.Integer, db.ForeignKey('inventory_item.id')) + quantity = db.Column(db.Integer) + +class InventoryItem(db.Model): + id = db.Column(db.Integer, primary_key=True) + part_name = db.Column(db.String(120)) + quantity = db.Column(db.Integer) + low_stock_threshold = db.Column(db.Integer, default=1) + +class AuditLog(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + action = db.Column(db.String(256)) + timestamp = db.Column(db.DateTime, default=datetime.utcnow) + details = db.Column(db.Text) \ No newline at end of file diff --git a/app/routes/audit.py b/app/routes/audit.py new file mode 100644 index 0000000..d63a4b6 --- /dev/null +++ b/app/routes/audit.py @@ -0,0 +1,11 @@ +from flask import Blueprint, render_template +from flask_login import login_required +from app.models import AuditLog + +audit_bp = Blueprint('audit', __name__, url_prefix='/audit') + +@audit_bp.route('/log') +@login_required +def log(): + logs = AuditLog.query.order_by(AuditLog.timestamp.desc()).all() + return render_template('audit/log.html', logs=logs) \ No newline at end of file diff --git a/app/routes/customer.py b/app/routes/customer.py new file mode 100644 index 0000000..23fc621 --- /dev/null +++ b/app/routes/customer.py @@ -0,0 +1,11 @@ +from flask import Blueprint, render_template +from flask_login import login_required, current_user +from app.models import Customer + +customer_bp = Blueprint('customer', __name__, url_prefix='/customers') + +@customer_bp.route('/portal') +@login_required +def portal(): + customer = Customer.query.filter_by(user_id=current_user.id).first_or_404() + return render_template('customers/portal.html', customer=customer) \ No newline at end of file diff --git a/app/routes/repair.py b/app/routes/repair.py new file mode 100644 index 0000000..eb1541d --- /dev/null +++ b/app/routes/repair.py @@ -0,0 +1,38 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app +from flask_login import login_required, current_user +from app.models import db, RepairJob, Customer, Technician, AuditLog +from app.utils.barcode import generate_barcode + +repair_bp = Blueprint('repair', __name__, url_prefix='/repairs') + +@repair_bp.route('/new', methods=['GET', 'POST']) +@login_required +def new_repair(): + if request.method == 'POST': + # ... get form data ... + repair = RepairJob( + device=request.form['device'], + description=request.form['description'], + customer_id=request.form['customer_id'], + technician_id=request.form.get('technician_id'), + ) + db.session.add(repair) + db.session.commit() + # Generate and save barcode image + barcode_path = generate_barcode(repair.id, repair.tracking_number) + repair.barcode_path = barcode_path + db.session.commit() + # Audit log + audit = AuditLog(user_id=current_user.id, action='Created repair', details=f"Repair ID {repair.id}") + db.session.add(audit) + db.session.commit() + flash('Repair job created!') + return redirect(url_for('repair.detail', repair_id=repair.id)) + # ... render form ... + return render_template('repairs/new.html') + +@repair_bp.route('/') +@login_required +def detail(repair_id): + repair = RepairJob.query.get_or_404(repair_id) + return render_template('repairs/detail.html', repair=repair) \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..8bf7aac --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,26 @@ + + + + + + {% block title %}Repair Tracker{% endblock %} + + + + + + + {% include 'navbar.html' %} +
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
{{ messages[0] }}
+ {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ + + + + \ No newline at end of file diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/barcode.py b/app/utils/barcode.py new file mode 100644 index 0000000..0568c9f --- /dev/null +++ b/app/utils/barcode.py @@ -0,0 +1,16 @@ +import os +import qrcode +from barcode import Code128 +from barcode.writer import ImageWriter +from flask import current_app + +def generate_barcode(repair_id, tracking_number): + # Generate barcode image + barcode = Code128(tracking_number, writer=ImageWriter()) + barcode_path = os.path.join(current_app.config['UPLOAD_FOLDER'], f'barcode_{repair_id}.png') + barcode.save(barcode_path) + # Optionally, also generate QR code + qr = qrcode.make(tracking_number) + qr_path = os.path.join(current_app.config['UPLOAD_FOLDER'], f'qr_{repair_id}.png') + qr.save(qr_path) + return barcode_path \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..af64e97 --- /dev/null +++ b/config.py @@ -0,0 +1,7 @@ +import os + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY', 'dev') + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///repair_tracker.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False + UPLOAD_FOLDER = 'app/static/barcodes' \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f93958a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3' +services: + web: + build: . + ports: + - "5000:5000" + environment: + - DATABASE_URL=postgresql://postgres:password@db:5432/repairdb + - FLASK_ENV=production + - SECRET_KEY=supersecret + depends_on: + - db + volumes: + - .:/app + db: + image: postgres:15 + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: repairdb + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data +volumes: + pgdata: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..211538a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +Flask +Flask-Login +Flask-WTF +Flask-SQLAlchemy +Flask-Migrate +psycopg2-binary +Werkzeug +python-barcode +qrcode +Pillow \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..b4ee1bc --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) \ No newline at end of file