Browse Source

added flagpole service,controller and page

master
yoganandhan 8 months ago
parent
commit
05ace9ef9b
  1. 22
      src/main/java/com/gcc/commissionerreview/configuration/DataSourceConfig.java
  2. 72
      src/main/java/com/gcc/commissionerreview/controller/FlagpolesApiController.java
  3. 6
      src/main/java/com/gcc/commissionerreview/controller/MainController.java
  4. 199
      src/main/java/com/gcc/commissionerreview/service/FlagpolesService.java
  5. 416
      src/main/resources/templates/modules/flagpoles/abstractreport.html

22
src/main/java/com/gcc/commissionerreview/configuration/DataSourceConfig.java

@ -107,4 +107,26 @@ public class DataSourceConfig {
return dataSource; return dataSource;
} }
////////////////////////////// vph ////////////////////////
@Bean(name = "mysqlVphDataSource")
public DataSource mysqlVphDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://" + host + "/victoriahallbooking");
dataSource.setUsername("root");
dataSource.setPassword(dbpassword);
return dataSource;
}
//////////////////////////// (For GCC flagpoles) ////////////////////////
@Bean(name = "mysqlFlagPoleManagerSystemDataSource")
public DataSource mysqlFlagPoleManagerSystemDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://" + host + "/gcc_flag_pole");
dataSource.setUsername("root");
dataSource.setPassword(dbpassword);
return dataSource;
}
} }

72
src/main/java/com/gcc/commissionerreview/controller/FlagpolesApiController.java

@ -0,0 +1,72 @@
package com.gcc.commissionerreview.controller;
import java.util.Map;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import com.gcc.commissionerreview.service.FlagpolesService;
import java.util.Map;
import java.util.List;
@RestController
@RequestMapping("/commissionerreview/api")
public class FlagpolesApiController {
@Autowired
private FlagpolesService flagpolesService;
@GetMapping("/flagpoles/abstract")
public Map<String, Object> getAbstractData(
@RequestParam(required = false) String fromDate,
@RequestParam(required = false) String toDate,
@RequestParam(required = false) String applicationType) {
try {
List<Map<String, Object>> data = flagpolesService.getZoneWiseRequestCounts(fromDate, toDate,
applicationType);
// Only fetch summary if no type filter or type is NULL (to avoid re-fetching
// summary when just filtering table, but front-end might want to keep it)
// Actually, keep fetching summary so numbers don't disappear if specific logic
// changes.
// But usually summary counts should stay static for the date range unless date
// changes.
Map<String, Integer> summary = flagpolesService.getTypeWiseCounts(fromDate, toDate);
return Map.of("status", "success", "data", data, "summary", summary);
} catch (Exception e) {
e.printStackTrace();
return Map.of("status", "error", "message", e.getMessage());
}
}
@GetMapping("/flagpoles/details")
public Map<String, Object> getRequestDetails(
@RequestParam String zone,
@RequestParam(required = false) String status,
@RequestParam(required = false) String fromDate,
@RequestParam(required = false) String toDate) {
try {
List<Map<String, Object>> data = flagpolesService.getRequestDetails(zone, status, fromDate, toDate);
return Map.of("status", "success", "data", data);
} catch (Exception e) {
e.printStackTrace();
return Map.of("status", "error", "message", e.getMessage());
}
}
@GetMapping("/flagpoles/street-details")
public Map<String, Object> getStreetDetails(@RequestParam int requestId) {
try {
List<Map<String, Object>> data = flagpolesService.getStreetDetails(requestId);
return Map.of("status", "success", "data", data);
} catch (Exception e) {
e.printStackTrace();
return Map.of("status", "error", "message", e.getMessage());
}
}
}

6
src/main/java/com/gcc/commissionerreview/controller/MainController.java

@ -15,6 +15,7 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -260,4 +261,9 @@ public class MainController {
// visistor based count // visistor based count
@GetMapping("/flagpolereport")
public String getFlagpoleReportPage(Model model) {
return "modules/flagpoles/abstractreport";
}
} }

199
src/main/java/com/gcc/commissionerreview/service/FlagpolesService.java

