20 Commits

Author SHA1 Message Date
d5d69a5ba5 Merge pull request 'subctr location filter, MH NO. search, subctr manadatory field removed, added select all for delete and also all edit at once feature added' (#24) from swapnil-dev into main
Reviewed-on: #24
Reviewed-by: Pankaj J Patil <pankajjpatil2001@gmail.com>
2026-08-07 07:44:07 +00:00
5b76beff8e subctr location filter, MH NO. search, subctr manadatory field removed, added select all for delete and also all edit at once feature added 2026-08-07 13:01:38 +05:30
aa9f5f0ebf log changes 2026-08-07 12:33:42 +05:30
82c57f8445 suctr location filter, MH NO. search, subctr manadatory field removed, added select all for delete and also all edit at once feature added 2026-08-07 12:30:21 +05:30
861a61c6ec Merge branch 'pankaj-dev' 2026-08-07 12:12:58 +05:30
375e899857 log changes 2026-08-07 12:12:07 +05:30
8304df6ba7 Merge pull request 'fix: category filter, action icons, download filename, reset button' (#19) from Laxmi-Devs into main
Reviewed-on: #19
Reviewed-by: Pankaj J Patil <pankajjpatil2001@gmail.com>
2026-08-07 06:25:22 +00:00
c09cb8ea3a suctr location filter, MH NO. search, subctr manadatory field removed, added select all for delete and also all edit at once feature added 2026-08-07 11:51:51 +05:30
12529800f0 fix: category filter, action icons, download filename, reset button 2026-08-07 11:49:18 +05:30
18874cc84e changes of rate model of sub-cont and there functions 2026-08-07 10:29:15 +05:30
de7b85034d replace 2026-08-06 13:39:13 +05:30
2f99c290df Merge branch 'pankaj-dev' 2026-08-06 12:46:16 +05:30
5577576112 Update README.md
update readme file
2026-04-07 11:42:23 +00:00
ce0e4f90cf Merge pull request 'updateds folder path and regex' (#14) from pankaj-dev into main
Reviewed-on: #14
2026-03-18 04:51:41 +00:00
446778a50c Merge pull request 'pankaj-dev' (#13) from pankaj-dev into main
Reviewed-on: #13
2026-03-17 12:54:08 +00:00
310ab40bf9 Merge pull request 'dataase field updateds and model updateds' (#12) from pankaj-dev into main
Reviewed-on: #12
2026-03-11 08:04:07 +00:00
bb56f00a6c Resolved merge conflicts 2026-02-28 16:59:24 +05:30
8353bb6426 Merge branch 'pankaj-dev' 2026-02-28 16:55:19 +05:30
1dceb640bd Merge pull request 'Dowload report of client report fix' (#10) from dev-anish into main
Reviewed-on: #10
2026-01-19 06:54:02 +00:00
359847958d Dowload report of client report fix 2026-01-17 18:06:58 +05:30
12 changed files with 1310 additions and 414 deletions

View File

@@ -36,7 +36,8 @@ The Comparison Project is designed to:
## Tech Stack ## Tech Stack
**Backend Framework**: Flask **Frontend Framework**: HTML, CSS, Js, Bootstrap
**Backend Framework**: Python Flask
**Database**: SQL Database (MySQL/PostgreSQL/SQLite configured via environment variables) **Database**: SQL Database (MySQL/PostgreSQL/SQLite configured via environment variables)
**ORM**: SQLAlchemy **ORM**: SQLAlchemy
**File Processing**: Pandas, OpenPyXL, XlsxWriter **File Processing**: Pandas, OpenPyXL, XlsxWriter
@@ -580,4 +581,4 @@ Open browser: `http://127.0.0.1:5000/`
For issues, feature requests, or contributions, please contact the development team. For issues, feature requests, or contributions, please contact the development team.
**Last Updated:** January 2026 **Last Updated:** April 2026

View File

@@ -4,7 +4,7 @@ from flask import (
request, request,
redirect, redirect,
url_for, url_for,
flash flash, jsonify
) )
from app.models.subcontractor_model import Subcontractor from app.models.subcontractor_model import Subcontractor
@@ -27,25 +27,7 @@ def engineering_master():
title="Engineering Masters" title="Engineering Masters"
) )
# Client rate model
@engi_bp.route("/subcontractor-rate")
def add_subcontractor_rates():
subcontractors = Subcontractor.query.filter_by(status="Active").all()
if request.method == "POST":
try:
SubcontractorRateService.save_rate(request.form)
flash(SuccessMessage.SAVE, "success")
return redirect(url_for("engineering.add_subcontractor_rates"))
except Exception as e:
flash(ErrorMessage.INTERNAL_SERVER_ERROR, "danger")
return render_template(
"engineering/add_rate.html",
title="Subcontractor Rate Master",
subcontractors=subcontractors
)
@engi_bp.route("/client-rate") @engi_bp.route("/client-rate")
def client_rates(): def client_rates():
@@ -54,3 +36,72 @@ def client_rates():
title="Client Rate Master" title="Client Rate Master"
) )
@engi_bp.route("/subcontractor-rate", methods=["GET", "POST"])
def add_subcontractor_rates():
subcontractors = Subcontractor.query.filter_by(status="Active").all()
if request.method == "POST":
result = SubcontractorRateService.save_or_update(request.form)
if result["success"]:
flash(result["message"], "success")
return redirect(url_for("engineering.add_subcontractor_rates"))
else:
flash(result["message"], "danger")
rates = SubcontractorRateService.get_all_rates()
return render_template(
"engineering/contractor_rate.html",
title="Subcontractor Rate Master",
subcontractors=subcontractors,
rates=rates
)
@engi_bp.route("/subcontractor-rate/edit/<int:rate_id>", methods=["GET", "POST"])
def edit_rate(rate_id):
subcontractors = Subcontractor.query.filter_by(status="Active").all()
if request.method == "POST":
result = SubcontractorRateService.save_or_update(request.form)
if result["success"]:
flash(result["message"], "success")
return redirect(url_for("engineering.add_subcontractor_rates"))
flash(result["message"], "danger")
rate = SubcontractorRateService.get_rate(rate_id)
rates = SubcontractorRateService.get_all_rates()
return render_template(
"engineering/contractor_rate.html",
subcontractors=subcontractors,
rate=rate,
rates=rates
)
@engi_bp.route("/subcontractor-rate/delete/<int:rate_id>")
def delete_rate(rate_id):
SubcontractorRateService.delete_rate(rate_id)
flash("Rate deleted successfully.", "success")
return redirect(url_for("engineering.add_subcontractor_rates"))
@engi_bp.route("/check-rate")
def check_rate():
exists = SubcontractorRateService.check_duplicate(
subcontractor_id=request.args.get("subcontractor_id"),
category=request.args.get("category"),
item_name=request.args.get("item_name"),
rate_id=request.args.get("rate_id")
)
return jsonify({
"exists": exists
})

View File

@@ -4,6 +4,7 @@ from flask import Blueprint, render_template, request, send_file, flash, jsonify
from app.utils.helpers import login_required from app.utils.helpers import login_required
from app.utils.regex_utils import RegularExpression from app.utils.regex_utils import RegularExpression
from app import db from app import db
from sqlalchemy import func
import re import re
from app.models.subcontractor_model import Subcontractor from app.models.subcontractor_model import Subcontractor
@@ -24,13 +25,80 @@ from app.services.abstract_service import AbstractReportService
file_report_bp = Blueprint("file_report", __name__, url_prefix="/file") file_report_bp = Blueprint("file_report", __name__, url_prefix="/file")
# ---------------- LOCATION HELPERS ----------------
WORK_MODELS = [TrenchExcavation, ManholeExcavation, ManholeDomesticChamber, Laying]
def get_distinct_locations():
"""Union of distinct, non-empty Location values across all 4 work tables."""
locations = set()
for Model in WORK_MODELS:
rows = db.session.query(Model.Location).distinct().all()
for (loc,) in rows:
if loc and loc.strip():
locations.add(loc.strip())
return sorted(locations)
def get_subcontractors_for_location(location):
"""Return Subcontractor objects that have at least one work record
(in any of the 4 category tables) at the given location. If no
location is given, returns every subcontractor. Shared by the AJAX
endpoint below and by the server-rendered dropdown so the list is
correct even before/without JS running (e.g. on page reload or a
validation-error re-render).
Matching is case- and whitespace-insensitive, since Location is a
free-text field and stored values can drift ("Pune" vs "PUNE " etc.)
even though the dropdown options themselves come from a distinct
query and look identical."""
location = (location or "").strip()
if not location:
return Subcontractor.query.order_by(Subcontractor.subcontractor_name).all()
target = location.upper()
sc_ids = set()
for Model in WORK_MODELS:
rows = (
db.session.query(Model.subcontractor_id)
.filter(func.upper(func.trim(Model.Location)) == target)
.distinct()
.all()
)
for (sid,) in rows:
if sid:
sc_ids.add(sid)
if not sc_ids:
return []
return (
Subcontractor.query.filter(Subcontractor.id.in_(sc_ids))
.order_by(Subcontractor.subcontractor_name)
.all()
)
@file_report_bp.route("/get_subcontractors_by_location")
@login_required
def get_subcontractors_by_location():
"""AJAX endpoint: return subcontractors that have at least one work
record (in any of the 4 category tables) at the given location."""
location = request.args.get("location", "").strip()
subs = get_subcontractors_for_location(location)
return jsonify([{"id": s.id, "name": s.subcontractor_name} for s in subs])
# ---------------- ACTION COLUMN ---------------- # ---------------- ACTION COLUMN ----------------
def add_action_columns(df, model_key): def add_action_columns(df, model_key):
if df.empty: if df.empty:
return df return df
df.insert(0, "Select", df["Id"].apply( df.insert(0, "Select", df["Id"].apply(
lambda x: f'<input type="checkbox" class="row-check" data-id="{x}">' lambda x: f'<input type="checkbox" class="row-check" data-model="{model_key}" data-id="{x}">'
)) ))
df["Update"] = df["Id"].apply( df["Update"] = df["Id"].apply(
@@ -44,6 +112,76 @@ def add_action_columns(df, model_key):
return df return df
# ---------------- SELECT-ALL HEADER ----------------
def add_select_all_header(table_html, model_key):
"""Swap pandas' plain 'Select' column header for a select-all checkbox
scoped to this table (via data-model), so checking it only toggles
rows in this table - not the other 3 category tables on the page."""
return table_html.replace(
"<th>Select</th>",
f'<th><input type="checkbox" class="select-all-checkbox" '
f'data-model="{model_key}" title="Select All"></th>',
1
)
# ---------------- TABLE OR EMPTY-STATE ----------------
def add_data_field_attrs(table_html, raw_fields):
"""Tag each editable data <td> with a data-field attribute naming its
underlying database column, so bulk-edit mode on the frontend knows
exactly which field to submit for each input it creates. The 'id'
column is deliberately skipped - it's the primary key and must never
be editable.
Uses match spans (not string search-and-replace) to rebuild each row,
because a naive .replace() on duplicate cell text (e.g. two cells
that both just say "0.00") would edit the wrong cell."""
total_cols = 1 + len(raw_fields) + 2 # Select + data columns + Update + Delete
def process_row(m):
row_html = m.group(0)
matches = list(re.finditer(r"<td>.*?</td>", row_html, flags=re.S))
if len(matches) != total_cols:
return row_html # shape mismatch - leave untouched rather than guess
pieces = []
last_end = 0
for i, mt in enumerate(matches):
pieces.append(row_html[last_end:mt.start()])
cell = mt.group(0)
if 1 <= i <= len(raw_fields):
field = raw_fields[i - 1]
if field.lower() != "id":
cell = f'<td data-field="{field}">' + cell[len("<td>"):]
pieces.append(cell)
last_end = mt.end()
pieces.append(row_html[last_end:])
return "".join(pieces)
return re.sub(r"<tr>.*?</tr>", process_row, table_html, flags=re.S)
def render_table_or_empty(df, model_key, table_class, raw_fields=None):
"""Render a table, or a friendly placeholder if there's no data.
IMPORTANT: pandas' to_html() on a fully empty DataFrame (0 rows AND
0 columns, which is what we get when a category has no matching
records) still emits a <table class="datatable"> - just with a
header row that has zero <th> cells. jQuery DataTables then tries to
initialize on a table with no columns and throws, which (since the
init code runs as one synchronous block) silently kills every bit of
JS registered after it - including the select-all checkbox handler
for the OTHER tables on the page. Returning a plain message instead
of an empty <table class="datatable"> avoids ever handing DataTables
something it can't initialize."""
if df.empty:
return '<div class="alert alert-info mb-0">No records found.</div>'
html = df.to_html(classes=table_class, index=False, escape=False)
html = add_select_all_header(html, model_key)
html = add_data_field_attrs(html, raw_fields or [])
return html
@@ -56,7 +194,7 @@ class SubcontractorBill:
self.df_laying = pd.DataFrame() self.df_laying = pd.DataFrame()
# self.df_abstract = pd.DataFrame() # NEW # self.df_abstract = pd.DataFrame() # NEW
def Fetch(self, RA_Bill_No=None, subcontractor_id=None, location=None): def Fetch(self, RA_Bill_No=None, subcontractor_id=None, location=None, mh_no=None):
filters = {} filters = {}
if subcontractor_id: if subcontractor_id:
@@ -73,7 +211,6 @@ class SubcontractorBill:
# LOCATION FILTER # LOCATION FILTER
if location: if location:
search = location.strip().lower() search = location.strip().lower()
print("location::",search)
trench = [ trench = [
t for t in trench t for t in trench
if search in (t.Location or "").strip().lower() if search in (t.Location or "").strip().lower()
@@ -94,6 +231,29 @@ class SubcontractorBill:
if search in (t.Location or "").strip().lower() if search in (t.Location or "").strip().lower()
] ]
# MH NO FILTER
if mh_no:
mh_search = mh_no.strip().lower()
trench = [
t for t in trench
if mh_search in (t.MH_NO or "").strip().lower()
]
mh = [
t for t in mh
if mh_search in (t.MH_NO or "").strip().lower()
]
dc = [
t for t in dc
if mh_search in (t.MH_NO or "").strip().lower()
]
lay = [
t for t in lay
if mh_search in (t.MH_NO or "").strip().lower()
]
# Set dataframe # Set dataframe
self.df_tr = pd.DataFrame([c.serialize() for c in trench]) self.df_tr = pd.DataFrame([c.serialize() for c in trench])
self.df_mh = pd.DataFrame([c.serialize() for c in mh]) self.df_mh = pd.DataFrame([c.serialize() for c in mh])
@@ -102,9 +262,24 @@ class SubcontractorBill:
drop_cols = ["11", "_sa_instance_state", "subcontractor_id" , "created_at"] drop_cols = ["11", "_sa_instance_state", "subcontractor_id" , "created_at"]
for df in [self.df_tr, self.df_mh, self.df_dc, self.df_laying]: # Raw (pre-format_column_names) column names for each table, e.g.
# "MH_NO" rather than the display label "MH No". Bulk-edit mode
# needs these to know which real database column each input maps
# to, since the table only shows the prettified header text.
self.tr_fields = []
self.mh_fields = []
self.dc_fields = []
self.laying_fields = []
for df, attr in [
(self.df_tr, "tr_fields"),
(self.df_mh, "mh_fields"),
(self.df_dc, "dc_fields"),
(self.df_laying, "laying_fields"),
]:
if not df.empty: if not df.empty:
df.drop(columns=drop_cols, errors="ignore", inplace=True) df.drop(columns=drop_cols, errors="ignore", inplace=True)
setattr(self, attr, list(df.columns))
format_column_names(df) format_column_names(df)
name = "" name = ""
@@ -152,6 +327,60 @@ def delete_records():
return jsonify({"status": "error", "message": str(e)}), 500 return jsonify({"status": "error", "message": str(e)}), 500
@file_report_bp.route("/bulk_update", methods=["POST"])
@login_required
def bulk_update():
"""Bulk-edit save endpoint. Expects JSON shaped like:
{ "tr": { "5": {"MH_NO": "12A", "Location": "Pune"}, ... }, "mh": {...}, ... }
Field names are validated against each model's real table columns
server-side - the frontend sending a field name is not enough on its
own to permit writing it; id/subcontractor_id/created_at are always
refused regardless of what's submitted.
"""
data = request.json or {}
model_map = {
"tr": TrenchExcavation,
"mh": ManholeExcavation,
"dc": ManholeDomesticChamber,
"laying": Laying
}
PROTECTED_FIELDS = {"id", "subcontractor_id", "created_at"}
updated = 0
errors = []
try:
for model_key, records in data.items():
ModelClass = model_map.get(model_key)
if not ModelClass:
errors.append(f"Unknown table '{model_key}'")
continue
valid_columns = {c.name for c in ModelClass.__table__.columns} - PROTECTED_FIELDS
for record_id, fields in (records or {}).items():
obj = ModelClass.query.get(record_id)
if not obj:
errors.append(f"{model_key} #{record_id}: record not found")
continue
for field, value in (fields or {}).items():
if field not in valid_columns:
errors.append(f"{model_key} #{record_id}: '{field}' is not editable")
continue
setattr(obj, field, value)
updated += 1
db.session.commit()
except Exception as e:
db.session.rollback()
return jsonify({"status": "error", "message": str(e)}), 500
return jsonify({"status": "success", "updated": updated, "errors": errors})
@file_report_bp.route("/edit/<string:model>/<int:record_id>", methods=["GET", "POST"]) @file_report_bp.route("/edit/<string:model>/<int:record_id>", methods=["GET", "POST"])
@login_required @login_required
def edit_record(model, record_id): def edit_record(model, record_id):
@@ -204,28 +433,40 @@ def edit_record(model, record_id):
def report_file(): def report_file():
# get all subcontractor data # get all subcontractor data
subcontractors = Subcontractor.query.all() subcontractors = Subcontractor.query.all()
locations = get_distinct_locations()
tables = None tables = None
abstract_html = "" abstract_html = ""
selected_sc_id = None selected_sc_id = None
has_data = {"tr": False, "mh": False, "dc": False, "laying": False}
ra_bill_no = "" ra_bill_no = ""
location = "" location = ""
mh_no = ""
category = "" category = ""
# Search or load data # Search or load data
if request.method == "POST": if request.method == "POST":
# get from data # get from data
subcontractor_id = request.form.get("subcontractor_id") subcontractor_id = request.form.get("subcontractor_id") or None
ra_bill_no = request.form.get("ra_bill_no", "").strip() ra_bill_no = request.form.get("ra_bill_no", "").strip()
location = request.form.get("location", "").strip() location = request.form.get("location", "").strip()
mh_no = request.form.get("mh_no", "").strip()
category = request.form.get("category", "") category = request.form.get("category", "")
action = request.form.get("action", "preview") action = request.form.get("action", "preview")
if not subcontractor_id: # Keep the subcontractor dropdown scoped to the chosen location
flash("Select Subcontractor", "danger") # even on a plain (non-JS) page render.
subcontractors = get_subcontractors_for_location(location)
# Subcontractor is now optional - at least one other filter must
# be given so the search isn't a "return everything" query.
if not subcontractor_id and not location and not ra_bill_no and not mh_no:
flash("Enter at least a Location, RA Bill No, MH No, or Subcontractor to search", "danger")
return render_template( return render_template(
"subcontractor_report.html", "subcontractor_report.html",
subcontractors=subcontractors subcontractors=subcontractors,
locations=locations,
has_data=has_data
) )
selected_sc_id = subcontractor_id selected_sc_id = subcontractor_id
@@ -234,7 +475,31 @@ def report_file():
if action == "excel_all": if action == "excel_all":
bill.Fetch(subcontractor_id=subcontractor_id) bill.Fetch(subcontractor_id=subcontractor_id)
else: else:
bill.Fetch(ra_bill_no,subcontractor_id,location) bill.Fetch(ra_bill_no, subcontractor_id, location, mh_no)
# ---------------------------------------------------------
if (
bill.df_tr.empty and
bill.df_mh.empty and
bill.df_dc.empty and
bill.df_laying.empty
):
flash(
f"No records found for RA Bill No '{ra_bill_no}'. "
"Please check the RA Bill No and try again."
if ra_bill_no else
"No records found for the selected filters.",
"warning"
)
return render_template(
"subcontractor_report.html",
subcontractors=subcontractors,
selected_sc_id=selected_sc_id,
selected_ra_bill=ra_bill_no,
selected_location=location,
selected_category=category
)
# ----------------------------------------- # -----------------------------------------
# Generate Abstract Report for Web # Generate Abstract Report for Web
@@ -256,6 +521,32 @@ def report_file():
bill.df_tr = bill.df_mh = bill.df_dc = pd.DataFrame() bill.df_tr = bill.df_mh = bill.df_dc = pd.DataFrame()
if (
category in ("tr", "mh", "dc", "laying")
and bill.df_tr.empty and bill.df_mh.empty
and bill.df_dc.empty and bill.df_laying.empty
):
category_labels = {
"tr": "Trench Excavation",
"mh": "Manhole Excavation",
"dc": "Domestic Chamber",
"laying": "Pipe Laying",
}
flash(
f"No {category_labels[category]} records found for the "
"selected filters.",
"warning"
)
return render_template(
"subcontractor_report.html",
subcontractors=subcontractors,
selected_sc_id=selected_sc_id,
selected_ra_bill=ra_bill_no,
selected_location=location,
selected_category=category
)
# =================================================== # ===================================================
# DOWNLOAD EXCEL # DOWNLOAD EXCEL
# =================================================== # ===================================================
@@ -267,16 +558,46 @@ def report_file():
abstract = AbstractReportService(subcontractor_id=subcontractor_id,ra_bill_no=ra_bill_no) abstract = AbstractReportService(subcontractor_id=subcontractor_id,ra_bill_no=ra_bill_no)
abstract.generate(workbook) abstract.generate(workbook)
bill.df_tr.to_excel(writer,sheet_name="Tr.Ex",index=False)
bill.df_mh.to_excel(writer,sheet_name="Mh.Ex",index=False) sheet_map = [
bill.df_dc.to_excel(writer,sheet_name="MH & DC",index=False) (bill.df_tr, "Tr.Ex"),
bill.df_laying.to_excel(writer,sheet_name="Pipe Laying",index=False) (bill.df_mh, "Mh.Ex"),
(bill.df_dc, "MH & DC"),
(bill.df_laying, "Pipe Laying"),
]
for df, sheet_name in sheet_map:
if not df.empty:
df.to_excel(writer, sheet_name=sheet_name, index=False)
writer.close() writer.close()
output.seek(0) output.seek(0)
sc_obj = next(
(s for s in subcontractors if str(s.id) == str(subcontractor_id)),
None
)
sc_name = sc_obj.subcontractor_name if sc_obj else "Subcontractor"
sc_name = re.sub(r'[^A-Za-z0-9_-]+', '_', sc_name).strip('_')
name_parts = [sc_name]
if ra_bill_no:
name_parts.append(f"RA{re.sub(r'[^A-Za-z0-9_-]+', '_', ra_bill_no)}")
if location:
name_parts.append(re.sub(r'[^A-Za-z0-9_-]+', '_', location).strip('_'))
if category and category != "all":
name_parts.append(category.upper())
if action == "excel_all":
name_parts.append("All")
filename = "_".join(name_parts) + "_Report.xlsx"
return send_file( return send_file(
output, output,
download_name= "subcontractor_Report.xlsx", download_name=filename,
as_attachment=True as_attachment=True
) )
@@ -297,6 +618,15 @@ def report_file():
bill.df_dc = add_action_columns(bill.df_dc, "dc") bill.df_dc = add_action_columns(bill.df_dc, "dc")
bill.df_laying = add_action_columns(bill.df_laying, "laying") bill.df_laying = add_action_columns(bill.df_laying, "laying")
# Used by the template to hide Delete Selected / Bulk Edit for a
# category that has no rows to act on.
has_data = {
"tr": not bill.df_tr.empty,
"mh": not bill.df_mh.empty,
"dc": not bill.df_dc.empty,
"laying": not bill.df_laying.empty,
}
# this are html classes # this are html classes
# table_class = ( "table " "table-bordered" "table-hover " "table-striped " "table-sm " "align-middle " "datatable " "mb-0") # table_class = ( "table " "table-bordered" "table-hover " "table-striped " "table-sm " "align-middle " "datatable " "mb-0")
table_class = ( table_class = (
@@ -313,21 +643,24 @@ def report_file():
# This are showing on web tables # This are showing on web tables
tables = { tables = {
"tr": bill.df_tr.to_html(classes=table_class, index=False, escape=False), "tr": render_table_or_empty(bill.df_tr, "tr", table_class, bill.tr_fields),
"mh": bill.df_mh.to_html(classes=table_class, index=False, escape=False), "mh": render_table_or_empty(bill.df_mh, "mh", table_class, bill.mh_fields),
"dc": bill.df_dc.to_html(classes=table_class, index=False, escape=False ), "dc": render_table_or_empty(bill.df_dc, "dc", table_class, bill.dc_fields),
"laying": bill.df_laying.to_html(classes=table_class, index=False, escape=False) "laying": render_table_or_empty(bill.df_laying, "laying", table_class, bill.laying_fields)
} }
return render_template( return render_template(
"subcontractor_report.html", "subcontractor_report.html",
subcontractors=subcontractors, subcontractors=subcontractors,
locations=locations,
selected_sc_id=selected_sc_id, selected_sc_id=selected_sc_id,
selected_ra_bill=ra_bill_no, selected_ra_bill=ra_bill_no,
selected_location=location, selected_location=location,
selected_mh_no=mh_no,
selected_category=category, selected_category=category,
tables=tables, tables=tables,
abstract_html=abstract_html abstract_html=abstract_html,
has_data=has_data
) )

View File

@@ -1,28 +1,66 @@
from app.services.db_service import db from app.services.db_service import db
from app.models.subcontractor_rate_model import SubcontractorRate from app.models.subcontractor_rate_model import SubcontractorRate
from sqlalchemy import func
class SubcontractorRateService: class SubcontractorRateService:
@staticmethod @staticmethod
def save_rate(form): def save_or_update(form):
rate = SubcontractorRate( rate_id = form.get("id")
subcontractor_id=form.get("subcontractor_id"),
category=form.get("category"), subcontractor_id = form.get("subcontractor_id")
item_code=form.get("item_code"), category = form.get("category")
item_name=form.get("item_name"), item_name = form.get("item_name").strip()
unit=form.get("unit"),
rate=form.get("rate"), # -----------------------------
effective_from=form.get("effective_from"), # Duplicate Validation
effective_to=form.get("effective_to") or None, # -----------------------------
status=form.get("status") duplicate = (
SubcontractorRate.query
.filter(
SubcontractorRate.subcontractor_id == subcontractor_id,
SubcontractorRate.category == category,
func.lower(SubcontractorRate.item_name) == item_name.lower()
)
.first()
) )
db.session.add(rate) # Ignore current record while editing
db.session.commit() if duplicate and (not rate_id or duplicate.id != int(rate_id)):
return rate return {
"success": False,
"message": "Category and Rate already exists for this Subcontractor."
}
# -----------------------------
# Insert / Update
# -----------------------------
if rate_id:
rate = SubcontractorRate.query.get_or_404(rate_id)
else:
rate = SubcontractorRate()
rate.subcontractor_id = subcontractor_id
rate.category = category
rate.item_code = form.get("item_code")
rate.item_name = item_name
rate.unit = form.get("unit")
rate.rate = form.get("rate")
rate.effective_from = form.get("effective_from")
rate.effective_to = form.get("effective_to") or None
rate.status = form.get("status")
if not rate_id:
db.session.add(rate)
db.session.commit()
return {
"success": True,
"message": "Saved Successfully."
}
@staticmethod @staticmethod
def get_all_rates(): def get_all_rates():
@@ -32,34 +70,33 @@ class SubcontractorRateService:
.order_by(SubcontractorRate.created_at.desc()) .order_by(SubcontractorRate.created_at.desc())
.all() .all()
) )
@staticmethod @staticmethod
def get_rate(rate_id): def get_rate(rate_id):
return SubcontractorRate.query.get_or_404(rate_id) return SubcontractorRate.query.get_or_404(rate_id)
@staticmethod
def update_rate(rate_id, form):
rate = SubcontractorRate.query.get_or_404(rate_id)
rate.subcontractor_id = form.get("subcontractor_id")
rate.category = form.get("category")
rate.item_code = form.get("item_code")
rate.item_name = form.get("item_name")
rate.unit = form.get("unit")
rate.rate = form.get("rate")
rate.effective_from = form.get("effective_from")
rate.effective_to = form.get("effective_to") or None
rate.status = form.get("status")
db.session.commit()
return rate
@staticmethod
def delete_rate(rate_id): def delete_rate(rate_id):
rate = SubcontractorRate.query.get_or_404(rate_id) rate = SubcontractorRate.query.get_or_404(rate_id)
db.session.delete(rate) db.session.delete(rate)
db.session.commit() db.session.commit()
@staticmethod
def check_duplicate(subcontractor_id, category, item_name, rate_id=None):
query = SubcontractorRate.query.filter(
SubcontractorRate.subcontractor_id == subcontractor_id,
SubcontractorRate.category == category,
func.lower(SubcontractorRate.item_name) == item_name.strip().lower()
)
if rate_id:
query = query.filter(SubcontractorRate.id != int(rate_id))
return query.first() is not None

View File

@@ -124,10 +124,21 @@
<i class="bi bi-arrow-left-right me-2"></i> Client vs Subcontractor <i class="bi bi-arrow-left-right me-2"></i> Client vs Subcontractor
</a> </a>
</li> </li>
<!-- <li>
<a class="dropdown-item" href="/file/client_vs_subcont">
<i class="bi bi-arrow-left-right me-2"></i> Comparison Report
</a>
</li> -->
</ul> </ul>
</li> </li>
<!-- Formats -->
<li class="nav-item">
<a class="nav-link" href="/file_format">
<i class="bi bi-file-earmark-text me-1"></i> Formats
</a>
</li>
<!-- Masters --> <!-- Masters -->
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/engi"> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/engi">
@@ -144,21 +155,12 @@
<!-- Client Standard Rates --> <!-- Client Standard Rates -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/engi/client-rate"> <a class="nav-link" href="/engi/client-rate">
<i class="bi bi-file-earmark-text me-1"></i> Client Standard Rates <i class="bi bi-file-earmark-text me-1"></i> Client Rates
</a> </a>
</li> </li>
<!-- Formats -->
<li class="nav-item">
<a class="nav-link" href="/file_format">
<i class="bi bi-file-earmark-text me-1"></i> Formats
</a>
</li>
</ul> </ul>
</li> </li>
<!-- USER DROPDOWN --> <!-- USER DROPDOWN -->
<li class="nav-item dropdown"> <li class="nav-item dropdown">
@@ -185,10 +187,10 @@
</small> </small>
</li> </li>
<!-- Dashboard --> <!-- Masters -->
<li> <li>
<a class="dropdown-item text-light py-2" href="{{ url_for('dashboard.dashboard') }}"> <a class="dropdown-item text-light py-2" href="{{ url_for('engineering.engineering_master') }}">
<i class="bi bi-speedometer2 me-2"></i> Dashboard <i class="bi bi-gear me-2"></i> Masters
</a> </a>
</li> </li>
@@ -199,12 +201,13 @@
</a> </a>
</li> </li>
<!-- Manage Account --> <!-- Manage Account
<li> <li>
<a class="dropdown-item py-2" href="#"> <a class="dropdown-item py-2" href="#">
<i class="bi bi-gear me-2"></i> Manage Account <i class="bi bi-gear me-2"></i> Manage Account
</a> </a>
</li> </li>
-->
<li><hr class="dropdown-divider border-secondary m-0"></li> <li><hr class="dropdown-divider border-secondary m-0"></li>
@@ -223,9 +226,7 @@
Secured by <strong>LCEPL</strong> Secured by <strong>LCEPL</strong>
</small> </small>
</li> </li>
</ul> </ul>
</li> </li>
</ul> </ul>
@@ -379,6 +380,7 @@
showLoader(); showLoader();
const btn = this.querySelector("button[type='submit']"); const btn = this.querySelector("button[type='submit']");
const originalBtnHtml = btn ? btn.innerHTML : null;
if(btn){ if(btn){
btn.disabled = true; btn.disabled = true;
@@ -388,10 +390,24 @@
`; `;
} }
setTimeout(function () {
hideLoader();
if (btn) {
btn.disabled = false;
btn.innerHTML = originalBtnHtml;
}
}, 3000);
}); });
}); });
}); });
window.addEventListener("pageshow", function () {
hideLoader();
});
</script> </script>

