65 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
0007d6e87d client laying and bedding in ui added 2026-01-17 17:33:07 +05:30
8cba86323e sub-cont model validations 2026-01-17 15:41:26 +05:30
e4837b203a modify file_report 2026-01-17 14:50:30 +05:30
4fa35fe05a Renames of report html files 2026-01-17 14:07:32 +05:30
a07c1ddf8a renames of report html files 2026-01-17 14:01:15 +05:30
15b07b3d55 marge code of static Menubar apply boostrap css 2026-01-16 15:01:40 +05:30
dd4b494435 static Menubar apply boostrap css 2026-01-16 13:04:59 +05:30
ff5da926f3 Merge pankaj-dev into main 2026-01-16 11:30:49 +05:30
1022923509 changes of report 2026-01-16 11:27:26 +05:30
c9a27b098b import laying model of client side 2026-01-15 23:50:08 +05:30
8f817c94fd Merged pankaj-dev into main 2026-01-15 14:29:58 +05:30
0bb670b152 suncontractor and client download ui changes -a 2026-01-15 14:21:36 +05:30
aad9b2b967 import modify subcontractor sheet Laying model import added 2026-01-15 12:27:29 +05:30
e0541ff80c comment on html page 2026-01-14 11:12:40 +05:30
ed85313d42 Update README.md 2026-01-13 11:43:08 +00:00
a853992e64 Update README.md 2026-01-13 11:12:33 +00:00
677df59863 Merged pankaj-dev into main after resolving conflicts 2026-01-13 15:40:45 +05:30
d46c7c0936 changes of ui mobile view of dashboard 2026-01-13 15:22:53 +05:30
a73d8e5ed1 changes of manu bar add report and dashboard 2026-01-13 13:21:06 +05:30
75 changed files with 6990 additions and 1226 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

16
.env
View File

@@ -4,7 +4,7 @@
FLASK_ENV=development
FLASK_DEBUG=True
FLASK_HOST=0.0.0.0
FLASK_PORT=5001
FLASK_PORT=5015
# -----------------------------
# Security
@@ -20,12 +20,18 @@ DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=comparisondb
DB_USER=root
DB_PASSWORD=admin
DB_PASSWORD=root
# DATABASE_URL=mysql+pymysql://root:root@localhost/comparisondb
# -----------------------------
# LDAP Configuration new
# -----------------------------
LDAP_SERVER=ldap://host.docker.internal
LDAP_PORT=389
LDAP_USE_SSL=False
LDAP_DOMAIN=lcepl.org
LDAP_BASE_DN=DC=lcepl,DC=org
LDAP_SEARCH_BASE=OU=Users,DC=lcepl,DC=org

5
.gitignore vendored
View File

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

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
**Backend Framework**: Flask
**Frontend Framework**: HTML, CSS, Js, Bootstrap
**Backend Framework**: Python Flask
**Database**: SQL Database (MySQL/PostgreSQL/SQLite configured via environment variables)
**ORM**: SQLAlchemy
**File Processing**: Pandas, OpenPyXL, XlsxWriter
@@ -54,6 +55,8 @@ Werkzeug
python-dotenv
cryptography
xlsxwriter
matplotlib
```
---
@@ -416,20 +419,31 @@ pip install -r requirements.txt
### 2. Configure Environment Variables
Create a `.env` file in the project root:
```env
# Flask Configuration
FLASK_HOST=127.0.0.1
FLASK_PORT=5000
# -----------------------------
# Flask App Configuration
# -----------------------------
FLASK_ENV=development
FLASK_DEBUG=True
SECRET_KEY=your-secret-key-here
FLASK_HOST='0.0.0.0'
FLASK_PORT=5001
# -----------------------------
# Security
# -----------------------------
SECRET_KEY=change-this-to-strong-secret-key
# -----------------------------
# Database Configuration
DB_DIALECT=mysql # or postgresql, sqlite
DB_DRIVER=pymysql # or psycopg2, etc.
# -----------------------------
DB_DIALECT=mysql
DB_DRIVER=pymysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=comparisondb
DB_USER=root
DB_PASSWORD=your_password
DB_HOST=localhost
DB_PORT=3306
DB_NAME=comparison_project
```
### 3. Initialize Database
@@ -567,4 +581,4 @@ Open browser: `http://127.0.0.1:5000/`
For issues, feature requests, or contributions, please contact the development team.
**Last Updated:** January 2026
**Last Updated:** April 2026

View File

@@ -1,37 +1,7 @@
# from flask import Flask
# from app.config import Config
# from app.services.db_service import db
# def create_app():
# app = Flask(__name__)
# app.config.from_object(Config)
# db.init_app(app)
# from app.routes.auth import auth_bp
# from app.routes.user import user_bp
# from app.routes.dashboard import dashboard_bp
# from app.routes.subcontractor_routes import subcontractor_bp
# from app.routes.file_import import file_import_bp
# from app.routes.file_report import file_report_bp
# from app.routes.generate_comparison_report import generate_report_bp
# from app.routes.file_format import file_format
# app.register_blueprint(auth_bp)
# app.register_blueprint(user_bp)
# app.register_blueprint(dashboard_bp)
# app.register_blueprint(subcontractor_bp)
# app.register_blueprint(file_import_bp)
# app.register_blueprint(file_report_bp)
# app.register_blueprint(generate_report_bp)
# app.register_blueprint(file_format)
# return app
from flask import Flask, redirect, url_for
from app.config import Config
from app.services.db_service import db
from app.services.logger_service import LoggerService
def create_app():
app = Flask(__name__)
@@ -40,6 +10,9 @@ def create_app():
# Initialize extensions
db.init_app(app)
# Initialize Logger
LoggerService.init_app(app)
# Register blueprints
register_blueprints(app)
# Register error handlers
@@ -55,7 +28,7 @@ def create_app():
def register_blueprints(app):
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.subcontractor_routes import subcontractor_bp
from app.routes.file_import import file_import_bp
@@ -63,6 +36,10 @@ def register_blueprints(app):
from app.routes.generate_comparison_report import generate_report_bp
from app.routes.file_format import file_format_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(dashboard_bp)
@@ -70,14 +47,23 @@ def register_blueprints(app):
app.register_blueprint(file_import_bp)
app.register_blueprint(file_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):
from flask import current_app
@app.errorhandler(404)
def page_not_found(e):
current_app.logger.warning("404 Page Not Found")
return "Page Not Found", 404
@app.errorhandler(500)
def internal_error(e):
current_app.logger.exception("500 Internal Server Error")
return "Internal Server Error", 500

View File

@@ -1,12 +1,10 @@
import os
# project base url
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
class Config:
# secret key
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key")
# Database varibles
# Database variables
DB_DIALECT = os.getenv("DB_DIALECT")
DB_DRIVER = os.getenv("DB_DRIVER")
DB_USER = os.getenv("DB_USER")
@@ -23,7 +21,14 @@ class Config:
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
# uploads folder path
UPLOAD_FOLDER = os.path.join(BASE_DIR, "static", "uploads")
# file extension
ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"}
# LDAP Configuration New
LDAP_SERVER = os.getenv("LDAP_SERVER")
LDAP_PORT = int(os.getenv("LDAP_PORT", 389))
LDAP_USE_SSL = os.getenv("LDAP_USE_SSL", "False").lower() == "true"
LDAP_BASE_DN = os.getenv("LDAP_BASE_DN")
LDAP_DOMAIN = os.getenv("LDAP_DOMAIN")
LDAP_SEARCH_BASE = os.getenv("LDAP_SEARCH_BASE")

