added new of dashboard and log apply on routes
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from flask import Flask, redirect, url_for
|
||||
from app.config import Config
|
||||
from app.services.db_service import db
|
||||
from app.services.logger_service import LoggerService
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
@@ -9,6 +10,9 @@ def create_app():
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
|
||||
# Initialize Logger
|
||||
LoggerService.init_app(app)
|
||||
|
||||
# Register blueprints
|
||||
register_blueprints(app)
|
||||
# Register error handlers
|
||||
@@ -43,10 +47,15 @@ def register_blueprints(app):
|
||||
|
||||
|
||||
def register_error_handlers(app):
|
||||
|
||||
from flask import current_app
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
current_app.logger.warning("404 Page Not Found")
|
||||
return "Page Not Found", 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(e):
|
||||
return "Internal Server Error", 500
|
||||
current_app.logger.exception("500 Internal Server Error")
|
||||
return "Internal Server Error", 500
|
||||
11
app/constants/http_status.py
Normal file
11
app/constants/http_status.py
Normal file
@@ -0,0 +1,11 @@
|
||||
class HTTPStatus:
|
||||
OK = 200
|
||||
CREATED = 201
|
||||
BAD_REQUEST = 400
|
||||
UNAUTHORIZED = 401
|
||||
FORBIDDEN = 403
|
||||
NOT_FOUND = 404
|
||||
METHOD_NOT_ALLOWED = 405
|
||||
CONFLICT = 409
|
||||
UNPROCESSABLE_ENTITY = 422
|
||||
INTERNAL_SERVER_ERROR = 500
|
||||
17
app/constants/messages.py
Normal file
17
app/constants/messages.py
Normal file
@@ -0,0 +1,17 @@
|
||||
class SuccessMessage:
|
||||
FETCHED = "Data fetched successfully"
|
||||
CREATED = "Resource created successfully"
|
||||
UPDATED = "Resource updated successfully"
|
||||
DELETED = "Resource deleted successfully"
|
||||
LOGIN = "Login successful"
|
||||
LOGOUT = "Logout successful"
|
||||
|
||||
|
||||
class ErrorMessage:
|
||||
INVALID_REQUEST = "Invalid request data"
|
||||
UNAUTHORIZED = "Unauthorized access"
|
||||
FORBIDDEN = "Access forbidden"
|
||||
NOT_FOUND = "Resource not found"
|
||||
VALIDATION_FAILED = "Validation failed"
|
||||
INTERNAL_ERROR = "Internal server error"
|
||||
DUPLICATE_ENTRY = "Duplicate record found"
|
||||
66
app/errors/error_handlers.py
Normal file
66
app/errors/error_handlers.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from flask import request, render_template
|
||||
from app.utils.response_handler import ResponseHandler
|
||||
from app.utils.exceptions import APIException
|
||||
from app.constants.http_status import HTTPStatus
|
||||
from app.constants.messages import ErrorMessage
|
||||
from app.services.db_service import db
|
||||
import traceback
|
||||
|
||||
|
||||
def register_error_handlers(app):
|
||||
|
||||
# Custom API Exception
|
||||
@app.errorhandler(APIException)
|
||||
def handle_api_exception(e):
|
||||
db.session.rollback()
|
||||
|
||||
if request.path.startswith("/api"):
|
||||
return ResponseHandler.error(
|
||||
message=e.message,
|
||||
errors=e.errors,
|
||||
status_code=e.status_code
|
||||
)
|
||||
|
||||
return render_template("errors/500.html"), e.status_code
|
||||
|
||||
|
||||
# 404
|
||||
@app.errorhandler(404)
|
||||
def handle_404(e):
|
||||
if request.path.startswith("/api"):
|
||||
return ResponseHandler.error(
|
||||
message=ErrorMessage.NOT_FOUND,
|
||||
status_code=HTTPStatus.NOT_FOUND
|
||||
)
|
||||
|
||||
return render_template("errors/404.html"), 404
|
||||
|
||||
|
||||
# 500
|
||||
@app.errorhandler(500)
|
||||
def handle_500(e):
|
||||
db.session.rollback()
|
||||
traceback.print_exc()
|
||||
|
||||
if request.path.startswith("/api"):
|
||||
return ResponseHandler.error(
|
||||
message=ErrorMessage.INTERNAL_ERROR,
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
return render_template("errors/500.html"), 500
|
||||
|
||||
|
||||
# Catch All
|
||||
@app.errorhandler(Exception)
|
||||
def handle_general_exception(e):
|
||||
db.session.rollback()
|
||||
traceback.print_exc()
|
||||
|
||||
if request.path.startswith("/api"):
|
||||
return ResponseHandler.error(
|
||||
message=ErrorMessage.INTERNAL_ERROR,
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
return render_template("errors/500.html"), 500
|
||||
@@ -16,7 +16,11 @@ from app.models.laying_model import Laying
|
||||
dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
|
||||
|
||||
|
||||
|
||||
@dashboard_bp.route("/")
|
||||
def dashboard():
|
||||
if not session.get("user_id"):
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("dashboard.html", title="Business Intelligence Dashboard")
|
||||
|
||||
|
||||
|
||||
@@ -144,11 +148,6 @@ def live_stats():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@dashboard_bp.route("/")
|
||||
def dashboard():
|
||||
if not session.get("user_id"):
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("dashboard.html", title="Business Intelligence Dashboard")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
from flask import Blueprint, render_template, request, redirect, flash
|
||||
from app import db
|
||||
from flask import Blueprint, render_template, request, redirect, flash, current_app, url_for
|
||||
from app.services.db_service import db
|
||||
from app.models.subcontractor_model import Subcontractor
|
||||
from app.utils.helpers import login_required
|
||||
|
||||
|
||||
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
|
||||
|
||||
|
||||
|
||||
# ---------------- ADD -----------------
|
||||
@subcontractor_bp.route("/add")
|
||||
@login_required
|
||||
def add_subcontractor():
|
||||
current_app.logger.info("Opened Add Subcontractor Page")
|
||||
return render_template("subcontractor/add.html")
|
||||
|
||||
|
||||
# ---------------- SAVE -----------------
|
||||
@subcontractor_bp.route("/save", methods=["POST"])
|
||||
@login_required
|
||||
def save_subcontractor():
|
||||
# 1. Get and clean the name from the form
|
||||
|
||||
name = request.form.get("subcontractor_name", "").strip()
|
||||
|
||||
# 2. Basic validation: Ensure the name isn't empty
|
||||
|
||||
if not name:
|
||||
current_app.logger.warning("Empty subcontractor name submitted")
|
||||
flash("Subcontractor name cannot be empty.", "danger")
|
||||
return redirect("/subcontractor/add")
|
||||
|
||||
# 3. Check if a subcontractor with this name already exists
|
||||
return redirect(url_for("subcontractor.add_subcontractor"))
|
||||
|
||||
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
|
||||
|
||||
|
||||
if existing_sub:
|
||||
current_app.logger.warning(f"Duplicate subcontractor attempt: {name}")
|
||||
flash(f"Subcontractor with name '{name}' already exists!", "danger")
|
||||
return redirect("/subcontractor/add")
|
||||
|
||||
# 4. If no duplicate is found, proceed to save
|
||||
return redirect(url_for("subcontractor.add_subcontractor"))
|
||||
|
||||
try:
|
||||
subcontractor = Subcontractor(
|
||||
subcontractor_name=name,
|
||||
@@ -37,155 +41,99 @@ def save_subcontractor():
|
||||
email_id=request.form.get("email_id"),
|
||||
gst_no=request.form.get("gst_no")
|
||||
)
|
||||
|
||||
|
||||
db.session.add(subcontractor)
|
||||
db.session.commit()
|
||||
|
||||
current_app.logger.info(f"Subcontractor Created Successfully: {name}")
|
||||
flash("Subcontractor added successfully!", "success")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
flash("An error occurred while saving. Please try again.", "danger")
|
||||
|
||||
return redirect("/subcontractor/list")
|
||||
|
||||
current_app.logger.exception("Error while saving subcontractor")
|
||||
flash("An error occurred while saving.", "danger")
|
||||
|
||||
return redirect(url_for("subcontractor.subcontractor_list"))
|
||||
|
||||
|
||||
# ---------------- LIST -----------------
|
||||
@subcontractor_bp.route("/list")
|
||||
@login_required
|
||||
def subcontractor_list():
|
||||
subcontractors = Subcontractor.query.all()
|
||||
current_app.logger.info("Viewed Subcontractor List")
|
||||
return render_template("subcontractor/list.html", subcontractors=subcontractors)
|
||||
|
||||
|
||||
|
||||
# ---------------- EDIT -----------------
|
||||
@subcontractor_bp.route("/edit/<int:id>")
|
||||
@login_required
|
||||
def edit_subcontractor(id):
|
||||
subcontractor = Subcontractor.query.get_or_404(id)
|
||||
current_app.logger.info(f"Editing Subcontractor ID: {id}")
|
||||
return render_template("subcontractor/edit.html", subcontractor=subcontractor)
|
||||
|
||||
|
||||
|
||||
# ---------------- UPDATE -----------------
|
||||
@subcontractor_bp.route("/update/<int:id>", methods=["POST"])
|
||||
@login_required
|
||||
def update_subcontractor(id):
|
||||
|
||||
subcontractor = Subcontractor.query.get_or_404(id)
|
||||
new_name = request.form.get("subcontractor_name")
|
||||
|
||||
# Check if the new name is taken by someone ELSE (not this current ID)
|
||||
new_name = request.form.get("subcontractor_name", "").strip()
|
||||
|
||||
duplicate = Subcontractor.query.filter(
|
||||
Subcontractor.subcontractor_name == new_name,
|
||||
Subcontractor.id != id
|
||||
).first()
|
||||
|
||||
if duplicate:
|
||||
flash("Another subcontractor already uses this name.", "danger")
|
||||
return redirect(f"/subcontractor/edit/{id}")
|
||||
|
||||
subcontractor.subcontractor_name = new_name
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flash("Subcontractor updated successfully!", "success")
|
||||
return redirect("/subcontractor/list")
|
||||
|
||||
# ---------------- DELETE -----------------
|
||||
@subcontractor_bp.route("/delete/<int:id>")
|
||||
@login_required
|
||||
def delete_subcontractor(id):
|
||||
subcontractor = Subcontractor.query.get_or_404(id)
|
||||
|
||||
db.session.delete(subcontractor)
|
||||
db.session.commit()
|
||||
|
||||
flash("Subcontractor deleted successfully!", "success")
|
||||
return redirect("/subcontractor/list")
|
||||
from flask import Blueprint, render_template, request, redirect, flash
|
||||
from app import db
|
||||
from app.models.subcontractor_model import Subcontractor
|
||||
from app.utils.helpers import login_required
|
||||
|
||||
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
|
||||
|
||||
# ---------------- ADD -----------------
|
||||
@subcontractor_bp.route("/add")
|
||||
@login_required
|
||||
def add_subcontractor():
|
||||
return render_template("subcontractor/add.html")
|
||||
|
||||
@subcontractor_bp.route("/save", methods=["POST"])
|
||||
@login_required
|
||||
def save_subcontractor():
|
||||
name = request.form.get("subcontractor_name", "").strip()
|
||||
if not name:
|
||||
flash("Subcontractor name cannot be empty.", "danger")
|
||||
return redirect("/subcontractor/add")
|
||||
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
|
||||
|
||||
if existing_sub:
|
||||
flash(f"Subcontractor with name '{name}' already exists!", "danger")
|
||||
return redirect("/subcontractor/add")
|
||||
if duplicate:
|
||||
current_app.logger.warning(f"Duplicate update attempt: {new_name}")
|
||||
flash("Another subcontractor already uses this name.", "danger")
|
||||
return redirect(url_for("subcontractor.edit_subcontractor", id=id))
|
||||
|
||||
try:
|
||||
subcontractor = Subcontractor(
|
||||
subcontractor_name=name,
|
||||
contact_person=request.form.get("contact_person"),
|
||||
mobile_no=request.form.get("mobile_no"),
|
||||
email_id=request.form.get("email_id"),
|
||||
gst_no=request.form.get("gst_no")
|
||||
)
|
||||
|
||||
db.session.add(subcontractor)
|
||||
old_name = subcontractor.subcontractor_name
|
||||
|
||||
subcontractor.subcontractor_name = new_name
|
||||
subcontractor.contact_person = request.form.get("contact_person")
|
||||
subcontractor.mobile_no = request.form.get("mobile_no")
|
||||
subcontractor.email_id = request.form.get("email_id")
|
||||
subcontractor.gst_no = request.form.get("gst_no")
|
||||
|
||||
db.session.commit()
|
||||
flash("Subcontractor added successfully!", "success")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
current_app.logger.info(
|
||||
f"Subcontractor Updated: {old_name} → {new_name}"
|
||||
)
|
||||
|
||||
flash("Subcontractor updated successfully!", "success")
|
||||
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
flash("An error occurred while saving. Please try again.", "danger")
|
||||
|
||||
return redirect("/subcontractor/list")
|
||||
|
||||
# ---------------- LIST -----------------
|
||||
@subcontractor_bp.route("/list")
|
||||
@login_required
|
||||
def subcontractor_list():
|
||||
subcontractors = Subcontractor.query.all()
|
||||
return render_template("subcontractor/list.html", subcontractors=subcontractors)
|
||||
|
||||
# ---------------- EDIT -----------------
|
||||
@subcontractor_bp.route("/edit/<int:id>")
|
||||
@login_required
|
||||
def edit_subcontractor(id):
|
||||
subcontractor = Subcontractor.query.get_or_404(id)
|
||||
return render_template("subcontractor/edit.html", subcontractor=subcontractor)
|
||||
|
||||
# ---------------- UPDATE -----------------
|
||||
@subcontractor_bp.route("/update/<int:id>", methods=["POST"])
|
||||
@login_required
|
||||
def update_subcontractor(id):
|
||||
subcontractor = Subcontractor.query.get_or_404(id)
|
||||
new_name = request.form.get("subcontractor_name")
|
||||
|
||||
# Check if the new name is taken by someone ELSE (not this current ID)
|
||||
duplicate = Subcontractor.query.filter(
|
||||
Subcontractor.subcontractor_name == new_name,
|
||||
Subcontractor.id != id
|
||||
).first()
|
||||
|
||||
if duplicate:
|
||||
flash("Another subcontractor already uses this name.", "danger")
|
||||
return redirect(f"/subcontractor/edit/{id}")
|
||||
|
||||
subcontractor.subcontractor_name = new_name
|
||||
db.session.commit()
|
||||
|
||||
flash("Subcontractor updated successfully!", "success")
|
||||
return redirect("/subcontractor/list")
|
||||
|
||||
current_app.logger.exception("Error updating subcontractor")
|
||||
flash("Update failed!", "danger")
|
||||
|
||||
return redirect(url_for("subcontractor.subcontractor_list"))
|
||||
|
||||
|
||||
# ---------------- DELETE -----------------
|
||||
@subcontractor_bp.route("/delete/<int:id>")
|
||||
@login_required
|
||||
def delete_subcontractor(id):
|
||||
subcontractor = Subcontractor.query.get_or_404(id)
|
||||
try:
|
||||
name = subcontractor.subcontractor_name
|
||||
|
||||
db.session.delete(subcontractor)
|
||||
db.session.commit()
|
||||
db.session.delete(subcontractor)
|
||||
db.session.commit()
|
||||
|
||||
flash("Subcontractor deleted successfully!", "success")
|
||||
return redirect("/subcontractor/list")
|
||||
current_app.logger.info(f"Subcontractor Deleted: {name}")
|
||||
flash("Subcontractor deleted successfully!", "success")
|
||||
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
current_app.logger.exception("Error deleting subcontractor")
|
||||
flash("Delete failed!", "danger")
|
||||
|
||||
return redirect(url_for("subcontractor.subcontractor_list"))
|
||||
@@ -1,11 +1,13 @@
|
||||
from flask import Blueprint, render_template
|
||||
from app.services.user_service import UserService
|
||||
from app.utils.helpers import login_required
|
||||
from flask import current_app
|
||||
|
||||
user_bp = Blueprint("user", __name__, url_prefix="/user")
|
||||
|
||||
@user_bp.route("/list")
|
||||
@login_required
|
||||
def list_users():
|
||||
current_app.logger.info("User list viewed")
|
||||
users = UserService.get_all_users()
|
||||
return render_template("users.html", users=users, title="Users")
|
||||
return render_template("users.html", users=users, title="Users")
|
||||
0
app/services/comparison_service.py
Normal file
0
app/services/comparison_service.py
Normal file
82
app/services/logger_service.py
Normal file
82
app/services/logger_service.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import os
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from flask import request, session
|
||||
|
||||
|
||||
class RequestFormatter(logging.Formatter):
|
||||
"""
|
||||
Custom formatter to safely inject request data
|
||||
"""
|
||||
|
||||
def format(self, record):
|
||||
record.user = getattr(record, "user", "Anonymous")
|
||||
record.ip = getattr(record, "ip", "N/A")
|
||||
record.method = getattr(record, "method", "N/A")
|
||||
record.url = getattr(record, "url", "N/A")
|
||||
return super().format(record)
|
||||
|
||||
|
||||
class LoggerService:
|
||||
|
||||
@staticmethod
|
||||
def init_app(app):
|
||||
|
||||
# Create logs folder if not exists
|
||||
if not os.path.exists("logs"):
|
||||
os.makedirs("logs")
|
||||
|
||||
formatter = RequestFormatter(
|
||||
"%(asctime)s | %(levelname)s | "
|
||||
"User:%(user)s | IP:%(ip)s | "
|
||||
"Method:%(method)s | URL:%(url)s | "
|
||||
"%(message)s"
|
||||
)
|
||||
|
||||
# 🔹 INFO LOG
|
||||
info_handler = RotatingFileHandler(
|
||||
"logs/app.log",
|
||||
maxBytes=5 * 1024 * 1024,
|
||||
backupCount=5
|
||||
)
|
||||
info_handler.setLevel(logging.INFO)
|
||||
info_handler.setFormatter(formatter)
|
||||
|
||||
# 🔹 ERROR LOG
|
||||
error_handler = RotatingFileHandler(
|
||||
"logs/error.log",
|
||||
maxBytes=5 * 1024 * 1024,
|
||||
backupCount=5
|
||||
)
|
||||
error_handler.setLevel(logging.ERROR)
|
||||
error_handler.setFormatter(formatter)
|
||||
|
||||
# 🔹 CONSOLE LOG
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
app.logger.setLevel(logging.DEBUG)
|
||||
app.logger.addHandler(info_handler)
|
||||
app.logger.addHandler(error_handler)
|
||||
app.logger.addHandler(console_handler)
|
||||
|
||||
# Auto request logging
|
||||
@app.before_request
|
||||
def log_request():
|
||||
app.logger.info(
|
||||
"Request Started",
|
||||
extra=LoggerService.get_request_data()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_request_data():
|
||||
try:
|
||||
return {
|
||||
"user": session.get("user_email", "Anonymous"),
|
||||
"ip": request.remote_addr,
|
||||
"method": request.method,
|
||||
"url": request.url
|
||||
}
|
||||
except:
|
||||
return {}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-2 px-md-4">
|
||||
<h4 class="mb-3 text-center text-md-start">Comparison dSoftware Solapur (UGD) - Live Dashboard</h4>
|
||||
<h4 class="mb-3 text-center text-md-start">Comparison Software Solapur (UGD) - Live Dashboard</h4>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
|
||||
@@ -2,39 +2,39 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
|
||||
<h4 class="mb-3">Add New Subcontractor</h4>
|
||||
|
||||
<form action="/subcontractor/save" method="POST">
|
||||
<form action="{{ url_for('subcontractor.save_subcontractor') }}" method="POST">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Subcontractor Name</label>
|
||||
<label class="form-label">Subcontractor Name:</label>
|
||||
<input type="text" class="form-control" name="subcontractor_name" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contact Person Name</label>
|
||||
<label class="form-label">Contact Person Name:</label>
|
||||
<input type="text" class="form-control" name="contact_person">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mobile</label>
|
||||
<label class="form-label">Mobile No:</label>
|
||||
<input type="text" class="form-control" name="mobile_no">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<label class="form-label">Email:</label>
|
||||
<input type="email" class="form-control" name="email_id">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">GST No</label>
|
||||
<label class="form-label">GST No:</label>
|
||||
<input type="text" class="form-control" name="gst_no">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success">Save</button>
|
||||
</form>
|
||||
<a href="{{ url_for('subcontractor.subcontractor_list') }}" class="btn btn-secondary">Back</a>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3">Edit Subcontractor</h4>
|
||||
|
||||
<form action="/subcontractor/update/{{ subcontractor.id }}" method="POST">
|
||||
<form action="{{ url_for('subcontractor.update_subcontractor', id=subcontractor.id) }}" method="POST">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Subcontractor Name</label>
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success">Update</button>
|
||||
<a href="/subcontractor/list" class="btn btn-secondary">Back</a>
|
||||
<a href="{{ url_for('subcontractor.subcontractor_list') }}" class="btn btn-secondary">Back</a>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3 text-center text-md-end">
|
||||
<a href="/subcontractor/add" class="btn btn-success w-100 w-md-auto">
|
||||
➕ Add Subcontractor
|
||||
<!-- <a href="/subcontractor/add" class="btn btn-success w-100 w-md-auto"> -->
|
||||
<a href="{{ url_for('subcontractor.add_subcontractor') }}" class="btn btn-success"></a>
|
||||
➕ Add Subcontractor
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,11 +57,15 @@
|
||||
<!-- Action Buttons -->
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<a href="/subcontractor/edit/{{ s.id }}" class="btn btn-sm btn-warning">
|
||||
Edit
|
||||
<!-- <a href="/subcontractor/edit/{{ s.id }}" class="btn btn-sm btn-warning"> -->
|
||||
<a href="{{ url_for('subcontractor.edit_subcontractor', id=s.id) }}"
|
||||
class="btn btn-warning btn-sm"></a>
|
||||
Edit
|
||||
</a>
|
||||
<a href="/subcontractor/delete/{{ s.id }}" class="btn btn-sm btn-danger"
|
||||
onclick="return confirm('Are you sure?')">
|
||||
<!-- <a href="/subcontractor/delete/{{ s.id }}" class="btn btn-sm btn-danger"
|
||||
onclick="return confirm('Are you sure?')"> -->
|
||||
<a href="{{ url_for('subcontractor.delete_subcontractor', id=s.id) }}"
|
||||
class="btn btn-danger btn-sm" onclick="return confirm('Are you sure?')">
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -6,24 +6,117 @@
|
||||
|
||||
<h4 class="mb-3 text-center text-md-start">Subcontractor Dashboard </h4>
|
||||
|
||||
<!-- Charts -->
|
||||
<div class="row g-3">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card text-white bg-primary shadow h-100">
|
||||
<div class="card-body text-center text-md-start">
|
||||
<h6>Trenching Units</h6>
|
||||
<h3 class="fw-bold" id="card-trench">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card text-white bg-success shadow h-100">
|
||||
<div class="card-body text-center text-md-start">
|
||||
<h6>Manhole Units</h6>
|
||||
<h3 class="fw-bold" id="card-manhole">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card text-dark bg-warning shadow h-100">
|
||||
<div class="card-body text-center text-md-start">
|
||||
<h6>Laying Units</h6>
|
||||
<h3 class="fw-bold" id="card-laying">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bar Chart -->
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-dark text-white text-center text-md-start">
|
||||
Work Category Bar Chart
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<img src="data:image/png;base64,{{ bar_chart }}" class="img-fluid" style="max-height:300px;">
|
||||
<div class="card-header bg-dark text-white">Live Category Bar Chart</div>
|
||||
<div class="card-body">
|
||||
<canvas id="liveBarChart" style="max-height:300px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-dark text-white">Location Distribution Pie Chart</div>
|
||||
<div class="card-body">
|
||||
<canvas id="livePieChart" style="max-height:300px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<script>
|
||||
// 2. Initialize the Bar Chart
|
||||
const barCtx = document.getElementById('liveBarChart').getContext('2d');
|
||||
let liveBarChart = new Chart(barCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Trenching', 'Manholes', 'Laying'],
|
||||
datasets: [{
|
||||
label: 'Units Completed',
|
||||
data: [0, 0, 0],
|
||||
backgroundColor: ['#0d6efd', '#198754', '#ffc107']
|
||||
}]
|
||||
},
|
||||
options: { responsive: true, maintainAspectRatio: false }
|
||||
});
|
||||
|
||||
// 3. Initialize the Pie Chart
|
||||
const pieCtx = document.getElementById('livePieChart').getContext('2d');
|
||||
let livePieChart = new Chart(pieCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: [], // Will be filled from SQL
|
||||
datasets: [{
|
||||
data: [],
|
||||
backgroundColor: ['#0d6efd', '#198754', '#ffc107', '#6f42c1', '#fd7e14']
|
||||
}]
|
||||
},
|
||||
options: { responsive: true, maintainAspectRatio: false }
|
||||
});
|
||||
|
||||
// 4. Function to Fetch Live Data from your Python API
|
||||
function fetchLiveData() {
|
||||
fetch('/dashboard/api/live-stats') // This matches the route we created in the "Kitchen"
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Update the Summary Cards
|
||||
document.getElementById('card-trench').innerText = data.summary.trench;
|
||||
document.getElementById('card-manhole').innerText = data.summary.manhole;
|
||||
document.getElementById('card-laying').innerText = data.summary.laying;
|
||||
|
||||
// Update Bar Chart
|
||||
liveBarChart.data.datasets[0].data = [
|
||||
data.summary.trench,
|
||||
data.summary.manhole,
|
||||
data.summary.laying
|
||||
];
|
||||
liveBarChart.update();
|
||||
|
||||
// Update Pie Chart (Location stats)
|
||||
livePieChart.data.labels = Object.keys(data.locations);
|
||||
livePieChart.data.datasets[0].data = Object.values(data.locations);
|
||||
livePieChart.update();
|
||||
})
|
||||
.catch(err => console.error("Error fetching live data:", err));
|
||||
}
|
||||
|
||||
// 5. Check for updates every 10 seconds (Real-time effect)
|
||||
setInterval(fetchLiveData, 10000);
|
||||
fetchLiveData(); // Load immediately on page open
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
8
app/utils/exceptions.py
Normal file
8
app/utils/exceptions.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from app.constants.http_status import HTTPStatus
|
||||
|
||||
class APIException(Exception):
|
||||
def __init__(self, message, status_code=HTTPStatus.BAD_REQUEST, errors=None):
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
self.errors = errors
|
||||
super().__init__(self.message)
|
||||
23
app/utils/response_handler.py
Normal file
23
app/utils/response_handler.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from flask import jsonify
|
||||
from app.constants.http_status import HTTPStatus
|
||||
|
||||
|
||||
class ResponseHandler:
|
||||
|
||||
@staticmethod
|
||||
def success(message, data=None, status_code=HTTPStatus.OK):
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": message,
|
||||
"data": data if data else {},
|
||||
"errors": []
|
||||
}), status_code
|
||||
|
||||
@staticmethod
|
||||
def error(message, errors=None, status_code=HTTPStatus.BAD_REQUEST):
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": message,
|
||||
"data": {},
|
||||
"errors": errors if errors else []
|
||||
}), status_code
|
||||
Reference in New Issue
Block a user