46 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
e98e1422a0 Add Activity log Service and model 2026-08-01 15:41:26 +05:30
2d7d146ec3 All model filter are work and show bar chat and data table. 2026-07-31 16:43:38 +05:30
604e948986 correction of comparison data show 2026-07-31 12:44:09 +05:30
46d11ec01f correction of table showing data 2026-07-30 17:05:33 +05:30
bf1df3a817 update code of dash and report of cont_client and devlp abstract of qty 2026-07-30 14:38:37 +05:30
5577576112 Update README.md
update readme file
2026-04-07 11:42:23 +00:00
12b2032430 dabase column field type changes 2026-03-20 16:25:38 +05:30
6e4406519a same changes of sub-cont dashboard 2026-03-18 17:18:05 +05:30
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
1d83534a95 updateds folder path and regex 2026-03-18 10:19:11 +05:30
446778a50c Merge pull request 'pankaj-dev' (#13) from pankaj-dev into main
Reviewed-on: #13
2026-03-17 12:54:08 +00:00
5ddfb1f440 regular exp updated on model 2026-03-17 18:02:28 +05:30
96a3a79731 create regular exp added and change model 2026-03-17 15:35:52 +05:30
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
c5b3e7bd60 dataase field updateds and model updateds 2026-03-11 13:20:49 +05:30
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
21f8576e33 client downlod report rewise data 2026-02-28 15:56:12 +05:30
5ab11bc5a6 added pagination - subconractor mdel 2026-02-28 11:07:38 +05:30
45bfd4b592 added new of dashboard and log apply on routes 2026-02-26 17:08:49 +05:30
568428b5d0 change of comparison report of 4 model update 2026-02-26 12:44:04 +05:30
cf7d1636f9 model class update total cal fields all model 2026-02-25 15:27:59 +05:30
163c7814ed added Docker file and docker compose yml files 2026-02-21 15:00:05 +05:30
5b557efd80 added new changes 2026-01-23 13:09:51 +05:30
71ce8ca819 comaparison code optimization and cal sum/total mh dc and laying 2026-01-20 16:34:16 +05:30
68a694d2c7 comaparison code optimization and cal sum/total mh dc and laying 2026-01-20 16:33:45 +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
90c18383da Merge pull request 'subcontractor dashboard ui page create' (#9) from pankaj-dev into main
Reviewed-on: #9
2026-01-17 12:12:03 +00:00
ac9d964ef4 subcontractor dashboard ui page create 2026-01-17 17:39:31 +05:30
f22dc0cccd Merge pull request 'client laying and bedding in ui added' (#8) from dev-anish into main
Reviewed-on: #8
2026-01-17 12:05:47 +00:00
71 changed files with 6607 additions and 1362 deletions

19
.dockerignore Normal file
View File

@@ -0,0 +1,19 @@
.git
.gitignore
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
*.egg-info/
dist/
build/
.vscode/
.idea/
*.log
.env
instance/
.pytest_cache/
.coverage

15
.env
View File

@@ -3,8 +3,8 @@
# ----------------------------- # -----------------------------
FLASK_ENV=development FLASK_ENV=development
FLASK_DEBUG=True FLASK_DEBUG=True
FLASK_HOST='0.0.0.0' FLASK_HOST=0.0.0.0
FLASK_PORT=5001 FLASK_PORT=5015
# ----------------------------- # -----------------------------
# Security # Security
@@ -24,3 +24,14 @@ DB_PASSWORD=root
# DATABASE_URL=mysql+pymysql://root:root@localhost/comparisondb # 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

5
.gitignore vendored
View File

@@ -13,8 +13,7 @@ venv
# Ignore Log files ss # Ignore Log files ss
logs/ logs/
*.log
# Ignore db folders
instance/

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create necessary directories
RUN mkdir -p app/logs app/static/uploads app/static/downloads
# Expose port
EXPOSE 5001
# Run the application
CMD ["python", "run.py"]

View File

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

View File

@@ -1,6 +1,7 @@
from flask import Flask, redirect, url_for from flask import Flask, redirect, url_for
from app.config import Config from app.config import Config
from app.services.db_service import db from app.services.db_service import db
from app.services.logger_service import LoggerService
def create_app(): def create_app():
app = Flask(__name__) app = Flask(__name__)
@@ -9,6 +10,9 @@ def create_app():
# Initialize extensions # Initialize extensions
db.init_app(app) db.init_app(app)
# Initialize Logger
LoggerService.init_app(app)
# Register blueprints # Register blueprints
register_blueprints(app) register_blueprints(app)
# Register error handlers # Register error handlers
@@ -24,7 +28,7 @@ def create_app():
def register_blueprints(app): def register_blueprints(app):
from app.routes.auth import auth_bp from app.routes.auth import auth_bp
from app.routes.user import user_bp from app.routes.user_routes import user_bp
from app.routes.dashboard import dashboard_bp from app.routes.dashboard import dashboard_bp
from app.routes.subcontractor_routes import subcontractor_bp from app.routes.subcontractor_routes import subcontractor_bp
from app.routes.file_import import file_import_bp from app.routes.file_import import file_import_bp
@@ -32,21 +36,34 @@ def register_blueprints(app):
from app.routes.generate_comparison_report import generate_report_bp from app.routes.generate_comparison_report import generate_report_bp
from app.routes.file_format import file_format_bp from app.routes.file_format import file_format_bp
app.register_blueprint(auth_bp) # new
from app.routes.activity_routes import activity_bp
from app.routes.engineering_master_routes import engi_bp
app.register_blueprint(auth_bp)
app.register_blueprint(user_bp) app.register_blueprint(user_bp)
app.register_blueprint(dashboard_bp) app.register_blueprint(dashboard_bp)
app.register_blueprint(subcontractor_bp) app.register_blueprint(subcontractor_bp)
app.register_blueprint(file_import_bp) app.register_blueprint(file_import_bp)
app.register_blueprint(file_report_bp) app.register_blueprint(file_report_bp)
app.register_blueprint(generate_report_bp) app.register_blueprint(generate_report_bp)
app.register_blueprint(file_format_bp ) app.register_blueprint(file_format_bp)
# new
app.register_blueprint(activity_bp)
app.register_blueprint(engi_bp)
def register_error_handlers(app): def register_error_handlers(app):
from flask import current_app
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
current_app.logger.warning("404 Page Not Found")
return "Page Not Found", 404 return "Page Not Found", 404
@app.errorhandler(500) @app.errorhandler(500)
def internal_error(e): def internal_error(e):
return "Internal Server Error", 500 current_app.logger.exception("500 Internal Server Error")
return "Internal Server Error", 500

View File

@@ -1,12 +1,10 @@
import os import os
# project base url
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
class Config: class Config:
# secret key # secret key
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key") SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key")
# Database varibles # Database variables
DB_DIALECT = os.getenv("DB_DIALECT") DB_DIALECT = os.getenv("DB_DIALECT")
DB_DRIVER = os.getenv("DB_DRIVER") DB_DRIVER = os.getenv("DB_DRIVER")
DB_USER = os.getenv("DB_USER") DB_USER = os.getenv("DB_USER")
@@ -23,7 +21,14 @@ class Config:
) )
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
# uploads folder path
UPLOAD_FOLDER = os.path.join(BASE_DIR, "static", "uploads")
# file extension # LDAP Configuration New
ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"} 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,29 @@
class HTTPStatus:
# ==========================
# 2xx Success
# ==========================
OK = 200 # Request successful
CREATED = 201 # Resource created successfully
ACCEPTED = 202 # Request accepted for processing
NO_CONTENT = 204 # Success, no response body
# ==========================
# 4xx Client Errors
# ==========================
BAD_REQUEST = 400 # Invalid request
UNAUTHORIZED = 401 # Authentication required
FORBIDDEN = 403 # Access denied
NOT_FOUND = 404 # Resource not found
METHOD_NOT_ALLOWED = 405 # HTTP method not allowed
CONFLICT = 409 # Resource conflict (e.g., duplicate)
UNPROCESSABLE_ENTITY = 422 # Validation error
TOO_MANY_REQUESTS = 429 # Rate limit exceeded
# ==========================
# 5xx Server Errors
# ==========================
INTERNAL_SERVER_ERROR = 500 # Internal server error
NOT_IMPLEMENTED = 501 # Feature not implemented
BAD_GATEWAY = 502 # Invalid response from upstream server
SERVICE_UNAVAILABLE = 503 # Server temporarily unavailable
GATEWAY_TIMEOUT = 504 # Upstream server timeout

65
app/constants/messages.py Normal file
View File

@@ -0,0 +1,65 @@
class SuccessMessage:
FETCHED = "Data fetched successfully."
CREATED = "Resource created successfully."
UPDATED = "Resource updated successfully."
DELETED = "Resource deleted successfully."
SAVED = "Data saved successfully."
IMPORTED = "Data imported successfully."
EXPORTED = "Data exported successfully."
UPLOADED = "File uploaded successfully."
DOWNLOADED = "File downloaded successfully."
LOGIN = "Login successful."
LOGOUT = "Logout successful."
PASSWORD_CHANGED = "Password changed successfully."
PASSWORD_RESET = "Password reset successfully."
EMAIL_SENT = "Email sent successfully."
STATUS_UPDATED = "Status updated successfully."
class ErrorMessage:
INVALID_REQUEST = "Invalid request."
INVALID_DATA = "Invalid data provided."
VALIDATION_FAILED = "Validation failed."
UNAUTHORIZED = "Unauthorized access."
FORBIDDEN = "Access denied."
NOT_FOUND = "Resource not found."
METHOD_NOT_ALLOWED = "Method not allowed."
DUPLICATE_ENTRY = "Duplicate record found."
RECORD_EXISTS = "Record already exists."
RECORD_NOT_FOUND = "Record does not exist."
FILE_NOT_FOUND = "File not found."
FILE_UPLOAD_FAILED = "File upload failed."
FILE_IMPORT_FAILED = "File import failed."
DATABASE_ERROR = "Database operation failed."
INTERNAL_SERVER_ERROR = "Internal server error."
SERVICE_UNAVAILABLE = "Service temporarily unavailable."
LOGIN_FAILED = "Invalid username or password."
SESSION_EXPIRED = "Session expired. Please login again."
PASSWORD_MISMATCH = "Passwords do not match."
INVALID_TOKEN = "Invalid or expired token."
class WarningMessage:
NO_DATA_FOUND = "No data found."
ALREADY_EXISTS = "Record already exists."
UNSAVED_CHANGES = "You have unsaved changes."
DELETE_CONFIRMATION = "Are you sure you want to delete the selected record(s)?"
INVALID_FILTER = "No records match the selected filters."
class InfoMessage:
PROCESSING = "Request is being processed."
LOADING = "Loading data..."
SAVING = "Saving data..."
DELETING = "Deleting record..."
IMPORTING = "Importing data..."
EXPORTING = "Preparing export..."

View File

@@ -0,0 +1,66 @@
from flask import request, render_template
from app.utils.response_handler import ResponseHandler
from app.utils.exceptions import APIException
from app.constants.http_status import HTTPStatus
from app.constants.messages import ErrorMessage
from app.services.db_service import db
import traceback
def register_error_handlers(app):
# Custom API Exception
@app.errorhandler(APIException)
def handle_api_exception(e):
db.session.rollback()
if request.path.startswith("/api"):
return ResponseHandler.error(
message=e.message,
errors=e.errors,
status_code=e.status_code
)
return render_template("errors/500.html"), e.status_code
# 404
@app.errorhandler(404)
def handle_404(e):
if request.path.startswith("/api"):
return ResponseHandler.error(
message=ErrorMessage.NOT_FOUND,
status_code=HTTPStatus.NOT_FOUND
)
return render_template("errors/404.html"), 404
# 500
@app.errorhandler(500)
def handle_500(e):
db.session.rollback()
traceback.print_exc()
if request.path.startswith("/api"):
return ResponseHandler.error(
message=ErrorMessage.INTERNAL_ERROR,
status_code=HTTPStatus.INTERNAL_SERVER_ERROR
)
return render_template("errors/500.html"), 500
# Catch All
@app.errorhandler(Exception)
def handle_general_exception(e):
db.session.rollback()
traceback.print_exc()
if request.path.startswith("/api"):
return ResponseHandler.error(
message=ErrorMessage.INTERNAL_ERROR,
status_code=HTTPStatus.INTERNAL_SERVER_ERROR
)
return render_template("errors/500.html"), 500

View File

@@ -1,43 +1,56 @@
from app import db from app import db
from datetime import datetime from datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class LayingClient(db.Model): class LayingClient(db.Model):
__tablename__ = "laying_client" __tablename__ = "laying_client"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
# Foreign Key to Subcontractor tables
# subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False)
# Relationship for easy access (subcontractor.subcontractor_name)
# subcontractor = db.relationship("Subcontractor", backref="laying_records")
# Basic Fields # Basic Fields
RA_Bill_No = db.Column(db.String(500))
Location = db.Column(db.String(500)) Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100)) MH_NO = db.Column(db.String(100))
CC_length = db.Column(db.Float) CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Bedding Qty.
Outer_dia_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Bedding_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Width = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Qty = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Pipe_Dia_mm = db.Column(db.Float) # PIPE LAYING Qty.
ID_of_MH_m = db.Column(db.Float) Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Laying_Length = db.Column(db.Float) ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Laying_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_150_mm = db.Column(db.Float) pipe_150_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_200_mm = db.Column(db.Float) pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_250_mm = db.Column(db.Float) pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_300_mm = db.Column(db.Float) pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_350_mm = db.Column(db.Float) pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_400_mm = db.Column(db.Float) pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_450_mm = db.Column(db.Float) pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_500_mm = db.Column(db.Float) pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_600_mm = db.Column(db.Float) pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_700_mm = db.Column(db.Float) pipe_700_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_900_mm = db.Column(db.Float) pipe_900_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_1200_mm = db.Column(db.Float) pipe_1200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
np4_pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
np4_pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
np4_pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
np4_pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
np4_pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
np4_pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
np4_pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
np4_pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Total = db.Column(db.Float) Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
Remarks = db.Column(db.String(500)) Remarks = db.Column(db.String(500), default="Import File")
RA_Bill_No=db.Column(db.String(500)) created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
created_at = db.Column(db.DateTime, default=datetime.today)
def __repr__(self): def __repr__(self):
@@ -45,3 +58,17 @@ class LayingClient(db.Model):
def serialize(self): def serialize(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns} return {c.name: getattr(self, c.name) for c in self.__table__.columns}
# AUTO TOTAL USING REGEX
def calculate_laying_total(mapper, connection, target):
total = Decimal("0.00")
for column in target.__table__.columns:
if RegularExpression.PIPE_MM_PATTERN.match(column.name):
value = getattr(target, column.name)
if value is not None:
total += Decimal(value)
target.Total = total.quantize(Decimal("0.01"))
event.listen(LayingClient, "before_insert", calculate_laying_total)
event.listen(LayingClient, "before_update", calculate_laying_total)

View File

@@ -1,5 +1,8 @@
from app import db from app import db
from datetime import datetime from datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class Laying(db.Model): class Laying(db.Model):
__tablename__ = "laying" __tablename__ = "laying"
@@ -10,34 +13,31 @@ class Laying(db.Model):
# Relationship for easy access (subcontractor.subcontractor_name) # Relationship for easy access (subcontractor.subcontractor_name)
subcontractor = db.relationship("Subcontractor", backref="laying_records") subcontractor = db.relationship("Subcontractor", backref="laying_records")
# Basic Fields # Pipe Laying Fields
RA_Bill_No=db.Column(db.String(500))
Location = db.Column(db.String(500)) Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100)) MH_NO = db.Column(db.String(100))
CC_length = db.Column(db.Float) CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Pipe_Dia_mm = db.Column(db.Float) ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
ID_of_MH_m = db.Column(db.Float) Laying_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Laying_Length = db.Column(db.Float)
pipe_150_mm = db.Column(db.Float) pipe_150_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_200_mm = db.Column(db.Float) pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_250_mm = db.Column(db.Float) pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_300_mm = db.Column(db.Float) pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_350_mm = db.Column(db.Float) pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_400_mm = db.Column(db.Float) pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_450_mm = db.Column(db.Float) pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_500_mm = db.Column(db.Float) pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_600_mm = db.Column(db.Float) pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_700_mm = db.Column(db.Float) pipe_700_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_900_mm = db.Column(db.Float) pipe_900_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_1200_mm = db.Column(db.Float) pipe_1200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
Total = db.Column(db.Float) Remarks = db.Column(db.String(500), default="Import File")
Remarks = db.Column(db.String(500)) created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
RA_Bill_No=db.Column(db.String(500))
created_at = db.Column(db.DateTime, default=datetime.today)
def __repr__(self): def __repr__(self):
@@ -45,3 +45,17 @@ class Laying(db.Model):
def serialize(self): def serialize(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns} return {c.name: getattr(self, c.name) for c in self.__table__.columns}
# AUTO TOTAL USING REGEX
def calculate_laying_total(mapper, connection, target):
total = Decimal("0.00")
for column in target.__table__.columns:
if RegularExpression.PIPE_MM_PATTERN.match(column.name):
value = getattr(target, column.name)
if value is not None:
total += Decimal(value)
target.Total = total.quantize(Decimal("0.01"))
event.listen(Laying, "before_insert", calculate_laying_total)
event.listen(Laying, "before_update", calculate_laying_total)

View File

@@ -1,5 +1,8 @@
from app import db from app import db
from datetime import datetime from datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class ManholeDomesticChamber(db.Model): class ManholeDomesticChamber(db.Model):
__tablename__ = "manhole_domestic_chamber" __tablename__ = "manhole_domestic_chamber"
@@ -11,42 +14,57 @@ class ManholeDomesticChamber(db.Model):
subcontractor = db.relationship("Subcontractor", backref="manhole_domestic_chamber_records") subcontractor = db.relationship("Subcontractor", backref="manhole_domestic_chamber_records")
# Basic Fields # Basic Fields
RA_Bill_No=db.Column(db.String(500))
Location = db.Column(db.String(500)) Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100)) MH_NO = db.Column(db.String(100))
Depth_of_MH = db.Column(db.Float) Depth_of_MH = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories
d_0_to_0_75 = db.Column(db.Float)
d_0_76_to_1_05 = db.Column(db.Float)
d_1_06_to_1_65 = db.Column(db.Float)
d_1_66_to_2_15 = db.Column(db.Float)
d_2_16_to_2_65 = db.Column(db.Float)
d_2_66_to_3_15 = db.Column(db.Float)
d_3_16_to_3_65= db.Column(db.Float)
d_3_66_to_4_15 = db.Column(db.Float)
d_4_16_to_4_65 = db.Column(db.Float)
d_4_66_to_5_15 = db.Column(db.Float)
d_5_16_to_5_65 = db.Column(db.Float)
d_5_66_to_6_15 = db.Column(db.Float) # Excavation categories
d_6_16_to_6_65 = db.Column(db.Float) d_0_to_0_75 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_6_66_to_7_15 = db.Column(db.Float) d_0_76_to_1_05 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_7_16_to_7_65 = db.Column(db.Float) d_1_06_to_1_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_7_66_to_8_15 = db.Column(db.Float) d_1_66_to_2_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_8_16_to_8_65 = db.Column(db.Float) d_2_16_to_2_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_8_66_to_9_15 = db.Column(db.Float) d_2_66_to_3_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_9_16_to_9_65 = db.Column(db.Float) d_3_16_to_3_65= db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_3_66_to_4_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_4_16_to_4_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_4_66_to_5_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_5_16_to_5_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_5_66_to_6_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_6_16_to_6_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_6_66_to_7_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_7_16_to_7_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_7_66_to_8_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_8_16_to_8_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_8_66_to_9_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_9_16_to_9_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Domestic_Chambers = db.Column(db.Float) Domestic_Chambers = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
DWC_Pipe_Length = db.Column(db.Float) DWC_Pipe_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
UPVC_Pipe_Length = db.Column(db.Float) UPVC_Pipe_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
RA_Bill_No=db.Column(db.String(500))
created_at = db.Column(db.DateTime, default=datetime.today) Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
Remarks = db.Column(db.String(500), default="Import File")
created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
def __repr__(self): def __repr__(self):
return f"<HanholeDomesticChamberConstruction {self.Location}>" return f"<HanholeDomesticChamberConstruction {self.Location}>"
def serialize(self): def serialize(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns} return {c.name: getattr(self, c.name) for c in self.__table__.columns}
# AUTO TOTAL USING REGEX
def calculate_mh_dc_total(mapper, connection, target):
total = Decimal("0.00")
for column in target.__table__.columns:
if RegularExpression.D_RANGE_PATTERN.match(column.name):
value = getattr(target, column.name)
if value is not None:
total += Decimal(value)
target.Total = total.quantize(Decimal("0.01"))
event.listen(ManholeDomesticChamber, "before_insert", calculate_mh_dc_total)
event.listen(ManholeDomesticChamber, "before_update", calculate_mh_dc_total)

View File

@@ -1,5 +1,8 @@
from app import db from app import db
from datetime import datetime from datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class ManholeExcavation(db.Model): class ManholeExcavation(db.Model):
__tablename__ = "manhole_excavation" __tablename__ = "manhole_excavation"
@@ -11,57 +14,70 @@ class ManholeExcavation(db.Model):
subcontractor = db.relationship("Subcontractor", backref="manhole_records") subcontractor = db.relationship("Subcontractor", backref="manhole_records")
# Basic Fields # Basic Fields
RA_Bill_No=db.Column(db.String(500))
Location = db.Column(db.String(500)) Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100)) MH_NO = db.Column(db.String(100))
Upto_IL_Depth = db.Column(db.Float) Upto_IL_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Cutting_Depth = db.Column(db.Float) Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
ID_of_MH_m = db.Column(db.Float) ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Ex_Dia_of_Manhole = db.Column(db.Float) Ex_Dia_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Area_of_Manhole = db.Column(db.Float) Area_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories # Excavation categories
Soft_Murum_0_to_1_5 = db.Column(db.Float) Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_1_5_to_3_0 = db.Column(db.Float) Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_3_0_to_4_5 = db.Column(db.Float) Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_0_to_1_5 = db.Column(db.Float) Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_1_5_to_3_0 = db.Column(db.Float) Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5 = db.Column(db.Float) Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_1_5_to_3_0 = db.Column(db.Float) Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5 = db.Column(db.Float) Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_1_5_to_3_0 = db.Column(db.Float) Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_3_0_to_4_5 = db.Column(db.Float) Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_4_5_to_6_0 = db.Column(db.Float) Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5 = db.Column(db.Float) Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Totals # Totals
Soft_Murum_0_to_1_5_total = db.Column(db.Float) Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_1_5_to_3_0_total = db.Column(db.Float) Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_3_0_to_4_5_total = db.Column(db.Float) Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_0_to_1_5_total = db.Column(db.Float) Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_1_5_and_above_total = db.Column(db.Float) Hard_Murum_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5_total = db.Column(db.Float) Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_1_5_and_above_total = db.Column(db.Float) Soft_Rock_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5_total = db.Column(db.Float) Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_1_5_to_3_0_total = db.Column(db.Float) Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float) Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float) Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5_total = db.Column(db.Float) Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Total = db.Column(db.Float) Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
Remarks = db.Column(db.String(500)) Remarks = db.Column(db.String(500), default="Import File")
RA_Bill_No=db.Column(db.String(500)) created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
created_at = db.Column(db.DateTime, default=datetime.today)
def __repr__(self): def __repr__(self):
return f"<HanholeExcavation {self.Location}>" return f"<HanholeExcavation {self.Location}>"
def serialize(self): def serialize(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns} return {c.name: getattr(self, c.name) for c in self.__table__.columns}
# AUTO TOTAL USING REGEX
def calculate_Manhole_total(mapper, connection, target):
total = Decimal("0.00")
for column in target.__table__.columns:
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
value = getattr(target, column.name)
if value is not None:
total += Decimal(value)
target.Total = total.quantize(Decimal("0.01"))
event.listen(ManholeExcavation, "before_insert", calculate_Manhole_total)
event.listen(ManholeExcavation, "before_update", calculate_Manhole_total)

View File

@@ -1,42 +1,40 @@
from app import db from app import db
from datetime import datetime from datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class ManholeDomesticChamberClient(db.Model): class ManholeDomesticChamberClient(db.Model):
__tablename__ = "mh_dc_client" __tablename__ = "mh_dc_client"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
# Foreign Key to Subcontractor table
# subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False)
# Relationship for easy access (subcontractor.subcontractor_name)
# subcontractor = db.relationship("Subcontractor", backref="mh_dc_records")
# Basic Fields # Basic Fields
RA_Bill_No=db.Column(db.String(500)) RA_Bill_No = db.Column(db.String(500))
Location = db.Column(db.String(500)) Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100)) MH_NO = db.Column(db.String(100))
MH_TOP_LEVEL = db.Column(db.Float) MH_TOP_LEVEL = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
MH_IL_LEVEL = db.Column(db.Float) MH_IL_LEVEL = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Depth_of_MH = db.Column(db.Float) Depth_of_MH = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories # Excavation categories
d_0_to_1_5 = db.Column(db.Float) d_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_1_5_to_2_0 = db.Column(db.Float) d_1_5_to_2_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_2_0_to_2_5 = db.Column(db.Float) d_2_0_to_2_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_2_5_to_3_0 = db.Column(db.Float) d_2_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_3_0_to_3_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_3_5_to_4_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_4_0_to_4_5= db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_4_5_to_5_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_5_0_to_5_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_5_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_6_0_to_6_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_3_0_to_3_5 = db.Column(db.Float) Domestic_Chambers = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_3_5_to_4_0 = db.Column(db.Float)
d_4_0_to_4_5= db.Column(db.Float)
d_4_5_to_5_0 = db.Column(db.Float)
d_5_0_to_5_5 = db.Column(db.Float) Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
d_5_5_to_6_0 = db.Column(db.Float) Remarks = db.Column(db.String(500), default="Import File")
d_6_0_to_6_5 = db.Column(db.Float) created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
Domestic_Chambers = db.Column(db.Float)
created_at = db.Column(db.DateTime, default=datetime.today)
def __repr__(self): def __repr__(self):
return f"<HanholeDomesticChamberConstruction {self.Location}>" return f"<HanholeDomesticChamberConstruction {self.Location}>"
@@ -44,3 +42,17 @@ class ManholeDomesticChamberClient(db.Model):
def serialize(self): def serialize(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns} return {c.name: getattr(self, c.name) for c in self.__table__.columns}
# AUTO TOTAL USING REGEX
def calculate_mh_dc_total(mapper, connection, target):
total = Decimal("0.00")
for column in target.__table__.columns:
if RegularExpression.D_RANGE_PATTERN.match(column.name):
value = getattr(target, column.name)
if value is not None:
total += Decimal(value)
target.Total = total.quantize(Decimal("0.01"))
event.listen(ManholeDomesticChamberClient, "before_insert", calculate_mh_dc_total)
event.listen(ManholeDomesticChamberClient, "before_update", calculate_mh_dc_total)

