Added Import for Large CSV

This commit is contained in:
James Richie
2026-05-21 19:16:27 -04:00
parent 3c5401f82b
commit 8a3cb15e55
22 changed files with 1571 additions and 317 deletions
+67 -149
View File
@@ -2,12 +2,12 @@
class Consolidation extends Base { class Consolidation extends Base {
// Variables. // Variables
private $current_user_serial = 0; private $current_user_serial = 0;
private $current_user_id = ""; private $current_user_id = "";
private $current_user_name = ""; private $current_user_name = "";
private $current_user_email = ""; private $current_user_email = "";
// ================= // =================
// Class Constructor // Class Constructor
@@ -20,9 +20,9 @@ class Consolidation extends Base {
date_default_timezone_set(self::TIMEZONE); date_default_timezone_set(self::TIMEZONE);
$this->current_user_serial = $_SESSION[self::USER_SERIAL] ?? 0; $this->current_user_serial = $_SESSION[self::USER_SERIAL] ?? 0;
$this->current_user_id = $_SESSION[self::USER_ID] ?? ""; $this->current_user_id = $_SESSION[self::USER_ID] ?? "";
$this->current_user_name = $_SESSION[self::USER_NAME] ?? ""; $this->current_user_name = $_SESSION[self::USER_NAME] ?? "";
$this->current_user_email = $_SESSION[self::USER_EMAIL] ?? ""; $this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
} }
// ====================== // ======================
@@ -47,6 +47,11 @@ class Consolidation extends Base {
case "run": case "run":
$XSLParms = array();
$permit_record_count = 0;
$processed_record_count = 0;
$connection = null;
$sql_insert = "insert into rdiconsolidations $sql_insert = "insert into rdiconsolidations
( rdiconsolidation_permit_facts_id, ( rdiconsolidation_permit_facts_id,
@@ -86,149 +91,72 @@ class Consolidation extends Base {
rdiconsolidation_12mth_prev_permits, rdiconsolidation_12mth_prev_permits,
rdiconsolidation_date ) rdiconsolidation_date )
values ( :rdiconsolidation_permit_facts_id, select coalesce(permit_permit_facts_id, 0),
:rdiconsolidation_entry_date_str, coalesce(permit_entry_date_str, ''),
:rdiconsolidation_entry_date, coalesce(permit_entry_date, '0000-00-00'),
:rdiconsolidation_permit_number, coalesce(permit_permit_number, ''),
:rdiconsolidation_permit_date_str, coalesce(permit_permit_date_str, ''),
:rdiconsolidation_permit_date, coalesce(permit_permit_date, '0000-00-00'),
:rdiconsolidation_project_addr, coalesce(permit_project_addr, ''),
:rdiconsolidation_sub_div_name, coalesce(permit_sub_div_name, ''),
:rdiconsolidation_project_type, coalesce(permit_project_type, ''),
:rdiconsolidation_company, coalesce(permit_company, ''),
:rdiconsolidation_address, coalesce(permit_address, ''),
:rdiconsolidation_city, coalesce(permit_city, ''),
:rdiconsolidation_state, coalesce(permit_state, ''),
:rdiconsolidation_zip, coalesce(permit_zip, ''),
:rdiconsolidation_phone, coalesce(permit_phone, ''),
:rdiconsolidation_project_city, coalesce(permit_project_city, ''),
:rdiconsolidation_project_state, coalesce(permit_project_state, ''),
:rdiconsolidation_size, coalesce(permit_size, 0),
:rdiconsolidation_units, coalesce(permit_units, 0),
:rdiconsolidation_building_type, coalesce(permit_building_type, 0),
:rdiconsolidation_value, coalesce(permit_value, 0),
:rdiconsolidation_work_type, coalesce(permit_work_type, 0),
:rdiconsolidation_county, coalesce(permit_county, ''),
:rdiconsolidation_lot, coalesce(permit_lot, ''),
:rdiconsolidation_dist_ll, coalesce(permit_dist_ll, ''),
:rdiconsolidation_ct1, coalesce(permit_ct1, 0),
:rdiconsolidation_owner_name, coalesce(permit_owner_name, ''),
:rdiconsolidation_owner_address, coalesce(permit_owner_address, ''),
:rdiconsolidation_owner_city, coalesce(permit_owner_city, ''),
:rdiconsolidation_owner_state, coalesce(permit_owner_state, ''),
:rdiconsolidation_owner_zip, coalesce(permit_owner_zip, ''),
:rdiconsolidation_owner_phone, coalesce(permit_owner_phone, ''),
:rdiconsolidation_ytd_permits, coalesce(permit_ytd_permits, 0),
:rdiconsolidation_ytd_value, coalesce(permit_ytd_value, 0),
:rdiconsolidation_12mth_prev_permits, coalesce(permit_12mth_prev_permits, 0),
:rdiconsolidation_date ) "; current_date()
from permits";
$connection = $this->connect(); try {
$connection->beginTransaction(); $connection = $this->connect();
$statement = $connection->prepare($sql_insert);
$rdi_records = $this->getRdiTable(); $connection->beginTransaction();
$rdi_record_count = $rdi_records->count();
$processed_record_count = 0; $permit_record_count = (integer) $connection->query("select count(*) from permits")->fetchColumn();
$processed_record_count = (integer) $connection->exec($sql_insert);
foreach ($rdi_records as $record) { if ($processed_record_count !== $permit_record_count) {
throw new RuntimeException("Record counts did not match. Rolling back database. Contact Support.");
$rdiconsolidation_permit_facts_id = (integer) $record->permit_facts_id ?: 0;
$rdiconsolidation_entry_date_str = (integer) $record->entry_date_str ?: "";
$rdiconsolidation_entry_date = (string) $record->entry_date ?: "0000-00-00";
$rdiconsolidation_permit_number = (string) $record->permit_number ?: "";
$rdiconsolidation_permit_date_str = (string) $record->permit_date_str ?: "";
$rdiconsolidation_permit_date = (string) $record->permit_date ?: "0000-00-00";
$rdiconsolidation_project_addr = (string) $record->project_addr ?: "";
$rdiconsolidation_sub_div_name = (string) $record->sub_div_name ?: "";
$rdiconsolidation_project_type = (string) $record->project_type ?: "";
$rdiconsolidation_company = (string) $record->company ?: "";
$rdiconsolidation_address = (string) $record->address ?: "";
$rdiconsolidation_city = (string) $record->city ?: "";
$rdiconsolidation_state = (string) $record->state ?: "";
$rdiconsolidation_zip = (string) $record->zip ?: "";
$rdiconsolidation_phone = (string) $record->phone ?: "";
$rdiconsolidation_project_city = (string) $record->project_city ?: "";
$rdiconsolidation_project_state = (string) $record->project_state ?: "";
$rdiconsolidation_size = (integer) $record->size ?: 0;
$rdiconsolidation_units = (integer) $record->units ?: 0;
$rdiconsolidation_building_type = (integer) $record->building_type ?: 0;
$rdiconsolidation_value = (integer) $record->value ?: 0;
$rdiconsolidation_work_type = (integer) $record->work_type ?: 0;
$rdiconsolidation_county = (string) $record->county ?: "";
$rdiconsolidation_lot = (string) $record->lot ?: "";
$rdiconsolidation_dist_ll = (string) $record->dist_ll ?: "";
$rdiconsolidation_ct1 = (integer) $record->ct1 ?: 0;
$rdiconsolidation_owner_name = (string) $record->owner_name ?: "";
$rdiconsolidation_owner_address = (string) $record->owner_address ?: "";
$rdiconsolidation_owner_city = (string) $record->owner_city ?: "";
$rdiconsolidation_owner_state = (string) $record->owner_state ?: "";
$rdiconsolidation_owner_zip = (string) $record->owner_zip ?: "";
$rdiconsolidation_owner_phone = (string) $record->owner_phone ?: "";
$rdiconsolidation_ytd_permits = (integer) $record->ytd_permits ?: 0;
$rdiconsolidation_ytd_value = (integer) $record->ytd_value ?: 0;
$rdiconsolidation_12mth_prev_permits = (integer) $record->{"12mth_prev_permits"} ?: 0;
$rdiconsolidation_date = date("Y-m-d");
$statement->bindValue(":rdiconsolidation_permit_facts_id", $rdiconsolidation_permit_facts_id);
$statement->bindValue(":rdiconsolidation_entry_date_str", $rdiconsolidation_entry_date_str);
$statement->bindValue(":rdiconsolidation_entry_date", $rdiconsolidation_entry_date);
$statement->bindValue(":rdiconsolidation_permit_number", $rdiconsolidation_permit_number);
$statement->bindValue(":rdiconsolidation_permit_date_str", $rdiconsolidation_permit_date_str);
$statement->bindValue(":rdiconsolidation_permit_date", $rdiconsolidation_permit_date);
$statement->bindValue(":rdiconsolidation_project_addr", $rdiconsolidation_project_addr);
$statement->bindValue(":rdiconsolidation_sub_div_name", $rdiconsolidation_sub_div_name);
$statement->bindValue(":rdiconsolidation_project_type", $rdiconsolidation_project_type);
$statement->bindValue(":rdiconsolidation_company", $rdiconsolidation_company);
$statement->bindValue(":rdiconsolidation_address", $rdiconsolidation_address);
$statement->bindValue(":rdiconsolidation_city", $rdiconsolidation_city);
$statement->bindValue(":rdiconsolidation_state", $rdiconsolidation_state);
$statement->bindValue(":rdiconsolidation_zip", $rdiconsolidation_zip);
$statement->bindValue(":rdiconsolidation_phone", $rdiconsolidation_phone);
$statement->bindValue(":rdiconsolidation_project_city", $rdiconsolidation_project_city);
$statement->bindValue(":rdiconsolidation_project_state", $rdiconsolidation_project_state);
$statement->bindValue(":rdiconsolidation_size", $rdiconsolidation_size);
$statement->bindValue(":rdiconsolidation_units", $rdiconsolidation_units);
$statement->bindValue(":rdiconsolidation_building_type", $rdiconsolidation_building_type);
$statement->bindValue(":rdiconsolidation_value", $rdiconsolidation_value);
$statement->bindValue(":rdiconsolidation_work_type", $rdiconsolidation_work_type);
$statement->bindValue(":rdiconsolidation_county", $rdiconsolidation_county);
$statement->bindValue(":rdiconsolidation_lot", $rdiconsolidation_lot);
$statement->bindValue(":rdiconsolidation_dist_ll", $rdiconsolidation_dist_ll);
$statement->bindValue(":rdiconsolidation_ct1", $rdiconsolidation_ct1);
$statement->bindValue(":rdiconsolidation_owner_name", $rdiconsolidation_owner_name);
$statement->bindValue(":rdiconsolidation_owner_address", $rdiconsolidation_owner_address);
$statement->bindValue(":rdiconsolidation_owner_city", $rdiconsolidation_owner_city);
$statement->bindValue(":rdiconsolidation_owner_state", $rdiconsolidation_owner_state);
$statement->bindValue(":rdiconsolidation_owner_zip", $rdiconsolidation_owner_zip);
$statement->bindValue(":rdiconsolidation_owner_phone", $rdiconsolidation_owner_phone);
$statement->bindValue(":rdiconsolidation_ytd_permits", $rdiconsolidation_ytd_permits);
$statement->bindValue(":rdiconsolidation_ytd_value", $rdiconsolidation_ytd_value);
$statement->bindValue(":rdiconsolidation_12mth_prev_permits", $rdiconsolidation_12mth_prev_permits);
$statement->bindValue(":rdiconsolidation_date", $rdiconsolidation_date);
if (!$statement->execute()) {
$connection->rollBack();
$this->debug($connection->errorInfo()[2]);
} }
$processed_record_count++;
}
if ($processed_record_count !== $rdi_record_count) {
$connection->rollBack();
$XSLParms["CONSOLIDATION_ERROR"] = "Record Counts Did not match. Rolling Back Database. Contact Support.";
} else {
$connection->commit(); $connection->commit();
$this->executeSQL("truncate rdis"); $connection->exec("truncate permits");
} catch (Exception $e) {
if (($connection instanceof PDO) && $connection->inTransaction()) {
$connection->rollBack();
}
$XSLParms["CONSOLIDATION-ERROR"] = $e->getMessage();
} }
$XSLParms["RDI-RECORD-COUNT"] = $rdi_record_count; $XSLParms["RDI-RECORD-COUNT"] = $permit_record_count;
$XSLParms["PROCESSED-RECORD-COUNT"] = $processed_record_count; $XSLParms["PROCESSED-RECORD-COUNT"] = $processed_record_count;
$content = $this->applyXSL("", $this->getXSL("consolidationResults"), $XSLParms); $content = $this->applyXSL("", $this->getXSL("consolidationResults"), $XSLParms);
$this->displayContent($content); $this->displayContent($content);
break; break;
@@ -239,16 +167,6 @@ class Consolidation extends Base {
} }
} }
// ====================
// Return an RDI Object
// ====================
public function getRdiTable() {
$SQL = "SELECT * FROM permits";
return $this->getTable("rdis", $SQL);
}
} }
?> ?>
+157 -82
View File
@@ -45,7 +45,11 @@ class Import extends Base {
case "prompt": case "prompt":
$content = $this->applyXSL("", $this->getXSL("uploadCSVFile")); $import_type = filter_input(INPUT_POST, "import_type") ?: "incremental";
$XSLParms['IMPORT-TYPE'] = $import_type;
$content = $this->applyXSL("", $this->getXSL("uploadCSVFile"), $XSLParms);
$this->displayContent($content); $this->displayContent($content);
break; break;
@@ -54,6 +58,7 @@ class Import extends Base {
$XSLParms['UPLOAD_STATUS'] = "UPLOAD SUCCESSFUL"; $XSLParms['UPLOAD_STATUS'] = "UPLOAD SUCCESSFUL";
$environment = (string) $this->getSettings()->Environment ?: "Production"; $environment = (string) $this->getSettings()->Environment ?: "Production";
$upload_type = filter_input(INPUT_POST, 'upload_type') ?: "incremental";
if (empty($environment)) { if (empty($environment)) {
$this->logError("Invalid Environment ({$environment}). " . __METHOD__); $this->logError("Invalid Environment ({$environment}). " . __METHOD__);
@@ -63,30 +68,57 @@ class Import extends Base {
// Process the Attachment // Process the Attachment
if (!empty($_FILES)) { if (empty($_FILES["csvfile"])) {
$this->displayNotice("No CSV file was uploaded.");
return;
}
if (!is_dir($environment_directory)) { if (!is_dir($environment_directory) && !mkdir($environment_directory, 0775, true)) {
mkdir($environment_directory); $this->displayNotice("Unable to create the upload directory. Please contact Support.");
} return;
}
$tempname = $_FILES["csvfile"]["tmp_name"]; if ($_FILES["csvfile"]["error"] !== UPLOAD_ERR_OK) {
$filename = $_FILES["csvfile"]["name"]; $this->displayNotice("The CSV file upload failed. Please try again.");
return;
}
if (empty($tempname) or empty($filename)) { $tempname = $_FILES["csvfile"]["tmp_name"];
return; $filename = basename($_FILES["csvfile"]["name"]);
}
if (!move_uploaded_file($tempname, "{$environment_directory}/{$filename}")) { if (empty($tempname) or empty($filename)) {
$XSLParms['UPLOAD_STATUS'] = "UPLOAD FAILED"; $this->displayNotice("The uploaded CSV file could not be read. Please try again.");
} return;
} }
$file_directory = "{$environment_directory}/{$filename}"; $file_directory = "{$environment_directory}/{$filename}";
$import = $this->parseCSVFile($file_directory); if (!move_uploaded_file($tempname, $file_directory)) {
$this->displayNotice("Unable to save the uploaded CSV file. Please contact Support.");
return;
}
$content = $this->applyXSL("", $this->getXSL("uploadMessage"), $XSLParms); if ($upload_type === 'incremental') {
$this->displayContent($content); $this->parseCSVFile($file_directory);
$content = $this->applyXSL("", $this->getXSL("uploadMessage"), $XSLParms);
$this->displayContent($content);
return;
}
if ($upload_type === 'full') {
$successful = $this->createImportRequest($file_directory);
if (!$successful) {
$this->displayNotice("An error occurred while creating the import request. Please contact Support.");
return;
}
$this->displayNotice("Your import has been queued. You will receive a notification when it is complete.");
return;
}
$this->displayNotice("Invalid upload type. Please contact Support.");
break; break;
} }
@@ -143,73 +175,74 @@ class Import extends Base {
$County = (new Counties())->getCountiesByName($County)->record->county_serial; $County = (new Counties())->getCountiesByName($County)->record->county_serial;
$SQL = "INSERT INTO permits ( $SQL = "INSERT INTO permits (
permit_permit_facts_id, permit_permit_facts_id,
permit_entry_date, permit_entry_date,
permit_permit_number, permit_permit_number,
permit_permit_date, permit_permit_date,
permit_project_addr, permit_project_addr,
permit_sub_div_name, permit_sub_div_name,
permit_project_type, permit_project_type,
permit_company, permit_company,
permit_address, permit_address,
permit_city, permit_city,
permit_state, permit_state,
permit_zip, permit_zip,
permit_phone, permit_phone,
permit_project_city, permit_project_city,
permit_project_state, permit_project_state,
permit_size, permit_size,
permit_units, permit_units,
permit_building_type, permit_building_type,
permit_value, permit_value,
permit_work_type, permit_work_type,
permit_county, permit_county,
permit_lot, permit_lot,
permit_dist_ll, permit_dist_ll,
permit_ct1, permit_ct1,
permit_owner_name, permit_owner_name,
permit_owner_address, permit_owner_address,
permit_owner_city, permit_owner_city,
permit_owner_state, permit_owner_state,
permit_owner_zip, permit_owner_zip,
permit_owner_phone, permit_owner_phone,
permit_ytd_permits, permit_ytd_permits,
permit_ytd_value, permit_ytd_value,
permit_12mth_prev_permits ) permit_12mth_prev_permits )
VALUES ( VALUES (
:permit_permit_facts_id, :permit_permit_facts_id,
:permit_entry_date, :permit_entry_date,
:permit_permit_number, :permit_permit_number,
:permit_permit_date, :permit_permit_date,
:permit_project_addr, :permit_project_addr,
:permit_sub_div_name, :permit_sub_div_name,
:permit_project_type, :permit_project_type,
:permit_company, :permit_company,
:permit_address, :permit_address,
:permit_city, :permit_city,
:permit_state, :permit_state,
:permit_zip, :permit_zip,
:permit_phone, :permit_phone,
:permit_project_city, :permit_project_city,
:permit_project_state, :permit_project_state,
:permit_size, :permit_size,
:permit_units, :permit_units,
:permit_building_type, :permit_building_type,
:permit_value, :permit_value,
:permit_work_type, :permit_work_type,
:permit_county, :permit_county,
:permit_lot, :permit_lot,
:permit_dist_ll, :permit_dist_ll,
:permit_ct1, :permit_ct1,
:permit_owner_name, :permit_owner_name,
:permit_owner_address, :permit_owner_address,
:permit_owner_city, :permit_owner_city,
:permit_owner_state, :permit_owner_state,
:permit_owner_zip, :permit_owner_zip,
:permit_owner_phone, :permit_owner_phone,
:permit_ytd_permits, :permit_ytd_permits,
:permit_ytd_value, :permit_ytd_value,
:permit_12mth_prev_permits )"; :permit_12mth_prev_permits )";
$statement = $connection->prepare($SQL); $statement = $connection->prepare($SQL);
@@ -255,6 +288,48 @@ class Import extends Base {
return true; return true;
} }
// =====================
// Create Import Request
// =====================
public function createImportRequest($filepath = "") {
if (!$filepath) {
return false;
}
$SQL = "insert into importjobs
( importjob_file_path,
importjob_notified,
importjob_creator,
importjob_created )
VALUES ( :importjob_file_path,
:importjob_notified,
:importjob_creator,
:importjob_created )";
$statement = $this->connect()->prepare($SQL);
$statement->bindValue(":importjob_file_path", "../{$filepath}", PDO::PARAM_STR);
$statement->bindValue(":importjob_notified", false, PDO::PARAM_BOOL);
$statement->bindValue(":importjob_creator", $this->current_user_name, PDO::PARAM_STR);
$statement->bindValue(":importjob_created", $this->getTimestamp());
if (!$statement->execute()) {
return false;
}
$to = $this->getSettings()->DeveloperEmail;
$subject = "CSV Import Jobs Queued";
$message = "Import initialized successfully. Processing will start momentarily.";
(new Email())->sendEmail($to, $subject, $message);
return true;
}
// ======================== // ========================
// Return an Formatted Date // Return an Formatted Date
// ======================== // ========================
+255
View File
@@ -0,0 +1,255 @@
<?php
// ======================
// Import Large CSV Class
// ======================
class ImportLargeCSV extends Base {
// Variables
private $current_user_serial = 0;
private $current_user_id = "";
private $current_user_name = "";
private $current_user_email = "";
private $pagination_size = 20;
private $pagination_page = 1;
// =================
// Class Constructor
// =================
public function __construct() {
set_error_handler(array($this, "displayError"));
date_default_timezone_set(self::TIMEZONE);
$this->current_user_serial = $_SESSION[self::USER_SERIAL] ?? 0;
$this->current_user_id = $_SESSION[self::USER_ID] ?? "";
$this->current_user_name = $_SESSION[self::USER_NAME] ?? "";
$this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
$this->pagination_page = $_SESSION[self::PAGINATION_PAGE] ?? 1;
}
// =====================
// Create Import Request
// =====================
public function createImportRequest() {
$subscription_serial = filter_input(INPUT_POST, "subscription_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
$subscription = (new Subscriptions())->getSubscription($subscription_serial);
if ($subscription->count() === 0) {
$this->logError("Invalid Subscription ({$subscription_serial}): " . __METHOD__);
}
$import_type = filter_input(INPUT_POST, "import_type") ?: "";
$report_type = filter_input(INPUT_POST, "report_type") ?: "";
$importjob_import_type = str_replace(' ', '', $subscription->record->subscription_full_title) . "$import_type";
$importjob_file_name = str_replace(' ', '_', $subscription->record->subscription_full_title) . "_all_records.{$report_type}";
// $importjob_file_name = str_replace('&', 'And', $subscription->record->subscription_full_title) . "_all_records.{$report_type}";
$SQL = "insert into importjobs
( importjob_subscription,
importjob_import_type,
importjob_file_name,
importjob_file_type,
importjob_creator,
importjob_created )
VALUES ( :importjob_subscription,
:importjob_import_type,
:importjob_file_name,
:importjob_file_type,
:importjob_creator,
:importjob_created )";
$statement = $this->connect()->prepare($SQL);
$statement->bindValue(":importjob_subscription", $subscription_serial);
$statement->bindValue(":importjob_import_type", $importjob_import_type);
$statement->bindValue(":importjob_file_name", $importjob_file_name);
$statement->bindValue(":importjob_file_type", strtoupper($report_type));
$statement->bindValue(":importjob_creator", $this->current_user_serial);
$statement->bindValue(":importjob_created", $this->getTimestamp());
$statement->execute();
$this->displayNotice("Your import has been queued. You will receive an notification when it is ready.");
}
// ============
// User Imports
// ============
public function usersImports() {
$imports = $this->getUsersImports();
$popovers = (new Popovers())->getPopovers();
$this->markImportsAsSeen();
$XML = $this->mergeXML($imports, "<XML/>");
$XML = $this->mergeXML($popovers, $XML);
$html = $this->applyXSL($XML, $this->getXSL("viewImports"));
$this->displayContent($html);
}
// =========================
// Mark User Imports as Seen
// =========================
public function markImportsAsSeen() {
$SQL = "update importjobs
set importjob_notified = true
where importjobs.importjob_creator = {$this->current_user_serial}";
$statement = $this->connect()->prepare($SQL);
$statement->execute();
}
// =======================
// Return a Imports Object
// =======================
public function getUsersImports() {
// Find Criteria
$find_importjob = filter_input(INPUT_POST, "find_importjob") ?: "";
$find_words = $this->sanitizeString($find_importjob);
// Show Per Page
$this->pagination_size = filter_input(INPUT_POST, "importjobs_per_page", FILTER_SANITIZE_NUMBER_INT) ?: $this->pagination_size;
// Record Count
$connection = $this->connect();
$SQL = "select count(*) as count
from view_importjobs
where view_importjobs.importjob_creator = {$this->current_user_serial}
and view_importjobs.subscription_full_title regexp :subscription_full_title";
$statement = $connection->prepare($SQL);
$statement->bindValue(":subscription_full_title", $find_words);
$statement->execute();
$recordset = $statement->fetch(PDO::FETCH_ASSOC);
$count = (count($recordset) > 0) ? (integer) $recordset["count"] : (integer) 0;
// Pagination
$pagination = filter_input(INPUT_POST, "pagination") ?: "";
switch ($pagination) {
case "first" : $this->pagination_page = 1;
break;
case "previous" : $this->pagination_page = max(($this->pagination_page - 1), 1);
break;
case "next" : $this->pagination_page = ($this->pagination_page + 1);
break;
case "last" : $this->pagination_page = ($count / $this->pagination_size);
break;
}
$offset = (($this->pagination_page * $this->pagination_size) - $this->pagination_size);
$start = ($offset + 1);
$end = min($count, (($start + $this->pagination_size) - 1));
// Data
$SQL = "select view_importjobs.*
from view_importjobs
where view_importjobs.importjob_creator = {$this->current_user_serial}
and view_importjobs.subscription_full_title regexp :subscription_full_title
limit {$this->pagination_size}
offset {$offset}";
$statement = $connection->prepare($SQL);
$statement->bindValue(":subscription_full_title", $find_words);
$statement->execute();
$importjobs = $this->getTableXML("importjobs", $statement);
// Insert Pagination Attributes
$importjobs->addAttribute("count", $count);
$importjobs->addAttribute("start", $start);
$importjobs->addAttribute("end", $end);
$importjobs->addAttribute("page", $this->pagination_page);
$_SESSION[self::PAGINATION_PAGE] = $this->pagination_page;
return $importjobs;
}
// =====================================
// Return if users imports are completed
// =====================================
public function checkUsersCompletedImports_AJAX() {
$SQL = "select count(*) as notify
from view_importjobs
where view_importjobs.importjob_creator = :current_user_serial
and view_importjobs.importjob_status = 'complete'
and view_importjobs.importjob_notified = 0";
$importjobs = $this->getTable("importjobs", $SQL, array('current_user_serial' => $this->current_user_serial));
// Create the User Object.
$users_imports = array();
$users_imports["notify"] = ($importjobs->record->notify == 0) ? false : true;
if ($_SESSION[self::AJAX]) {
echo json_encode($users_imports);
} else {
return $users_imports;
}
}
// ===============
// Download Import
// ===============
public function downloadImport() {
$import_code = filter_input(INPUT_POST, "import_code") ?: "";
$SQL = "select view_importjobs.*
from view_importjobs
where view_importjobs.importjob_download_token = :import_code
and view_importjobs.importjob_status = 'complete' limit 1";
$importjob = $this->getTable("importjob", $SQL, array("import_code" => $import_code));
$file_path = str_replace('../imports/', './imports/', $importjob->record->importjob_file_path);
$file_name = $importjob->record->importjob_file_name;
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=\"$file_name\"");
readfile($file_path);
}
}
?>
+584
View File
@@ -0,0 +1,584 @@
<?php
declare(strict_types=1);
$basePath = realpath(__DIR__ . '/..');
require_once $basePath . '/vendor/autoload.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
// =====================
// Process_ImportRequest
// =====================
class Process_ImportRequest {
// Variables
private $log = "";
private $settings = "";
private $connection = "";
// ==================
// Class Constructor.
// ==================
public function __construct() {
$basePath = realpath(__DIR__ . '/..');
$logfile = $basePath . "/scripts/importrequests.log";
// ==========================
// Log Rotation (50 MB limit)
// ==========================
$maxLogSize = 50 * 1024 * 1024; // 50 MB
if (file_exists($logfile) && filesize($logfile) >= $maxLogSize) {
$rotated = $logfile . '-';
rename($logfile, $rotated);
}
$this->log = fopen($logfile, "a");
if ($this->log === false) {
error_log("Process_ImportRequest : Unable to open log file");
exit();
}
$this->log(str_repeat("*", 50));
// Get Settings
$this->settings = new SimpleXMLElement((file_exists($basePath . "/xml/settings.xml") ? file_get_contents($basePath . "/xml/settings.xml") : "<settings/>"));
if ($this->settings->count() === 0) {
error_log("Process_ImportRequest : Unable to initialize settings");
exit();
}
// Open Database Connections
$this->connection = $this->connect();
}
// ================================================
// Return a New DEC-DR Database Database Connection
// ================================================
public function connect() {
if ($this->settings->Environment == 'Production') {
$host = 'localhost';
} else {
$host = '192.168.1.190';
}
try {
$database = (string) $this->settings->DatabaseName;
$user = (string) $this->settings->DatabaseUser;
$password = (string) $this->settings->DatabasePassword;
$connection = new PDO("mysql:host={$host};dbname={$database};charset=utf8", $user, $password);
$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException | Exception $e) {
trigger_error($e->getMessage());
}
return $connection;
}
// =============================
// Log a message to the Log File
// =============================
private function log($message = "") {
fwrite($this->log, date("Y-m-d H:i A") . " : " . "{$message} \n");
}
// ===================
// Process Parcel Data
// ===================
public function process() {
$this->log("Starting All Record Import Job");
$this->runImportJob();
$this->purgeImportJobs();
$this->log("Completed All Record Import Job");
}
// ==============
// Run Import Job
// ==============
private function runImportJob() {
$this->log("Processing Import Job");
$nextjob = $this->claimNextJob();
if (!$nextjob) {
$this->log("Nothing to do.");
return;
}
$importjob_serial = (int) $nextjob['importjob_serial'];
$importjob_file_path = (string) $nextjob['importjob_file_path'];
try {
$success = $this->parseCSVFile($importjob_file_path);
$SQL = "UPDATE importjobs
SET importjob_status = 'complete',
importjob_completed = NOW()
WHERE importjob_serial = :importjob_serial ";
$update = $this->connect()->prepare($SQL);
$update->execute([':importjob_serial' => $importjob_serial]);
} catch (Throwable $e) {
$SQL = "UPDATE importjobs
SET importjob_status = 'failed',
importjob_completed = NOW(),
importjob_error_message = :importjob_error_message
WHERE importjob_serial = :importjob_serial";
$fail = $this->connect()->prepare($SQL);
$fail->execute([':importjob_serial' => $importjob_serial, ':importjob_error_message' => $e->getMessage()]);
// Notify Admins of error
$this->sendWebHookAlert("Import Job {$importjob_serial} has failed. Check Error Logs for details.");
$this->email_admins("Import Job Failure.", "Import Job {$importjob_serial} has failed. Check Error Logs for details.");
}
}
// =================================
// Purge Imports Older then 24 Hours
// =================================
private function purgeImportJobs() {
$this->log("Purging Import Jobs Database Files");
$this->purgeFiles();
}
// =======================
// Claim Next Job in Queue
// =======================
private function claimNextJob() {
$this->connection->beginTransaction();
$SQL = "SELECT view_importjobs.*
FROM view_importjobs
WHERE view_importjobs.importjob_status = 'queued'
ORDER BY view_importjobs.importjob_created ASC
LIMIT 1 FOR UPDATE";
$statement = $this->connection->prepare($SQL);
$statement->execute();
$job = $statement->fetch(PDO::FETCH_ASSOC);
if (!$job) {
$this->connection->commit();
return null;
}
$SQL = "UPDATE importjobs
SET importjob_status = 'running'
WHERE importjob_serial = :importjob_serial";
$statement = $this->connection->prepare($SQL);
$statement->execute([':importjob_serial' => (int) $job['importjob_serial']]);
$this->connection->commit();
return $job;
}
// ====================
// Purges Files on Disk
// ====================
private function purgeFiles() {
$SQL = "select *
from importjobs
where importjobs.importjob_created < (NOW() - INTERVAL 24 HOUR)";
$importfiles = $this->connect()->query($SQL)->fetchAll(PDO::FETCH_ASSOC);
if (count($importfiles) == 0) {
$this->log("Nothing To Purge");
return;
}
foreach ($importfiles as $files) {
$filename = $files['importjob_file_path'];
unlink($filename);
}
$baseDir = './imports';
foreach (new DirectoryIterator($baseDir) as $item) {
if ($item->isDot() || !$item->isDir()) {
continue;
}
$folderPath = $item->getPathname();
// Check if directory is empty
$files = scandir($folderPath);
if ($files !== false && count($files) === 2) {
rmdir($folderPath);
}
}
$this->log("Purging Import Jobs Database Records");
$SQL = "delete from importjobs where importjobs.importjob_created < (NOW() - INTERVAL 24 HOUR)";
$this->connection->query($SQL);
}
// =======================
// Format Raw Phone Number
// =======================
public function formatPhoneNumber(string $phonenumber): string {
// Remove everything except digits
$digits = preg_replace('/\D+/', '', $phonenumber);
// Handle leading country code (US)
if (strlen($digits) === 11 && str_starts_with($digits, '1')) {
$digits = substr($digits, 1);
}
return substr($digits, 0, 3) . '-' . substr($digits, 3, 3) . '-' . substr($digits, 6, 4);
}
// ==============
// Parse CSV File
// ==============
public function parseCSVFile($file) {
$csvfile = fopen($file, 'r');
$RecordCount = 0;
while (($data = fgetcsv($csvfile, 0, ",", '"', '\\')) !== FALSE) {
$PermitFactsId = $data[1];
$EntryDate = $this->fixDate($data[2]);
$PermitNumber = $data[4];
$PermitDate = $this->fixDate($data[5]);
$ProjectAddress = $data[7];
$SubdivisionName = $data[8];
$ProjectType = $data[9];
$Company = substr($data[10], 0, 30);
$Address = substr($data[11], 0, 30);
$City = $data[12];
$State = $data[13];
$Zip = $data[14];
$Phone = $data[15];
$ProjectCity = $data[16];
$ProjectState = $data[17];
$Size = $data[18] ?: 0;
$Units = $data[19];
$BuildingType = $data[20];
$Value = $data[21] ?: 0;
$WorkType = $data[22];
$County = $data[23] ?: 0;
$Lot = $data[24];
$DistrictLL = $data[25];
$Ctl = $data[26];
$OwnerName = substr($data[27], 0, 30);
$OwnerAddress = substr($data[28], 0, 30);
$OwnerCity = $data[29];
$OwnerState = substr($data[30], 0, 2);
$OwnerZip = $data[31];
$OwnerPhone = $data[32];
$YTDPermits = $data[33] ?: 0;
$YTDValue = $data[34] ?: 0;
$TwelveMonthPreviousPermits = $data[35] ?: 0;
$County = $this->getCountiesByName($County)['county_serial'] ?? 0;
$SQL = "INSERT INTO permits (
permit_permit_facts_id,
permit_entry_date,
permit_permit_number,
permit_permit_date,
permit_project_addr,
permit_sub_div_name,
permit_project_type,
permit_company,
permit_address,
permit_city,
permit_state,
permit_zip,
permit_phone,
permit_project_city,
permit_project_state,
permit_size,
permit_units,
permit_building_type,
permit_value,
permit_work_type,
permit_county,
permit_lot,
permit_dist_ll,
permit_ct1,
permit_owner_name,
permit_owner_address,
permit_owner_city,
permit_owner_state,
permit_owner_zip,
permit_owner_phone,
permit_ytd_permits,
permit_ytd_value,
permit_12mth_prev_permits )
VALUES (
:permit_permit_facts_id,
:permit_entry_date,
:permit_permit_number,
:permit_permit_date,
:permit_project_addr,
:permit_sub_div_name,
:permit_project_type,
:permit_company,
:permit_address,
:permit_city,
:permit_state,
:permit_zip,
:permit_phone,
:permit_project_city,
:permit_project_state,
:permit_size,
:permit_units,
:permit_building_type,
:permit_value,
:permit_work_type,
:permit_county,
:permit_lot,
:permit_dist_ll,
:permit_ct1,
:permit_owner_name,
:permit_owner_address,
:permit_owner_city,
:permit_owner_state,
:permit_owner_zip,
:permit_owner_phone,
:permit_ytd_permits,
:permit_ytd_value,
:permit_12mth_prev_permits )";
$statement = $this->connection->prepare($SQL);
$statement->bindValue(":permit_permit_facts_id", $PermitFactsId);
$statement->bindValue(":permit_permit_number", $PermitNumber);
$statement->bindValue(":permit_entry_date", $EntryDate);
$statement->bindValue(":permit_permit_date", $PermitDate);
$statement->bindValue(":permit_project_addr", $ProjectAddress);
$statement->bindValue(":permit_sub_div_name", $SubdivisionName);
$statement->bindValue(":permit_project_type", $ProjectType);
$statement->bindValue(":permit_company", $Company);
$statement->bindValue(":permit_address", $Address);
$statement->bindValue(":permit_city", $City);
$statement->bindValue(":permit_state", $State);
$statement->bindValue(":permit_zip", $Zip);
$statement->bindValue(":permit_phone", $Phone);
$statement->bindValue(":permit_project_city", $ProjectCity);
$statement->bindValue(":permit_project_state", $ProjectState);
$statement->bindValue(":permit_size", $Size);
$statement->bindValue(":permit_units", $Units);
$statement->bindValue(":permit_building_type", $BuildingType);
$statement->bindValue(":permit_value", $Value);
$statement->bindValue(":permit_work_type", $WorkType);
$statement->bindValue(":permit_county", $County);
$statement->bindValue(":permit_lot", $Lot);
$statement->bindValue(":permit_dist_ll", $DistrictLL);
$statement->bindValue(":permit_ct1", $Ctl);
$statement->bindValue(":permit_owner_name", $OwnerName);
$statement->bindValue(":permit_owner_address", $OwnerAddress);
$statement->bindValue(":permit_owner_city", $OwnerCity);
$statement->bindValue(":permit_owner_state", $OwnerState);
$statement->bindValue(":permit_owner_zip", $OwnerZip);
$statement->bindValue(":permit_owner_phone", $OwnerPhone);
$statement->bindValue(":permit_ytd_permits", $YTDPermits);
$statement->bindValue(":permit_ytd_value", $YTDValue);
$statement->bindValue(":permit_12mth_prev_permits", $TwelveMonthPreviousPermits);
$statement->execute();
$RecordCount++;
}
return true;
}
// ========================
// Return a Counties Object
// ========================
public function getCountiesByName($county_name) {
$SQL = "select view_counties.*
from view_counties
where view_counties.county_name = '{$county_name}'";
$statement = $this->connection->prepare($SQL);
$statement->execute();
$county_name = $statement->fetch(PDO::FETCH_ASSOC);
return $county_name;
}
// ==========================
// Send an Alert/Notification
// ==========================
public function sendWebHookAlert($message, $title = "🚨 DEC Application Alert", $alerttype = "1fff00") {
$webhookUrl = (string) $this->settings->DiscordWebHookURL;
$data = [
'embeds' => [[
'title' => $title,
'description' => $message,
'color' => hexdec($alerttype),
'footer' => [
'text' => 'Real Estate Data Inc. System Notificiation',
],
'timestamp' => date('c'), // ISO 8601 format
]]
];
$jsonData = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ch = curl_init($webhookUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
$response = curl_exec($ch);
if ($response === false) {
$this->log('Curl error: ' . curl_error($ch));
}
curl_close($ch);
}
// ============
// Email Admins
// ============
public function email_admins($subject = "", $message = "") {
$this->log(date("Y-m-d H:i:s") . " - Emailing Admins...");
$settings = new SimpleXMLElement((file_exists("./xml/settings.xml") ? file_get_contents("./xml/settings.xml") : "<settings/>"));
// Server Variables
$host = (string) $settings->SmtpServer;
$username = (string) $settings->SmtpUserName;
$password = (string) $settings->SmtpPassword;
// Recipient Variables
$from_email = (string) $settings->FromEmail;
$from_name = (string) $settings->FromName;
$support_email = (string) $settings->SupportEmail;
$support_name = (string) $settings->SupportName;
if ((string) $settings->Environment == 'Production') {
$admin_email = (string) $settings->AdminEmail;
$admin_name = (string) $settings->AdminName;
}
// Content Variables
$attachment_name = "";
$mail = new PHPMailer(true);
try {
//Server settings
$mail->isSMTP();
$mail->Host = $host;
$mail->SMTPAuth = true;
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mail->Port = 465;
//Recipients
$mail->setFrom($from_email, $from_name);
$mail->addAddress($support_email, $support_name);
if ($settings->Environment == 'Production') {
$mail->addAddress($admin_email, $admin_name);
}
//Attachments
$mail->addAttachment($attachment_name);
//Content
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $message;
$mail->send();
} catch (Exception $e) {
$this->log("Message could not be sent. Mailer Error: {$e->ErrorInfo}");
}
}
// ========================
// Return an Formatted Date
// ========================
public function fixDate($givenDate) {
$formattedDate = date('Y-m-d', strtotime($givenDate));
return $formattedDate;
}
}
?>
+22 -6
View File
@@ -321,10 +321,12 @@ class Subscriptions extends Base {
$user_serial = filter_input(INPUT_POST, "user_reference", FILTER_SANITIZE_NUMBER_INT) ?: 0; $user_serial = filter_input(INPUT_POST, "user_reference", FILTER_SANITIZE_NUMBER_INT) ?: 0;
$user = (new Users())->getUser($user_serial); $user = (new Users())->getUser($user_serial);
$usersubscriptions = $this->getUsersActiveSubscriptions($user_serial);
$subscriptions = $this->getSubscriptions(); $subscriptions = $this->getSubscriptions();
$XML = $this->mergeXML($user, "<XML/>"); $XML = $this->mergeXML($user, "<XML/>");
$XML = $this->mergeXML($subscriptions, $XML); $XML = $this->mergeXML($subscriptions, $XML);
$XML = $this->mergeXML($usersubscriptions, $XML);
$content = $this->applyXSL($XML, $this->getXSL("addSubscriptionToUser")); $content = $this->applyXSL($XML, $this->getXSL("addSubscriptionToUser"));
@@ -334,11 +336,18 @@ class Subscriptions extends Base {
case "add": case "add":
$subscription_serial = filter_input(INPUT_POST, "subscription_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
$user_serial = filter_input(INPUT_POST, "user_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0; $user_serial = filter_input(INPUT_POST, "user_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
$subscription_stop_date = filter_input(INPUT_POST, "subscription_stop_date") ?: ""; $subscription_serials = filter_input(INPUT_POST, "subscriptions", FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY) ?: array();
$subscription_end_dates = filter_input(INPUT_POST, "subscription_end_date", FILTER_DEFAULT, FILTER_REQUIRE_ARRAY) ?: array();
$subscriptions = array();
$subscription_stop_date = date("Y-m-d", strtotime($subscription_stop_date)); foreach ($subscription_serials as $index => $subscription_serial) {
$subscriptions[] = array(
"usersubscription_subscription" => $subscription_serial,
"usersubscription_end_date" => date("Y-m-d", strtotime($subscription_end_dates[$index] ?? ""))
);
}
$SQL = "insert into usersubscriptions $SQL = "insert into usersubscriptions
@@ -357,12 +366,19 @@ class Subscriptions extends Base {
$statement = $this->connect()->prepare($SQL); $statement = $this->connect()->prepare($SQL);
$statement->bindValue(":usersubscription_user_serial", $user_serial); $statement->bindValue(":usersubscription_user_serial", $user_serial);
$statement->bindValue(":usersubscription_subscription_serial", $subscription_serial);
$statement->bindValue(":usersubscription_stop_date", $subscription_stop_date);
$statement->bindValue(":usersubscription_timestamp", $this->getTimestamp()); $statement->bindValue(":usersubscription_timestamp", $this->getTimestamp());
$statement->bindValue(":usersubscription_origin", $this->current_user_name); $statement->bindValue(":usersubscription_origin", $this->current_user_name);
$statement->execute(); foreach ($subscriptions as $subscription) {
$subscription_serial = $subscription['usersubscription_subscription'];
$subscription_stop_date = $subscription['usersubscription_end_date'];
$statement->bindValue(":usersubscription_subscription_serial", $subscription_serial);
$statement->bindValue(":usersubscription_stop_date", $subscription_stop_date);
$statement->execute();
}
break; break;
+14
View File
@@ -743,6 +743,20 @@ class Users extends Base {
error_log($statement->errorInfo()[2]); error_log($statement->errorInfo()[2]);
} }
} }
// ====================================
// Return a Users Login Activity Object
// ====================================
public function getLoginActivity($userserial) {
$SQL = "select view_journal.journal_timestamp_verbose, view_journal.journal_ip
from view_journal
where view_journal.journal_origin = '{$userserial}'
and view_journal.journal_entry like '{$userserial} %logged in%'";
return $this->getTable("loginactivity", $SQL);
}
} }
?> ?>
+84 -21
View File
@@ -16,9 +16,9 @@ $(document).ready(function () {
placeholder: "MM/DD/YYYY", placeholder: "MM/DD/YYYY",
showMaskOnHover: false}); showMaskOnHover: false});
// -------------- // -------------
// Select Picker. // Select Picker
// -------------- // -------------
$(".search-dropdown").selectpicker({noneSelectedText: "", $(".search-dropdown").selectpicker({noneSelectedText: "",
style: "", style: "",
@@ -34,25 +34,31 @@ $(document).ready(function () {
// Date Pickers // Date Pickers
// ------------ // ------------
$("#subscription_stop_date").datepicker({format: "mm-dd-yyyy", $(".subscription-end-date").each(function () {
container: "div.subscription_stop_date", $(this).datepicker({
keyboardNavigation: false, format: "mm/dd/yyyy",
showOnFocus: false, container: $(this).closest(".subscription_end_date"),
autoclose: true, keyboardNavigation: false,
orientation: "left bottom", showOnFocus: false,
todayBtn: "linked", autoclose: true,
todayHighlight: true}); orientation: "bottom left",
todayBtn: "linked",
todayHighlight: true
});
});
// --------------------- // ---------------------
// Show Calendar Buttons // Show Calendar Buttons
// --------------------- // ---------------------
$(".btnSubscriptionStopDateCalendar").on("click", function (e) { $(".btnSubscriptionEndDateCalendar").on("click", function (e) {
e.preventDefault(); e.preventDefault();
let end_date = $(this).siblings(".subscription-end-date");
$("#subscription_stop_date").focus().datepicker("show"); if (!end_date.prop("disabled")) {
end_date.focus().datepicker("show");
}
}); });
// ----------- // -----------
@@ -81,24 +87,81 @@ $(document).ready(function () {
}); });
function setSubscriptionDateState(subscription_checkbox) {
let row = subscription_checkbox.closest("tr");
let checked = subscription_checkbox.prop("checked");
row.find(".subscription-end-date").prop("disabled", !checked);
row.find(".btnSubscriptionEndDateCalendar").prop("disabled", !checked);
}
function parseSubscriptionDate(date_value) {
let parts = date_value.split("/");
return new Date(parts[2], parts[0] - 1, parts[1]);
}
$(".subscription-checkbox").each(function () {
setSubscriptionDateState($(this));
});
$(".subscription-checkbox").on("change", function () {
let totalSubscriptions = $(".subscription-checkbox").length;
let checkedSubscriptions = $(".subscription-checkbox:checked").length;
$("#selectAllSubscriptions").prop("checked", totalSubscriptions === checkedSubscriptions);
setSubscriptionDateState($(this));
});
$("#selectAllSubscriptions").on("change", function () {
$(".subscription-checkbox").prop("checked", $(this).prop("checked")).each(function () {
setSubscriptionDateState($(this));
});
});
// --------------- // ---------------
// Validate Inputs // Validate Inputs
// --------------- // ---------------
$("#addSubscriptionToUser").submit(function (e) { $("#addSubscriptionToUser").submit(function (e) {
var subscription_title = $("#subscription_serial"), subscription_stop_date = $("#subscription_stop_date"); let checkedSubscriptions = $(".subscription-checkbox:checked");
clearInputErrors(); clearInputErrors();
$("#subscriptions_errors").addClass("d-none");
if ($("option:selected", subscription_title).val() === "") { // Validate at least one subscription selected
createInputError(subscription_title, "Subscription is required"); if (checkedSubscriptions.length === 0) {
e.preventDefault();
$("#subscriptions_errors").removeClass("d-none");
return false; return false;
} }
if (isBlank(subscription_stop_date)) { for (let i = 0; i < checkedSubscriptions.length; i++) {
createInputError(subscription_stop_date, "Date is required");
return false; let row = checkedSubscriptions.eq(i).closest("tr");
let end_date = row.find(".subscription-end-date");
if (isBlank(end_date)) {
createInputError(end_date, "Date is required");
e.preventDefault();
return false;
}
if ((!isBlank(end_date)) && (!end_date.inputmask("isComplete"))) {
createInputError(end_date, "Date is incomplete");
e.preventDefault();
return false;
}
} }
e.preventDefault(); e.preventDefault();
+1
View File
@@ -15,6 +15,7 @@ $(document).ready(function () {
$sessionStorage.action = "Import.uploadCSVFile"; $sessionStorage.action = "Import.uploadCSVFile";
$sessionStorage.step = "prompt"; $sessionStorage.step = "prompt";
$sessionStorage.import_type = "full";
sessionStorage.setItem($application, JSON.stringify($sessionStorage)); sessionStorage.setItem($application, JSON.stringify($sessionStorage));
+2 -2
View File
@@ -54,7 +54,7 @@ $(document).ready(function () {
$(".file-input").on("change", function (e) { $(".file-input").on("change", function (e) {
if ((e.target.files.length === 0) || (e.target.files[0].type !== "text/csv")) { if (e.target.files.length === 0) {
return false; return false;
} }
@@ -93,7 +93,7 @@ $(document).ready(function () {
.done(function () { .done(function () {
$sessionStorage.action = ""; $sessionStorage.action = "";
$sessionStorage.toast = "RDI Records Imported"; $sessionStorage.toast = ($("#upload_type").val() === "full") ? "CSV Import Queued" : "Permit Records Imported";
sessionStorage.setItem($application, JSON.stringify($sessionStorage)); sessionStorage.setItem($application, JSON.stringify($sessionStorage));
+6 -1
View File
@@ -2,6 +2,11 @@
<project-private xmlns="http://www.netbeans.org/ns/project-private/1"> <project-private xmlns="http://www.netbeans.org/ns/project-private/1">
<editor-bookmarks xmlns="http://www.netbeans.org/ns/editor-bookmarks/2" lastBookmarkId="0"/> <editor-bookmarks xmlns="http://www.netbeans.org/ns/editor-bookmarks/2" lastBookmarkId="0"/>
<open-files xmlns="http://www.netbeans.org/ns/projectui-open-files/2"> <open-files xmlns="http://www.netbeans.org/ns/projectui-open-files/2">
<group/> <group>
<file>file:/C:/Users/JamesRichie/OneDrive%20-%20OneSource%20IT%20Solutions%20LLC/Documents/NetBeansProjects/dec-international/atlhousingreport/classes/Import.php</file>
<file>file:/C:/Users/JamesRichie/OneDrive%20-%20OneSource%20IT%20Solutions%20LLC/Documents/NetBeansProjects/dec-international/atlhousingreport/xsl/uploadCSVFile.xsl</file>
<file>file:/C:/Users/JamesRichie/OneDrive%20-%20OneSource%20IT%20Solutions%20LLC/Documents/NetBeansProjects/dec-international/atlhousingreport/classes/Consolidation.php</file>
<file>file:/C:/Users/JamesRichie/OneDrive%20-%20OneSource%20IT%20Solutions%20LLC/Documents/NetBeansProjects/dec-international/atlhousingreport/xml/settings.xml</file>
</group>
</open-files> </open-files>
</project-private> </project-private>
+50 -3
View File
@@ -81,7 +81,8 @@ class User_Summary extends TCPDF {
} }
$usernotes = (new Notes())->getAllNotes($this->user_serial); $usernotes = (new Notes())->getAllNotes($this->user_serial);
$subscriptions = (new Subscriptions())->getUsersSubscriptions($this->user_serial); $subscriptions = (new Subscriptions())->getUsersActiveSubscriptions($this->user_serial);
$userloginactivity = (new Users())->getLoginActivity($user->record->user_name);
// Set report defaults // Set report defaults
@@ -167,11 +168,57 @@ class User_Summary extends TCPDF {
$this->RoundedRect(10, 23, 190, $this->GetY() - 21, 1.50, "1111"); $this->RoundedRect(10, 23, 190, $this->GetY() - 21, 1.50, "1111");
// --------------
// Login Activity
// --------------
$this->Ln(7);
$this->SetFont("Calibri", "B", 12);
$this->Cell(0, 0, "Login Activity", 0, 1, "L", false, "", 3);
$this->Ln(3);
$this->SetFont("Calibri", "B", 10);
$this->Cell(30, 0, "Date", "B", 0, "L", false, "", 3);
$this->Cell(20, 0, "IP Address", "B", 0, "L", false, "", 3);
$this->SetFont("Calibri", "", 10);
$this->Ln(6);
if ($userloginactivity->count() == 0) {
$this->Cell(0, 0, "No Login Activity...", 0, 1, "L", false, "", 3);
} else {
foreach ($userloginactivity as $userlogin) {
$this->Cell(30, 0, $userlogin->journal_timestamp_verbose, 0, 0, "L", false, "", 3);
$this->Cell(20, 0, $userlogin->journal_ip, 0, 0, "L", false, "", 3);
$this->Ln(4);
if ($this->GetY() > 265) {
$this->AddPage("P", "LETTER");
$this->SetFont("Calibri", "B", 10);
$this->Cell(30, 0, "Date", "B", 0, "L", false, "", 3);
$this->Cell(20, 0, "Origin", "B", 0, "L", false, "", 3);
$this->SetFont("Calibri", "", 10);
$this->Ln(6);
}
}
}
// ----- // -----
// Notes // Notes
// ----- // -----
$this->Ln(7); $this->Ln(5);
$this->SetFont("Calibri", "B", 12); $this->SetFont("Calibri", "B", 12);
$this->Cell(0, 0, "Notes", 0, 1, "L", false, "", 3); $this->Cell(0, 0, "Notes", 0, 1, "L", false, "", 3);
@@ -282,7 +329,7 @@ class User_Summary extends TCPDF {
$this->setCellPaddings(0, 0, 0, 0); $this->setCellPaddings(0, 0, 0, 0);
$this->setCellMargins(2, 0, 0, 0); $this->setCellMargins(2, 0, 0, 0);
$this->ImageSVG('images/dec-logo.svg', 10, 0, 25, 25); $this->Image('images/dec-international-logo.png', 10, 7, 25, 10);
$this->SetFont("Calibri", "B", 16); $this->SetFont("Calibri", "B", 16);
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# -- Development --
cd /var/www/html/clients/dec-international/ahrhousingreport
# -- Production --
#cd /var/www/html/webapps/dec-international/ahrhousingreport
php ./scripts/ImportRequest_Worker.php
+9
View File
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
$basePath = realpath(__DIR__ . '/..');
require_once $basePath . '/classes/Process_ImportRequest.php';
(new Process_ImportRequest())->process();
?>
+10
View File
@@ -0,0 +1,10 @@
2025-11-14 22:35 PM : **************************************************
2025-11-14 22:35 PM : Starting Migration of RDI Users
2025-11-14 22:35 PM : Processing RDI Users
2025-11-14 22:35 PM : Processed 10 records...
2025-11-14 22:35 PM : Processed 20 records...
2025-11-14 22:35 PM : Processed 30 records...
2025-11-14 22:35 PM : Processed 40 records...
2025-11-14 22:35 PM : Processed 50 records...
2025-11-14 22:35 PM : Processed 60 records...
2025-11-14 22:35 PM : RDI Users Migration Process Complete
+16
View File
@@ -0,0 +1,16 @@
<?php
$SQL = "create table importjobs (
importjob_serial serial,
importjob_file_path varchar(1024) default null,
importjob_status enum('queued','running','complete','failed') NOT NULL DEFAULT 'queued',
importjob_notified boolean default false,
importjob_error_message text default null,
importjob_creator varchar(100) default null,
importjob_created datetime default null,
importjob_completed datetime default null,
primary key (importjob_serial))";
?>
+10
View File
@@ -0,0 +1,10 @@
<?php
$SQL = "create view view_importjobs as
select importjobs.*,
date_format(importjob_created, '%c/%e/%Y %l:%i %p') as importjob_created_verbose,
date_format(importjob_completed, '%c/%e/%Y %l:%i %p') as importjob_completed_verbose
from importjobs";
?>
+82 -40
View File
@@ -22,48 +22,88 @@
<xsl:value-of select="$PAGE-TITLE"/> <xsl:value-of select="$PAGE-TITLE"/>
| |
<xsl:value-of select="//users/record/user_name"/> <xsl:value-of select="//users/record/user_name"/>
|
<xsl:value-of select="//users/record/user_id"/>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="" name="{$PAGE-NAME}" method="post" id="{$PAGE-NAME}"> <form action="" name="{$PAGE-NAME}" method="post" id="{$PAGE-NAME}">
<input type="hidden" name="action" value="Subscriptions.addSubscriptionToUser"/> <input type="hidden" name="action" value="Subscriptions.addSubscriptionToUser"/>
<input type="hidden" name="step" value="add"/> <input type="hidden" name="step" value="add"/>
<input type="hidden" name="user_serial" value="{//users/record/user_serial}"/>
<input type="hidden" name="user_serial" value="{//users/record/user_serial}"/> <div class="alert alert-info py-2 small">
<i class="bi bi-info-circle me-2"/>
The subscriptions listed below will be added to the user.
</div>
<div class="row"> <div class="row g-4">
<div class="col-lg-8 mb-3"> <!-- Left Side: Subscriptions Being Copied -->
<label class="form-label" for="subscription_serial">Subscription</label> <div class="col-lg-8">
<select class="form-control form-control-sm search-dropdown" name="subscription_serial" id="subscription_serial" size="1">
<xsl:apply-templates select="//subscriptions"/>
</select>
</div>
<div class="col-lg-3 mb-3"> <div class="card h-100">
<div class="card-body p-0">
<div class="">
<table class="table table-sm table-striped table-hover mb-0 align-middle subscriptions_error">
<thead class="table-light">
<tr>
<th style="width: 20px;">
<input type="checkbox" class="form-check-input" id="selectAllSubscriptions"/>
</th>
<th>Subscription</th>
<th>End Date</th>
</tr>
</thead>
<tbody>
<xsl:choose>
<xsl:when test="count(//subscriptions/record[not(subscription_serial = //usersubscriptions/record/usersubscription_subscription_serial)]) = 0">
<tr>
<td colspan="4" class="text-center text-muted py-3">
This user already has all available subscriptions.
</td>
</tr>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="//subscriptions/record[not(subscription_serial = //usersubscriptions/record/usersubscription_subscription_serial)]"/>
</xsl:otherwise>
</xsl:choose>
</tbody>
</table>
</div>
<div id="subscriptions_errors" class="d-none px-3 pt-3">
<div class="alert alert-danger py-2 small">
<i class="bi bi-info-circle me-2"/>
Select at least one subscription to copy.
</div>
</div>
</div>
<label class="form-label" for="subscription_stop_date">Stop Date</label>
<div class="input-group input-group-sm subscription_stop_date">
<input type="text" class="form-control form-control-sm date-mask" name="subscription_stop_date" id="subscription_stop_date" value="{$TODAY_MMDDYYYY}"/>
<button class="btn btn-sm btn-outline-primary btnSubscriptionStopDateCalendar" type="button" data-bs-toggle="tooltip" data-bs-title="Calendar" data-bs-custom-class="custom-tooltip" tabindex="-1">
<i class="bi bi-calendar3"/>
</button>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
<div class="buttons mt-2 mb-2"> <div class="buttons mt-1 mb-2">
<button type="button" class="btn btn-sm btn-primary me-2 btnSave">
<i class="bi bi-save me-2"/>Save</button> <div class="buttons mt-3 mb-1">
<button type="button" class="btn btn-sm btn-primary me-2 btnCancel"> <button type="button" class="btn btn-sm btn-primary me-2 btnCancel">
<i class="bi bi-x-lg me-2"/>Cancel</button> <i class="bi bi-box-arrow-left me-2"/>Cancel
</button>
<button type="button" class="btn btn-sm btn-primary me-2 btnSave">
<i class="bi bi-floppy me-2"/>Save</button>
</div>
</div> </div>
</div> </div>
@@ -74,22 +114,24 @@
</xsl:template> </xsl:template>
<!-- Template for Subscription dropdown --> <!-- Subscriptions Rows -->
<xsl:template match="subscriptions/record">
<xsl:template match="subscriptions"> <tr>
<td>
<option value="">Choose..</option> <input type="checkbox" class="form-check-input subscription-checkbox" name="subscriptions[]" value="{subscription_serial}"/>
</td>
<xsl:for-each select="record"> <td class="fw-semibold">
<xsl:value-of select="concat(subscription_description, ' - ', subscription_projecttype, ' - ', subscription_area)"/>
<option value="{subscription_serial}" data-tokens="{subscription_serial}"> </td>
<xsl:value-of select="subscription_title"/> <td>
- <div class="input-group input-group-sm subscription_end_date">
<xsl:value-of select="subscription_description"/> <input type="text" class="form-control form-control-sm date-mask subscription-end-date" name="subscription_end_date[]"/>
</option> <button class="btn btn-sm btn-outline-primary btnSubscriptionEndDateCalendar" type="button" data-bs-toggle="tooltip" data-bs-title="Calendar" data-bs-custom-class="custom-tooltip" tabindex="-1">
<i class="bi bi-calendar3"/>
</xsl:for-each> </button>
</div>
</td>
</tr>
</xsl:template> </xsl:template>
</xsl:stylesheet> </xsl:stylesheet>
+95
View File
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" encoding="utf-8" indent="yes"/>
<!-- Parameters -->
<xsl:param name="VERSION">1.0</xsl:param>
<xsl:param name="PAGE-TITLE">Add Subscription to User</xsl:param>
<xsl:param name="PAGE-NAME">addSubscriptionToUser</xsl:param>
<xsl:template match="/">
<script src="js/{$PAGE-NAME}.js?version={$VERSION}"/>
<div class="{$PAGE-NAME}">
<div class="card">
<div class="card-header bg-primary text-white p-1 ps-3">
<i class="bi bi-list-check me-2"/>
<xsl:value-of select="$PAGE-TITLE"/>
|
<xsl:value-of select="//users/record/user_name"/>
</div>
<div class="card-body">
<form action="" name="{$PAGE-NAME}" method="post" id="{$PAGE-NAME}">
<input type="hidden" name="action" value="Subscriptions.addSubscriptionToUser"/>
<input type="hidden" name="step" value="add"/>
<input type="hidden" name="user_serial" value="{//users/record/user_serial}"/>
<div class="row">
<div class="col-lg-8 mb-3">
<label class="form-label" for="subscription_serial">Subscription</label>
<select class="form-control form-control-sm search-dropdown" name="subscription_serial" id="subscription_serial" size="1">
<xsl:apply-templates select="//subscriptions"/>
</select>
</div>
<div class="col-lg-3 mb-3">
<label class="form-label" for="subscription_stop_date">Stop Date</label>
<div class="input-group input-group-sm subscription_stop_date">
<input type="text" class="form-control form-control-sm date-mask" name="subscription_stop_date" id="subscription_stop_date" value="{$TODAY_MMDDYYYY}"/>
<button class="btn btn-sm btn-outline-primary btnSubscriptionStopDateCalendar" type="button" data-bs-toggle="tooltip" data-bs-title="Calendar" data-bs-custom-class="custom-tooltip" tabindex="-1">
<i class="bi bi-calendar3"/>
</button>
</div>
</div>
</div>
</form>
<div class="buttons mt-2 mb-2">
<button type="button" class="btn btn-sm btn-primary me-2 btnSave">
<i class="bi bi-save me-2"/>Save</button>
<button type="button" class="btn btn-sm btn-primary me-2 btnCancel">
<i class="bi bi-x-lg me-2"/>Cancel</button>
</div>
</div>
</div>
</div>
</xsl:template>
<!-- Template for Subscription dropdown -->
<xsl:template match="subscriptions">
<option value="">Choose..</option>
<xsl:for-each select="record">
<option value="{subscription_serial}" data-tokens="{subscription_serial}">
<xsl:value-of select="subscription_title"/>
-
<xsl:value-of select="subscription_description"/>
</option>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
+2 -2
View File
@@ -29,10 +29,10 @@
<p>Processed Records Count: <xsl:value-of select="$PROCESSED-RECORD-COUNT"/></p> <p>Processed Records Count: <xsl:value-of select="$PROCESSED-RECORD-COUNT"/></p>
<xsl:choose> <xsl:choose>
<xsl:when test="$RDI-RECORD-COUNT != $PROCESSED-RECORD-COUNT"> <xsl:when test="$CONSOLIDATION-ERROR != '' or $RDI-RECORD-COUNT != $PROCESSED-RECORD-COUNT">
<h3 class="text-danger">Consolidation Unsuccessful</h3> <h3 class="text-danger">Consolidation Unsuccessful</h3>
<p> <p>
<xsl:value-of select="CONSOLIDATION-ERROR"/> <xsl:value-of select="$CONSOLIDATION-ERROR"/>
</p> </p>
</xsl:when> </xsl:when>
<xsl:otherwise> <xsl:otherwise>
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" encoding="utf-8" indent="yes"/>
<!-- Parameters -->
<xsl:param name="PAGE-NAME">Queued Import Message</xsl:param>
<xsl:template match="/">
<script src="js/{$PAGE-NAME}.js?version={$VERSION}"/>
<div class="card">
<div class="card-header bg-primary text-white p-1 ps-3 border-bottom-0">
<i class="bi bi-exclamation-triangle me-2" aria-hidden="true"/>
<xsl:copy-of select="$PAGE-NAME"/>
</div>
<div class="card-body text-center">
<p>
The CSV File has been successfully uploaded and queued for processing. An email will be sent when the import process is complete.
</p>
<div class="buttons mt-2 mb-2">
<a role="button" class="btn btn-sm btn-primary me-2 btnDone">
<i class="bi bi-box-arrow-right me-2"/>Done</a>
</div>
</div>
</div>
</xsl:template>
</xsl:stylesheet>
+49 -1
View File
@@ -8,6 +8,7 @@
<xsl:param name="VERSION">1.0</xsl:param> <xsl:param name="VERSION">1.0</xsl:param>
<xsl:param name="PAGE-TITLE">Upload CSV File</xsl:param> <xsl:param name="PAGE-TITLE">Upload CSV File</xsl:param>
<xsl:param name="PAGE-NAME">uploadCSVFile</xsl:param> <xsl:param name="PAGE-NAME">uploadCSVFile</xsl:param>
<xsl:param name="IMPORT-TYPE">incremental</xsl:param>
<xsl:template match="/"> <xsl:template match="/">
@@ -35,9 +36,56 @@
<div class="row"> <div class="row">
<div class="col-6 mt-3 mb-4"> <div class="col-6 mt-3 mb-4">
<input type="file" class="form-control file-input" name="csvfile" id="csvfile" accept=".csv"/> <input type="file" class="form-control form-control-sm file-input" name="csvfile" id="csvfile" accept=".csv"/>
</div> </div>
<div class="col-3 mt-3 mb-4">
<select class="form-control form-control-sm" name="upload_type" id="upload_type">
<xsl:choose>
<xsl:when test="$IMPORT-TYPE = 'incremental'">
<option value="incremental" selected="selected">
Incremental Upload
</option>
<option value="full">
Full Upload
</option>
</xsl:when>
<xsl:when test="$IMPORT-TYPE = 'full'">
<option value="incremental">
Incremental Upload
</option>
<option value="full" selected="selected">
Full Upload
</option>
</xsl:when>
<xsl:otherwise>
<option value="incremental">
Incremental Upload
</option>
<option value="full">
Full Upload
</option>
</xsl:otherwise>
</xsl:choose>
</select>
</div>
</div> </div>
</form> </form>
+2 -2
View File
@@ -261,7 +261,7 @@
<div class="buttons mt-2 mb-1"> <div class="buttons mt-2 mb-1">
<button type="button" class="btn btn-sm btn-primary mt-2 me-1 mb-1 btnAddSubscription"> <button type="button" class="btn btn-sm btn-primary mt-2 me-1 mb-1 btnAddSubscription">
<i class="bi bi-plus me-2"/>Add Subscription</button> <i class="bi bi-plus-lg me-2"/>Add Subscription</button>
</div> </div>
@@ -309,7 +309,7 @@
<div class="buttons mt-2 mb-1"> <div class="buttons mt-2 mb-1">
<button type="button" class="btn btn-sm btn-primary mt-2 me-1 mb-1 btnAddNote"> <button type="button" class="btn btn-sm btn-primary mt-2 me-1 mb-1 btnAddNote">
<i class="bi bi-plus me-2"/>Add Note</button> <i class="bi bi-plus-lg me-2"/>Add Note</button>
</div> </div>
</div> </div>