View File

@@ -0,0 +1,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

@@ -0,0 +1,74 @@
from app import db
from datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class LayingClient(db.Model):
__tablename__ = "laying_client"
id = db.Column(db.Integer, primary_key=True)
# Basic Fields
RA_Bill_No = db.Column(db.String(500))
Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100))
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 LAYING Qty.
Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_700_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_900_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
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.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):
return f"<LayingModel {self.Location}>"
def serialize(self):
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

@@ -0,0 +1,61 @@
from app import db
from datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class Laying(db.Model):
__tablename__ = "laying"
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")
# Pipe Laying Fields
RA_Bill_No=db.Column(db.String(500))
Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100))
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"))
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.Numeric(10, 2), default=Decimal("0.00"))
pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_700_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_900_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
pipe_1200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
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):
return f"<LayingModel {self.Location}>"
def serialize(self):
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 datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class ManholeDomesticChamber(db.Model):
__tablename__ = "manhole_domestic_chamber"
@@ -11,41 +14,57 @@ class ManholeDomesticChamber(db.Model):
subcontractor = db.relationship("Subcontractor", backref="manhole_domestic_chamber_records")
# Basic Fields
RA_Bill_No=db.Column(db.String(500))
Location = db.Column(db.String(500))
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_0_to_0_75 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_0_76_to_1_05 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_1_06_to_1_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_1_66_to_2_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_2_16_to_2_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_2_66_to_3_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
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"))
d_5_66_to_6_15 = db.Column(db.Float)
d_6_16_to_6_65 = db.Column(db.Float)
d_6_66_to_7_15 = db.Column(db.Float)
d_7_16_to_7_65 = db.Column(db.Float)
d_7_66_to_8_15 = db.Column(db.Float)
d_8_16_to_8_65 = db.Column(db.Float)
d_8_66_to_9_15 = db.Column(db.Float)
d_9_16_to_9_65 = db.Column(db.Float)
Domestic_Chambers = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
DWC_Pipe_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
UPVC_Pipe_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Domestic_Chambers = db.Column(db.Float)
DWC_Pipe_Length = db.Column(db.Float)
UPVC_Pipe_Length = db.Column(db.Float)
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):
return f"<HanholeDomesticChamberConstruction {self.Location}>"
def serialize(self):
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 datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class ManholeExcavation(db.Model):
__tablename__ = "manhole_excavation"
@@ -11,57 +14,70 @@ class ManholeExcavation(db.Model):
subcontractor = db.relationship("Subcontractor", backref="manhole_records")
# Basic Fields
RA_Bill_No=db.Column(db.String(500))
Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100))
Upto_IL_Depth = db.Column(db.Float)
Cutting_Depth = db.Column(db.Float)
ID_of_MH_m = db.Column(db.Float)
Ex_Dia_of_Manhole = db.Column(db.Float)
Area_of_Manhole = db.Column(db.Float)
Upto_IL_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Ex_Dia_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Area_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories
Soft_Murum_0_to_1_5 = db.Column(db.Float)
Soft_Murum_1_5_to_3_0 = db.Column(db.Float)
Soft_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0 = 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.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5 = db.Column(db.Float)
Soft_Rock_1_5_to_3_0 = 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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5 = db.Column(db.Float)
Hard_Rock_1_5_to_3_0 = db.Column(db.Float)
Hard_Rock_3_0_to_4_5 = db.Column(db.Float)
Hard_Rock_4_5_to_6_0 = db.Column(db.Float)
Hard_Rock_6_0_to_7_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.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Totals
Soft_Murum_0_to_1_5_total = db.Column(db.Float)
Soft_Murum_1_5_to_3_0_total = db.Column(db.Float)
Soft_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_and_above_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.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5_total = db.Column(db.Float)
Soft_Rock_1_5_and_above_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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5_total = db.Column(db.Float)
Hard_Rock_1_5_to_3_0_total = db.Column(db.Float)
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float)
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float)
Hard_Rock_6_0_to_7_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.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Total = db.Column(db.Float)
Remarks = db.Column(db.String(500))
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):
return f"<HanholeExcavation {self.Location}>"
def serialize(self):
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 datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class ManholeDomesticChamberClient(db.Model):
__tablename__ = "mh_dc_client"
id = db.Column(db.Integer, primary_key=True)
# Foreign Key to Subcontractor table
# subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False)
# Relationship for easy access (subcontractor.subcontractor_name)
# subcontractor = db.relationship("Subcontractor", backref="mh_dc_records")
# Basic Fields
RA_Bill_No=db.Column(db.String(500))
RA_Bill_No = db.Column(db.String(500))
Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100))
MH_TOP_LEVEL = db.Column(db.Float)
MH_IL_LEVEL = db.Column(db.Float)
Depth_of_MH = db.Column(db.Float)
MH_TOP_LEVEL = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
MH_IL_LEVEL = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Depth_of_MH = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories
d_0_to_1_5 = db.Column(db.Float)
d_1_5_to_2_0 = db.Column(db.Float)
d_2_0_to_2_5 = db.Column(db.Float)
d_2_5_to_3_0 = 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.Numeric(10, 2), default=Decimal("0.00"))
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.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)
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)
Domestic_Chambers = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
d_5_0_to_5_5 = db.Column(db.Float)
d_5_5_to_6_0 = db.Column(db.Float)
d_6_0_to_6_5 = db.Column(db.Float)
Domestic_Chambers = db.Column(db.Float)
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):
return f"<HanholeDomesticChamberConstruction {self.Location}>"
@@ -44,3 +42,17 @@ class ManholeDomesticChamberClient(db.Model):
def serialize(self):
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 datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class ManholeExcavationClient(db.Model):
__tablename__ = "mh_ex_client"
id = db.Column(db.Integer, primary_key=True)
# Foreign Key to Subcontractor table
# subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False)
# Relationship for easy access (subcontractor.subcontractor_name)
# subcontractor = db.relationship("Subcontractor", backref="mh_ex_records")
# Basic Fields
RA_Bill_No=db.Column(db.String(500))
RA_Bill_No = db.Column(db.String(500))
Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100))
Ground_Level = db.Column(db.Float)
MH_Invert_Level = db.Column(db.Float)
MH_Top_Level = db.Column(db.Float)
Ex_Level = db.Column(db.Float)
Cutting_Depth = db.Column(db.Float)
MH_Depth = db.Column(db.Float)
ID_of_MH_m = db.Column(db.Float)
Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
MH_Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
MH_Top_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Ex_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
MH_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Dia_of_MH_Cutting = db.Column(db.Float)
Area_of_Manhole = db.Column(db.Float)
Dia_of_MH_Cutting = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Area_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories
Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Float)
Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Float)
Marshi_Muddy_Slushy_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0 = db.Column(db.Float)
Soft_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0 = db.Column(db.Float)
Hard_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0 = db.Column(db.Float)
Soft_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0 = db.Column(db.Float)
Hard_Rock_3_0_to_4_5 = db.Column(db.Float)
Hard_Rock_4_5_to_6_0 = db.Column(db.Float)
Hard_Rock_6_0_to_7_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.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Totals
Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Float)
Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Float)
Marshi_Muddy_Slushy_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0_total = db.Column(db.Float)
Soft_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0_total = db.Column(db.Float)
Hard_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0_total = db.Column(db.Float)
Soft_Rock_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0_total = db.Column(db.Float)
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float)
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float)
Hard_Rock_6_0_to_7_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.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
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)
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):
return f"<HanholeExcavation {self.Location}>"
def serialize(self):
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 datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class TrenchExcavationClient(db.Model):
__tablename__ = "tr_ex_client"
id = db.Column(db.Integer, primary_key=True)
# Foreign Key to Subcontractor table
# subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False)
# Relationship for easy access (subcontractor.subcontractor_name)
# subcontractor = db.relationship("Subcontractor", backref="tr_ex_records")
# Basic Fields
RA_Bill_No=db.Column(db.String(500))
Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100))
CC_length = db.Column(db.Float)
Actual_Trench_Length = db.Column(db.Float)
Ground_Level = db.Column(db.Float)
Invert_Level = db.Column(db.Float)
Excavated_level = db.Column(db.Float)
Cutting_Depth = db.Column(db.Float)
Avg_Depth = db.Column(db.Float)
Pipe_Dia_mm = db.Column(db.Float)
CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Actual_Trench_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Excavated_level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Avg_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# width
Width_0_to_1_5_m = db.Column(db.Float)
Width_1_5_to_3_0_m = db.Column(db.Float)
Width_3_0_to_4_5_m = db.Column(db.Float)
Width_4_5_to_6_0_m = db.Column(db.Float)
Width_6_0_to_7_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.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
Width_6_0_to_7_5_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories
Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Float)
Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Float)
Marshi_Muddy_Slushy_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0 = db.Column(db.Float)
Soft_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0 = db.Column(db.Float)
Hard_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0 = db.Column(db.Float)
Soft_Rock_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0 = db.Column(db.Float)
Hard_Rock_3_0_to_4_5 = db.Column(db.Float)
Hard_Rock_4_5_to_6_0 = db.Column(db.Float)
Hard_Rock_6_0_to_7_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.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Totals
Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Float)
Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Float)
Marshi_Muddy_Slushy_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0_total = db.Column(db.Float)
Soft_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0_total = db.Column(db.Float)
Hard_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0_total = db.Column(db.Float)
Soft_Rock_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0_total = db.Column(db.Float)
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float)
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float)
Hard_Rock_6_0_to_7_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.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Total = db.Column(db.Float)
Remarks = 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):
return f"<TrenchExcavation {self.Location}>"
def serialize(self):
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 datetime import datetime
from sqlalchemy import event
from app.utils.regex_utils import RegularExpression
from decimal import Decimal
class TrenchExcavation(db.Model):
__tablename__ = "trench_excavation"
@@ -11,65 +14,64 @@ class TrenchExcavation(db.Model):
subcontractor = db.relationship("Subcontractor", backref="trench_records")
# Basic Fields
RA_Bill_No=db.Column(db.String(500))
Location = db.Column(db.String(500))
MH_NO = db.Column(db.String(100))
CC_length = db.Column(db.Float)
Invert_Level = db.Column(db.Float)
MH_Top_Level = db.Column(db.Float)
Ground_Level = db.Column(db.Float)
ID_of_MH_m = db.Column(db.Float)
Actual_Trench_Length = db.Column(db.Float)
Pipe_Dia_mm = db.Column(db.Float)
CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
MH_Top_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Actual_Trench_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# width
Width_0_to_2_5 = db.Column(db.Float)
Width_2_5_to_3_0 = db.Column(db.Float)
Width_3_0_to_4_5 = db.Column(db.Float)
Width_4_5_to_6_0 = 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.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
Upto_IL_Depth = db.Column(db.Float)
Cutting_Depth = db.Column(db.Float)
Avg_Depth = db.Column(db.Float)
Upto_IL_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Avg_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Excavation categories
Soft_Murum_0_to_1_5 = db.Column(db.Float)
Soft_Murum_1_5_to_3_0 = db.Column(db.Float)
Soft_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_to_3_0 = 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.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5 = db.Column(db.Float)
Soft_Rock_1_5_to_3_0 = 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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5 = db.Column(db.Float)
Hard_Rock_1_5_to_3_0 = db.Column(db.Float)
Hard_Rock_3_0_to_4_5 = db.Column(db.Float)
Hard_Rock_4_5_to_6_0 = db.Column(db.Float)
Hard_Rock_6_0_to_7_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.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
# Totals
Soft_Murum_0_to_1_5_total = db.Column(db.Float)
Soft_Murum_1_5_to_3_0_total = db.Column(db.Float)
Soft_Murum_3_0_to_4_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.Numeric(10, 2), default=Decimal("0.00"))
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_1_5_and_above_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.Numeric(10, 2), default=Decimal("0.00"))
Soft_Rock_0_to_1_5_total = db.Column(db.Float)
Soft_Rock_1_5_and_above_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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_0_to_1_5_total = db.Column(db.Float)
Hard_Rock_1_5_to_3_0_total = db.Column(db.Float)
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float)
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float)
Hard_Rock_6_0_to_7_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.Numeric(10, 2), default=Decimal("0.00"))
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.Numeric(10, 2), default=Decimal("0.00"))
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
Total = db.Column(db.Float)
Remarks = db.Column(db.String(500))
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):
@@ -77,3 +79,17 @@ class TrenchExcavation(db.Model):
def serialize(self):
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"
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)
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.constants.messages import SuccessMessage, ErrorMessage
from app.constants.http_status import HTTPStatus
auth_bp = Blueprint("auth", __name__)
# ==========================
# LOGIN
# ==========================
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if session.get("user_id"):
current_app.logger.info("User already logged in.")
return redirect(url_for("dashboard.dashboard"))
if request.method == "POST":
email = request.form.get("email")
password = request.form.get("password")
try:
email = request.form.get("email", "").strip()
password = request.form.get("password", "")
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
flash("Login successful", "success")
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"))
flash("Invalid email or password", "danger")
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")
# ==========================
# LOGOUT
# ==========================
@auth_bp.route("/logout")
def logout():
username = session.get("user_name", "Unknown")
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"))
# ==========================
# REGISTER
# ==========================
@auth_bp.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
name = request.form.get("name")
email = request.form.get("email")
password = request.form.get("password")
user = UserService.register_user(name, email, password)
if not user:
flash("Email already exists", "danger")
if request.method == "POST":
try:
name = request.form.get("name", "").strip()
email = request.form.get("email", "").strip()
password = request.form.get("password", "")
if not name or not email or not password:
flash(ErrorMessage.INVALID_REQUEST,"danger")
return redirect(url_for("auth.register"))
flash("User registered successfully", "success")
user = UserService.register_user(name, email, password)
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"))
return render_template("register.html", title="Register")
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,11 +1,614 @@
from flask import Blueprint, render_template, session, redirect, url_for
import matplotlib
matplotlib.use("Agg")
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request
import matplotlib.pyplot as plt
import io
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.route("/")
def dashboard():
if not session.get("user_id"):
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(
"subcontractor_dashboard.html",
subcontractors=subcontractors
)
# 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]
})
return render_template("dashboard.html", title="Dashboard")

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 app.utils.helpers import login_required
from app.utils.file_utils import get_download_format_folder
import os
file_format_bp = Blueprint("file_format", __name__)
@@ -14,10 +15,7 @@ def download_format():
@login_required
def download_excel_format(filename):
download_folder = os.path.join(
current_app.root_path, "static", "downloads/format"
)
download_folder = get_download_format_folder()
file_path = os.path.join(download_folder, filename)
if not os.path.exists(file_path):