View File

@@ -1,83 +1,96 @@
from app import db from app import db
from datetime import datetime from datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class ManholeExcavationClient(db.Model): class ManholeExcavationClient(db.Model):
__tablename__ = "mh_ex_client" __tablename__ = "mh_ex_client"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
# Foreign Key to Subcontractor table
# subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False)
# Relationship for easy access (subcontractor.subcontractor_name)
# subcontractor = db.relationship("Subcontractor", backref="mh_ex_records")
# Basic Fields # Basic Fields
RA_Bill_No=db.Column(db.String(500)) RA_Bill_No = db.Column(db.String(500))
Location = db.Column(db.String(500)) Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100)) MH_NO = db.Column(db.String(100))
Ground_Level = db.Column(db.Float) Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
MH_Invert_Level = db.Column(db.Float) MH_Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
MH_Top_Level = db.Column(db.Float) MH_Top_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Ex_Level = db.Column(db.Float) Ex_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Cutting_Depth = db.Column(db.Float) Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
MH_Depth = db.Column(db.Float) MH_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
ID_of_MH_m = db.Column(db.Float) ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Dia_of_MH_Cutting = db.Column(db.Float) Dia_of_MH_Cutting = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Area_of_Manhole = db.Column(db.Float) Area_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories # Excavation categories
Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Float) Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Float) Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Float) Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_0_to_1_5 = db.Column(db.Float) Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_1_5_to_3_0 = db.Column(db.Float) Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_3_0_to_4_5 = db.Column(db.Float) Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_0_to_1_5 = db.Column(db.Float) Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_1_5_to_3_0 = db.Column(db.Float) Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_3_0_to_4_5 = db.Column(db.Float) Hard_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5 = db.Column(db.Float) Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_1_5_to_3_0 = db.Column(db.Float) Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_3_0_to_4_5 = db.Column(db.Float) Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5 = db.Column(db.Float) Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_1_5_to_3_0 = db.Column(db.Float) Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_3_0_to_4_5 = db.Column(db.Float) Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_4_5_to_6_0 = db.Column(db.Float) Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5 = db.Column(db.Float) Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Totals # Totals
Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Float) Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Float) Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Float) Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_0_to_1_5_total = db.Column(db.Float) Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_1_5_to_3_0_total = db.Column(db.Float) Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_3_0_to_4_5_total = db.Column(db.Float) Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_0_to_1_5_total = db.Column(db.Float) Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_1_5_to_3_0_total = db.Column(db.Float) Hard_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_3_0_to_4_5_total = db.Column(db.Float) Hard_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5_total = db.Column(db.Float) Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_1_5_to_3_0_total = db.Column(db.Float) Soft_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_3_0_to_4_5_total = db.Column(db.Float) Soft_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5_total = db.Column(db.Float) Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_1_5_to_3_0_total = db.Column(db.Float) Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float) Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float) Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5_total = db.Column(db.Float) Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Remarks = db.Column(db.String(500))
Total = db.Column(db.Float) Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
Remarks = db.Column(db.String(500), default="Import File")
created_at = db.Column(db.DateTime, default=datetime.today) created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
def __repr__(self): def __repr__(self):
return f"<HanholeExcavation {self.Location}>" return f"<HanholeExcavation {self.Location}>"
def serialize(self): def serialize(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns} return {c.name: getattr(self, c.name) for c in self.__table__.columns}
# AUTO TOTAL USING REGEX
def calculate_Manhole_total(mapper, connection, target):
total = Decimal("0.00")
for column in target.__table__.columns:
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
value = getattr(target, column.name)
if value is not None:
total += Decimal(value)
target.Total = total.quantize(Decimal("0.01"))
event.listen(ManholeExcavationClient, "before_insert", calculate_Manhole_total)
event.listen(ManholeExcavationClient, "before_update", calculate_Manhole_total)

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

@@ -1,88 +1,101 @@
from app import db from app import db
from datetime import datetime from datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class TrenchExcavationClient(db.Model): class TrenchExcavationClient(db.Model):
__tablename__ = "tr_ex_client" __tablename__ = "tr_ex_client"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
# Foreign Key to Subcontractor table
# subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False)
# Relationship for easy access (subcontractor.subcontractor_name)
# subcontractor = db.relationship("Subcontractor", backref="tr_ex_records")
# Basic Fields # Basic Fields
RA_Bill_No=db.Column(db.String(500)) RA_Bill_No=db.Column(db.String(500))
Location = db.Column(db.String(500)) Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100)) MH_NO = db.Column(db.String(100))
CC_length = db.Column(db.Float) CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Actual_Trench_Length = db.Column(db.Float) Actual_Trench_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Ground_Level = db.Column(db.Float) Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Invert_Level = db.Column(db.Float) Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Excavated_level = db.Column(db.Float) Excavated_level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Cutting_Depth = db.Column(db.Float) Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Avg_Depth = db.Column(db.Float) Avg_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Pipe_Dia_mm = db.Column(db.Float) Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# width # width
Width_0_to_1_5_m = db.Column(db.Float) Width_0_to_1_5_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Width_1_5_to_3_0_m = db.Column(db.Float) Width_1_5_to_3_0_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Width_3_0_to_4_5_m = db.Column(db.Float) Width_3_0_to_4_5_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Width_4_5_to_6_0_m = db.Column(db.Float) Width_4_5_to_6_0_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Width_6_0_to_7_5_m = db.Column(db.Float) Width_6_0_to_7_5_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories # Excavation categories
Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Float) Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Float) Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Float) Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_0_to_1_5 = db.Column(db.Float) Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_1_5_to_3_0 = db.Column(db.Float) Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_3_0_to_4_5 = db.Column(db.Float) Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_0_to_1_5 = db.Column(db.Float) Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_1_5_to_3_0 = db.Column(db.Float) Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_3_0_to_4_5 = db.Column(db.Float) Hard_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5 = db.Column(db.Float) Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_1_5_to_3_0 = db.Column(db.Float) Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_3_0_to_4_5 = db.Column(db.Float) Soft_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5 = db.Column(db.Float) Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_1_5_to_3_0 = db.Column(db.Float) Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_3_0_to_4_5 = db.Column(db.Float) Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_4_5_to_6_0 = db.Column(db.Float) Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5 = db.Column(db.Float) Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Totals # Totals
Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Float) Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Float) Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Float) Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_0_to_1_5_total = db.Column(db.Float) Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_1_5_to_3_0_total = db.Column(db.Float) Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_3_0_to_4_5_total = db.Column(db.Float) Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_0_to_1_5_total = db.Column(db.Float) Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_1_5_to_3_0_total = db.Column(db.Float) Hard_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_3_0_to_4_5_total = db.Column(db.Float) Hard_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5_total = db.Column(db.Float) Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_1_5_to_3_0_total = db.Column(db.Float) Soft_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_3_0_to_4_5_total = db.Column(db.Float) Soft_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5_total = db.Column(db.Float) Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_1_5_to_3_0_total = db.Column(db.Float) Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float) Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float) Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5_total = db.Column(db.Float) Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Total = db.Column(db.Float) Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
Remarks = db.Column(db.String(500)) Remarks = db.Column(db.String(500), default="Import File")
created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
created_at = db.Column(db.DateTime, default=datetime.today)
def __repr__(self): def __repr__(self):
return f"<TrenchExcavation {self.Location}>" return f"<TrenchExcavation {self.Location}>"
def serialize(self): def serialize(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns} return {c.name: getattr(self, c.name) for c in self.__table__.columns}
# AUTO TOTAL USING REGEX
def calculate_trench_client_total(mapper, connection, target):
total = Decimal("0.00")
for column in target.__table__.columns:
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
value = getattr(target, column.name)
if value is not None:
total += Decimal(value)
target.Total = total.quantize(Decimal("0.01"))
event.listen(TrenchExcavationClient, "before_insert", calculate_trench_client_total)
event.listen(TrenchExcavationClient, "before_update", calculate_trench_client_total)

View File

@@ -1,5 +1,8 @@
from app import db from app import db
from datetime import datetime from datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class TrenchExcavation(db.Model): class TrenchExcavation(db.Model):
__tablename__ = "trench_excavation" __tablename__ = "trench_excavation"
@@ -11,65 +14,64 @@ class TrenchExcavation(db.Model):
subcontractor = db.relationship("Subcontractor", backref="trench_records") subcontractor = db.relationship("Subcontractor", backref="trench_records")
# Basic Fields # Basic Fields
RA_Bill_No=db.Column(db.String(500))
Location = db.Column(db.String(500)) Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100)) MH_NO = db.Column(db.String(100))
CC_length = db.Column(db.Float) CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Invert_Level = db.Column(db.Float) Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
MH_Top_Level = db.Column(db.Float) MH_Top_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Ground_Level = db.Column(db.Float) Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
ID_of_MH_m = db.Column(db.Float) ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Actual_Trench_Length = db.Column(db.Float) Actual_Trench_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Pipe_Dia_mm = db.Column(db.Float) Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# width # width
Width_0_to_2_5 = db.Column(db.Float) Width_0_to_2_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Width_2_5_to_3_0 = db.Column(db.Float) Width_2_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Width_3_0_to_4_5 = db.Column(db.Float) Width_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Width_4_5_to_6_0 = db.Column(db.Float) Width_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Upto_IL_Depth = db.Column(db.Float) Upto_IL_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Cutting_Depth = db.Column(db.Float) Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Avg_Depth = db.Column(db.Float) Avg_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories # Excavation categories
Soft_Murum_0_to_1_5 = db.Column(db.Float) Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_1_5_to_3_0 = db.Column(db.Float) Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_3_0_to_4_5 = db.Column(db.Float) Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_0_to_1_5 = db.Column(db.Float) Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_1_5_to_3_0 = db.Column(db.Float) Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5 = db.Column(db.Float) Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_1_5_to_3_0 = db.Column(db.Float) Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5 = db.Column(db.Float) Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_1_5_to_3_0 = db.Column(db.Float) Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_3_0_to_4_5 = db.Column(db.Float) Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_4_5_to_6_0 = db.Column(db.Float) Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5 = db.Column(db.Float) Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Totals # Totals
Soft_Murum_0_to_1_5_total = db.Column(db.Float) Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_1_5_to_3_0_total = db.Column(db.Float) Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Murum_3_0_to_4_5_total = db.Column(db.Float) Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_0_to_1_5_total = db.Column(db.Float) Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Murum_1_5_and_above_total = db.Column(db.Float) Hard_Murum_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5_total = db.Column(db.Float) Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_1_5_and_above_total = db.Column(db.Float) Soft_Rock_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5_total = db.Column(db.Float) Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_1_5_to_3_0_total = db.Column(db.Float) Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float) Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float) Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5_total = db.Column(db.Float) Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Total = db.Column(db.Float) Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
Remarks = db.Column(db.String(500)) Remarks = db.Column(db.String(500), default="Import File")
RA_Bill_No=db.Column(db.String(500)) created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
created_at = db.Column(db.DateTime, default=datetime.today)
def __repr__(self): def __repr__(self):
@@ -77,3 +79,17 @@ class TrenchExcavation(db.Model):
def serialize(self): def serialize(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns} return {c.name: getattr(self, c.name) for c in self.__table__.columns}
# AUTO TOTAL USING REGEX
def calculate_trench_total(mapper, connection, target):
total = Decimal("0.00")
for column in target.__table__.columns:
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
value = getattr(target, column.name)
if value is not None:
total += Decimal(value)
target.Total = total.quantize(Decimal("0.01"))
event.listen(TrenchExcavation, "before_insert", calculate_trench_total)
event.listen(TrenchExcavation, "before_update", calculate_trench_total)

View File

@@ -5,7 +5,7 @@ class User(db.Model):
__tablename__ = "users" __tablename__ = "users"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), nullable=False) name = db.Column(db.String(200), nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False) password_hash = db.Column(db.String(255), nullable=False)

21
app/models/width_model.py Normal file
View File

@@ -0,0 +1,21 @@
from app import db
class Width(db.Model):
__tablename__ = "width"
id = db.Column(db.Integer, primary_key=True)
Dia_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_150_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_200_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_250_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_300_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_350_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_400_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_450_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_500_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_600_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_700_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_900_mm = db.Column(db.Numeric(10, 2), default=0)
pipe_1200_mm = db.Column(db.Numeric(10, 2), default=0)

View File

@@ -0,0 +1,59 @@
from flask import Blueprint, render_template, request, send_file, abort
import os
from app.utils.helpers import login_required
from app.utils.file_utils import get_logs_folder , ALLOWED_LOG_FILE
from app.services.activity_service import ActivityService
activity_bp = Blueprint("activity", __name__, url_prefix="/activity")
# call activity_log page
@activity_bp.route("/")
@login_required
def activity():
file_name = request.args.get("file", "app.log")
search = request.args.get("search", "")
level = request.args.get("level", "").upper()
user = request.args.get("user", "")
from_date = request.args.get("from_date", "")
to_date = request.args.get("to_date", "")
logs = ActivityService.read_logs(
file_name=file_name,
search=search,
level=level,
user=user,
from_date=from_date,
to_date=to_date
)
return render_template(
"activity/activity_log.html",
logs=logs,
file_name=file_name,
search=search,
level=level,
user=user,
from_date=from_date,
to_date=to_date
)
# Download activity_log files
@activity_bp.route("/logs")
@login_required
def download_log():
filename = request.args.get("file", "app.log")
if filename not in ALLOWED_LOG_FILE:
abort(400, "Invalid log file.")
file_path = os.path.join(get_logs_folder(), filename)
if not os.path.isfile(file_path):
abort(404, "Log file not found.")
return send_file(
file_path,
as_attachment=True,
download_name=filename,
mimetype="text/plain"
)

View File

@@ -1,48 +1,98 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, session from flask import (Blueprint, render_template, request, redirect, url_for, flash, session, current_app)
from app.services.user_service import UserService from app.services.user_service import UserService
from app.constants.messages import SuccessMessage, ErrorMessage
from app.constants.http_status import HTTPStatus
auth_bp = Blueprint("auth", __name__) auth_bp = Blueprint("auth", __name__)
# ==========================
# LOGIN
# ==========================
@auth_bp.route("/login", methods=["GET", "POST"]) @auth_bp.route("/login", methods=["GET", "POST"])
def login(): def login():
if session.get("user_id"): if session.get("user_id"):
current_app.logger.info("User already logged in.")
return redirect(url_for("dashboard.dashboard")) return redirect(url_for("dashboard.dashboard"))
if request.method == "POST": if request.method == "POST":
email = request.form.get("email")
password = request.form.get("password")
user = UserService.validate_login(email, password) try:
if user: email = request.form.get("email", "").strip()
session["user_id"] = user.id password = request.form.get("password", "")
session["user_name"] = user.name
flash("Login successful", "success")
return redirect(url_for("dashboard.dashboard"))
flash("Invalid email or password", "danger") if not email or not password:
flash(ErrorMessage.INVALID_REQUEST, "danger")
current_app.logger.warning("Login failed. Email or password missing.")
return render_template("login.html", title="Login")
user = UserService.validate_login(email, password)
if user:
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}")
flash(SuccessMessage.LOGIN, "success")
return redirect(url_for("dashboard.dashboard"))
current_app.logger.warning(f"Invalid login attempt. Email={email}")
flash(ErrorMessage.LOGIN_FAILED,"danger")
except Exception as e:
current_app.logger.exception("Login Error" )
flash(ErrorMessage.INTERNAL_SERVER_ERROR,"danger")
return render_template("login.html", title="Login") return render_template("login.html", title="Login")
# ==========================
# LOGOUT
# ==========================
@auth_bp.route("/logout") @auth_bp.route("/logout")
def logout(): def logout():
username = session.get("user_name", "Unknown")
session.clear() session.clear()
flash("Logged out successfully", "info") current_app.logger.info(f"Logout successful. User={username}")
flash(SuccessMessage.LOGOUT,"info")
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
# ==========================
# REGISTER
# ==========================
@auth_bp.route("/register", methods=["GET", "POST"]) @auth_bp.route("/register", methods=["GET", "POST"])
def register(): def register():
if request.method == "POST": if request.method == "POST":
name = request.form.get("name") try:
email = request.form.get("email") name = request.form.get("name", "").strip()
password = request.form.get("password") email = request.form.get("email", "").strip()
password = request.form.get("password", "")
user = UserService.register_user(name, email, password) if not name or not email or not password:
if not user: flash(ErrorMessage.INVALID_REQUEST,"danger")
flash("Email already exists", "danger") return redirect(url_for("auth.register"))
return redirect(url_for("auth.register"))
flash("User registered successfully", "success") user = UserService.register_user(name, email, password)
return redirect(url_for("auth.login"))
return render_template("register.html", title="Register") if not user:
current_app.logger.warning(f"Duplicate Registration: {email}")
flash(ErrorMessage.DUPLICATE_ENTRY,"danger")
return redirect(url_for("auth.register"))
current_app.logger.info(f"New user registered: {email}")
flash(SuccessMessage.CREATED,"success")
return redirect(url_for("auth.login"))
except Exception:
current_app.logger.exception("User Registration Failed" )
flash(ErrorMessage.INTERNAL_SERVER_ERROR,"danger")
return render_template("register.html",title="Register")

View File

