23 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
e7805b71a8 Added ldap service 2026-08-06 12:43:13 +05:30
aa759c9d96 add ldap confi 2026-08-06 12:24:34 +05:30
bc4b9c778e New rate model added 2026-08-06 12:20:50 +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
24 changed files with 1856 additions and 366 deletions

11
.env
View File

@@ -24,3 +24,14 @@ DB_PASSWORD=root
# DATABASE_URL=mysql+pymysql://root:root@localhost/comparisondb
# -----------------------------
# LDAP Configuration new
# -----------------------------
LDAP_SERVER=ldap://host.docker.internal
LDAP_PORT=389
LDAP_USE_SSL=False
LDAP_DOMAIN=lcepl.org
LDAP_BASE_DN=DC=lcepl,DC=org
LDAP_SEARCH_BASE=OU=Users,DC=lcepl,DC=org

View File

@@ -36,7 +36,8 @@ The Comparison Project is designed to:
## 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)
**ORM**: SQLAlchemy
**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.
**Last Updated:** January 2026
**Last Updated:** April 2026

View File

@@ -38,7 +38,7 @@ def register_blueprints(app):
# new
from app.routes.activity_routes import activity_bp
from app.routes.engineering import engi_bp
from app.routes.engineering_master_routes import engi_bp
app.register_blueprint(auth_bp)
app.register_blueprint(user_bp)

View File

@@ -22,3 +22,13 @@ class Config:
SQLALCHEMY_TRACK_MODIFICATIONS = False
# LDAP Configuration New
LDAP_SERVER = os.getenv("LDAP_SERVER")
LDAP_PORT = int(os.getenv("LDAP_PORT", 389))
LDAP_USE_SSL = os.getenv("LDAP_USE_SSL", "False").lower() == "true"
LDAP_BASE_DN = os.getenv("LDAP_BASE_DN")
LDAP_DOMAIN = os.getenv("LDAP_DOMAIN")
LDAP_SEARCH_BASE = os.getenv("LDAP_SEARCH_BASE")

View File

@@ -0,0 +1,23 @@
from app import db
from datetime import datetime
class SubcontractorRate(db.Model):
__tablename__ = "subcontractor_rates"
id = db.Column(db.Integer, primary_key=True)
subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False)
subcontractor = db.relationship("Subcontractor", backref="rate_master")
category = db.Column(db.String(50), nullable=False)
item_code = db.Column(db.String(50), nullable=False)
item_name = db.Column(db.String(200), nullable=False)
unit = db.Column(db.String(20))
rate = db.Column(db.Numeric(12,2), nullable=False)
effective_from = db.Column(db.Date, nullable=False)
effective_to = db.Column(db.Date)
status = db.Column(db.String(20), default="Active")
created_at = db.Column(db.DateTime, default=datetime.now)
def __repr__(self):
return f"<subcontractor Rate {self.item_name}>"

View File

@@ -34,6 +34,7 @@ def login():
session.clear()
session["user_id"] = user.id
session["user_name"] = user.name
session["email"] = user.email
session.permanent = True
current_app.logger.info(f"Login successful. User={user.name}")

View File

@@ -1,16 +0,0 @@
from flask import Blueprint
from app.models.width_model import Width
engi_bp = Blueprint("engineering",__name__, url_prefix="/engi")
@engi_bp.route("/add")
def add_width_md():
return True
@engi_bp.route("/list")
def display_list():
return True

View File