View File

@@ -5,7 +5,8 @@ from app.utils.helpers import login_required
file_import_bp = Blueprint("file_import", __name__, url_prefix="/file")
@file_import_bp.route("/import", methods=["GET", "POST"])
# this is contractractor immport routes
@file_import_bp.route("/import_Subcontractor", methods=["GET", "POST"])
@login_required
def import_file():
subcontractors = Subcontractor.query.all()
@@ -21,13 +22,13 @@ def import_file():
flash(msg, "success" if success else "danger")
return render_template(
"file_import.html",
"file_import_subcontractor.html",
title="Sub-cont. File Import",
subcontractors=subcontractors
)
# this route import client files
# this is client import routes
@file_import_bp.route("/import_client", methods=["GET", "POST"])
@login_required
def client_import_file():

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,24 @@
from flask import Blueprint, render_template, request, send_file, flash
from collections import defaultdict
import pandas as pd
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.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.mh_ex_client_model import ManholeExcavationClient
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.utils.helpers import login_required
from app.models.laying_client_model import LayingClient
generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report")
@@ -53,85 +62,109 @@ def make_lookup(rows, key_field):
key_val = normalize_key(r.get(key_field))
if location and key_val:
lookup[(location, key_val)] = r
lookup.setdefault((location, key_val), []).append(r)
return lookup
# COMPARISON BUILDER
def build_comparison(client_rows, contractor_rows, key_field):
contractor_lookup = make_lookup(contractor_rows, key_field)
output = []
used_index = defaultdict(int) # 🔥 THIS FIXES YOUR ISSUE
for c in client_rows:
client_location = normalize_key(c.get("Location"))
client_key = normalize_key(c.get(key_field))
if not client_location or not client_key:
continue
s = contractor_lookup.get((client_location, client_key))
if not s:
subs = contractor_lookup.get((client_location, client_key))
if not subs:
continue
idx = used_index[(client_location, client_key)]
# ❗ If subcontractor rows are exhausted, skip
if idx >= len(subs):
continue
s = subs[idx] # ✅ take NEXT subcontractor row
used_index[(client_location, client_key)] += 1
# ---- totals ----
client_total = sum(
float(v or 0)
for k, v in c.items()
if k.endswith("_total")
or RegularExpression.D_RANGE_PATTERN.match(k)
or RegularExpression.PIPE_MM_PATTERN.match(k)
)
sub_total = sum(
float(v or 0)
for k, v in s.items()
if k.endswith("_total")
or RegularExpression.D_RANGE_PATTERN.match(k)
or RegularExpression.PIPE_MM_PATTERN.match(k)
)
diff = client_total - sub_total
row = {
"Location": client_location,
key_field.replace("_", " "): client_key
}
# CLIENT DATA
for k, v in c.items():
if k in ["id", "created_at"]:
continue
if k not in ["id", "created_at"]:
row[f"Client-{k}"] = v
row["Client-Total"] = round(client_total, 2)
row[" "] = ""
# SUBCONTRACTOR DATA
for k, v in s.items():
if k in ["id", "created_at", "subcontractor_id"]:
continue
if k not in ["id", "created_at", "subcontractor_id"]:
row[f"Subcontractor-{k}"] = v
row["Subcontractor-Total"] = round(sub_total, 2)
row["Diff"] = round(diff, 2)
row["Diff"] = round(client_total - sub_total, 2)
output.append(row)
df = pd.DataFrame(output)
# formatting headers
df.columns = [format_header(col) for col in df.columns]
return df
# EXCEL SHEET WRITER
def write_sheet(writer, df, sheet_name, subcontractor_name):
workbook = writer.book
# write dataframe (data already correct)
df.to_excel(writer, sheet_name=sheet_name, index=False, startrow=3)
ws = writer.sheets[sheet_name]
# formats
title_fmt = workbook.add_format({"bold": True, "font_size": 14})
client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#D9EDF7"})
sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7E1D9"})
total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#FFF2CC"})
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(
0, 0, 0, len(df.columns) - 1,
"CLIENT vs SUBCONTRACTOR",
@@ -143,6 +176,7 @@ def write_sheet(writer, df, sheet_name, subcontractor_name):
title_fmt
)
# header formatting
for col_num, col_name in enumerate(df.columns):
if col_name.startswith("Client-"):
ws.write(3, col_num, col_name, client_fmt)
@@ -191,6 +225,12 @@ def comparison_report():
).all()]
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 --------
output = io.BytesIO()
filename = f"{subcontractor.subcontractor_name}_Comparison_Report.xlsx"
@@ -199,6 +239,7 @@ def comparison_report():
write_sheet(writer, df_tr, "Tr.Ex", subcontractor.subcontractor_name)
write_sheet(writer, df_mh, "Mh.Ex", subcontractor.subcontractor_name)
write_sheet(writer, df_dc, "MH & DC", subcontractor.subcontractor_name)
write_sheet(writer, df_lay, "Laying", subcontractor.subcontractor_name)
output.seek(0)
return send_file(
@@ -210,4 +251,3 @@ def comparison_report():
return render_template("generate_comparison_report.html",subcontractors=subcontractors)

View File

@@ -1,62 +1,141 @@
from flask import Blueprint, render_template, request, redirect, flash
from app import db
from flask import Blueprint, render_template, request, redirect, flash, current_app, url_for
from app.services.db_service 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 -----------------
@subcontractor_bp.route("/add")
@login_required
def add_subcontractor():
current_app.logger.info("Opened Add Subcontractor Page")
return render_template("subcontractor/add.html")
# ---------------- SAVE -----------------
@subcontractor_bp.route("/save", methods=["POST"])
@login_required
def save_subcontractor():
name = request.form.get("subcontractor_name", "").strip()
if not name:
current_app.logger.warning("Empty subcontractor name submitted")
flash("Subcontractor name cannot be empty.", "danger")
return redirect(url_for("subcontractor.add_subcontractor"))
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
if existing_sub:
current_app.logger.warning(f"Duplicate subcontractor attempt: {name}")
flash(f"Subcontractor with name '{name}' already exists!", "danger")
return redirect(url_for("subcontractor.add_subcontractor"))
try:
subcontractor = Subcontractor(
subcontractor_name=request.form.get("subcontractor_name"),
subcontractor_name=name,
contact_person=request.form.get("contact_person"),
address=request.form.get("address"),
mobile_no=request.form.get("mobile_no"),
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.commit()
current_app.logger.info(f"Subcontractor Created Successfully: {name}")
flash("Subcontractor added successfully!", "success")
return redirect("/subcontractor/list")
# ---------------- LIST -----------------
except Exception:
db.session.rollback()
current_app.logger.exception("Error while saving subcontractor")
flash("An error occurred while saving.", "danger")
return redirect(url_for("subcontractor.subcontractor_list"))
# ---------------- LIST (UPDATED WITH PAGINATION) -----------------
@subcontractor_bp.route("/list")
@login_required
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 -----------------
@subcontractor_bp.route("/edit/<int:id>")
@login_required
def edit_subcontractor(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)
# ---------------- UPDATE -----------------
@subcontractor_bp.route("/update/<int:id>", methods=["POST"])
@login_required
def update_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
subcontractor.subcontractor_name = request.form.get("subcontractor_name")
subcontractor = Subcontractor.query.get_or_404(id)
new_name = request.form.get("subcontractor_name", "").strip()
duplicate = Subcontractor.query.filter(
Subcontractor.subcontractor_name == new_name,
Subcontractor.id != id
).first()
if duplicate:
current_app.logger.warning(f"Duplicate update attempt: {new_name}")
flash("Another subcontractor already uses this name.", "danger")
return redirect(url_for("subcontractor.edit_subcontractor", id=id))
try:
old_name = subcontractor.subcontractor_name
subcontractor.subcontractor_name = new_name
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")
return redirect("/subcontractor/list")
except Exception:
db.session.rollback()
current_app.logger.exception("Error updating subcontractor")
flash("Update failed!", "danger")
return redirect(url_for("subcontractor.subcontractor_list"))
# ---------------- DELETE -----------------
@subcontractor_bp.route("/delete/<int:id>")
@@ -64,8 +143,17 @@ def update_subcontractor(id):
def delete_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
try:
name = subcontractor.subcontractor_name
db.session.delete(subcontractor)
db.session.commit()
current_app.logger.info(f"Subcontractor Deleted: {name}")
flash("Subcontractor deleted successfully!", "success")
return redirect("/subcontractor/list")
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,27 +1,30 @@
from app import db
import os
import pandas as pd
from werkzeug.utils import secure_filename
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
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
from app.utils.file_utils import ensure_upload_folder
class FileService:
# ---------------- COMMON HELPERS ----------------
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):
if val is None or pd.isna(val):
return None
@@ -32,6 +35,7 @@ class FileService:
return val.upper()
# --------------- Sub-contractor service --------------
# ---------------- SUBCONTRACTOR FILE UPLOAD ----------------
def handle_file_upload(self, file, subcontractor_id, RA_Bill_No):
@@ -48,8 +52,9 @@ class FileService:
return False, "Invalid file type! Allowed: CSV, XLSX, XLS"
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)
filename = secure_filename(file.filename)
@@ -60,10 +65,12 @@ class FileService:
df_tr_ex = pd.read_excel(filepath, sheet_name="Tr.Ex.", header=12, dtype={"MH No": str})
df_mh_ex = pd.read_excel(filepath, sheet_name="MH Ex.", header=12, dtype={"MH No": str})
df_mh_dc = pd.read_excel(filepath, sheet_name="MH & DC", header=11, dtype={"MH No": str})
df_laying = pd.read_excel(filepath, sheet_name="Laying", header=11, dtype={"MH No": str})
self.process_trench_excavation(df_tr_ex, subcontractor_id, RA_Bill_No)
self.process_manhole_excavation(df_mh_ex, subcontractor_id, RA_Bill_No)
self.process_manhole_domestic_chamber(df_mh_dc, subcontractor_id, RA_Bill_No)
self.process_laying(df_laying, subcontractor_id, RA_Bill_No)
return True, "SUBCONTRACTOR File uploaded successfully."
@@ -96,18 +103,18 @@ class FileService:
if not location or not mh_no:
continue
exists = TrenchExcavation.query.filter_by(
subcontractor_id=subcontractor_id,
RA_Bill_No=RA_Bill_No,
Location=location,
MH_NO=mh_no,
).first()
# exists = TrenchExcavation.query.filter_by(
# subcontractor_id=subcontractor_id,
# RA_Bill_No=RA_Bill_No,
# Location=location,
# MH_NO=mh_no,
# ).first()
if exists:
errors.append(
f"Model-Tr.Ex. (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
)
continue
# if exists:
# errors.append(
# f"Model-Tr.Ex. (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
# )
# continue
record_data = {}
for col in df.columns:
@@ -154,18 +161,18 @@ class FileService:
if not location or not mh_no:
continue
exists = ManholeExcavation.query.filter_by(
subcontractor_id=subcontractor_id,
RA_Bill_No=RA_Bill_No,
Location=location,
MH_NO=mh_no,
).first()
# exists = ManholeExcavation.query.filter_by(
# subcontractor_id=subcontractor_id,
# RA_Bill_No=RA_Bill_No,
# Location=location,
# MH_NO=mh_no,
# ).first()
if exists:
errors.append(
f"Model-MH Ex. (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
)
continue
# if exists:
# errors.append(
# f"Model-MH Ex. (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
# )
# continue
record_data = {}
for col in df.columns:
@@ -212,18 +219,18 @@ class FileService:
if not location or not mh_no:
continue
exists = ManholeDomesticChamber.query.filter_by(
subcontractor_id=subcontractor_id,
RA_Bill_No=RA_Bill_No,
Location=location,
MH_NO=mh_no,
).first()
# exists = ManholeDomesticChamber.query.filter_by(
# subcontractor_id=subcontractor_id,
# RA_Bill_No=RA_Bill_No,
# Location=location,
# MH_NO=mh_no,
# ).first()
if exists:
errors.append(
f"Model-MH & DC (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
)
continue
# if exists:
# errors.append(
# f"Model-MH & DC (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
# )
# continue
record_data = {}
for col in df.columns:
@@ -245,6 +252,65 @@ class FileService:
db.session.commit()
# ---------------- Laying (Subcontractor) ----------------
def process_laying(self, df, subcontractor_id, RA_Bill_No):
df.columns = (
df.columns.astype(str)
.str.strip()
.str.replace(r"[^\w]", "_", regex=True)
.str.replace("__+", "_", regex=True)
.str.strip("_")
)
df = df.dropna(how="all")
if "Location" in df.columns:
df["Location"] = df["Location"].ffill()
errors = []
for idx, row in df.iterrows():
location = self.normalize(row.get("Location"))
mh_no = self.normalize(row.get("MH_NO"))
if not location or not mh_no:
continue
# exists = ManholeDomesticChamber.query.filter_by(
# subcontractor_id=subcontractor_id,
# RA_Bill_No=RA_Bill_No,
# Location=location,
# MH_NO=mh_no,
# ).first()
# if exists:
# errors.append(
# f"Model-MH & DC (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
# )
# continue
record_data = {}
for col in df.columns:
if hasattr(Laying, col):
val = row[col]
if pd.isna(val) or str(val).strip() in ["", "-", "", "nan"]:
val = None
record_data[col] = val
record = Laying(
subcontractor_id=subcontractor_id,
RA_Bill_No=RA_Bill_No,
**record_data,
)
db.session.add(record)
if errors:
raise Exception(" | ".join(errors))
db.session.commit()
# --------------- Client service --------------
# ---------------- CLIENT FILE UPLOAD ----------------
def handle_client_file_upload(self, file, RA_Bill_No):
@@ -258,8 +324,9 @@ class FileService:
return False, "Invalid file type! Allowed: CSV, XLSX, XLS"
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)
filename = secure_filename(file.filename)
@@ -270,10 +337,12 @@ class FileService:
df_tr_ex = pd.read_excel(filepath, sheet_name="Tr.Ex.", header=4)
df_mh_ex = pd.read_excel(filepath, sheet_name="MH Ex.", header=4)
df_mh_dc = pd.read_excel(filepath, sheet_name="MH & DC", header=3)
df_lay = pd.read_excel(filepath, sheet_name="Laying & Bedding", header=3)
self.save_client_data(df_tr_ex, TrenchExcavationClient, RA_Bill_No)
self.save_client_data(df_mh_ex, ManholeExcavationClient, RA_Bill_No)
self.save_client_data(df_mh_dc, ManholeDomesticChamberClient, RA_Bill_No)
self.save_client_data(df_lay, LayingClient, RA_Bill_No)
db.session.commit()
return True, "Client file uploaded successfully."
@@ -304,3 +373,4 @@ class FileService:
record = model(RA_Bill_No=RA_Bill_No, **record_data)
db.session.add(record)

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.services.db_service import db
from flask import current_app
class UserService:
@@ -13,6 +14,7 @@ class UserService:
db.session.add(user)
db.session.commit()
current_app.logger.info("User list viewed")
return user
@staticmethod

