create new command for insert init spatial plannings dan remove unused retribution tables

This commit is contained in:
arifal
2025-06-23 17:52:55 +07:00
parent 7eb5a850c2
commit 5dd92aa323
5 changed files with 826 additions and 2 deletions

View File

@@ -29,7 +29,7 @@ class InitSpatialPlanningDatas extends Command
*/
public function handle()
{
$filePath = $this->argument('file') ?? 'public/templates/Data_2025___Estimasi_Jumlah_Lantai.csv';
$filePath = $this->argument('file') ?? 'public/templates/2025.xlsx';
$fullPath = storage_path('app/' . $filePath);
// Check if file exists
@@ -192,6 +192,8 @@ class InitSpatialPlanningDatas extends Command
$this->info("Found " . count($rows) . " data rows to import.");
$this->info("Headers: " . implode(', ', $headers));
dd($rows[0]);
$progressBar = $this->output->createProgressBar(count($rows));
$progressBar->start();

View File

@@ -0,0 +1,675 @@
<?php
namespace App\Console\Commands;
use App\Models\SpatialPlanning;
use Illuminate\Console\Command;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Exception;
class InjectSpatialPlanningsData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'spatial-planning:inject
{--file=storage/app/public/templates/2025.xlsx : Path to Excel file}
{--sheet=0 : Sheet index to read from}
{--dry-run : Run without actually inserting data}
{--debug : Show Excel content for debugging}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Inject spatial planning data from Excel file with BCR area calculation';
/**
* Execute the console command.
*/
public function handle()
{
try {
$filePath = $this->option('file');
$sheetIndex = (int) $this->option('sheet');
$isDryRun = $this->option('dry-run');
$isDebug = $this->option('debug');
if (!file_exists($filePath)) {
$this->error("File not found: {$filePath}");
return 1;
}
$this->info("Reading Excel file: {$filePath}");
$this->info("Sheet index: {$sheetIndex}");
if ($isDryRun) {
$this->warn("DRY RUN MODE - No data will be inserted");
}
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('spatial_plannings')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
$this->info('Spatial plannings table truncated successfully.');
$spreadsheet = IOFactory::load($filePath);
$worksheet = $spreadsheet->getSheet($sheetIndex);
$rows = $worksheet->toArray(null, true, true, true);
if ($isDebug) {
$this->info("=== EXCEL CONTENT DEBUG ===");
foreach (array_slice($rows, 0, 20) as $index => $row) {
if (!empty(array_filter($row))) {
$this->line("Row $index: " . json_encode($row));
}
}
$this->info("=== END DEBUG ===");
}
// Find BCR percentages from last rows (columns D and E)
$bcrPercentages = $this->findBcrPercentages($rows);
$this->info("Found BCR Percentages: " . json_encode($bcrPercentages));
// Process data by sections
$sections = $this->processSections($rows, $bcrPercentages, $isDebug);
$this->info("Found " . count($sections) . " sections");
$totalInserted = 0;
foreach ($sections as $sectionIndex => $section) {
$this->info("Processing Section " . ($sectionIndex + 1) . ": " . $section['applicant_name']);
// Gudang/pergudangan keywords successfully added to Fungsi Usaha classification
if (!$isDryRun) {
$inserted = $this->insertSpatialPlanningData($section);
$totalInserted += $inserted;
$this->info("Inserted {$inserted} record for this section");
} else {
$this->info("Would insert 1 record for this section");
}
}
if (!$isDryRun) {
$this->info("Successfully inserted {$totalInserted} spatial planning records");
} else {
$this->info("Dry run completed. Total records that would be inserted: " . count($sections));
}
return 0;
} catch (Exception $e) {
$this->error("Error: " . $e->getMessage());
Log::error("InjectSpatialPlanningsData failed", ['error' => $e->getMessage()]);
return 1;
}
}
/**
* Find BCR percentages from last rows in columns D and E
*/
private function findBcrPercentages(array $rows): array
{
$bcrPercentages = [];
// Look for BCR percentages in the last few rows
$totalRows = count($rows);
$searchRows = max(1, $totalRows - 10); // Search last 10 rows
for ($i = $totalRows; $i >= $searchRows; $i--) {
if (isset($rows[$i]['D']) && isset($rows[$i]['E'])) {
$valueD = $this->cleanNumericValue($rows[$i]['D']);
$valueE = $this->cleanNumericValue($rows[$i]['E']);
// Check if these look like percentages (between 0 and 100)
if ($valueD > 0 && $valueD <= 100 && $valueE > 0 && $valueE <= 100) {
$bcrPercentages['D'] = $valueD;
$bcrPercentages['E'] = $valueE;
break;
}
}
}
// Default values if not found
if (empty($bcrPercentages)) {
$bcrPercentages = ['D' => 60, 'E' => 40]; // Default BCR percentages
}
return $bcrPercentages;
}
/**
* Process data by sections (each applicant)
*/
private function processSections(array $rows, array $bcrPercentages, bool $isDebug): array
{
$sections = [];
$currentSection = null;
$currentSectionNumber = null;
$sectionData = [];
foreach ($rows as $rowIndex => $row) {
// Skip empty rows
if (empty(array_filter($row))) {
continue;
}
if ($isDebug) {
$this->line("Checking row $rowIndex: " . substr(json_encode($row), 0, 100) . "...");
}
// Check if this is a new section (applicant)
if ($this->isNewSection($row)) {
if ($isDebug) {
$this->info("Found new section at row $rowIndex");
}
// Save previous section if exists
if ($currentSection && !empty($sectionData)) {
$sections[] = [
'applicant_name' => $currentSection,
'section_number' => $currentSectionNumber,
'data' => $sectionData
];
if ($isDebug) {
$this->info("Saved section: $currentSection with " . count($sectionData) . " data rows");
}
}
// Start new section
$currentSectionNumber = trim($row['A'] ?? ''); // Store section number
$currentSection = $this->extractApplicantName($row);
$sectionData = [];
// Also process the header row itself for F, G, H data
$headerRow = $this->processDataRow($row, $bcrPercentages);
if ($headerRow) {
$sectionData[] = $headerRow;
}
if ($isDebug) {
$this->info("Starting new section: $currentSection");
$this->line(" Header F: " . ($row['F'] ?? 'null'));
$this->line(" Header G: " . ($row['G'] ?? 'null'));
$this->line(" Header H: " . ($row['H'] ?? 'null'));
}
} elseif ($currentSection && $this->isDataRow($row)) {
if ($isDebug) {
$this->line("Found data row for section: $currentSection");
$this->line(" Column D: " . ($row['D'] ?? 'null'));
$this->line(" Column E: " . ($row['E'] ?? 'null'));
$this->line(" Column F: " . ($row['F'] ?? 'null'));
$this->line(" Column G: " . ($row['G'] ?? 'null'));
$this->line(" Column H: " . ($row['H'] ?? 'null'));
}
// Add data to current section
$processedRow = $this->processDataRow($row, $bcrPercentages);
if ($processedRow) {
$sectionData[] = $processedRow;
}
}
}
// Add last section
if ($currentSection && !empty($sectionData)) {
$sections[] = [
'applicant_name' => $currentSection,
'section_number' => $currentSectionNumber,
'data' => $sectionData
];
}
return $sections;
}
/**
* Check if row indicates a new section/applicant
*/
private function isNewSection(array $row): bool
{
// Look for patterns that indicate a new applicant
$firstCell = trim($row['A'] ?? '');
// Check for pattern like "55 / 1565", "56 / 1543", etc.
return !empty($firstCell) && preg_match('/^\d+\s*\/\s*\d+$/', $firstCell);
}
/**
* Extract applicant name from section header
*/
private function extractApplicantName(array $row): string
{
// Row A contains number like "55 / 1565", Row B contains name and phone
$numberPart = trim($row['A'] ?? '');
$namePart = trim($row['B'] ?? '');
// Extract name from column B (remove phone number part)
if (!empty($namePart)) {
// Remove phone number pattern "No Telpon : xxxxx"
$name = preg_replace('/\s*No Telpon\s*:\s*[\d\s\-\+\(\)]+.*$/i', '', $namePart);
$name = trim($name);
return !empty($name) ? $name : $numberPart;
}
return $numberPart ?: 'Unknown Applicant';
}
/**
* Check if row contains data
*/
private function isDataRow(array $row): bool
{
// Check if row has data we're interested in
$columnD = trim($row['D'] ?? '');
$columnE = trim($row['E'] ?? '');
$columnF = trim($row['F'] ?? '');
$columnG = trim($row['G'] ?? '');
$columnH = trim($row['H'] ?? '');
// Look for important data patterns in column D
$importantPatterns = [
'A. Total luas lahan',
'Total luas lahan',
'Total Luas Lahan',
'BCR Kawasan',
'E. BCR Kawasan',
'D. BCR Kawasan',
'KWT',
'Total KWT',
'KWT Perumahan',
'D. KWT Perumahan',
'E. KWT Perumahan',
'BCR',
'Koefisien Wilayah Terbangun'
];
foreach ($importantPatterns as $pattern) {
if (stripos($columnD, $pattern) !== false && !empty($columnE)) {
return true;
}
}
// Also check for location data
if (stripos($columnD, 'Desa') !== false) {
return true;
}
// Check if any of the important columns (F, G, H) have data
// We want to capture ALL non-empty data in these columns within a section
if (!empty($columnF) && trim($columnF) !== '') {
return true;
}
if (!empty($columnG) && trim($columnG) !== '') {
return true;
}
if (!empty($columnH) && trim($columnH) !== '') {
return true;
}
return false;
}
/**
* Process a data row and calculate area using BCR formula
*/
private function processDataRow(array $row, array $bcrPercentages): ?array
{
try {
$columnD = trim($row['D'] ?? '');
$columnE = trim($row['E'] ?? '');
$columnF = trim($row['F'] ?? '');
$columnG = trim($row['G'] ?? '');
$columnH = trim($row['H'] ?? '');
$landArea = 0;
$bcrPercentage = $bcrPercentages['D'] ?? 60; // Default BCR percentage
$location = '';
// Extract land area if this is a "Total luas lahan" row
if (stripos($columnD, 'Total luas lahan') !== false ||
stripos($columnD, 'A. Total luas lahan') !== false) {
$landArea = $this->cleanNumericValue($columnE);
}
// Extract BCR percentage if this is a BCR row - comprehensive detection
if (stripos($columnD, 'BCR Kawasan') !== false ||
stripos($columnD, 'E. BCR Kawasan') !== false ||
stripos($columnD, 'D. BCR Kawasan') !== false ||
stripos($columnD, 'KWT Perumahan') !== false ||
stripos($columnD, 'D. KWT Perumahan') !== false ||
stripos($columnD, 'E. KWT Perumahan') !== false ||
stripos($columnD, 'KWT') !== false ||
(stripos($columnD, 'BCR') !== false && stripos($columnE, '%') !== false) ||
stripos($columnD, 'Koefisien Wilayah Terbangun') !== false) {
$bcrValue = $this->cleanNumericValue($columnE);
if ($bcrValue > 0 && $bcrValue <= 100) {
$bcrPercentage = $bcrValue;
}
}
// Get location from village/subdistrict info (previous rows in the section)
if (stripos($columnD, 'Desa') !== false) {
$location = $columnD;
}
// Calculate area: total luas lahan dibagi persentase BCR
$calculatedArea = $landArea > 0 && $bcrPercentage > 0 ?
($landArea / ($bcrPercentage / 100)) : 0;
return [
'data_type' => $columnD,
'value' => $columnE,
'land_area' => $landArea,
'bcr_percentage' => $bcrPercentage,
'calculated_area' => $calculatedArea,
'location' => $location,
'no_tapak' => !empty($columnF) ? $columnF : null,
'no_skkl' => !empty($columnG) ? $columnG : null,
'no_ukl' => !empty($columnH) ? $columnH : null,
'raw_data' => $row
];
} catch (Exception $e) {
Log::warning("Error processing row", ['row' => $row, 'error' => $e->getMessage()]);
return null;
}
}
/**
* Insert spatial planning data
*/
private function insertSpatialPlanningData(array $section): int
{
try {
// Process section data to extract key values
$sectionData = $this->consolidateSectionData($section);
if (empty($sectionData) || !$sectionData['has_valid_data']) {
$this->warn("No valid data found for section: " . $section['applicant_name']);
return 0;
}
SpatialPlanning::create([
'name' => $section['applicant_name'],
'number' => $section['section_number'], // Kolom A - section number
'location' => $sectionData['location'], // Column C from header row
'land_area' => $sectionData['land_area'],
'area' => $sectionData['calculated_area'],
'building_function' => $sectionData['building_function'], // Determined from activities
'sub_building_function' => $sectionData['sub_building_function'], // UMKM or Usaha Besar
'activities' => $sectionData['activities'], // Activities from column D of first row
'site_bcr' => $sectionData['bcr_percentage'],
'no_tapak' => $sectionData['no_tapak'],
'no_skkl' => $sectionData['no_skkl'],
'no_ukl' => $sectionData['no_ukl'],
'date' => now()->format('Y-m-d'),
'created_at' => now(),
'updated_at' => now()
]);
return 1;
} catch (Exception $e) {
Log::error("Error inserting spatial planning data", [
'section' => $section['applicant_name'],
'error' => $e->getMessage()
]);
$this->warn("Failed to insert record for: " . $section['applicant_name']);
return 0;
}
}
/**
* Consolidate section data into a single record
*/
private function consolidateSectionData(array $section): array
{
$landArea = 0;
$bcrPercentage = 60; // Default from Excel file
$location = '';
$activities = ''; // Activities from column D of first row
$villages = [];
$noTapakValues = [];
$noSKKLValues = [];
$noUKLValues = [];
// Get activities from first row (header row) column D
if (!empty($section['data']) && !empty($section['data'][0]['data_type'])) {
$activities = $section['data'][0]['data_type']; // Column D data
}
// Get location from first row (header row) column C (alamat)
// We need to get this from raw data since processDataRow doesn't capture column C
if (!empty($section['data']) && !empty($section['data'][0]['raw_data']['C'])) {
$location = trim($section['data'][0]['raw_data']['C']);
}
foreach ($section['data'] as $dataRow) {
// Extract land area
if ($dataRow['land_area'] > 0) {
$landArea = $dataRow['land_area'];
}
// Extract BCR percentage - prioritize specific BCR from this section
// Always use section-specific BCR if found, regardless of value
if ($dataRow['bcr_percentage'] > 0 && $dataRow['bcr_percentage'] <= 100) {
$bcrPercentage = $dataRow['bcr_percentage'];
}
// Extract additional location info from village/subdistrict data if main location is empty
if (empty($location) && !empty($dataRow['location'])) {
$villages[] = trim(str_replace('Desa ', '', $dataRow['location']));
}
// Collect no_tapak values
if (!empty($dataRow['no_tapak']) && !in_array($dataRow['no_tapak'], $noTapakValues)) {
$noTapakValues[] = $dataRow['no_tapak'];
}
// Collect no_skkl values
if (!empty($dataRow['no_skkl']) && !in_array($dataRow['no_skkl'], $noSKKLValues)) {
$noSKKLValues[] = $dataRow['no_skkl'];
}
// Collect no_ukl values
if (!empty($dataRow['no_ukl']) && !in_array($dataRow['no_ukl'], $noUKLValues)) {
$noUKLValues[] = $dataRow['no_ukl'];
}
}
// Use first village as fallback location if main location is empty
if (empty($location)) {
$location = !empty($villages) ? $villages[0] : 'Unknown Location';
}
// Merge multiple values with | separator
$noTapak = !empty($noTapakValues) ? implode('|', $noTapakValues) : null;
$noSKKL = !empty($noSKKLValues) ? implode('|', $noSKKLValues) : null;
$noUKL = !empty($noUKLValues) ? implode('|', $noUKLValues) : null;
// Calculate area using BCR formula: land_area / (bcr_percentage / 100)
$calculatedArea = $landArea > 0 && $bcrPercentage > 0 ?
($landArea / ($bcrPercentage / 100)) : 0;
// Determine building_function and sub_building_function based on activities and applicant name
$buildingFunction = 'Mixed Development'; // Default
$subBuildingFunction = null;
// Get applicant name for PT validation
$applicantName = $section['applicant_name'] ?? '';
$isCompany = (strpos($applicantName, 'PT ') === 0 || strpos($applicantName, 'PT.') === 0);
// PRIORITY: PT Company validation - PT/PT. automatically classified as Fungsi Usaha
if ($isCompany) {
$buildingFunction = 'Fungsi Usaha';
// For PT companies: area-based classification
if ($landArea > 0 && $landArea < 500) { // < 500 m² for PT = Non-Mikro (since PT is already established business)
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
} elseif ($landArea >= 500) { // >= 500 m² for PT = Large Business
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
} else {
$subBuildingFunction = 'Usaha Besar (Non-Mikro)'; // Default for PT
}
}
// For non-PT applicants, use activity-based classification
elseif (!empty($activities)) {
$activitiesLower = strtolower($activities);
// 1. FUNGSI KEAGAMAAN
if (strpos($activitiesLower, 'masjid') !== false ||
strpos($activitiesLower, 'gereja') !== false ||
strpos($activitiesLower, 'pura') !== false ||
strpos($activitiesLower, 'vihara') !== false ||
strpos($activitiesLower, 'klenteng') !== false ||
strpos($activitiesLower, 'tempat ibadah') !== false ||
strpos($activitiesLower, 'keagamaan') !== false ||
strpos($activitiesLower, 'mushola') !== false) {
$buildingFunction = 'Fungsi Keagamaan';
$subBuildingFunction = 'Fungsi Keagamaan';
}
// 2. FUNGSI SOSIAL BUDAYA
elseif (strpos($activitiesLower, 'sekolah') !== false ||
strpos($activitiesLower, 'rumah sakit') !== false ||
strpos($activitiesLower, 'puskesmas') !== false ||
strpos($activitiesLower, 'klinik') !== false ||
strpos($activitiesLower, 'universitas') !== false ||
strpos($activitiesLower, 'kampus') !== false ||
strpos($activitiesLower, 'pendidikan') !== false ||
strpos($activitiesLower, 'kesehatan') !== false ||
strpos($activitiesLower, 'sosial') !== false ||
strpos($activitiesLower, 'budaya') !== false ||
strpos($activitiesLower, 'museum') !== false ||
strpos($activitiesLower, 'tower') !== false ||
strpos($activitiesLower, 'perpustakaan') !== false) {
$buildingFunction = 'Fungsi Sosial Budaya';
$subBuildingFunction = 'Fungsi Sosial Budaya';
}
// 3. FUNGSI USAHA
elseif (strpos($activitiesLower, 'perdagangan') !== false ||
strpos($activitiesLower, 'dagang') !== false ||
strpos($activitiesLower, 'toko') !== false ||
strpos($activitiesLower, 'usaha') !== false ||
strpos($activitiesLower, 'komersial') !== false ||
strpos($activitiesLower, 'pabrik') !== false ||
strpos($activitiesLower, 'industri') !== false ||
strpos($activitiesLower, 'manufaktur') !== false ||
strpos($activitiesLower, 'bisnis') !== false ||
strpos($activitiesLower, 'resto') !== false ||
strpos($activitiesLower, 'villa') !== false ||
strpos($activitiesLower, 'vila') !== false ||
strpos($activitiesLower, 'gudang') !== false ||
strpos($activitiesLower, 'pergudangan') !== false ||
strpos($activitiesLower, 'kolam renang') !== false ||
strpos($activitiesLower, 'minimarket') !== false ||
strpos($activitiesLower, 'supermarket') !== false ||
strpos($activitiesLower, 'perdaganagan') !== false ||
strpos($activitiesLower, 'waterpark') !== false ||
strpos($activitiesLower, 'pasar') !== false ||
strpos($activitiesLower, 'perumahan') !== false ||
strpos($activitiesLower, 'perumhan') !== false ||
strpos($activitiesLower, 'kantor') !== false) {
$buildingFunction = 'Fungsi Usaha';
// Determine business size based on land area for non-PT businesses
if ($landArea > 0 && $landArea > 500) { // > 500 m² considered large business
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
} else {
$subBuildingFunction = 'UMKM'; // For small individual businesses
}
}
// 4. FUNGSI HUNIAN (PERUMAHAN)
elseif (strpos($activitiesLower, 'rumah') !== false ||
strpos($activitiesLower, 'hunian') !== false ||
strpos($activitiesLower, 'residence') !== false ||
strpos($activitiesLower, 'residential') !== false ||
strpos($activitiesLower, 'housing') !== false ||
strpos($activitiesLower, 'town') !== false) {
$buildingFunction = 'Fungsi Hunian';
// Determine housing type based on area and keywords
if (strpos($activitiesLower, 'mbr') !== false ||
strpos($activitiesLower, 'masyarakat berpenghasilan rendah') !== false ||
strpos($activitiesLower, 'sederhana') !== false ||
($landArea > 0 && $landArea < 2000)) { // Small area indicates MBR
$subBuildingFunction = 'Rumah Tinggal Deret (MBR) dan Rumah Tinggal Tunggal (MBR)';
}
elseif ($landArea > 0 && $landArea < 100) {
$subBuildingFunction = 'Sederhana <100';
}
elseif ($landArea > 0 && $landArea > 100) {
$subBuildingFunction = 'Tidak Sederhana >100';
}
else {
$subBuildingFunction = 'Fungsi Hunian'; // Default for housing
}
}
// 5. FUNGSI CAMPURAN
elseif (strpos($activitiesLower, 'campuran') !== false ||
strpos($activitiesLower, 'mixed') !== false ||
strpos($activitiesLower, 'mix') !== false ||
strpos($activitiesLower, 'multi') !== false) {
$buildingFunction = 'Fungsi Campuran (lebih dari 1)';
// Determine mixed use size
if ($landArea > 0 && $landArea > 3000) { // > 3000 m² considered large mixed use
$subBuildingFunction = 'Campuran Besar';
} else {
$subBuildingFunction = 'Campuran Kecil';
}
}
}
return [
'land_area' => $landArea,
'bcr_percentage' => $bcrPercentage,
'calculated_area' => $calculatedArea,
'location' => $location,
'activities' => $activities, // Activities from column D of first row
'building_function' => $buildingFunction,
'sub_building_function' => $subBuildingFunction,
'no_tapak' => $noTapak,
'no_skkl' => $noSKKL,
'no_ukl' => $noUKL,
'has_valid_data' => $landArea > 0 // Flag untuk validasi
];
}
/**
* Clean and convert string to numeric value
*/
private function cleanNumericValue($value): float
{
if (is_numeric($value)) {
return (float) $value;
}
// Remove non-numeric characters except decimal points and commas
$cleaned = preg_replace('/[^0-9.,]/', '', $value);
// Handle different decimal separators
if (strpos($cleaned, ',') !== false && strpos($cleaned, '.') !== false) {
// Both comma and dot present, assume comma is thousands separator
$cleaned = str_replace(',', '', $cleaned);
} elseif (strpos($cleaned, ',') !== false) {
// Only comma present, assume it's decimal separator
$cleaned = str_replace(',', '.', $cleaned);
}
return is_numeric($cleaned) ? (float) $cleaned : 0;
}
}

View File

@@ -18,7 +18,7 @@ class LackOfPotentialController extends Controller
$total_pdam = Customer::count();
$total_tata_ruang = SpatialPlanning::count();
$total_tata_ruang_usaha = SpatialPlanning::where('building_function','like', '%usaha%')->count();
$total_tata_ruang_non_usaha = SpatialPlanning::where('building_function','like', '%hunian%')->count();
$total_tata_ruang_non_usaha = SpatialPlanning::where('building_function','not like', '%usaha%')->count();
$data_report_tourism = TourismBasedKBLI::all();
return response()->json([