@ -0,0 +1,199 @@
package com.gcc.commissionerreview.service;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class FlagpolesService {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(@Qualifier("mysqlFlagPoleManagerSystemDataSource") DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public List<Map<String, Object>> getZoneWiseRequestCounts(String fromDate, String toDate, String applicationType) {
StringBuilder sql = new StringBuilder("SELECT " +
" s.zone, " +
" COUNT(u.id) as total_requests, " +
" SUM(CASE WHEN u.final_status = 'Approved' THEN 1 ELSE 0 END) as approved_count, " +
" SUM(CASE WHEN u.final_status = 'Rejected' THEN 1 ELSE 0 END) as rejected_count " +
"FROM user_request_details u " +
"JOIN street_details s ON u.id = s.user_req_id " +
"LEFT JOIN application_type at ON u.application_type = at.id ");
List<Object> params = new java.util.ArrayList<>();
// Helper to handle date and type filters
boolean whereAdded = false;
if (fromDate != null && !fromDate.isEmpty() && toDate != null && !toDate.isEmpty()) {
sql.append("WHERE DATE(u.cdate) BETWEEN ? AND ? ");
params.add(fromDate);
params.add(toDate);
whereAdded = true;
}
if (applicationType != null && !applicationType.isEmpty() && !applicationType.equalsIgnoreCase("OVERALL")) {
if (whereAdded) {
sql.append("AND at.type = ? ");
} else {
sql.append("WHERE at.type = ? ");
whereAdded = true;
}
params.add(applicationType);
}
sql.append("GROUP BY s.zone ORDER BY s.zone");
List<Map<String, Object>> dbResults = jdbcTemplate.queryForList(sql.toString(), params.toArray());
// Parse DB results into a Map keyed by Zone Number for robust matching
Map<Integer, Map<String, Object>> zoneDataMap = new java.util.HashMap<>();
for (Map<String, Object> row : dbResults) {
String zoneStr = (String) row.get("zone");
if (zoneStr != null) {
// Extract number from "Zone 10" or "Zone 05"
String numberOnly = zoneStr.replaceAll("[^0-9]", "");
if (!numberOnly.isEmpty()) {
try {
int zoneNum = Integer.parseInt(numberOnly);
// Update zone display format to "01", "02", etc.
row.put("zone", String.format("%02d", zoneNum));
zoneDataMap.put(zoneNum, row);
} catch (NumberFormatException e) {
// ignore malformed zone names
}
}
}
}
// Generate final list for Zones 1 to 15
List<Map<String, Object>> finalResults = new java.util.ArrayList<>();
for (int i = 1; i <= 15; i++) {
if (zoneDataMap.containsKey(i)) {
finalResults.add(zoneDataMap.get(i));
} else {
Map<String, Object> emptyRow = new java.util.HashMap<>();
emptyRow.put("zone", String.format("%02d", i));
emptyRow.put("total_requests", 0);
emptyRow.put("approved_count", 0);
emptyRow.put("rejected_count", 0);
finalResults.add(emptyRow);
}
}
return finalResults;
}
public List<Map<String, Object>> getRequestDetails(String zone, String status, String fromDate, String toDate) {
StringBuilder sql = new StringBuilder("SELECT " +
" u.id, " +
" u.refid, " +
" u.applicant_name, " +
" u.mobile_number, " +
" u.event_desc, " +
" DATE_FORMAT(u.event_date, '%d-%m-%Y') as event_date, " +
" u.final_status, " +
" at.type as application_type, " +
" ao.option_name as application_option, " +
" it.installation_name as installation_type " +
"FROM user_request_details u " +
"LEFT JOIN application_type at ON u.application_type = at.id " +
"LEFT JOIN application_option ao ON u.application_option = ao.id " +
"LEFT JOIN installation_type it ON u.installation_type = it.id " +
"LEFT JOIN street_details s ON u.id = s.user_req_id " +
"WHERE 1=1 ");
List<Object> params = new java.util.ArrayList<>();
if (zone != null && !zone.isEmpty()) {
// Handle various zone formats (01, 1, Zone 1, Zone 01)
String zoneNumStr = zone.replaceAll("[^0-9]", "");
if (!zoneNumStr.isEmpty()) {
try {
int z = Integer.parseInt(zoneNumStr);
sql.append("AND (s.zone = ? OR s.zone = ? OR s.zone = ? OR s.zone = ?) ");
params.add("Zone " + z);
params.add("Zone " + String.format("%02d", z));
params.add(String.valueOf(z));
params.add(String.format("%02d", z));
} catch (NumberFormatException e) {
sql.append("AND s.zone = ? ");
params.add(zone);
}
} else {
sql.append("AND s.zone = ? ");
params.add(zone);
}
}
if (status != null && !status.isEmpty() && !status.equalsIgnoreCase("Total")) {
sql.append("AND u.final_status = ? ");
params.add(status);
}
if (fromDate != null && !fromDate.isEmpty() && toDate != null && !toDate.isEmpty()) {
sql.append("AND DATE(u.cdate) BETWEEN ? AND ? ");
params.add(fromDate);
params.add(toDate);
}
sql.append("GROUP BY u.id ORDER BY u.cdate DESC");
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
}
public List<Map<String, Object>> getStreetDetails(int requestId) {
String sql = "SELECT " +
" s.zone, " +
" s.ward, " +
" s.street_name, " +
" s.no_of_poles, " +
" s.no_of_statue " +
"FROM street_details s " +
"WHERE s.user_req_id = ?";
return jdbcTemplate.queryForList(sql, requestId);
}
public Map<String, Integer> getTypeWiseCounts(String fromDate, String toDate) {
StringBuilder sql = new StringBuilder("SELECT " +
" at.type, " +
" COUNT(u.id) as count " +
"FROM user_request_details u " +
"JOIN application_type at ON u.application_type = at.id ");
Object[] params = new Object[] {};
if (fromDate != null && !fromDate.isEmpty() && toDate != null && !toDate.isEmpty()) {
sql.append("WHERE DATE(u.cdate) BETWEEN ? AND ? ");
params = new Object[] { fromDate, toDate };
}
sql.append("GROUP BY at.type");
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql.toString(), params);
Map<String, Integer> counts = new java.util.HashMap<>();
counts.put("TEMPORARY", 0);
counts.put("PERMANENT", 0);
int total = 0;
for (Map<String, Object> row : results) {
String type = ((String) row.get("type")).toUpperCase();
Number count = (Number) row.get("count");
int val = count.intValue();
counts.put(type, val);
total += val;
}
counts.put("OVERALL", total);
return counts;
}
}