View File

@@ -1,35 +0,0 @@
body {
background: #f5f7fa;
font-family: Arial;
padding: 40px;
}
form {
width: 420px;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
label {
margin-top: 10px;
display: block;
font-weight: bold;
}
input,
textarea {
width: 100%;
padding: 8px;
margin-top: 4px;
}
button {
margin-top: 20px;
padding: 10px 20px;
background: #0055ff;
color: #fff;
border: 0;
cursor: pointer;
}

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,25 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title if title else "Comparison Software" }}</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='images/lcepl.png') }}">
<!-- 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 -->
<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>
<body class="bg-light">
<!-- 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">
<div class="container-fluid">
<!-- Brand -->
@@ -40,48 +47,55 @@
</a>
</li>
<!-- Subcontractor -->
<!-- Subcontractor Model -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
<i class="bi bi-people-fill me-1"></i> Subcontractor
<i class="bi bi-people-fill me-1"></i> Subcontractor List
</a>
<ul class="dropdown-menu dropdown-menu-dark">
<li>
<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>
</li>
<li>
<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>
</li>
</ul>
</li>
<!-- File System -->
<!-- Subcontractor File System -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
<i class="bi bi-folder-fill me-1"></i> File System
<i class="bi bi-folder-fill me-1"></i>Subcontractor RA Bills
</a>
<ul class="dropdown-menu dropdown-menu-dark">
<li>
<a class="dropdown-item" href="/file/import">
<a class="dropdown-item" href="/dashboard/subcontractor_dashboard">
<i class="bi bi-speedometer2 me-2"></i> Subcontractor Dashboard
</a>
</li>
<li>
<a class="dropdown-item" href="/file/import_Subcontractor">
<i class="bi bi-upload me-2"></i> Import File
</a>
</li>
<li>
<a class="dropdown-item" href="/file/report">
<i class="bi bi-download me-2"></i> Download Reports
<a class="dropdown-item" href="/file/Subcontractor_report">
<i class="bi bi-arrow-left-right me-2"></i> Show Reports
</a>
</li>
</ul>
</li>
<!-- Client System -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
<i class="bi bi-building me-1"></i> Client System
<i class="bi bi-building me-1"></i> Client RA Bills
</a>
<ul class="dropdown-menu dropdown-menu-dark">
<li>
@@ -91,15 +105,30 @@
</li>
<li>
<a class="dropdown-item" href="/report/comparison_report">
<i class="bi bi-arrow-left-right me-2"></i> client vs sub-cont. Comparison Report
<a class="dropdown-item" href="/file/client_report">
<i class="bi bi-arrow-left-right me-2"></i> Show Report
</a>
</li>
</ul>
</li>
<!-- Reports -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
<i class="bi bi-building me-1"></i> Comparison Report
</a>
<ul class="dropdown-menu dropdown-menu-dark">
<li>
<a class="dropdown-item" href="/report/comparison_report">
<i class="bi bi-arrow-left-right me-2"></i> Client vs Subcontractor
</a>
</li>
<!-- <li>
<a class="dropdown-item" href="/file/client_vs_subcont">
<i class="bi bi-arrow-left-right me-2"></i> Comparison Report
</a>
</li>
</li> -->
</ul>
</li>
@@ -110,72 +139,278 @@
</a>
</li>
<!-- USER DROPDOWN -->
{% if session.get("user_id") %}
<li class="nav-item dropdown ms-lg-3">
<!-- Masters -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/engi">
<i class="bi bi-gear me-2"></i> Masters
</a>
<ul class="dropdown-menu dropdown-menu-dark">
<!-- Client Standard Rates -->
<li class="nav-item">
<a class="nav-link" href="/engi/subcontractor-rate">
<i class="bi bi-file-earmark-text me-1"></i> Rate Master
</a>
</li>
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" href="#"
data-bs-toggle="dropdown">
<i class="bi bi-person-circle fs-5"></i>
<span class="d-none d-lg-inline">
<!-- Client Standard Rates -->
<li class="nav-item">
<a class="nav-link" href="/engi/client-rate">
<i class="bi bi-file-earmark-text me-1"></i> Client Rates
</a>
</li>
</ul>
</li>
<!-- USER DROPDOWN -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center text-white"
href="#" id="profileDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle fs-4"></i>
<span class="ms-2 fw-semibold">
{{ session.get("user_name") }}
</span>
</a>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark shadow">
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark border-0 shadow-lg bg-dark p-0"
style="width:320px;">
<!-- User card -->
<li class="px-3 py-3 text-center border-bottom">
<i class="bi bi-person-circle fs-1"></i>
<div class="fw-semibold mt-1">
<!-- Profile Header -->
<li class="text-center py-4 border-bottom border-secondary">
<i class="bi bi-person-circle text-light" style="font-size:60px;"></i>
<h5 class="mt-2 mb-0 fw-bold">
{{ session.get("user_name") }}
</div>
<small class="text-muted">Logged in user</small>
</h5>
<small class="text-secondary">
{{ session.get("email") }}
</small>
</li>
<!-- Masters -->
<li>
<a class="dropdown-item" href="/dashboard">
<i class="bi bi-speedometer2 me-2"></i> Dashboard
<a class="dropdown-item text-light py-2" href="{{ url_for('engineering.engineering_master') }}">
<i class="bi bi-gear me-2"></i> Masters
</a>
</li>
<!-- Activity Log page -->
<li>
<a class="dropdown-item text-warning" href="/logout">
<i class="bi bi-box-arrow-right me-2"></i> Logout
<a class="dropdown-item text-light py-2" href="{{ url_for('activity.activity') }}">
<i class="bi bi-clock-history me-2"></i> Activity Log
</a>
</li>
<!-- Manage Account
<li>
<a class="dropdown-item py-2" href="#">
<i class="bi bi-gear me-2"></i> Manage Account
</a>
</li>
-->
<li><hr class="dropdown-divider border-secondary m-0"></li>
<!-- Logout Account -->
<li>
<a class="dropdown-item text-warning py-2" href="{{ url_for('auth.logout') }}">
<i class="bi bi-box-arrow-right me-2"></i>
Logout
</a>
</li>
<!-- Footer -->
<li class="text-center py-3 bg-secondary bg-opacity-10 border-top border-secondary">
<small class="text-light">
<i class="bi bi-shield-check text-success"></i>
Secured by <strong>LCEPL</strong>
</small>
</li>
</ul>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- PAGE CONTENT -->
<div class="container-fluid vh-100 pt-5 overflow-hidden">
<!-- 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) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show">
{{ message }}
<button class="btn-close" data-bs-dismiss="alert"></button>
<div class="alert alert-{{ category }} alert-dismissible fade show shadow notification-alert mb-2"
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>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!-- PAGE CONTENT -->
<div class="overflow-auto h-100">
<div class="container mt-4">
{% block content %}{% endblock %}
</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 -->
<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>
</html>

