Browse Source

street vendor report added

master
Hariprasath S 5 months ago
parent
commit
99463e80ae
  1. 25
      src/main/java/com/gcc/commissionerreview/configuration/DataSourceConfig.java
  2. 10
      src/main/java/com/gcc/commissionerreview/controller/MainController.java
  3. 46
      src/main/java/com/gcc/commissionerreview/controller/StreetVendorApiController.java
  4. 475
      src/main/java/com/gcc/commissionerreview/service/StreetVendorService.java
  5. 543
      src/main/resources/templates/modules/streetvendor/inspectiondetails.html
  6. 577
      src/main/resources/templates/modules/streetvendor/overalldetails.html

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

@ -190,4 +190,29 @@ public class DataSourceConfig {
return dataSource;
}
////////// GCC Street Vendor//////////////////////
@Bean(name = "mysqlStreetVendorDataSource")
public DataSource mysqlStreetVendorDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://" + host + "/gcc_street_vendor");
dataSource.setUsername("root");
dataSource.setPassword(dbpassword);
return dataSource;
}
////////// Vending Committee //////////////////////
@Bean(name = "mysqlVendingCommitteeDataSource")
public DataSource mysqlVendingCommitteeDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://" + host + "/vendingcommitte");
dataSource.setUsername("root");
dataSource.setPassword(dbpassword);
return dataSource;
}
}

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

@ -326,4 +326,14 @@ public class MainController {
return "modules/vendorreport";
}
@GetMapping("/stvendor/overallreport")
public String stvendor_overallreport(Model model) {
return "modules/streetvendor/overalldetails";
}
@GetMapping("/stvendor/inspectiondetails")
public String stvendor_inspectiondetails(Model model) {
return "modules/streetvendor/inspectiondetails";
}
}

46
src/main/java/com/gcc/commissionerreview/controller/StreetVendorApiController.java

@ -0,0 +1,46 @@
package com.gcc.commissionerreview.controller;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.gcc.commissionerreview.service.StreetVendorService;
@RestController
@RequestMapping("/commissionerreview/api/streetvendor")
public class StreetVendorApiController {
@Autowired
private StreetVendorService stvservice;
@GetMapping("/getzoneoveralldetails")
public List<Map<String, Object>> getzoneoveralldetails(@RequestParam String fromdate,@RequestParam String todate)
{
return stvservice.getzoneoveralldetails(fromdate,todate);
}
@GetMapping("/getwardoveralldetails")
public List<Map<String, Object>> getwardoveralldetails(@RequestParam String fromdate,@RequestParam String todate,@RequestParam String zone)
{
return stvservice.getwardoveralldetails(fromdate, todate, zone);
}
@GetMapping("/getzoneinspectiondetails")
public List<Map<String, Object>> getzoneinspectiondetails(@RequestParam String fromdate,@RequestParam String todate)
{
return stvservice.getzoneinspectiondetails(fromdate,todate);
}
@GetMapping("/getwardinspectiondetails")
public List<Map<String, Object>> getwardinspectiondetails(@RequestParam String fromdate,@RequestParam String todate,@RequestParam String zone)
{
return stvservice.getwardinspectiondetails(fromdate,todate,zone);
}
}

475
src/main/java/com/gcc/commissionerreview/service/StreetVendorService.java