@@ -1,68 +1,614 @@
import matplotlib import matplotlib
matplotlib.use("Agg") matplotlib.use("Agg")
from flask import Blueprint, render_template, session, redirect, url_for from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import io import io
import base64 import base64
from app.utils.plot_utils import plot_to_base64
from app.utils.helpers import login_required
from app.services.dashboard_service import DashboardService
from sqlalchemy import func
from app import db
from sqlalchemy import tuple_
# Subcontractor models import
from app.models.subcontractor_model import Subcontractor
from app.models.trench_excavation_model import TrenchExcavation
from app.models.manhole_excavation_model import ManholeExcavation
from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber
from app.models.laying_model import Laying
# client models import
from app.models.tr_ex_client_model import TrenchExcavationClient
from app.models.mh_ex_client_model import ManholeExcavationClient
from app.models.mh_dc_client_model import ManholeDomesticChamberClient
from app.models.laying_client_model import LayingClient
dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard") dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
# charts
def plot_to_base64():
img = io.BytesIO()
plt.savefig(img, format="png", bbox_inches="tight")
plt.close()
img.seek(0)
return base64.b64encode(img.getvalue()).decode()
# bar chart
def bar_chart():
categories = ["Trench", "Manhole", "Pipe Laying", "Restoration"]
values = [120, 80, 150, 60]
plt.figure()
plt.bar(categories, values)
plt.title("Work Category Report")
plt.xlabel("Category")
plt.ylabel("Count")
return plot_to_base64()
# Pie chart
def pie_chart():
labels = ["Completed", "In Progress", "Pending"]
sizes = [55, 20, 25]
plt.figure()
plt.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=140)
plt.title("Project Status")
return plot_to_base64()
# Histogram chart
def histogram_chart():
daily_work = [5, 10, 15, 20, 20, 25, 30, 35, 40, 45, 50]
plt.figure()
plt.hist(daily_work, bins=5)
plt.title("Daily Work Distribution")
plt.xlabel("Work Units")
plt.ylabel("Frequency")
return plot_to_base64()
# Dashboaed page
@dashboard_bp.route("/") @dashboard_bp.route("/")
def dashboard(): def dashboard():
if not session.get("user_id"): if not session.get("user_id"):
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
return render_template("dashboard.html", title="Business Intelligence Dashboard")
@dashboard_bp.route("/api/live-stats")
@login_required
def live_stats():
try:
# 1. Overall Volume
t_count = TrenchExcavation.query.count()
m_count = ManholeExcavation.query.count()
l_count = Laying.query.count()
# 2. Location Distribution (Business reach)
loc_results = db.session.query(
TrenchExcavation.Location,
func.count(TrenchExcavation.id)
).group_by(TrenchExcavation.Location).all()
# 3. Work Timeline (Business productivity trend)
# Assuming your models have a 'created_at' field
timeline_results = db.session.query(
func.date(TrenchExcavation.created_at),
func.count(TrenchExcavation.id)
).group_by(func.date(TrenchExcavation.created_at)).order_by(func.date(TrenchExcavation.created_at)).all()
return jsonify({
"summary": {
"trench": t_count,
"manhole": m_count,
"laying": l_count,
"total": t_count + m_count + l_count
},
"locations": {row[0]: row[1] for row in loc_results if row[0]},
"timeline": {str(row[0]): row[1] for row in timeline_results}
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# subcontractor dashboard
@dashboard_bp.route("/subcontractor_dashboard")
@login_required
def subcontractor_dashboard():
if not session.get("user_id"):
return redirect(url_for("auth.login"))
subcontractors = Subcontractor.query.all()
return render_template( return render_template(
"dashboard.html", "subcontractor_dashboard.html",
title="Dashboard", subcontractors=subcontractors
bar_chart=bar_chart(),
pie_chart=pie_chart(),
histogram=histogram_chart()
) )
# API: Get Unique RA Bills
@dashboard_bp.route("/api/get-ra-bills")
@login_required
def get_ra_bills():
subcontractor_id = request.args.get("subcontractor")
category = request.args.get("category")
if not subcontractor_id or not category:
return {"ra_bills": []}
match category:
case "trench_excavation":
results = db.session.query(
TrenchExcavation.RA_Bill_No
).filter(
TrenchExcavation.subcontractor_id == subcontractor_id
).distinct().order_by(TrenchExcavation.RA_Bill_No).all()
# (Add others same pattern later)
case "manhole_excavation":
results = db.session.query(
ManholeExcavation.RA_Bill_No
).filter(
ManholeExcavation.subcontractor_id == subcontractor_id
).distinct().order_by(ManholeExcavation.RA_Bill_No).all()
case "Manhole_Domestic_Chamber":
results = db.session.query(
ManholeDomesticChamber.RA_Bill_No
).filter(
ManholeDomesticChamber.subcontractor_id == subcontractor_id
).distinct().order_by(ManholeDomesticChamber.RA_Bill_No).all()
case "Laying":
results = db.session.query(
Laying.RA_Bill_No
).filter(
Laying.subcontractor_id == subcontractor_id
).distinct().order_by(Laying.RA_Bill_No).all()
ra_bills = [r[0] for r in results if r[0]]
return {"ra_bills": ra_bills}
def total(records, field):
return float(sum(getattr(r, field) or 0 for r in records))
# category= trench_excavation
@dashboard_bp.route("/api/tr-analysis")
def trench_analysis():
subcontractor_id = request.args.get("subcontractor", "").strip()
ra_bill = request.args.get("ra_bill", "").strip()
# Convert "1,2,3" -> ["1", "2", "3"]
ra_bill_list = []
if ra_bill:
ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()]
# Subcontractor Query
sub_query = TrenchExcavation.query
if subcontractor_id:
sub_query = sub_query.filter(
TrenchExcavation.subcontractor_id == int(subcontractor_id)
)
if ra_bill_list:
sub_query = sub_query.filter(
TrenchExcavation.RA_Bill_No.in_(ra_bill_list)
)
sub_records = sub_query.all()
sub_keys = [
(
(r.MH_NO or "").strip().upper(),
(r.Location or "").strip().upper()
)
for r in sub_records
]
client_query = TrenchExcavationClient.query
if sub_keys:
client_query = client_query.filter(
tuple_(
func.upper(func.trim(TrenchExcavationClient.MH_NO)),
func.upper(func.trim(TrenchExcavationClient.Location))
).in_(sub_keys)
)
client_records = client_query.all()
chart_data = [
{
"label": "Marshi 0 to 1.5",
"client": total(client_records, "Marshi_Muddy_Slushy_0_to_1_5_total"),
"sub": 0
},
{
"label": "Marshi 1.5 to 3.0",
"client": total(client_records, "Marshi_Muddy_Slushy_1_5_to_3_0_total"),
"sub": 0
},
{
"label": "Marshi 3.0 to 4.5",
"client": total(client_records, "Marshi_Muddy_Slushy_3_0_to_4_5_total"),
"sub": 0
},
{
"label": "Soft Murum 0 to 1.5",
"client": total(client_records, "Soft_Murum_0_to_1_5_total"),
"sub": total(sub_records, "Soft_Murum_0_to_1_5_total")
},
{
"label": "Soft Murum 1.5 to 3.0",
"client": total(client_records, "Soft_Murum_1_5_to_3_0_total"),
"sub": total(sub_records, "Soft_Murum_1_5_to_3_0_total")
},
{
"label": "Soft Murum 3.0 to 4.5",
"client": total(client_records, "Soft_Murum_3_0_to_4_5_total"),
"sub": total(sub_records, "Soft_Murum_3_0_to_4_5_total")
},
{
"label": "Hard Murum 0 to 1.5",
"client": total(client_records, "Hard_Murum_0_to_1_5_total"),
"sub": total(sub_records, "Hard_Murum_0_to_1_5_total")
},
{
"label": "Hard Murum 1.5 to 3.0",
"client": total(client_records, "Hard_Murum_1_5_to_3_0_total"),
"sub": total(sub_records, "Hard_Murum_1_5_and_above_total")
},
{
"label": "Soft Rock 0 to 1.5",
"client": total(client_records, "Soft_Rock_0_to_1_5_total"),
"sub": total(sub_records, "Soft_Rock_0_to_1_5_total")
},
{
"label": "Soft Rock 1.5 to 3.0",
"client": total(client_records, "Soft_Rock_1_5_to_3_0_total"),
"sub": total(sub_records, "Soft_Rock_1_5_and_above_total")
},
{
"label": "Hard Rock 0 to 1.5",
"client": total(client_records, "Hard_Rock_0_to_1_5_total"),
"sub": total(sub_records, "Hard_Rock_0_to_1_5_total")
},
{
"label": "Hard Rock 1.5 to 3.0",
"client": total(client_records, "Hard_Rock_1_5_to_3_0_total"),
"sub": total(sub_records, "Hard_Rock_1_5_to_3_0_total")
},
{
"label": "Hard Rock 3.0 to 4.5",
"client": total(client_records, "Hard_Rock_3_0_to_4_5_total"),
"sub": total(sub_records, "Hard_Rock_3_0_to_4_5_total")
},
{
"label": "Hard Rock 4.5 to 6.0",
"client": total(client_records, "Hard_Rock_4_5_to_6_0_total"),
"sub": total(sub_records, "Hard_Rock_4_5_to_6_0_total")
},
{
"label": "Hard Rock 6.0 to 7.5",
"client": total(client_records, "Hard_Rock_6_0_to_7_5_total"),
"sub": total(sub_records, "Hard_Rock_6_0_to_7_5_total")
}
]
return jsonify({
"title": "Trench Excavation Comparison",
"y_title": "Excavation Qty (Cum)",
"labels": [x["label"] for x in chart_data],
"client_qty": [x["client"] for x in chart_data],
"sub_qty": [x["sub"] for x in chart_data]
})
# category = manhole_excavation
@dashboard_bp.route("/api/mh-analysis")
def manhole_analysis():
subcontractor_id = request.args.get("subcontractor", "").strip()
ra_bill = request.args.get("ra_bill", "").strip()
# Convert "1,2,3" -> ["1", "2", "3"]
ra_bill_list = []
if ra_bill:
ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()]
# Subcontractor Query
sub_query = ManholeExcavation.query
if subcontractor_id:
sub_query = sub_query.filter(
ManholeExcavation.subcontractor_id == int(subcontractor_id)
)
if ra_bill_list:
sub_query = sub_query.filter(
ManholeExcavation.RA_Bill_No.in_(ra_bill_list)
)
sub_records = sub_query.all()
sub_keys = [
(
(r.MH_NO or "").strip().upper(),
(r.Location or "").strip().upper()
)
for r in sub_records
]
client_query = ManholeExcavationClient.query
if sub_keys:
client_query = client_query.filter(
tuple_(
func.upper(func.trim(ManholeExcavationClient.MH_NO)),
func.upper(func.trim(ManholeExcavationClient.Location))
).in_(sub_keys)
)
client_records = client_query.all()
chart_data = [
{
"label": "Marshi 0 to 1.5",
"client": total(client_records, "Marshi_Muddy_Slushy_0_to_1_5_total"),
"sub": 0
},
{
"label": "Marshi 1.5 to 3.0",
"client": total(client_records, "Marshi_Muddy_Slushy_1_5_to_3_0_total"),
"sub": 0
},
{
"label": "Marshi 3.0 to 4.5",
"client": total(client_records, "Marshi_Muddy_Slushy_3_0_to_4_5_total"),
"sub": 0
},
{
"label": "Soft Murum 0 to 1.5",
"client": total(client_records, "Soft_Murum_0_to_1_5_total"),
"sub": total(sub_records, "Soft_Murum_0_to_1_5_total")
},
{
"label": "Soft Murum 1.5 to 3.0",
"client": total(client_records, "Soft_Murum_1_5_to_3_0_total"),
"sub": total(sub_records, "Soft_Murum_1_5_to_3_0_total")
},
{
"label": "Soft Murum 3.0 to 4.5",
"client": total(client_records, "Soft_Murum_3_0_to_4_5_total"),
"sub": total(sub_records, "Soft_Murum_3_0_to_4_5_total")
},
{
"label": "Hard Murum 0 to 1.5",
"client": total(client_records, "Hard_Murum_0_to_1_5_total"),
"sub": total(sub_records, "Hard_Murum_0_to_1_5_total")
},
{
"label": "Hard Murum 1.5 to 3.0",
"client": total(client_records, "Hard_Murum_1_5_to_3_0_total"),
"sub": total(sub_records, "Hard_Murum_1_5_and_above_total")
},
{
"label": "Soft Rock 0 to 1.5",
"client": total(client_records, "Soft_Rock_0_to_1_5_total"),
"sub": total(sub_records, "Soft_Rock_0_to_1_5_total")
},
{
"label": "Soft Rock 1.5 to 3.0",
"client": total(client_records, "Soft_Rock_1_5_to_3_0_total"),
"sub": total(sub_records, "Soft_Rock_1_5_and_above_total")
},
{
"label": "Hard Rock 0 to 1.5",
"client": total(client_records, "Hard_Rock_0_to_1_5_total"),
"sub": total(sub_records, "Hard_Rock_0_to_1_5_total")
},
{
"label": "Hard Rock 1.5 to 3.0",
"client": total(client_records, "Hard_Rock_1_5_to_3_0_total"),
"sub": total(sub_records, "Hard_Rock_1_5_to_3_0_total")
},
{
"label": "Hard Rock 3.0 to 4.5",
"client": total(client_records, "Hard_Rock_3_0_to_4_5_total"),
"sub": total(sub_records, "Hard_Rock_3_0_to_4_5_total")
},
{
"label": "Hard Rock 4.5 to 6.0",
"client": total(client_records, "Hard_Rock_4_5_to_6_0_total"),
"sub": total(sub_records, "Hard_Rock_4_5_to_6_0_total")
},
{
"label": "Hard Rock 6.0 to 7.5",
"client": total(client_records, "Hard_Rock_6_0_to_7_5_total"),
"sub": total(sub_records, "Hard_Rock_6_0_to_7_5_total")
}
]
return jsonify({
"title": "Manhole Excavation Comparison",
"y_title": "Manhole Qty (Nos)",
"labels": [x["label"] for x in chart_data],
"client_qty": [x["client"] for x in chart_data],
"sub_qty": [x["sub"] for x in chart_data]
})
# category = Manhole_Domestic_Chamber
@dashboard_bp.route("/api/mdc-analysis")
def Manhole_Domestic_Chamber_analysis():
subcontractor_id = request.args.get("subcontractor", "").strip()
ra_bill = request.args.get("ra_bill", "").strip()
# Convert "1,2,3" -> ["1", "2", "3"]
ra_bill_list = []
if ra_bill:
ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()]
# Subcontractor Query
sub_query = ManholeDomesticChamber.query
if subcontractor_id:
sub_query = sub_query.filter(
ManholeDomesticChamber.subcontractor_id == int(subcontractor_id)
)
if ra_bill_list:
sub_query = sub_query.filter(
ManholeDomesticChamber.RA_Bill_No.in_(ra_bill_list)
)
sub_records = sub_query.all()
sub_keys = [
(
(r.MH_NO or "").strip().upper(),
(r.Location or "").strip().upper()
)
for r in sub_records
]
client_query = ManholeDomesticChamberClient.query
if sub_keys:
client_query = client_query.filter(
tuple_(
func.upper(func.trim(ManholeDomesticChamberClient.MH_NO)),
func.upper(func.trim(ManholeDomesticChamberClient.Location))
).in_(sub_keys)
)
client_records = client_query.all()
chart_data = [
{
"label": "Depth of MH",
"client": total(client_records, "Depth_of_MH"),
"sub": total(sub_records, "Depth_of_MH")
},
{
"label": "Domestic_Chambers Total",
"client": total(client_records, "Total"),
"sub": total(sub_records, "Total")
}
]
return jsonify({
"title": "Domestic Chamber Comparison",
"y_title": "Quantity (Nos)",
"labels": [x["label"] for x in chart_data],
"client_qty": [x["client"] for x in chart_data],
"sub_qty": [x["sub"] for x in chart_data]
})
# category = Laying
@dashboard_bp.route("/api/laying-analysis")
def laying_analysis():
subcontractor_id = request.args.get("subcontractor", "").strip()
ra_bill = request.args.get("ra_bill", "").strip()
# Convert "1,2,3" -> ["1", "2", "3"]
ra_bill_list = []
if ra_bill:
ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()]
# Subcontractor Query
sub_query = Laying.query
if subcontractor_id:
sub_query = sub_query.filter(
Laying.subcontractor_id == int(subcontractor_id)
)
if ra_bill_list:
sub_query = sub_query.filter(
Laying.RA_Bill_No.in_(ra_bill_list)
)
sub_records = sub_query.all()
sub_keys = [
(
(r.MH_NO or "").strip().upper(),
(r.Location or "").strip().upper()
)
for r in sub_records
]
client_query = LayingClient.query
if sub_keys:
client_query = client_query.filter(
tuple_(
func.upper(func.trim(LayingClient.MH_NO)),
func.upper(func.trim(LayingClient.Location))
).in_(sub_keys)
)
client_records = client_query.all()
chart_data = [
{
"label": "150 mm",
"client": total(client_records, "pipe_150_mm"),
"sub": total(sub_records, "pipe_150_mm")
},
{
"label": "200 mm",
"client": total(client_records, "pipe_200_mm"),
"sub": total(sub_records, "pipe_200_mm")
},
{
"label": "250 mm",
"client": total(client_records, "pipe_250_mm"),
"sub": total(sub_records, "pipe_250_mm")
},
{
"label": "300 mm",
"client": total(client_records, "pipe_300_mm"),
"sub": total(sub_records, "pipe_300_mm")
},
{
"label": "350 mm",
"client": total(client_records, "pipe_350_mm"),
"sub": total(sub_records, "pipe_350_mm")
},
{
"label": "400 mm",
"client": total(client_records, "pipe_400_mm"),
"sub": total(sub_records, "pipe_400_mm")
},
{
"label": "450 mm",
"client": total(client_records, "pipe_450_mm"),
"sub": total(sub_records, "pipe_450_mm")
},
{
"label": "500 mm",
"client": total(client_records, "pipe_500_mm"),
"sub": total(sub_records, "pipe_500_mm")
},
{
"label": "600 mm",
"client": total(client_records, "pipe_600_mm"),
"sub": total(sub_records, "pipe_600_mm")
},
{
"label": "700 mm",
"client": total(client_records, "pipe_700_mm"),
"sub": total(sub_records, "pipe_700_mm")
},
{
"label": "900 mm",
"client": total(client_records, "pipe_900_mm"),
"sub": total(sub_records, "pipe_900_mm")
},
{
"label": "1200 mm",
"client": total(client_records, "pipe_1200_mm"),
"sub": total(sub_records, "pipe_1200_mm")
}
]
return jsonify({
"title": "Pipe Laying Comparison",
"y_title": "Pipe Length (Mtr)",
"labels": [x["label"] for x in chart_data],
"client_qty": [x["client"] for x in chart_data],
"sub_qty": [x["sub"] for x in chart_data]
})

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

@@ -1,5 +1,6 @@
from flask import Blueprint, render_template, send_from_directory, abort, current_app from flask import Blueprint, render_template, send_from_directory, abort, current_app
from app.utils.helpers import login_required from app.utils.helpers import login_required
from app.utils.file_utils import get_download_format_folder
import os import os
file_format_bp = Blueprint("file_format", __name__) file_format_bp = Blueprint("file_format", __name__)
@@ -14,10 +15,7 @@ def download_format():
@login_required @login_required
def download_excel_format(filename): def download_excel_format(filename):
download_folder = os.path.join( download_folder = get_download_format_folder()
current_app.root_path, "static", "downloads/format"
)
file_path = os.path.join(download_folder, filename) file_path = os.path.join(download_folder, filename)
if not os.path.exists(file_path): if not os.path.exists(file_path):

View File

@@ -43,4 +43,4 @@ def client_import_file():
flash(msg, "success" if success else "danger") flash(msg, "success" if success else "danger")
return render_template("file_import_client.html", title="Client File Import", subcontractors=subcontractors) return render_template("file_import_client.html", title="Client File Import", subcontractors=subcontractors)

View File

@@ -1,10 +1,13 @@
import pandas as pd import pandas as pd
import io import io
from flask import Blueprint, render_template, request, send_file, flash from flask import Blueprint, render_template, request, send_file, flash, jsonify,redirect, url_for
from app.utils.helpers import login_required 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 from app.models.subcontractor_model import Subcontractor
from app.models.manhole_excavation_model import ManholeExcavation from app.models.manhole_excavation_model import ManholeExcavation
from app.models.trench_excavation_model import TrenchExcavation from app.models.trench_excavation_model import TrenchExcavation
from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber
@@ -15,10 +18,652 @@ from app.models.tr_ex_client_model import TrenchExcavationClient
from app.models.mh_dc_client_model import ManholeDomesticChamberClient from app.models.mh_dc_client_model import ManholeDomesticChamberClient
from app.models.laying_client_model import LayingClient from app.models.laying_client_model import LayingClient
from app.services.abstract_service import AbstractReportService
# --- BLUEPRINT DEFINITION --- # --- BLUEPRINT DEFINITION ---
file_report_bp = Blueprint("file_report", __name__, url_prefix="/file") file_report_bp = Blueprint("file_report", __name__, url_prefix="/file")
# ---------------- LOCATION HELPERS ----------------
WORK_MODELS = [TrenchExcavation, ManholeExcavation, ManholeDomesticChamber, Laying]
def get_distinct_locations():
"""Union of distinct, non-empty Location values across all 4 work tables."""
locations = set()
for Model in WORK_MODELS:
rows = db.session.query(Model.Location).distinct().all()
for (loc,) in rows:
if loc and loc.strip():
locations.add(loc.strip())
return sorted(locations)
def get_subcontractors_for_location(location):
"""Return Subcontractor objects that have at least one work record
(in any of the 4 category tables) at the given location. If no
location is given, returns every subcontractor. Shared by the AJAX
endpoint below and by the server-rendered dropdown so the list is
correct even before/without JS running (e.g. on page reload or a
validation-error re-render).
Matching is case- and whitespace-insensitive, since Location is a
free-text field and stored values can drift ("Pune" vs "PUNE " etc.)
even though the dropdown options themselves come from a distinct
query and look identical."""
location = (location or "").strip()
if not location:
return Subcontractor.query.order_by(Subcontractor.subcontractor_name).all()
target = location.upper()
sc_ids = set()
for Model in WORK_MODELS:
rows = (
db.session.query(Model.subcontractor_id)
.filter(func.upper(func.trim(Model.Location)) == target)
.distinct()
.all()
)
for (sid,) in rows:
if sid:
sc_ids.add(sid)
if not sc_ids:
return []
return (
Subcontractor.query.filter(Subcontractor.id.in_(sc_ids))
.order_by(Subcontractor.subcontractor_name)
.all()
)
@file_report_bp.route("/get_subcontractors_by_location")
@login_required
def get_subcontractors_by_location():
"""AJAX endpoint: return subcontractors that have at least one work
record (in any of the 4 category tables) at the given location."""
location = request.args.get("location", "").strip()
subs = get_subcontractors_for_location(location)
return jsonify([{"id": s.id, "name": s.subcontractor_name} for s in subs])
# ---------------- ACTION COLUMN ----------------
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-model="{model_key}" data-id="{x}">'
))
df["Update"] = df["Id"].apply(
lambda x: f'<a href="/file/edit/{model_key}/{x}" class="btn btn-sm btn-warning edit-btn"><i class="bi bi-pencil-square"></i> Edit</a>'
)
df["Delete"] = df["Id"].apply(
lambda x: f'<button class="btn btn-sm btn-danger delete-btn" data-id="{x}" data-model="{model_key}">Delete</button>'
)
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
# ---------------- FETCH ----------------
class SubcontractorBill:
def __init__(self):
self.df_tr = pd.DataFrame()
self.df_mh = pd.DataFrame()
self.df_dc = pd.DataFrame()
self.df_laying = pd.DataFrame()
# self.df_abstract = pd.DataFrame() # NEW
def Fetch(self, RA_Bill_No=None, subcontractor_id=None, location=None, mh_no=None):
filters = {}
if subcontractor_id:
filters["subcontractor_id"] = subcontractor_id
if RA_Bill_No:
filters["RA_Bill_No"] = RA_Bill_No
# Fetch data in database
trench = TrenchExcavation.query.filter_by(**filters).all()
mh = ManholeExcavation.query.filter_by(**filters).all()
dc = ManholeDomesticChamber.query.filter_by(**filters).all()
lay = Laying.query.filter_by(**filters).all()
# LOCATION FILTER
if location:
search = location.strip().lower()
trench = [
t for t in trench
if search in (t.Location or "").strip().lower()
]
mh = [
t for t in mh
if search in (t.Location or "").strip().lower()
]
dc = [
t for t in dc
if search in (t.Location or "").strip().lower()
]
lay = [
t for t in lay
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])
self.df_dc = pd.DataFrame([c.serialize() for c in dc])
self.df_laying = pd.DataFrame([c.serialize() for c in lay])
drop_cols = ["11", "_sa_instance_state", "subcontractor_id" , "created_at"]
# 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 = ""
if subcontractor_id:
sc = Subcontractor.query.get(subcontractor_id)
if sc:
name = sc.subcontractor_name
# ---------------- DELETE ----------------
@file_report_bp.route("/delete_records", methods=["POST"])
@login_required
def delete_records():
data = request.json or {}
model = data.get("model")
ids = data.get("ids", [])
model_map = {
"tr": TrenchExcavation,
"mh": ManholeExcavation,
"dc": ManholeDomesticChamber,
"laying": Laying
}
ModelClass = model_map.get(model)
# validate model BEFORE using it
if not ModelClass:
return jsonify({"status": "error", "message": f"Invalid model '{model}'"}), 400
if not ids:
return jsonify({"status": "error", "message": "No IDs provided"}), 400
try:
for record_id in ids:
obj = ModelClass.query.get(record_id)
if obj:
db.session.delete(obj)
db.session.commit()
return jsonify({"status": "success"})
except Exception as e:
db.session.rollback()
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):
model_map = {
"tr": TrenchExcavation,
"mh": ManholeExcavation,
"dc": ManholeDomesticChamber,
"laying": Laying
}
ModelClass = model_map.get(model)
if not ModelClass:
flash("Invalid Model.", "danger")
return redirect(url_for("file_report.report_file"))
record = ModelClass.query.get_or_404(record_id)
if request.method == "POST":
# Update all fields except id
for column in record.__table__.columns:
if column.name == "id":
continue
if column.name in request.form:
setattr(record, column.name, request.form.get(column.name))
try:
db.session.commit()
flash("Record updated successfully.", "success")
# ✅ fixed: correct blueprint name
return redirect(url_for("file_report.report_file"))
except Exception as e:
db.session.rollback()
flash(str(e), "danger")
return render_template(
"edit_record.html",
record=record,
model=model
)
@file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"])
@login_required
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") 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")
# 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,
locations=locations,
has_data=has_data
)
selected_sc_id = subcontractor_id
bill = SubcontractorBill()
if action == "excel_all":
bill.Fetch(subcontractor_id=subcontractor_id)
else:
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
# -----------------------------------------
abstract_service = AbstractReportService(
subcontractor_id=subcontractor_id,
ra_bill_no=ra_bill_no
)
abstract_html = abstract_service.generate_html()
# ---------------- CATEGORY FILTER ----------------
if category == "tr":
bill.df_mh = bill.df_dc = bill.df_laying = pd.DataFrame()
elif category == "mh":
bill.df_tr = bill.df_dc = bill.df_laying = pd.DataFrame()
elif category == "dc":
bill.df_tr = bill.df_mh = bill.df_laying = pd.DataFrame()
elif category == "laying":
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
# ===================================================
if action in ["excel", "excel_all"]:
output = io.BytesIO()
with pd.ExcelWriter(output,engine="xlsxwriter") as writer:
workbook = writer.book
abstract = AbstractReportService(subcontractor_id=subcontractor_id,ra_bill_no=ra_bill_no)
abstract.generate(workbook)
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=filename,
as_attachment=True
)
# ===================================================
# PDF
# ===================================================
if action == "pdf":
flash(
"PDF Export Coming Soon.",
"info"
)
# ===================================================
# ADD ACTIONS
# ===================================================
bill.df_tr = add_action_columns(bill.df_tr, "tr")
bill.df_mh = add_action_columns(bill.df_mh, "mh")
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 = (
"table "
"table-bordered "
"table-hover "
"table-striped "
"table-sm "
"align-middle "
"datatable "
"text-nowrap "
"mb-0"
)
# This are showing on web tables
tables = {
"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,
has_data=has_data
)
# --- Client class --- # --- Client class ---
class ClientBill: class ClientBill:
def __init__(self): def __init__(self):
@@ -43,171 +688,155 @@ class ClientBill:
if not df.empty: if not df.empty:
df.drop(columns=drop_cols, errors="ignore", inplace=True) df.drop(columns=drop_cols, errors="ignore", inplace=True)
# --- Subcontractor class ---
class SubcontractorBill:
def __init__(self): # --- CLIENT REPORT (PREVIEW + DOWNLOAD) ---
self.df_tr = pd.DataFrame()
self.df_mh = pd.DataFrame()
self.df_dc = pd.DataFrame()
self.df_laying = pd.DataFrame()
def Fetch(self, RA_Bill_No=None, subcontractor_id=None):
filters = {}
if subcontractor_id:
filters["subcontractor_id"] = subcontractor_id
if RA_Bill_No:
filters["RA_Bill_No"] = RA_Bill_No
trench = TrenchExcavation.query.filter_by(**filters).all()
mh = ManholeExcavation.query.filter_by(**filters).all()
dc = ManholeDomesticChamber.query.filter_by(**filters).all()
lay = Laying.query.filter_by(**filters).all()
self.df_tr = pd.DataFrame([c.serialize() for c in trench])
self.df_mh = pd.DataFrame([c.serialize() for c in mh])
self.df_dc = pd.DataFrame([c.serialize() for c in dc])
self.df_laying = pd.DataFrame([c.serialize() for c in lay])
drop_cols = ["id", "created_at", "_sa_instance_state"]
for df in [self.df_tr, self.df_mh, self.df_dc, self.df_laying]:
if not df.empty:
df.drop(columns=drop_cols, errors="ignore", inplace=True)
# --- subcontractor report only ---
@file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"])
@login_required
def report_file():
subcontractors = Subcontractor.query.all()
tables = None
selected_sc_id = None
ra_bill_no = None
download_all = False
if request.method == "POST":
subcontractor_id = request.form.get("subcontractor_id")
ra_bill_no = request.form.get("ra_bill_no")
download_all = request.form.get("download_all") == "true"
action = request.form.get("action")
if not subcontractor_id:
flash("Please select a subcontractor.", "danger")
return render_template("report.html", subcontractors=subcontractors)
subcontractor = Subcontractor.query.get(subcontractor_id)
bill_gen = SubcontractorBill()
if download_all:
bill_gen.Fetch(subcontractor_id=subcontractor_id)
file_name = f"{subcontractor.subcontractor_name}_ALL_BILLS.xlsx"
else:
if not ra_bill_no:
flash("Please enter an RA Bill Number.", "danger")
return render_template("report.html", subcontractors=subcontractors)
bill_gen.Fetch(RA_Bill_No=ra_bill_no, subcontractor_id=subcontractor_id)
file_name = f"{subcontractor.subcontractor_name}_RA_{ra_bill_no}_Report.xlsx"
if bill_gen.df_tr.empty and bill_gen.df_mh.empty and bill_gen.df_dc.empty:
flash("No data found for this selection.", "warning")
return render_template("report.html", subcontractors=subcontractors)
# If download is clicked, return file immediately
if action == "download":
output = io.BytesIO()
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
bill_gen.df_tr.to_excel(writer, index=False, sheet_name="Tr.Ex.")
bill_gen.df_mh.to_excel(writer, index=False, sheet_name="MH.Ex.")
bill_gen.df_dc.to_excel(writer, index=False, sheet_name="MH & DC")
bill_gen.df_laying.to_excel(writer, index=False, sheet_name="Laying")
output.seek(0)
return send_file(output, download_name=file_name, as_attachment=True)
# We add bootstrap classes directly to the pandas output
table_classes = "table table-bordered table-striped table-hover table-sm mb-0"
tables = {
"tr": bill_gen.df_tr.to_html(classes=table_classes, index=False),
"mh": bill_gen.df_mh.to_html(classes=table_classes, index=False),
"dc": bill_gen.df_dc.to_html(classes=table_classes, index=False),
"laying": bill_gen.df_laying.to_html(classes=table_classes, index=False)
}
selected_sc_id = subcontractor_id
return render_template(
"subcontractor_report.html",
subcontractors=subcontractors,
tables=tables,
selected_sc_id=selected_sc_id,
ra_bill_no=ra_bill_no,
download_all=download_all
)
# --- client report only ---
@file_report_bp.route("/client_report", methods=["GET", "POST"]) @file_report_bp.route("/client_report", methods=["GET", "POST"])
@login_required @login_required
def client_vs_all_subcontractor(): def client_report():
tables = {"tr": None, "mh": None, "dc": None}
tables = {"tr": None, "mh": None, "dc": None, "laying": None}
ra_val = "" ra_val = ""
if request.method == "POST": if request.method == "POST":
# ⚠ MUST match HTML name
RA_Bill_No = request.form.get("RA_Bill_No") RA_Bill_No = request.form.get("RA_Bill_No")
action = request.form.get("action")
ra_val = RA_Bill_No ra_val = RA_Bill_No
if not RA_Bill_No: if not RA_Bill_No:
flash("Please enter RA Bill No.", "danger") flash("Please enter RA Bill No.", "danger")
return render_template("generate_comparison_client_vs_subcont.html", tables=tables, ra_val=ra_val)
clientBill = ClientBill()
clientBill.Fetch(RA_Bill_No=RA_Bill_No)
contractorBill = SubcontractorBill()
contractorBill.Fetch(RA_Bill_No=RA_Bill_No)
# --- SAFETY CHECK: Verify data exists before merging ---
if clientBill.df_tr.empty and clientBill.df_mh.empty:
flash(f"No Client records found for RA Bill {RA_Bill_No}", "warning")
return render_template("generate_comparison_client_vs_subcont.html", tables=tables, ra_val=ra_val)
qty_cols = [...] # (Keep your existing list)
mh_dc_qty_cols = [...] # (Keep your existing list)
mh_lay_qty_cols =[...]
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) return render_template("client_report.html", tables=tables, ra_val=ra_val)
# -------- FETCH CLIENT DATA --------
# Convert to HTML for preview bill_gen = ClientBill()
tables["tr"] = df_tr_cmp.to_html(classes='table table-striped table-hover table-sm', index=False) bill_gen.Fetch(RA_Bill_No)
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) # If no data
tables["laying"] = df_lay_cmp.to_html(classes='table table-striped table-hover table-sm', index=False) if (
bill_gen.df_tr.empty and
bill_gen.df_mh.empty and
return render_template("client_report.html", tables=tables, ra_val=ra_val) bill_gen.df_dc.empty and
bill_gen.df_laying.empty
):
flash(f"No Client records found for RA Bill {RA_Bill_No}", "warning")
return render_template("client_report.html", tables=tables, ra_val=ra_val)
# -------- DOWNLOAD --------
if action == "download":
output = io.BytesIO()
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
bill_gen.df_tr.to_excel(writer, index=False, sheet_name="Trench")
bill_gen.df_mh.to_excel(writer, index=False, sheet_name="MH")
bill_gen.df_dc.to_excel(writer, index=False, sheet_name="MH & DC")
bill_gen.df_laying.to_excel(writer, index=False, sheet_name="Laying")
output.seek(0)
return send_file(
output,
download_name=f"Client_RA_{RA_Bill_No}_Report.xlsx",
as_attachment=True
)
# -------- PREVIEW --------
table_class = "table table-bordered table-striped table-hover table-sm"
tables["tr"] = bill_gen.df_tr.to_html(classes=table_class, index=False)
tables["mh"] = bill_gen.df_mh.to_html(classes=table_class, index=False)
tables["dc"] = bill_gen.df_dc.to_html(classes=table_class, index=False)
tables["laying"] = bill_gen.df_laying.to_html(classes=table_class, index=False)
return render_template("client_report.html", tables=tables, ra_val=ra_val)
def format_column_names(df):
if df.empty:
return df
new_columns = []
for col in df.columns:
# ----------------------------------------
# Pipe columns
# pipe_150_mm -> Pipe 150 MM
# ----------------------------------------
if RegularExpression.PIPE_MM_PATTERN.match(col):
m = re.match(r"pipe_(\d+)_mm", col)
new_columns.append(f"Pipe {m.group(1)} MM")
continue
# ----------------------------------------
# Domestic Chamber
# d_0_to_0_75 -> 0.00 To 0.75
# d_1_5_to_3_0 -> 1.50 To 3.00
# ----------------------------------------
if RegularExpression.D_RANGE_PATTERN.match(col):
value = col[2:] # remove d_
value = re.sub(
r'(\d+)_(\d+)',
lambda m: f"{m.group(1)}.{m.group(2)}",
value
)
value = value.replace("_to_", " To ")
new_columns.append(value)
continue
# ----------------------------------------
# Total columns
# Soft_Murum_0_to_1_5_total
# ->
# Soft Murum 0 To 1.5 Total
# ----------------------------------------
if RegularExpression.STR_TOTAL_PATTERN.match(col):
value = col[:-6] # remove _total
value = re.sub(
r'(\d+)_(\d+)',
lambda m: f"{m.group(1)}.{m.group(2)}",
value
)
value = value.replace("_to_", " To ")
value = value.replace("_", " ")
new_columns.append(value.title() + " Total")
continue
# ----------------------------------------
# General columns
# ----------------------------------------
value = col.replace("_", " ").title()
replacements = {
"Mh No": "MH No",
"Ra Bill No": "RA Bill No",
"Cc Length": "CC Length",
"Id Of Mh M": "ID of MH (m)",
"Pipe Dia Mm": "Pipe Dia (MM)",
"Mh Top Level": "MH Top Level",
"Upto Il Depth": "Upto IL Depth",
"Actual Trench Length": "Actual Trench Length",
"Ground Level": "Ground Level",
"Invert Level": "Invert Level",
"Ex Dia Of Manhole": "External Dia of Manhole",
"Area Of Manhole": "Area of Manhole",
"Depth Of Mh": "Depth of MH",
}
value = replacements.get(value, value)
new_columns.append(value)
df.columns = new_columns
return df