View File

@@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid mt-4">
<h2 class="mb-4">Client RA Bills Reports</h2>
<div class="card p-4 shadow-sm mb-5">
<form method="POST" class="loading-form">
<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>
<div class="row">
<div class="col-md-6">
<button type="submit" name="action" value="preview" class="btn btn-secondary w-100">Preview
Data</button>
</div>
<div class="col-md-6">
<button type="submit" name="action" value="download" class="btn btn-primary w-100">Download Excel Report</button>
</div>
</div>
</form>
</div>
{% if tables.tr or tables.mh or tables.dc or tables.laying %}
<div class="card shadow-sm p-4">
<h4 class="mb-3">Table Preview</h4>
<ul class="nav nav-tabs" id="reportTabs" role="tablist">
<li class="nav-item">
<button class="nav-link active" id="tr-tab" data-bs-toggle="tab" data-bs-target="#tr"
type="button">Tr.Ex </button>
</li>
<li class="nav-item">
<button class="nav-link" id="mh-tab" data-bs-toggle="tab" data-bs-target="#mh" type="button">Mh.Ex
</button>
</li>
<li class="nav-item">
<button class="nav-link" id="dc-tab" data-bs-toggle="tab" data-bs-target="#dc" type="button">MH & DC
</button>
</li>
<li class="nav-item">
<button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying" type="button">Laying
& Bedding </button>
</li>
</ul>
<div class="tab-content mt-3" id="reportTabsContent">
<div class="tab-pane fade show active" id="tr" role="tabpanel">
<div class="table-responsive" style="max-height: 500px;">
{{ tables.tr|safe }}
</div>
</div>
<div class="tab-pane fade" id="mh" role="tabpanel">
<div class="table-responsive" style="max-height: 500px;">
{{ tables.mh|safe }}
</div>
</div>
<div class="tab-pane fade" id="dc" role="tabpanel">
<div class="table-responsive" style="max-height: 500px;">
{{ tables.dc|safe }}
</div>
</div>
<div class="tab-pane fade" id="laying" role="tabpanel">
<div class="table-responsive" style="max-height: 500px;">
{{ tables.laying|safe }}
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -1,10 +1,118 @@
{% extends "base.html" %}
{% block content %}
<h2 class="mb-4">Dashboard</h2>
<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>
<div class="card p-4 shadow-sm">
<h5>Welcome to Comparison Project</h5>
<p>This is dashboard panel.</p>
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<div class="card text-white bg-primary shadow h-100">
<div class="card-body text-center text-md-start">
<h6>Trenching Units</h6>
<h3 class="fw-bold" id="card-trench">0</h3>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="card text-white bg-success shadow h-100">
<div class="card-body text-center text-md-start">
<h6>Manhole Units</h6>
<h3 class="fw-bold" id="card-manhole">0</h3>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="card text-dark bg-warning shadow h-100">
<div class="card-body text-center text-md-start">
<h6>Laying Units</h6>
<h3 class="fw-bold" id="card-laying">0</h3>
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-dark text-white">Live Category Bar Chart</div>
<div class="card-body">
<canvas id="liveBarChart" style="max-height:300px;"></canvas>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-dark text-white">Location Distribution Pie Chart</div>
<div class="card-body">
<canvas id="livePieChart" style="max-height:300px;"></canvas>
</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 %}

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,32 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h2 class="mb-4">Sub-Contractor File Import</h2>
<div class="card p-4 shadow-sm">
<form method="POST" enctype="multipart/form-data">
<!-- 1. SELECT SUBCONTRACTOR -->
<label class="form-label">Select Subcontractor</label>
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
<option value="">-- Select Subcontractor --</option>
{% for sc in subcontractors %}
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
{% endfor %}
</select>
<!-- 2. RA bill no -->
<label class="form-label">RA Bill No</label>
<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>
</form>
</div>
{% endblock %}

