"Error: Database configuration file is missing or invalid.")); exit(); } $host = $config['db_servername']; $db_name = $config['db_name']; $username = $config['db_username']; $password = $config['db_password']; $conn = null; try { $conn = new PDO("mysql:host=" . $host . ";dbname=" . $db_name, $username, $password); $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $conn->exec("set names utf8"); } catch(PDOException $exception) { http_response_code(503); echo json_encode(array("message" => "Database connection error: " . $exception->getMessage())); exit(); } $request_method = $_SERVER["REQUEST_METHOD"]; $action = $_GET['action'] ?? ''; // ================================ // GET ALL DATA UNTUK GRID // ================================ if ($request_method == 'GET' && $action === '') { try { $stmt = $conn->query(" SELECT dbk.id, dbk.company_id, dbk.trx_sales_id, dbk.konsumen_id, dbk.tgltransaksi, dbk.jatuh_tempo, dbk.kodebayar, dbk.type_bayar, dbk.nama_bayar, dbk.nilai_bayar, dbk.deskripsi, dbk.klasifikasi, // ✅ TAMBAHAN: Tampilkan klasifikasi dbk.disetor, dbk.unit_code, dbk.bukti_kodebayar, dbk.nilai_cicilan, dbk.saldo, dbk.created_at, dbk.updated_at, // Data Konsumen k.full_name AS nama_konsumen, k.nik AS konsumen_nik, // Data Transaksi tsp.notransaksi, tsp.status_sales, tsp.dp, tsp.Selisih_Realisasi, tsp.iscicilandp, // ✅ TAMBAHAN: Flag cicilan DP tsp.iscicilanselisih, // ✅ TAMBAHAN: Flag cicilan Selisih // Data Unit sp.unit_code AS unit_detail, sp.cluster_name, sp.block_no, sp.kavling_no, sp.house_no, // Data Project apm.housing_name AS project_name FROM data_bayar_konsumen dbk LEFT JOIN konsumen k ON dbk.konsumen_id = k.id LEFT JOIN trx_sales_property tsp ON dbk.trx_sales_id = tsp.id LEFT JOIN stock_property sp ON tsp.stock_property_id = sp.id LEFT JOIN asset_project_master apm ON sp.project_code = apm.project_code ORDER BY dbk.tgltransaksi DESC, dbk.id DESC "); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); // Format data untuk frontend $formattedData = array_map(function($row) { $jatuhTempoStatus = 'Tidak Ada Jatuh Tempo'; if ($row['jatuh_tempo']) { $jatuhTempo = new DateTime($row['jatuh_tempo']); $today = new DateTime(); if ($jatuhTempo < $today) { $jatuhTempoStatus = 'Lewat Jatuh Tempo'; } else { $jatuhTempoStatus = 'Belum Jatuh Tempo'; } } return [ 'id' => $row['id'], 'company_id' => $row['company_id'], 'trx_sales_id' => $row['trx_sales_id'], 'konsumen_id' => $row['konsumen_id'], 'tgltransaksi' => $row['tgltransaksi'] ? (new DateTime($row['tgltransaksi']))->format('Y-m-d H:i:s') : null, 'jatuh_tempo' => $row['jatuh_tempo'] ? (new DateTime($row['jatuh_tempo']))->format('Y-m-d') : null, 'kodebayar' => $row['kodebayar'], 'type_bayar' => $row['type_bayar'], 'nama_bayar' => $row['nama_bayar'], 'nilai_bayar' => (float)$row['nilai_bayar'], 'deskripsi' => $row['deskripsi'], 'klasifikasi' => $row['klasifikasi'] ?? 'DP', // ✅ TAMBAHAN: Klasifikasi 'disetor' => (int)$row['disetor'], 'unit_code' => $row['unit_code'], 'bukti_kodebayar' => $row['bukti_kodebayar'], 'nilai_cicilan' => (float)$row['nilai_cicilan'], 'saldo' => (float)$row['saldo'], 'created_at' => $row['created_at'], 'updated_at' => $row['updated_at'], // Relasi data 'nama_konsumen' => $row['nama_konsumen'], 'konsumen_nik' => $row['konsumen_nik'], 'notransaksi' => $row['notransaksi'], 'status_sales' => $row['status_sales'], 'dp_total' => (float)$row['dp'], 'selisih_realisasi' => (float)$row['Selisih_Realisasi'], 'iscicilandp' => (int)$row['iscicilandp'], // ✅ TAMBAHAN: Flag DP 'iscicilanselisih' => (int)$row['iscicilanselisih'], // ✅ TAMBAHAN: Flag Selisih 'unit_detail' => $row['unit_detail'], 'cluster_name' => $row['cluster_name'], 'block_no' => $row['block_no'], 'kavling_no' => $row['kavling_no'], 'house_no' => $row['house_no'], 'project_name' => $row['project_name'], // Computed fields untuk UI 'status_display' => $row['disetor'] ? 'Sudah Disetor' : 'Belum Disetor', 'persentase_bayar' => $row['nilai_cicilan'] > 0 ? round(($row['nilai_bayar'] / $row['nilai_cicilan']) * 100, 2) : 0, 'status_jatuh_tempo' => $jatuhTempoStatus ]; }, $data); echo json_encode($formattedData); } catch (Exception $e) { http_response_code(500); echo json_encode(array( "status" => "error", "message" => "Gagal mengambil data pembayaran: " . $e->getMessage() )); } exit(); } // ================================ // GET BY TRX_SALES_ID (UNTUK DETAIL TRANSAKSI) // ================================ if ($request_method == 'GET' && $action === 'by_trx') { $trx_sales_id = (int)($_GET['trx_sales_id'] ?? 0); if (!$trx_sales_id) { http_response_code(400); echo json_encode(array("message" => "trx_sales_id tidak disediakan.")); exit(); } try { // Perbaikan di sini: Pastikan semua LEFT JOIN sudah benar $stmt = $conn->prepare(" SELECT dbk.id, dbk.company_id, dbk.trx_sales_id, dbk.konsumen_id, dbk.tgltransaksi, dbk.jatuh_tempo, dbk.kodebayar, dbk.type_bayar, dbk.nama_bayar, dbk.nilai_bayar, dbk.deskripsi, dbk.klasifikasi, dbk.disetor, dbk.unit_code, dbk.bukti_kodebayar, dbk.nilai_cicilan, dbk.saldo, dbk.created_at, dbk.updated_at, k.full_name AS nama_konsumen, k.nik AS konsumen_nik, tsp.notransaksi, tsp.status_sales, tsp.dp, tsp.Selisih_Realisasi, tsp.iscicilandp, tsp.iscicilanselisih, sp.unit_code AS unit_detail, sp.cluster_name, sp.block_no, sp.kavling_no, sp.house_no, apm.housing_name AS project_name FROM data_bayar_konsumen dbk LEFT JOIN konsumen k ON dbk.konsumen_id = k.id LEFT JOIN trx_sales_property tsp ON dbk.trx_sales_id = tsp.id LEFT JOIN stock_property sp ON tsp.stock_property_id = sp.id LEFT JOIN asset_project_master apm ON sp.project_code = apm.project_code WHERE dbk.trx_sales_id = ? ORDER BY dbk.tgltransaksi ASC, dbk.id ASC "); $stmt->execute([$trx_sales_id]); $payments = $stmt->fetchAll(PDO::FETCH_ASSOC); // Format data $formattedPayments = array_map(function($row) { $jatuhTempoStatus = 'Tidak Ada Jatuh Tempo'; if ($row['jatuh_tempo']) { $jatuhTempo = new DateTime($row['jatuh_tempo']); $today = new DateTime(); if ($jatuhTempo < $today) { $jatuhTempoStatus = 'Lewat Jatuh Tempo'; } else { $jatuhTempoStatus = 'Belum Jatuh Tempo'; } } return [ 'id' => $row['id'], 'company_id' => $row['company_id'], 'trx_sales_id' => $row['trx_sales_id'], 'konsumen_id' => $row['konsumen_id'], 'tgltransaksi' => $row['tgltransaksi'] ? (new DateTime($row['tgltransaksi']))->format('Y-m-d H:i:s') : null, 'jatuh_tempo' => $row['jatuh_tempo'] ? (new DateTime($row['jatuh_tempo']))->format('Y-m-d') : null, 'kodebayar' => $row['kodebayar'], 'type_bayar' => $row['type_bayar'], 'nama_bayar' => $row['nama_bayar'], 'nilai_bayar' => (float)$row['nilai_bayar'], 'deskripsi' => $row['deskripsi'], 'klasifikasi' => $row['klasifikasi'] ?? 'DP', 'disetor' => (int)$row['disetor'], 'unit_code' => $row['unit_code'], 'bukti_kodebayar' => $row['bukti_kodebayar'], 'nilai_cicilan' => (float)$row['nilai_cicilan'], 'saldo' => (float)$row['saldo'], 'created_at' => $row['created_at'], 'updated_at' => $row['updated_at'], // Relasi 'nama_konsumen' => $row['nama_konsumen'], 'konsumen_nik' => $row['konsumen_nik'], 'notransaksi' => $row['notransaksi'], 'status_sales' => $row['status_sales'], 'dp_total' => (float)$row['dp'], 'selisih_realisasi' => (float)$row['Selisih_Realisasi'], 'iscicilandp' => (int)$row['iscicilandp'], 'iscicilanselisih' => (int)$row['iscicilanselisih'], 'unit_detail' => $row['unit_detail'], 'cluster_name' => $row['cluster_name'], 'block_no' => $row['block_no'], 'kavling_no' => $row['kavling_no'], 'house_no' => $row['house_no'], 'project_name' => $row['project_name'], // Computed 'status_display' => $row['disetor'] ? 'Sudah Disetor' : 'Belum Disetor', 'persentase_bayar' => $row['nilai_cicilan'] > 0 ? round(($row['nilai_bayar'] / $row['nilai_cicilan']) * 100, 2) : 0, 'status_jatuh_tempo' => $jatuhTempoStatus ]; }, $payments); // Hitung summary untuk transaksi ini $totalCicilan = array_sum(array_column($formattedPayments, 'nilai_cicilan')); $totalBayar = array_sum(array_column($formattedPayments, 'nilai_bayar')); echo json_encode([ "status" => "success", "data" => $formattedPayments, "summary" => [ 'total_records' => count($formattedPayments), 'total_cicilan' => $totalCicilan, 'total_bayar' => $totalBayar, 'persentase_lunas' => $totalCicilan > 0 ? round(($totalBayar / $totalCicilan) * 100, 2) : 0, 'dp_lunas' => $totalBayar >= ($formattedPayments[0]['dp_total'] ?? 0), 'selisih_lunas' => $totalBayar >= ($formattedPayments[0]['selisih_realisasi'] ?? 0) ] ]); } catch (PDOException $e) { http_response_code(500); echo json_encode(array( "status" => "error", "message" => "Database query error: " . $e->getMessage() )); } exit(); } // ================================ // GET SINGLE BY ID // ================================ if ($request_method == 'GET' && isset($_GET["id"]) && !empty($_GET["id"]) && $action === '') { $id = (int)$_GET["id"]; try { $stmt = $conn->prepare(" SELECT dbk.*, dbk.klasifikasi, // ✅ TAMBAHAN: Klasifikasi k.full_name AS nama_konsumen, k.nik AS konsumen_nik, tsp.notransaksi, tsp.status_sales, tsp.dp, tsp.Selisih_Realisasi, tsp.iscicilandp, // ✅ TAMBAHAN: Flag DP tsp.iscicilanselisih, // ✅ TAMBAHAN: Flag Selisih sp.unit_code AS unit_detail, sp.cluster_name, sp.block_no, sp.kavling_no, sp.house_no, apm.housing_name AS project_name FROM data_bayar_konsumen dbk LEFT JOIN konsumen k ON dbk.konsumen_id = k.id LEFT JOIN trx_sales_property tsp ON dbk.trx_sales_id = tsp.id LEFT JOIN stock_property sp ON tsp.stock_property_id = sp.id LEFT JOIN asset_project_master apm ON sp.project_code = apm.project_code WHERE dbk.id = ? "); $stmt->execute([$id]); if ($stmt->rowCount() > 0) { $row = $stmt->fetch(PDO::FETCH_ASSOC); $jatuhTempoStatus = 'Tidak Ada Jatuh Tempo'; if ($row['jatuh_tempo']) { $jatuhTempo = new DateTime($row['jatuh_tempo']); $today = new DateTime(); if ($jatuhTempo < $today) { $jatuhTempoStatus = 'Lewat Jatuh Tempo'; } else { $jatuhTempoStatus = 'Belum Jatuh Tempo'; } } $response = [ 'id' => $row['id'], 'company_id' => $row['company_id'], 'trx_sales_id' => $row['trx_sales_id'], 'konsumen_id' => $row['konsumen_id'], 'tgltransaksi' => $row['tgltransaksi'] ? (new DateTime($row['tgltransaksi']))->format('Y-m-d H:i:s') : null, 'jatuh_tempo' => $row['jatuh_tempo'] ? (new DateTime($row['jatuh_tempo']))->format('Y-m-d') : null, 'kodebayar' => $row['kodebayar'], 'type_bayar' => $row['type_bayar'], 'nama_bayar' => $row['nama_bayar'], 'nilai_bayar' => (float)$row['nilai_bayar'], 'deskripsi' => $row['deskripsi'], 'klasifikasi' => $row['klasifikasi'] ?? 'DP', // ✅ TAMBAHAN: Klasifikasi 'disetor' => (int)$row['disetor'], 'unit_code' => $row['unit_code'], 'bukti_kodebayar' => $row['bukti_kodebayar'], 'nilai_cicilan' => (float)$row['nilai_cicilan'], 'saldo' => (float)$row['saldo'], 'created_at' => $row['created_at'], 'updated_at' => $row['updated_at'], // Relasi 'nama_konsumen' => $row['nama_konsumen'], 'konsumen_nik' => $row['konsumen_nik'], 'notransaksi' => $row['notransaksi'], 'status_sales' => $row['status_sales'], 'dp_total' => (float)$row['dp'], 'selisih_realisasi' => (float)$row['Selisih_Realisasi'], 'iscicilandp' => (int)$row['iscicilandp'], // ✅ TAMBAHAN: Flag DP 'iscicilanselisih' => (int)$row['iscicilanselisih'], // ✅ TAMBAHAN: Flag Selisih 'unit_detail' => $row['unit_detail'], 'cluster_name' => $row['cluster_name'], 'block_no' => $row['block_no'], 'kavling_no' => $row['kavling_no'], 'house_no' => $row['house_no'], 'project_name' => $row['project_name'], // Computed 'status_display' => $row['disetor'] ? 'Sudah Disetor' : 'Belum Disetor', 'persentase_bayar' => $row['nilai_cicilan'] > 0 ? round(($row['nilai_bayar'] / $row['nilai_cicilan']) * 100, 2) : 0, 'status_jatuh_tempo' => $jatuhTempoStatus ]; echo json_encode(array( "status" => "success", "data" => $response )); } else { http_response_code(404); echo json_encode(array( "status" => "error", "message" => "Data pembayaran tidak ditemukan." )); } } catch (PDOException $e) { http_response_code(500); echo json_encode(array("message" => "Database query error: " . $e->getMessage())); } exit(); } // ================================ // GET SUMMARY BY TRX (UNTUK KOMITMEN CICILAN) // ================================ if ($request_method == 'GET' && $action === 'summary_by_trx') { $trx_sales_id = (int)($_GET['trx_sales_id'] ?? 0); if (!$trx_sales_id) { http_response_code(400); echo json_encode(array("message" => "trx_sales_id tidak disediakan.")); exit(); } try { // Ambil data transaksi $stmt_trx = $conn->prepare(" SELECT id, company_id, konsumen_id, dp, Selisih_Realisasi, status_sales, notransaksi, stock_property_id, iscicilandp, // ✅ TAMBAHAN: Flag DP iscicilanselisih // ✅ TAMBAHAN: Flag Selisih FROM trx_sales_property WHERE id = ? "); $stmt_trx->execute([$trx_sales_id]); $trx_data = $stmt_trx->fetch(PDO::FETCH_ASSOC); if (!$trx_data) { http_response_code(404); echo json_encode(array("message" => "Transaksi tidak ditemukan.")); exit(); } // Ambil unit code $stmt_unit = $conn->prepare("SELECT unit_code FROM stock_property WHERE id = ?"); $stmt_unit->execute([$trx_data['stock_property_id']]); $unit_data = $stmt_unit->fetch(PDO::FETCH_ASSOC); $unit_code = $unit_data['unit_code'] ?? 'UNKNOWN'; // Hitung pembayaran DP yang sudah ada $stmt_dp = $conn->prepare(" SELECT COUNT(*) as total_dp, SUM(nilai_bayar) as total_bayar_dp, SUM(nilai_cicilan) as total_cicilan_dp, SUM(saldo) as total_sisa_dp FROM data_bayar_konsumen WHERE trx_sales_id = ? AND type_bayar REGEXP '^DP[0-9]+$' "); $stmt_dp->execute([$trx_sales_id]); $dp_summary = $stmt_dp->fetch(PDO::FETCH_ASSOC); // Hitung pembayaran selisih yang sudah ada $stmt_selisih = $conn->prepare(" SELECT COUNT(*) as total_selisih, SUM(nilai_bayar) as total_bayar_selisih, SUM(nilai_cicilan) as total_cicilan_selisih, SUM(saldo) as total_sisa_selisih FROM data_bayar_konsumen WHERE trx_sales_id = ? AND type_bayar IN ('PELUNASAN', 'LAINNYA') "); $stmt_selisih->execute([$trx_sales_id]); $selisih_summary = $stmt_selisih->fetch(PDO::FETCH_ASSOC); // Hitung next DP number $stmt_next_dp = $conn->prepare(" SELECT MAX(CAST(REGEXP_REPLACE(type_bayar, 'DP', '') AS UNSIGNED)) as max_dp FROM data_bayar_konsumen WHERE trx_sales_id = ? AND type_bayar REGEXP '^DP[0-9]+$' "); $stmt_next_dp->execute([$trx_sales_id]); $next_dp_result = $stmt_next_dp->fetch(PDO::FETCH_ASSOC); $next_dp_number = ($next_dp_result['max_dp'] ?? 0) + 1; $next_dp_number = min($next_dp_number, 12); echo json_encode([ "status" => "success", "data" => [ "trx_id" => $trx_data['id'], "company_id" => $trx_data['company_id'], "konsumen_id" => $trx_data['konsumen_id'], "unit_code" => $unit_code, "notransaksi" => $trx_data['notransaksi'], "status_sales" => $trx_data['status_sales'], "iscicilandp" => (int)$trx_data['iscicilandp'], // ✅ TAMBAHAN "iscicilanselisih" => (int)$trx_data['iscicilanselisih'], // ✅ TAMBAHAN // Nilai transaksi "dp_total" => (float)$trx_data['dp'], "selisih_realisasi" => (float)$trx_data['Selisih_Realisasi'], // Summary DP "dp_summary" => [ "total_records" => (int)$dp_summary['total_dp'], "total_cicilan" => (float)$dp_summary['total_cicilan_dp'], "total_bayar" => (float)$dp_summary['total_bayar_dp'], "total_sisa" => (float)$dp_summary['total_sisa_dp'], "dp_lunas" => (float)$dp_summary['total_bayar_dp'] >= (float)$trx_data['dp'], "next_dp_number" => $next_dp_number ], // Summary Selisih "selisih_summary" => [ "total_records" => (int)$selisih_summary['total_selisih'], "total_cicilan" => (float)$selisih_summary['total_cicilan_selisih'], "total_bayar" => (float)$selisih_summary['total_bayar_selisih'], "total_sisa" => (float)$selisih_summary['total_sisa_selisih'], "selisih_lunas" => (float)$selisih_summary['total_bayar_selisih'] >= (float)$trx_data['Selisih_Realisasi'] ] ] ]); } catch (PDOException $e) { http_response_code(500); echo json_encode(array( "status" => "error", "message" => "Database query error: " . $e->getMessage() )); } exit(); } // ================================ // POST - BATCH CREATE UNTUK KOMITMEN CICILAN - ✅ REVISION LENGKAP // ================================ if ($request_method == 'POST' && $action === 'batch-create') { $input = file_get_contents("php://input"); $data = json_decode($input, true); // 🔥 DEBUG: Log data yang diterima error_log("🔍 [BATCH-CREATE] Data diterima: " . print_r($data, true)); if (json_last_error() !== JSON_ERROR_NONE || !isset($data['records']) || !is_array($data['records'])) { http_response_code(400); echo json_encode(array( "status" => "error", "message" => "Data JSON tidak valid atau format records salah." )); exit(); } $records = $data['records']; $trx_sales_id = $data['trx_sales_id'] ?? null; if (!$trx_sales_id || !is_numeric($trx_sales_id)) { http_response_code(400); echo json_encode(array( "status" => "error", "message" => "trx_sales_id wajib disediakan untuk batch create." )); exit(); } // Validasi transaksi ada $stmt_trx = $conn->prepare("SELECT id, company_id, konsumen_id, stock_property_id FROM trx_sales_property WHERE id = ?"); $stmt_trx->execute([$trx_sales_id]); $trx_data = $stmt_trx->fetch(PDO::FETCH_ASSOC); if (!$trx_data) { http_response_code(404); echo json_encode(array( "status" => "error", "message" => "Transaksi dengan ID {$trx_sales_id} tidak ditemukan." )); exit(); } // Ambil unit_code $stmt_unit = $conn->prepare("SELECT unit_code FROM stock_property WHERE id = ?"); $stmt_unit->execute([$trx_data['stock_property_id']]); $unit_data = $stmt_unit->fetch(PDO::FETCH_ASSOC); $unit_code = $unit_data['unit_code'] ?? null; if (!$unit_code) { http_response_code(400); echo json_encode(array( "status" => "error", "message" => "Unit code tidak ditemukan untuk transaksi ini." )); exit(); } try { $conn->beginTransaction(); $inserted_ids = []; $valid_records = 0; $has_dp_cicilan = false; // ✅ TAMBAHAN: Track apakah ada cicilan DP $has_selisih_cicilan = false; // ✅ TAMBAHAN: Track apakah ada cicilan SELISIH // ✅ REVISION: Tambah klasifikasi ke INSERT query $stmt_insert = $conn->prepare(" INSERT INTO data_bayar_konsumen ( company_id, trx_sales_id, konsumen_id, tgltransaksi, jatuh_tempo, kodebayar, type_bayar, nama_bayar, nilai_bayar, deskripsi, klasifikasi, disetor, unit_code, bukti_kodebayar, nilai_cicilan, saldo, created_at ) VALUES ( :company_id, :trx_sales_id, :konsumen_id, NOW(), :jatuh_tempo, :kodebayar, :type_bayar, :nama_bayar, :nilai_bayar, :deskripsi, :klasifikasi, :disetor, :unit_code, :bukti_kodebayar, :nilai_cicilan, :saldo, NOW() ) "); foreach ($records as $index => $record) { // 🔥 DEBUG: Log setiap record error_log("🔍 [BATCH-CREATE] Record {$index}: " . print_r($record, true)); // Validasi record $required_fields = ['type_bayar', 'nilai_cicilan', 'saldo', 'kodebayar']; // ✅ Tambah kodebayar foreach ($required_fields as $field) { if (!isset($record[$field])) { error_log("❌ Record {$index}: Field {$field} tidak disediakan, dilewati."); continue; } } // ✅ TAMBAHAN: Validasi klasifikasi $klasifikasi = strtoupper($record['klasifikasi'] ?? 'DP'); if (!in_array($klasifikasi, ['DP', 'SELISIH'])) { error_log("❌ Record {$index}: klasifikasi '{$record['klasifikasi']}' tidak valid (harus DP/SELISIH), dilewati."); continue; } // Validasi type_bayar $valid_types = ['DP RESERVE', 'TANDA JADI', 'DP1', 'DP2', 'DP3', 'DP4', 'DP5', 'DP6', 'DP7', 'DP8', 'DP9', 'DP10', 'DP11', 'DP12', 'PELUNASAN', 'LAINNYA','SALDO AWAL']; if (!in_array($record['type_bayar'], $valid_types)) { error_log("❌ Record {$index}: type_bayar '{$record['type_bayar']}' tidak valid, dilewati."); continue; } // Cek duplikasi kodebayar $stmt_check = $conn->prepare("SELECT id FROM data_bayar_konsumen WHERE kodebayar = ?"); $stmt_check->execute([$record['kodebayar']]); if ($stmt_check->rowCount() > 0) { error_log("❌ Record {$index}: kodebayar '{$record['kodebayar']}' sudah ada, dilewati."); continue; } // Generate bukti_kodebayar jika kosong if (empty($record['bukti_kodebayar'])) { $record['bukti_kodebayar'] = 'BK-' . $unit_code . '-' . $record['type_bayar'] . '-' . date('YmdHis') . '-' . ($index + 1); } // Set default values $default_values = [ 'company_id' => $trx_data['company_id'], 'trx_sales_id' => $trx_sales_id, 'konsumen_id' => $trx_data['konsumen_id'], 'jatuh_tempo' => $record['jatuh_tempo'] ?? null, 'nama_bayar' => $record['nama_bayar'] ?? 'TRANSFER', 'nilai_bayar' => $record['nilai_bayar'] ?? 0.00, 'deskripsi' => $record['deskripsi'] ?? "Cicilan {$record['type_bayar']} - Unit {$unit_code}", 'disetor' => $record['disetor'] ?? 1, 'unit_code' => $unit_code, 'klasifikasi' => $klasifikasi // ✅ TAMBAHAN: Dari validasi di atas ]; // Merge dengan data dari request $final_record = array_merge($default_values, $record); // ✅ TAMBAHAN: Track jenis cicilan untuk update flag if ($klasifikasi === 'DP') { $has_dp_cicilan = true; } elseif ($klasifikasi === 'SELISIH') { $has_selisih_cicilan = true; } // 🔥 DEBUG: Log final record sebelum insert error_log("✅ [BATCH-CREATE] Final record {$index}: " . print_r([ 'type_bayar' => $final_record['type_bayar'], 'kodebayar' => $final_record['kodebayar'], 'klasifikasi' => $final_record['klasifikasi'], // ✅ DEBUG klasifikasi 'jatuh_tempo' => $final_record['jatuh_tempo'], 'nilai_cicilan' => $final_record['nilai_cicilan'], 'saldo' => $final_record['saldo'] ], true)); // Execute insert $stmt_insert->bindValue(':company_id', $final_record['company_id']); $stmt_insert->bindValue(':trx_sales_id', $final_record['trx_sales_id']); $stmt_insert->bindValue(':konsumen_id', $final_record['konsumen_id']); $stmt_insert->bindValue(':jatuh_tempo', $final_record['jatuh_tempo'], PDO::PARAM_STR); $stmt_insert->bindValue(':kodebayar', $final_record['kodebayar']); $stmt_insert->bindValue(':type_bayar', $final_record['type_bayar']); $stmt_insert->bindValue(':nama_bayar', $final_record['nama_bayar']); $stmt_insert->bindValue(':nilai_bayar', (float)$final_record['nilai_bayar']); $stmt_insert->bindValue(':deskripsi', $final_record['deskripsi']); $stmt_insert->bindValue(':klasifikasi', $final_record['klasifikasi']); // ✅ TAMBAHAN: Bind klasifikasi $stmt_insert->bindValue(':disetor', (int)$final_record['disetor']); $stmt_insert->bindValue(':unit_code', $final_record['unit_code']); $stmt_insert->bindValue(':bukti_kodebayar', $final_record['bukti_kodebayar']); $stmt_insert->bindValue(':nilai_cicilan', (float)$final_record['nilai_cicilan']); $stmt_insert->bindValue(':saldo', (float)$final_record['saldo']); $stmt_insert->execute(); $inserted_id = $conn->lastInsertId(); $inserted_ids[] = $inserted_id; $valid_records++; error_log("🎉 [BATCH-CREATE] Record {$index} berhasil diinsert dengan ID: {$inserted_id} (klasifikasi: {$final_record['klasifikasi']})"); } $conn->commit(); // ✅ TAMBAHAN: Update flag iscicilandp dan iscicilanselisih di trx_sales_property $flags_updated = []; if ($has_dp_cicilan) { $stmt_flag_dp = $conn->prepare("UPDATE trx_sales_property SET iscicilandp = 1 WHERE id = ?"); $stmt_flag_dp->execute([$trx_sales_id]); $flags_updated[] = 'iscicilandp'; error_log("🔧 [BATCH-CREATE] Flag iscicilandp = 1 diupdate untuk trx_sales_id: {$trx_sales_id}"); } if ($has_selisih_cicilan) { $stmt_flag_selisih = $conn->prepare("UPDATE trx_sales_property SET iscicilanselisih = 1 WHERE id = ?"); $stmt_flag_selisih->execute([$trx_sales_id]); $flags_updated[] = 'iscicilanselisih'; error_log("🔧 [BATCH-CREATE] Flag iscicilanselisih = 1 diupdate untuk trx_sales_id: {$trx_sales_id}"); } if ($valid_records > 0) { http_response_code(201); echo json_encode(array( "status" => "success", "message" => "Berhasil menambahkan {$valid_records} record cicilan baru.", "total_sent" => count($records), "valid_records" => $valid_records, "inserted_ids" => $inserted_ids, "skipped_records" => count($records) - $valid_records, "flags_updated" => $flags_updated, // ✅ TAMBAHAN: Info flag yang diupdate "klasifikasi_processed" => [ // ✅ TAMBAHAN: Info klasifikasi yang diproses 'DP' => $has_dp_cicilan, 'SELISIH' => $has_selisih_cicilan ] )); } else { http_response_code(400); echo json_encode(array( "status" => "error", "message" => "Tidak ada record yang valid untuk disimpan.", "total_sent" => count($records), "valid_records" => 0 )); } } catch (Exception $e) { $conn->rollBack(); http_response_code(500); echo json_encode(array( "status" => "error", "message" => "Gagal menambahkan record cicilan: " . $e->getMessage(), "debug" => $e->getTraceAsString() )); error_log("❌ [BATCH-CREATE] ERROR: " . $e->getMessage()); } exit(); } // ================================ // POST - CREATE SINGLE (UNTUK PAYMENT MANAGEMENT) - TIDAK DIUBAH // ================================ if ($request_method == 'POST' && $action === 'create') { // ... kode asli kamu tetap sama, tidak diubah ... $input = file_get_contents("php://input"); $data = json_decode($input, true); if (json_last_error() !== JSON_ERROR_NONE) { http_response_code(400); echo json_encode(array("message" => "Data JSON tidak valid.")); exit(); } $required_fields = ['trx_sales_id', 'konsumen_id', 'kodebayar', 'type_bayar']; foreach ($required_fields as $field) { if (empty($data[$field])) { http_response_code(400); echo json_encode(array( "status" => "error", "message" => "Field {$field} wajib diisi." )); exit(); } } // Validasi transaksi $stmt_trx = $conn->prepare("SELECT id, company_id, konsumen_id, stock_property_id FROM trx_sales_property WHERE id = ?"); $stmt_trx->execute([$data['trx_sales_id']]); $trx_data = $stmt_trx->fetch(PDO::FETCH_ASSOC); if (!$trx_data) { http_response_code(404); echo json_encode(array( "status" => "error", "message" => "Transaksi tidak ditemukan." )); exit(); } // Ambil unit_code $stmt_unit = $conn->prepare("SELECT unit_code FROM stock_property WHERE id = ?"); $stmt_unit->execute([$trx_data['stock_property_id']]); $unit_data = $stmt_unit->fetch(PDO::FETCH_ASSOC); $unit_code = $unit_data['unit_code'] ?? null; try { $conn->beginTransaction(); // Set default values $default_values = [ 'company_id' => $trx_data['company_id'], 'konsumen_id' => $trx_data['konsumen_id'], 'jatuh_tempo' => $data['jatuh_tempo'] ?? null, 'nama_bayar' => $data['nama_bayar'] ?? 'TRANSFER', 'nilai_bayar' => $data['nilai_bayar'] ?? 0.00, 'deskripsi' => $data['deskripsi'] ?? "Pembayaran {$data['type_bayar']} - Unit {$unit_code}", 'disetor' => $data['disetor'] ?? 0, 'unit_code' => $unit_code, 'bukti_kodebayar' => $data['bukti_kodebayar'] ?? 'BK-' . $unit_code . '-' . $data['type_bayar'] . '-' . date('YmdHis'), 'nilai_cicilan' => $data['nilai_cicilan'] ?? 0.00, 'saldo' => $data['saldo'] ?? 0.00, 'klasifikasi' => $data['klasifikasi'] ?? 'DP' // ✅ TAMBAHAN: Default klasifikasi ]; $final_data = array_merge($default_values, $data); // ✅ REVISION: Tambah klasifikasi ke INSERT single $stmt = $conn->prepare(" INSERT INTO data_bayar_konsumen ( company_id, trx_sales_id, konsumen_id, tgltransaksi, jatuh_tempo, kodebayar, type_bayar, nama_bayar, nilai_bayar, deskripsi, klasifikasi, disetor, unit_code, bukti_kodebayar, nilai_cicilan, saldo ) VALUES ( :company_id, :trx_sales_id, :konsumen_id, NOW(), :jatuh_tempo, :kodebayar, :type_bayar, :nama_bayar, :nilai_bayar, :deskripsi, :klasifikasi, :disetor, :unit_code, :bukti_kodebayar, :nilai_cicilan, :saldo ) "); $stmt->bindValue(':company_id', $final_data['company_id']); $stmt->bindValue(':trx_sales_id', $final_data['trx_sales_id']); $stmt->bindValue(':konsumen_id', $final_data['konsumen_id']); $stmt->bindValue(':jatuh_tempo', $final_data['jatuh_tempo'], PDO::PARAM_STR); $stmt->bindValue(':kodebayar', $final_data['kodebayar']); $stmt->bindValue(':type_bayar', $final_data['type_bayar']); $stmt->bindValue(':nama_bayar', $final_data['nama_bayar']); $stmt->bindValue(':nilai_bayar', (float)$final_data['nilai_bayar']); $stmt->bindValue(':deskripsi', $final_data['deskripsi']); $stmt->bindValue(':klasifikasi', $final_data['klasifikasi']); // ✅ TAMBAHAN $stmt->bindValue(':disetor', (int)$final_data['disetor']); $stmt->bindValue(':unit_code', $final_data['unit_code']); $stmt->bindValue(':bukti_kodebayar', $final_data['bukti_kodebayar']); $stmt->bindValue(':nilai_cicilan', (float)$final_data['nilai_cicilan']); $stmt->bindValue(':saldo', (float)$final_data['saldo']); $stmt->execute(); $payment_id = $conn->lastInsertId(); $conn->commit(); http_response_code(201); echo json_encode(array( "status" => "success", "message" => "Pembayaran berhasil ditambahkan.", "data" => [ "id" => $payment_id, "kodebayar" => $final_data['kodebayar'], "klasifikasi" => $final_data['klasifikasi'], // ✅ TAMBAHAN "jatuh_tempo" => $final_data['jatuh_tempo'] ] )); } catch (Exception $e) { $conn->rollBack(); http_response_code(500); echo json_encode(array( "status" => "error", "message" => $e->getMessage() )); } exit(); } // ================================ // PUT - UPDATE PEMBAYARAN (REVISI SEDIKIT) // ================================ if ($request_method == 'PUT') { $input = file_get_contents("php://input"); $data = json_decode($input, true); // 🔥 DEBUG: Log data JSON mentah yang diterima error_log("🔍 [PUT] Raw JSON received: " . $input); if (json_last_error() !== JSON_ERROR_NONE || !isset($data['id'])) { http_response_code(400); echo json_encode(array("message" => "Data JSON tidak valid atau ID tidak disediakan.")); exit(); } $id = (int)$data['id']; // Ambil data lama $stmt_old = $conn->prepare("SELECT trx_sales_id, disetor FROM data_bayar_konsumen WHERE id = ?"); $stmt_old->execute([$id]); $old_data = $stmt_old->fetch(PDO::FETCH_ASSOC); if (!$old_data) { http_response_code(404); echo json_encode(array( "status" => "error", "message" => "Data pembayaran dengan ID {$id} tidak ditemukan." )); exit(); } try { $conn->beginTransaction(); // Perbaikan SQL Query: Pastikan tidak ada komentar dalam string query // Semua komentar sudah dipindahkan ke luar string query $stmt = $conn->prepare(" UPDATE data_bayar_konsumen SET tgltransaksi = :tgltransaksi, jatuh_tempo = :jatuh_tempo, nama_bayar = :nama_bayar, nilai_bayar = :nilai_bayar, deskripsi = :deskripsi, klasifikasi = :klasifikasi, disetor = :disetor, bukti_kodebayar = :bukti_kodebayar, nilai_cicilan = :nilai_cicilan, saldo = :saldo, updated_at = NOW() WHERE id = :id "); // Siapkan nilai untuk binding $tgltransaksi_val = $data['tgltransaksi'] ?? date('Y-m-d H:i:s'); $jatuh_tempo_val = $data['jatuh_tempo'] ?? null; // Pastikan ini sudah diformat YYYY-MM-DD dari frontend $nama_bayar_val = $data['nama_bayar'] ?? 'TRANSFER'; $nilai_bayar_val = (float)($data['nilai_bayar'] ?? 0.00); $deskripsi_val = $data['deskripsi'] ?? ''; $klasifikasi_val = strtoupper($data['klasifikasi'] ?? 'DP'); if (!in_array($klasifikasi_val, ['DP', 'SELISIH'])) $klasifikasi_val = 'DP'; $disetor_val = (int)($data['disetor'] ?? 0); $bukti_kodebayar_val = $data['bukti_kodebayar'] ?? null; $nilai_cicilan_val = (float)($data['nilai_cicilan'] ?? 0.00); $saldo_val = (float)($data['saldo'] ?? 0.00); // Bind parameter $stmt->bindValue(':tgltransaksi', $tgltransaksi_val); $stmt->bindValue(':jatuh_tempo', $jatuh_tempo_val, PDO::PARAM_STR); $stmt->bindValue(':nama_bayar', $nama_bayar_val); $stmt->bindValue(':nilai_bayar', $nilai_bayar_val); $stmt->bindValue(':deskripsi', $deskripsi_val); $stmt->bindValue(':klasifikasi', $klasifikasi_val); $stmt->bindValue(':disetor', $disetor_val); $stmt->bindValue(':bukti_kodebayar', $bukti_kodebayar_val); $stmt->bindValue(':nilai_cicilan', $nilai_cicilan_val); $stmt->bindValue(':saldo', $saldo_val); $stmt->bindValue(':id', $id); // Eksekusi query $stmt->execute(); $rows_affected = $stmt->rowCount(); // Jumlah baris yang terpengaruh // 🔥 DEBUG: Log jumlah baris yang terpengaruh error_log("✅ [PUT] UPDATE query executed for ID {$id}. Rows affected: {$rows_affected}"); $conn->commit(); // 🔥 DEBUG: Log keberhasilan commit error_log("🎉 [PUT] Transaction committed successfully for ID {$id}."); http_response_code(200); echo json_encode(array( "status" => "success", "message" => ($rows_affected > 0) ? "Pembayaran berhasil diupdate." : "Tidak ada perubahan data untuk pembayaran ID {$id}.", "data" => [ "id" => $id, "saldo" => $saldo_val, "klasifikasi" => $klasifikasi_val, "rows_affected" => $rows_affected // Tambahkan ini agar frontend tahu ada perubahan atau tidak ] )); } catch (Exception $e) { $conn->rollBack(); // 🔥 DEBUG: Log error lengkap jika terjadi exception error_log("❌ [PUT] Transaction rolled back for ID {$id}. Error: " . $e->getMessage() . " Stack: " . $e->getTraceAsString()); http_response_code(500); echo json_encode(array( "status" => "error", "message" => "Gagal mengupdate pembayaran: " . $e->getMessage(), "debug_info" => $e->getTraceAsString() // Kirim debug info ke frontend (sementara) )); } exit(); } // ================================ // DELETE - HAPUS PEMBAYARAN (TIDAK DIUBAH) // ================================ if ($request_method == 'DELETE') { // ... kode asli kamu tetap sama ... if (!isset($_GET["id"]) || empty($_GET["id"])) { http_response_code(400); echo json_encode(array("message" => "ID pembayaran harus disediakan untuk dihapus.")); exit(); } $id = (int)$_GET["id"]; try { $conn->beginTransaction(); $stmt_get = $conn->prepare("SELECT trx_sales_id, disetor FROM data_bayar_konsumen WHERE id = ?"); $stmt_get->execute([$id]); $payment_to_delete = $stmt_get->fetch(PDO::FETCH_ASSOC); if (!$payment_to_delete) { http_response_code(404); echo json_encode(array( "status" => "error", "message" => "Pembayaran tidak ditemukan." )); $conn->rollBack(); exit(); } if ($payment_to_delete['disetor'] == 1) { http_response_code(400); echo json_encode(array( "status" => "error", "message" => "Tidak bisa menghapus pembayaran yang sudah disetor (dianggap final)." )); $conn->rollBack(); exit(); } $stmt_delete = $conn->prepare("DELETE FROM data_bayar_konsumen WHERE id = ?"); $stmt_delete->execute([$id]); $deleted_rows = $stmt_delete->rowCount(); $conn->commit(); if ($deleted_rows > 0) { http_response_code(200); echo json_encode(array( "status" => "success", "message" => "Pembayaran berhasil dihapus." )); } else { http_response_code(404); echo json_encode(array( "status" => "error", "message" => "Tidak ada data yang dihapus." )); } } catch (Exception $e) { $conn->rollBack(); http_response_code(500); echo json_encode(array( "status" => "error", "message" => $e->getMessage() )); } exit(); } // ================================ // GET - DROPDOWN UNTUK TYPE BAYAR (TIDAK DIUBAH) // ================================ if ($request_method == 'GET' && $action === 'get_type_bayar') { // ... kode asli kamu tetap sama ... $valid_types = [ 'DP RESERVE' => 'DP Reserve', 'TANDA JADI' => 'Tanda Jadi', 'DP1' => 'Cicilan DP 1', 'DP2' => 'Cicilan DP 2', 'DP3' => 'Cicilan DP 3', 'DP4' => 'Cicilan DP 4', 'DP5' => 'Cicilan DP 5', 'DP6' => 'Cicilan DP 6', 'DP7' => 'Cicilan DP 7', 'DP8' => 'Cicilan DP 8', 'DP9' => 'Cicilan DP 9', 'DP10' => 'Cicilan DP 10', 'DP11' => 'Cicilan DP 11', 'DP12' => 'Cicilan DP 12', 'PELUNASAN' => 'Pelunasan', 'LAINNYA' => 'Lainnya', 'SALDO AWAL'=>'Saldo Awal' ]; echo json_encode(array( "status" => "success", "data" => $valid_types )); exit(); } // ================================ // GET - STATISTICS UNTUK DASHBOARD (REVISI SEDIKIT) // ================================ if ($request_method == 'GET' && $action === 'statistics') { try { $stmt_total = $conn->query("SELECT COUNT(*) as total, SUM(nilai_bayar) as total_amount FROM data_bayar_konsumen"); $total_stats = $stmt_total->fetch(PDO::FETCH_ASSOC); $stmt_monthly = $conn->prepare(" SELECT COUNT(*) as monthly_count, SUM(nilai_bayar) as monthly_amount FROM data_bayar_konsumen WHERE MONTH(tgltransaksi) = MONTH(CURDATE()) AND YEAR(tgltransaksi) = YEAR(CURDATE()) "); $stmt_monthly->execute(); $monthly_stats = $stmt_monthly->fetch(PDO::FETCH_ASSOC); $stmt_overdue = $conn->prepare(" SELECT COUNT(*) as overdue_count, SUM(nilai_bayar) as overdue_amount FROM data_bayar_konsumen WHERE jatuh_tempo < CURDATE() AND saldo > 0 "); $stmt_overdue->execute(); $overdue_stats = $stmt_overdue->fetch(PDO::FETCH_ASSOC); // ✅ TAMBAHAN: Statistics per klasifikasi $stmt_klasifikasi = $conn->query(" SELECT klasifikasi, COUNT(*) as count, SUM(nilai_bayar) as amount, SUM(nilai_cicilan) as total_cicilan FROM data_bayar_konsumen GROUP BY klasifikasi "); $klasifikasi_stats = $stmt_klasifikasi->fetchAll(PDO::FETCH_ASSOC); $stmt_type = $conn->query(" SELECT type_bayar, COUNT(*) as count, SUM(nilai_bayar) as amount FROM data_bayar_konsumen GROUP BY type_bayar ORDER BY count DESC "); $type_stats = $stmt_type->fetchAll(PDO::FETCH_ASSOC); echo json_encode(array( "status" => "success", "data" => [ "total_records" => (int)$total_stats['total'], "total_amount" => (float)$total_stats['total_amount'], "monthly_records" => (int)$monthly_stats['monthly_count'], "monthly_amount" => (float)$monthly_stats['monthly_amount'], "overdue_records" => (int)$overdue_stats['overdue_count'], "overdue_amount" => (float)$overdue_stats['overdue_amount'], "klasifikasi_breakdown" => $klasifikasi_stats, // ✅ TAMBAHAN "type_breakdown" => $type_stats ] )); } catch (PDOException $e) { http_response_code(500); echo json_encode(array( "status" => "error", "message" => "Database query error: " . $e->getMessage() )); } exit(); } // ================================ // POST - CLEANUP FROM BOTTOM (TAMBAHAN BARU) // ================================ if ($request_method == 'POST' && $action === 'cleanup_from_bottom') { $input = file_get_contents("php://input"); $data = json_decode($input, true); $trx_sales_id = isset($data['trx_sales_id']) ? intval($data['trx_sales_id']) : 0; $klasifikasi = isset($data['klasifikasi']) ? $data['klasifikasi'] : 'DP'; if ($trx_sales_id == 0) { echo json_encode(["status" => "error", "message" => "trx_sales_id required"]); exit(); } try { $conn->beginTransaction(); // 1. Ambil semua record URUT DARI BAWAH $stmt = $conn->prepare(" SELECT id, nilai_bayar, nilai_cicilan FROM data_bayar_konsumen WHERE trx_sales_id = ? AND klasifikasi = ? ORDER BY urutan_cicilan DESC -- ⬅️ BAWAH KE ATAS "); $stmt->execute([$trx_sales_id, $klasifikasi]); $records = $stmt->fetchAll(PDO::FETCH_ASSOC); $toDelete = []; $foundNonZero = false; // 2. LOGIKA: hapus 0-0, stop saat ketemu nilai_cicilan > 0 foreach ($records as $record) { $nilaiBayar = floatval($record['nilai_bayar']); $nilaiCicilan = floatval($record['nilai_cicilan']); if (!$foundNonZero && $nilaiBayar == 0 && $nilaiCicilan == 0) { $toDelete[] = $record['id']; } elseif ($nilaiCicilan > 0) { $foundNonZero = true; // ⬅️ STOP, tidak hapus lagi } // Kalau sudah foundNonZero = true, skip semua record di atasnya } // 3. Hapus record yang terpilih $deletedCount = 0; if (!empty($toDelete)) { $placeholders = implode(',', array_fill(0, count($toDelete), '?')); $stmtDelete = $conn->prepare(" DELETE FROM data_bayar_konsumen WHERE id IN ($placeholders) "); $stmtDelete->execute($toDelete); $deletedCount = $stmtDelete->rowCount(); } $conn->commit(); echo json_encode([ "status" => "success", "message" => "Cleanup from bottom completed", "deleted_count" => $deletedCount, "deleted_ids" => $toDelete, "logic_applied" => "Delete 0-0 records from BOTTOM, stop when nilai_cicilan > 0" ]); } catch (Exception $e) { $conn->rollBack(); error_log("Cleanup error: " . $e->getMessage()); echo json_encode(["status" => "error", "message" => "Cleanup failed"]); } exit(); } // ================================ // ERROR HANDLER UNTUK REQUEST YANG TIDAK DUKUNG // ================================ http_response_code(405); echo json_encode(array( "status" => "error", "message" => "Method tidak diizinkan atau action tidak dikenali." )); ?>