View File

@@ -1,26 +1,35 @@
from flask import Blueprint, render_template, request, send_file, flash from flask import Blueprint, render_template, request, send_file, flash
from collections import defaultdict
import pandas as pd import pandas as pd
import io import io
from app.utils.helpers import login_required
from app.utils.regex_utils import RegularExpression
# Contractor models import
from app.models.subcontractor_model import Subcontractor from app.models.subcontractor_model import Subcontractor
from app.models.trench_excavation_model import TrenchExcavation from app.models.trench_excavation_model import TrenchExcavation
from app.models.tr_ex_client_model import TrenchExcavationClient
from app.models.manhole_excavation_model import ManholeExcavation from app.models.manhole_excavation_model import ManholeExcavation
from app.models.mh_ex_client_model import ManholeExcavationClient
from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber
from app.models.laying_model import Laying
# Client models import
from app.models.tr_ex_client_model import TrenchExcavationClient
from app.models.mh_ex_client_model import ManholeExcavationClient
from app.models.mh_dc_client_model import ManholeDomesticChamberClient from app.models.mh_dc_client_model import ManholeDomesticChamberClient
from app.utils.helpers import login_required from app.models.laying_client_model import LayingClient
generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report") generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report")
# NORMALIZER # NORMALIZER
def normalize_key(value): def normalize_key(value):
if value is None: if value is None:
return None return None
return str(value).strip().upper() return str(value).strip().upper()
# HEADER FORMATTER # HEADER FORMATTER
def format_header(header): def format_header(header):
if "-" in header: if "-" in header:
@@ -28,11 +37,11 @@ def format_header(header):
prefix = prefix.title() prefix = prefix.title()
else: else:
prefix, rest = None, header prefix, rest = None, header
parts = rest.split("_") parts = rest.split("_")
result = [] result = []
i = 0 i = 0
while i < len(parts): while i < len(parts):
if i + 1 < len(parts) and parts[i].isdigit() and parts[i + 1].isdigit(): if i + 1 < len(parts) and parts[i].isdigit() and parts[i + 1].isdigit():
result.append(f"{parts[i]}.{parts[i + 1]}") result.append(f"{parts[i]}.{parts[i + 1]}")
@@ -40,98 +49,122 @@ def format_header(header):
else: else:
result.append(parts[i].title()) result.append(parts[i].title())
i += 1 i += 1
final_text = " ".join(result) final_text = " ".join(result)
return f"{prefix}-{final_text}" if prefix else final_text return f"{prefix}-{final_text}" if prefix else final_text
# LOOKUP CREATOR # LOOKUP CREATOR
def make_lookup(rows, key_field): def make_lookup(rows, key_field):
lookup = {} lookup = {}
for r in rows: for r in rows:
location = normalize_key(r.get("Location")) location = normalize_key(r.get("Location"))
key_val = normalize_key(r.get(key_field)) key_val = normalize_key(r.get(key_field))
if location and key_val: if location and key_val:
lookup[(location, key_val)] = r lookup.setdefault((location, key_val), []).append(r)
return lookup return lookup
# COMPARISON BUILDER # COMPARISON BUILDER
def build_comparison(client_rows, contractor_rows, key_field): def build_comparison(client_rows, contractor_rows, key_field):
contractor_lookup = make_lookup(contractor_rows, key_field) contractor_lookup = make_lookup(contractor_rows, key_field)
output = [] output = []
used_index = defaultdict(int) # 🔥 THIS FIXES YOUR ISSUE
for c in client_rows: for c in client_rows:
client_location = normalize_key(c.get("Location")) client_location = normalize_key(c.get("Location"))
client_key = normalize_key(c.get(key_field)) client_key = normalize_key(c.get(key_field))
if not client_location or not client_key: if not client_location or not client_key:
continue continue
s = contractor_lookup.get((client_location, client_key)) subs = contractor_lookup.get((client_location, client_key))
if not s: if not subs:
continue continue
idx = used_index[(client_location, client_key)]
# ❗ If subcontractor rows are exhausted, skip
if idx >= len(subs):
continue
s = subs[idx] # ✅ take NEXT subcontractor row
used_index[(client_location, client_key)] += 1
# ---- totals ----
client_total = sum( client_total = sum(
float(v or 0) float(v or 0)
for k, v in c.items() for k, v in c.items()
if k.endswith("_total") if k.endswith("_total")
or RegularExpression.D_RANGE_PATTERN.match(k)
or RegularExpression.PIPE_MM_PATTERN.match(k)
) )
sub_total = sum( sub_total = sum(
float(v or 0) float(v or 0)
for k, v in s.items() for k, v in s.items()
if k.endswith("_total") if k.endswith("_total")
or RegularExpression.D_RANGE_PATTERN.match(k)
or RegularExpression.PIPE_MM_PATTERN.match(k)
) )
diff = client_total - sub_total
row = { row = {
"Location": client_location, "Location": client_location,
key_field.replace("_", " "): client_key key_field.replace("_", " "): client_key
} }
# CLIENT DATA
for k, v in c.items(): for k, v in c.items():
if k in ["id", "created_at"]: if k not in ["id", "created_at"]:
continue row[f"Client-{k}"] = v
row[f"Client-{k}"] = v
row["Client-Total"] = round(client_total, 2) row["Client-Total"] = round(client_total, 2)
row[" "] = "" row[" "] = ""
# SUBCONTRACTOR DATA
for k, v in s.items(): for k, v in s.items():
if k in ["id", "created_at", "subcontractor_id"]: if k not in ["id", "created_at", "subcontractor_id"]:
continue row[f"Subcontractor-{k}"] = v
row[f"Subcontractor-{k}"] = v
row["Subcontractor-Total"] = round(sub_total, 2) row["Subcontractor-Total"] = round(sub_total, 2)
row["Diff"] = round(diff, 2) row["Diff"] = round(client_total - sub_total, 2)
output.append(row) output.append(row)
df = pd.DataFrame(output) df = pd.DataFrame(output)
# formatting headers
df.columns = [format_header(col) for col in df.columns] df.columns = [format_header(col) for col in df.columns]
return df return df
# EXCEL SHEET WRITER # EXCEL SHEET WRITER
def write_sheet(writer, df, sheet_name, subcontractor_name): def write_sheet(writer, df, sheet_name, subcontractor_name):
workbook = writer.book workbook = writer.book
# write dataframe (data already correct)
df.to_excel(writer, sheet_name=sheet_name, index=False, startrow=3) df.to_excel(writer, sheet_name=sheet_name, index=False, startrow=3)
ws = writer.sheets[sheet_name] ws = writer.sheets[sheet_name]
# formats
title_fmt = workbook.add_format({"bold": True, "font_size": 14}) title_fmt = workbook.add_format({"bold": True, "font_size": 14})
client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#D9EDF7"}) client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#D9EDF7"})
sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7E1D9"}) sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7E1D9"})
total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#FFF2CC"}) total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#FFF2CC"})
diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#E2EFDA"}) diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#E2EFDA"})
default_header_fmt = workbook.add_format({"bold": True,"border": 1,"bg_color": "#E7E6E6","align": "center","valign": "vcenter"}) default_header_fmt = workbook.add_format({
"bold": True,
"border": 1,
"bg_color": "#E7E6E6",
"align": "center",
"valign": "vcenter"
})
# titles
ws.merge_range( ws.merge_range(
0, 0, 0, len(df.columns) - 1, 0, 0, 0, len(df.columns) - 1,
"CLIENT vs SUBCONTRACTOR", "CLIENT vs SUBCONTRACTOR",
@@ -143,6 +176,7 @@ def write_sheet(writer, df, sheet_name, subcontractor_name):
title_fmt title_fmt
) )
# header formatting
for col_num, col_name in enumerate(df.columns): for col_num, col_name in enumerate(df.columns):
if col_name.startswith("Client-"): if col_name.startswith("Client-"):
ws.write(3, col_num, col_name, client_fmt) ws.write(3, col_num, col_name, client_fmt)
@@ -157,57 +191,63 @@ def write_sheet(writer, df, sheet_name, subcontractor_name):
ws.set_column(col_num, col_num, 20) ws.set_column(col_num, col_num, 20)
# REPORT ROUTE # REPORT ROUTE
@generate_report_bp.route("/comparison_report", methods=["GET", "POST"]) @generate_report_bp.route("/comparison_report", methods=["GET", "POST"])
@login_required @login_required
def comparison_report(): def comparison_report():
subcontractors = Subcontractor.query.all() subcontractors = Subcontractor.query.all()
if request.method == "POST": if request.method == "POST":
subcontractor_id = request.form.get("subcontractor_id") subcontractor_id = request.form.get("subcontractor_id")
if not subcontractor_id: if not subcontractor_id:
flash("Please select subcontractor", "danger") flash("Please select subcontractor", "danger")
return render_template("generate_comparison_report.html",subcontractors=subcontractors) return render_template("generate_comparison_report.html",subcontractors=subcontractors)
subcontractor = Subcontractor.query.get_or_404(subcontractor_id) subcontractor = Subcontractor.query.get_or_404(subcontractor_id)
# -------- DATA -------- # -------- DATA --------
tr_client = [r.serialize() for r in TrenchExcavationClient.query.all()] tr_client = [r.serialize() for r in TrenchExcavationClient.query.all()]
tr_sub = [r.serialize() for r in TrenchExcavation.query.filter_by( tr_sub = [r.serialize() for r in TrenchExcavation.query.filter_by(
subcontractor_id=subcontractor_id subcontractor_id=subcontractor_id
).all()] ).all()]
df_tr = build_comparison(tr_client, tr_sub, "MH_NO") df_tr = build_comparison(tr_client, tr_sub, "MH_NO")
mh_client = [r.serialize() for r in ManholeExcavationClient.query.all()] mh_client = [r.serialize() for r in ManholeExcavationClient.query.all()]
mh_sub = [r.serialize() for r in ManholeExcavation.query.filter_by( mh_sub = [r.serialize() for r in ManholeExcavation.query.filter_by(
subcontractor_id=subcontractor_id subcontractor_id=subcontractor_id
).all()] ).all()]
df_mh = build_comparison(mh_client, mh_sub, "MH_NO") df_mh = build_comparison(mh_client, mh_sub, "MH_NO")
dc_client = [r.serialize() for r in ManholeDomesticChamberClient.query.all()] dc_client = [r.serialize() for r in ManholeDomesticChamberClient.query.all()]
dc_sub = [r.serialize() for r in ManholeDomesticChamber.query.filter_by( dc_sub = [r.serialize() for r in ManholeDomesticChamber.query.filter_by(
subcontractor_id=subcontractor_id subcontractor_id=subcontractor_id
).all()] ).all()]
df_dc = build_comparison(dc_client, dc_sub, "MH_NO") df_dc = build_comparison(dc_client, dc_sub, "MH_NO")
lay_client = [r.serialize() for r in LayingClient.query.all()]
lay_sub = [r.serialize() for r in Laying.query.filter_by(
subcontractor_id=subcontractor_id
).all()]
df_lay = build_comparison(lay_client, lay_sub, "MH_NO")
# -------- EXCEL -------- # -------- EXCEL --------
output = io.BytesIO() output = io.BytesIO()
filename = f"{subcontractor.subcontractor_name}_Comparison_Report.xlsx" filename = f"{subcontractor.subcontractor_name}_Comparison_Report.xlsx"
with pd.ExcelWriter(output, engine="xlsxwriter") as writer: with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
write_sheet(writer, df_tr, "Tr.Ex", subcontractor.subcontractor_name) write_sheet(writer, df_tr, "Tr.Ex", subcontractor.subcontractor_name)
write_sheet(writer, df_mh, "Mh.Ex", subcontractor.subcontractor_name) write_sheet(writer, df_mh, "Mh.Ex", subcontractor.subcontractor_name)
write_sheet(writer, df_dc, "MH & DC", subcontractor.subcontractor_name) write_sheet(writer, df_dc, "MH & DC", subcontractor.subcontractor_name)
write_sheet(writer, df_lay, "Laying", subcontractor.subcontractor_name)
output.seek(0)
output.seek(0)
return send_file( return send_file(
output, output,
as_attachment=True, as_attachment=True,
download_name=filename, download_name=filename,
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
) )
return render_template("generate_comparison_report.html",subcontractors=subcontractors) return render_template("generate_comparison_report.html",subcontractors=subcontractors)

View File

@@ -1,191 +1,159 @@
from flask import Blueprint, render_template, request, redirect, flash from flask import Blueprint, render_template, request, redirect, flash, current_app, url_for
from app import db from app.services.db_service import db
from app.models.subcontractor_model import Subcontractor from app.models.subcontractor_model import Subcontractor
from app.utils.helpers import login_required from app.utils.helpers import login_required
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor") subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
# ---------------- ADD -----------------
@subcontractor_bp.route("/add")
@login_required
def add_subcontractor():
return render_template("subcontractor/add.html")
@subcontractor_bp.route("/save", methods=["POST"])
@login_required
def save_subcontractor():
# 1. Get and clean the name from the form
name = request.form.get("subcontractor_name", "").strip()
# 2. Basic validation: Ensure the name isn't empty
if not name:
flash("Subcontractor name cannot be empty.", "danger")
return redirect("/subcontractor/add")
# 3. Check if a subcontractor with this name already exists
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
if existing_sub:
flash(f"Subcontractor with name '{name}' already exists!", "danger")
return redirect("/subcontractor/add")
# 4. If no duplicate is found, proceed to save
try:
subcontractor = Subcontractor(
subcontractor_name=name,
contact_person=request.form.get("contact_person"),
mobile_no=request.form.get("mobile_no"),
email_id=request.form.get("email_id"),
gst_no=request.form.get("gst_no")
)
db.session.add(subcontractor)
db.session.commit()
flash("Subcontractor added successfully!", "success")
except Exception as e:
db.session.rollback()
flash("An error occurred while saving. Please try again.", "danger")
return redirect("/subcontractor/list")
# ---------------- LIST -----------------
@subcontractor_bp.route("/list")
@login_required
def subcontractor_list():
subcontractors = Subcontractor.query.all()
return render_template("subcontractor/list.html", subcontractors=subcontractors)
# ---------------- EDIT -----------------
@subcontractor_bp.route("/edit/<int:id>")
@login_required
def edit_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
return render_template("subcontractor/edit.html", subcontractor=subcontractor)
# ---------------- UPDATE -----------------
@subcontractor_bp.route("/update/<int:id>", methods=["POST"])
@login_required
def update_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
new_name = request.form.get("subcontractor_name")
# Check if the new name is taken by someone ELSE (not this current ID)
duplicate = Subcontractor.query.filter(
Subcontractor.subcontractor_name == new_name,
Subcontractor.id != id
).first()
if duplicate:
flash("Another subcontractor already uses this name.", "danger")
return redirect(f"/subcontractor/edit/{id}")
subcontractor.subcontractor_name = new_name
db.session.commit()
flash("Subcontractor updated successfully!", "success")
return redirect("/subcontractor/list")
# ---------------- DELETE -----------------
@subcontractor_bp.route("/delete/<int:id>")
@login_required
def delete_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
db.session.delete(subcontractor)
db.session.commit()
flash("Subcontractor deleted successfully!", "success")
return redirect("/subcontractor/list")
from flask import Blueprint, render_template, request, redirect, flash
from app import db
from app.models.subcontractor_model import Subcontractor
from app.utils.helpers import login_required
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
# ---------------- ADD ----------------- # ---------------- ADD -----------------
@subcontractor_bp.route("/add") @subcontractor_bp.route("/add")
@login_required @login_required
def add_subcontractor(): def add_subcontractor():
current_app.logger.info("Opened Add Subcontractor Page")
return render_template("subcontractor/add.html") return render_template("subcontractor/add.html")
# ---------------- SAVE -----------------
@subcontractor_bp.route("/save", methods=["POST"]) @subcontractor_bp.route("/save", methods=["POST"])
@login_required @login_required
def save_subcontractor(): def save_subcontractor():
name = request.form.get("subcontractor_name", "").strip() name = request.form.get("subcontractor_name", "").strip()
if not name: if not name:
current_app.logger.warning("Empty subcontractor name submitted")
flash("Subcontractor name cannot be empty.", "danger") flash("Subcontractor name cannot be empty.", "danger")
return redirect("/subcontractor/add") return redirect(url_for("subcontractor.add_subcontractor"))
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first() existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
if existing_sub: if existing_sub:
current_app.logger.warning(f"Duplicate subcontractor attempt: {name}")
flash(f"Subcontractor with name '{name}' already exists!", "danger") flash(f"Subcontractor with name '{name}' already exists!", "danger")
return redirect("/subcontractor/add") return redirect(url_for("subcontractor.add_subcontractor"))
try: try:
subcontractor = Subcontractor( subcontractor = Subcontractor(
subcontractor_name=name, subcontractor_name=name,
contact_person=request.form.get("contact_person"), contact_person=request.form.get("contact_person"),
address=request.form.get("address"),
mobile_no=request.form.get("mobile_no"), mobile_no=request.form.get("mobile_no"),
email_id=request.form.get("email_id"), email_id=request.form.get("email_id"),
gst_no=request.form.get("gst_no") gst_no=request.form.get("gst_no"),
pan_no=request.form.get("pan_no")
) )
db.session.add(subcontractor) db.session.add(subcontractor)
db.session.commit() db.session.commit()
current_app.logger.info(f"Subcontractor Created Successfully: {name}")
flash("Subcontractor added successfully!", "success") flash("Subcontractor added successfully!", "success")
except Exception as e: except Exception:
db.session.rollback() db.session.rollback()
flash("An error occurred while saving. Please try again.", "danger") current_app.logger.exception("Error while saving subcontractor")
flash("An error occurred while saving.", "danger")
return redirect("/subcontractor/list")
return redirect(url_for("subcontractor.subcontractor_list"))
# ---------------- LIST -----------------
# ---------------- LIST (UPDATED WITH PAGINATION) -----------------
@subcontractor_bp.route("/list") @subcontractor_bp.route("/list")
@login_required @login_required
def subcontractor_list(): def subcontractor_list():
subcontractors = Subcontractor.query.all()
return render_template("subcontractor/list.html", subcontractors=subcontractors) page = request.args.get("page", 1, type=int)
per_page = 10 # Change how many records per page
pagination = Subcontractor.query.order_by(
Subcontractor.created_at
).paginate(
page=page,
per_page=per_page,
error_out=False
)
subcontractors = pagination.items
current_app.logger.info(f"Viewed Subcontractor List - Page {page}")
return render_template(
"subcontractor/list.html",
subcontractors=subcontractors,
pagination=pagination
)
# ---------------- EDIT ----------------- # ---------------- EDIT -----------------
@subcontractor_bp.route("/edit/<int:id>") @subcontractor_bp.route("/edit/<int:id>")
@login_required @login_required
def edit_subcontractor(id): def edit_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id) subcontractor = Subcontractor.query.get_or_404(id)
current_app.logger.info(f"Editing Subcontractor ID: {id}")
return render_template("subcontractor/edit.html", subcontractor=subcontractor) return render_template("subcontractor/edit.html", subcontractor=subcontractor)
# ---------------- UPDATE ----------------- # ---------------- UPDATE -----------------
@subcontractor_bp.route("/update/<int:id>", methods=["POST"]) @subcontractor_bp.route("/update/<int:id>", methods=["POST"])
@login_required @login_required
def update_subcontractor(id): def update_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id) subcontractor = Subcontractor.query.get_or_404(id)
new_name = request.form.get("subcontractor_name") new_name = request.form.get("subcontractor_name", "").strip()
# Check if the new name is taken by someone ELSE (not this current ID)
duplicate = Subcontractor.query.filter( duplicate = Subcontractor.query.filter(
Subcontractor.subcontractor_name == new_name, Subcontractor.subcontractor_name == new_name,
Subcontractor.id != id Subcontractor.id != id
).first() ).first()
if duplicate: if duplicate:
current_app.logger.warning(f"Duplicate update attempt: {new_name}")
flash("Another subcontractor already uses this name.", "danger") flash("Another subcontractor already uses this name.", "danger")
return redirect(f"/subcontractor/edit/{id}") return redirect(url_for("subcontractor.edit_subcontractor", id=id))
subcontractor.subcontractor_name = new_name try:
db.session.commit() old_name = subcontractor.subcontractor_name
flash("Subcontractor updated successfully!", "success") subcontractor.subcontractor_name = new_name
return redirect("/subcontractor/list") subcontractor.contact_person = request.form.get("contact_person")
subcontractor.address = request.form.get("address")
subcontractor.mobile_no = request.form.get("mobile_no")
subcontractor.email_id = request.form.get("email_id")
subcontractor.gst_no = request.form.get("gst_no")
subcontractor.pan_no = request.form.get("pan_no")
subcontractor.status = request.form.get("status")
db.session.commit()
current_app.logger.info(f"Subcontractor Updated: {old_name}{new_name}")
flash("Subcontractor updated successfully!", "success")
except Exception:
db.session.rollback()
current_app.logger.exception("Error updating subcontractor")
flash("Update failed!", "danger")
return redirect(url_for("subcontractor.subcontractor_list"))
# ---------------- DELETE ----------------- # ---------------- DELETE -----------------
@subcontractor_bp.route("/delete/<int:id>") @subcontractor_bp.route("/delete/<int:id>")
@login_required @login_required
def delete_subcontractor(id): def delete_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id) subcontractor = Subcontractor.query.get_or_404(id)
db.session.delete(subcontractor) try:
db.session.commit() name = subcontractor.subcontractor_name
db.session.delete(subcontractor)
db.session.commit()
flash("Subcontractor deleted successfully!", "success") current_app.logger.info(f"Subcontractor Deleted: {name}")
return redirect("/subcontractor/list") flash("Subcontractor deleted successfully!", "success")
except Exception:
db.session.rollback()
current_app.logger.exception("Error deleting subcontractor")
flash("Delete failed!", "danger")
return redirect(url_for("subcontractor.subcontractor_list"))