View File

@@ -1,41 +1,145 @@
{% extends "base.html" %}
{% 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 -->
<!-- <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>
<div class="col-12">
{% for sc in subcontractors %}
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
{% endfor %}
</select> -->
<div class="card border-0 shadow-sm bg-success text-white">
<!-- 2. FILE TYPE (MODEL NAME) -->
<!-- <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> -->
<div class="card-body d-flex justify-content-between align-items-center">
<label class="form-label">RA Bill No</label>
<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>
<div>
<h3 class="fw-bold mb-1">
<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>
<button class="btn btn-primary w-100">Upload</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,125 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid py-4">
<!-- 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">
<div>
<h3 class="mb-1 fw-bold">
<i class="bi bi-cloud-arrow-up-fill me-2"></i>
Sub-Contractor File Import
</h3>
<p class="mb-0 opacity-75">
Upload RA Bill Excel file for the selected subcontractor.
</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-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>
{% endblock %}

View File

@@ -1,43 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2 class="mb-4">File 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">
<form method="POST">
<!-- SELECT SUBCONTRACTOR -->
<!-- <label class="form-label fw-semibold">Select Subcontractor</label>
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
<option value="">-- Select Subcontractor --</option>
{% for sc in subcontractors %}
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
{% endfor %}
</select> -->
<label class="form-label">RA Bill No</label>
<input type="text" name="RA_Bill_No" class="form-control mb-3" required>
<button class="btn btn-primary w-100">Generate Report</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -5,22 +5,9 @@
<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">
<form method="POST">
<!-- SELECT SUBCONTRACTOR -->
<label class="form-label fw-semibold">Select Subcontractor</label>
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>

View File

@@ -8,75 +8,200 @@
<!-- 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-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container-fluid vh-100">
<div class="row h-100 justify-content-center align-items-center">
<!-- Flash Messages -->
<div class="position-fixed top-0 end-0 p-3"
style="z-index:1080; width:min(95vw,420px);">
<!-- Increased column width -->
<div class="col-12 col-sm-10 col-md-8 col-lg-5 col-xl-4">
<div class="card shadow-lg border-0">
<!-- Increased padding -->
<div class="card-body p-5">
<!-- Branding -->
<div class="text-center mb-4">
<img src="{{ url_for('static', filename='images/lcepl.png') }}" alt="LCEPL Logo"
class="img-fluid mb-3" style="max-height:80px;">
<h4 class="fw-bold mb-1">
Laxmi Civil Engineering Services Pvt Ltd
</h4>
<p class="text-muted mb-0">
Data Comparison System
</p>
</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 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 %}
<!-- Login Form -->
</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-body p-5">
<div class="text-center mb-4">
<img src="{{ url_for('static', filename='images/lcepl.png') }}"
class="img-fluid mb-3"
style="max-height:100px;">
<h3 class="fw-bold text-success">Laxmi Civil Engineering Services Pvt Ltd</h3>
<p class="text-muted">Data Comparison Software (UGD)</p>
</div>
<form method="POST">
<div class="mb-4">
<label class="form-label fw-semibold">User Name</label>
<input type="email" name="email" class="form-control " placeholder="Enter email"
required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Username</label>
<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">
<label class="form-label fw-semibold">Password</label>
<input type="password" name="password" class="form-control" placeholder="Enter password"
<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>
<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
</button>
<div class="text-center mt-3">
<a href="#">Forgot Password?</a>
</div>
</form>
</div>
</div>
<div class="text-center mt-3">
<small class="text-muted">
© 2026 Laxmi Civil Engineering Services Pvt Ltd
</small>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<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>
</html>

