suctr location filter, MH NO. search, subctr manadatory field removed, added select all for delete and also all edit at once feature added
This commit is contained in:
@@ -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.regex_utils import RegularExpression
|
||||
from app import db
|
||||
from sqlalchemy import func
|
||||
import re
|
||||
|
||||
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")
|
||||
|
||||
|
||||
# ---------------- 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 ----------------
|
||||
def add_action_columns(df, model_key):
|
||||
if df.empty:
|
||||
return df
|
||||
|
||||
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(
|
||||
@@ -44,6 +112,76 @@ def add_action_columns(df, model_key):
|
||||
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_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 = {}
|
||||
if subcontractor_id:
|
||||
@@ -73,7 +211,6 @@ class SubcontractorBill:
|
||||
# LOCATION FILTER
|
||||
if location:
|
||||
search = location.strip().lower()
|
||||
print("location::",search)
|
||||
trench = [
|
||||
t for t in trench
|
||||
if search in (t.Location or "").strip().lower()
|
||||
@@ -94,6 +231,29 @@ class SubcontractorBill:
|
||||
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
|
||||
self.df_tr = pd.DataFrame([c.serialize() for c in trench])
|
||||
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"]
|
||||
|
||||
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:
|
||||
df.drop(columns=drop_cols, errors="ignore", inplace=True)
|
||||
setattr(self, attr, list(df.columns))
|
||||
format_column_names(df)
|
||||
|
||||
name = ""
|
||||
@@ -152,6 +327,60 @@ def delete_records():
|
||||
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"])
|
||||
@login_required
|
||||
def edit_record(model, record_id):
|
||||
@@ -204,28 +433,40 @@ def edit_record(model, record_id):
|
||||
def report_file():
|
||||
# get all subcontractor data
|
||||
subcontractors = Subcontractor.query.all()
|
||||
locations = get_distinct_locations()
|
||||
|
||||
tables = None
|
||||
abstract_html = ""
|
||||
selected_sc_id = None
|
||||
has_data = {"tr": False, "mh": False, "dc": False, "laying": False}
|
||||
ra_bill_no = ""
|
||||
location = ""
|
||||
mh_no = ""
|
||||
category = ""
|
||||
|
||||
# Search or load data
|
||||
if request.method == "POST":
|
||||
# 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()
|
||||
location = request.form.get("location", "").strip()
|
||||
mh_no = request.form.get("mh_no", "").strip()
|
||||
category = request.form.get("category", "")
|
||||
action = request.form.get("action", "preview")
|
||||
|
||||
if not subcontractor_id:
|
||||
flash("Select Subcontractor", "danger")
|
||||
# Keep the subcontractor dropdown scoped to the chosen location
|
||||
# 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(
|
||||
"subcontractor_report.html",
|
||||
subcontractors=subcontractors
|
||||
subcontractors=subcontractors,
|
||||
locations=locations,
|
||||
has_data=has_data
|
||||
)
|
||||
|
||||
selected_sc_id = subcontractor_id
|
||||
@@ -234,7 +475,7 @@ def report_file():
|
||||
if action == "excel_all":
|
||||
bill.Fetch(subcontractor_id=subcontractor_id)
|
||||
else:
|
||||
bill.Fetch(ra_bill_no,subcontractor_id,location)
|
||||
bill.Fetch(ra_bill_no, subcontractor_id, location, mh_no)
|
||||
|
||||
# -----------------------------------------
|
||||
# Generate Abstract Report for Web
|
||||
@@ -297,6 +538,15 @@ def report_file():
|
||||
bill.df_dc = add_action_columns(bill.df_dc, "dc")
|
||||
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
|
||||
# table_class = ( "table " "table-bordered" "table-hover " "table-striped " "table-sm " "align-middle " "datatable " "mb-0")
|
||||
table_class = (
|
||||
@@ -313,21 +563,24 @@ def report_file():
|
||||
|
||||
# This are showing on web tables
|
||||
tables = {
|
||||
"tr": bill.df_tr.to_html(classes=table_class, index=False, escape=False),
|
||||
"mh": bill.df_mh.to_html(classes=table_class, index=False, escape=False),
|
||||
"dc": bill.df_dc.to_html(classes=table_class, index=False, escape=False ),
|
||||
"laying": bill.df_laying.to_html(classes=table_class, index=False, escape=False)
|
||||
"tr": render_table_or_empty(bill.df_tr, "tr", table_class, bill.tr_fields),
|
||||
"mh": render_table_or_empty(bill.df_mh, "mh", table_class, bill.mh_fields),
|
||||
"dc": render_table_or_empty(bill.df_dc, "dc", table_class, bill.dc_fields),
|
||||
"laying": render_table_or_empty(bill.df_laying, "laying", table_class, bill.laying_fields)
|
||||
}
|
||||
|
||||
return render_template(
|
||||
"subcontractor_report.html",
|
||||
subcontractors=subcontractors,
|
||||
locations=locations,
|
||||
selected_sc_id=selected_sc_id,
|
||||
selected_ra_bill=ra_bill_no,
|
||||
selected_location=location,
|
||||
selected_mh_no=mh_no,
|
||||
selected_category=category,
|
||||
tables=tables,
|
||||
abstract_html=abstract_html
|
||||
abstract_html=abstract_html,
|
||||
has_data=has_data
|
||||
)
|
||||
|
||||
|
||||
@@ -360,78 +613,35 @@ class ClientBill:
|
||||
# --- 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)
|
||||
return render_template("client_report.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)
|
||||
# -------- FETCH CLIENT DATA --------
|
||||
bill_gen = ClientBill()
|
||||
bill_gen.Fetch(RA_Bill_No)
|
||||
|
||||
# --- SAFETY CHECK: Verify data exists before merging ---
|
||||
if clientBill.df_tr.empty and clientBill.df_mh.empty:
|
||||
# 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("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 =[...]
|
||||
|
||||
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("client_report.html", tables=tables, ra_val=ra_val)
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
=======
|
||||
|
||||
# -------- DOWNLOAD --------
|
||||
if action == "download":
|
||||
@@ -550,4 +760,3 @@ def format_column_names(df):
|
||||
df.columns = new_columns
|
||||
|
||||
return df
|
||||
>>>>>>> pankaj-dev
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
type="button">Tr.Ex </button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<<<<<<< HEAD
|
||||
<button class="nav-link" id="mh-tab" data-bs-toggle="tab" data-bs-target="#mh" type="button">Mh.Ex
|
||||
</button>
|
||||
</li>
|
||||
@@ -40,26 +39,9 @@
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<<<<<<< HEAD
|
||||
<button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying"
|
||||
type="button">Laying
|
||||
& Bedding Comparison</button>
|
||||
=======
|
||||
<button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying" type="button">Laying
|
||||
& Bedding </button>
|
||||
>>>>>>> 1dceb640bd930c37888799f10f02fe90b219be67
|
||||
=======
|
||||
<button class="nav-link" id="mh-tab" data-bs-toggle="tab" data-bs-target="#mh" type="button">
|
||||
Mh.Ex</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" id="dc-tab" data-bs-toggle="tab" data-bs-target="#dc" type="button">
|
||||
MH & DC</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying"type="button">
|
||||
Laying & Bedding </button>
|
||||
>>>>>>> pankaj-dev
|
||||
</ul>
|
||||
|
||||
<div class="tab-content mt-3" id="reportTabsContent">
|
||||
|
||||
@@ -33,13 +33,26 @@
|
||||
<form method="POST" class="loading-form">
|
||||
|
||||
<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">
|
||||
<label class="form-label fw-semibold">
|
||||
Subcontractor
|
||||
<span class="text-danger">*</span>
|
||||
<span class="text-muted fw-normal"></span>
|
||||
</label>
|
||||
<select name="subcontractor_id" class="form-select" required>
|
||||
<option value="">--- Select Contractor ---</option>
|
||||
<select name="subcontractor_id" id="subcontractor_id" class="form-select">
|
||||
<option value="">--- All Subcontractors ---</option>
|
||||
{% for sc in subcontractors %}
|
||||
<option value="{{ sc.id }}"
|
||||
{% if selected_sc_id|string == sc.id|string %}selected{% endif %}>
|
||||
@@ -61,13 +74,13 @@
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label fw-semibold"> Location </label>
|
||||
<label class="form-label fw-semibold"> MH No </label>
|
||||
<input
|
||||
type="text"
|
||||
name="location"
|
||||
name="mh_no"
|
||||
class="form-control"
|
||||
placeholder="Project Location"
|
||||
value="{{ selected_location or '' }}">
|
||||
placeholder="Enter MH No"
|
||||
value="{{ selected_mh_no or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
@@ -200,11 +213,19 @@
|
||||
|
||||
<!-- Trench -->
|
||||
<div class="tab-pane fade "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">
|
||||
<i class="bi bi-trash"></i> Delete Selected
|
||||
</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>
|
||||
{% endif %}
|
||||
<div class="table-responsive border rounded shadow-sm">
|
||||
{{ tables.tr|safe }}
|
||||
</div>
|
||||
@@ -212,11 +233,19 @@
|
||||
|
||||
<!-- MH -->
|
||||
<div class="tab-pane fade" 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">
|
||||
<i class="bi bi-trash"></i>Delete Selected
|
||||
</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>
|
||||
{% endif %}
|
||||
<div class="table-responsive border rounded shadow-sm">
|
||||
{{ tables.mh|safe }}
|
||||
</div>
|
||||
@@ -224,12 +253,20 @@
|
||||
|
||||
<!-- DC -->
|
||||
<div class="tab-pane fade" 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">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete Selected
|
||||
</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>
|
||||
{% endif %}
|
||||
<div class="table-responsive border rounded shadow-sm">
|
||||
{{ tables.dc|safe }}
|
||||
</div>
|
||||
@@ -237,13 +274,21 @@
|
||||
|
||||
<!-- Laying -->
|
||||
<div class="tab-pane fade" id="laying">
|
||||
<div class="mb-3">
|
||||
{% if has_data.laying %}
|
||||
<div class="mb-3 d-flex gap-2 flex-wrap">
|
||||
<button onclick="deleteSelected('laying')"
|
||||
class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete Selected
|
||||
</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>
|
||||
{% endif %}
|
||||
<div class="table-responsive border rounded shadow-sm">
|
||||
{{ tables.laying|safe }}
|
||||
</div>
|
||||
@@ -256,62 +301,246 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
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
|
||||
$(document).ready(function () {
|
||||
$('.datatable').DataTable({
|
||||
// Initialize each table independently and wrap in try/catch: if
|
||||
// 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,
|
||||
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(() => {
|
||||
$("table thead tr th:first-child").html('<input type="checkbox" id="select-all">');
|
||||
}, 500);
|
||||
// SELECT ALL (per table)
|
||||
// The header checkbox is now rendered server-side with
|
||||
// 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
|
||||
$(document).on("change", "#select-all", function () {
|
||||
$(".row-check").prop("checked", this.checked);
|
||||
// Keep a table's select-all checkbox in sync if a row is
|
||||
// unchecked/checked individually.
|
||||
$(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
|
||||
function getSelectedIds() {
|
||||
let ids = [];
|
||||
$(".row-check:checked").each(function () {
|
||||
ids.push($(this).data("id"));
|
||||
});
|
||||
return ids;
|
||||
// ============== BULK EDIT (per table) ==============
|
||||
// Each of the 4 tables has its own Bulk Edit / Cancel button
|
||||
// (data-model="tr|mh|dc|laying"). Editing one table never
|
||||
// touches the others - each is entered, tracked, saved, and
|
||||
// cancelled independently, and multiple tables can be in edit
|
||||
// mode at the same time if you click more than one toggle.
|
||||
//
|
||||
// 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 deleteSelected(model) {
|
||||
let ids = getSelectedIds();
|
||||
if (ids.length === 0) return alert("Select records");
|
||||
function enterBulkEditMode(model) {
|
||||
bulkEditActiveModels[model] = true;
|
||||
pendingBulkChanges[model] = {};
|
||||
|
||||
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",
|
||||
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(({ ok, data }) => {
|
||||
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();
|
||||
} else {
|
||||
alert("Delete failed: " + (data.message || "Unknown error"));
|
||||
alert("Update failed: " + (data.message || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert("Delete request failed: " + err);
|
||||
});
|
||||
.catch(err => alert("Update 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
|
||||
$(document).on("click", ".delete-btn", function () {
|
||||
let id = $(this).data("id");
|
||||
@@ -338,6 +567,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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user