@ -0,0 +1,475 @@
package com.gcc.commissionerreview.service;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class StreetVendorService {
private final Environment environment;
private String fileBaseUrl;
@Autowired
private JdbcTemplate gccstrTemplate;
@Autowired
private JdbcTemplate vendingcommitteTemplate;
public StreetVendorService(Environment environment) {
this.environment = environment;
this.fileBaseUrl = environment.getProperty("fileBaseUrl");
}
@Autowired
public void setDataSource1(@Qualifier("mysqlVendingCommitteeDataSource") DataSource dataSource) {
this.vendingcommitteTemplate = new JdbcTemplate(dataSource);
}
@Autowired
public void setDataSource2(@Qualifier("mysqlStreetVendorDataSource") DataSource dataSource) {
this.gccstrTemplate = new JdbcTemplate(dataSource);
}
public List<Map<String, Object>> getzoneoveralldetails(String fromdate, String todate) {
StringBuilder sql = new StringBuilder();
sql.append(" SELECT ");
sql.append(" vd.zone AS zone, ");
// 1. Application Received
sql.append(" COUNT(DISTINCT vd.id) AS application_received, ");
// 2. Assistant Processed
sql.append(" COUNT(DISTINCT CASE ");
sql.append(" WHEN vd.tamil_name IS NOT NULL AND vd.tamil_name <> '' ");
sql.append(" AND vd.tamil_f_h_name IS NOT NULL AND vd.tamil_f_h_name <> '' ");
sql.append(" AND vd.tamil_landmark IS NOT NULL AND vd.tamil_landmark <> '' ");
sql.append(" AND vd.tamil_gender IS NOT NULL AND vd.tamil_gender <> '' ");
sql.append(" AND vd.tamil_present_district IS NOT NULL AND vd.tamil_present_district <> '' ");
sql.append(" AND vd.tamil_vending_category IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) AS assistant_processed, ");
// 3. LI Processed
sql.append(" COUNT(DISTINCT va1.vdid) AS li_processed, ");
// 4. ARO Processed
sql.append(" COUNT(DISTINCT va2.vdid) AS aro_processed, ");
// 5. EE Processed
sql.append(" COUNT(DISTINCT va3.vdid) AS ee_processed, ");
// 6. Committee Processed
sql.append(" COUNT(DISTINCT CASE ");
sql.append(" WHEN vrl.vdid IS NOT NULL OR vpl.vdid IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) AS committee_processed, ");
// 7. Total Processed
sql.append(" ( ");
sql.append(" COUNT(DISTINCT CASE ");
sql.append(" WHEN vd.tamil_name IS NOT NULL AND vd.tamil_name <> '' ");
sql.append(" AND vd.tamil_f_h_name IS NOT NULL AND vd.tamil_f_h_name <> '' ");
sql.append(" AND vd.tamil_landmark IS NOT NULL AND vd.tamil_landmark <> '' ");
sql.append(" AND vd.tamil_gender IS NOT NULL AND vd.tamil_gender <> '' ");
sql.append(" AND vd.tamil_present_district IS NOT NULL AND vd.tamil_present_district <> '' ");
sql.append(" AND vd.tamil_vending_category IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) ");
sql.append(" + COUNT(DISTINCT va1.vdid) ");
sql.append(" + COUNT(DISTINCT va2.vdid) ");
sql.append(" + COUNT(DISTINCT va3.vdid) ");
sql.append(" + COUNT(DISTINCT CASE ");
sql.append(" WHEN vrl.vdid IS NOT NULL OR vpl.vdid IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) ");
sql.append(" ) AS total_processed, ");
// 8. Pending
sql.append(" ( ");
sql.append(" COUNT(DISTINCT vd.id) - ");
sql.append(" ( ");
sql.append(" COUNT(DISTINCT CASE ");
sql.append(" WHEN vd.tamil_name IS NOT NULL AND vd.tamil_name <> '' ");
sql.append(" AND vd.tamil_f_h_name IS NOT NULL AND vd.tamil_f_h_name <> '' ");
sql.append(" AND vd.tamil_landmark IS NOT NULL AND vd.tamil_landmark <> '' ");
sql.append(" AND vd.tamil_gender IS NOT NULL AND vd.tamil_gender <> '' ");
sql.append(" AND vd.tamil_present_district IS NOT NULL AND vd.tamil_present_district <> '' ");
sql.append(" AND vd.tamil_vending_category IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) ");
sql.append(" + COUNT(DISTINCT va1.vdid) ");
sql.append(" + COUNT(DISTINCT va2.vdid) ");
sql.append(" + COUNT(DISTINCT va3.vdid) ");
sql.append(" + COUNT(DISTINCT CASE ");
sql.append(" WHEN vrl.vdid IS NOT NULL OR vpl.vdid IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) ");
sql.append(" ) ");
sql.append(" ) AS pending, ");
// 9. Processed within 30 days
sql.append(" COUNT(DISTINCT CASE ");
sql.append(" WHEN vrl.cdate IS NOT NULL ");
sql.append(" AND DATEDIFF(vrl.cdate, vd.cdate) <= 30 ");
sql.append(" THEN vd.id ");
sql.append(" WHEN vpl.cdate IS NOT NULL ");
sql.append(" AND DATEDIFF(vpl.cdate, vd.cdate) <= 30 ");
sql.append(" THEN vd.id ");
sql.append(" END) AS processed_within_30_days ");
sql.append(" FROM gcc_street_vendor.vendor_details vd ");
sql.append(" LEFT JOIN gcc_street_vendor.vendor_approval_level_1 va1 ");
sql.append(" ON va1.vdid = vd.id ");
sql.append(" LEFT JOIN gcc_street_vendor.vendor_approval_level_2 va2 ");
sql.append(" ON va2.vdid = vd.id ");
sql.append(" LEFT JOIN gcc_street_vendor.vendor_approval_level_3 va3 ");
sql.append(" ON va3.vdid = vd.id ");
sql.append(" LEFT JOIN vendingcommitte.vendor_request_list vrl ");
sql.append(" ON vrl.vdid = vd.id ");
sql.append(" LEFT JOIN vendingcommitte.vendor_postponed_list vpl ");
sql.append(" ON vpl.vdid = vd.id ");
sql.append(" WHERE vd.isactive = 1 ");
List<Object> params = new ArrayList<>();
if (fromdate != null && !fromdate.isEmpty()
&& todate != null && !todate.isEmpty()) {
sql.append(" AND DATE(vd.cdate) BETWEEN ? AND ? ");
params.add(fromdate);
params.add(todate);
}
sql.append(" AND vd.zone IS NOT NULL ");
sql.append(" GROUP BY vd.zone ");
sql.append(" ORDER BY vd.zone ");
List<Map<String, Object>> reportList =
gccstrTemplate.queryForList(sql.toString(), params.toArray());
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "Success");
response.put("message", "Zone-wise Report");
response.put("data", reportList);
return Collections.singletonList(response);
}
public List<Map<String, Object>> getwardoveralldetails(String fromdate, String todate, String zone) {
StringBuilder sql = new StringBuilder();
sql.append(" SELECT ");
sql.append(" vd.ward AS ward, ");
// 1. Application Received
sql.append(" COUNT(DISTINCT vd.id) AS application_received, ");
// 2. Assistant Processed
sql.append(" COUNT(DISTINCT CASE ");
sql.append(" WHEN vd.tamil_name IS NOT NULL AND vd.tamil_name <> '' ");
sql.append(" AND vd.tamil_f_h_name IS NOT NULL AND vd.tamil_f_h_name <> '' ");
sql.append(" AND vd.tamil_landmark IS NOT NULL AND vd.tamil_landmark <> '' ");
sql.append(" AND vd.tamil_gender IS NOT NULL AND vd.tamil_gender <> '' ");
sql.append(" AND vd.tamil_present_district IS NOT NULL AND vd.tamil_present_district <> '' ");
sql.append(" AND vd.tamil_vending_category IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) AS assistant_processed, ");
// 3. LI Processed
sql.append(" COUNT(DISTINCT va1.vdid) AS li_processed, ");
// 4. ARO Processed
sql.append(" COUNT(DISTINCT va2.vdid) AS aro_processed, ");
// 5. EE Processed
sql.append(" COUNT(DISTINCT va3.vdid) AS ee_processed, ");
// 6. Committee Processed
sql.append(" COUNT(DISTINCT CASE ");
sql.append(" WHEN vrl.vdid IS NOT NULL OR vpl.vdid IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) AS committee_processed, ");
// 7. Total Processed
sql.append(" ( ");
sql.append(" COUNT(DISTINCT CASE ");
sql.append(" WHEN vd.tamil_name IS NOT NULL AND vd.tamil_name <> '' ");
sql.append(" AND vd.tamil_f_h_name IS NOT NULL AND vd.tamil_f_h_name <> '' ");
sql.append(" AND vd.tamil_landmark IS NOT NULL AND vd.tamil_landmark <> '' ");
sql.append(" AND vd.tamil_gender IS NOT NULL AND vd.tamil_gender <> '' ");
sql.append(" AND vd.tamil_present_district IS NOT NULL AND vd.tamil_present_district <> '' ");
sql.append(" AND vd.tamil_vending_category IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) ");
sql.append(" + COUNT(DISTINCT va1.vdid) ");
sql.append(" + COUNT(DISTINCT va2.vdid) ");
sql.append(" + COUNT(DISTINCT va3.vdid) ");
sql.append(" + COUNT(DISTINCT CASE ");
sql.append(" WHEN vrl.vdid IS NOT NULL OR vpl.vdid IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) ");
sql.append(" ) AS total_processed, ");
// 8. Pending
sql.append(" ( ");
sql.append(" COUNT(DISTINCT vd.id) - ");
sql.append(" ( ");
sql.append(" COUNT(DISTINCT CASE ");
sql.append(" WHEN vd.tamil_name IS NOT NULL AND vd.tamil_name <> '' ");
sql.append(" AND vd.tamil_f_h_name IS NOT NULL AND vd.tamil_f_h_name <> '' ");
sql.append(" AND vd.tamil_landmark IS NOT NULL AND vd.tamil_landmark <> '' ");
sql.append(" AND vd.tamil_gender IS NOT NULL AND vd.tamil_gender <> '' ");
sql.append(" AND vd.tamil_present_district IS NOT NULL AND vd.tamil_present_district <> '' ");
sql.append(" AND vd.tamil_vending_category IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) ");
sql.append(" + COUNT(DISTINCT va1.vdid) ");
sql.append(" + COUNT(DISTINCT va2.vdid) ");
sql.append(" + COUNT(DISTINCT va3.vdid) ");
sql.append(" + COUNT(DISTINCT CASE ");
sql.append(" WHEN vrl.vdid IS NOT NULL OR vpl.vdid IS NOT NULL ");
sql.append(" THEN vd.id ");
sql.append(" END) ");
sql.append(" ) ");
sql.append(" ) AS pending, ");
// 9. Processed within 30 days
sql.append(" COUNT(DISTINCT CASE ");
sql.append(" WHEN vrl.cdate IS NOT NULL ");
sql.append(" AND DATEDIFF(vrl.cdate, vd.cdate) <= 30 ");
sql.append(" THEN vd.id ");
sql.append(" WHEN vpl.cdate IS NOT NULL ");
sql.append(" AND DATEDIFF(vpl.cdate, vd.cdate) <= 30 ");
sql.append(" THEN vd.id ");
sql.append(" END) AS processed_within_30_days ");
sql.append(" FROM gcc_street_vendor.vendor_details vd ");
sql.append(" LEFT JOIN gcc_street_vendor.vendor_approval_level_1 va1 ");
sql.append(" ON va1.vdid = vd.id ");
sql.append(" LEFT JOIN gcc_street_vendor.vendor_approval_level_2 va2 ");
sql.append(" ON va2.vdid = vd.id ");
sql.append(" LEFT JOIN gcc_street_vendor.vendor_approval_level_3 va3 ");
sql.append(" ON va3.vdid = vd.id ");
sql.append(" LEFT JOIN vendingcommitte.vendor_request_list vrl ");
sql.append(" ON vrl.vdid = vd.id ");
sql.append(" LEFT JOIN vendingcommitte.vendor_postponed_list vpl ");
sql.append(" ON vpl.vdid = vd.id ");
sql.append(" WHERE vd.isactive = 1 ");
List<Object> params = new ArrayList<>();
if (fromdate != null && !fromdate.isEmpty()
&& todate != null && !todate.isEmpty()) {
sql.append(" AND DATE(vd.cdate) BETWEEN ? AND ? ");
params.add(fromdate);
params.add(todate);
}
if(zone != null && !zone.isEmpty()) {
sql.append(" AND vd.zone = ? ");
params.add(zone);
}
sql.append(" AND vd.zone IS NOT NULL AND vd.ward IS NOT NULL ");
sql.append(" GROUP BY vd.ward ");
sql.append(" ORDER BY vd.ward ");
List<Map<String, Object>> reportList =
gccstrTemplate.queryForList(sql.toString(), params.toArray());
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "Success");
response.put("message","Zone:"+ zone+" Ward-wise Report");
response.put("data", reportList);
return Collections.singletonList(response);
}
public List<Map<String, Object>> getzoneinspectiondetails(String fromdate, String todate) {
StringBuilder sql = new StringBuilder();
sql.append(" SELECT ");
sql.append(" vd.zone AS zone, ");
// 1. Application Received
sql.append(" COUNT(DISTINCT vd.id) AS application_received, ");
// 2. LI Processed
sql.append(" COUNT(DISTINCT va1.vdid) AS li_processed, ");
// 8. Pending
sql.append(" ( ");
sql.append(" COUNT(DISTINCT vd.id) - ");
sql.append(" COUNT(DISTINCT va1.vdid) ");
sql.append(" ) AS li_pending ");
sql.append(" FROM gcc_street_vendor.vendor_details vd ");
sql.append(" LEFT JOIN gcc_street_vendor.vendor_approval_level_1 va1 ");
sql.append(" ON va1.vdid = vd.id ");
sql.append(" WHERE vd.isactive = 1 ");
List<Object> params = new ArrayList<>();
if (fromdate != null && !fromdate.isEmpty()
&& todate != null && !todate.isEmpty()) {
sql.append(" AND DATE(vd.cdate) BETWEEN ? AND ? ");
params.add(fromdate);
params.add(todate);
}
sql.append(" AND vd.zone IS NOT NULL ");
sql.append(" GROUP BY vd.zone ");
sql.append(" ORDER BY vd.zone ");
List<Map<String, Object>> reportList =
gccstrTemplate.queryForList(sql.toString(), params.toArray());
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "Success");
response.put("message", "LI Inspection Zone-wise Report");
response.put("data", reportList);
return Collections.singletonList(response);
}
public List<Map<String, Object>> getwardinspectiondetails(String fromdate, String todate, String zone) {
StringBuilder sql = new StringBuilder();
sql.append(" SELECT ");
sql.append(" vd.ward AS ward, ");
// 1. Application Received
sql.append(" COUNT(DISTINCT vd.id) AS application_received, ");
// 2. LI Processed
sql.append(" COUNT(DISTINCT va1.vdid) AS li_processed, ");
// 8. Pending
sql.append(" ( ");
sql.append(" COUNT(DISTINCT vd.id) - ");
sql.append(" ( ");
sql.append(" COUNT(DISTINCT va1.vdid) ");
sql.append(" ) ");
sql.append(" ) AS li_pending ");
sql.append(" FROM gcc_street_vendor.vendor_details vd ");
sql.append(" LEFT JOIN gcc_street_vendor.vendor_approval_level_1 va1 ");
sql.append(" ON va1.vdid = vd.id ");
sql.append(" WHERE vd.isactive = 1 ");
List<Object> params = new ArrayList<>();
if (fromdate != null && !fromdate.isEmpty()
&& todate != null && !todate.isEmpty()) {
sql.append(" AND DATE(vd.cdate) BETWEEN ? AND ? ");
params.add(fromdate);
params.add(todate);
}
if(zone != null && !zone.isEmpty()) {
sql.append(" AND vd.zone = ? ");
params.add(zone);
}
sql.append(" AND vd.zone IS NOT NULL AND vd.ward IS NOT NULL ");
sql.append(" GROUP BY vd.ward ");
sql.append(" ORDER BY vd.ward ");
List<Map<String, Object>> reportList =
gccstrTemplate.queryForList(sql.toString(), params.toArray());
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "Success");
response.put("message","Zone:"+ zone+" Ward-wise Inspection Report");
response.put("data", reportList);
return Collections.singletonList(response);
}
}