View File

@@ -1,11 +0,0 @@
from flask import Blueprint, render_template
from app.services.user_service import UserService
from app.utils.helpers import login_required
user_bp = Blueprint("user", __name__, url_prefix="/user")
@user_bp.route("/list")
@login_required
def list_users():
users = UserService.get_all_users()
return render_template("users.html", users=users, title="Users")

50
app/routes/user_routes.py Normal file
View File

@@ -0,0 +1,50 @@
# from flask import Blueprint, render_template
# from app.services.user_service import UserService
# from app.utils.helpers import login_required
# from flask import current_app
# user_bp = Blueprint("user", __name__, url_prefix="/user")
# @user_bp.route("/list")
# @login_required
# def list_users():
# current_app.logger.info("User list viewed")
# users = UserService.get_all_users()
# return render_template("/user/users.html", users=users, title="Users | List")
from flask import (Blueprint,render_template,current_app,flash)
from app.services.user_service import UserService
from app.utils.helpers import login_required
from app.constants.messages import SuccessMessage, ErrorMessage
from app.constants.http_status import HTTPStatus
user_bp = Blueprint("user", __name__, url_prefix="/user")
# ==================================================
# User List
# ==================================================
@user_bp.route("/list", methods=["GET"])
@login_required
def list_users():
try:
current_app.logger.info("Fetching user list.")
users = UserService.get_all_users()
current_app.logger.info(f"User list loaded successfully. Total Users: {len(users)}")
return render_template("user/users.html", users=users, title="Users | List")
except Exception as e:
current_app.logger.exception("Failed to load user list.")
flash(ErrorMessage.INTERNAL_SERVER_ERROR,"danger")
return render_template("user/users.html",users=[],title="Users | List"), HTTPStatus.INTERNAL_SERVER_ERROR

View File

@@ -0,0 +1,359 @@
from sqlalchemy import func
from app import db
from app.models.subcontractor_model import Subcontractor
from app.models.trench_excavation_model import TrenchExcavation
from app.models.manhole_excavation_model import ManholeExcavation
from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber
from app.models.laying_model import Laying
class AbstractReportService:
def __init__(self, subcontractor_id=None, ra_bill_no=None):
self.subcontractor_id = subcontractor_id
self.ra_bill_no = ra_bill_no
# ---------------------------------------------------------
# FILTER
# ---------------------------------------------------------
def filters(self):
filters = {}
if self.subcontractor_id:
filters["subcontractor_id"] = self.subcontractor_id
if self.ra_bill_no:
filters["RA_Bill_No"] = self.ra_bill_no
return filters
# ---------------------------------------------------------
# CONTRACTOR
# ---------------------------------------------------------
def contractor_name(self):
if not self.subcontractor_id:
return ""
contractor = Subcontractor.query.get(self.subcontractor_id)
if contractor:
return contractor.subcontractor_name
return ""
# ---------------------------------------------------------
# WRITE ABSTRACT SHEET
# ---------------------------------------------------------
def generate(self, workbook):
worksheet = workbook.add_worksheet("Abstract")
title = workbook.add_format({
"bold": True,
"font_size": 16,
"align": "center",
"valign": "vcenter",
"border": 1
})
heading = workbook.add_format({
"bold": True,
"bg_color": "#D9EAD3",
"border": 1,
"align": "center"
})
cell = workbook.add_format({"border": 1})
number = workbook.add_format({"border": 1,"num_format": "#,##0.00"})
# -----------------------------------------------------
# HEADER
# -----------------------------------------------------
worksheet.merge_range(
"A1:D1",
"ABSTRACT OF QUANTITY",
title
)
worksheet.write("A3", "Contractor", heading)
worksheet.write("B3", self.contractor_name(), cell)
worksheet.write("C3", "RA Bill", heading)
worksheet.write("D3", self.ra_bill_no or "", cell)
worksheet.write_row(
"A5",
[
"Sr",
"Description",
"UOM",
"Qty"
],
heading
)
row = 5
sr = 1
# -----------------------------------------------------
# TRENCH
# -----------------------------------------------------
worksheet.write(row, 0, "")
worksheet.write(row, 1, "TRENCH EXCAVATION", heading)
row += 1
for item in self.trench_summary():
worksheet.write(row, 0, sr, cell)
worksheet.write(row, 1, item["Description"], cell)
worksheet.write(row, 2, item["UOM"], cell)
worksheet.write(row, 3, item["Qty"], number)
sr += 1
row += 1
# -----------------------------------------------------
# MANHOLE
# -----------------------------------------------------
worksheet.write(row, 1, "MANHOLE EXCAVATION", heading)
row += 1
for item in self.manhole_summary():
worksheet.write(row, 0, sr, cell)
worksheet.write(row, 1, item["Description"], cell)
worksheet.write(row, 2, item["UOM"], cell)
worksheet.write(row, 3, item["Qty"], number)
sr += 1
row += 1
# -----------------------------------------------------
# DOMESTIC CHAMBER
# -----------------------------------------------------
worksheet.write(row, 1, "DOMESTIC CHAMBER", heading)
row += 1
for item in self.domestic_summary():
worksheet.write(row, 0, sr, cell)
worksheet.write(row, 1, item["Description"], cell)
worksheet.write(row, 2, item["UOM"], cell)
worksheet.write(row, 3, item["Qty"], number)
sr += 1
row += 1
# -----------------------------------------------------
# PIPE LAYING
# -----------------------------------------------------
worksheet.write(row, 1, "PIPE LAYING", heading)
row += 1
for item in self.laying_summary():
worksheet.write(row, 0, sr, cell)
worksheet.write(row, 1, item["Description"], cell)
worksheet.write(row, 2, item["UOM"], cell)
worksheet.write(row, 3, item["Qty"], number)
sr += 1
row += 1
worksheet.set_column("A:A", 8)
worksheet.set_column("B:B", 55)
worksheet.set_column("C:C", 10)
worksheet.set_column("D:D", 18)
# ============================================================
# TRENCH
# ============================================================
def trench_summary(self):
f = self.filters()
data = [
("Soft Murum 0-1.5 mm","Cum",TrenchExcavation.Soft_Murum_0_to_1_5_total),
("Soft Murum 1.5-3.0 mm","Cum",TrenchExcavation.Soft_Murum_1_5_to_3_0_total),
("Soft Murum 3.0-4.5 mm","Cum",TrenchExcavation.Soft_Murum_3_0_to_4_5_total),
("Hard Murum 0-1.5 mm","Cum",TrenchExcavation.Hard_Murum_0_to_1_5_total),
("Hard Murum Above 1.5 mm","Cum",TrenchExcavation.Hard_Murum_1_5_and_above_total),
("Soft Rock 0-1.5 mm","Cum",TrenchExcavation.Soft_Rock_0_to_1_5_total),
("Soft Rock Above 1.5 mm","Cum",TrenchExcavation.Soft_Rock_1_5_and_above_total),
("Hard Rock 0-1.5 mm","Cum",TrenchExcavation.Hard_Rock_0_to_1_5_total),
("Hard Rock 1.5-3.0 mm","Cum",TrenchExcavation.Hard_Rock_1_5_to_3_0_total),
("Hard Rock 3.0-4.5 mm","Cum",TrenchExcavation.Hard_Rock_3_0_to_4_5_total),
("Hard Rock 4.5-6.0 mm","Cum",TrenchExcavation.Hard_Rock_4_5_to_6_0_total),
("Hard Rock 6.0-7.5 mm","Cum",TrenchExcavation.Hard_Rock_6_0_to_7_5_total),
]
return self.make_summary(data, TrenchExcavation, f)
# ============================================================
# MANHOLE
# ============================================================
def manhole_summary(self):
f = self.filters()
data = [
("Soft Murum 0-1.5 mm","Cum",ManholeExcavation.Soft_Murum_0_to_1_5_total),
("Soft Murum 1.5-3.0 mm","Cum",ManholeExcavation.Soft_Murum_1_5_to_3_0_total),
("Soft Murum 3.0-4.5 mm","Cum",ManholeExcavation.Soft_Murum_3_0_to_4_5_total),
("Hard Murum 0-1.5 mm","Cum",ManholeExcavation.Hard_Murum_0_to_1_5_total),
("Hard Murum Above 1.5 mm","Cum",ManholeExcavation.Hard_Murum_1_5_and_above_total),
("Soft Rock 0-1.5 mm","Cum",ManholeExcavation.Soft_Rock_0_to_1_5_total),
("Soft Rock Above 1.5 mm","Cum",ManholeExcavation.Soft_Rock_1_5_and_above_total),
("Hard Rock 0-1.5 mm","Cum",ManholeExcavation.Hard_Rock_0_to_1_5_total),
("Hard Rock 1.5-3.0 mm","Cum",ManholeExcavation.Hard_Rock_1_5_to_3_0_total),
("Hard Rock 3.0-4.5 mm","Cum",ManholeExcavation.Hard_Rock_3_0_to_4_5_total),
("Hard Rock 4.5-6.0 mm","Cum",ManholeExcavation.Hard_Rock_4_5_to_6_0_total),
("Hard Rock 6.0-7.5 mm","Cum",ManholeExcavation.Hard_Rock_6_0_to_7_5_total),
]
return self.make_summary(data, ManholeExcavation, f)
# ============================================================
# DOMESTIC CHAMBER
# ============================================================
def domestic_summary(self):
f = self.filters()
data = [
("0-0.75 mm","Nos",ManholeDomesticChamber.d_0_to_0_75),
("0.76-1.05 mm","Nos",ManholeDomesticChamber.d_0_76_to_1_05),
("1.06-1.65 mm","Nos",ManholeDomesticChamber.d_1_06_to_1_65),
("1.66-2.15 mm","Nos",ManholeDomesticChamber.d_1_66_to_2_15),
("2.16-2.65 mm","Nos",ManholeDomesticChamber.d_2_16_to_2_65),
("2.66-3.15 mm","Nos",ManholeDomesticChamber.d_2_66_to_3_15),
("3.16-3.65 mm","Nos",ManholeDomesticChamber.d_3_16_to_3_65),
("3.66-4.15 mm","Nos",ManholeDomesticChamber.d_3_66_to_4_15),
("4.16-4.65 mm","Nos",ManholeDomesticChamber.d_4_16_to_4_65),
("4.66-5.15 mm","Nos",ManholeDomesticChamber.d_4_66_to_5_15),
("5.16-5.65 mm","Nos",ManholeDomesticChamber.d_5_16_to_5_65),
("5.66-6.15 mm","Nos",ManholeDomesticChamber.d_5_66_to_6_15),
("6.16-6.65 mm","Nos",ManholeDomesticChamber.d_6_16_to_6_65),
("6.66-7.15 mm","Nos",ManholeDomesticChamber.d_6_66_to_7_15),
("7.16-7.65 mm","Nos",ManholeDomesticChamber.d_7_16_to_7_65),
("7.66-8.15 mm","Nos",ManholeDomesticChamber.d_7_66_to_8_15),
("8.16-8.65 mm","Nos",ManholeDomesticChamber.d_8_16_to_8_65),
("8.66-9.15 mm","Nos",ManholeDomesticChamber.d_8_66_to_9_15),
("9.16-9.65 mm","Nos",ManholeDomesticChamber.d_9_16_to_9_65),
]
return self.make_summary(data, ManholeDomesticChamber, f)
# ============================================================
# PIPE LAYING
# ============================================================
def laying_summary(self):
f = self.filters()
data = [
("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)
# ============================================================
# COMMON SUMMARY
# ============================================================
def make_summary(self, items, model, filters):
summary = []
for desc, uom, column in items:
qty = (db.session.query(func.sum(column)).filter_by(**filters).scalar())
summary.append({
"Description": desc,
"UOM": uom,
"Qty": float(qty or 0)
})
return summary
def generate_html(self):
html = """
<div class="table-responsive">
<table class="table table-bordered table-hover table-striped">
<thead class="table-success">
<tr>
<th colspan="4" class="text-center fs-4">
ABSTRACT OF QUANTITY
</th>
</tr>
<tr>
<th>Contractor</th>
<td>{}</td>
<th>RA Bill NO</th>
<td>{}</td>
</tr>
<tr>
<th width="8%">Sr</th>
<th>Description</th>
<th width="10%">UOM</th>
<th width="15%">Qty</th>
</tr>
</thead>
<tbody>
""".format(
self.contractor_name(),
self.ra_bill_no or ""
)
sr = 1
sections = [
("TRENCH EXCAVATION", self.trench_summary()),
("MANHOLE EXCAVATION", self.manhole_summary()),
("DOMESTIC CHAMBER", self.domestic_summary()),
("PIPE LAYING", self.laying_summary())
]
for title, rows in sections:
html += f"""
<tr class="table-secondary fw-bold">
<td colspan="4">{title}</td>
</tr>
"""
for item in rows:
html += f"""
<tr>
<td>{sr}</td>
<td>{item['Description']}</td>
<td>{item['UOM']}</td>
<td class="text-end">
{item['Qty']:.2f}
</td>
</tr>
"""
sr += 1
html += """
</tbody>
</table>
</div>
"""
return html

View File

@@ -0,0 +1,56 @@
import re
from pathlib import Path
# Activity log Service
class ActivityService:
@staticmethod
def read_logs(file_name="", search="", level="", user="", from_date="", to_date=""):
file = Path("logs") / file_name
if not file.exists():
return []
logs = []
with open(file, "r", encoding="utf-8") as f:
for line in reversed(f.readlines()):
# Example:
# 2026-08-01 10:15:55 | INFO | admin | Dashboard | Dashboard Opened
parts = [x.strip() for x in line.split("|")]
if len(parts) < 5:
continue
record = {
"date": parts[0],
"level": parts[1],
"user": parts[2],
"module": parts[3],
"message": "|".join(parts[4:])
}
# Search Filter
if search and search.lower() not in line.lower():
continue
# Level Filter
if level and record["level"] != level:
continue
# User Filter
if user and user.lower() not in record["user"].lower():
continue
# Date Filter
if from_date and record["date"][:10] < from_date:
continue
if to_date and record["date"][:10] > to_date:
continue
logs.append(record)
return logs

View File

@@ -0,0 +1,204 @@
# from collections import defaultdict
# import pandas as pd
# from app.utils.regex_utils import RegularExpression
# class ComparisonService:
# TRENCH_MAPPING = [
# {
# "label": "Marshi 0 to 1.5",
# "client": "Client_Marshi_Muddy_Slushy_0_to_1_5_total",
# "sub": None
# },
# {
# "label": "Marshi 1.5 to 3.0",
# "client": "Client_Marshi_Muddy_Slushy_1_5_to_3_0_total",
# "sub": None
# },
# {
# "label": "Marshi 3.0 to 4.5",
# "client": "Client_Marshi_Muddy_Slushy_3_0_to_4_5_total",
# "sub": None
# },
# {
# "label": "Soft Murum 0 to 1.5",
# "client": "Client_Soft_Murum_0_to_1_5_total",
# "sub": "Sub_Soft_Murum_0_to_1_5_total"
# },
# {
# "label": "Soft Murum 1.5 to 3.0",
# "client": "Client_Soft_Murum_1_5_to_3_0_total",
# "sub": "Sub_Soft_Murum_1_5_to_3_0_total"
# },
# {
# "label": "Soft Murum 3.0 to 4.5",
# "client": "Client_Soft_Murum_3_0_to_4_5_total",
# "sub": "Sub_Soft_Murum_3_0_to_4_5_total"
# },
# {
# "label": "Hard Murum 0 to 1.5",
# "client": "Client_Hard_Murum_0_to_1_5_total",
# "sub": "Sub_Hard_Murum_0_to_1_5_total"
# },
# {
# "label": "Hard Murum 1.5+",
# "client": "Client_Hard_Murum_1_5_to_3_0_total",
# "sub": "Sub_Hard_Murum_1_5_and_above_total"
# },
# {
# "label": "Soft Rock 0 to 1.5",
# "client": "Client_Soft_Rock_0_to_1_5_total",
# "sub": "Sub_Soft_Rock_0_to_1_5_total"
# },
# {
# "label": "Soft Rock 1.5+",
# "client": "Client_Soft_Rock_1_5_to_3_0_total",
# "sub": "Sub_Soft_Rock_1_5_and_above_total"
# },
# {
# "label": "Hard Rock 0 to 1.5",
# "client": "Client_Hard_Rock_0_to_1_5_total",
# "sub": "Sub_Hard_Rock_0_to_1_5_total"
# },
# {
# "label": "Hard Rock 1.5 to 3.0",
# "client": "Client_Hard_Rock_1_5_to_3_0_total",
# "sub": "Sub_Hard_Rock_1_5_to_3_0_total"
# },
# {
# "label": "Hard Rock 3.0 to 4.5",
# "client": "Client_Hard_Rock_3_0_to_4_5_total",
# "sub": "Sub_Hard_Rock_3_0_to_4_5_total"
# },
# {
# "label": "Hard Rock 4.5 to 6.0",
# "client": "Client_Hard_Rock_4_5_to_6_0_total",
# "sub": "Sub_Hard_Rock_4_5_to_6_0_total"
# },
# {
# "label": "Hard Rock 6.0 to 7.5",
# "client": "Client_Hard_Rock_6_0_to_7_5_total",
# "sub": "Sub_Hard_Rock_6_0_to_7_5_total"
# }
# ]
# @staticmethod
# def normalize_key(value):
# if value is None:
# return ""
# return str(value).strip().upper()
# @classmethod
# def make_lookup(cls, rows, key_field):
# """
# Create lookup dictionary using:
# (Location, MH_NO)
# """
# lookup = defaultdict(list)
# for row in rows:
# location = cls.normalize_key(row.get("Location"))
# key = cls.normalize_key(row.get(key_field))
# if location and key:
# lookup[(location, key)].append(row)
# return lookup
# @classmethod
# def build_comparison(cls, client_rows, subcontractor_rows, key_field="MH_NO"):
# subcontractor_lookup = cls.make_lookup(
# subcontractor_rows,
# key_field
# )
# used = defaultdict(int)
# output = []
# for client in client_rows:
# location = cls.normalize_key(client.get("Location"))
# key = cls.normalize_key(client.get(key_field))
# if not location or not key:
# continue
# rows = subcontractor_lookup.get((location, key))
# if not rows:
# continue
# index = used[(location, key)]
# if index >= len(rows):
# continue
# subcontractor = rows[index]
# used[(location, key)] += 1
# client_total = sum(
# float(v or 0)
# for k, v in client.items()
# if k.endswith("_total")
# or RegularExpression.D_RANGE_PATTERN.match(k)
# or RegularExpression.PIPE_MM_PATTERN.match(k)
# )
# subcontractor_total = sum(
# float(v or 0)
# for k, v in subcontractor.items()
# if k.endswith("_total")
# or RegularExpression.D_RANGE_PATTERN.match(k)
# or RegularExpression.PIPE_MM_PATTERN.match(k)
# )
# row = {
# "Location": location,
# key_field: key,
# "Client_Total": round(client_total, 2),
# "Subcontractor_Total": round(subcontractor_total, 2),
# "Difference": round(
# client_total - subcontractor_total,
# 2
# )
# }
# # Client Columns
# for column, value in client.items():
# if column in [
# "id",
# "created_at"
# ]:
# continue
# row[f"Client_{column}"] = value
# # Subcontractor Columns
# for column, value in subcontractor.items():
# if column in [
# "id",
# "created_at",
# "subcontractor_id"
# ]:
# continue
# row[f"Sub_{column}"] = value
# output.append(row)
# return pd.DataFrame(output)

View File

@@ -0,0 +1,49 @@
from app.config import Config
from app import db
import matplotlib
matplotlib.use("Agg")
from app.routes.dashboard import plot_to_base64
import matplotlib.pyplot as plt
import io
import base64
from app.utils.plot_utils import plot_to_base64
# Subcontractor models import
from app.models.trench_excavation_model import TrenchExcavation
# Client models import
# from app.models.tr_ex_client_model import TrenchExcavationClient
class DashboardService:
# bar chart
def bar_chart_of_tr_ex():
categories = ["Soft Murum", "Hard Murum", "Soft Rock", "Hard Rock"]
values = [120, 80, 150, 60]
tr = TrenchExcavation()
record = TrenchExcavation.query.first()
print(" RA_Bill_No::",record["RA_Bill_No"])
totals = tr.excavation_category_sums()
# print(totals["Soft_Murum_Total"])
# print(totals["Hard_Rock_Total"])
plt.figure()
plt.bar(categories, values)
plt.title("Trench Excavation Work Category Report")
plt.xlabel("Excavation category")
plt.ylabel("Quantity")
return plot_to_base64(plt)
def subcontractor_dash():
return True

View File

@@ -1,10 +1,8 @@
from app import db
import os import os
import pandas as pd import pandas as pd
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from app.utils.file_utils import ensure_upload_folder from app.utils.file_utils import ensure_upload_folder, get_uploads_folder, ALLOWED_EXTENSIONS
from app.config import Config
from app import db
# Subcontractor models import # Subcontractor models import
from app.models.trench_excavation_model import TrenchExcavation from app.models.trench_excavation_model import TrenchExcavation
@@ -24,8 +22,9 @@ class FileService:
# ---------------- COMMON HELPERS ---------------- # ---------------- COMMON HELPERS ----------------
def allowed_file(self, filename): def allowed_file(self, filename):
return ("." in filename and filename.rsplit(".", 1)[1].lower() in Config.ALLOWED_EXTENSIONS) return ("." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS)
# data normalizations
def normalize(self, val): def normalize(self, val):
if val is None or pd.isna(val): if val is None or pd.isna(val):
return None return None
@@ -35,7 +34,8 @@ class FileService:
return None return None
return val.upper() return val.upper()
# --------------- Sub-contractor service --------------
# ---------------- SUBCONTRACTOR FILE UPLOAD ---------------- # ---------------- SUBCONTRACTOR FILE UPLOAD ----------------
def handle_file_upload(self, file, subcontractor_id, RA_Bill_No): def handle_file_upload(self, file, subcontractor_id, RA_Bill_No):
@@ -52,8 +52,9 @@ class FileService:
return False, "Invalid file type! Allowed: CSV, XLSX, XLS" return False, "Invalid file type! Allowed: CSV, XLSX, XLS"
ensure_upload_folder() ensure_upload_folder()
path = get_uploads_folder()
folder = os.path.join(Config.UPLOAD_FOLDER, f"sub_{subcontractor_id}") folder = os.path.join(path, f"sub_{subcontractor_id}")
os.makedirs(folder, exist_ok=True) os.makedirs(folder, exist_ok=True)
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
@@ -75,7 +76,7 @@ class FileService:
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
return False, f"Import failed: {e}" return False, f"Import failed: {e}"
# ---------------- Trench Excavation (Subcontractor) ---------------- # ---------------- Trench Excavation (Subcontractor) ----------------
def process_trench_excavation(self, df, subcontractor_id, RA_Bill_No): def process_trench_excavation(self, df, subcontractor_id, RA_Bill_No):
@@ -309,8 +310,7 @@ class FileService:
db.session.commit() db.session.commit()
# --------------- Client service --------------
# ---------------- CLIENT FILE UPLOAD ---------------- # ---------------- CLIENT FILE UPLOAD ----------------
def handle_client_file_upload(self, file, RA_Bill_No): def handle_client_file_upload(self, file, RA_Bill_No):
@@ -324,8 +324,9 @@ class FileService:
return False, "Invalid file type! Allowed: CSV, XLSX, XLS" return False, "Invalid file type! Allowed: CSV, XLSX, XLS"
ensure_upload_folder() ensure_upload_folder()
path = get_uploads_folder()
folder = os.path.join(Config.UPLOAD_FOLDER, f"Client_Bill_{RA_Bill_No}") folder = os.path.join(path, f"Client_Bill_{RA_Bill_No}")
os.makedirs(folder, exist_ok=True) os.makedirs(folder, exist_ok=True)
filename = secure_filename(file.filename) filename = secure_filename(file.filename)

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,149 @@
import os
import logging
from logging.handlers import RotatingFileHandler
from flask import request, session, has_request_context
class RequestContextFilter(logging.Filter):
"""Adds request information to every log record."""
def filter(self, record):
if has_request_context():
record.user = session.get("user_name", "Anonymous")
record.ip = request.remote_addr or "Unknown"
record.method = request.method
record.url = request.url
else:
record.user = "System"
record.ip = "-"
record.method = "-"
record.url = "-"
return True
class LoggerService:
LOG_DIR = "logs"
APP_LOG = "app.log"
ERROR_LOG = "error.log"
DEBUG_LOG = "debug.log"
MAX_BYTES = 10 * 1024 * 1024 # 10 MB
BACKUP_COUNT = 10
@classmethod
def init_app(cls, app):
os.makedirs(cls.LOG_DIR, exist_ok=True)
formatter = logging.Formatter(
fmt=(
"%(asctime)s | "
"%(levelname)-8s | "
"User=%(user)s | "
"IP=%(ip)s | "
"%(method)s | "
"%(url)s | "
"%(message)s"
),
datefmt="%Y-%m-%d %H:%M:%S"
)
context_filter = RequestContextFilter()
# Remove default handlers
app.logger.handlers.clear()
# ==========================
# Application Log
# ==========================
app_handler = RotatingFileHandler(
os.path.join(cls.LOG_DIR, cls.APP_LOG),
maxBytes=cls.MAX_BYTES,
backupCount=cls.BACKUP_COUNT,
encoding="utf-8"
)
app_handler.setLevel(logging.INFO)
app_handler.setFormatter(formatter)
app_handler.addFilter(context_filter)
# ==========================
# Error Log
# ==========================
error_handler = RotatingFileHandler(
os.path.join(cls.LOG_DIR, cls.ERROR_LOG),
maxBytes=cls.MAX_BYTES,
backupCount=cls.BACKUP_COUNT,
encoding="utf-8"
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(formatter)
error_handler.addFilter(context_filter)
# ==========================
# Debug Log
# ==========================
debug_handler = RotatingFileHandler(
os.path.join(cls.LOG_DIR, cls.DEBUG_LOG),
maxBytes=cls.MAX_BYTES,
backupCount=cls.BACKUP_COUNT,
encoding="utf-8"
)
debug_handler.setLevel(logging.DEBUG)
debug_handler.setFormatter(formatter)
debug_handler.addFilter(context_filter)
# ==========================
# Console Log
# ==========================
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
console_handler.addFilter(context_filter)
# ==========================
# Logger Configuration
# ==========================
app.logger.setLevel(logging.DEBUG)
app.logger.addHandler(app_handler)
app.logger.addHandler(error_handler)
app.logger.addHandler(debug_handler)
app.logger.addHandler(console_handler)
# ==========================
# Automatic Request Logging
# ==========================
@app.before_request
def before_request():
app.logger.info("Request Started")
@app.after_request
def after_request(response):
app.logger.info(
f"Request Completed | Status={response.status_code}"
)
return response
# ==========================
# Exception Logging
# ==========================
@app.errorhandler(Exception)
def log_exception(error):
app.logger.exception(
f"Unhandled Exception: {str(error)}"
)
raise error
app.logger.info("=" * 70)
app.logger.info("Application Started Successfully")
app.logger.info("=" * 70)

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

@@ -1,5 +1,6 @@
from app.models.user_model import User from app.models.user_model import User
from app.services.db_service import db from app.services.db_service import db
from flask import current_app
class UserService: class UserService:
@@ -13,6 +14,7 @@ class UserService:
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
current_app.logger.info("User list viewed")
return user return user
@staticmethod @staticmethod

View File

@@ -0,0 +1,208 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid py-4">
<!-- HEADER -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center">
<div class="mb-3 mb-lg-0">
<h3 class="fw-bold text-primary mb-1"> <i class="bi bi-clock-history me-2"></i> Activity Logs </h3>
<small class="text-muted"> View application logs, search records and download log files. </small>
</div>
<div class="d-flex flex-wrap gap-2">
<a id="downloadLogBtn" href="{{ url_for('activity.download_log') }}?file={{ file_name }}" class="btn btn-success">
<i class="bi bi-download me-1"></i>
Download Log
</a>
<a href="{{ url_for('activity.activity') }}" class="btn btn-outline-primary">
<i class="bi bi-arrow-clockwise me-1"></i>
Refresh
</a>
</div>
</div>
</div>
</div>
<!-- FILTERS -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-funnel-fill me-2"></i> Filters
</h5>
</div>
<div class="card-body">
<form method="GET">
<div class="row g-3">
<!-- Log File select -->
<div class="col-xl-2 col-lg-4 col-md-6">
<label class="form-label fw-bold">Log File</label>
<select class="form-select" name="file">
<option value="app.log" {% if file_name=="app.log" %}selected{% endif %}> Application </option>
<option value="debug.log" {% if file_name=="debug.log" %}selected{% endif %}> Debug </option>
<option value="error.log" {% if file_name=="error.log" %}selected{% endif %}> Error </option>
</select>
</div>
<!-- User search -->
<div class="col-xl-2 col-lg-4 col-md-6">
<label class="form-label fw-bold">User</label>
<input type="text" class="form-control" name="user" value="{{ user }}" placeholder="Username">
</div>
<!-- Level -->
<div class="col-xl-2 col-lg-4 col-md-6">
<label class="form-label fw-bold">Level</label>
<select class="form-select" name="level">
<option value="">All</option>
<option value="INFO"
{% if level=="INFO" %}selected{% endif %}>
INFO
</option>
<option value="WARNING"
{% if level=="WARNING" %}selected{% endif %}>
WARNING
</option>
<option value="ERROR"
{% if level=="ERROR" %}selected{% endif %}>
ERROR
</option>
</select>
</div>
<!-- From Date -->
<div class="col-xl-2 col-lg-4 col-md-6">
<label class="form-label fw-bold">From Date</label>
<input type="date" class="form-control" name="from_date" value="{{ from_date }}">
</div>
<!-- To Date -->
<div class="col-xl-2 col-lg-4 col-md-6">
<label class="form-label fw-bold">To Date</label>
<input type="date" class="form-control" name="to_date" value="{{ to_date }}">
</div>
<!-- Search button -->
<div class="col-xl-2 col-lg-12">
<label class="form-label fw-bold">Search</label>
<input type="text" class="form-control" name="search" value="{{ search }}" placeholder="Search">
</div>
</div>
<hr>
<!-- Search and Reset button -->
<div class="d-flex gap-2">
<button class="btn btn-primary">
<i class="bi bi-search"></i> Search
</button>
<button type="reset" class="btn btn-secondary">
<i class="bi bi-arrow-clockwise"></i> Reset
</button>
</div>
</form>
</div>
</div>
<!-- LOG TABLE -->
<div class="card shadow-sm">
<!-- Log Entries -->
<div class="card-header bg-dark text-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-list-ul me-2"></i>
Log Entries
</h5>
<span class="badge bg-light text-dark">
{{ logs|length }} Records
</span>
</div>
</div>
<!-- Show table Entries -->
<div class="card-body p-0">
<div class="table-responsive" style="max-height:600px; overflow-y:auto; font-size:12px;">
<table class="table table-striped table-hover table-bordered align-middle mb-0 small">
<thead class="table-dark sticky-top">
<tr>
<th>Sr No</th>
<th>Date & Time</th>
<th>User</th>
<th>Level</th>
<th>Module</th>
<th>Activity</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ log.date }}</td>
<td>{{ log.user }}</td>
<td>
{% if log.level=="INFO" %}
<span class="badge bg-success">INFO</span>
{% elif log.level=="WARNING" %}
<span class="badge bg-warning text-dark">WARNING</span>
{% else %}
<span class="badge bg-danger">ERROR</span>
{% endif %}
</td>
<td>{{ log.module }}</td>
<td>{{ log.message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const fileSelect = document.querySelector('select[name="file"]');
const downloadBtn = document.getElementById("downloadLogBtn");
function updateDownloadLink() {
downloadBtn.href =
"{{ url_for('activity.download_log') }}?file=" +
encodeURIComponent(fileSelect.value);
}
updateDownloadLink();
fileSelect.addEventListener("change", updateDownloadLink);
});
</script>
{% endblock %}

View File

@@ -5,18 +5,23 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title if title else "Comparison Software" }}</title> <title>{{ title if title else "Comparison Software" }}</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='images/lcepl.png') }}">
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<!-- Bootstrap Icons --> <!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" >
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.1/css/buttons.dataTables.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-multiselect@1.1.2/dist/css/bootstrap-multiselect.min.css">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<!-- NAVBAR --> <!-- NAVBAR -->
<!-- <nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm"> -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm fixed-top"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm fixed-top">
<div class="container-fluid"> <div class="container-fluid">
@@ -45,17 +50,17 @@
<!-- Subcontractor Model --> <!-- Subcontractor Model -->
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#"> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
<i class="bi bi-people-fill me-1"></i> Subcontractor Model <i class="bi bi-people-fill me-1"></i> Subcontractor List
</a> </a>
<ul class="dropdown-menu dropdown-menu-dark"> <ul class="dropdown-menu dropdown-menu-dark">
<li> <li>
<a class="dropdown-item" href="/subcontractor/add"> <a class="dropdown-item" href="/subcontractor/add">
<i class="bi bi-plus-circle me-2"></i> Add Subcontractor <i class="bi bi-plus-circle me-2"></i> New Add
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="/subcontractor/list"> <a class="dropdown-item" href="/subcontractor/list">
<i class="bi bi-list-ul me-2"></i> Subcontractor List <i class="bi bi-list-ul me-2"></i> List
</a> </a>
</li> </li>
</ul> </ul>
@@ -64,9 +69,15 @@
<!-- Subcontractor File System --> <!-- Subcontractor File System -->
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#"> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
<i class="bi bi-folder-fill me-1"></i>Subcontractor File System <i class="bi bi-folder-fill me-1"></i>Subcontractor RA Bills
</a> </a>
<ul class="dropdown-menu dropdown-menu-dark"> <ul class="dropdown-menu dropdown-menu-dark">
<li>
<a class="dropdown-item" href="/dashboard/subcontractor_dashboard">
<i class="bi bi-speedometer2 me-2"></i> Subcontractor Dashboard
</a>
</li>
<li> <li>
<a class="dropdown-item" href="/file/import_Subcontractor"> <a class="dropdown-item" href="/file/import_Subcontractor">
<i class="bi bi-upload me-2"></i> Import File <i class="bi bi-upload me-2"></i> Import File
@@ -74,16 +85,17 @@
</li> </li>
<li> <li>
<a class="dropdown-item" href="/file/Subcontractor_report"> <a class="dropdown-item" href="/file/Subcontractor_report">
<i class="bi bi-download me-2"></i> Show Reports <i class="bi bi-arrow-left-right me-2"></i> Show Reports
</a> </a>
</li> </li>
</ul> </ul>
</li> </li>
<!-- Client System --> <!-- Client System -->
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#"> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
<i class="bi bi-building me-1"></i> Client File System <i class="bi bi-building me-1"></i> Client RA Bills
</a> </a>
<ul class="dropdown-menu dropdown-menu-dark"> <ul class="dropdown-menu dropdown-menu-dark">
<li> <li>
@@ -103,13 +115,13 @@
<!-- Reports --> <!-- Reports -->
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#"> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
<i class="bi bi-building me-1"></i> Reports <i class="bi bi-building me-1"></i> Comparison Report
</a> </a>
<ul class="dropdown-menu dropdown-menu-dark"> <ul class="dropdown-menu dropdown-menu-dark">
<li> <li>
<a class="dropdown-item" href="/report/comparison_report"> <a class="dropdown-item" href="/report/comparison_report">
<i class="bi bi-arrow-left-right me-2"></i> client vs sub-cont. Comparison Report <i class="bi bi-arrow-left-right me-2"></i> Client vs Subcontractor
</a> </a>
</li> </li>
<!-- <li> <!-- <li>
@@ -120,7 +132,6 @@
</ul> </ul>
</li> </li>
<!-- Formats --> <!-- Formats -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/file_format"> <a class="nav-link" href="/file_format">
@@ -128,44 +139,95 @@
</a> </a>
</li> </li>
<!-- USER DROPDOWN --> <!-- Masters -->
{% if session.get("user_id") %} <li class="nav-item dropdown">
<li class="nav-item dropdown ms-lg-3"> <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="#" <!-- Client Standard Rates -->
data-bs-toggle="dropdown"> <li class="nav-item">
<i class="bi bi-person-circle fs-5"></i> <a class="nav-link" href="/engi/client-rate">
<span class="d-none d-lg-inline"> <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") }} {{ session.get("user_name") }}
</span> </span>
</a> </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 --> <!-- Profile Header -->
<li class="px-3 py-3 text-center border-bottom"> <li class="text-center py-4 border-bottom border-secondary">
<i class="bi bi-person-circle fs-1"></i> <i class="bi bi-person-circle text-light" style="font-size:60px;"></i>
<div class="fw-semibold mt-1"> <h5 class="mt-2 mb-0 fw-bold">
{{ session.get("user_name") }} {{ session.get("user_name") }}
</div> </h5>
<small class="text-muted">Logged in user</small> <small class="text-secondary">
{{ session.get("email") }}
</small>
</li> </li>
<!-- Masters -->
<li> <li>
<a class="dropdown-item" href="/dashboard"> <a class="dropdown-item text-light py-2" href="{{ url_for('engineering.engineering_master') }}">
<i class="bi bi-speedometer2 me-2"></i> Dashboard <i class="bi bi-gear me-2"></i> Masters
</a> </a>
</li> </li>
<!-- Activity Log page -->
<li> <li>
<a class="dropdown-item text-warning" href="/logout"> <a class="dropdown-item text-light py-2" href="{{ url_for('activity.activity') }}">
<i class="bi bi-box-arrow-right me-2"></i> Logout <i class="bi bi-clock-history me-2"></i> Activity Log
</a> </a>
</li> </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> </ul>
</li> </li>
{% endif %}
</ul> </ul>
</div> </div>
@@ -176,19 +238,63 @@
<!-- PAGE CONTENT --> <!-- PAGE CONTENT -->
<div class="container-fluid vh-100 pt-5 overflow-hidden"> <div class="container-fluid vh-100 pt-5 overflow-hidden">
<!-- FLASH MESSAGES --> <!-- FLASH MESSAGES -->
<div class="container mt-3"> <div class="position-fixed top-0 end-0 p-2 p-md-3 mt-5" style="z-index:1080; width:min(95vw,500px);">
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
{% for category, message in messages %} {% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show">
{{ message }} <div class="alert alert-{{ category }} alert-dismissible fade show shadow notification-alert mb-2"
<button class="btn-close" data-bs-dismiss="alert"></button> role="alert">
<div class="d-flex align-items-start flex-wrap">
{% if category == 'success' %}
<i class="bi bi-check-circle-fill me-2 fs-5 flex-shrink-0"></i>
{% elif category == 'danger' %}
<i class="bi bi-x-circle-fill me-2 fs-5 flex-shrink-0"></i>
{% elif category == 'warning' %}
<i class="bi bi-exclamation-triangle-fill me-2 fs-5 flex-shrink-0"></i>
{% elif category == 'info' %}
<i class="bi bi-info-circle-fill me-2 fs-5 flex-shrink-0"></i>
{% endif %}
<div class="flex-grow-1">
<div class="fw-semibold">
{% if category == 'success' %}
Success
{% elif category == 'danger' %}
Error
{% elif category == 'warning' %}
Warning
{% elif category == 'info' %}
Information
{% endif %}
</div>
<div>
{{ message }}
</div>
<!-- Timeline -->
<div class="progress mt-2" style="height:5px;">
<div class="progress-bar progress-bar-striped progress-bar-animated timer-bar" role="progressbar" style="width:100%">
</div>
</div>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert">
</button>
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</div>
</div>
<div class="overflow-auto h-100"> <div class="overflow-auto h-100">
<div class="container mt-4"> <div class="container mt-4">
@@ -197,10 +303,114 @@
</div> </div>
</div> </div>
<!-- Global Loading Overlay -->
<div id="globalLoader"
class="position-fixed top-0 start-0 w-100 h-100 bg-dark bg-opacity-50 d-none"
style="z-index:9999;">
<div class="d-flex justify-content-center align-items-center h-100">
<div class="card shadow-lg border-0 rounded-4" style="width:430px;">
<div class="card-body text-center p-5">
<div class="spinner-border text-primary" style="width:70px;height:70px;"role="status">
</div>
<h4 class="fw-bold mt-4 mb-2">Please Wait...</h4>
<p id="loaderMessage" class="text-muted mb-4">Processing your request...</p>
<div class="progress" style="height:8px;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
style="width:100%">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.1/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.html5.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.print.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-multiselect@1.1.2/dist/js/bootstrap-multiselect.min.js"></script>
<script>
// show alert msg
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".notification-alert").forEach(function(alert){
const progressBar = alert.querySelector(".timer-bar");
let width = 100;
const interval = setInterval(function() {
width -= 2;
progressBar.style.width = width + "%";
if(width <= 0){
clearInterval(interval);
const bsAlert = bootstrap.Alert.getOrCreateInstance(alert);
bsAlert.close();
}
}, 100);
});
});
// show msg Processing your request
function showLoader(message = "Processing your request...") {
document.getElementById("loaderMessage").innerHTML = message;
document.getElementById("globalLoader").classList.remove("d-none");
}
// Automatically hide
function hideLoader(){
document.getElementById("globalLoader").classList.add("d-none");
}
// Automatically apply to all forms having class="loading-form"
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".loading-form").forEach(function(form){
form.addEventListener("submit", function(){
if(!this.checkValidity())
return;
showLoader();
const btn = this.querySelector("button[type='submit']");
const originalBtnHtml = btn ? btn.innerHTML : null;
if(btn){
btn.disabled = true;
btn.innerHTML = `
<span class="spinner-border spinner-border-sm me-2"></span>
Processing...
`;
}
setTimeout(function () {
hideLoader();
if (btn) {
btn.disabled = false;
btn.innerHTML = originalBtnHtml;
}
}, 3000);
});
});
});
window.addEventListener("pageshow", function () {
hideLoader();
});
</script>
</body> </body>
</html> </html>