View File

@@ -1,40 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2 class="mb-4">Generate Subcontractor Report</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">
<form method="POST">
<div class="mb-3">
<label class="form-label fw-semibold">Select Subcontractor</label>
<select name="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>
<div class="mb-3">
<label class="form-label fw-semibold">RA Bill Number</label>
<input type="text" name="ra_bill_no" class="form-control" placeholder="e.g. 01" required>
</div>
<button type="submit" class="btn btn-primary w-100">Generate Excel Report</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -1,40 +1,170 @@
{% extends "base.html" %}
{% block content %}
<div class="card shadow-sm p-4">
<div class="container-fluid">
<h4 class="mb-3">Add New Subcontractor</h4>
<form action="/subcontractor/save" method="POST">
<div class="mb-3">
<label class="form-label">Subcontractor Name</label>
<input type="text" class="form-control" name="subcontractor_name" required>
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h3 class="fw-bold mb-1">
<i class="bi bi-person-plus-fill text-success me-2"></i>
Add New Subcontractor
</h3>
<p class="text-muted mb-0">
Enter the subcontractor details below.
</p>
</div>
<div class="mb-3">
<label class="form-label">Contact Person</label>
<input type="text" class="form-control" name="contact_person">
<a href="{{ url_for('subcontractor.subcontractor_list') }}"
class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>
Back to List
</a>
</div>
<div class="mb-3">
<label class="form-label">Mobile</label>
<input type="text" class="form-control" name="mobile_no">
<div class="card shadow border-0">
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="bi bi-building-fill-add me-2"></i>
Subcontractor Information
</h5>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="email_id">
<div class="card-body">
<form action="{{ url_for('subcontractor.save_subcontractor') }}"
method="POST"
class="loading-form">
<div class="row g-4">
<!-- Subcontractor Name -->
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="bi bi-building me-1"></i>
Subcontractor Name
<span class="text-danger">*</span>
</label>
<input type="text"
class="form-control"
name="subcontractor_name"
placeholder="Enter Subcontractor Name"
required>
</div>
<div class="mb-3">
<label class="form-label">GST No</label>
<input type="text" class="form-control" name="gst_no">
<!-- 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>
<button class="btn btn-success">Save</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -4,36 +4,58 @@
<div class="card shadow-sm p-4">
<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">
<label class="form-label">Subcontractor Name</label>
<label class="form-label">Subcontractor Name:</label>
<input type="text" class="form-control" name="subcontractor_name"
value="{{ subcontractor.subcontractor_name }}" required>
</div>
<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 }}">
</div>
<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 }}">
</div>
<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 }}">
</div>
<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 }}">
</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>
<a href="/subcontractor/list" class="btn btn-secondary">Back</a>
<a href="{{ url_for('subcontractor.subcontractor_list') }}" class="btn btn-secondary">Back</a>
</form>
</div>

