From 46fa0d1ae5137265ce8e0aa5001eaaef14ae4268 Mon Sep 17 00:00:00 2001 From: cdez Date: Tue, 3 Jun 2025 21:13:39 -0700 Subject: [PATCH] Improve on the template, most things work??? --- README.md | 28 ++++++++- app/__init__.py | 33 +++------- app/forms.py | 24 +++++++ app/models.py | 83 ++++++++++--------------- app/routes/__init__.py | 9 +++ app/routes/admin.py | 54 ++++++++++++++++ app/routes/audit.py | 11 ---- app/routes/customer.py | 41 +++++++++--- app/routes/repair.py | 38 ----------- app/routes/technician.py | 24 +++++++ app/templates/admin/dashboard.html | 29 +++++++++ app/templates/base.html | 45 +++++++++----- app/templates/customer/dashboard.html | 58 +++++++++++++++++ app/templates/technician/dashboard.html | 44 +++++++++++++ app/utils.py | 0 app/utils/barcode.py | 16 ----- config.py | 18 +++++- requirements.txt | 20 +++--- run.py | 2 +- 19 files changed, 396 insertions(+), 181 deletions(-) create mode 100644 app/routes/__init__.py create mode 100644 app/routes/admin.py delete mode 100644 app/routes/audit.py delete mode 100644 app/routes/repair.py create mode 100644 app/routes/technician.py create mode 100644 app/templates/admin/dashboard.html create mode 100644 app/templates/customer/dashboard.html create mode 100644 app/templates/technician/dashboard.html delete mode 100644 app/utils.py delete mode 100644 app/utils/barcode.py diff --git a/README.md b/README.md index 6d1cdee..eacabb9 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,33 @@ Open Service Tracker Repair Tool, created as an internal tool for Miracle Comput - [ ] Simple Authentication - [ ] OAuth2/LDAP/External Authentication Integration - [ ] Form and paperwork generation/editor (contains templates for forms/other paperwork to be filled out) - - [ ] SQLite or full SQL Database support + - [ ] SQLite or full SQL Database support (switchable) - [ ] Mailing for customer repairs and technician appointments - [ ] Uses SMTP and other information to send mail - [ ] Self-hostable & Licensable - [ ] Self-hostable for personal/limited commercial use - - [ ] Licensable for full commercial use \ No newline at end of file + - [ ] Licensable for full commercial use + - [ ] Dockerized for self-hosting + +## Setup Instructions +1. Clone the repository: + ``` + git clone + ``` +2. Navigate to the project directory: + ``` + cd OpenSTR + ``` +3. Install the required dependencies: + ``` + pip install -r requirements.txt + ``` +4. Configure the application settings in `config.py`. +5. Run the application: + ``` + python run.py + ``` + +## Usage +- Access the application through your web browser at `http://localhost:5000`. +- Use the customer, technician, and admin panels to manage repairs and appointments. \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 0a19175..ec208fc 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,35 +1,20 @@ 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) - + + from app.routes import customer, technician, admin + app.register_blueprint(customer.customer_bp) + app.register_blueprint(technician.technician_bp) + app.register_blueprint(admin.admin_bp) + + with app.app_context(): + from app import models + return app \ No newline at end of file diff --git a/app/forms.py b/app/forms.py index e69de29..0d1d39a 100644 --- a/app/forms.py +++ b/app/forms.py @@ -0,0 +1,24 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, TextAreaField, DateTimeField, SubmitField +from wtforms.validators import DataRequired, Email + +class AppointmentForm(FlaskForm): + customer_email = StringField('Customer Email', validators=[DataRequired(), Email()]) + appointment_date = DateTimeField('Appointment Date', format='%Y-%m-%d %H:%M:%S', validators=[DataRequired()]) + notes = TextAreaField('Notes', validators=[DataRequired()]) + submit = SubmitField('Schedule Appointment') + +class RepairNoteForm(FlaskForm): + device_id = StringField('Device ID', validators=[DataRequired()]) + repair_notes = TextAreaField('Repair Notes', validators=[DataRequired()]) + submit = SubmitField('Submit Repair Notes') + +class UserForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + email = StringField('Email', validators=[DataRequired(), Email()]) + submit = SubmitField('Create User') + +class RepairBayForm(FlaskForm): + name = StringField('Repair Bay Name', validators=[DataRequired()]) + location = StringField('Location', validators=[DataRequired()]) + submit = SubmitField('Create Repair Bay') \ No newline at end of file diff --git a/app/models.py b/app/models.py index 1e03b14..780ba42 100644 --- a/app/models.py +++ b/app/models.py @@ -1,67 +1,48 @@ -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') +from flask_sqlalchemy import SQLAlchemy +from app import db 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') + email = db.Column(db.String(120), unique=True, nullable=False) + username = db.Column(db.String(80), unique=True, nullable=False) + appointments = db.relationship('Appointment', backref='customer', lazy=True) + repairs = db.relationship('Repair', backref='customer', lazy=True) 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') + username = db.Column(db.String(80), unique=True, nullable=False) + repairs = db.relationship('Repair', backref='technician', lazy=True) -class RepairJob(db.Model): +class Repair(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') + device_info = db.Column(db.String(200), nullable=False) + status = db.Column(db.String(50), nullable=False) + technician_id = db.Column(db.Integer, db.ForeignKey('technician.id'), nullable=True) + customer_id = db.Column(db.Integer, db.ForeignKey('customer.id'), nullable=False) + notes = db.relationship('RepairNote', backref='repair', lazy=True) + +class Appointment(db.Model): + id = db.Column(db.Integer, primary_key=True) + date_time = db.Column(db.DateTime, nullable=False) + customer_id = db.Column(db.Integer, db.ForeignKey('customer.id'), nullable=False) 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) + content = db.Column(db.Text, nullable=False) + repair_id = db.Column(db.Integer, db.ForeignKey('repair.id'), nullable=False) -class RepairPart(db.Model): +class User(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) + username = db.Column(db.String(64), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) -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) + def __repr__(self): + return f'' -class AuditLog(db.Model): +class RepairBay(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 + name = db.Column(db.String(100), unique=True, nullable=False) + location = db.Column(db.String(100), nullable=False) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..0a95b4b --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1,9 @@ +from flask import Blueprint + +customer_bp = Blueprint('customer', __name__) +technician_bp = Blueprint('technician', __name__) +admin_bp = Blueprint('admin', __name__) + +from .customer import * +from .technician import * +from .admin import * \ No newline at end of file diff --git a/app/routes/admin.py b/app/routes/admin.py new file mode 100644 index 0000000..3f7944f --- /dev/null +++ b/app/routes/admin.py @@ -0,0 +1,54 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash +from app.forms import UserForm, RepairBayForm +from app.models import User, RepairBay +from app import db + +admin_bp = Blueprint('admin', __name__) + +@admin_bp.route('/admin/dashboard', methods=['GET', 'POST']) +def dashboard(): + if request.method == 'POST': + # Handle form submission for creating/removing users or repair bays + pass # Implementation will go here + + users = User.query.all() + repair_bays = RepairBay.query.all() + return render_template('admin/dashboard.html', users=users, repair_bays=repair_bays) + +@admin_bp.route('/admin/user/create', methods=['GET', 'POST']) +def create_user(): + form = UserForm() + if form.validate_on_submit(): + new_user = User(username=form.username.data, email=form.email.data) + db.session.add(new_user) + db.session.commit() + flash('User created successfully!', 'success') + return redirect(url_for('admin.dashboard')) + return render_template('admin/create_user.html', form=form) + +@admin_bp.route('/admin/user/delete/', methods=['POST']) +def delete_user(user_id): + user = User.query.get_or_404(user_id) + db.session.delete(user) + db.session.commit() + flash('User deleted successfully!', 'success') + return redirect(url_for('admin.dashboard')) + +@admin_bp.route('/admin/repair_bay/create', methods=['GET', 'POST']) +def create_repair_bay(): + form = RepairBayForm() + if form.validate_on_submit(): + new_bay = RepairBay(name=form.name.data) + db.session.add(new_bay) + db.session.commit() + flash('Repair bay created successfully!', 'success') + return redirect(url_for('admin.dashboard')) + return render_template('admin/create_repair_bay.html', form=form) + +@admin_bp.route('/admin/repair_bay/delete/', methods=['POST']) +def delete_repair_bay(bay_id): + bay = RepairBay.query.get_or_404(bay_id) + db.session.delete(bay) + db.session.commit() + flash('Repair bay deleted successfully!', 'success') + return redirect(url_for('admin.dashboard')) \ No newline at end of file diff --git a/app/routes/audit.py b/app/routes/audit.py deleted file mode 100644 index d63a4b6..0000000 --- a/app/routes/audit.py +++ /dev/null @@ -1,11 +0,0 @@ -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 index 23fc621..5289906 100644 --- a/app/routes/customer.py +++ b/app/routes/customer.py @@ -1,11 +1,34 @@ -from flask import Blueprint, render_template -from flask_login import login_required, current_user -from app.models import Customer +from flask import Blueprint, render_template, request, redirect, url_for, flash +from app.forms import AppointmentForm +from app.models import Appointment, Repair, Customer -customer_bp = Blueprint('customer', __name__, url_prefix='/customers') +customer_bp = Blueprint('customer', __name__) -@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 +@customer_bp.route('/dashboard') +def dashboard(): + # Logic to retrieve customer data, repairs, and appointments + customer_id = 1 # Placeholder for actual customer ID retrieval + repairs = Repair.query.filter_by(customer_id=customer_id).all() + appointments = Appointment.query.filter_by(customer_id=customer_id).all() + customer = Customer.query.filter_by(customer_id=customer_id).all() + return render_template('customer/dashboard.html', repairs=repairs, appointments=appointments, customer=customer) + +@customer_bp.route('/schedule_appointment', methods=['GET', 'POST']) +def schedule_appointment(): + form = AppointmentForm() + if form.validate_on_submit(): + new_appointment = Appointment( + customer_id=form.customer_id.data, + date=form.date.data, + time=form.time.data, + notes=form.notes.data + ) + # Logic to save the appointment to the database + flash('Appointment scheduled successfully!', 'success') + return redirect(url_for('customer.dashboard')) + return render_template('customer/schedule_appointment.html', form=form) + +@customer_bp.route('/track_progress/') +def track_progress(repair_id): + repair = Repair.query.get_or_404(repair_id) + return render_template('customer/track_progress.html', repair=repair) \ No newline at end of file diff --git a/app/routes/repair.py b/app/routes/repair.py deleted file mode 100644 index eb1541d..0000000 --- a/app/routes/repair.py +++ /dev/null @@ -1,38 +0,0 @@ -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/routes/technician.py b/app/routes/technician.py new file mode 100644 index 0000000..64ecda6 --- /dev/null +++ b/app/routes/technician.py @@ -0,0 +1,24 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash +from app.models import Repair, Technician +from app.forms import RepairNoteForm + +technician_bp = Blueprint('technician', __name__) + +@technician_bp.route('/technician/dashboard') +def dashboard(): + repairs = Repair.query.filter_by(status='in_progress').all() + return render_template('technician/dashboard.html', repairs=repairs) + +@technician_bp.route('/technician/repair/', methods=['GET', 'POST']) +def repair_detail(repair_id): + repair = Repair.query.get_or_404(repair_id) + form = RepairNoteForm() + + if form.validate_on_submit(): + repair.notes = form.notes.data + repair.status = 'completed' + # Add logic to save the repair status and notes + flash('Repair notes updated successfully!', 'success') + return redirect(url_for('technician.dashboard')) + + return render_template('technician/repair_detail.html', repair=repair, form=form) \ No newline at end of file diff --git a/app/templates/admin/dashboard.html b/app/templates/admin/dashboard.html new file mode 100644 index 0000000..28b2825 --- /dev/null +++ b/app/templates/admin/dashboard.html @@ -0,0 +1,29 @@ + + + + + + Admin Dashboard + + + + {% include 'base.html' %} +
+

