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/models/manhole_excavation_model.py b/app/models/manhole_excavation_model.py index 51e773c..39a5444 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_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/models/mh_ex_client_model.py b/app/models/mh_ex_client_model.py index c4fd6c8..d1ed411 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_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/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 eacfd0a..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" @@ -108,3 +109,17 @@ class TrenchExcavation(db.Model): + 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 cd101d5..90970e1 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -1,75 +1,156 @@ 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") -# 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] +# import matplotlib +# matplotlib.use("Agg") - plt.figure() - plt.hist(daily_work, bins=5) - plt.title("Daily Work Distribution") - plt.xlabel("Work Units") - plt.ylabel("Frequency") +# 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 - return plot_to_base64(plt) +# 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 -# 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") + - 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"]) diff --git a/app/routes/generate_comparison_report.py b/app/routes/generate_comparison_report.py index ea344b2..b94f2b6 100644 --- a/app/routes/generate_comparison_report.py +++ b/app/routes/generate_comparison_report.py @@ -1,38 +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.manhole_excavation_model import ManholeExcavation 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: @@ -40,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]}") @@ -52,98 +53,111 @@ 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) + 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) + 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 df.to_excel(writer, sheet_name=sheet_name, index=False, startrow=3) ws = writer.sheets[sheet_name] - + title_fmt = workbook.add_format({"bold": True, "font_size": 14}) client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#B6DAED"}) sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F3A081"}) total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7D261"}) diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#82DD49"}) default_header_fmt = workbook.add_format({"bold": True,"border": 1,"bg_color": "#E7E6E6","align": "center","valign": "vcenter"}) - - + + ws.merge_range( 0, 0, 0, len(df.columns) - 1, "CLIENT vs SUBCONTRACTOR", @@ -154,8 +168,8 @@ def write_sheet(writer, df, sheet_name, subcontractor_name): f"Subcontractor Name - {subcontractor_name}", title_fmt ) - - + + for col_num, col_name in enumerate(df.columns): if col_name.startswith("Client-"): ws.write(3, col_num, col_name, client_fmt) @@ -167,62 +181,62 @@ def write_sheet(writer, df, sheet_name, subcontractor_name): ws.write(3, col_num, col_name, diff_fmt) else: ws.write(3, col_num, col_name, default_header_fmt) - + 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) - + # 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, @@ -230,78 +244,78 @@ 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) @@ -311,27 +325,27 @@ def comparison_report(): # "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 diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index d4d5bee..a5e5716 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -1,87 +1,118 @@ {% extends "base.html" %} {% block content %} -
+

Comparison dSoftware 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