View File

@@ -31,16 +31,17 @@
type="button">Tr.Ex </button> type="button">Tr.Ex </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" id="mh-tab" data-bs-toggle="tab" data-bs-target="#mh" type="button"> <button class="nav-link" id="mh-tab" data-bs-toggle="tab" data-bs-target="#mh" type="button">Mh.Ex
Mh.Ex</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" id="dc-tab" data-bs-toggle="tab" data-bs-target="#dc" type="button"> <button class="nav-link" id="dc-tab" data-bs-toggle="tab" data-bs-target="#dc" type="button">MH & DC
MH & DC</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying"type="button"> <button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying" type="button">Laying
Laying & Bedding </button> & Bedding </button>
</li>
</ul> </ul>
<div class="tab-content mt-3" id="reportTabsContent"> <div class="tab-content mt-3" id="reportTabsContent">

View File

@@ -1,236 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid mt-4">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="bi bi-currency-rupee"></i>
Subcontractor Rate Master
</h4>
</div>
<div class="card-body">
<form method="POST">
<div class="row">
<!-- Subcontractor -->
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">
Subcontractor
</label>
<select name="subcontractor_id"
class="form-select"
required>
<option value="">
Select Subcontractor
</option>
{% for sub in subcontractors %}
<option value="{{ sub.id }}">
{{ sub.subcontractor_name }}
</option>
{% endfor %}
</select>
</div>
<!-- Category -->
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">
Category
</label>
<select name="category"
class="form-select"
required>
<option value="">
Select Category
</option>
<option value="TR_EX">
Trench Excavation
</option>
<option value="MH_EX">
Manhole Excavation
</option>
<option value="MH_DC">
Manhole & Domestic Chamber
</option>
<option value="LAYING">
Pipe Laying
</option>
</select>
</div>
<!-- Status -->
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">
Status
</label>
<select name="status"
class="form-select">
<option value="Active">
Active
</option>
<option value="Inactive">
Inactive
</option>
</select>
</div>
</div>
<div class="row">
<!-- Item Code -->
<div class="col-md-3 mb-3">
<label class="form-label fw-bold">
Item Code
</label>
<input type="text"
name="item_code"
class="form-control"
placeholder="SM01"
required>
</div>
<!-- Item Name -->
<div class="col-md-5 mb-3">
<label class="form-label fw-bold">
Item Name
</label>
<input type="text"
name="item_name"
class="form-control"
placeholder="Soft Murum 0-1.5"
required>
</div>
<!-- Unit -->
<div class="col-md-2 mb-3">
<label class="form-label fw-bold">
Unit
</label>
<select name="unit"
class="form-select">
<option value="Cum">Cum</option>
<option value="Nos">Nos</option>
<option value="Rmt">Rmt</option>
<option value="Sqm">Sqm</option>
</select>
</div>
<!-- Rate -->
<div class="col-md-2 mb-3">
<label class="form-label fw-bold">
Rate (₹)
</label>
<input type="number"
step="0.01"
min="0"
name="rate"
class="form-control"
placeholder="0.00"
required>
</div>
</div>
<div class="row">
<!-- Effective From -->
<div class="col-md-3 mb-3">
<label class="form-label fw-bold">
Effective From
</label>
<input type="date"
name="effective_from"
class="form-control"
required>
</div>
<!-- Effective To -->
<div class="col-md-3 mb-3">
<label class="form-label fw-bold">
Effective To
</label>
<input type="date"
name="effective_to"
class="form-control">
</div>
</div>
<hr>
<div class="text-end">
<button type="reset"
class="btn btn-secondary">
<i class="bi bi-arrow-clockwise"></i>
Reset
</button>
<button type="submit"
class="btn btn-success">
<i class="bi bi-check-circle"></i>
Save Rate
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid mt-4">
<div class="card shadow">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0">
<i class="bi bi-currency-rupee"></i>
Client Rate Master
</h4>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,384 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid mt-4">
<div class="card shadow">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0">
<i class="bi bi-currency-rupee"></i>
Subcontractor Rate Master
</h4>
</div>
<div class="card-body">
<form method="POST" id="rateForm">
<!-- Hidden ID -->
<input type="hidden" id="rate_id" name="id" value="{{ rate.id if rate else '' }}">
<div class="row">
<!-- Subcontractor -->
<div class="col-md-4 mb-3">
<label class="form-label fw-bold"> Subcontractor </label>
<select id="subcontractor_id" name="subcontractor_id" class="form-select" required>
<option value="">-- Select Subcontractor --</option>
{% for sub in subcontractors %}
<option value="{{ sub.id }}"
{% if rate and rate.subcontractor_id==sub.id %}
selected
{% endif %}>
{{ sub.subcontractor_name }}
</option>
{% endfor %}
</select>
</div>
<!-- Category -->
<div class="col-md-4 mb-3">
<label class="form-label fw-bold"> Category </label>
<select id="category" name="category" class="form-select" required>
<option value="">-- Select Category --</option>
<option value="trench_excavation"
{% if rate and rate.category=="trench_excavation" %}selected{% endif %}>
Trench Excavation
</option>
<option value="manhole_excavation"
{% if rate and rate.category=="manhole_excavation" %}selected{% endif %}>
Manhole Excavation
</option>
<option value="Manhole_Domestic_Chamber"
{% if rate and rate.category=="Manhole_Domestic_Chamber" %}selected{% endif %}>
MH & Domestic Chamber
</option>
<option value="Laying"
{% if rate and rate.category=="Laying" %}selected{% endif %}>
Pipe Laying
</option>
</select>
</div>
<!-- Status -->
<div class="col-md-4 mb-3">
<label class="form-label fw-bold"> Status </label>
<select name="status" class="form-select">
<option value="Active"
{% if not rate or rate.status=="Active" %}selected{% endif %}>
Active
</option>
<option value="Inactive"
{% if rate and rate.status=="Inactive" %}selected{% endif %}>
Inactive
</option>
</select>
</div>
</div>
<div class="row">
<!-- Item Code -->
<div class="col-md-3 mb-3">
<label class="form-label fw-bold"> Item Code </label>
<input type="text" name="item_code" class="form-control" value="{{ rate.item_code if rate else '' }}" required>
</div>
<!-- Item Name -->
<div class="col-md-5 mb-3">
<label class="form-label fw-bold"> Item Name </label>
<input type="text"
id="item_name"
name="item_name"
class="form-control"
autocomplete="off"
value="{{ rate.item_name if rate else '' }}"
required>
<small id="duplicateMessage"
class="text-danger"
style="display:none;">
</small>
</div>
<!-- Unit -->
<div class="col-md-2 mb-3">
<label class="form-label fw-bold">Unit</label>
<select name="unit" class="form-select">
<option value="Cum"
{% if not rate or rate.unit=="Cum" %}selected{% endif %}>
Cum
</option>
<option value="Nos"
{% if rate and rate.unit=="Nos" %}selected{% endif %}>
Nos
</option>
<option value="Rmt"
{% if rate and rate.unit=="Rmt" %}selected{% endif %}>
Rmt
</option>
<option value="Sqm"
{% if rate and rate.unit=="Sqm" %}selected{% endif %}>
Sqm
</option>
</select>
</div>
<!-- Rate -->
<div class="col-md-2 mb-3">
<label class="form-label fw-bold">Rate</label>
<input type="number" step="0.01" name="rate" class="form-control" value="{{ rate.rate if rate else '' }}" required>
</div>
</div>
<div class="row">
<div class="col-md-3 mb-3">
<label class="form-label fw-bold"> Effective From </label>
<input type="date" name="effective_from" class="form-control" value="{{ rate.effective_from if rate else '' }}" required>
</div>
<div class="col-md-3 mb-3">
<label class="form-label fw-bold"> Effective To </label>
<input type="date" name="effective_to" class="form-control" value="{{ rate.effective_to if rate else '' }}">
</div>
</div>
<hr>
<div class="text-end">
<a href="{{ url_for('engineering.add_subcontractor_rates') }}" class="btn btn-secondary">
<i class="bi bi-arrow-clockwise"></i> Reset
</a>
<button type="submit" id="saveBtn" class="btn btn-success">
{% if rate %}
<i class="bi bi-pencil-square"></i>
Update Rate
{% else %}
<i class="bi bi-check-circle"></i>
Save Rate
{% endif %}
</button>
</div>
</form>
</div>
</div>
<!-- Table -->
<div class="card shadow mt-4">
<div class="card-header bg-dark text-white">
<h5 class="mb-0">
<i class="bi bi-table"></i>Rate List
</h5>
</div>
<div class="card-body">
<table class="table table-bordered table-hover table-striped" id="rateTable">
<thead class="table-primary">
<tr>
<th>#</th>
<th>Subcontractor</th>
<th>Category</th>
<th>Item Code</th>
<th>Item Name</th>
<th>Unit</th>
<th>Rate</th>
<th>Status</th>
<th width="130">Action</th>
</tr>
</thead>
<tbody>
{% for row in rates %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ row.subcontractor.subcontractor_name }}</td>
<td>{{ row.category }}</td>
<td>{{ row.item_code }}</td>
<td>{{ row.item_name }}</td>
<td>{{ row.unit }}</td>
<td>{{ row.rate }}</td>
<td>
{% if row.status=="Active" %}
<span class="badge bg-success">
Active
</span>
{% else %}
<span class="badge bg-danger">
Inactive
</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('engineering.edit_rate', rate_id=row.id) }}" class="btn btn-warning btn-sm">
<i class="bi bi-pencil-square"></i>
</a>
<a href="{{ url_for('engineering.delete_rate', rate_id=row.id) }}"
class="btn btn-danger btn-sm" onclick="return confirm('Delete this Item:{{row.item_name}} & Rate:{{row.rate}} ?')">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
$(document).ready(function () {
// ----------------------------
// DataTable
// ----------------------------
$("#rateTable").DataTable({
responsive: true,
pageLength: 10,
destroy: true
});
// ----------------------------
// Validate on field change
// ----------------------------
$("#subcontractor_id, #category").on("change", function () {
clearValidation();
checkDuplicateRate();
});
// ----------------------------
// Validate while typing
// ----------------------------
let timer;
$("#item_name").on("keyup input", function () {
clearTimeout(timer);
timer = setTimeout(function () {
checkDuplicateRate();
}, 300);
});
// Edit Mode
checkDuplicateRate();
// ----------------------------
// Prevent Submit
// ----------------------------
$("#rateForm").submit(function (e) {
if ($("#saveBtn").prop("disabled")) {
e.preventDefault();
$("#item_name").focus();
return false;
}
});
});
//=========================================
// Reset Validation
//=========================================
function clearValidation() {
$("#duplicateMessage")
.hide()
.text("")
.removeClass("text-success text-danger");
$("#item_name")
.removeClass("is-valid is-invalid");
$("#saveBtn").prop("disabled", false);
}
//=========================================
// Duplicate Validation
//=========================================
function checkDuplicateRate() {
let subcontractor = $("#subcontractor_id").val();
let category = $("#category").val();
let item_name = $("#item_name").val().trim();
let rate_id = $("#rate_id").val();
// Mandatory fields
if (
subcontractor == "" ||
category == "" ||
item_name.length < 2
) {
clearValidation();
return;
}
$.ajax({
url: "{{ url_for('engineering.check_rate') }}",
type: "GET",
dataType: "json",
data: {
subcontractor_id: subcontractor,
category: category,
item_name: item_name,
rate_id: rate_id
},
success: function (response) {
if (response.exists) {
$("#duplicateMessage")
.text("This Item Name already exists for the selected Subcontractor and Category.")
.removeClass("text-success")
.addClass("text-danger")
.show();
$("#item_name")
.removeClass("is-valid")
.addClass("is-invalid");
$("#saveBtn").prop("disabled", true);
}
else {
$("#duplicateMessage")
.text("Item Name is available.")
.removeClass("text-danger")
.addClass("text-success")
.show();
$("#item_name")
.removeClass("is-invalid")
.addClass("is-valid");
$("#saveBtn").prop("disabled", false);
}
},
error: function () {
clearValidation();
}
});
}
</script>
{% endblock %}

