diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8a75f36 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +.gitignore +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.egg-info/ +dist/ +build/ +.vscode/ +.idea/ +*.log +.env +instance/ +.pytest_cache/ +.coverage diff --git a/.env b/.env index c4c74c7..2477ef9 100644 --- a/.env +++ b/.env @@ -3,8 +3,8 @@ # ----------------------------- FLASK_ENV=development FLASK_DEBUG=True -FLASK_HOST='0.0.0.0' -FLASK_PORT=5001 +FLASK_HOST=0.0.0.0 +FLASK_PORT=5011 # ----------------------------- # Security diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..75cb21a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create necessary directories +RUN mkdir -p app/logs app/static/uploads app/static/downloads + +# Expose port +EXPOSE 5001 + +# Run the application +CMD ["python", "run.py"] diff --git a/app/__init__.py b/app/__init__.py index c236761..52606e3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 @@ -32,7 +36,7 @@ def register_blueprints(app): from app.routes.generate_comparison_report import generate_report_bp from app.routes.file_format import file_format_bp - app.register_blueprint(auth_bp) + app.register_blueprint(auth_bp) app.register_blueprint(user_bp) app.register_blueprint(dashboard_bp) app.register_blueprint(subcontractor_bp) @@ -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 \ No newline at end of file diff --git a/app/config.py b/app/config.py index 2ca03b2..ece5bed 100644 --- a/app/config.py +++ b/app/config.py @@ -6,7 +6,7 @@ class Config: # secret key SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key") - # Database varibles + # Database variables DB_DIALECT = os.getenv("DB_DIALECT") DB_DRIVER = os.getenv("DB_DRIVER") DB_USER = os.getenv("DB_USER") diff --git a/app/constants/http_status.py b/app/constants/http_status.py new file mode 100644 index 0000000..9859f7d --- /dev/null +++ b/app/constants/http_status.py @@ -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 \ No newline at end of file diff --git a/app/constants/messages.py b/app/constants/messages.py new file mode 100644 index 0000000..ada80e3 --- /dev/null +++ b/app/constants/messages.py @@ -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" diff --git a/app/errors/error_handlers.py b/app/errors/error_handlers.py new file mode 100644 index 0000000..aebb5b0 --- /dev/null +++ b/app/errors/error_handlers.py @@ -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 \ No newline at end of file diff --git a/app/models/laying_client_model.py b/app/models/laying_client_model.py index 4152165..bb9ef3d 100644 --- a/app/models/laying_client_model.py +++ b/app/models/laying_client_model.py @@ -1,20 +1,27 @@ from app import db from datetime import datetime +from sqlalchemy import event +import re +# REGEX PATTERN +PIPE_MM_PATTERN = re.compile(r"^pipe_\d+_mm$") class LayingClient(db.Model): __tablename__ = "laying_client" id = db.Column(db.Integer, primary_key=True) - # Foreign Key to Subcontractor tables - # subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False) - # Relationship for easy access (subcontractor.subcontractor_name) - # subcontractor = db.relationship("Subcontractor", backref="laying_records") # Basic Fields Location = db.Column(db.String(500)) MH_NO = db.Column(db.String(100)) CC_length = db.Column(db.Float) + # Bedding Qty. + Outer_dia_of_MH_m = db.Column(db.Float) + Bedding_Length = db.Column(db.Float) + Width = db.Column(db.Float) + Depth = db.Column(db.Float) + Qty = db.Column(db.Float) + # PIPE LAYING Qty. Pipe_Dia_mm = db.Column(db.Float) ID_of_MH_m = db.Column(db.Float) Laying_Length = db.Column(db.Float) @@ -32,7 +39,6 @@ class LayingClient(db.Model): pipe_900_mm = db.Column(db.Float) pipe_1200_mm = db.Column(db.Float) - Total = db.Column(db.Float) Remarks = db.Column(db.String(500)) RA_Bill_No=db.Column(db.String(500)) @@ -45,3 +51,27 @@ class LayingClient(db.Model): def serialize(self): return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + + def sum_laying_fields(): + return [ + "pipe_150_mm", "pipe_200_mm", "pipe_250_mm", + "pipe_300_mm", "pipe_350_mm", "pipe_400_mm", + "pipe_450_mm", "pipe_500_mm", "pipe_600_mm", + "pipe_700_mm", "pipe_900_mm", "pipe_1200_mm" + ] + +# =============================== +# AUTO TOTAL USING REGEX +# =============================== +def calculate_laying_total(mapper, connection, target): + total = 0 + + for column in target.__table__.columns: + if PIPE_MM_PATTERN.match(column.name): + total += getattr(target, column.name) or 0 + + target.Total = total + +event.listen(LayingClient, "before_insert", calculate_laying_total) +event.listen(LayingClient, "before_update", calculate_laying_total) \ No newline at end of file diff --git a/app/models/laying_model.py b/app/models/laying_model.py index b8510ff..f193e64 100644 --- a/app/models/laying_model.py +++ b/app/models/laying_model.py @@ -1,5 +1,9 @@ from app import db from datetime import datetime +from sqlalchemy import event +import re +# REGEX PATTERN +PIPE_MM_PATTERN = re.compile(r"^pipe_\d+_mm$") class Laying(db.Model): __tablename__ = "laying" @@ -10,11 +14,10 @@ class Laying(db.Model): # Relationship for easy access (subcontractor.subcontractor_name) subcontractor = db.relationship("Subcontractor", backref="laying_records") - # Basic Fields + # Pipe Laying Fields Location = db.Column(db.String(500)) MH_NO = db.Column(db.String(100)) CC_length = db.Column(db.Float) - Pipe_Dia_mm = db.Column(db.Float) ID_of_MH_m = db.Column(db.Float) Laying_Length = db.Column(db.Float) @@ -32,7 +35,6 @@ class Laying(db.Model): pipe_900_mm = db.Column(db.Float) pipe_1200_mm = db.Column(db.Float) - Total = db.Column(db.Float) Remarks = db.Column(db.String(500)) RA_Bill_No=db.Column(db.String(500)) @@ -45,3 +47,27 @@ class Laying(db.Model): def serialize(self): return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + + def sum_laying_fields(): + return [ + "pipe_150_mm", "pipe_200_mm", "pipe_250_mm", + "pipe_300_mm", "pipe_350_mm", "pipe_400_mm", + "pipe_450_mm", "pipe_500_mm", "pipe_600_mm", + "pipe_700_mm", "pipe_900_mm", "pipe_1200_mm" + ] + +# =============================== +# AUTO TOTAL USING REGEX +# =============================== +def calculate_laying_total(mapper, connection, target): + total = 0 + + for column in target.__table__.columns: + if PIPE_MM_PATTERN.match(column.name): + total += getattr(target, column.name) or 0 + + target.Total = total + +event.listen(Laying, "before_insert", calculate_laying_total) +event.listen(Laying, "before_update", calculate_laying_total) \ No newline at end of file diff --git a/app/models/manhole_domestic_chamber_model.py b/app/models/manhole_domestic_chamber_model.py index 0902f0d..348e2a7 100644 --- a/app/models/manhole_domestic_chamber_model.py +++ b/app/models/manhole_domestic_chamber_model.py @@ -1,5 +1,10 @@ from app import db from datetime import datetime +from sqlalchemy import event +import re + +# REGEX PATTERN +D_RANGE_PATTERN = re.compile(r"^d_\d+(?:_\d+)?_to_\d+(?:_\d+)?$") class ManholeDomesticChamber(db.Model): __tablename__ = "manhole_domestic_chamber" @@ -27,7 +32,6 @@ class ManholeDomesticChamber(db.Model): d_4_16_to_4_65 = db.Column(db.Float) d_4_66_to_5_15 = db.Column(db.Float) d_5_16_to_5_65 = db.Column(db.Float) - d_5_66_to_6_15 = db.Column(db.Float) d_6_16_to_6_65 = db.Column(db.Float) d_6_66_to_7_15 = db.Column(db.Float) @@ -42,6 +46,7 @@ class ManholeDomesticChamber(db.Model): UPVC_Pipe_Length = db.Column(db.Float) RA_Bill_No=db.Column(db.String(500)) + Total = db.Column(db.Float) created_at = db.Column(db.DateTime, default=datetime.today) def __repr__(self): @@ -49,4 +54,30 @@ class ManholeDomesticChamber(db.Model): def serialize(self): return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + + def sum_mh_dc_fields(): + return [ + "d_0_to_0_75", "d_0_76_to_1_05", "d_1_06_to_1_65", + "d_1_66_to_2_15", "d_2_16_to_2_65", "d_2_66_to_3_15", + "d_3_16_to_3_65", "d_3_66_to_4_15", "d_4_16_to_4_65", + "d_4_66_to_5_15", "d_5_16_to_5_65", "d_5_66_to_6_15", + "d_6_16_to_6_65", "d_6_66_to_7_15", "d_7_16_to_7_65", + "d_7_66_to_8_15", "d_8_16_to_8_65", "d_8_66_to_9_15", + "d_9_16_to_9_65"] +# =============================== +# AUTO TOTAL USING REGEX +# =============================== +def calculate_mh_dc_total(mapper, connection, target): + total = 0 + + for column in target.__table__.columns: + if D_RANGE_PATTERN.match(column.name): + total += getattr(target, column.name) or 0 + + target.Total = total + + +event.listen(ManholeDomesticChamber, "before_insert", calculate_mh_dc_total) +event.listen(ManholeDomesticChamber, "before_update", calculate_mh_dc_total) \ No newline at end of file diff --git a/app/models/manhole_excavation_model.py b/app/models/manhole_excavation_model.py index 51e773c..db99751 100644 --- a/app/models/manhole_excavation_model.py +++ b/app/models/manhole_excavation_model.py @@ -1,5 +1,6 @@ from app import db from datetime import datetime +from sqlalchemy import event class ManholeExcavation(db.Model): __tablename__ = "manhole_excavation" @@ -65,3 +66,17 @@ class ManholeExcavation(db.Model): def serialize(self): return {c.name: getattr(self, c.name) for c in self.__table__.columns} + +# ========================================== +# AUTO CALCULATE GRAND TOTAL +# ========================================== +def calculate_Manhole_total(mapper, connection, target): + total = 0 + for column in target.__table__.columns: + if column.name.endswith("_total"): + total += getattr(target, column.name) or 0 + target.Total = total + + +event.listen(ManholeExcavation, "before_insert", calculate_Manhole_total) +event.listen(ManholeExcavation, "before_update", calculate_Manhole_total) \ No newline at end of file diff --git a/app/models/mh_dc_client_model.py b/app/models/mh_dc_client_model.py index 6ea30bc..8ed661f 100644 --- a/app/models/mh_dc_client_model.py +++ b/app/models/mh_dc_client_model.py @@ -1,5 +1,10 @@ from app import db from datetime import datetime +from sqlalchemy import event +import re + +# REGEX PATTERN +D_RANGE_PATTERN = re.compile(r"^d_\d+(?:_\d+)?_to_\d+(?:_\d+)?$") class ManholeDomesticChamberClient(db.Model): __tablename__ = "mh_dc_client" @@ -24,18 +29,17 @@ class ManholeDomesticChamberClient(db.Model): d_1_5_to_2_0 = db.Column(db.Float) d_2_0_to_2_5 = db.Column(db.Float) d_2_5_to_3_0 = db.Column(db.Float) - d_3_0_to_3_5 = db.Column(db.Float) d_3_5_to_4_0 = db.Column(db.Float) d_4_0_to_4_5= db.Column(db.Float) d_4_5_to_5_0 = db.Column(db.Float) - d_5_0_to_5_5 = db.Column(db.Float) d_5_5_to_6_0 = db.Column(db.Float) d_6_0_to_6_5 = db.Column(db.Float) Domestic_Chambers = db.Column(db.Float) + Total = db.Column(db.Float) created_at = db.Column(db.DateTime, default=datetime.today) def __repr__(self): @@ -44,3 +48,30 @@ class ManholeDomesticChamberClient(db.Model): def serialize(self): return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + + def sum_mh_dc_fields(): + return [ + "d_0_to_0_75", "d_0_76_to_1_05", "d_1_06_to_1_65", + "d_1_66_to_2_15", "d_2_16_to_2_65", "d_2_66_to_3_15", + "d_3_16_to_3_65", "d_3_66_to_4_15", "d_4_16_to_4_65", + "d_4_66_to_5_15", "d_5_16_to_5_65", "d_5_66_to_6_15", + "d_6_16_to_6_65", "d_6_66_to_7_15", "d_7_16_to_7_65", + "d_7_66_to_8_15", "d_8_16_to_8_65", "d_8_66_to_9_15", + "d_9_16_to_9_65" ] + +# =============================== +# AUTO TOTAL USING REGEX +# =============================== +def calculate_mh_dc_total(mapper, connection, target): + total = 0 + + for column in target.__table__.columns: + if D_RANGE_PATTERN.match(column.name): + total += getattr(target, column.name) or 0 + + target.Total = total + + +event.listen(ManholeDomesticChamberClient, "before_insert", calculate_mh_dc_total) +event.listen(ManholeDomesticChamberClient, "before_update", calculate_mh_dc_total) \ No newline at end of file diff --git a/app/models/mh_ex_client_model.py b/app/models/mh_ex_client_model.py index c4fd6c8..ba8adb9 100644 --- a/app/models/mh_ex_client_model.py +++ b/app/models/mh_ex_client_model.py @@ -1,5 +1,6 @@ from app import db from datetime import datetime +from sqlalchemy import event class ManholeExcavationClient(db.Model): __tablename__ = "mh_ex_client" @@ -81,3 +82,18 @@ class ManholeExcavationClient(db.Model): def serialize(self): return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +# ========================================== +# AUTO CALCULATE GRAND TOTAL +# ========================================== +def calculate_Manhole_total(mapper, connection, target): + total = 0 + for column in target.__table__.columns: + if column.name.endswith("_total"): + total += getattr(target, column.name) or 0 + target.Total = total + + +event.listen(ManholeExcavationClient, "before_insert", calculate_Manhole_total) +event.listen(ManholeExcavationClient, "before_update", calculate_Manhole_total) \ No newline at end of file diff --git a/app/models/tr_ex_client_model.py b/app/models/tr_ex_client_model.py index 1214de0..c004765 100644 --- a/app/models/tr_ex_client_model.py +++ b/app/models/tr_ex_client_model.py @@ -1,14 +1,11 @@ from app import db from datetime import datetime +from sqlalchemy import event class TrenchExcavationClient(db.Model): __tablename__ = "tr_ex_client" id = db.Column(db.Integer, primary_key=True) - # Foreign Key to Subcontractor table - # subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False) - # Relationship for easy access (subcontractor.subcontractor_name) - # subcontractor = db.relationship("Subcontractor", backref="tr_ex_records") # Basic Fields RA_Bill_No=db.Column(db.String(500)) @@ -86,3 +83,19 @@ class TrenchExcavationClient(db.Model): def serialize(self): return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +# ========================================== +# AUTO CALCULATE GRAND TOTAL +# ========================================== + +def calculate_trench_client_total(mapper, connection, target): + total = 0 + for column in target.__table__.columns: + if column.name.endswith("_total"): + total += getattr(target, column.name) or 0 + target.Total = total + + +event.listen(TrenchExcavationClient, "before_insert", calculate_trench_client_total) +event.listen(TrenchExcavationClient, "before_update", calculate_trench_client_total) \ No newline at end of file diff --git a/app/models/trench_excavation_model.py b/app/models/trench_excavation_model.py index 446bf55..2323a55 100644 --- a/app/models/trench_excavation_model.py +++ b/app/models/trench_excavation_model.py @@ -1,5 +1,6 @@ from app import db from datetime import datetime +from sqlalchemy import event class TrenchExcavation(db.Model): __tablename__ = "trench_excavation" @@ -77,3 +78,48 @@ class TrenchExcavation(db.Model): def serialize(self): return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + def excavation_category_sums(self): + + def safe(val): + return val or 0 + + return { + "Soft_Murum_Total": ( + safe(self.Soft_Murum_0_to_1_5) + + safe(self.Soft_Murum_1_5_to_3_0) + + safe(self.Soft_Murum_3_0_to_4_5) + ), + + "Hard_Murum_Total": ( + safe(self.Hard_Murum_0_to_1_5) + + safe(self.Hard_Murum_1_5_to_3_0) + ), + + "Soft_Rock_Total": ( + safe(self.Soft_Rock_0_to_1_5) + + safe(self.Soft_Rock_1_5_to_3_0) + ), + + "Hard_Rock_Total": ( + safe(self.Hard_Rock_0_to_1_5) + + safe(self.Hard_Rock_1_5_to_3_0) + + safe(self.Hard_Rock_3_0_to_4_5) + + safe(self.Hard_Rock_4_5_to_6_0) + + safe(self.Hard_Rock_6_0_to_7_5) + ), + } + +# ========================================== +# AUTO CALCULATE GRAND TOTAL +# ========================================== +def calculate_trench_total(mapper, connection, target): + total = 0 + for column in target.__table__.columns: + if column.name.endswith("_total"): + total += getattr(target, column.name) or 0 + target.Total = total + + +event.listen(TrenchExcavation, "before_insert", calculate_trench_total) +event.listen(TrenchExcavation, "before_update", calculate_trench_total) \ No newline at end of file diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index 3aebc4c..2190316 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -1,68 +1,167 @@ import matplotlib matplotlib.use("Agg") -from flask import Blueprint, render_template, session, redirect, url_for +from flask import Blueprint, render_template, session, redirect, url_for, jsonify import matplotlib.pyplot as plt import io import base64 +from app.utils.plot_utils import plot_to_base64 +from app.services.dashboard_service import DashboardService +from sqlalchemy import func +from app import db +from app.models.trench_excavation_model import TrenchExcavation +from app.models.manhole_excavation_model import ManholeExcavation +from app.models.laying_model import Laying dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard") -# charts -def plot_to_base64(): - img = io.BytesIO() - plt.savefig(img, format="png", bbox_inches="tight") - plt.close() - img.seek(0) - return base64.b64encode(img.getvalue()).decode() -# bar chart -def bar_chart(): - categories = ["Trench", "Manhole", "Pipe Laying", "Restoration"] - values = [120, 80, 150, 60] - - plt.figure() - plt.bar(categories, values) - plt.title("Work Category Report") - plt.xlabel("Category") - plt.ylabel("Count") - - - return plot_to_base64() - -# Pie chart -def pie_chart(): - labels = ["Completed", "In Progress", "Pending"] - sizes = [55, 20, 25] - - plt.figure() - plt.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=140) - plt.title("Project Status") - - return plot_to_base64() - -# Histogram chart -def histogram_chart(): - daily_work = [5, 10, 15, 20, 20, 25, 30, 35, 40, 45, 50] - - plt.figure() - plt.hist(daily_work, bins=5) - plt.title("Daily Work Distribution") - plt.xlabel("Work Units") - plt.ylabel("Frequency") - - return plot_to_base64() - -# Dashboaed page @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") + + + +# import matplotlib +# matplotlib.use("Agg") + +# from flask import Blueprint, render_template, session, redirect, url_for +# import matplotlib.pyplot as plt +# import io +# import base64 +# from app.utils.plot_utils import plot_to_base64 +# from app.services.dashboard_service import DashboardService + +# dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard") + +# # dashboard_bp = Blueprint("dashboard", __name__) + +# # charts +# # def plot_to_base64(): +# # img = io.BytesIO() +# # plt.savefig(img, format="png", bbox_inches="tight") +# # plt.close() +# # img.seek(0) +# # return base64.b64encode(img.getvalue()).decode() + +# # bar chart +# def bar_chart(): +# categories = ["Trench", "Manhole", "Pipe Laying", "Restoration"] +# values = [120, 80, 150, 60] + +# plt.figure() +# plt.bar(categories, values) +# plt.title("Work Category Report") +# plt.xlabel("test Category") +# plt.ylabel("test Quantity") + + +# return plot_to_base64(plt) + +# # Pie chart +# def pie_chart(): +# labels = ["Completed", "In Progress", "Pending"] +# sizes = [55, 20, 25] + +# plt.figure() +# plt.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=140) +# plt.title("Project Status") + +# return plot_to_base64(plt) + +# # Histogram chart +# def histogram_chart(): +# daily_work = [5, 10, 15, 20, 20, 25, 30, 35, 40, 45, 50] + +# plt.figure() +# plt.hist(daily_work, bins=5) +# plt.title("Daily Work Distribution") +# plt.xlabel("Work Units") +# plt.ylabel("Frequency") + +# return plot_to_base64(plt) + +# # Dashboaed page +# @dashboard_bp.route("/") +# def dashboard(): +# if not session.get("user_id"): +# return redirect(url_for("auth.login")) + +# return render_template( +# "dashboard.html", +# title="Dashboard", +# bar_chart=bar_chart(), +# pie_chart=pie_chart(), +# histogram=histogram_chart() +# ) + +# # subcontractor dashboard +# @dashboard_bp.route("/subcontractor_dashboard", methods=["GET", "POST"]) +# def subcontractor_dashboard(): +# if not session.get("user_id"): +# return redirect(url_for("auth.login")) + +# tr_dash = DashboardService().bar_chart_of_tr_ex + + +# return render_template( +# "subcontractor_dashboard.html", +# title="Dashboard", +# bar_chart=tr_dash +# ) + + +@dashboard_bp.route("/api/live-stats") +def live_stats(): + try: + # 1. Overall Volume + t_count = TrenchExcavation.query.count() + m_count = ManholeExcavation.query.count() + l_count = Laying.query.count() + + # 2. Location Distribution (Business reach) + loc_results = db.session.query( + TrenchExcavation.Location, + func.count(TrenchExcavation.id) + ).group_by(TrenchExcavation.Location).all() + + # 3. Work Timeline (Business productivity trend) + # Assuming your models have a 'created_at' field + timeline_results = db.session.query( + func.date(TrenchExcavation.created_at), + func.count(TrenchExcavation.id) + ).group_by(func.date(TrenchExcavation.created_at)).order_by(func.date(TrenchExcavation.created_at)).all() + + return jsonify({ + "summary": { + "trench": t_count, + "manhole": m_count, + "laying": l_count, + "total": t_count + m_count + l_count + }, + "locations": {row[0]: row[1] for row in loc_results if row[0]}, + "timeline": {str(row[0]): row[1] for row in timeline_results} + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + + + +# subcontractor dashboard +@dashboard_bp.route("/subcontractor_dashboard", methods=["GET", "POST"]) +def subcontractor_dashboard(): + if not session.get("user_id"): + return redirect(url_for("auth.login")) + + tr_dash = DashboardService().bar_chart_of_tr_ex + + return render_template( - "dashboard.html", + "subcontractor_dashboard.html", title="Dashboard", - bar_chart=bar_chart(), - pie_chart=pie_chart(), - histogram=histogram_chart() - ) + bar_chart=tr_dash + ) \ No newline at end of file diff --git a/app/routes/file_import.py b/app/routes/file_import.py index c30b2bd..1dda07d 100644 --- a/app/routes/file_import.py +++ b/app/routes/file_import.py @@ -43,4 +43,4 @@ def client_import_file(): flash(msg, "success" if success else "danger") - return render_template("file_import_client.html", title="Client File Import", subcontractors=subcontractors) + return render_template("file_import_client.html", title="Client File Import", subcontractors=subcontractors) \ No newline at end of file diff --git a/app/routes/file_report.py b/app/routes/file_report.py index f7db588..0469dd5 100644 --- a/app/routes/file_report.py +++ b/app/routes/file_report.py @@ -72,7 +72,8 @@ class SubcontractorBill: for df in [self.df_tr, self.df_mh, self.df_dc, self.df_laying]: if not df.empty: df.drop(columns=drop_cols, errors="ignore", inplace=True) - + + # --- subcontractor report only --- @file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"]) @login_required @@ -91,7 +92,7 @@ def report_file(): if not subcontractor_id: flash("Please select a subcontractor.", "danger") - return render_template("report.html", subcontractors=subcontractors) + return render_template("subcontractor_report.html", subcontractors=subcontractors) subcontractor = Subcontractor.query.get(subcontractor_id) bill_gen = SubcontractorBill() @@ -102,13 +103,13 @@ def report_file(): else: if not ra_bill_no: flash("Please enter an RA Bill Number.", "danger") - return render_template("report.html", subcontractors=subcontractors) + return render_template("subcontractor_report.html", subcontractors=subcontractors) bill_gen.Fetch(RA_Bill_No=ra_bill_no, subcontractor_id=subcontractor_id) file_name = f"{subcontractor.subcontractor_name}_RA_{ra_bill_no}_Report.xlsx" if bill_gen.df_tr.empty and bill_gen.df_mh.empty and bill_gen.df_dc.empty: flash("No data found for this selection.", "warning") - return render_template("report.html", subcontractors=subcontractors) + return render_template("subcontractor_report.html", subcontractors=subcontractors) # If download is clicked, return file immediately if action == "download": @@ -140,74 +141,65 @@ def report_file(): download_all=download_all ) -# --- client report only --- + +# --- CLIENT REPORT (PREVIEW + DOWNLOAD) --- @file_report_bp.route("/client_report", methods=["GET", "POST"]) @login_required -def client_vs_all_subcontractor(): - tables = {"tr": None, "mh": None, "dc": None} +def client_report(): + + tables = {"tr": None, "mh": None, "dc": None, "laying": None} ra_val = "" - + if request.method == "POST": + + # ⚠ MUST match HTML name RA_Bill_No = request.form.get("RA_Bill_No") + action = request.form.get("action") ra_val = RA_Bill_No - + if not RA_Bill_No: flash("Please enter RA Bill No.", "danger") - return render_template("generate_comparison_client_vs_subcont.html", tables=tables, ra_val=ra_val) - - clientBill = ClientBill() - clientBill.Fetch(RA_Bill_No=RA_Bill_No) - contractorBill = SubcontractorBill() - contractorBill.Fetch(RA_Bill_No=RA_Bill_No) - - # --- SAFETY CHECK: Verify data exists before merging --- - if clientBill.df_tr.empty and clientBill.df_mh.empty: - flash(f"No Client records found for RA Bill {RA_Bill_No}", "warning") - return render_template("generate_comparison_client_vs_subcont.html", tables=tables, ra_val=ra_val) - - qty_cols = [...] # (Keep your existing list) - mh_dc_qty_cols = [...] # (Keep your existing list) - mh_lay_qty_cols =[...] + return render_template("client_report.html", tables=tables, ra_val=ra_val) - def aggregate_df(df, group_cols, sum_cols): - if df.empty: - # Create an empty DF with the correct columns to avoid Merge/Key Errors - return pd.DataFrame(columns=group_cols + sum_cols) - existing_cols = [c for c in sum_cols if c in df.columns] - # Ensure group_cols exist in the DF - for col in group_cols: - if col not in df.columns: - df[col] = "N/A" # Fill missing join keys - return df.groupby(group_cols, as_index=False)[existing_cols].sum() - - # Aggregate data - df_sub_tr_grp = aggregate_df(contractorBill.df_tr, ["Location", "MH_NO"], qty_cols) - df_sub_mh_grp = aggregate_df(contractorBill.df_mh, ["Location", "MH_NO"], qty_cols) - df_sub_dc_grp = aggregate_df(contractorBill.df_dc, ["Location", "MH_NO"], mh_dc_qty_cols) - df_sub_lay_grp = aggregate_df(contractorBill.df_dc, ["Location", "MH_NO"], mh_lay_qty_cols) - - # --- FINAL MERGE LOGIC --- - # We check if "Location" exists in the client data. If not, we add it to prevent the KeyError. - for df_client in [clientBill.df_tr, clientBill.df_mh, clientBill.df_dc, clientBill.df_laying ]: - if not df_client.empty and "Location" not in df_client.columns: - df_client["Location"] = "Unknown" - - try: - df_tr_cmp = clientBill.df_tr.merge(df_sub_tr_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub")) - df_mh_cmp = clientBill.df_mh.merge(df_sub_mh_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub")) - df_dc_cmp = clientBill.df_dc.merge(df_sub_dc_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub")) - df_lay_cmp = clientBill.df_laying.merge(df_sub_lay_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub")) - except KeyError as e: - flash(f"Merge Error: Missing column {str(e)}. Check if 'Location' is defined in your database models.", "danger") - return render_template("generate_comparison_client_vs_subcont.html", tables=tables, ra_val=ra_val) - - - # Convert to HTML for preview - tables["tr"] = df_tr_cmp.to_html(classes='table table-striped table-hover table-sm', index=False) - tables["mh"] = df_mh_cmp.to_html(classes='table table-striped table-hover table-sm', index=False) - tables["dc"] = df_dc_cmp.to_html(classes='table table-striped table-hover table-sm', index=False) - tables["laying"] = df_lay_cmp.to_html(classes='table table-striped table-hover table-sm', index=False) - - - return render_template("client_report.html", tables=tables, ra_val=ra_val) - \ No newline at end of file + # -------- FETCH CLIENT DATA -------- + bill_gen = ClientBill() + bill_gen.Fetch(RA_Bill_No) + + # If no data + if ( + bill_gen.df_tr.empty and + bill_gen.df_mh.empty and + bill_gen.df_dc.empty and + bill_gen.df_laying.empty + ): + flash(f"No Client records found for RA Bill {RA_Bill_No}", "warning") + return render_template("client_report.html", tables=tables, ra_val=ra_val) + + # -------- DOWNLOAD -------- + if action == "download": + + output = io.BytesIO() + + with pd.ExcelWriter(output, engine="xlsxwriter") as writer: + bill_gen.df_tr.to_excel(writer, index=False, sheet_name="Trench") + bill_gen.df_mh.to_excel(writer, index=False, sheet_name="MH") + bill_gen.df_dc.to_excel(writer, index=False, sheet_name="MH & DC") + bill_gen.df_laying.to_excel(writer, index=False, sheet_name="Laying") + + output.seek(0) + + return send_file( + output, + download_name=f"Client_RA_{RA_Bill_No}_Report.xlsx", + as_attachment=True + ) + + # -------- PREVIEW -------- + table_class = "table table-bordered table-striped table-hover table-sm" + + tables["tr"] = bill_gen.df_tr.to_html(classes=table_class, index=False) + tables["mh"] = bill_gen.df_mh.to_html(classes=table_class, index=False) + tables["dc"] = bill_gen.df_dc.to_html(classes=table_class, index=False) + tables["laying"] = bill_gen.df_laying.to_html(classes=table_class, index=False) + + return render_template("client_report.html", tables=tables, ra_val=ra_val) \ No newline at end of file diff --git a/app/routes/generate_comparison_report.py b/app/routes/generate_comparison_report.py index a2ed92d..47ec5c8 100644 --- a/app/routes/generate_comparison_report.py +++ b/app/routes/generate_comparison_report.py @@ -1,26 +1,39 @@ from flask import Blueprint, render_template, request, send_file, flash +from collections import defaultdict import pandas as pd import io - + from app.models.subcontractor_model import Subcontractor from app.models.trench_excavation_model import TrenchExcavation -from app.models.tr_ex_client_model import TrenchExcavationClient from app.models.manhole_excavation_model import ManholeExcavation -from app.models.mh_ex_client_model import ManholeExcavationClient from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber +from app.models.laying_model import Laying + +from app.models.tr_ex_client_model import TrenchExcavationClient +from app.models.mh_ex_client_model import ManholeExcavationClient from app.models.mh_dc_client_model import ManholeDomesticChamberClient +from app.models.laying_client_model import LayingClient + from app.utils.helpers import login_required - +import re + + generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report") - - + + +# sum field of pipe laying (pipe_150_mm) +PIPE_MM_PATTERN = re.compile(r"^pipe_\d+_mm$") +# sum fields of MH dc (d_0_to_0_75) +D_RANGE_PATTERN = re.compile( r"^d_\d+(?:_\d+)?_to_\d+(?:_\d+)?$") + + # NORMALIZER def normalize_key(value): if value is None: return None return str(value).strip().upper() - - + + # HEADER FORMATTER def format_header(header): if "-" in header: @@ -28,11 +41,11 @@ def format_header(header): prefix = prefix.title() else: prefix, rest = None, header - + parts = rest.split("_") result = [] i = 0 - + while i < len(parts): if i + 1 < len(parts) and parts[i].isdigit() and parts[i + 1].isdigit(): result.append(f"{parts[i]}.{parts[i + 1]}") @@ -40,98 +53,119 @@ def format_header(header): else: result.append(parts[i].title()) i += 1 - + final_text = " ".join(result) return f"{prefix}-{final_text}" if prefix else final_text - - -# LOOKUP CREATOR + + +# LOOKUP CREATOR def make_lookup(rows, key_field): lookup = {} for r in rows: location = normalize_key(r.get("Location")) key_val = normalize_key(r.get(key_field)) - + if location and key_val: - lookup[(location, key_val)] = r - + lookup.setdefault((location, key_val), []).append(r) + return lookup - - + + + # COMPARISON BUILDER def build_comparison(client_rows, contractor_rows, key_field): contractor_lookup = make_lookup(contractor_rows, key_field) output = [] - + + used_index = defaultdict(int) # 🔥 THIS FIXES YOUR ISSUE + for c in client_rows: client_location = normalize_key(c.get("Location")) client_key = normalize_key(c.get(key_field)) - + if not client_location or not client_key: continue - - s = contractor_lookup.get((client_location, client_key)) - if not s: + + subs = contractor_lookup.get((client_location, client_key)) + if not subs: continue - + + idx = used_index[(client_location, client_key)] + + # ❗ If subcontractor rows are exhausted, skip + if idx >= len(subs): + continue + + s = subs[idx] # ✅ take NEXT subcontractor row + used_index[(client_location, client_key)] += 1 + + # ---- totals ---- client_total = sum( float(v or 0) for k, v in c.items() if k.endswith("_total") + or D_RANGE_PATTERN.match(k) + or PIPE_MM_PATTERN.match(k) ) - + sub_total = sum( float(v or 0) for k, v in s.items() if k.endswith("_total") + or D_RANGE_PATTERN.match(k) + or PIPE_MM_PATTERN.match(k) ) - - diff = client_total - sub_total - + row = { "Location": client_location, key_field.replace("_", " "): client_key } - - # CLIENT DATA + for k, v in c.items(): - if k in ["id", "created_at"]: - continue - row[f"Client-{k}"] = v - + if k not in ["id", "created_at"]: + row[f"Client-{k}"] = v + row["Client-Total"] = round(client_total, 2) row[" "] = "" - - # SUBCONTRACTOR DATA + for k, v in s.items(): - if k in ["id", "created_at", "subcontractor_id"]: - continue - row[f"Subcontractor-{k}"] = v - + if k not in ["id", "created_at", "subcontractor_id"]: + row[f"Subcontractor-{k}"] = v + row["Subcontractor-Total"] = round(sub_total, 2) - row["Diff"] = round(diff, 2) - + row["Diff"] = round(client_total - sub_total, 2) + output.append(row) - + df = pd.DataFrame(output) df.columns = [format_header(col) for col in df.columns] return df - - + + + # EXCEL SHEET WRITER def write_sheet(writer, df, sheet_name, subcontractor_name): workbook = writer.book + + # write dataframe (data already correct) df.to_excel(writer, sheet_name=sheet_name, index=False, startrow=3) ws = writer.sheets[sheet_name] + # formats title_fmt = workbook.add_format({"bold": True, "font_size": 14}) client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#D9EDF7"}) sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7E1D9"}) total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#FFF2CC"}) diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#E2EFDA"}) - default_header_fmt = workbook.add_format({"bold": True,"border": 1,"bg_color": "#E7E6E6","align": "center","valign": "vcenter"}) - + default_header_fmt = workbook.add_format({ + "bold": True, + "border": 1, + "bg_color": "#E7E6E6", + "align": "center", + "valign": "vcenter" + }) + # titles ws.merge_range( 0, 0, 0, len(df.columns) - 1, "CLIENT vs SUBCONTRACTOR", @@ -143,6 +177,7 @@ def write_sheet(writer, df, sheet_name, subcontractor_name): title_fmt ) + # header formatting for col_num, col_name in enumerate(df.columns): if col_name.startswith("Client-"): ws.write(3, col_num, col_name, client_fmt) @@ -157,49 +192,59 @@ def write_sheet(writer, df, sheet_name, subcontractor_name): ws.set_column(col_num, col_num, 20) - + # REPORT ROUTE @generate_report_bp.route("/comparison_report", methods=["GET", "POST"]) @login_required def comparison_report(): subcontractors = Subcontractor.query.all() - + if request.method == "POST": subcontractor_id = request.form.get("subcontractor_id") if not subcontractor_id: flash("Please select subcontractor", "danger") return render_template("generate_comparison_report.html",subcontractors=subcontractors) - + subcontractor = Subcontractor.query.get_or_404(subcontractor_id) - + # -------- DATA -------- tr_client = [r.serialize() for r in TrenchExcavationClient.query.all()] tr_sub = [r.serialize() for r in TrenchExcavation.query.filter_by( subcontractor_id=subcontractor_id ).all()] df_tr = build_comparison(tr_client, tr_sub, "MH_NO") - + mh_client = [r.serialize() for r in ManholeExcavationClient.query.all()] mh_sub = [r.serialize() for r in ManholeExcavation.query.filter_by( subcontractor_id=subcontractor_id ).all()] df_mh = build_comparison(mh_client, mh_sub, "MH_NO") - + dc_client = [r.serialize() for r in ManholeDomesticChamberClient.query.all()] dc_sub = [r.serialize() for r in ManholeDomesticChamber.query.filter_by( subcontractor_id=subcontractor_id ).all()] df_dc = build_comparison(dc_client, dc_sub, "MH_NO") - + # df_dc = build_comparison_mh_dc(dc_client, dc_sub, "MH_NO") + + lay_client = [r.serialize() for r in LayingClient.query.all()] + lay_sub = [r.serialize() for r in Laying.query.filter_by( + subcontractor_id=subcontractor_id + ).all()] + df_lay = build_comparison(lay_client, lay_sub, "MH_NO") + # df_lay = build_comparison_laying(lay_client, lay_sub, "MH_NO") + + # -------- EXCEL -------- output = io.BytesIO() filename = f"{subcontractor.subcontractor_name}_Comparison_Report.xlsx" - + with pd.ExcelWriter(output, engine="xlsxwriter") as writer: write_sheet(writer, df_tr, "Tr.Ex", subcontractor.subcontractor_name) write_sheet(writer, df_mh, "Mh.Ex", subcontractor.subcontractor_name) write_sheet(writer, df_dc, "MH & DC", subcontractor.subcontractor_name) - + write_sheet(writer, df_lay, "Laying", subcontractor.subcontractor_name) + output.seek(0) return send_file( output, @@ -207,7 +252,108 @@ def comparison_report(): download_name=filename, mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) - + return render_template("generate_comparison_report.html",subcontractors=subcontractors) - - + + +# def build_comparison_mh_dc(client_rows, contractor_rows, key_field): +# contractor_lookup = make_lookup(contractor_rows, key_field) +# mh_dc_fields = ManholeDomesticChamberClient.sum_mh_dc_fields() + +# output = [] + +# for c in client_rows: +# loc = normalize_key(c.get("Location")) +# key = normalize_key(c.get(key_field)) +# if not loc or not key: +# continue + +# s = contractor_lookup.get((loc, key)) +# if not s: +# continue + +# client_total = sum(float(c.get(f, 0) or 0) for f in mh_dc_fields) +# sub_total = sum(float(s.get(f, 0) or 0) for f in mh_dc_fields) + +# row = { +# "Location": loc, +# key_field.replace("_", " "): key +# } + +# # CLIENT – ALL FIELDS +# for k, v in c.items(): +# if k in ["id", "created_at"]: +# continue +# row[f"Client-{k}"] = v + +# row["Client-Total"] = round(client_total, 2) +# row[" "] = "" + +# # SUBCONTRACTOR – ALL FIELDS +# for k, v in s.items(): +# if k in ["id", "created_at", "subcontractor_id"]: +# continue +# row[f"Subcontractor-{k}"] = v + +# row["Subcontractor-Total"] = round(sub_total, 2) +# row["Diff"] = round(client_total - sub_total, 2) + +# output.append(row) + +# df = pd.DataFrame(output) +# df.columns = [format_header(col) for col in df.columns] +# return df + + +# def build_comparison_laying(client_rows, contractor_rows, key_field): +# contractor_lookup = make_lookup(contractor_rows, key_field) +# laying_fields = Laying.sum_laying_fields() + +# output = [] + +# for c in client_rows: +# loc = normalize_key(c.get("Location")) +# key = normalize_key(c.get(key_field)) +# if not loc or not key: +# continue + +# s = contractor_lookup.get((loc, key)) +# if not s: +# continue + +# client_total = sum(float(c.get(f, 0) or 0) for f in laying_fields) +# sub_total = sum(float(s.get(f, 0) or 0) for f in laying_fields) + +# print("--------------",key,"----------") +# print("sum -client_total ",client_total) +# print("sum -sub_total ",sub_total) +# print("Diff ---- ",client_total - sub_total) +# print("------------------------") +# row = { +# "Location": loc, +# key_field.replace("_", " "): key +# } + +# # CLIENT – ALL FIELDS +# for k, v in c.items(): +# if k in ["id", "created_at"]: +# continue +# row[f"Client-{k}"] = v + +# row["Client-Total"] = round(client_total, 2) +# row[" "] = "" + +# # SUBCONTRACTOR – ALL FIELDS +# for k, v in s.items(): +# if k in ["id", "created_at", "subcontractor_id"]: +# continue +# row[f"Subcontractor-{k}"] = v + +# row["Subcontractor-Total"] = round(sub_total, 2) +# row["Diff"] = round(client_total - sub_total, 2) + +# output.append(row) + +# df = pd.DataFrame(output) +# df.columns = [format_header(col) for col in df.columns] +# return df \ No newline at end of file diff --git a/app/routes/subcontractor_routes.py b/app/routes/subcontractor_routes.py index 5a6a4b6..9de484e 100644 --- a/app/routes/subcontractor_routes.py +++ b/app/routes/subcontractor_routes.py @@ -1,191 +1,156 @@ -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(): - return render_template("subcontractor/add.html") -@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: - flash("Subcontractor name cannot be empty.", "danger") - return redirect("/subcontractor/add") - - # 3. Check if a subcontractor with this name already exists - 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") - - # 4. If no duplicate is found, proceed to save - 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) - db.session.commit() - flash("Subcontractor added successfully!", "success") - - except Exception as e: - 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/") -@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/", 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") - -# ---------------- DELETE ----------------- -@subcontractor_bp.route("/delete/") -@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(): + 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(): + name = request.form.get("subcontractor_name", "").strip() + if not name: + current_app.logger.warning("Empty subcontractor name submitted") flash("Subcontractor name cannot be empty.", "danger") - return redirect("/subcontractor/add") + 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") + return redirect(url_for("subcontractor.add_subcontractor")) + try: subcontractor = Subcontractor( subcontractor_name=name, contact_person=request.form.get("contact_person"), + address=request.form.get("address"), 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) 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") - -# ---------------- LIST ----------------- + current_app.logger.exception("Error while saving subcontractor") + flash("An error occurred while saving.", "danger") + + return redirect(url_for("subcontractor.subcontractor_list")) + + +# ---------------- LIST (UPDATED WITH PAGINATION) ----------------- @subcontractor_bp.route("/list") @login_required def subcontractor_list(): - subcontractors = Subcontractor.query.all() - return render_template("subcontractor/list.html", subcontractors=subcontractors) - + + page = request.args.get("page", 1, type=int) + per_page = 10 # Change how many records per page + + pagination = Subcontractor.query.order_by( + Subcontractor.created_at + ).paginate( + page=page, + per_page=per_page, + error_out=False + ) + + subcontractors = pagination.items + + current_app.logger.info(f"Viewed Subcontractor List - Page {page}") + + return render_template( + "subcontractor/list.html", + subcontractors=subcontractors, + pagination=pagination + ) + + # ---------------- EDIT ----------------- @subcontractor_bp.route("/edit/") @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/", 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: + current_app.logger.warning(f"Duplicate update attempt: {new_name}") 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") - + return redirect(url_for("subcontractor.edit_subcontractor", id=id)) + + try: + old_name = subcontractor.subcontractor_name + + subcontractor.subcontractor_name = new_name + subcontractor.contact_person = request.form.get("contact_person") + subcontractor.address = request.form.get("address") + 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() + + current_app.logger.info(f"Subcontractor Updated: {old_name} → {new_name}") + flash("Subcontractor updated successfully!", "success") + + except Exception: + db.session.rollback() + current_app.logger.exception("Error updating subcontractor") + flash("Update failed!", "danger") + + return redirect(url_for("subcontractor.subcontractor_list")) + + # ---------------- DELETE ----------------- @subcontractor_bp.route("/delete/") @login_required def delete_subcontractor(id): subcontractor = Subcontractor.query.get_or_404(id) - db.session.delete(subcontractor) - db.session.commit() + try: + name = subcontractor.subcontractor_name + db.session.delete(subcontractor) + db.session.commit() - flash("Subcontractor deleted successfully!", "success") - return redirect("/subcontractor/list") \ No newline at end of file + 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")) \ No newline at end of file diff --git a/app/routes/user.py b/app/routes/user.py index 5f16e3c..afef697 100644 --- a/app/routes/user.py +++ b/app/routes/user.py @@ -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") \ No newline at end of file diff --git a/app/services/comparison_service.py b/app/services/comparison_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/dashboard_service.py b/app/services/dashboard_service.py new file mode 100644 index 0000000..af082ad --- /dev/null +++ b/app/services/dashboard_service.py @@ -0,0 +1,49 @@ +from app.config import Config +from app import db +import matplotlib +matplotlib.use("Agg") +from app.routes.dashboard import plot_to_base64 +import matplotlib.pyplot as plt +import io +import base64 + +from app.utils.plot_utils import plot_to_base64 + +# Subcontractor models import +from app.models.trench_excavation_model import TrenchExcavation + +# Client models import +# from app.models.tr_ex_client_model import TrenchExcavationClient + + +class DashboardService: + + + # bar chart + def bar_chart_of_tr_ex(): + categories = ["Soft Murum", "Hard Murum", "Soft Rock", "Hard Rock"] + values = [120, 80, 150, 60] + + tr = TrenchExcavation() + + record = TrenchExcavation.query.first() + print(" RA_Bill_No::",record["RA_Bill_No"]) + + totals = tr.excavation_category_sums() + + # print(totals["Soft_Murum_Total"]) + # print(totals["Hard_Rock_Total"]) + + + plt.figure() + plt.bar(categories, values) + plt.title("Trench Excavation Work Category Report") + plt.xlabel("Excavation category") + plt.ylabel("Quantity") + + return plot_to_base64(plt) + + + def subcontractor_dash(): + return True + \ No newline at end of file diff --git a/app/services/logger_service.py b/app/services/logger_service.py new file mode 100644 index 0000000..347f396 --- /dev/null +++ b/app/services/logger_service.py @@ -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 {} \ No newline at end of file diff --git a/app/static/downloads/format/client_format.xlsx b/app/static/downloads/format/client_format.xlsx index ac6eab6..8dc754e 100644 Binary files a/app/static/downloads/format/client_format.xlsx and b/app/static/downloads/format/client_format.xlsx differ diff --git a/app/static/downloads/format/subcontractor_format.xlsx b/app/static/downloads/format/subcontractor_format.xlsx index b82cc4d..6991030 100644 Binary files a/app/static/downloads/format/subcontractor_format.xlsx and b/app/static/downloads/format/subcontractor_format.xlsx differ diff --git a/app/templates/base.html b/app/templates/base.html index e2fe209..1472d63 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -77,6 +77,13 @@ Show Reports + +
  • + + Subcontractor Dashboard + +
  • + diff --git a/app/templates/client_report.html b/app/templates/client_report.html index edd5355..9426901 100644 --- a/app/templates/client_report.html +++ b/app/templates/client_report.html @@ -4,17 +4,6 @@

    Client File Reports

    - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} - - {% endfor %} - {% endif %} - {% endwith %} -
    @@ -33,7 +22,7 @@
    - {% if tables.tr or tables.mh or tables.dc %} + {% if tables.tr or tables.mh or tables.dc or tables.laying %}

    Comparison Preview

    @@ -50,6 +39,10 @@ +
    -
    - - {% endif %} +
    +
    + {{ tables.laying|safe }} +
    +
    + + {% endif %} + + {% endblock %} \ No newline at end of file diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index d4d5bee..4399c4b 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -1,87 +1,118 @@ {% extends "base.html" %} {% block content %} -
    +

    Comparison Software Solapur (UGD) - Live Dashboard

    -

    Comparison Software Solapur(UGD)

    - -
    - -
    -
    Test Total Work
    -

    30%

    +
    Trenching Units
    +

    0

    - -
    -
    test Completed
    -

    35%

    +
    Manhole Units
    +

    0

    - -
    -
    Pending
    -

    35%

    +
    Laying Units
    +

    0

    -
    -
    - -
    -
    - Work Category Bar Chart -
    -
    - +
    Live Category Bar Chart
    +
    +
    -
    -
    - Project Status Pie Chart -
    -
    - +
    Location Distribution Pie Chart
    +
    +
    - - -
    -
    -
    - Daily Work Histogram -
    -
    - -
    -
    -
    -
    -
    + + + + {% endblock %} \ No newline at end of file diff --git a/app/templates/subcontractor/add.html b/app/templates/subcontractor/add.html index 61d15fa..68e5c36 100644 --- a/app/templates/subcontractor/add.html +++ b/app/templates/subcontractor/add.html @@ -2,39 +2,44 @@ {% block content %}
    -

    Add New Subcontractor

    -
    +
    - +
    - +
    - + + +
    + +
    +
    - +
    - +
    -
    + Back +
    {% endblock %} \ No newline at end of file diff --git a/app/templates/subcontractor/edit.html b/app/templates/subcontractor/edit.html index 4c4c037..430cce0 100644 --- a/app/templates/subcontractor/edit.html +++ b/app/templates/subcontractor/edit.html @@ -4,36 +4,41 @@

    Edit Subcontractor

    -
    +
    - +
    - +
    - + + +
    + +
    +
    - +
    - +
    - Back + Back
    diff --git a/app/templates/subcontractor/list.html b/app/templates/subcontractor/list.html index 2acd851..983cc50 100644 --- a/app/templates/subcontractor/list.html +++ b/app/templates/subcontractor/list.html @@ -1,89 +1,160 @@ {% extends "base.html" %} {% block content %} -
    +
    - -
    -
    -

    - Subcontractor List -

    -
    + +
    +
    -
    - - - - - - - - - - - + +
    +
    IDNameMobileEmailGST NoAction
    + + + + + + + + + + + + + {% for s in subcontractors %} + + + + + + + + + + Delete + + + + {% endfor %} + +
    IDNameContactMobileEmailStatusAction
    {{ s.id }}{{ s.subcontractor_name }}{{ s.contact_person }}{{ s.mobile_no }}{{ s.email_id }} + {% if s.status == "Active" %} + Active + {% else %} + Inactive + {% endif %} + + + Edit + -
    +
    + + +
    {% for s in subcontractors %} - - {{ s.id }} +
    +
    +
    {{ s.subcontractor_name }}
    - - {{ s.subcontractor_name }} - +

    Contact Person Name: {{ s.contact_person }}

    +

    Mobile: {{ s.mobile_no }}

    +

    Email: {{ s.email_id }}

    - - {{ s.mobile_no }} - +

    + Status: + {% if s.status == "Active" %} + Active + {% else %} + Inactive + {% endif %} +

    - - {{ s.email_id }} - - - - {{ s.gst_no }} - - - - - +
    {% endfor %} - +
    - - - - Total Subcontractors + + + +
    diff --git a/app/templates/subcontractor_dashboard.html b/app/templates/subcontractor_dashboard.html new file mode 100644 index 0000000..e9e1d24 --- /dev/null +++ b/app/templates/subcontractor_dashboard.html @@ -0,0 +1,122 @@ +{% extends "base.html" %} + +{% block content %} + +
    + +

    Subcontractor Dashboard

    + +
    +
    +
    +
    +
    Trenching Units
    +

    0

    +
    +
    +
    +
    +
    +
    +
    Manhole Units
    +

    0

    +
    +
    +
    +
    +
    +
    +
    Laying Units
    +

    0

    +
    +
    +
    +
    + +
    +
    +
    +
    Live Category Bar Chart
    +
    + +
    +
    +
    + +
    +
    +
    Location Distribution Pie Chart
    +
    + +
    +
    +
    +
    + +
    + + + + + + +{% endblock %} \ No newline at end of file diff --git a/app/utils/exceptions.py b/app/utils/exceptions.py new file mode 100644 index 0000000..b9571f6 --- /dev/null +++ b/app/utils/exceptions.py @@ -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) \ No newline at end of file diff --git a/app/utils/plot_utils.py b/app/utils/plot_utils.py new file mode 100644 index 0000000..5bed985 --- /dev/null +++ b/app/utils/plot_utils.py @@ -0,0 +1,10 @@ +import base64 +from io import BytesIO +import matplotlib +matplotlib.use("Agg") + +def plot_to_base64(plt): + img = BytesIO() + plt.savefig(img, format="png", bbox_inches="tight") + img.seek(0) + return base64.b64encode(img.read()).decode("utf-8") diff --git a/app/utils/response_handler.py b/app/utils/response_handler.py new file mode 100644 index 0000000..3154104 --- /dev/null +++ b/app/utils/response_handler.py @@ -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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a3dace9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +version: '3.8' + +services: + db: + image: mysql:8.0 + container_name: comparison_db + restart: always + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: comparisondb + ports: + - "3307:3306" + volumes: + - mysql_data:/var/lib/mysql + + app: + build: . + container_name: comparison_app + restart: always + environment: + FLASK_ENV: development + FLASK_DEBUG: "True" + FLASK_HOST: "0.0.0.0" + FLASK_PORT: "5001" + + DB_DIALECT: mysql + DB_DRIVER: pymysql + DB_HOST: db + DB_PORT: 3306 + DB_NAME: comparisondb + DB_USER: root + DB_PASSWORD: root + + ports: + - "5001:5001" + + depends_on: + - db + + volumes: + - ./app/logs:/app/app/logs + - ./app/static/uploads:/app/app/static/uploads + - ./app/static/downloads:/app/app/static/downloads + +volumes: + mysql_data: diff --git a/logs/app.log b/logs/app.log index 3b781b9..e69de29 100644 --- a/logs/app.log +++ b/logs/app.log @@ -1,200 +0,0 @@ -2025-12-09 13:11:05,606 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5000 -2025-12-09 13:11:05,607 | INFO | Press CTRL+C to quit -2025-12-09 13:11:05,608 | INFO | * Restarting with stat -2025-12-09 13:11:06,239 | WARNING | * Debugger is active! -2025-12-09 13:11:06,240 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:11:48,880 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5000 -2025-12-09 13:11:48,881 | INFO | Press CTRL+C to quit -2025-12-09 13:11:48,882 | INFO | * Restarting with stat -2025-12-09 13:11:49,519 | WARNING | * Debugger is active! -2025-12-09 13:11:49,521 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:12:05,727 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\user_service.py', reloading -2025-12-09 13:12:05,826 | INFO | * Restarting with stat -2025-12-09 13:12:06,499 | WARNING | * Debugger is active! -2025-12-09 13:12:06,501 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:12:09,545 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading -2025-12-09 13:12:09,654 | INFO | * Restarting with stat -2025-12-09 13:12:10,286 | WARNING | * Debugger is active! -2025-12-09 13:12:10,288 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:12:12,311 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\routes\\auth.py', reloading -2025-12-09 13:12:12,407 | INFO | * Restarting with stat -2025-12-09 13:12:13,071 | WARNING | * Debugger is active! -2025-12-09 13:12:13,072 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:12:16,128 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading -2025-12-09 13:12:16,257 | INFO | * Restarting with stat -2025-12-09 13:12:16,898 | WARNING | * Debugger is active! -2025-12-09 13:12:16,900 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:12:20,944 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\routes\\user.py', reloading -2025-12-09 13:12:21,042 | INFO | * Restarting with stat -2025-12-09 13:12:21,719 | WARNING | * Debugger is active! -2025-12-09 13:12:21,721 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:12:23,762 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\routes\\file_import.py', reloading -2025-12-09 13:12:23,870 | INFO | * Restarting with stat -2025-12-09 13:12:24,505 | WARNING | * Debugger is active! -2025-12-09 13:12:24,507 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:12:27,561 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\__init__.py', reloading -2025-12-09 13:12:27,670 | INFO | * Restarting with stat -2025-12-09 13:12:28,294 | WARNING | * Debugger is active! -2025-12-09 13:12:28,296 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:12:31,336 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\db_service.py', reloading -2025-12-09 13:12:31,448 | INFO | * Restarting with stat -2025-12-09 13:12:32,097 | WARNING | * Debugger is active! -2025-12-09 13:12:32,099 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:13:05,662 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading -2025-12-09 13:13:05,773 | INFO | * Restarting with stat -2025-12-09 13:13:06,466 | WARNING | * Debugger is active! -2025-12-09 13:13:06,469 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:13:10,944 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5000 -2025-12-09 13:13:10,944 | INFO | Press CTRL+C to quit -2025-12-09 13:13:10,945 | INFO | * Restarting with stat -2025-12-09 13:13:11,623 | WARNING | * Debugger is active! -2025-12-09 13:13:11,625 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:14:11,295 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading -2025-12-09 13:14:11,393 | INFO | * Restarting with stat -2025-12-09 13:14:12,004 | WARNING | * Debugger is active! -2025-12-09 13:14:12,006 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:14:32,108 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:14:32,109 | INFO | Press CTRL+C to quit -2025-12-09 13:14:32,110 | INFO | * Restarting with stat -2025-12-09 13:14:32,699 | WARNING | * Debugger is active! -2025-12-09 13:14:32,701 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:15:58,632 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading -2025-12-09 13:15:58,733 | INFO | * Restarting with stat -2025-12-09 13:15:59,415 | WARNING | * Debugger is active! -2025-12-09 13:15:59,416 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:16:03,475 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading -2025-12-09 13:16:03,583 | INFO | * Restarting with stat -2025-12-09 13:16:04,204 | WARNING | * Debugger is active! -2025-12-09 13:16:04,206 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:16:33,504 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading -2025-12-09 13:16:33,605 | INFO | * Restarting with stat -2025-12-09 13:16:34,213 | WARNING | * Debugger is active! -2025-12-09 13:16:34,215 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:16:41,815 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5000 -2025-12-09 13:16:41,816 | INFO | Press CTRL+C to quit -2025-12-09 13:18:12,302 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5000 -2025-12-09 13:18:12,302 | INFO | Press CTRL+C to quit -2025-12-09 13:22:07,114 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:22:07,114 | INFO | Press CTRL+C to quit -2025-12-09 13:22:07,116 | INFO | * Restarting with stat -2025-12-09 13:22:07,935 | WARNING | * Debugger is active! -2025-12-09 13:22:07,937 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:23:21,204 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading -2025-12-09 13:23:21,305 | INFO | * Restarting with stat -2025-12-09 13:24:06,973 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:24:06,973 | INFO | Press CTRL+C to quit -2025-12-09 13:24:06,974 | INFO | * Restarting with stat -2025-12-09 13:24:07,689 | WARNING | * Debugger is active! -2025-12-09 13:24:07,691 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:24:36,315 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\app.py', reloading -2025-12-09 13:24:36,418 | INFO | * Restarting with stat -2025-12-09 13:24:37,074 | WARNING | * Debugger is active! -2025-12-09 13:24:37,076 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:26:54,442 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading -2025-12-09 13:26:54,543 | INFO | * Restarting with stat -2025-12-09 13:26:59,170 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:26:59,170 | INFO | Press CTRL+C to quit -2025-12-09 13:26:59,171 | INFO | * Restarting with stat -2025-12-09 13:26:59,827 | WARNING | * Debugger is active! -2025-12-09 13:26:59,829 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:28:47,631 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading -2025-12-09 13:28:47,747 | INFO | * Restarting with stat -2025-12-09 13:28:48,478 | WARNING | * Debugger is active! -2025-12-09 13:28:48,480 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:28:51,150 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:28:51,151 | INFO | Press CTRL+C to quit -2025-12-09 13:28:51,153 | INFO | * Restarting with stat -2025-12-09 13:28:51,788 | WARNING | * Debugger is active! -2025-12-09 13:28:51,790 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:28:54,904 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading -2025-12-09 13:28:55,010 | INFO | * Restarting with stat -2025-12-09 13:28:55,608 | WARNING | * Debugger is active! -2025-12-09 13:28:55,610 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:28:56,644 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading -2025-12-09 13:28:56,752 | INFO | * Restarting with stat -2025-12-09 13:29:04,454 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:29:04,454 | INFO | Press CTRL+C to quit -2025-12-09 13:29:04,455 | INFO | * Restarting with stat -2025-12-09 13:29:05,096 | WARNING | * Debugger is active! -2025-12-09 13:29:05,098 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:30:01,657 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:30:01,657 | INFO | Press CTRL+C to quit -2025-12-09 13:30:01,658 | INFO | * Restarting with stat -2025-12-09 13:30:02,278 | WARNING | * Debugger is active! -2025-12-09 13:30:02,280 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:30:27,872 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:30:27,872 | INFO | Press CTRL+C to quit -2025-12-09 13:30:27,873 | INFO | * Restarting with stat -2025-12-09 13:30:28,474 | WARNING | * Debugger is active! -2025-12-09 13:30:28,476 | INFO | * Debugger PIN: 105-645-384 -2025-12-09 13:33:22,709 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:33:22,709 | INFO | Press CTRL+C to quit -2025-12-09 13:33:22,710 | INFO | * Restarting with stat -2025-12-09 13:33:23,778 | WARNING | * Debugger is active! -2025-12-09 13:33:23,781 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:33:29,939 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\db_service.py', reloading -2025-12-09 13:33:30,080 | INFO | * Restarting with stat -2025-12-09 13:33:44,462 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:33:44,462 | INFO | Press CTRL+C to quit -2025-12-09 13:33:44,464 | INFO | * Restarting with stat -2025-12-09 13:33:45,216 | WARNING | * Debugger is active! -2025-12-09 13:33:45,218 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:35:23,298 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:35:23,299 | INFO | Press CTRL+C to quit -2025-12-09 13:35:23,301 | INFO | * Restarting with stat -2025-12-09 13:35:24,098 | WARNING | * Debugger is active! -2025-12-09 13:35:24,100 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:38:25,991 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading -2025-12-09 13:38:26,126 | INFO | * Restarting with stat -2025-12-09 13:38:27,120 | WARNING | * Debugger is active! -2025-12-09 13:38:27,122 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:38:37,386 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading -2025-12-09 13:38:37,513 | INFO | * Restarting with stat -2025-12-09 13:38:38,297 | WARNING | * Debugger is active! -2025-12-09 13:38:38,300 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:38:45,485 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading -2025-12-09 13:38:45,605 | INFO | * Restarting with stat -2025-12-09 13:38:46,348 | WARNING | * Debugger is active! -2025-12-09 13:38:46,350 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:38:55,109 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:38:55,109 | INFO | Press CTRL+C to quit -2025-12-09 13:38:55,110 | INFO | * Restarting with stat -2025-12-09 13:38:55,959 | WARNING | * Debugger is active! -2025-12-09 13:38:55,961 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:39:27,813 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading -2025-12-09 13:39:27,937 | INFO | * Restarting with stat -2025-12-09 13:39:28,684 | WARNING | * Debugger is active! -2025-12-09 13:39:28,687 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:40:00,602 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading -2025-12-09 13:40:00,728 | INFO | * Restarting with stat -2025-12-09 13:40:01,428 | WARNING | * Debugger is active! -2025-12-09 13:40:01,430 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 13:40:21,531 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 13:40:21,531 | INFO | Press CTRL+C to quit -2025-12-09 13:40:21,533 | INFO | * Restarting with stat -2025-12-09 13:40:22,307 | WARNING | * Debugger is active! -2025-12-09 13:40:22,309 | INFO | * Debugger PIN: 697-115-033 -2025-12-09 14:03:58,363 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5001 -2025-12-09 14:03:58,363 | INFO | Press CTRL+C to quit -2025-12-09 14:03:58,364 | INFO | * Restarting with stat -2025-12-09 14:03:59,038 | WARNING | * Debugger is active! -2025-12-09 14:03:59,041 | INFO | * Debugger PIN: 697-115-033 diff --git a/requirements.txt b/requirements.txt index 3398384..cf84ea1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,5 @@ python-dotenv cryptography xlsxwriter matplotlib +flask_sqlalchemy +flask_migrate \ No newline at end of file