Admin Dashboard

+
+

Manage Repair Bays

+ + +
+
+

User Management

+ + +
+
+

Reports

+ +
+
+ + \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 8bf7aac..b67c7e0 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,26 +1,37 @@ - + - - - {% block title %}Repair Tracker{% endblock %} - - - + + + {% block title %}OpenSTR{% endblock %} + - - {% include 'navbar.html' %} -
+
+

Open Service Tracker Repair Tool

+ +
+
{% with messages = get_flashed_messages() %} - {% if messages %} -
{{ messages[0] }}
- {% endif %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} {% endwith %} {% block content %}{% endblock %} -
- - + +
+

© {{ current_year }} Miracle Computer Consulting

+
+ - \ No newline at end of file diff --git a/app/templates/customer/dashboard.html b/app/templates/customer/dashboard.html new file mode 100644 index 0000000..302db54 --- /dev/null +++ b/app/templates/customer/dashboard.html @@ -0,0 +1,58 @@ + + + + + + Customer Dashboard + + + + {% include 'base.html' %} +
+

Welcome to Your Dashboard, {{ customer.name }}

+

Your Appointments

+ + + + + + + + + + + {% for appointment in appointments %} + + + + + + + {% endfor %} + +
DateTimeServiceStatus
{{ appointment.date }}{{ appointment.time }}{{ appointment.service }}{{ appointment.status }}
+ +