543
src/main/resources/templates/modules/streetvendor/inspectiondetails.html

@ -0,0 +1,543 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>StreetVendor Inspection Details</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>
.filter-section {
background: #fff;
padding: 15px;
border-radius: 8px;
/* box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); */
margin-bottom: 20px;
}
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;
}
.dataTables_wrapper .dt-buttons {
margin-bottom: 10px;
}
.dataTables_filter {
margin-bottom: 25px; /* adjust as needed */
}
.page-header-title h4 {
margin: 20px 0;
text-align: center;
}
#zoneDetailsTable_wrapper .dataTables_scrollBody {
border: none !important;
}
#zoneDetailsTable td, #zoneDetailsTable th {
vertical-align: middle;
text-align: center;
white-space: nowrap;
}
</style>
</head>
<body>
<div class="page-header-title">
<h4>Street Vendor - Inspection Report (LI)</h4>
</div>
<div class="filter-section d-flex justify-content-center align-items-end gap-3 flex-wrap">
<div>
<label for="fromDate" class="form-label fw-bold">From Date:</label>
<input type="date" id="fromDate" class="form-control" onclick="this.showPicker()">
</div>
<div>
<label for="toDate" class="form-label fw-bold">To Date:</label>
<input type="date" id="toDate" class="form-control" onclick="this.showPicker()">
</div>
<div>
<button id="btnSearch" class="btn btn-primary px-4"><i class="fa-solid fa-search"></i> Search</button>
</div>
<div>
<button id="btnReset" class="btn btn-secondary px-4"> Reset</button>
</div>
</div>
<div id="opSummaryTable" style="display: block;width: 100%;" class="container">
<table id="ZoneOpDatatable" class="table table-striped table-bordered display nowrap w-100">
<thead>
<tr>
<th class="text-center">Zone</th>
<th class="text-center">Application Received</th>
<th class="text-center">LI Processed</th>
<th class="text-center">LI Pending </th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="modal fade" id="zoneDetailsModal" tabindex="-1" aria-labelledby="zoneDetailsLabel" aria-hidden="true" data-bs-keyboard="false" data-bs-backdrop="static">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="zoneModalTitle">Zone Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table id="WardDatatable" class="table table-striped table-bordered display nowrap w-100">
<thead>
<tr>
<th class="text-center">Ward</th>
<th class="text-center">Application Received</th>
<th class="text-center">LI Processed</th>
<th class="text-center">LI Pending </th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</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>
<!-- Add the 'Please wait' text below the spinner -->
<p style="margin-top: 1rem; font-size: 1.2rem; color: #007bff;">Please wait...</p>
</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>
<!-- SweetAlert -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
$('#fromDate, #toDate').on('change', function () {
let fromDate = $('#fromDate').val();
let toDate = $('#toDate').val();
// Both dates selected
if (fromDate && toDate) {
if (new Date(fromDate) > new Date(toDate)) {
Swal.fire({
icon: 'warning',
title: 'Invalid Date Range',
text: 'From Date should not be greater than To Date'
});
$(this).val('');
return;
}
}
// Set min/max dynamically
$('#toDate').attr('min', fromDate);
$('#fromDate').attr('max', toDate);
});
$('#btnReset').click(function () {
$('#fromDate').val('');
$('#toDate').val('');
});
$('#btnSearch').click(function () {
var fromDate = $('#fromDate').val() || '';
var toDate = $('#toDate').val() || '';
loadZoneReport(fromDate,toDate);
});
function loadZoneReport(fromdate,todate) {
$('#pageLoader').show();
$.ajax({
url: '/commissionerreview/api/streetvendor/getzoneinspectiondetails',
method: 'GET',
data:{fromdate:fromdate,todate:todate},
success: function(response) {
// Safely extract data whether it's an array or an object
var res = Array.isArray(response) ? response[0] : response;
console.log("Response:", res);
$('#pageLoader').hide();
if (res.status === "Success") {
if (Array.isArray(res.data) && res.data.length > 0) {
console.log("Details Array:", response);
let table = $('#ZoneOpDatatable').DataTable({
destroy: true,
autoWidth: true,
info: true,
data: res.data, // using response directly
columns: [
{
data: 'zone',
render: function (data, type, row) {
return `<a href="#" onclick="openZoneModal('${data}')" style="text-decoration: underline;">${data}</a>`;
}
},
{ data: 'application_received', defaultContent: '-', className: 'text-center' },
{ data: 'li_processed', defaultContent: '-', className: 'text-center' },
{ data: 'li_pending', defaultContent: '-', className: 'text-center' }
],
dom: 'Bfrtip',
buttons: [
{
extend: 'excelHtml5',
text: 'Export Excel',
title: 'Street Vendor Inspection Zone Wise Report',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'pdfHtml5',
text: 'Export PDF',
title: 'Street Vendor Inspection Zone Wise Report',
pageSize: 'A4',
exportOptions: {
columns: ':visible'
},
customize: function (doc) {
doc.styles.tableHeader.alignment = 'center';
doc.defaultStyle.fontSize = 9;
doc.content[1].table.widths = [
'*', '*', '*', '*', '*'
];
// Center align all table cells
let rowCount = doc.content[1].table.body.length;
for (let i = 0; i < rowCount; i++) {
let row = doc.content[1].table.body[i];
for (let j = 0; j < row.length; j++) {
row[j].alignment = 'center';
}
}
}
}
],
lengthMenu: [
[50, 100, 200, 500, -1],
['50 rows', '100 rows', '200 rows', 'Show all']
],
scrollY: 'auto',
scrollX: true,
responsive: false,
searching: false,
ordering: false
});
}
else if(Array.isArray(res.data) && res.data.length === 0) {
$('#pageLoader').hide();
//alert("No data available.");
Swal.fire({
icon: 'info',
title: 'No Data',
text: 'No data available.',
showConfirmButton: true,
timer: 3000
});
}
else {
$('#pageLoader').hide();
//alert("Error: " + response.message);
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Error',
showConfirmButton: true,
timer: 3000
});
}
}
else {
$('#pageLoader').hide();
//alert("Error: " + response.message);
Swal.fire({
icon: 'error',
title: 'Error',
text: res.message,
showConfirmButton: true,
timer: 3000
});
}
},
error: function(xhr, status, error) {
$('#pageLoader').hide();
// alert("Request failed: " + error);
Swal.fire({
icon: 'error',
title: 'Error',
text: error,
showConfirmButton: true,
timer: 3000
});
}
});
}
$(document).ready(function () {
loadZoneReport("","");
$('#zoneDetailsModal').on('shown.bs.modal', function () {
if ($.fn.DataTable.isDataTable('#WardDatatable')) {
let questionDatatable = $('#WardDatatable').DataTable();
questionDatatable.columns.adjust().draw();
}
});
});
function openZoneModal(zoneCode) {
console.log("Opening modal for zone:", zoneCode);
var fromDate = $('#fromDate').val() || '';
var toDate = $('#toDate').val() || '';
$('#pageLoader').show();
// Set data in modal (example)
$('#zoneModalTitle').text('Zone ' + zoneCode + ' Details');
$.ajax({
url: '/commissionerreview/api/streetvendor/getwardinspectiondetails',
method: 'GET',
data:{fromdate:fromDate,todate:toDate,zone:zoneCode},
success: function(response) {
var res = Array.isArray(response) ? response[0] : response;
console.log("Response:", res);
$('#pageLoader').hide();
if (res.status === "Success") {
if (Array.isArray(res.data) && res.data.length > 0){
console.log("Details Array:", response);
let table = $('#WardDatatable').DataTable({
destroy: true,
autoWidth: true,
info: true,
data: res.data, // using response directly
columns: [
{ data: 'ward', defaultContent: '-', className: 'text-center' },
{ data: 'application_received', defaultContent: '-', className: 'text-center' },
{ data: 'li_processed', defaultContent: '-', className: 'text-center' },
{ data: 'li_pending', defaultContent: '-', className: 'text-center' }
],
dom: 'Bfrtip',
lengthMenu: [
[50, 100, 200, 500, -1],
['50 rows', '100 rows', '200 rows', 'Show all']
],
scrollY: '400px',
scrollX: true,
responsive: false,
searching: false,
ordering: false,
buttons: [
{
extend: 'excelHtml5',
text: 'Export Excel',
title: 'Street Vendor Inspection Zone: '+zoneCode+' ward wise Report',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'pdfHtml5',
text: 'Export PDF',
title: 'Street Vendor Inspection Zone: '+zoneCode+' ward wise Report',
pageSize: 'A4',
exportOptions: {
columns: ':visible'
},
customize: function (doc) {
doc.styles.tableHeader.alignment = 'center';
doc.defaultStyle.fontSize = 9;
doc.content[1].table.widths = [
'*', '*', '*', '*', '*'
];
// Center align all table cells
let rowCount = doc.content[1].table.body.length;
for (let i = 0; i < rowCount; i++) {
let row = doc.content[1].table.body[i];
for (let j = 0; j < row.length; j++) {
row[j].alignment = 'center';
}
}
}
}
]
});
$('#zoneDetailsModal').modal('show');
}
else if (Array.isArray(res.data) && res.data.length === 0){
$('#pageLoader').hide();
//alert("No data available.");
Swal.fire({
icon: 'info',
title: 'No Data',
text: 'No data available.',
showConfirmButton: true,
timer: 3000
});
}
else{
$('#pageLoader').hide();
//alert("Error: " + response.message);
Swal.fire({
icon: 'error',
title: 'Error',
text: res.message,
showConfirmButton: true,
timer: 3000
});
}
}
else {
$('#pageLoader').hide();
//alert("Error: " + response.message);
Swal.fire({
icon: 'error',
title: 'Error',
text: res.message,
showConfirmButton: true,
timer: 3000
});
}
},
error: function(xhr, status, error) {
$('#pageLoader').hide();
//alert("Request failed: " + error);
Swal.fire({
icon: 'error',
title: 'Error',
text: error,
showConfirmButton: true,
timer: 3000
});
}
});
}
</script>
</body>
</html>