@@ -0,0 +1,107 @@
from flask import (
Blueprint,
render_template,
request,
redirect,
url_for,
flash, jsonify
)
from app.models.subcontractor_model import Subcontractor
from app.services.subcontractor_rate_service import SubcontractorRateService
from app.constants.messages import SuccessMessage, ErrorMessage
engi_bp = Blueprint(
"engineering",
__name__,
url_prefix="/engi"
)
@engi_bp.route("/")
def engineering_master():
return render_template(
"engineering/index.html",
title="Engineering Masters"
)
# Client rate model
@engi_bp.route("/client-rate")
def client_rates():
return render_template(
"engineering/client_rate.html",
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.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,31 @@ 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)
# ---------------------------------------------------------
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
@@ -256,6 +521,32 @@ def report_file():
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
# ===================================================
@@ -267,16 +558,46 @@ def report_file():
abstract = AbstractReportService(subcontractor_id=subcontractor_id,ra_bill_no=ra_bill_no)
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)
bill.df_dc.to_excel(writer,sheet_name="MH & DC",index=False)
bill.df_laying.to_excel(writer,sheet_name="Pipe Laying",index=False)
sheet_map = [
(bill.df_tr, "Tr.Ex"),
(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()
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(
output,
download_name= "subcontractor_Report.xlsx",
download_name=filename,
as_attachment=True
)
@@ -297,6 +618,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 +643,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
)

View File

@@ -257,18 +257,18 @@ class AbstractReportService:
def laying_summary(self):
f = self.filters()
data = [
("150 mm Pipe","RM",Laying.pipe_150_mm),
("200 mm Pipe","RM",Laying.pipe_200_mm),
("250 mm Pipe","RM",Laying.pipe_250_mm),
("300 mm Pipe","RM",Laying.pipe_300_mm),
("350 mm Pipe","RM",Laying.pipe_350_mm),
("400 mm Pipe","RM",Laying.pipe_400_mm),
("450 mm Pipe","RM",Laying.pipe_450_mm),
("500 mm Pipe","RM",Laying.pipe_500_mm),
("600 mm Pipe","RM",Laying.pipe_600_mm),
("700 mm Pipe","RM",Laying.pipe_700_mm),
("900 mm Pipe","RM",Laying.pipe_900_mm),
("1200 mm Pipe","RM",Laying.pipe_1200_mm),
("150 mm Dia","RM",Laying.pipe_150_mm),
("200 mm Dia","RM",Laying.pipe_200_mm),
("250 mm Dia","RM",Laying.pipe_250_mm),
("300 mm Dia","RM",Laying.pipe_300_mm),
("350 mm Dia","RM",Laying.pipe_350_mm),
("400 mm Dia","RM",Laying.pipe_400_mm),
("450 mm Dia","RM",Laying.pipe_450_mm),
("500 mm Dia","RM",Laying.pipe_500_mm),
("600 mm Dia","RM",Laying.pipe_600_mm),
("700 mm Dia","RM",Laying.pipe_700_mm),
("900 mm Dia","RM",Laying.pipe_900_mm),
("1200 mm Dia","RM",Laying.pipe_1200_mm),
]
return self.make_summary(data, Laying, f)

View File

@@ -0,0 +1,130 @@
from ldap3 import (
Server,
Connection,
ALL,
NTLM,
SIMPLE,
SUBTREE
)
from flask import current_app
from app.config import Config
class LDAPService:
"""
LDAP / Active Directory Authentication Service
"""
@staticmethod
def authenticate(username, password):
"""
Authenticate LDAP User
Returns:
{
"success": True,
"user": {
"username": "...",
"name": "...",
"email": "..."
}
}
OR
{
"success": False,
"message": "Invalid username or password"
}
"""
if not username or not password:
return {
"success": False,
"message": "Username and Password are required."
}
try:
# -----------------------------------
# LDAP SERVER
# -----------------------------------
server = Server(
Config.LDAP_SERVER,
port=Config.LDAP_PORT,
use_ssl=Config.LDAP_USE_SSL,
get_info=ALL
)
# -----------------------------------
# Login Format
#
# username@domain.com
# -----------------------------------
user_dn = f"{username}@{Config.LDAP_DOMAIN}"
conn = Connection(
server,
user=user_dn,
password=password,
authentication=SIMPLE,
auto_bind=True
)
# -----------------------------------
# Search User
# -----------------------------------
search_filter = f"(sAMAccountName={username})"
conn.search(
search_base=Config.LDAP_SEARCH_BASE,
search_filter=search_filter,
search_scope=SUBTREE,
attributes=[
"displayName",
"mail",
"givenName",
"sn",
"cn"
]
)
display_name = username
email = ""
if conn.entries:
entry = conn.entries[0]
if "displayName" in entry:
display_name = str(entry.displayName)
if "mail" in entry:
email = str(entry.mail)
conn.unbind()
current_app.logger.info(
f"LDAP Login Success : {username}"
)
return {
"success": True,
"user": {
"username": username,
"name": display_name,
"email": email
}
}
except Exception as ex:
current_app.logger.warning(
f"LDAP Login Failed : {username} : {str(ex)}"
)
return {
"success": False,
"message": "Invalid Username or Password."
}

View File

@@ -0,0 +1,102 @@
from app.services.db_service import db
from app.models.subcontractor_rate_model import SubcontractorRate
from sqlalchemy import func
class SubcontractorRateService:
@staticmethod
def save_or_update(form):
rate_id = form.get("id")
subcontractor_id = form.get("subcontractor_id")
category = form.get("category")
item_name = form.get("item_name").strip()
# -----------------------------
# Duplicate Validation
# -----------------------------
duplicate = (
SubcontractorRate.query
.filter(
SubcontractorRate.subcontractor_id == subcontractor_id,
SubcontractorRate.category == category,
func.lower(SubcontractorRate.item_name) == item_name.lower()
)
.first()
)
# Ignore current record while editing
if duplicate and (not rate_id or duplicate.id != int(rate_id)):
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
def get_all_rates():
return (
SubcontractorRate.query
.order_by(SubcontractorRate.created_at.desc())
.all()
)
@staticmethod
def get_rate(rate_id):
return SubcontractorRate.query.get_or_404(rate_id)
def delete_rate(rate_id):
rate = SubcontractorRate.query.get_or_404(rate_id)
db.session.delete(rate)
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

@@ -132,7 +132,6 @@
</ul>
</li>
<!-- Formats -->
<li class="nav-item">
<a class="nav-link" href="/file_format">
@@ -140,50 +139,95 @@
</a>
</li>
<!-- USER DROPDOWN -->
{% if session.get("user_id") %}
<li class="nav-item dropdown ms-lg-3">
<!-- Masters -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/engi">
<i class="bi bi-gear me-2"></i> Masters
</a>
<ul class="dropdown-menu dropdown-menu-dark">
<!-- Client Standard Rates -->
<li class="nav-item">
<a class="nav-link" href="/engi/subcontractor-rate">
<i class="bi bi-file-earmark-text me-1"></i> Rate Master
</a>
</li>
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" href="#"
data-bs-toggle="dropdown">
<i class="bi bi-person-circle fs-5"></i>
<span class="d-none d-lg-inline">
<!-- Client Standard Rates -->
<li class="nav-item">
<a class="nav-link" href="/engi/client-rate">
<i class="bi bi-file-earmark-text me-1"></i> Client Rates
</a>
</li>
</ul>
</li>
<!-- USER DROPDOWN -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center text-white"
href="#" id="profileDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle fs-4"></i>
<span class="ms-2 fw-semibold">
{{ session.get("user_name") }}
</span>
</a>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark shadow">
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark border-0 shadow-lg bg-dark p-0"
style="width:320px;">
<!-- User card -->
<li class="px-3 py-3 text-center border-bottom">
<i class="bi bi-person-circle fs-1"></i>
<div class="fw-semibold mt-1">
<!-- Profile Header -->
<li class="text-center py-4 border-bottom border-secondary">
<i class="bi bi-person-circle text-light" style="font-size:60px;"></i>
<h5 class="mt-2 mb-0 fw-bold">
{{ session.get("user_name") }}
</div>
<small class="text-muted">Logged in user</small>
</h5>
<small class="text-secondary">
{{ session.get("email") }}
</small>
</li>
<!-- Masters -->
<li>
<a class="dropdown-item" href="/dashboard">
<i class="bi bi-speedometer2 me-2"></i> Dashboard
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('activity.activity') }}">
<i class="bi bi-clock-history me-2"></i>
Activity Log
<a class="dropdown-item text-light py-2" href="{{ url_for('engineering.engineering_master') }}">
<i class="bi bi-gear me-2"></i> Masters
</a>
</li>
<!-- Activity Log page -->
<li>
<a class="dropdown-item text-warning" href="/logout">
<i class="bi bi-box-arrow-right me-2"></i> Logout
<a class="dropdown-item text-light py-2" href="{{ url_for('activity.activity') }}">
<i class="bi bi-clock-history me-2"></i> Activity Log
</a>
</li>
<!-- Manage Account
<li>
<a class="dropdown-item py-2" href="#">
<i class="bi bi-gear me-2"></i> Manage Account
</a>
</li>
-->
<li><hr class="dropdown-divider border-secondary m-0"></li>
<!-- Logout Account -->
<li>
<a class="dropdown-item text-warning py-2" href="{{ url_for('auth.logout') }}">
<i class="bi bi-box-arrow-right me-2"></i>
Logout
</a>
</li>
<!-- Footer -->
<li class="text-center py-3 bg-secondary bg-opacity-10 border-top border-secondary">
<small class="text-light">
<i class="bi bi-shield-check text-success"></i>
Secured by <strong>LCEPL</strong>
</small>
</li>
</ul>
</li>
{% endif %}
</ul>
</div>
@@ -336,6 +380,7 @@
showLoader();
const btn = this.querySelector("button[type='submit']");
const originalBtnHtml = btn ? btn.innerHTML : null;
if(btn){
btn.disabled = true;
@@ -345,10 +390,24 @@
`;
}
setTimeout(function () {
hideLoader();
if (btn) {
btn.disabled = false;
btn.innerHTML = originalBtnHtml;
}
}, 3000);
});
});
});
window.addEventListener("pageshow", function () {
hideLoader();
});
</script>

View File

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

View File

@@ -1,50 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="card shadow-sm p-4">
<h4 class="mb-3">Add New Subcontractor</h4>
<form action="{{ url_for('subcontractor.save_subcontractor') }}" method="POST">
<div class="mb-3">
<label class="form-label">Subcontractor Name:</label>
<input type="text" class="form-control" name="subcontractor_name" required>
</div>
<div class="mb-3">
<label class="form-label">Contact Person Name:</label>
<input type="text" class="form-control" name="contact_person">
</div>
<div class="mb-3">
<label class="form-label">Address:</label>
<textarea type="text" class="form-control" name="address"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Mobile No:</label>
<input type="text" class="form-control" name="mobile_no">
</div>
<div class="mb-3">
<label class="form-label">Email:</label>
<input type="email" class="form-control" name="email_id">
</div>
<div class="mb-3">
<label class="form-label">GST No:</label>
<input type="text" class="form-control" name="gst_no">
</div>
<div class="mb-3">
<label class="form-label">PAN No:</label>
<input type="text" class="form-control" name="pan_no">
</div>
<button class="btn btn-success">Save</button>
<a href="{{ url_for('subcontractor.subcontractor_list') }}" class="btn btn-secondary">Back</a>
</form>
</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

@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-12">
<h3 class="fw-bold">
<i class="bi bi-gear-fill text-primary"></i>
Engineering Masters
</h3>
<hr>
</div>
</div>
<div class="row g-4">
<!-- Subcontractor Rate Master -->
<div class="col-lg-4 col-md-6">
<div class="card shadow h-100">
<div class="card-body text-center">
<i class="bi bi-cash-stack text-success"
style="font-size:55px;"></i>
<h5 class="mt-3">
Subcontractor Rate Master
</h5>
<p class="text-muted">
Manage subcontractor-wise rates.
</p>
<a href="{{ url_for('engineering.add_subcontractor_rates') }}"
class="btn btn-success">
<i class="bi bi-arrow-right-circle"></i>
Open Module
</a>
</div>
</div>
</div>
<!-- Client Rate Master -->
<div class="col-lg-4 col-md-6">
<div class="card shadow h-100">
<div class="card-body text-center">
<i class="bi bi-building text-primary"
style="font-size:55px;"></i>
<h5 class="mt-3">
Client Standard Rate Master
</h5>
<p class="text-muted">
Manage client standard rates.
</p>
<a href="{{ url_for('engineering.client_rates') }}"
class="btn btn-primary">
<i class="bi bi-arrow-right-circle"></i>
Open Module
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -84,10 +84,11 @@
</span>
<input
type="email"
type="text"
name="email"
class="form-control"
placeholder="Enter Email"
placeholder="Enter Domain Username"
autocomplete="username"
required>
</div>

View File

@@ -1,50 +1,170 @@
{% extends "base.html" %}
{% block content %}
<div class="card shadow-sm p-4">
<h4 class="mb-3">Add New Subcontractor</h4>
<div class="container-fluid">
<form action="{{ url_for('subcontractor.save_subcontractor') }}" method="POST">
<div class="mb-3">
<label class="form-label">Subcontractor Name:</label>
<input type="text" class="form-control" name="subcontractor_name" required>
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h3 class="fw-bold mb-1">
<i class="bi bi-person-plus-fill text-success me-2"></i>
Add New Subcontractor
</h3>
<p class="text-muted mb-0">
Enter the subcontractor details below.
</p>
</div>
<div class="mb-3">
<label class="form-label">Contact Person Name:</label>
<input type="text" class="form-control" name="contact_person">
<a href="{{ url_for('subcontractor.subcontractor_list') }}"
class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>
Back to List
</a>
</div>
<div class="mb-3">
<label class="form-label">Address:</label>
<textarea type="text" class="form-control" name="address"></textarea>
<div class="card shadow border-0">
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="bi bi-building-fill-add me-2"></i>
Subcontractor Information
</h5>
</div>
<div class="mb-3">
<label class="form-label">Mobile No:</label>
<input type="text" class="form-control" name="mobile_no">
<div class="card-body">
<form action="{{ url_for('subcontractor.save_subcontractor') }}"
method="POST"
class="loading-form">
<div class="row g-4">
<!-- Subcontractor Name -->
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="bi bi-building me-1"></i>
Subcontractor Name
<span class="text-danger">*</span>
</label>
<input type="text"
class="form-control"
name="subcontractor_name"
placeholder="Enter Subcontractor Name"
required>
</div>
<div class="mb-3">
<label class="form-label">Email:</label>
<input type="email" class="form-control" name="email_id">
<!-- Contact Person -->
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="bi bi-person-fill me-1"></i>
Contact Person
</label>
<input type="text"
class="form-control"
name="contact_person"
placeholder="Enter Contact Person">
</div>
<div class="mb-3">
<label class="form-label">GST No:</label>
<input type="text" class="form-control" name="gst_no">
<!-- Mobile -->
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="bi bi-telephone-fill me-1"></i>
Mobile Number
</label>
<input type="text"
class="form-control"
name="mobile_no"
maxlength="10"
placeholder="Enter Mobile Number">
</div>
<div class="mb-3">
<label class="form-label">PAN No:</label>
<input type="text" class="form-control" name="pan_no">
<!-- Email -->
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="bi bi-envelope-fill me-1"></i>
Email Address
</label>
<input type="email"
class="form-control"
name="email_id"
placeholder="Enter Email Address">
</div>
<button class="btn btn-success">Save</button>
<a href="{{ url_for('subcontractor.subcontractor_list') }}" class="btn btn-secondary">Back</a>
<!-- GST -->
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="bi bi-receipt-cutoff me-1"></i>
GST Number
</label>
<input type="text"
class="form-control"
name="gst_no"
placeholder="Enter GST Number">
</div>
<!-- PAN -->
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="bi bi-credit-card-2-front-fill me-1"></i>
PAN Number
</label>
<input type="text"
class="form-control"
name="pan_no"
placeholder="Enter PAN Number">
</div>
<!-- Address -->
<div class="col-12">
<label class="form-label fw-semibold">
<i class="bi bi-geo-alt-fill me-1"></i>
Address
</label>
<textarea class="form-control"
rows="4"
name="address"
placeholder="Enter Complete Address"></textarea>
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-end gap-2">
<button type="reset"
class="btn btn-outline-warning">
<i class="bi bi-arrow-clockwise me-2"></i>
Reset
</button>
<a href="{{ url_for('subcontractor.subcontractor_list') }}"
class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-2"></i>
Cancel
</a>
<button type="submit"
class="btn btn-success">
<i class="bi bi-check-circle-fill me-2"></i>
Save Subcontractor
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -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">
@@ -88,7 +101,7 @@
<option value="dc"
{% if request.form.get('category')=='dc' %}selected{% endif %}>
Domestic Chamber
Manhole Domestic Chamber
</option>
<option value="laying"
@@ -155,99 +168,148 @@
</div>
{% if tables %}
{% set show_all = (not selected_category) or selected_category == 'all' %}
<!-- Tabs -->
<div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-light">
<ul class="nav nav-pills">
<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
</button>
</li>
{% if show_all or selected_category == 'tr' %}
<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
</button>
</li>
{% endif %}
{% if show_all or selected_category == 'mh' %}
<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
</button>
</li>
{% endif %}
{% if show_all or selected_category == 'dc' %}
<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
</button>
</li>
{% endif %}
{% if show_all or selected_category == 'laying' %}
<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
</button>
</li>
{% endif %}
</ul>
</div>
<div class="card-body">
<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 }}
</div>
{% if show_all or selected_category == 'tr' %}
<!-- Trench -->
<div class="tab-pane fade "id="tr">
<div class="mb-3">
<div class="tab-pane fade {% if selected_category == 'tr' %}show active{% endif %}" id="tr">
{% 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>
</div>
{% endif %}
{% if show_all or selected_category == 'mh' %}
<!-- MH -->
<div class="tab-pane fade" id="mh">
<div class="mb-3">
<div class="tab-pane fade {% if selected_category == 'mh' %}show active{% endif %}" id="mh">
{% 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>
</div>
{% endif %}
{% if show_all or selected_category == 'dc' %}
<!-- DC -->
<div class="tab-pane fade" id="dc">
<div class="mb-3">
<div class="tab-pane fade {% if selected_category == 'dc' %}show active{% endif %}" id="dc">
{% 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>
</div>
{% endif %}
{% if show_all or selected_category == 'laying' %}
<!-- Laying -->
<div class="tab-pane fade" id="laying">
<div class="mb-3">
<div class="tab-pane fade {% if selected_category == 'laying' %}show active{% endif %}" id="laying">
{% 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>
</div>
{% endif %}
</div>
</div>
</div>
@@ -256,62 +318,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 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 +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>

View File

@@ -1,119 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-3">
<h4>RA Bill Dashboard</h4>
<div class="row mb-3">
<!-- Contractor -->
<div class="col-md-4">
<select id="subcontractor" class="form-control">
<option value="">Select Contractor</option>
{% for s in subcontractors %}
<option value="{{s.id}}">{{s.subcontractor_name}}</option>
{% endfor %}
</select>
</div>
<!-- Category -->
<div class="col-md-4">
<select id="category" class="form-control">
<option value="">Select Category</option>
<option value="trench_excavation">Trench Excavation</option>
<option value="manhole_excavation">Manhole Excavation</option>
</select>
</div>
<!-- RA Bill -->
<div class="col-md-4">
<select id="ra_bill" class="form-control">
<option value="">RA Bill</option>
</select>
</div>
</div>
<div class="card">
<div class="card-body">
<canvas id="comparisonChart"></canvas>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let chart;
// ✅ Load RA Bills
function loadRABills() {
let subcontractor = document.getElementById("subcontractor").value
let category = document.getElementById("category").value
if (!subcontractor || !category) return
fetch(`/dashboard/api/get-ra-bills?subcontractor=${subcontractor}&category=${category}`)
.then(res => res.json())
.then(data => {
let ra = document.getElementById("ra_bill")
ra.innerHTML = '<option value="">RA Bill</option>'
data.ra_bills.forEach(bill => {
ra.innerHTML += `<option value="${bill}">${bill}</option>`
})
})
}
// ✅ Load Chart
function loadChart() {
let subcontractor = document.getElementById("subcontractor").value
let ra_bill = document.getElementById("ra_bill").value
fetch(`/dashboard/api/trench-analysis?subcontractor=${subcontractor}&ra_bill=${ra_bill}`)
.then(res => res.json())
.then(data => {
if (chart) chart.destroy()
chart = new Chart(document.getElementById("comparisonChart"), {
type: "bar",
data: {
labels: data.labels,
datasets: [
{
label: "Depth",
data: data.depth,
backgroundColor: "green"
},
{
label: "Excavation Qty (cum)",
data: data.qty,
backgroundColor: "blue"
}
]
}
})
})
}
// Events
document.getElementById("subcontractor").addEventListener("change", () => {
loadRABills()
})
document.getElementById("category").addEventListener("change", () => {
loadRABills()
})
document.getElementById("ra_bill").addEventListener("change", loadChart)
</script>
{% endblock %}

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=- | - | - | ======================================================================