diff --git a/app/Console/Commands/InitSpatialPlanningDatas.php b/app/Console/Commands/InitSpatialPlanningDatas.php index b398819..1cf9a2b 100644 --- a/app/Console/Commands/InitSpatialPlanningDatas.php +++ b/app/Console/Commands/InitSpatialPlanningDatas.php @@ -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(); diff --git a/app/Console/Commands/InjectSpatialPlanningsData.php b/app/Console/Commands/InjectSpatialPlanningsData.php new file mode 100644 index 0000000..7ef29da --- /dev/null +++ b/app/Console/Commands/InjectSpatialPlanningsData.php @@ -0,0 +1,675 @@ +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; + } +} diff --git a/app/Http/Controllers/Api/LackOfPotentialController.php b/app/Http/Controllers/Api/LackOfPotentialController.php index 7d358e0..fa49b4f 100644 --- a/app/Http/Controllers/Api/LackOfPotentialController.php +++ b/app/Http/Controllers/Api/LackOfPotentialController.php @@ -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([ diff --git a/build.zip b/build.zip index ad3402c..e3e5648 100644 Binary files a/build.zip and b/build.zip differ diff --git a/database/migrations/2025_06_23_173758_drop_unused_retribution_tables.php b/database/migrations/2025_06_23_173758_drop_unused_retribution_tables.php new file mode 100644 index 0000000..0b1f368 --- /dev/null +++ b/database/migrations/2025_06_23_173758_drop_unused_retribution_tables.php @@ -0,0 +1,147 @@ +id(); + $table->string('code')->unique()->comment('Kode unik fungsi bangunan'); + $table->string('name', 255)->comment('Nama fungsi bangunan'); + $table->text('description')->nullable()->comment('Deskripsi detail fungsi bangunan'); + $table->unsignedBigInteger('parent_id')->nullable()->comment('ID parent untuk hierarki'); + $table->foreign('parent_id')->references('id')->on('building_functions')->onDelete('cascade'); + $table->integer('level')->default(0)->comment('Level hierarki (0=root, 1=child, dst)'); + $table->integer('sort_order')->default(0)->comment('Urutan tampilan'); + $table->decimal('base_tariff', 15, 2)->nullable()->comment('Tarif dasar per m2'); + + // Indexes untuk performa + $table->index(['parent_id', 'level']); + $table->index(['level', 'sort_order']); + $table->timestamps(); + }); + + // Recreate building_function_parameters table + Schema::create('building_function_parameters', function (Blueprint $table) { + $table->id(); + $table->foreignId('building_function_id')->constrained('building_functions')->onDelete('cascade'); + $table->decimal('fungsi_bangunan', 10, 6)->nullable()->comment('Parameter fungsi bangunan'); + $table->decimal('ip_permanen', 10, 6)->nullable()->comment('Parameter IP permanen'); + $table->decimal('ip_kompleksitas', 10, 6)->nullable()->comment('Parameter IP kompleksitas'); + $table->decimal('indeks_lokalitas', 10, 6)->nullable()->comment('Parameter indeks lokalitas'); + $table->decimal('asumsi_prasarana', 8, 6)->nullable()->comment('Parameter asumsi prasarana untuk perhitungan retribusi'); + $table->decimal('koefisien_dasar', 15, 6)->nullable()->comment('Koefisien dasar perhitungan'); + $table->timestamps(); + + // Unique constraint untuk 1:1 relationship + $table->unique('building_function_id'); + }); + + // Recreate retribution_formulas table + Schema::create('retribution_formulas', function (Blueprint $table) { + $table->id(); + $table->foreignId('building_function_id')->constrained('building_functions')->onDelete('cascade'); + $table->string('name', 255)->comment('Nama formula'); + $table->integer('floor_number')->comment('Nomor lantai (1, 2, 3, dst, 0=semua lantai)'); + $table->text('formula_expression')->comment('Rumus matematika untuk perhitungan'); + $table->timestamps(); + + // Indexes untuk performa + $table->index(['building_function_id', 'floor_number'], 'idx_building_floor'); + }); + + // Recreate retribution_proposals table + Schema::create('retribution_proposals', function (Blueprint $table) { + $table->id(); + $table->foreignId('spatial_planning_id')->nullable()->constrained('spatial_plannings')->onDelete('cascade'); + $table->foreignId('building_function_id')->constrained('building_functions')->onDelete('cascade'); + $table->foreignId('retribution_formula_id')->constrained('retribution_formulas')->onDelete('cascade'); + $table->string('proposal_number')->unique()->comment('Nomor usulan retribusi'); + $table->integer('floor_number')->comment('Nomor lantai (1, 2, 3, dst)'); + $table->decimal('floor_area', 15, 6)->comment('Luas lantai ini (m2)'); + $table->decimal('total_building_area', 15, 6)->comment('Total luas bangunan (m2)'); + $table->decimal('ip_ketinggian', 10, 6)->comment('IP ketinggian untuk lantai ini'); + $table->decimal('floor_retribution_amount', 15, 2)->comment('Jumlah retribusi untuk lantai ini'); + $table->decimal('total_retribution_amount', 15, 2)->comment('Total retribusi keseluruhan'); + $table->json('calculation_parameters')->nullable()->comment('Parameter yang digunakan dalam perhitungan'); + $table->json('calculation_breakdown')->nullable()->comment('Breakdown detail perhitungan'); + $table->text('notes')->nullable()->comment('Catatan tambahan'); + $table->datetime('calculated_at')->comment('Waktu perhitungan dilakukan'); + $table->timestamps(); + + // Indexes untuk performa + $table->index(['spatial_planning_id', 'floor_number'], 'idx_spatial_floor'); + $table->index(['building_function_id'], 'idx_function'); + $table->index('calculated_at'); + }); + + // Recreate floor_height_indices table + Schema::create('floor_height_indices', function (Blueprint $table) { + $table->id(); + $table->integer('floor_number')->comment('Nomor lantai'); + $table->decimal('ip_ketinggian', 10, 6)->comment('Indeks ketinggian per lantai'); + $table->text('description')->nullable()->comment('Deskripsi indeks ketinggian'); + $table->timestamps(); + + // Unique constraint untuk floor_number + $table->unique('floor_number'); + $table->index('floor_number'); + }); + } +};