Your Repairs

+ + + + + + + + + + + {% for repair in repairs %} + + + + + + + {% endfor %} + +
DeviceIssueStatusNotes
{{ repair.device }}{{ repair.issue }}{{ repair.status }}{{ repair.notes }}
+
+ + \ No newline at end of file diff --git a/app/templates/technician/dashboard.html b/app/templates/technician/dashboard.html new file mode 100644 index 0000000..20187eb --- /dev/null +++ b/app/templates/technician/dashboard.html @@ -0,0 +1,44 @@ + + + + + + Technician Dashboard + + + + {% include 'base.html' %} +
+

Technician Dashboard

+
+

Repair Status Overview

+
    +
  • Submitted Repairs: {{ submitted_count }}
  • +
  • In Progress Repairs: {{ in_progress_count }}
  • +
  • Completed Repairs: {{ completed_count }}
  • +
+
+
+

Repairs

+ + + + + + + + + + {% for repair in repairs %} + + + + + + {% endfor %} + +
DeviceStatusTechnician Notes
{{ repair.device_name }}{{ repair.status }}{{ repair.notes }}
+
+
+ + \ No newline at end of file diff --git a/app/utils.py b/app/utils.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/utils/barcode.py b/app/utils/barcode.py deleted file mode 100644 index 0568c9f..0000000 --- a/app/utils/barcode.py +++ /dev/null @@ -1,16 +0,0 @@ -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 index af64e97..bd5b1e6 100644 --- a/config.py +++ b/config.py @@ -2,6 +2,20 @@ 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_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///repair_tracker.db') + SQLALCHEMY_DATABASE_URI = 'sqlite:///site.db' SQLALCHEMY_TRACK_MODIFICATIONS = False - UPLOAD_FOLDER = 'app/static/barcodes' \ No newline at end of file + +""" +from pathlib import Path + +class Config: + SECRET_KEY = 'your_secret_key_here' + SQLALCHEMY_DATABASE_URI = 'sqlite:///site.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False + +# Load environment variables if needed +# For example, you can use python-dotenv to load variables from a .env file +# from dotenv import load_dotenv +# load_dotenv() +""" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 211538a..9e385a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -Flask -Flask-Login -Flask-WTF -Flask-SQLAlchemy -Flask-Migrate -psycopg2-binary -Werkzeug -python-barcode -qrcode -Pillow \ No newline at end of file +Flask==2.2.2 +Flask-SQLAlchemy==2.5.1 +Flask-WTF==1.0.0 +Flask-Login==0.6.2 +Flask-Mail==0.9.1 +python-dotenv==0.20.0 +Werkzeug==2.2.2 +requests==2.28.1 +pytest==7.1.2 +pytest-flask==1.2.0 \ No newline at end of file diff --git a/run.py b/run.py index b4ee1bc..031e1d7 100644 --- a/run.py +++ b/run.py @@ -3,4 +3,4 @@ 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 + app.run(host="0.0.0.0", port=5000, debug=True) \ No newline at end of file