View File

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

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

@@ -0,0 +1,630 @@
{% 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">
<div class="d-flex justify-content-between align-items-center">
<div>
<h2 class="fw-bold text-primary mb-1">
<i class="bi bi-file-earmark-bar-graph"></i>
Subcontractor Report
</h2>
<small class="text-muted">
View, Filter, Edit and Delete Subcontractor Records
</small>
</div>
</div>
</div>
</div>
<!-- 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>
{% if tables %}
{% set show_all = (not selected_category) or selected_category == 'all' %}
<!-- Tabs -->
<div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-light">
<ul class="nav nav-pills">
<li class="nav-item">
<button class="nav-link {% if show_all %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#abstract">
<i class="bi bi-file-earmark-text"></i> Abstract
</button>
</li>
{% if show_all or selected_category == 'tr' %}
<li class="nav-item">
<button class="nav-link {% if selected_category == 'tr' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#tr">
<i class="bi bi-cone-striped"></i> Trench Excavation
</button>
</li>
{% endif %}
{% if show_all or selected_category == 'mh' %}
<li class="nav-item">
<button class="nav-link {% if selected_category == 'mh' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#mh">
<i class="bi bi-nut"></i> Manhole Excavation
</button>
</li>
{% endif %}
{% if show_all or selected_category == 'dc' %}
<li class="nav-item">
<button class="nav-link {% if selected_category == 'dc' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#dc">
<i class="bi bi-grid-3x3"></i> Manhole & Domestic Chambers Construction
</button>
</li>
{% endif %}
{% if show_all or selected_category == 'laying' %}
<li class="nav-item">
<button class="nav-link {% if selected_category == 'laying' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#laying">
<i class="bi bi-bezier2"></i> Pipe Laying
</button>
</li>
{% endif %}
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade {% if show_all %}show active{% endif %}" id="abstract">
{{ abstract_html|safe }}
</div>
{% if show_all or selected_category == 'tr' %}
<!-- Trench -->
<div class="tab-pane fade {% if selected_category == 'tr' %}show active{% endif %}" id="tr">
{% if has_data.tr %}
<div class="mb-3 d-flex gap-2 flex-wrap">
<button onclick="deleteSelected('tr')" class="btn btn-danger">
<i class="bi bi-trash"></i> Delete Selected
</button>
<button type="button" class="btn btn-outline-primary bulk-edit-toggle-btn" data-model="tr">
<i class="bi bi-pencil-square"></i> Bulk Edit
</button>
<button type="button" class="btn btn-outline-secondary bulk-edit-cancel-btn d-none" data-model="tr">
Cancel
</button>
</div>
{% endif %}
<div class="table-responsive border rounded shadow-sm">
{{ tables.tr|safe }}
</div>
</div>
{% endif %}
{% if show_all or selected_category == 'mh' %}
<!-- MH -->
<div class="tab-pane fade {% if selected_category == 'mh' %}show active{% endif %}" id="mh">
{% if has_data.mh %}
<div class="mb-3 d-flex gap-2 flex-wrap">
<button onclick="deleteSelected('mh')" class="btn btn-danger">
<i class="bi bi-trash"></i>Delete Selected
</button>
<button type="button" class="btn btn-outline-primary bulk-edit-toggle-btn" data-model="mh">
<i class="bi bi-pencil-square"></i> Bulk Edit
</button>
<button type="button" class="btn btn-outline-secondary bulk-edit-cancel-btn d-none" data-model="mh">
Cancel
</button>
</div>
{% endif %}
<div class="table-responsive border rounded shadow-sm">
{{ tables.mh|safe }}
</div>
</div>
{% endif %}
{% if show_all or selected_category == 'dc' %}
<!-- DC -->
<div class="tab-pane fade {% 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>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
<script>
// NOTE: this content block renders before base.html's bottom-of-body
// <script src="jquery..."> tag, so $ is not defined yet if this runs
// immediately - that was silently breaking the location cascade
// (and DataTable init) below. Deferring to DOMContentLoaded (native
// JS, no jQuery needed to register it) guarantees jQuery has loaded
// by the time this code actually runs.
document.addEventListener("DOMContentLoaded", function () {
// Remember which tab is active so that actions which reload the
// page (Delete Selected, bulk delete, bulk-edit Save) bring you
// back to the tab you were working on instead of resetting to
// the Abstract tab (the default "active" one in the markup).
const TAB_STORAGE_KEY = "subReportActiveTab";
$(document).on("shown.bs.tab", '[data-bs-toggle="tab"]', function (e) {
let target = $(e.target).attr("data-bs-target"); // e.g. "#tr"
if (target) sessionStorage.setItem(TAB_STORAGE_KEY, target);
});
(function restoreActiveTab() {
let target = sessionStorage.getItem(TAB_STORAGE_KEY);
if (!target) return;
let btn = document.querySelector(`[data-bs-toggle="tab"][data-bs-target="${target}"]`);
if (btn) {
bootstrap.Tab.getOrCreateInstance(btn).show();
}
})();
// Reset
document.getElementById("resetBtn").addEventListener("click", function () {
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);
});
}
</script>
{% 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
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():
if not os.path.exists(Config.UPLOAD_FOLDER):
os.makedirs(Config.UPLOAD_FOLDER)
if not os.path.exists(get_uploads_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.
* Running on http://127.0.0.1:5000
2025-12-09 13:11:05,607 | INFO | Press CTRL+C to quit
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
2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | ======================================================================
2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | Application Started Successfully
2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | ======================================================================

View File

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