577
src/main/resources/templates/modules/streetvendor/overalldetails.html

@ -0,0 +1,577 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>StreetVendor Overall Details</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>
.filter-section {
background: #fff;
padding: 15px;
border-radius: 8px;
/* box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); */
margin-bottom: 20px;
}
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;
}
.dataTables_wrapper .dt-buttons {
margin-bottom: 10px;
}
.dataTables_filter {
margin-bottom: 25px; /* adjust as needed */
}
.page-header-title h4 {
margin: 20px 0;
text-align: center;
}
#zoneDetailsTable_wrapper .dataTables_scrollBody {
border: none !important;
}
#zoneDetailsTable td, #zoneDetailsTable th {
vertical-align: middle;
text-align: center;
white-space: nowrap;
}
</style>
</head>
<body>
<div class="page-header-title">
<h4>Street Vendor - Zonewise Report</h4>
</div>
<div class="filter-section d-flex justify-content-center align-items-end gap-3 flex-wrap">
<div>
<label for="fromDate" class="form-label fw-bold">From Date:</label>
<input type="date" id="fromDate" class="form-control" onclick="this.showPicker()">
</div>
<div>
<label for="toDate" class="form-label fw-bold">To Date:</label>
<input type="date" id="toDate" class="form-control" onclick="this.showPicker()">
</div>
<div>
<button id="btnSearch" class="btn btn-primary px-4"><i class="fa-solid fa-search"></i> Search</button>
</div>
<div>
<button id="btnReset" class="btn btn-secondary px-4"> Reset</button>
</div>
</div>
<div id="opSummaryTable" style="display: block;width: 100%;" class="container">
<table id="ZoneOpDatatable" class="table table-striped table-bordered display nowrap w-100">
<thead>
<tr>
<th rowspan="2" class="text-center">Zone</th>
<th rowspan="2" class="text-center">Application Received</th>
<th colspan="5" class="text-center">Application Processed</th>
<th rowspan="2" class="text-center">Total Processed</th>
<th rowspan="2" class="text-center">Pending </th>
<th rowspan="2" class="text-center">Processed within 30 days</th>
</tr>
<tr>
<th class="text-center">Assistant Processed</th>
<th class="text-center">LI Processed</th>
<th class="text-center">ARO Processed</th>
<th class="text-center">EE Processed</th>
<th class="text-center">Committee Processed</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="modal fade" id="zoneDetailsModal" tabindex="-1" aria-labelledby="zoneDetailsLabel" aria-hidden="true" data-bs-keyboard="false" data-bs-backdrop="static">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="zoneModalTitle">Zone Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table id="WardDatatable" class="table table-striped table-bordered display nowrap w-100">
<thead>
<tr>
<th rowspan="2" class="text-center">Ward</th>
<th rowspan="2" class="text-center">Application Received</th>
<th colspan="5" class="text-center">Application Processed</th>
<th rowspan="2" class="text-center">Total Processed</th>
<th rowspan="2" class="text-center">Pending </th>
<th rowspan="2" class="text-center">Processed within 30 days</th>
</tr>
<tr>
<th class="text-center">Assistant Processed</th>
<th class="text-center">LI Processed</th>
<th class="text-center">ARO Processed</th>
<th class="text-center">EE Processed</th>
<th class="text-center">Committee Processed</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</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>
<!-- Add the 'Please wait' text below the spinner -->
<p style="margin-top: 1rem; font-size: 1.2rem; color: #007bff;">Please wait...</p>
</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>
<!-- SweetAlert -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
$('#fromDate, #toDate').on('change', function () {
let fromDate = $('#fromDate').val();
let toDate = $('#toDate').val();
// Both dates selected
if (fromDate && toDate) {
if (new Date(fromDate) > new Date(toDate)) {
Swal.fire({
icon: 'warning',
title: 'Invalid Date Range',
text: 'From Date should not be greater than To Date'
});
$(this).val('');
return;
}
}
// Set min/max dynamically
$('#toDate').attr('min', fromDate);
$('#fromDate').attr('max', toDate);
});
$('#btnReset').click(function () {
$('#fromDate').val('');
$('#toDate').val('');
});
$('#btnSearch').click(function () {
var fromDate = $('#fromDate').val() || '';
var toDate = $('#toDate').val() || '';
loadZoneReport(fromDate,toDate);
});
function loadZoneReport(fromdate,todate) {
$('#pageLoader').show();
$.ajax({
url: '/commissionerreview/api/streetvendor/getzoneoveralldetails',
method: 'GET',
data:{fromdate:fromdate,todate:todate},
success: function(response) {
// Safely extract data whether it's an array or an object
var res = Array.isArray(response) ? response[0] : response;
console.log("Response:", res);
$('#pageLoader').hide();
if (res.status === "Success") {
if (Array.isArray(res.data) && res.data.length > 0) {
console.log("Details Array:", response);
let table = $('#ZoneOpDatatable').DataTable({
destroy: true,
autoWidth: true,
info: true,
data: res.data, // using response directly
columns: [
{
data: 'zone',
render: function (data, type, row) {
return `<a href="#" onclick="openZoneModal('${data}')" style="text-decoration: underline;">${data}</a>`;
}
},
{ data: 'application_received', defaultContent: '-', className: 'text-center' },
{ data: 'assistant_processed', defaultContent: '-', className: 'text-center' },
{ data: 'li_processed', defaultContent: '-', className: 'text-center' },
{ data: 'aro_processed', defaultContent: '-', className: 'text-center' },
{ data: 'ee_processed', defaultContent: '-', className: 'text-center' },
{ data: 'committee_processed', defaultContent: '-', className: 'text-center' },
{ data: 'total_processed', defaultContent: '-', className: 'text-center' },
{ data: 'pending', defaultContent: '-', className: 'text-center' },
{ data: 'processed_within_30_days', defaultContent: '-', className: 'text-center' }
],
dom: 'Bfrtip',
buttons: [
{
extend: 'excelHtml5',
text: 'Export Excel',
title: 'Street Vendor - Zone Wise Report',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'pdfHtml5',
text: 'Export PDF',
title: 'Street Vendor - Zone Wise Report',
orientation: 'landscape',
pageSize: 'A4',
exportOptions: {
columns: ':visible'
},
customize: function (doc) {
doc.styles.tableHeader.alignment = 'center';
doc.defaultStyle.fontSize = 9;
doc.content[1].table.widths = [
'*', '*', '*', '*', '*',
'*', '*', '*', '*', '*'
];
// Center align all table cells
let rowCount = doc.content[1].table.body.length;
for (let i = 0; i < rowCount; i++) {
let row = doc.content[1].table.body[i];
for (let j = 0; j < row.length; j++) {
row[j].alignment = 'center';
}
}
}
}
],
lengthMenu: [
[50, 100, 200, 500, -1],
['50 rows', '100 rows', '200 rows', 'Show all']
],
scrollY: 'auto',
scrollX: true,
responsive: false,
searching: false,
ordering: false
});
}
else if(Array.isArray(res.data) && res.data.length === 0) {
$('#pageLoader').hide();
//alert("No data available.");
Swal.fire({
icon: 'info',
title: 'No Data',
text: 'No data available.',
showConfirmButton: true,
timer: 3000
});
}
else {
$('#pageLoader').hide();
//alert("Error: " + response.message);
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Error',
showConfirmButton: true,
timer: 3000
});
}
}
else {
$('#pageLoader').hide();
//alert("Error: " + response.message);
Swal.fire({
icon: 'error',
title: 'Error',
text: res.message,
showConfirmButton: true,
timer: 3000
});
}
},
error: function(xhr, status, error) {
$('#pageLoader').hide();
// alert("Request failed: " + error);
Swal.fire({
icon: 'error',
title: 'Error',
text: error,
showConfirmButton: true,
timer: 3000
});
}
});
}
$(document).ready(function () {
loadZoneReport("","");
$('#zoneDetailsModal').on('shown.bs.modal', function () {
if ($.fn.DataTable.isDataTable('#WardDatatable')) {
let questionDatatable = $('#WardDatatable').DataTable();
questionDatatable.columns.adjust().draw();
}
});
});
function openZoneModal(zoneCode) {
console.log("Opening modal for zone:", zoneCode);
var fromDate = $('#fromDate').val() || '';
var toDate = $('#toDate').val() || '';
$('#pageLoader').show();
// Set data in modal (example)
$('#zoneModalTitle').text('Zone ' + zoneCode + ' Details');
$.ajax({
url: '/commissionerreview/api/streetvendor/getwardoveralldetails',
method: 'GET',
data:{fromdate:fromDate,todate:toDate,zone:zoneCode},
success: function(response) {
var res = Array.isArray(response) ? response[0] : response;
console.log("Response:", res);
$('#pageLoader').hide();
if (res.status === "Success") {
if (Array.isArray(res.data) && res.data.length > 0){
console.log("Details Array:", response);
let table = $('#WardDatatable').DataTable({
destroy: true,
autoWidth: true,
info: true,
data: res.data, // using response directly
columns: [
{ data: 'ward', defaultContent: '-', className: 'text-center' },
{ data: 'application_received', defaultContent: '-', className: 'text-center' },
{ data: 'assistant_processed', defaultContent: '-', className: 'text-center' },
{ data: 'li_processed', defaultContent: '-', className: 'text-center' },
{ data: 'aro_processed', defaultContent: '-', className: 'text-center' },
{ data: 'ee_processed', defaultContent: '-', className: 'text-center' },
{ data: 'committee_processed', defaultContent: '-', className: 'text-center' },
{ data: 'total_processed', defaultContent: '-', className: 'text-center' },
{ data: 'pending', defaultContent: '-', className: 'text-center' },
{ data: 'processed_within_30_days', defaultContent: '-', className: 'text-center' }
],
dom: 'Bfrtip',
lengthMenu: [
[50, 100, 200, 500, -1],
['50 rows', '100 rows', '200 rows', 'Show all']
],
scrollY: '400px',
scrollX: true,
responsive: false,
searching: false,
ordering: false,
buttons: [
{
extend: 'excelHtml5',
text: 'Export Excel',
title: 'Street Vendor - Zone: '+zoneCode+' ward wise Report',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'pdfHtml5',
text: 'Export PDF',
title: 'Street Vendor - Zone: '+zoneCode+' ward wise Report',
orientation: 'landscape',
pageSize: 'A4',
exportOptions: {
columns: ':visible'
},
customize: function (doc) {
doc.styles.tableHeader.alignment = 'center';
doc.defaultStyle.fontSize = 9;
doc.content[1].table.widths = [
'*', '*', '*', '*', '*',
'*', '*', '*', '*', '*'
];
// Center align all table cells
let rowCount = doc.content[1].table.body.length;
for (let i = 0; i < rowCount; i++) {
let row = doc.content[1].table.body[i];
for (let j = 0; j < row.length; j++) {
row[j].alignment = 'center';
}
}
}
}
]
});
$('#zoneDetailsModal').modal('show');
}
else if (Array.isArray(res.data) && res.data.length === 0){
$('#pageLoader').hide();
//alert("No data available.");
Swal.fire({
icon: 'info',
title: 'No Data',
text: 'No data available.',
showConfirmButton: true,
timer: 3000
});
}
else{
$('#pageLoader').hide();
//alert("Error: " + response.message);
Swal.fire({
icon: 'error',
title: 'Error',
text: res.message,
showConfirmButton: true,
timer: 3000
});
}
}
else {
$('#pageLoader').hide();
//alert("Error: " + response.message);
Swal.fire({
icon: 'error',
title: 'Error',
text: res.message,
showConfirmButton: true,
timer: 3000
});
}
},
error: function(xhr, status, error) {
$('#pageLoader').hide();
//alert("Request failed: " + error);
Swal.fire({
icon: 'error',
title: 'Error',
text: error,
showConfirmButton: true,
timer: 3000
});
}
});
}
</script>
</body>
</html>
Loading…
Cancel
Save