View File

@@ -2,21 +2,10 @@
{% block content %} {% block content %}
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
<h2 class="mb-4">Client File Reports</h2> <h2 class="mb-4">Client RA Bills Reports</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card p-4 shadow-sm mb-5"> <div class="card p-4 shadow-sm mb-5">
<form method="POST"> <form method="POST" class="loading-form">
<label class="form-label fw-bold">RA Bill No</label> <label class="form-label fw-bold">RA Bill No</label>
<input type="text" name="RA_Bill_No" class="form-control mb-3" value="{{ ra_val }}" required> <input type="text" name="RA_Bill_No" class="form-control mb-3" value="{{ ra_val }}" required>
@@ -26,33 +15,33 @@
Data</button> Data</button>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<button type="submit" name="action" value="download" class="btn btn-primary w-100">Download Excel <button type="submit" name="action" value="download" class="btn btn-primary w-100">Download Excel Report</button>
Report</button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
{% if tables.tr or tables.mh or tables.dc or tables.laying %} {% if tables.tr or tables.mh or tables.dc or tables.laying %}
<div class="card shadow-sm p-3"> <div class="card shadow-sm p-4">
<h4 class="mb-3">Comparison Preview</h4> <h4 class="mb-3">Table Preview</h4>
<ul class="nav nav-tabs" id="reportTabs" role="tablist"> <ul class="nav nav-tabs" id="reportTabs" role="tablist">
<li class="nav-item"> <li class="nav-item">
<button class="nav-link active" id="tr-tab" data-bs-toggle="tab" data-bs-target="#tr" <button class="nav-link active" id="tr-tab" data-bs-toggle="tab" data-bs-target="#tr"
type="button">Tr.Ex Comparison</button> type="button">Tr.Ex </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" id="mh-tab" data-bs-toggle="tab" data-bs-target="#mh" type="button">Mh.Ex <button class="nav-link" id="mh-tab" data-bs-toggle="tab" data-bs-target="#mh" type="button">Mh.Ex
Comparison</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" id="dc-tab" data-bs-toggle="tab" data-bs-target="#dc" type="button">MH & DC <button class="nav-link" id="dc-tab" data-bs-toggle="tab" data-bs-target="#dc" type="button">MH & DC
Comparison</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying" type="button">Laying <button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying" type="button">Laying
& Bedding Comparison</button> & Bedding </button>
</li>
</ul> </ul>
<div class="tab-content mt-3" id="reportTabsContent"> <div class="tab-content mt-3" id="reportTabsContent">
@@ -76,9 +65,10 @@
{{ tables.laying|safe }} {{ tables.laying|safe }}
</div> </div>
</div>
</div> </div>
</div> {% endif %}
{% endif %}
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,87 +1,118 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container-fluid px-2 px-md-4"> <div class="container-fluid px-2 px-md-4">
<h4 class="mb-3 text-center text-md-start">Comparison Software Solapur (UGD) - Live Dashboard</h4>
<h4 class="mb-3 text-center text-md-start">Comparison Software Solapur(UGD) </h4>
<!-- Summary Cards -->
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<!-- Total Work -->
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<div class="card text-white bg-primary shadow h-100"> <div class="card text-white bg-primary shadow h-100">
<div class="card-body text-center text-md-start"> <div class="card-body text-center text-md-start">
<h6>Test Total Work</h6> <h6>Trenching Units</h6>
<h3 class="fw-bold">30%</h3> <h3 class="fw-bold" id="card-trench">0</h3>
</div> </div>
</div> </div>
</div> </div>
<!-- Completed -->
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<div class="card text-white bg-success shadow h-100"> <div class="card text-white bg-success shadow h-100">
<div class="card-body text-center text-md-start"> <div class="card-body text-center text-md-start">
<h6>test Completed</h6> <h6>Manhole Units</h6>
<h3 class="fw-bold">35%</h3> <h3 class="fw-bold" id="card-manhole">0</h3>
</div> </div>
</div> </div>
</div> </div>
<!-- Pending -->
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<div class="card text-dark bg-warning shadow h-100"> <div class="card text-dark bg-warning shadow h-100">
<div class="card-body text-center text-md-start"> <div class="card-body text-center text-md-start">
<h6>Pending</h6> <h6>Laying Units</h6>
<h3 class="fw-bold">35%</h3> <h3 class="fw-bold" id="card-laying">0</h3>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Charts -->
<div class="row g-3"> <div class="row g-3">
<!-- Bar Chart -->
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="card shadow-sm h-100"> <div class="card shadow-sm h-100">
<div class="card-header bg-dark text-white text-center text-md-start"> <div class="card-header bg-dark text-white">Live Category Bar Chart</div>
Work Category Bar Chart <div class="card-body">
</div> <canvas id="liveBarChart" style="max-height:300px;"></canvas>
<div class="card-body text-center">
<img src="data:image/png;base64,{{ bar_chart }}" class="img-fluid" style="max-height:300px;">
</div> </div>
</div> </div>
</div> </div>
<!-- Pie Chart -->
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="card shadow-sm h-100"> <div class="card shadow-sm h-100">
<div class="card-header bg-dark text-white text-center text-md-start"> <div class="card-header bg-dark text-white">Location Distribution Pie Chart</div>
Project Status Pie Chart <div class="card-body">
</div> <canvas id="livePieChart" style="max-height:300px;"></canvas>
<div class="card-body text-center">
<img src="data:image/png;base64,{{ pie_chart }}" class="img-fluid" style="max-height:300px;">
</div> </div>
</div> </div>
</div> </div>
<!-- Histogram -->
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-dark text-white text-center text-md-start">
Daily Work Histogram
</div>
<div class="card-body text-center">
<img src="data:image/png;base64,{{ histogram }}" class="img-fluid" style="max-height:350px;">
</div>
</div>
</div>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// 2. Initialize the Bar Chart
const barCtx = document.getElementById('liveBarChart').getContext('2d');
let liveBarChart = new Chart(barCtx, {
type: 'bar',
data: {
labels: ['Trenching', 'Manholes', 'Laying'],
datasets: [{
label: 'Units Completed',
data: [0, 0, 0],
backgroundColor: ['#0d6efd', '#198754', '#ffc107']
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
// 3. Initialize the Pie Chart
const pieCtx = document.getElementById('livePieChart').getContext('2d');
let livePieChart = new Chart(pieCtx, {
type: 'pie',
data: {
labels: [], // Will be filled from SQL
datasets: [{
data: [],
backgroundColor: ['#0d6efd', '#198754', '#ffc107', '#6f42c1', '#fd7e14']
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
// 4. Function to Fetch Live Data from your Python API
function fetchLiveData() {
fetch('/dashboard/api/live-stats') // This matches the route we created in the "Kitchen"
.then(response => response.json())
.then(data => {
// Update the Summary Cards
document.getElementById('card-trench').innerText = data.summary.trench;
document.getElementById('card-manhole').innerText = data.summary.manhole;
document.getElementById('card-laying').innerText = data.summary.laying;
// Update Bar Chart
liveBarChart.data.datasets[0].data = [
data.summary.trench,
data.summary.manhole,
data.summary.laying
];
liveBarChart.update();
// Update Pie Chart (Location stats)
livePieChart.data.labels = Object.keys(data.locations);
livePieChart.data.datasets[0].data = Object.values(data.locations);
livePieChart.update();
})
.catch(err => console.error("Error fetching live data:", err));
}
// 5. Check for updates every 10 seconds (Real-time effect)
setInterval(fetchLiveData, 10000);
fetchLiveData(); // Load immediately on page open
</script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,84 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid py-4">
<!-- Page Header -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h2 class="fw-bold text-primary mb-1">
<i class="bi bi-pencil-square"></i>
Edit Record
</h2>
<small class="text-muted">
Model: <strong>{{ model|upper }}</strong> &nbsp;|&nbsp; Record ID: {{ record.id }}
</small>
</div>
<a href="{{ url_for('file_report.report_file') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to Report
</a>
</div>
</div>
<!-- Edit Form -->
<div class="card shadow-sm border-0">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-card-checklist"></i>
Update Record Details
</h5>
</div>
<div class="card-body">
<form method="POST">
<div class="row g-3">
{% for column in record.__table__.columns %}
{% if column.name != "id" %}
<div class="col-lg-4">
<label class="form-label fw-semibold">
{{ column.name.replace('_', ' ')|title }}
</label>
{% set value = record|attr(column.name) %}
{% if column.type.python_type.__name__ == 'bool' %}
<select name="{{ column.name }}" class="form-select">
<option value="true" {% if value %}selected{% endif %}>Yes</option>
<option value="false" {% if not value %}selected{% endif %}>No</option>
</select>
{% elif column.type.python_type.__name__ in ['int', 'float', 'Decimal'] %}
<input type="number" step="any" name="{{ column.name }}"
class="form-control" value="{{ value if value is not none else '' }}">
{% elif column.type.python_type.__name__ == 'date' %}
<input type="date" name="{{ column.name }}"
class="form-control" value="{{ value if value is not none else '' }}">
{% else %}
<input type="text" name="{{ column.name }}"
class="form-control" value="{{ value if value is not none else '' }}">
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
<div class="mt-4 d-flex justify-content-end gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Save Changes
</button>
<a href="{{ url_for('file_report.report_file') }}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

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

View File

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

View File

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

@@ -1,41 +1,145 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2 class="mb-4">Client File Import</h2>
<div class="card p-4 shadow-sm"> <div class="container-fluid py-4">
<form method="POST" enctype="multipart/form-data"> <!-- Page Header -->
<div class="row mb-4">
<!-- 1. SELECT SUBCONTRACTOR --> <div class="col-12">
<!-- <label class="form-label">Select Subcontractor vs Client</label>
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
<option value="">-- Select Subcontractor --</option>
{% for sc in subcontractors %} <div class="card border-0 shadow-sm bg-success text-white">
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
{% endfor %}
</select> -->
<!-- 2. FILE TYPE (MODEL NAME) --> <div class="card-body d-flex justify-content-between align-items-center">
<!-- <label class="form-label">Select File Type</label>
<select name="file_type" id="file_type" class="form-select mb-3" required>
<option value="">-- Select File Type --</option>
<option value="">Sheet</option>
<option value="tr_ex_client">Tr. Ex</option>
<option value="mh_ex_client">Mh. Ex </option>
<option value="mh_dc_client">MH & DC </option>
<option value="">Laying Sheet</option>
</select> -->
<label class="form-label">RA Bill No</label> <div>
<input type="text" name="RA_Bill_No" class="form-control mb-3" required>
<!-- 3. FILE UPLOAD -->
<label class="form-label">Choose File</label>
<input type="file" name="file" class="form-control mb-3" required>
<button class="btn btn-primary w-100">Upload</button> <h3 class="fw-bold mb-1">
</form> <i class="bi bi-building-fill-up me-2"></i>
Client RA Bill Import
</h3>
<p class="mb-0 opacity-75">
Upload the Client RA Bill Excel file for comparison.
</p>
</div>
<div class="display-5">
<i class="bi bi-file-earmark-excel-fill"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Upload Card -->
<div class="row justify-content-center">
<div class="col-lg-7">
<div class="card border-0 shadow">
<div class="card-header bg-light">
<h5 class="mb-0 fw-bold text-success">
<i class="bi bi-upload me-2"></i>
Upload Details
</h5>
</div>
<div class="card-body p-4">
<form method="POST"
enctype="multipart/form-data"
class="loading-form"
onsubmit="showLoader('Reading Excel file...<br>Validating records...<br>Saving data to database...')">
<!-- RA Bill -->
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-receipt-cutoff text-success me-2"></i>
RA Bill No
<span class="text-danger">*</span>
</label>
<input
type="text"
name="RA_Bill_No"
class="form-control"
placeholder="Enter RA Bill Number"
required>
</div>
<!-- Upload -->
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-file-earmark-arrow-up text-primary me-2"></i>
Select Excel File
<span class="text-danger">*</span>
</label>
<input
type="file"
name="file"
class="form-control"
accept=".xlsx,.xls,.csv"
required>
<div class="form-text">
Supported Formats:
<strong>.xlsx</strong>,
<strong>.xls</strong>,
<strong>.csv</strong>
</div>
</div>
<hr>
<!-- Action Buttons -->
<div class="d-flex justify-content-between">
<button
type="submit"
class="btn btn-success px-5">
<i class="bi bi-cloud-upload-fill me-2"></i>
Upload File
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,32 +1,125 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2 class="mb-4">Sub-Contractor File Import</h2>
<div class="card p-4 shadow-sm"> <div class="container-fluid py-4">
<form method="POST" enctype="multipart/form-data"> <!-- Page Header -->
<div class="row mb-4">
<div class="col-lg-12">
<div class="card border-0 shadow-sm bg-primary text-white">
<div class="card-body d-flex justify-content-between align-items-center">
<!-- 1. SELECT SUBCONTRACTOR --> <div>
<label class="form-label">Select Subcontractor</label> <h3 class="mb-1 fw-bold">
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required> <i class="bi bi-cloud-arrow-up-fill me-2"></i>
<option value="">-- Select Subcontractor --</option> Sub-Contractor File Import
</h3>
{% for sc in subcontractors %} <p class="mb-0 opacity-75">
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option> Upload RA Bill Excel file for the selected subcontractor.
{% endfor %} </p>
</select> </div>
<!-- 2. RA bill no --> <div class="display-5">
<label class="form-label">RA Bill No</label> <i class="bi bi-file-earmark-excel-fill"></i>
<input type="text" name="RA_Bill_No" class="form-control mb-3" required> </div>
<!-- 3. FILE UPLOAD --> </div>
<label class="form-label">Choose File</label> </div>
<input type="file" name="file" class="form-control mb-3" required> </div>
</div>
<button class="btn btn-primary w-100">Upload</button> <!-- Upload Card -->
</form> <div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow">
<div class="card-header bg-light">
<h5 class="mb-0 fw-bold text-primary">
<i class="bi bi-upload me-2"></i>
Upload Details
</h5>
</div>
<div class="card-body p-4">
<form method="POST"
enctype="multipart/form-data"
class="loading-form"
onsubmit="showLoader('Reading Excel file...<br>Saving data to database...')">
<!-- Select Subcontractor -->
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-building me-1 text-primary"></i>
Select Subcontractor
<span class="text-danger">*</span>
</label>
<select name="subcontractor_id" id="subcontractor_id"
class="form-select" required>
<option value=""> -- Select Subcontractor -- </option>
{% for sc in subcontractors %}
<option value="{{ sc.id }}">{{ sc.subcontractor_name }} </option>
{% endfor %}
</select>
</div>
<!-- RA Bill No -->
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-receipt me-1 text-success"></i>
RA Bill No
<span class="text-danger">*</span>
</label>
<input type="text"
name="RA_Bill_No"
class="form-control"
placeholder="Enter RA Bill Number"
required>
</div>
<!-- File Upload -->
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-file-earmark-arrow-up me-1 text-warning"></i>
Upload Excel File
<span class="text-danger">*</span>
</label>
<input type="file"
name="file"
class="form-control"
accept=".xlsx,.xls,.csv"
required>
<div class="form-text">
Supported formats:
<strong>.xlsx</strong>,
<strong>.xls</strong>,
<strong>.csv</strong>
</div>
</div>
<hr>
<!-- Buttons -->
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary px-5">
<i class="bi bi-cloud-upload-fill me-2"></i>
Upload File
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -5,22 +5,9 @@
<h2 class="mb-4">Subcontractor vs Client Comparison</h2> <h2 class="mb-4">Subcontractor vs Client Comparison</h2>
<!-- FLASH MESSAGES -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card p-4 shadow-sm"> <div class="card p-4 shadow-sm">
<form method="POST"> <form method="POST">
<!-- SELECT SUBCONTRACTOR --> <!-- SELECT SUBCONTRACTOR -->
<label class="form-label fw-semibold">Select Subcontractor</label> <label class="form-label fw-semibold">Select Subcontractor</label>
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required> <select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>

View File

@@ -8,75 +8,200 @@
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container-fluid vh-100"> <!-- Flash Messages -->
<div class="row h-100 justify-content-center align-items-center"> <div class="position-fixed top-0 end-0 p-3"
style="z-index:1080; width:min(95vw,420px);">
<!-- Increased column width --> {% with messages = get_flashed_messages(with_categories=true) %}
<div class="col-12 col-sm-10 col-md-8 col-lg-5 col-xl-4"> {% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show shadow notification-alert">
<strong>
{% if category=="success" %}
<i class="bi bi-check-circle-fill me-2"></i>Success
{% elif category=="danger" %}
<i class="bi bi-x-circle-fill me-2"></i>Error
{% elif category=="warning" %}
<i class="bi bi-exclamation-triangle-fill me-2"></i>Warning
{% else %}
<i class="bi bi-info-circle-fill me-2"></i>Information
{% endif %}
</strong>
<div>{{ message }}</div>
<div class="progress mt-2" style="height:4px;">
<div class="progress-bar progress-bar-striped progress-bar-animated timer-bar"
style="width:100%"></div>
</div>
<button class="btn-close"
data-bs-dismiss="alert">
</button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<div class="container">
<div class="row justify-content-center align-items-center min-vh-100">
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
<div class="card shadow-lg border-0"> <div class="card shadow-lg border-0">
<!-- Increased padding -->
<div class="card-body p-5"> <div class="card-body p-5">
<!-- Branding -->
<div class="text-center mb-4"> <div class="text-center mb-4">
<img src="{{ url_for('static', filename='images/lcepl.png') }}" alt="LCEPL Logo" <img src="{{ url_for('static', filename='images/lcepl.png') }}"
class="img-fluid mb-3" style="max-height:80px;"> class="img-fluid mb-3"
style="max-height:100px;">
<h4 class="fw-bold mb-1"> <h3 class="fw-bold text-success">Laxmi Civil Engineering Services Pvt Ltd</h3>
Laxmi Civil Engineering Services Pvt Ltd <p class="text-muted">Data Comparison Software (UGD)</p>
</h4>
<p class="text-muted mb-0">
Data Comparison Software Solapur(UGD)
</p>
</div> </div>
<!-- Flash messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Login Form -->
<form method="POST"> <form method="POST">
<div class="mb-4"> <div class="mb-3">
<label class="form-label fw-semibold">User Name</label> <label class="form-label fw-semibold">Username</label>
<input type="email" name="email" class="form-control " placeholder="Enter email"
required>
</div>
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-envelope-fill"></i>
</span>
<input
type="text"
name="email"
class="form-control"
placeholder="Enter Domain Username"
autocomplete="username"
required>
</div>
</div>
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-semibold">Password</label> <label class="form-label fw-semibold">Password</label>
<input type="password" name="password" class="form-control" placeholder="Enter password"
required> <div class="input-group">
<span class="input-group-text">
<i class="bi bi-lock-fill"></i>
</span>
<input
type="password"
id="password"
name="password"
class="form-control"
placeholder="Enter Password"
required>
<button
type="button"
id="togglePassword"
class="input-group-text bg-white border-start-0"
style="cursor:pointer;">
<i class="bi bi-eye text-secondary"></i>
</button>
</div>
</div> </div>
<button class="btn btn-success btn-lg w-100"> <button
class="btn btn-success w-100 btn-lg">
<i class="bi bi-box-arrow-in-right me-2"></i>
Login Login
</button> </button>
<div class="text-center mt-3">
<a href="#">Forgot Password?</a>
</div>
</form> </form>
</div> </div>
</div> </div>
</div> <div class="text-center mt-3">
<small class="text-muted">
© 2026 Laxmi Civil Engineering Services Pvt Ltd
</small>
</div>
</div>
</div> </div>
</div> </div>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
// Password Show / Hide
const togglePassword = document.getElementById("togglePassword");
const password = document.getElementById("password");
if (togglePassword && password) {
togglePassword.addEventListener("click", function () {
const type =
password.getAttribute("type") === "password"
? "text"
: "password";
password.setAttribute("type", type);
this.innerHTML =
type === "password"
? '<i class="bi bi-eye"></i>'
: '<i class="bi bi-eye-slash"></i>';
});
}
// Auto Close Notification after 5 sec
document.querySelectorAll(".notification-alert").forEach(function (alert) {
const progressBar = alert.querySelector(".timer-bar");
let width = 100;
const interval = setInterval(function () {
width -= 2;
progressBar.style.width = width + "%";
if (width <= 0) {
clearInterval(interval);
bootstrap.Alert
.getOrCreateInstance(alert)
.close();
}
}, 100);
});
});
</script>
</body> </body>
</html> </html>

View File

@@ -1,39 +1,169 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="card shadow-sm p-4"> <div class="container-fluid">
<h4 class="mb-3">Add New Subcontractor</h4> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<form action="/subcontractor/save" method="POST"> <div>
<h3 class="fw-bold mb-1">
<div class="mb-3"> <i class="bi bi-person-plus-fill text-success me-2"></i>
<label class="form-label">Subcontractor Name</label> Add New Subcontractor
<input type="text" class="form-control" name="subcontractor_name" required> </h3>
<p class="text-muted mb-0">
Enter the subcontractor details below.
</p>
</div> </div>
<div class="mb-3"> <a href="{{ url_for('subcontractor.subcontractor_list') }}"
<label class="form-label">Contact Person</label> class="btn btn-outline-secondary">
<input type="text" class="form-control" name="contact_person"> <i class="bi bi-arrow-left me-2"></i>
Back to List
</a>
</div>
<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>
<div class="mb-3"> <div class="card-body">
<label class="form-label">Mobile</label>
<input type="text" class="form-control" name="mobile_no"> <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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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 class="mb-3"> </div>
<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>
<button class="btn btn-success">Save</button>
</form>
</div> </div>

View File

@@ -4,36 +4,58 @@
<div class="card shadow-sm p-4"> <div class="card shadow-sm p-4">
<h4 class="mb-3">Edit Subcontractor</h4> <h4 class="mb-3">Edit Subcontractor</h4>
<form action="/subcontractor/update/{{ subcontractor.id }}" method="POST"> <form action="{{ url_for('subcontractor.update_subcontractor', id=subcontractor.id) }}" method="POST">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Subcontractor Name</label> <label class="form-label">Subcontractor Name:</label>
<input type="text" class="form-control" name="subcontractor_name" <input type="text" class="form-control" name="subcontractor_name"
value="{{ subcontractor.subcontractor_name }}" required> value="{{ subcontractor.subcontractor_name }}" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Contact Person</label> <label class="form-label">Contact Person Name:</label>
<input type="text" class="form-control" name="contact_person" value="{{ subcontractor.contact_person }}"> <input type="text" class="form-control" name="contact_person" value="{{ subcontractor.contact_person }}">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Mobile</label> <label class="form-label">Address:</label>
<input type="text" class="form-control" name="address" value="{{ subcontractor.address }}">
</div>
<div class="mb-3">
<label class="form-label">Mobile No:</label>
<input type="text" class="form-control" name="mobile_no" value="{{ subcontractor.mobile_no }}"> <input type="text" class="form-control" name="mobile_no" value="{{ subcontractor.mobile_no }}">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Email</label> <label class="form-label">Email:</label>
<input type="email" class="form-control" name="email_id" value="{{ subcontractor.email_id }}"> <input type="email" class="form-control" name="email_id" value="{{ subcontractor.email_id }}">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">GST No</label> <label class="form-label">GST No:</label>
<input type="text" class="form-control" name="gst_no" value="{{ subcontractor.gst_no }}"> <input type="text" class="form-control" name="gst_no" value="{{ subcontractor.gst_no }}">
</div> </div>
<div class="mb-3">
<label class="form-label">PAN No:</label>
<input type="text" class="form-control" name="pan_no" value="{{ subcontractor.pan_no }}">
</div>
<div class="mb-3">
<label class="form-label">Status:</label>
<select name="status" class="form-control">
<option value="Active" {% if subcontractor.status=="Active" %}selected{% endif %}>
Active
</option>
<option value="Inactive" {% if subcontractor.status=="Inactive" %}selected{% endif %}>
Inactive
</option>
</select>
</div>
<button class="btn btn-success">Update</button> <button class="btn btn-success">Update</button>
<a href="/subcontractor/list" class="btn btn-secondary">Back</a> <a href="{{ url_for('subcontractor.subcontractor_list') }}" class="btn btn-secondary">Back</a>
</form> </form>
</div> </div>

View File

@@ -1,89 +1,160 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="card shadow-sm p-3 p-md-4"> <div class="container mt-4">
<!-- Header --> <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-3">
<div class="row mb-3 align-items-center"> <h4 class="mb-3 mb-md-0">Subcontractor List</h4>
<div class="col-12 col-md-6 ">
<h4 class="mb-2 mb-md-0 text-center text-md-start">
Subcontractor List
</h4>
</div>
<div class="col-12 col-md-3 text-center text-md-end"> <a href="{{ url_for('subcontractor.add_subcontractor') }}" class="btn btn-primary btn-sm">
<a href="/subcontractor/add" class="btn btn-success w-100 w-md-auto"> + Add Subcontractor
Add Subcontractor </a>
</a>
</div>
</div> </div>
<div class="card shadow-sm">
<div class="card-body p-2 p-md-3">
<div class="table-responsive"> <!-- Desktop Table View -->
<table class="table table-bordered table-striped align-middle text-nowrap"> <div class="table-responsive d-none d-md-block">
<thead class="table-dark"> <table class="table table-bordered table-hover align-middle">
<tr> <thead class="table-dark">
<th>ID</th> <tr>
<th>Name</th> <th>Sr. No</th>
<th class="d-none d-md-table-cell">Mobile</th> <th>Subcontractor Name</th>
<th class="d-none d-lg-table-cell">Email</th> <th>GST No</th>
<th class="d-none d-lg-table-cell">GST No</th> <th>Mobile</th>
<th class="text-center">Action</th> <th>Email</th>
</tr> <th>Status</th>
</thead> <th width="150">Action</th>
</tr>
</thead>
<tbody>
{% for s in subcontractors %}
<tr>
<td>{{ s.id }}</td>
<td>{{ s.subcontractor_name }}</td>
<td>{{ s.gst_no }}</td>
<td>{{ s.mobile_no }}</td>
<td>{{ s.email_id }}</td>
<td>
{% if s.status == "Active" %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-danger">Inactive</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('subcontractor.edit_subcontractor', id=s.id) }}"
class="btn btn-sm btn-warning mb-1">
Edit
</a>
<tbody> <a href="{{ url_for('subcontractor.delete_subcontractor', id=s.id) }}"
class="btn btn-sm btn-danger" onclick="return confirm('Are you sure to delete?')">
Delete
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Mobile Card View -->
<div class="d-md-none">
{% for s in subcontractors %} {% for s in subcontractors %}
<tr> <div class="card mb-3 shadow-sm">
<td>{{ s.id }}</td> <div class="card-body p-3">
<h5 class="fw-bold mb-2">{{ s.subcontractor_name }}</h5>
<td class="fw-semibold text-wrap"> <p class="mb-1"><strong>GST No:</strong> {{ s.gst_no }}</p>
{{ s.subcontractor_name }} <p class="mb-1"><strong>Mobile:</strong> {{ s.mobile_no }}</p>
</td> <p class="mb-1"><strong>Email:</strong> {{ s.email_id }}</p>
<td class="d-none d-md-table-cell"> <p class="mb-2">
{{ s.mobile_no }} <strong>Status:</strong>
</td> {% if s.status == "Active" %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-danger">Inactive</span>
{% endif %}
</p>
<td class="d-none d-lg-table-cell"> <div class="d-flex gap-2">
{{ s.email_id }} <a href="{{ url_for('subcontractor.edit_subcontractor', id=s.id) }}"
</td> class="btn btn-sm btn-warning w-50">
<td class="d-none d-lg-table-cell">
{{ s.gst_no }}
</td>
<!-- Action Buttons -->
<td class="text-center">
<div class="d-flex flex-column gap-1">
<a href="/subcontractor/edit/{{ s.id }}" class="btn btn-sm btn-warning">
Edit Edit
</a> </a>
<a href="/subcontractor/delete/{{ s.id }}" class="btn btn-sm btn-danger"
onclick="return confirm('Are you sure?')"> <a href="{{ url_for('subcontractor.delete_subcontractor', id=s.id) }}"
class="btn btn-sm btn-danger w-50" onclick="return confirm('Are you sure to delete?')">
Delete Delete
</a> </a>
</div> </div>
</td> </div>
</tr> </div>
{% endfor %} {% endfor %}
</tbody> </div>
<!-- TOTAL ROW --> <!-- Pagination -->
<tfoot> <nav>
<tr class="table-secondary fw-bold"> <ul class="pagination justify-content-center flex-wrap mt-4">
<td colspan="2" class="text-end"> Total Subcontractors</td>
<td class="d-none d-lg-table-cell">-</td> {% if pagination.has_prev %}
<td class="d-none d-lg-table-cell">-</td> <li class="page-item">
<td class="d-none d-lg-table-cell">-</td> <a class="page-link" href="{{ url_for('subcontractor.subcontractor_list', page=1) }}">
First
</a>
</li>
<td class="text-center">{{ subcontractors|length }}</td> <li class="page-item">
<a class="page-link"
href="{{ url_for('subcontractor.subcontractor_list', page=pagination.prev_num) }}">
Prev
</a>
</li>
{% endif %}
</tr> {% for page_num in pagination.iter_pages() %}
</tfoot> {% if page_num %}
{% if page_num != pagination.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('subcontractor.subcontractor_list', page=page_num) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
</table> {% if pagination.has_next %}
<li class="page-item">
<a class="page-link"
href="{{ url_for('subcontractor.subcontractor_list', page=pagination.next_num) }}">
Next
</a>
</li>
<li class="page-item">
<a class="page-link"
href="{{ url_for('subcontractor.subcontractor_list', page=pagination.pages) }}">
Last
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,379 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css">
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
<div class="container-fluid py-4">
<!-- HEADER -->
<div class="card shadow border-0 mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h3 class="fw-bold text-primary">
<i class="bi bi-bar-chart-fill"></i>
RA Bill Analytics Dashboard
</h3>
<small class="text-muted">
Compare Quantity, Depth & RA Bill Analytics
</small>
</div>
<div>
<button class="btn btn-success" id="exportExcel">
<i class="bi bi-file-earmark-excel"></i>
Excel
</button>
<button class="btn btn-danger" id="exportPDF">
<i class="bi bi-file-earmark-pdf"></i>
PDF
</button>
</div>
</div>
</div>
</div>
<!-- FILTERS -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-funnel-fill"></i>
Filters
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<!-- Contractor -->
<div class="col-lg-4">
<label class="form-label fw-bold"> Subcontractor </label>
<select class="form-select" id="subcontractor">
<option value="">--- select contractor ---</option>
{% for s in subcontractors %}
<option value="{{s.id}}">
{{s.subcontractor_name}}
</option>
{% endfor %}
</select>
</div>
<!-- Category -->
<div class="col-lg-4">
<label class="form-label fw-bold">Category</label>
<select class="form-select" id="category">
<option value="">--- select category ---</option>
<option value="trench_excavation">Trench Excavation</option>
<option value="manhole_excavation">Manhole Excavation</option>
<option value="Manhole_Domestic_Chamber">Manhole Domestic Chamber</option>
<option value="Laying">Pipe Laying</option>
</select>
</div>
<!-- RA Bill -->
<div class="col-lg-4">
<label class="form-label fw-bold"> RA Bills</label>
<select id="ra_bill" class="form-select" multiple></select>
</div>
</div>
<hr>
<!-- Search button -->
<button class="btn btn-primary" id="searchBtn">
<i class="bi bi-search"></i>Search
</button>
<!-- Reset button -->
<button class="btn btn-secondary" id="resetBtn">
<i class="bi bi-arrow-clockwise"></i>Reset
</button>
</div>
</div>
<!-- TABS -->
<div class="card shadow">
<div class="card-header">
<ul class="nav nav-pills">
<!--Bar Chart Tab -->
<li class="nav-item">
<button class="nav-link active"
data-bs-toggle="tab"
data-bs-target="#barTab">
<i class="bi bi-bar-chart"></i> Bar Chart
</button>
</li>
<!-- Data Table Tab -->
<li class="nav-item">
<button class="nav-link"
data-bs-toggle="tab"
data-bs-target="#tableTab">
<i class="bi bi-table"></i> Data Table
</button>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- BAR TAB -->
<div class="tab-pane fade show active" id="barTab">
<div style="height:600px">
<canvas id="barChart"></canvas>
</div>
</div>
<!-- TABLE TAB -->
<div class="tab-pane fade" id="tableTab">
<div class="table-responsive">
<table class="table table-bordered table-hover" id="resultTable">
<thead class="table-dark">
<tr>
<th>Sr No</th>
<th>Strata Type & Depth</th>
<th class="text-end">Client Qty</th>
<th class="text-end">Sub Contractor Qty</th>
<th class="text-end">Difference</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
let barChart;
let raBillChoice;
/* Load RA Bills */
function loadRABills(){
let subcontractor = document.getElementById("subcontractor").value
let category = document.getElementById("category").value
if (!raBillChoice)
return;
if (!subcontractor || !category) {
raBillChoice.clearStore();
return;
}
fetch(`/dashboard/api/get-ra-bills?subcontractor=${subcontractor}&category=${category}`)
.then(res => res.json())
.then(data => {
raBillChoice.clearStore();
let choices = [];
data.ra_bills.forEach(function(bill){
choices.push({
value: bill,
label: bill
});
});
raBillChoice.setChoices(
choices,
"value",
"label",
true
);
});
}
/* Search */
function loadDashboard() {
const subcontractor = document.getElementById("subcontractor").value;
const category = document.getElementById("category").value;
let raBills = [];
if (raBillChoice) {
raBills = raBillChoice.getValue(true);
}
let apiUrl = "";
switch (category) {
case "trench_excavation":
apiUrl = "/dashboard/api/tr-analysis";
break;
case "manhole_excavation":
apiUrl = "/dashboard/api/mh-analysis";
break;
case "Manhole_Domestic_Chamber":
apiUrl = "/dashboard/api/mdc-analysis";
break;
case "Laying":
apiUrl = "/dashboard/api/laying-analysis";
break;
default:
alert("Please select category");
return;
}
fetch(`${apiUrl}?subcontractor=${subcontractor}&category=${category}&ra_bill=${raBills.join(",")}`)
.then(response => response.json())
.then(data => {
drawBar(data);
drawTable(data);
})
.catch(err => console.error(err));
}
/* BAR */
function drawBar(data) {
if (barChart) {
barChart.destroy();
}
const ctx = document.getElementById("barChart");
barChart = new Chart(ctx, {
type: "bar",
data: {
labels: data.labels,
datasets: [
{
label: "Client Qty",
data: data.client_qty,
backgroundColor: "#0d6efd",
borderColor: "#0d6efd",
borderWidth: 1
},
{
label: "Sub-Contractor Qty",
data: data.sub_qty,
backgroundColor: "#fd7e14",
borderColor: "#fd7e14",
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: "index",
intersect: false
},
plugins: {
title: {
display: true,
text: data.title
},
legend: {
position: "bottom"
}
},
scales: {
x: {
ticks: {
autoSkip: false,
maxRotation: 45,
minRotation: 45
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: data.y_title
}
}
}
}
});
}
/* TABLE */
function drawTable(data){
let html='';
for(let i=0;i<data.labels.length;i++){
const clientQty = Number(data.client_qty[i] || 0);
const subQty = Number(data.sub_qty[i] || 0);
const diff = clientQty - subQty;
html+=`
<tr>
<td class="text-center">${i + 1}</td>
<td>${data.labels[i]}</td>
<td class="text-end">${data.client_qty[i]}</td>
<td class="text-end">${data.sub_qty[i]}</td>
<td class="text-end fw-bold ${diff >= 0 ? 'text-success' : 'text-danger'}">${diff.toFixed(2)}</td>
</tr>
`;
}
document.querySelector("#resultTable tbody").innerHTML = html;
}
/* EVENTS */
document.getElementById("subcontractor").addEventListener("change", loadRABills);
document.getElementById("category").addEventListener("change", loadRABills);
document.getElementById("searchBtn").addEventListener("click", loadDashboard);
document.getElementById("resetBtn").addEventListener("click", function () {location.reload();});
document.addEventListener("DOMContentLoaded", function () {
raBillChoice = new Choices("#ra_bill", {
removeItemButton: true,
searchEnabled: true,
searchPlaceholderValue: "Search RA Bills",
placeholder: true,
placeholderValue: "Select RA Bills",
shouldSort: false,
itemSelectText: ""
});
});
</script>
{% endblock %}

View File

@@ -1,117 +1,315 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container my-4">
<h2 class="text-center mb-4">Generate Subcontractor Report</h2> <div class="container-fluid py-4">
<!-- FORM --> <!-- Page Header -->
<div class="card shadow-sm p-3 p-md-4 mx-auto" style="max-width:600px;"> <div class="card shadow-sm border-0 mb-4">
<form method="POST"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div class="mb-3"> <div>
<label class="form-label fw-semibold">Select Subcontractor</label> <h2 class="fw-bold text-primary mb-1">
<select name="subcontractor_id" class="form-select" required> <i class="bi bi-file-earmark-bar-graph"></i>
<option value="">-- Select Subcontractor --</option> Subcontractor Report
{% for sc in subcontractors %} </h2>
<option value="{{ sc.id }}" {% if selected_sc_id==sc.id|string %}selected{% endif %}> <small class="text-muted">
{{ sc.subcontractor_name }} View, Filter, Edit and Delete Subcontractor Records
</option> </small>
{% endfor %}
</select>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="downloadAllSwitch" name="download_all" value="true"
{% if download_all %}checked{% endif %}>
<label class="form-check-label fw-bold text-primary">
Download All RA Bills
</label>
</div>
<div class="mb-4" id="ra_bill_container">
<label class="form-label fw-semibold">RA Bill Number</label>
<input type="text" name="ra_bill_no" id="ra_bill_input" class="form-control"
value="{{ ra_bill_no or '' }}">
</div>
<div class="row g-2">
<div class="col-12 col-md-6">
<button class="btn btn-outline-primary w-100" name="action" value="preview">
Preview Data
</button>
</div>
<div class="col-12 col-md-6">
<button class="btn btn-primary w-100" name="action" value="download">
Download Excel
</button>
</div> </div>
</div> </div>
</div>
</div>
</form> <!-- Filter Card -->
<div class="card shadow-sm border-0">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-funnel-fill"></i>
Report Filters
</h5>
</div>
<div class="card-body">
<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-muted fw-normal"></span>
</label>
<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 %}>
{{ sc.subcontractor_name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-lg-3">
<label class="form-label fw-semibold"> RA Bill No</label>
<input
type="text"
name="ra_bill_no"
class="form-control"
placeholder="Enter RA Bill"
value="{{ selected_ra_bill or '' }}">
</div>
<div class="col-lg-3">
<label class="form-label fw-semibold"> MH No </label>
<input
type="text"
name="mh_no"
class="form-control"
placeholder="Enter MH No"
value="{{ selected_mh_no or '' }}">
</div>
<div class="col-lg-3">
<label class="form-label fw-semibold">Work Category</label>
<select name="category" class="form-select">
<option value="all">All Categories</option>
<option value="tr"
{% if request.form.get('category')=='tr' %}selected{% endif %}>
Trench Excavation
</option>
<option value="mh"
{% if request.form.get('category')=='mh' %}selected{% endif %}>
Manhole Excavation
</option>
<option value="dc"
{% if request.form.get('category')=='dc' %}selected{% endif %}>
Manhole Domestic Chamber
</option>
<option value="laying"
{% if request.form.get('category')=='laying' %}selected{% endif %}>
Pipe Laying
</option>
</select>
</div>
</div>
<div class="mt-4 d-flex justify-content-end gap-2">
<!-- Preview -->
<button type="submit" name="action" value="preview"class="btn btn-primary">
<i class="bi bi-search"></i>
Preview Report
</button>
<!-- Download -->
<div class="btn-group">
<button type="button" class="btn btn-success dropdown-toggle"data-bs-toggle="dropdown">
<i class="bi bi-download"></i>
Download Report
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button class="dropdown-item"type="submit" name="action" value="excel">
<i class="bi bi-file-earmark-excel text-success"></i>
Download Excel
</button>
</li>
<li>
<button class="dropdown-item" type="submit" name="action"
value="excel_all">
<i class="bi bi-file-earmark-spreadsheet text-primary"></i>
Download All Excel
</button>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<button class="dropdown-item"type="submit"name="action" value="pdf">
<i class="bi bi-file-earmark-pdf text-danger"></i>
PDF (Coming Soon)
</button>
</li>
</ul>
</div>
<button type="reset" class="btn btn-secondary" id="resetBtn">
<i class="bi bi-arrow-clockwise"></i>
Reset
</button>
</div>
</form>
</div>
</div> </div>
{% if tables %} {% if tables %}
<!-- REPORT PREVIEW --> {% set show_all = (not selected_category) or selected_category == 'all' %}
<div class="mt-5"> <!-- 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 {% if show_all %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#abstract">
<i class="bi bi-file-earmark-text"></i> Abstract
</button>
</li>
<h3 class="text-center mb-3">Report Preview</h3> {% if show_all or selected_category == 'tr' %}
<li class="nav-item">
<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 %}
<div class="card shadow-sm"> {% if show_all or selected_category == 'mh' %}
<li class="nav-item">
<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 %}
<!-- MOBILE SCROLLABLE TABS --> {% if show_all or selected_category == 'dc' %}
<div class="card-header p-0"> <li class="nav-item">
<ul class="nav nav-tabs flex-nowrap overflow-auto"> <button class="nav-link {% if selected_category == 'dc' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#dc">
<li class="nav-item"> <i class="bi bi-grid-3x3"></i> Manhole & Domestic Chambers Construction
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tr"> </button>
Trench Excavation </li>
</button> {% endif %}
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#mh">
Manhole Excavation
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#dc">
Manhole & Domestic Chambers
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#laying">
Pipe Laying
</button>
</li>
</ul>
</div>
<!-- TAB CONTENT --> {% if show_all or selected_category == 'laying' %}
<div class="card-body tab-content"> <li class="nav-item">
<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="tab-pane fade show active" id="tr"> <div class="card-body">
<div class="table-responsive overflow-auto"> <div class="tab-content">
{{ tables.tr | safe }}
</div> <div class="tab-pane fade {% if show_all %}show active{% endif %}" id="abstract">
{{ abstract_html|safe }}
</div> </div>
<div class="tab-pane fade" id="mh"> {% if show_all or selected_category == 'tr' %}
<div class="table-responsive overflow-auto"> <!-- Trench -->
{{ tables.mh | safe }} <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>
</div> </div>
{% endif %}
<div class="tab-pane fade" id="dc"> {% if show_all or selected_category == 'mh' %}
<div class="table-responsive overflow-auto"> <!-- MH -->
{{ tables.dc | safe }} <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>
</div> </div>
{% endif %}
<div class="tab-pane fade" id="laying"> {% if show_all or selected_category == 'dc' %}
<div class="table-responsive overflow-auto"> <!-- DC -->
{{ tables.laying | safe }} <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 {% 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>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -119,24 +317,314 @@
</div> </div>
<!-- BOOTSTRAP-ONLY JS -->
<script> <script>
function toggleRAInput() { // NOTE: this content block renders before base.html's bottom-of-body
const checkbox = document.getElementById("downloadAllSwitch"); // <script src="jquery..."> tag, so $ is not defined yet if this runs
const input = document.getElementById("ra_bill_input"); // immediately - that was silently breaking the location cascade
const container = document.getElementById("ra_bill_container"); // (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 () {
if (checkbox.checked) { // Remember which tab is active so that actions which reload the
input.value = ""; // page (Delete Selected, bulk delete, bulk-edit Save) bring you
input.disabled = true; // back to the tab you were working on instead of resetting to
container.classList.add("opacity-50"); // the Abstract tab (the default "active" one in the markup).
} else { const TAB_STORAGE_KEY = "subReportActiveTab";
input.disabled = false;
container.classList.remove("opacity-50"); $(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 () {
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
// 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'],
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);
}
});
// 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);
});
// 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);
});
// ============== 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}"]`)
};
} }
function enterBulkEditMode(model) {
bulkEditActiveModels[model] = true;
pendingBulkChanges[model] = {};
// 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);
}
$(`#${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(payload)
})
.then(res => res.json().then(data => ({ ok: res.ok, data })))
.then(({ ok, data }) => {
if (ok && data.status === "success") {
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("Update failed: " + (data.message || "Unknown error"));
}
})
.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");
let model = $(this).data("model");
if (!confirm("Are you sure you want to delete this record?")) return;
fetch("/file/delete_records", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: model, ids: [id] })
})
.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);
});
});
});
// 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);
});
} }
document.addEventListener("DOMContentLoaded", toggleRAInput);
document.getElementById("downloadAllSwitch").addEventListener("change", toggleRAInput);
</script> </script>
{% endblock %} {% endblock %}

8
app/utils/exceptions.py Normal file
View File

@@ -0,0 +1,8 @@
from app.constants.http_status import HTTPStatus
class APIException(Exception):
def __init__(self, message, status_code=HTTPStatus.BAD_REQUEST, errors=None):
self.message = message
self.status_code = status_code
self.errors = errors
super().__init__(self.message)

View File

@@ -1,6 +1,40 @@
import os import os
from app.config import Config from flask import current_app
# file extension
ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"}
# .log file allowed
ALLOWED_LOG_FILE = {
"app.log",
"debug.log",
"error.log"
}
# Get path of format excel folder
def get_download_format_folder():
return os.path.join(
current_app.root_path,
"static",
"downloads",
"format"
)
# Get path of uploads folder
def get_uploads_folder():
return os.path.join(
current_app.root_path,
"static",
"uploads"
)
def ensure_upload_folder(): def ensure_upload_folder():
if not os.path.exists(Config.UPLOAD_FOLDER): if not os.path.exists(get_uploads_folder()):
os.makedirs(Config.UPLOAD_FOLDER) os.makedirs(get_uploads_folder())
# Get path of logs folder
def get_logs_folder():
project_root = os.path.dirname(current_app.root_path)
return os.path.join(project_root, "logs")

10
app/utils/plot_utils.py Normal file
View File

@@ -0,0 +1,10 @@
import base64
from io import BytesIO
import matplotlib
matplotlib.use("Agg")
def plot_to_base64(plt):
img = BytesIO()
plt.savefig(img, format="png", bbox_inches="tight")
img.seek(0)
return base64.b64encode(img.read()).decode("utf-8")

12
app/utils/regex_utils.py Normal file
View File

@@ -0,0 +1,12 @@
import re
class RegularExpression:
# sum fields of TrEx, MhEx (_total)
STR_TOTAL_PATTERN = re.compile(r".*_total$")
# sum fields of pipe laying (pipe_150_mm)
PIPE_MM_PATTERN = re.compile(r"^pipe_\d+_mm$")
# sum fields of MH dc (d_0_to_0_75)
D_RANGE_PATTERN = re.compile( r"^d_\d+(?:_\d+)?_to_\d+(?:_\d+)?$")

View File

@@ -0,0 +1,23 @@
from flask import jsonify
from app.constants.http_status import HTTPStatus
class ResponseHandler:
@staticmethod
def success(message, data=None, status_code=HTTPStatus.OK):
return jsonify({
"status": "success",
"message": message,
"data": data if data else {},
"errors": []
}), status_code
@staticmethod
def error(message, errors=None, status_code=HTTPStatus.BAD_REQUEST):
return jsonify({
"status": "error",
"message": message,
"data": {},
"errors": errors if errors else []
}), status_code

46
docker-compose.yml Normal file
View File

@@ -0,0 +1,46 @@
version: '3.8'
services:
db:
image: mysql:8.0
container_name: comparison_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: comparisondb
ports:
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
app:
build: .
container_name: comparison_app
restart: always
environment:
FLASK_ENV: development
FLASK_DEBUG: "True"
FLASK_HOST: "0.0.0.0"
FLASK_PORT: "5001"
DB_DIALECT: mysql
DB_DRIVER: pymysql
DB_HOST: db
DB_PORT: 3306
DB_NAME: comparisondb
DB_USER: root
DB_PASSWORD: root
ports:
- "5001:5001"
depends_on:
- db
volumes:
- ./app/logs:/app/app/logs
- ./app/static/uploads:/app/app/static/uploads
- ./app/static/downloads:/app/app/static/downloads
volumes:
mysql_data:

Binary file not shown.

View File

@@ -1,200 +1,3 @@
2025-12-09 13:11:05,606 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. 2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | ======================================================================
* Running on http://127.0.0.1:5000 2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | Application Started Successfully
2025-12-09 13:11:05,607 | INFO | Press CTRL+C to quit 2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | ======================================================================
2025-12-09 13:11:05,608 | INFO | * Restarting with stat
2025-12-09 13:11:06,239 | WARNING | * Debugger is active!
2025-12-09 13:11:06,240 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:11:48,880 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
2025-12-09 13:11:48,881 | INFO | Press CTRL+C to quit
2025-12-09 13:11:48,882 | INFO | * Restarting with stat
2025-12-09 13:11:49,519 | WARNING | * Debugger is active!
2025-12-09 13:11:49,521 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:12:05,727 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\user_service.py', reloading
2025-12-09 13:12:05,826 | INFO | * Restarting with stat
2025-12-09 13:12:06,499 | WARNING | * Debugger is active!
2025-12-09 13:12:06,501 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:12:09,545 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading
2025-12-09 13:12:09,654 | INFO | * Restarting with stat
2025-12-09 13:12:10,286 | WARNING | * Debugger is active!
2025-12-09 13:12:10,288 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:12:12,311 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\routes\\auth.py', reloading
2025-12-09 13:12:12,407 | INFO | * Restarting with stat
2025-12-09 13:12:13,071 | WARNING | * Debugger is active!
2025-12-09 13:12:13,072 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:12:16,128 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading
2025-12-09 13:12:16,257 | INFO | * Restarting with stat
2025-12-09 13:12:16,898 | WARNING | * Debugger is active!
2025-12-09 13:12:16,900 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:12:20,944 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\routes\\user.py', reloading
2025-12-09 13:12:21,042 | INFO | * Restarting with stat
2025-12-09 13:12:21,719 | WARNING | * Debugger is active!
2025-12-09 13:12:21,721 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:12:23,762 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\routes\\file_import.py', reloading
2025-12-09 13:12:23,870 | INFO | * Restarting with stat
2025-12-09 13:12:24,505 | WARNING | * Debugger is active!
2025-12-09 13:12:24,507 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:12:27,561 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\__init__.py', reloading
2025-12-09 13:12:27,670 | INFO | * Restarting with stat
2025-12-09 13:12:28,294 | WARNING | * Debugger is active!
2025-12-09 13:12:28,296 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:12:31,336 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\db_service.py', reloading
2025-12-09 13:12:31,448 | INFO | * Restarting with stat
2025-12-09 13:12:32,097 | WARNING | * Debugger is active!
2025-12-09 13:12:32,099 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:13:05,662 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading
2025-12-09 13:13:05,773 | INFO | * Restarting with stat
2025-12-09 13:13:06,466 | WARNING | * Debugger is active!
2025-12-09 13:13:06,469 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:13:10,944 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
2025-12-09 13:13:10,944 | INFO | Press CTRL+C to quit
2025-12-09 13:13:10,945 | INFO | * Restarting with stat
2025-12-09 13:13:11,623 | WARNING | * Debugger is active!
2025-12-09 13:13:11,625 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:14:11,295 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
2025-12-09 13:14:11,393 | INFO | * Restarting with stat
2025-12-09 13:14:12,004 | WARNING | * Debugger is active!
2025-12-09 13:14:12,006 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:14:32,108 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:14:32,109 | INFO | Press CTRL+C to quit
2025-12-09 13:14:32,110 | INFO | * Restarting with stat
2025-12-09 13:14:32,699 | WARNING | * Debugger is active!
2025-12-09 13:14:32,701 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:15:58,632 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
2025-12-09 13:15:58,733 | INFO | * Restarting with stat
2025-12-09 13:15:59,415 | WARNING | * Debugger is active!
2025-12-09 13:15:59,416 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:16:03,475 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
2025-12-09 13:16:03,583 | INFO | * Restarting with stat
2025-12-09 13:16:04,204 | WARNING | * Debugger is active!
2025-12-09 13:16:04,206 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:16:33,504 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
2025-12-09 13:16:33,605 | INFO | * Restarting with stat
2025-12-09 13:16:34,213 | WARNING | * Debugger is active!
2025-12-09 13:16:34,215 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:16:41,815 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
2025-12-09 13:16:41,816 | INFO | Press CTRL+C to quit
2025-12-09 13:18:12,302 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
2025-12-09 13:18:12,302 | INFO | Press CTRL+C to quit
2025-12-09 13:22:07,114 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:22:07,114 | INFO | Press CTRL+C to quit
2025-12-09 13:22:07,116 | INFO | * Restarting with stat
2025-12-09 13:22:07,935 | WARNING | * Debugger is active!
2025-12-09 13:22:07,937 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:23:21,204 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
2025-12-09 13:23:21,305 | INFO | * Restarting with stat
2025-12-09 13:24:06,973 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:24:06,973 | INFO | Press CTRL+C to quit
2025-12-09 13:24:06,974 | INFO | * Restarting with stat
2025-12-09 13:24:07,689 | WARNING | * Debugger is active!
2025-12-09 13:24:07,691 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:24:36,315 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\app.py', reloading
2025-12-09 13:24:36,418 | INFO | * Restarting with stat
2025-12-09 13:24:37,074 | WARNING | * Debugger is active!
2025-12-09 13:24:37,076 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:26:54,442 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
2025-12-09 13:26:54,543 | INFO | * Restarting with stat
2025-12-09 13:26:59,170 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:26:59,170 | INFO | Press CTRL+C to quit
2025-12-09 13:26:59,171 | INFO | * Restarting with stat
2025-12-09 13:26:59,827 | WARNING | * Debugger is active!
2025-12-09 13:26:59,829 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:28:47,631 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
2025-12-09 13:28:47,747 | INFO | * Restarting with stat
2025-12-09 13:28:48,478 | WARNING | * Debugger is active!
2025-12-09 13:28:48,480 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:28:51,150 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:28:51,151 | INFO | Press CTRL+C to quit
2025-12-09 13:28:51,153 | INFO | * Restarting with stat
2025-12-09 13:28:51,788 | WARNING | * Debugger is active!
2025-12-09 13:28:51,790 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:28:54,904 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
2025-12-09 13:28:55,010 | INFO | * Restarting with stat
2025-12-09 13:28:55,608 | WARNING | * Debugger is active!
2025-12-09 13:28:55,610 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:28:56,644 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
2025-12-09 13:28:56,752 | INFO | * Restarting with stat
2025-12-09 13:29:04,454 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:29:04,454 | INFO | Press CTRL+C to quit
2025-12-09 13:29:04,455 | INFO | * Restarting with stat
2025-12-09 13:29:05,096 | WARNING | * Debugger is active!
2025-12-09 13:29:05,098 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:30:01,657 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:30:01,657 | INFO | Press CTRL+C to quit
2025-12-09 13:30:01,658 | INFO | * Restarting with stat
2025-12-09 13:30:02,278 | WARNING | * Debugger is active!
2025-12-09 13:30:02,280 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:30:27,872 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:30:27,872 | INFO | Press CTRL+C to quit
2025-12-09 13:30:27,873 | INFO | * Restarting with stat
2025-12-09 13:30:28,474 | WARNING | * Debugger is active!
2025-12-09 13:30:28,476 | INFO | * Debugger PIN: 105-645-384
2025-12-09 13:33:22,709 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:33:22,709 | INFO | Press CTRL+C to quit
2025-12-09 13:33:22,710 | INFO | * Restarting with stat
2025-12-09 13:33:23,778 | WARNING | * Debugger is active!
2025-12-09 13:33:23,781 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:33:29,939 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\db_service.py', reloading
2025-12-09 13:33:30,080 | INFO | * Restarting with stat
2025-12-09 13:33:44,462 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:33:44,462 | INFO | Press CTRL+C to quit
2025-12-09 13:33:44,464 | INFO | * Restarting with stat
2025-12-09 13:33:45,216 | WARNING | * Debugger is active!
2025-12-09 13:33:45,218 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:35:23,298 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:35:23,299 | INFO | Press CTRL+C to quit
2025-12-09 13:35:23,301 | INFO | * Restarting with stat
2025-12-09 13:35:24,098 | WARNING | * Debugger is active!
2025-12-09 13:35:24,100 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:38:25,991 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
2025-12-09 13:38:26,126 | INFO | * Restarting with stat
2025-12-09 13:38:27,120 | WARNING | * Debugger is active!
2025-12-09 13:38:27,122 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:38:37,386 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading
2025-12-09 13:38:37,513 | INFO | * Restarting with stat
2025-12-09 13:38:38,297 | WARNING | * Debugger is active!
2025-12-09 13:38:38,300 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:38:45,485 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
2025-12-09 13:38:45,605 | INFO | * Restarting with stat
2025-12-09 13:38:46,348 | WARNING | * Debugger is active!
2025-12-09 13:38:46,350 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:38:55,109 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:38:55,109 | INFO | Press CTRL+C to quit
2025-12-09 13:38:55,110 | INFO | * Restarting with stat
2025-12-09 13:38:55,959 | WARNING | * Debugger is active!
2025-12-09 13:38:55,961 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:39:27,813 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
2025-12-09 13:39:27,937 | INFO | * Restarting with stat
2025-12-09 13:39:28,684 | WARNING | * Debugger is active!
2025-12-09 13:39:28,687 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:40:00,602 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
2025-12-09 13:40:00,728 | INFO | * Restarting with stat
2025-12-09 13:40:01,428 | WARNING | * Debugger is active!
2025-12-09 13:40:01,430 | INFO | * Debugger PIN: 697-115-033
2025-12-09 13:40:21,531 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 13:40:21,531 | INFO | Press CTRL+C to quit
2025-12-09 13:40:21,533 | INFO | * Restarting with stat
2025-12-09 13:40:22,307 | WARNING | * Debugger is active!
2025-12-09 13:40:22,309 | INFO | * Debugger PIN: 697-115-033
2025-12-09 14:03:58,363 | INFO | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
2025-12-09 14:03:58,363 | INFO | Press CTRL+C to quit
2025-12-09 14:03:58,364 | INFO | * Restarting with stat
2025-12-09 14:03:59,038 | WARNING | * Debugger is active!
2025-12-09 14:03:59,041 | INFO | * Debugger PIN: 697-115-033

View File

@@ -7,3 +7,6 @@ python-dotenv
cryptography cryptography
xlsxwriter xlsxwriter
matplotlib matplotlib
flask_sqlalchemy
flask_migrate
weasyprint