416
src/main/resources/templates/modules/flagpoles/abstractreport.html

@ -0,0 +1,416 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flagpoles Abstract Report</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- DataTables CSS -->
<link href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css" rel="stylesheet" />
<link href="https://cdn.datatables.net/buttons/2.3.6/css/buttons.dataTables.min.css" rel="stylesheet" />
<style>
table.dataTable th,
table.dataTable td {
text-align: center;
vertical-align: middle;
border: 1px solid #000;
}
table.dataTable th {
background-color: #003366 !important;
color: white;
text-align: center !important;
}
.page-header-title h4 {
margin: 20px 0;
text-align: center;
}
</style>
</head>
<body>
<div class="page-header-title">
<h4>Flagpoles Abstract Report</h4>
</div>
<div class="container mt-4">
<div class="card p-3 shadow-sm">
<h5 class="mb-3"><strong>Report</strong></h5>
<div class="row g-3 align-items-end mb-3">
<div class="col-auto">
<label for="fromDate" class="form-label">From Date</label>
<input type="date" id="fromDate" class="form-control">
</div>
<div class="col-auto">
<label for="toDate" class="form-label">To Date</label>
<input type="date" id="toDate" class="form-control">
</div>
<div class="col-auto">
<button class="btn btn-primary" id="searchBtn">Search</button>
</div>
</div>
<!-- Summary Cards -->
<!-- Summary Card -->
<!-- Summary Card -->
<div class="card mb-3 shadow-sm border-primary">
<div class="card-body">
<div class="row text-center">
<div class="col-md-4 border-end" style="cursor: pointer;" onclick="loadReport('OVERALL')">
<h6 class="text-primary fw-bold">Total Overall Requests</h6>
<h3 id="overallCount">0</h3>
</div>
<div class="col-md-4 border-end" style="cursor: pointer;" onclick="loadReport('TEMPORARY')">
<h6 class="text-info fw-bold">Total Temporary Requests</h6>
<h3 id="tempCount">0</h3>
</div>
<div class="col-md-4" style="cursor: pointer;" onclick="loadReport('PERMANENT')">
<h6 class="text-success fw-bold">Total Permanent Requests</h6>
<h3 id="permCount">0</h3>
</div>
</div>
</div>
</div>
<table id="reportTable" class="table table-bordered table-hover mt-3 text-center">
<thead class="table-primary">
<tr>
<th class="text-center">Zone</th>
<th class="text-center">Total Requests</th>
<th class="text-center">Approved</th>
<th class="text-center">Rejected</th>
</tr>
</thead>
<tbody id="reportTableBody">
<!-- Data will be populated here -->
</tbody>
</table>
</div>
</div>
<div id="pageLoader"
style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.8); z-index: 9999; text-align: center;">
<div style="position: relative; top: 50%; transform: translateY(-50%);">
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
<span class="sr-only"></span>
</div>
<p style="margin-top: 1rem; font-size: 1.2rem; color: #007bff;">Please wait...</p>
</div>
</div>
<!-- Details Modal -->
<div class="modal fade" id="detailsModal" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="detailsModalLabel">Request Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="detailsPageLoader" style="display: none; text-align: center; padding: 20px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only"></span>
</div>
<p>Loading details...</p>
</div>
<table id="detailsTable" class="table table-bordered table-striped" style="width:100%">
<thead class="table-dark">
<tr>
<th>S.No</th>
<th>Ref ID</th>
<th>Applicant Name</th>
<th>Mobile</th>
<th>App Type</th>
<th>App Option</th>
<th>Install Type</th>
<th>Event Desc</th>
<th>Event Date</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody id="detailsTableBody">
<!-- Details will be populated here -->
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<!-- DataTables JS -->
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.3.6/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.3.6/js/buttons.html5.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.3.6/js/buttons.print.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/pdfmake.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/vfs_fonts.js"></script>
<script>
let dataTable;
function initDataTable() {
dataTable = $('#reportTable').DataTable({
dom: 'Bfrtip',
paging: false,
searching: false,
ordering: false, // Let SQL handle ordering by Zone
info: false,
buttons: [
{
extend: 'excelHtml5',
title: 'Flagpoles_Abstract_Report',
text: 'Download Excel',
className: 'btn btn-success btn-sm'
},
{
extend: 'pdfHtml5',
title: 'Flagpoles_Abstract_Report',
text: 'Download PDF',
className: 'btn btn-danger btn-sm',
orientation: 'portrait',
pageSize: 'A4'
}
]
});
}
let currentType = 'OVERALL';
function loadReport(type) {
if (type) {
currentType = type;
} else {
if (!currentType) currentType = 'OVERALL';
}
const fromDate = document.getElementById("fromDate").value;
const toDate = document.getElementById("toDate").value;
// Update UI to show selected filter (optional visual cue)
// Reset colors
$('.card-body .col-md-4').removeClass('bg-light');
// Highlight selected
if (currentType === 'OVERALL') $('.card-body .col-md-4:eq(0)').addClass('bg-light');
if (currentType === 'TEMPORARY') $('.card-body .col-md-4:eq(1)').addClass('bg-light');
if (currentType === 'PERMANENT') $('.card-body .col-md-4:eq(2)').addClass('bg-light');
$('#pageLoader').show();
let url = `/commissionerreview/api/flagpoles/abstract?fromDate=${fromDate}&toDate=${toDate}`;
if (currentType && currentType !== 'OVERALL') {
url += `&applicationType=${currentType}`;
}
fetch(url)
.then(res => res.json())
.then(json => {
$('#pageLoader').hide();
if (json.status === "success") {
populateTable(json.data);
if (json.summary) {
$('#tempCount').text(json.summary.TEMPORARY || 0);
$('#permCount').text(json.summary.PERMANENT || 0);
$('#overallCount').text(json.summary.OVERALL || 0);
} else {
if (!type) { // Only zero out if not maintaining state, but here we keep summary if just filtering
// actually backend returns summary always so update it matches current date range
$('#tempCount').text(0);
$('#permCount').text(0);
$('#overallCount').text(0);
}
}
} else {
alert("Error: " + json.message);
}
})
.catch(err => {
$('#pageLoader').hide();
console.error("Fetch Error:", err);
alert("Failed to load data");
});
}
function populateTable(data) {
dataTable.clear();
data.forEach(row => {
dataTable.row.add([
row.zone,
`<a href="#" onclick="viewDetails('${row.zone}', 'Total')">${row.total_requests}</a>`,
`<a href="#" onclick="viewDetails('${row.zone}', 'Approved')">${row.approved_count}</a>`,
`<a href="#" onclick="viewDetails('${row.zone}', 'Rejected')">${row.rejected_count}</a>`
]);
});
dataTable.draw();
}
function viewDetails(zone, status) {
const fromDate = document.getElementById("fromDate").value;
const toDate = document.getElementById("toDate").value;
$('#detailsModalLabel').text(`${status} Requests for ${zone}`);
$('#detailsPageLoader').show();
// Clear previous table data
if ($.fn.DataTable.isDataTable('#detailsTable')) {
$('#detailsTable').DataTable().destroy();
}
$('#detailsTableBody').empty();
new bootstrap.Modal(document.getElementById('detailsModal')).show();
fetch(`/commissionerreview/api/flagpoles/details?zone=${zone}&status=${status}&fromDate=${fromDate}&toDate=${toDate}`)
.then(res => res.json())
.then(json => {
$('#detailsPageLoader').hide();
if (json.status === "success") {
populateDetailsTable(json.data);
} else {
alert("Error: " + json.message);
}
})
.catch(err => {
$('#detailsPageLoader').hide();
console.error("Fetch Error:", err);
alert("Failed to load details");
});
}
function formatValue(val) {
if (val === null || val === undefined || val === 'null' || val === '') {
return '-';
}
return val;
}
function populateDetailsTable(data) {
const tbody = $('#detailsTableBody');
tbody.empty();
data.forEach((row, index) => {
tbody.append(`
<tr>
<td>${index + 1}</td>
<td>${formatValue(row.refid)}</td>
<td>${formatValue(row.applicant_name)}</td>
<td>${formatValue(row.mobile_number)}</td>
<td>${formatValue(row.application_type)}</td>
<td>${formatValue(row.application_option)}</td>
<td>${formatValue(row.installation_type)}</td>
<td>${formatValue(row.event_desc)}</td>
<td>${formatValue(row.event_date)}</td>
<td>${formatValue(row.final_status)}</td>
<td>
<button class="btn btn-info btn-sm" onclick="viewStreetDetails(${row.id})">
View Streets
</button>
</td>
</tr>
`);
});
$('#detailsTable').DataTable({
dom: 'Bfrtip',
buttons: [
'excelHtml5',
'pdfHtml5'
]
});
}
function viewStreetDetails(requestId) {
$('#streetModalLabel').text(`Street Details for Request ID`);
$('#streetTableBody').html('<tr><td colspan="5" class="text-center">Loading...</td></tr>');
new bootstrap.Modal(document.getElementById('streetModal')).show();
fetch(`/commissionerreview/api/flagpoles/street-details?requestId=${requestId}`)
.then(res => res.json())
.then(json => {
const tbody = $('#streetTableBody');
tbody.empty();
if (json.status === "success" && json.data.length > 0) {
json.data.forEach(row => {
tbody.append(`
<tr>
<td>${formatValue(row.zone)}</td>
<td>${formatValue(row.ward)}</td>
<td>${formatValue(row.street_name)}</td>
<td>${formatValue(row.no_of_poles)}</td>
<td>${formatValue(row.no_of_statue)}</td>
</tr>
`);
});
} else {
tbody.html('<tr><td colspan="5" class="text-center">No street details found</td></tr>');
}
})
.catch(err => {
console.error("Fetch Error:", err);
$('#streetTableBody').html('<tr><td colspan="5" class="text-center text-danger">Failed to load data</td></tr>');
});
}
$(document).ready(function () {
console.log("Document ready, initializing...");
initDataTable();
// Bind search button click
$('#searchBtn').click(function () {
loadReport();
});
// Initial load
loadReport();
});
</script>
<!-- Street Details Modal -->
<div class="modal fade" id="streetModal" tabindex="-1" aria-labelledby="streetModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="streetModalLabel">Street Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="table table-bordered table-striped" width="100%">
<thead class="table-dark">
<tr>
<th>Zone</th>
<th>Ward</th>
<th>Street Name</th>
<th>Poles</th>
<th>Statues</th>
</tr>
</thead>
<tbody id="streetTableBody">
<!-- Streets will be populated here -->
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</body>
</html>
Loading…
Cancel
Save