View File

@@ -33,13 +33,26 @@
<form method="POST" class="loading-form"> <form method="POST" class="loading-form">
<div class="row g-3"> <div class="row g-3">
<div class="col-lg-3">
<label class="form-label fw-semibold"> Location </label>
<select name="location" id="location" class="form-select">
<option value="">--- Select Location ---</option>
{% for loc in locations %}
<option value="{{ loc }}"
{% if selected_location == loc %}selected{% endif %}>
{{ loc }}
</option>
{% endfor %}
</select>
</div>
<div class="col-lg-3"> <div class="col-lg-3">
<label class="form-label fw-semibold"> <label class="form-label fw-semibold">
Subcontractor Subcontractor
<span class="text-danger">*</span> <span class="text-muted fw-normal"></span>
</label> </label>
<select name="subcontractor_id" class="form-select" required> <select name="subcontractor_id" id="subcontractor_id" class="form-select">
<option value="">--- Select Contractor ---</option> <option value="">--- All Subcontractors ---</option>
{% for sc in subcontractors %} {% for sc in subcontractors %}
<option value="{{ sc.id }}" <option value="{{ sc.id }}"
{% if selected_sc_id|string == sc.id|string %}selected{% endif %}> {% if selected_sc_id|string == sc.id|string %}selected{% endif %}>
@@ -61,13 +74,13 @@
</div> </div>
<div class="col-lg-3"> <div class="col-lg-3">
<label class="form-label fw-semibold"> Location </label> <label class="form-label fw-semibold"> MH No </label>
<input <input
type="text" type="text"
name="location" name="mh_no"
class="form-control" class="form-control"
placeholder="Project Location" placeholder="Enter MH No"
value="{{ selected_location or '' }}"> value="{{ selected_mh_no or '' }}">
</div> </div>
<div class="col-lg-3"> <div class="col-lg-3">
@@ -155,99 +168,148 @@
</div> </div>
{% if tables %} {% if tables %}
{% set show_all = (not selected_category) or selected_category == 'all' %}
<!-- Tabs --> <!-- Tabs -->
<div class="card shadow-sm border-0 mt-4"> <div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-light"> <div class="card-header bg-light">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="nav-item"> <li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#abstract"> <button class="nav-link {% if show_all %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#abstract">
<i class="bi bi-file-earmark-text"></i> Abstract <i class="bi bi-file-earmark-text"></i> Abstract
</button> </button>
</li> </li>
{% if show_all or selected_category == 'tr' %}
<li class="nav-item"> <li class="nav-item">
<button class="nav-link " data-bs-toggle="tab" data-bs-target="#tr"> <button class="nav-link {% if selected_category == 'tr' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#tr">
<i class="bi bi-cone-striped"></i> Trench Excavation <i class="bi bi-cone-striped"></i> Trench Excavation
</button> </button>
</li> </li>
{% endif %}
{% if show_all or selected_category == 'mh' %}
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#mh"> <button class="nav-link {% if selected_category == 'mh' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#mh">
<i class="bi bi-nut"></i> Manhole Excavation <i class="bi bi-nut"></i> Manhole Excavation
</button> </button>
</li> </li>
{% endif %}
{% if show_all or selected_category == 'dc' %}
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#dc"> <button class="nav-link {% if selected_category == 'dc' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#dc">
<i class="bi bi-grid-3x3"></i> Manhole & Domestic Chambers Construction <i class="bi bi-grid-3x3"></i> Manhole & Domestic Chambers Construction
</button> </button>
</li> </li>
{% endif %}
{% if show_all or selected_category == 'laying' %}
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#laying"> <button class="nav-link {% if selected_category == 'laying' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#laying">
<i class="bi bi-bezier2"></i> Pipe Laying <i class="bi bi-bezier2"></i> Pipe Laying
</button> </button>
</li> </li>
{% endif %}
</ul> </ul>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane fade show active" id="abstract"> <div class="tab-pane fade {% if show_all %}show active{% endif %}" id="abstract">
{{ abstract_html|safe }} {{ abstract_html|safe }}
</div> </div>
{% if show_all or selected_category == 'tr' %}
<!-- Trench --> <!-- Trench -->
<div class="tab-pane fade "id="tr"> <div class="tab-pane fade {% if selected_category == 'tr' %}show active{% endif %}" id="tr">
<div class="mb-3"> {% if has_data.tr %}
<div class="mb-3 d-flex gap-2 flex-wrap">
<button onclick="deleteSelected('tr')" class="btn btn-danger"> <button onclick="deleteSelected('tr')" class="btn btn-danger">
<i class="bi bi-trash"></i> Delete Selected <i class="bi bi-trash"></i> Delete Selected
</button> </button>
<button type="button" class="btn btn-outline-primary bulk-edit-toggle-btn" data-model="tr">
<i class="bi bi-pencil-square"></i> Bulk Edit
</button>
<button type="button" class="btn btn-outline-secondary bulk-edit-cancel-btn d-none" data-model="tr">
Cancel
</button>
</div> </div>
{% endif %}
<div class="table-responsive border rounded shadow-sm"> <div class="table-responsive border rounded shadow-sm">
{{ tables.tr|safe }} {{ tables.tr|safe }}
</div> </div>
</div> </div>
{% endif %}
{% if show_all or selected_category == 'mh' %}
<!-- MH --> <!-- MH -->
<div class="tab-pane fade" id="mh"> <div class="tab-pane fade {% if selected_category == 'mh' %}show active{% endif %}" id="mh">
<div class="mb-3"> {% if has_data.mh %}
<div class="mb-3 d-flex gap-2 flex-wrap">
<button onclick="deleteSelected('mh')" class="btn btn-danger"> <button onclick="deleteSelected('mh')" class="btn btn-danger">
<i class="bi bi-trash"></i>Delete Selected <i class="bi bi-trash"></i>Delete Selected
</button> </button>
<button type="button" class="btn btn-outline-primary bulk-edit-toggle-btn" data-model="mh">
<i class="bi bi-pencil-square"></i> Bulk Edit
</button>
<button type="button" class="btn btn-outline-secondary bulk-edit-cancel-btn d-none" data-model="mh">
Cancel
</button>
</div> </div>
{% endif %}
<div class="table-responsive border rounded shadow-sm"> <div class="table-responsive border rounded shadow-sm">
{{ tables.mh|safe }} {{ tables.mh|safe }}
</div> </div>
</div> </div>
{% endif %}
{% if show_all or selected_category == 'dc' %}
<!-- DC --> <!-- DC -->
<div class="tab-pane fade" id="dc"> <div class="tab-pane fade {% if selected_category == 'dc' %}show active{% endif %}" id="dc">
<div class="mb-3"> {% if has_data.dc %}
<div class="mb-3 d-flex gap-2 flex-wrap">
<button onclick="deleteSelected('dc')"class="btn btn-danger"> <button onclick="deleteSelected('dc')"class="btn btn-danger">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
Delete Selected Delete Selected
</button> </button>
<button type="button" class="btn btn-outline-primary bulk-edit-toggle-btn" data-model="dc">
<i class="bi bi-pencil-square"></i> Bulk Edit
</button>
<button type="button" class="btn btn-outline-secondary bulk-edit-cancel-btn d-none" data-model="dc">
Cancel
</button>
</div> </div>
{% endif %}
<div class="table-responsive border rounded shadow-sm"> <div class="table-responsive border rounded shadow-sm">
{{ tables.dc|safe }} {{ tables.dc|safe }}
</div> </div>
</div> </div>
{% endif %}
{% if show_all or selected_category == 'laying' %}
<!-- Laying --> <!-- Laying -->
<div class="tab-pane fade" id="laying"> <div class="tab-pane fade {% if selected_category == 'laying' %}show active{% endif %}" id="laying">
<div class="mb-3"> {% if has_data.laying %}
<div class="mb-3 d-flex gap-2 flex-wrap">
<button onclick="deleteSelected('laying')" <button onclick="deleteSelected('laying')"
class="btn btn-danger"> class="btn btn-danger">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
Delete Selected Delete Selected
</button> </button>
<button type="button" class="btn btn-outline-primary bulk-edit-toggle-btn" data-model="laying">
<i class="bi bi-pencil-square"></i> Bulk Edit
</button>
<button type="button" class="btn btn-outline-secondary bulk-edit-cancel-btn d-none" data-model="laying">
Cancel
</button>
</div> </div>
{% endif %}
<div class="table-responsive border rounded shadow-sm"> <div class="table-responsive border rounded shadow-sm">
{{ tables.laying|safe }} {{ tables.laying|safe }}
</div> </div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -256,62 +318,246 @@
</div> </div>
<script> <script>
// NOTE: this content block renders before base.html's bottom-of-body
// <script src="jquery..."> tag, so $ is not defined yet if this runs
// immediately - that was silently breaking the location cascade
// (and DataTable init) below. Deferring to DOMContentLoaded (native
// JS, no jQuery needed to register it) guarantees jQuery has loaded
// by the time this code actually runs.
document.addEventListener("DOMContentLoaded", function () {
// Remember which tab is active so that actions which reload the
// page (Delete Selected, bulk delete, bulk-edit Save) bring you
// back to the tab you were working on instead of resetting to
// the Abstract tab (the default "active" one in the markup).
const TAB_STORAGE_KEY = "subReportActiveTab";
$(document).on("shown.bs.tab", '[data-bs-toggle="tab"]', function (e) {
let target = $(e.target).attr("data-bs-target"); // e.g. "#tr"
if (target) sessionStorage.setItem(TAB_STORAGE_KEY, target);
});
(function restoreActiveTab() {
let target = sessionStorage.getItem(TAB_STORAGE_KEY);
if (!target) return;
let btn = document.querySelector(`[data-bs-toggle="tab"][data-bs-target="${target}"]`);
if (btn) {
bootstrap.Tab.getOrCreateInstance(btn).show();
}
})();
// Reset // Reset
document.getElementById("resetBtn").addEventListener("click", function () {location.reload();}); document.getElementById("resetBtn").addEventListener("click", function () {
sessionStorage.removeItem(TAB_STORAGE_KEY);
location.reload();
});
// LOCATION -> SUBCONTRACTOR CASCADE
$(document).on("change", "#location", function () {
let location = $(this).val();
let $sc = $("#subcontractor_id");
let currentVal = $sc.val();
$.getJSON("/file/get_subcontractors_by_location", { location: location }, function (data) {
$sc.empty().append('<option value="">--- All Subcontractors ---</option>');
data.forEach(function (sc) {
$sc.append(`<option value="${sc.id}">${sc.name}</option>`);
});
if (data.some(sc => String(sc.id) === String(currentVal))) {
$sc.val(currentVal);
}
}).fail(function () {
alert("Failed to load subcontractors for this location.");
});
});
// DATATABLE // DATATABLE
$(document).ready(function () { // Initialize each table independently and wrap in try/catch: if
$('.datatable').DataTable({ // one table is malformed for any reason, it logs an error but
// does NOT stop the rest of this script (select-all handlers,
// delete handlers, etc.) from registering for the other tables.
// Instances are kept in dtInstances (keyed by model) so bulk-edit
// mode can force "show all rows" and disable search on demand.
const dtInstances = {};
$('.datatable').each(function () {
try {
let model = $(this).find('.select-all-checkbox').data('model');
let dt = $(this).DataTable({
pageLength: 10, pageLength: 10,
dom: 'Bfrtip', dom: 'Bfrtip',
buttons: ['copy', 'csv', 'excel', 'print'] buttons: ['copy', 'csv', 'excel', 'print'],
columnDefs: [
{ orderable: false, targets: [0, -1, -2] } // Select, Update, Delete columns
]
});
if (model) dtInstances[model] = dt;
} catch (err) {
console.error("DataTable init failed for a table:", err);
}
}); });
setTimeout(() => { // SELECT ALL (per table)
$("table thead tr th:first-child").html('<input type="checkbox" id="select-all">'); // The header checkbox is rendered server-side with
}, 500); // class="select-all-checkbox" and data-model="tr|mh|dc|laying"
// (see add_select_all_header in file_report.py), and each row
// checkbox carries the same data-model. Scoping by data-model
// means checking "select all" on the Trench Excavation tab only
// toggles Trench Excavation rows, not the other 3 tables.
$(document).on("change", ".select-all-checkbox", function () {
let model = $(this).data("model");
$(`.row-check[data-model="${model}"]`).prop("checked", this.checked);
}); });
// SELECT ALL // Keep a table's select-all checkbox in sync if a row is
$(document).on("change", "#select-all", function () { // unchecked/checked individually.
$(".row-check").prop("checked", this.checked); $(document).on("change", ".row-check", function () {
let model = $(this).data("model");
let $rows = $(`.row-check[data-model="${model}"]`);
let allChecked = $rows.length > 0 && $rows.length === $rows.filter(":checked").length;
$(`.select-all-checkbox[data-model="${model}"]`).prop("checked", allChecked);
}); });
// GET IDS // ============== BULK EDIT (per table) ==============
function getSelectedIds() { // Each of the 4 tables has its own Bulk Edit / Cancel button
let ids = []; // (data-model="tr|mh|dc|laying"). Editing one table never
$(".row-check:checked").each(function () { // touches the others - each is entered, tracked, saved, and
ids.push($(this).data("id")); // cancelled independently, and multiple tables can be in edit
}); // mode at the same time if you click more than one toggle.
return ids; //
// Every <td data-field="..."> (see add_data_field_attrs in
// file_report.py) is an editable data cell whose data-field
// names the real database column. Tab-pane ids ("tr", "mh",
// "dc", "laying") match the model keys 1:1, so `#${model}` scopes
// every selector below to just that one table.
let bulkEditActiveModels = {};
let pendingBulkChanges = {};
function bulkEditButtons(model) {
return {
$toggle: $(`.bulk-edit-toggle-btn[data-model="${model}"]`),
$cancel: $(`.bulk-edit-cancel-btn[data-model="${model}"]`)
};
} }
// BULK DELETE function enterBulkEditMode(model) {
function deleteSelected(model) { bulkEditActiveModels[model] = true;
let ids = getSelectedIds(); pendingBulkChanges[model] = {};
if (ids.length === 0) return alert("Select records");
if (!confirm(`Delete ${ids.length} record(s)?`)) return; // Show every row and disable search for THIS table only -
// otherwise DataTables pagination/filtering could redraw
// this table using its own stored (unedited) data and
// silently wipe out edits on a row that's since scrolled
// off-page.
let dt = dtInstances[model];
if (dt) {
dt.page.len(-1).draw(false);
$(dt.table().container()).find('.dataTables_filter input').prop('disabled', true);
}
fetch("/file/delete_records", { $(`#${model} td[data-field]`).each(function () {
let original = $(this).text().trim();
$(this).data('original', original);
$(this).empty().append(
$('<input>', {
type: 'text',
class: 'form-control form-control-sm bulk-edit-input',
value: original
})
);
});
let { $toggle, $cancel } = bulkEditButtons(model);
$toggle.html('<i class="bi bi-save"></i> Save All Changes')
.removeClass('btn-outline-primary').addClass('btn-success');
$cancel.removeClass('d-none');
}
function exitBulkEditMode(model, { revert }) {
bulkEditActiveModels[model] = false;
pendingBulkChanges[model] = {};
$(`#${model} td[data-field]`).each(function () {
let $td = $(this);
let $input = $td.find('.bulk-edit-input');
if ($input.length) {
let value = revert ? ($td.data('original') ?? '') : $input.val();
$td.empty().text(value);
}
});
let dt = dtInstances[model];
if (dt) {
dt.page.len(10).draw(false);
$(dt.table().container()).find('.dataTables_filter input').prop('disabled', false);
}
let { $toggle, $cancel } = bulkEditButtons(model);
$toggle.html('<i class="bi bi-pencil-square"></i> Bulk Edit')
.removeClass('btn-success').addClass('btn-outline-primary');
$cancel.addClass('d-none');
}
$(document).on('input', '.bulk-edit-input', function () {
let $td = $(this).closest('td');
let $tr = $(this).closest('tr');
let field = $td.data('field');
let model = $tr.find('.delete-btn').data('model');
let id = $tr.find('.delete-btn').data('id');
if (!model || id === undefined) return;
if (!pendingBulkChanges[model]) pendingBulkChanges[model] = {};
if (!pendingBulkChanges[model][id]) pendingBulkChanges[model][id] = {};
pendingBulkChanges[model][id][field] = $(this).val();
});
function saveBulkChanges(model) {
let changes = pendingBulkChanges[model] || {};
if (Object.keys(changes).length === 0) {
alert("No changes to save.");
return;
}
if (!confirm("Save all changes?")) return;
let payload = {};
payload[model] = changes;
fetch("/file/bulk_update", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: model, ids: ids }) body: JSON.stringify(payload)
}) })
.then(res => res.json().then(data => ({ ok: res.ok, data }))) .then(res => res.json().then(data => ({ ok: res.ok, data })))
.then(({ ok, data }) => { .then(({ ok, data }) => {
if (ok && data.status === "success") { if (ok && data.status === "success") {
alert("Deleted Successfully"); let msg = `Updated ${data.updated} field(s).`;
if (data.errors && data.errors.length) {
msg += "\n\nSome entries were skipped:\n" + data.errors.join("\n");
}
alert(msg);
location.reload(); location.reload();
} else { } else {
alert("Delete failed: " + (data.message || "Unknown error")); alert("Update failed: " + (data.message || "Unknown error"));
} }
}) })
.catch(err => { .catch(err => alert("Update request failed: " + err));
alert("Delete request failed: " + err);
});
} }
$(document).on('click', '.bulk-edit-toggle-btn', function () {
let model = $(this).data('model');
if (!bulkEditActiveModels[model]) {
enterBulkEditMode(model);
} else {
saveBulkChanges(model);
}
});
$(document).on('click', '.bulk-edit-cancel-btn', function () {
let model = $(this).data('model');
exitBulkEditMode(model, { revert: true });
});
// SINGLE DELETE // SINGLE DELETE
$(document).on("click", ".delete-btn", function () { $(document).on("click", ".delete-btn", function () {
let id = $(this).data("id"); let id = $(this).data("id");
@@ -338,6 +584,46 @@
}); });
}); });
});
// GET IDS - scoped to a single table via data-model, so "Delete
// Selected" on one tab never picks up checked rows from another tab.
// (kept global - called from deleteSelected below)
function getSelectedIds(model) {
let ids = [];
$(`.row-check[data-model="${model}"]:checked`).each(function () {
ids.push($(this).data("id"));
});
return ids;
}
// BULK DELETE
// Exposed on window because it's invoked from inline onclick="" attributes
// in the markup above, which always resolve names in the global scope.
window.deleteSelected = function (model) {
let ids = getSelectedIds(model);
if (ids.length === 0) return alert("Select records");
if (!confirm(`Delete ${ids.length} record(s)?`)) return;
fetch("/file/delete_records", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: model, ids: ids })
})
.then(res => res.json().then(data => ({ ok: res.ok, data })))
.then(({ ok, data }) => {
if (ok && data.status === "success") {
alert("Deleted Successfully");
location.reload();
} else {
alert("Delete failed: " + (data.message || "Unknown error"));
}
})
.catch(err => {
alert("Delete request failed: " + err);
});
}
</script> </script>

View File

@@ -0,0 +1,3 @@
2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | ======================================================================
2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | Application Started Successfully
2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | ======================================================================