613 lines
26 KiB
HTML
613 lines
26 KiB
HTML
{% 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 %}
|
|
<!-- Tabs -->
|
|
<div class="card shadow-sm border-0 mt-4">
|
|
<div class="card-header bg-light">
|
|
<ul class="nav nav-pills">
|
|
<li class="nav-item">
|
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#abstract">
|
|
<i class="bi bi-file-earmark-text"></i> Abstract
|
|
</button>
|
|
</li>
|
|
|
|
<li class="nav-item">
|
|
<button class="nav-link " data-bs-toggle="tab" data-bs-target="#tr">
|
|
<i class="bi bi-cone-striped"></i> Trench Excavation
|
|
</button>
|
|
</li>
|
|
|
|
<li class="nav-item">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#mh">
|
|
<i class="bi bi-nut"></i> Manhole Excavation
|
|
</button>
|
|
</li>
|
|
|
|
<li class="nav-item">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#dc">
|
|
<i class="bi bi-grid-3x3"></i> Manhole & Domestic Chambers Construction
|
|
</button>
|
|
</li>
|
|
|
|
<li class="nav-item">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#laying">
|
|
<i class="bi bi-bezier2"></i> Pipe Laying
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="tab-content">
|
|
|
|
<div class="tab-pane fade show active" id="abstract">
|
|
{{ abstract_html|safe }}
|
|
</div>
|
|
|
|
<!-- Trench -->
|
|
<div class="tab-pane fade "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>
|
|
|
|
<!-- MH -->
|
|
<div class="tab-pane fade" 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>
|
|
|
|
<!-- DC -->
|
|
<div class="tab-pane fade" 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>
|
|
|
|
<!-- Laying -->
|
|
<div class="tab-pane fade" id="laying">
|
|
{% if has_data.laying %}
|
|
<div class="mb-3 d-flex gap-2 flex-wrap">
|
|
<button onclick="deleteSelected('laying')"
|
|
class="btn btn-danger">
|
|
<i class="bi bi-trash"></i>
|
|
Delete Selected
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary bulk-edit-toggle-btn" data-model="laying">
|
|
<i class="bi bi-pencil-square"></i> Bulk Edit
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary bulk-edit-cancel-btn d-none" data-model="laying">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
<div class="table-responsive border rounded shadow-sm">
|
|
{{ tables.laying|safe }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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 now rendered server-side with
|
|
// class="select-all-checkbox" and data-model="tr|mh|dc|laying"
|
|
// (see add_select_all_header in file_report.py), and each row
|
|
// checkbox carries the same data-model. Scoping by data-model
|
|
// means checking "select all" on the Trench Excavation tab only
|
|
// toggles Trench Excavation rows, not the other 3 tables.
|
|
$(document).on("change", ".select-all-checkbox", function () {
|
|
let model = $(this).data("model");
|
|
$(`.row-check[data-model="${model}"]`).prop("checked", this.checked);
|
|
});
|
|
|
|
// 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 %} |