load->database(); // ONLY CLI ACCESS // if (!$this->input->is_cli_request()) { // exit('No direct access allowed'); // } } public function generate_insight() { $period = date('Y-m'); // ===================================================== // 🔥 REPLACE MODE (hapus data lama dulu) // ===================================================== $this->db->where('period', $period); $this->db->delete('ai_insights'); // ===================================================== // 1. FINANCE - OVERDUE INVOICE // ===================================================== $overdue = $this->db->query(" SELECT COUNT(*) as total FROM invoices WHERE status != 'paid' AND jatuh_tempo < CURDATE() AND deleted_at IS NULL ")->row()->total; if ($overdue > 0) { $this->db->insert('ai_insights', [ 'type' => 'finance', 'severity' => 'warning', 'message' => "$overdue invoice sudah melewati jatuh tempo", 'meta' => json_encode([ 'overdue_count' => (int)$overdue ]), 'period' => $period ]); } // ===================================================== // 2. FINANCE - CASHFLOW BULAN INI // ===================================================== $cash = $this->db->query(" SELECT COALESCE(SUM(jd.debit),0) as cash_in FROM journal_details jd JOIN journals j ON j.id = jd.journal_id WHERE MONTH(j.tanggal) = MONTH(CURDATE()) AND YEAR(j.tanggal) = YEAR(CURDATE()) ")->row()->cash_in; if ($cash > 0) { $this->db->insert('ai_insights', [ 'type' => 'finance', 'severity' => ($cash > 10000000) ? 'info' : 'warning', 'message' => "Cashflow bulan ini Rp " . number_format($cash), 'meta' => json_encode([ 'cash_in' => (float)$cash ]), 'period' => $period ]); } // ===================================================== // 3. STOCK - KRITIS // ===================================================== $stocks = $this->db->query(" SELECT i.kode_id, k.nama_barang, SUM(i.stok) as total_stok, k.limit_stock FROM items i JOIN kode_barang k ON k.id = i.kode_id GROUP BY i.kode_id, k.nama_barang, k.limit_stock HAVING total_stok <= k.limit_stock ")->result(); foreach ($stocks as $s) { $this->db->insert('ai_insights', [ 'type' => 'stock', 'severity' => 'critical', 'message' => "Stok {$s->nama_barang} kritis ({$s->total_stok})", 'meta' => json_encode([ 'kode_id' => $s->kode_id, 'item' => $s->nama_barang, 'stok' => (int)$s->total_stok, 'limit' => (int)$s->limit_stock ]), 'period' => $period ]); } // ===================================================== // 4. CUSTOMER - PAYMENT RISK // ===================================================== $lateCustomers = $this->db->query(" SELECT c.nama, COUNT(i.id) as total_invoice FROM invoices i JOIN customers c ON c.id = i.customer_id WHERE i.status != 'paid' AND i.jatuh_tempo < CURDATE() GROUP BY i.customer_id HAVING total_invoice > 2 ")->result(); foreach ($lateCustomers as $c) { $this->db->insert('ai_insights', [ 'type' => 'customer', 'severity' => 'warning', 'message' => "Customer {$c->nama} memiliki {$c->total_invoice} invoice overdue", 'meta' => json_encode([ 'customer' => $c->nama, 'overdue_invoice' => (int)$c->total_invoice ]), 'period' => $period ]); } // ===================================================== // RESPONSE // ===================================================== echo json_encode([ 'status' => true, 'message' => 'AI Insight generated (REPLACE MODE)', 'period' => $period ]); } // ===================================================== // CRONJOB UNTUK LIHAT KEHADIRAN SELAMA 1 BULAN // ===================================================== // | Kriteria | Pengurangan | // | ---------------------- | ------------------: | // | Alpha | -20 poin / hari | // | Terlambat | -2 poin / kejadian | // | Izin (permit) | -1 poin / hari | // | Akumulasi telat | -1 poin / 60 menit | // | Akumulasi pulang cepat | -1 poin / 120 menit | public function generateMonthlyAttendance() { $month = date('Y-m-01'); $startDate = date('Y-m-01', strtotime($month)); $endDate = date('Y-m-t', strtotime($month)); $employees = $this->db ->where('is_active', 1) ->get('k_employees') ->result(); $processed = 0; $this->db->trans_begin(); try { foreach ($employees as $employee) { // Total Hari Kerja $schedule = $this->db->query(" SELECT COUNT(*) AS total_hari_kerja FROM k_employee_shift_schedules WHERE employee_id = ? AND status IN ('published','done') AND schedule_date BETWEEN ? AND ? ", array( $employee->id, $startDate, $endDate ))->row(); $total_hari_kerja = (int) $schedule->total_hari_kerja; // Rekap Absensi $attendance = $this->db->query(" SELECT COALESCE(SUM(attendance_status='present'),0) AS total_present, COALESCE(SUM(attendance_status='late'),0) AS total_late, COALESCE(SUM(attendance_status='alpha'),0) AS total_alpha, COALESCE(SUM(attendance_status='sick'),0) AS total_sick, COALESCE(SUM(attendance_status='permit'),0) AS total_permit, COALESCE(SUM(attendance_status='annual'),0) AS total_annual, COALESCE(SUM(late_minutes),0) AS total_late_minutes, COALESCE(SUM(early_leave_minutes),0) AS total_early_leave_minutes, COALESCE(SUM(work_hours),0) AS total_work_hours, COALESCE(SUM(overtime_hours),0) AS total_overtime_hours FROM k_attendances WHERE employee_id = ? AND attendance_date BETWEEN ? AND ? ", array( $employee->id, $startDate, $endDate ))->row(); $total_present = (int) $attendance->total_present; $total_late = (int) $attendance->total_late; $total_alpha = (int) $attendance->total_alpha; $total_sick = (int) $attendance->total_sick; $total_permit = (int) $attendance->total_permit; $total_annual = (int) $attendance->total_annual; // Kehadiran $total_hadir = $total_present + $total_late; $attendance_percentage = 0; if ($total_hari_kerja > 0) { $attendance_percentage = round( ($total_hadir / $total_hari_kerja) * 100, 2 ); } // SCOR $score = 100; // Alpha $score -= ($total_alpha * 20); // Telat $score -= ($total_late * 2); // Izin $score -= ($total_permit * 1); // Total menit telat $score -= floor( ((int) $attendance->total_late_minutes) / 60 ); // Total menit pulang cepat $score -= floor( ((int) $attendance->total_early_leave_minutes) / 120 ); if ($score < 0) { $score = 0; } // LOGIKA STATUS if ($score >= 90) { $status = 'baik sekali'; } elseif ($score >= 75) { $status = 'baik'; } elseif ($score >= 60) { $status = 'kurang baik'; } else { $status = 'buruk'; } // INSERT OR UPDATE TO k_attendance_monthly $sql = " INSERT INTO k_attendance_monthly ( employee_id, month, total_present, total_late, total_alpha, total_sick, total_permit, total_annual, total_late_minutes, total_early_leave_minutes, total_work_hours, total_overtime_hours, total_hari_kerja, total_hadir, attendance_percentage, score, status ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ON DUPLICATE KEY UPDATE total_present = VALUES(total_present), total_late = VALUES(total_late), total_alpha = VALUES(total_alpha), total_sick = VALUES(total_sick), total_permit = VALUES(total_permit), total_annual = VALUES(total_annual), total_late_minutes = VALUES(total_late_minutes), total_early_leave_minutes = VALUES(total_early_leave_minutes), total_work_hours = VALUES(total_work_hours), total_overtime_hours = VALUES(total_overtime_hours), total_hari_kerja = VALUES(total_hari_kerja), total_hadir = VALUES(total_hadir), attendance_percentage = VALUES(attendance_percentage), score = VALUES(score), status = VALUES(status), updated_at = CURRENT_TIMESTAMP "; $this->db->query($sql, array( $employee->id, $month, $total_present, $total_late, $total_alpha, $total_sick, $total_permit, $total_annual, (int)$attendance->total_late_minutes, (int)$attendance->total_early_leave_minutes, (float)$attendance->total_work_hours, (float)$attendance->total_overtime_hours, $total_hari_kerja, $total_hadir, $attendance_percentage, $score, $status )); $processed++; } if ($this->db->trans_status() === FALSE) { throw new Exception('Transaction failed'); } $this->db->trans_commit(); echo json_encode(array( 'status' => true, 'month' => $month, 'processed' => $processed, 'message' => 'Monthly attendance generated successfully' )); } catch (Exception $e) { $this->db->trans_rollback(); echo json_encode(array( 'status' => false, 'month' => $month, 'message' => $e->getMessage() )); } } // ===================================================== // CRONJOB AUTO PENYUSUTAN ASSET BULANAN // ===================================================== public function depreciation_asset() { $period = date('Y-m'); $runDate = date('Y-m-d'); $akun_beban_penyusutan = 54; $assets = $this->db ->select('id, nama_asset, nilai_perolehan, nilai_buku, nilai_residu, penyusutan_per_bulan, masa_manfaat, tanggal_perolehan, akumulasi_penyusutan, account_debit_id') ->from('assets') ->where('status', 'aktif') ->where('nilai_buku > nilai_residu', null, false) ->where('penyusutan_per_bulan >', 0) ->get() ->result(); $processed = 0; $success = 0; $skipped = 0; $errors = 0; $details = []; $this->db->trans_begin(); try { foreach ($assets as $a) { $processed++; $assetDetails = [ 'asset_id' => (int)$a->id, 'asset_name' => $a->nama_asset, 'processed_months' => [], 'skipped_months' => [] ]; $account_parent = $this->db ->select('id') ->where('parent_id', $a->account_debit_id) ->get('accounts') ->row(); if (!$account_parent || empty($account_parent->id)) { $skipped++; $assetDetails['status'] = 'skip'; $assetDetails['reason'] = 'account_parent_not_found'; $details[] = $assetDetails; continue; } $akun_akumulasi_penyusutan = (int)$account_parent->id; $mutasiPatokan = $this->db ->select('tanggal_dibuat') ->from('asset_mutations') ->where('asset_id', $a->id) ->where_in('tipe', ['perolehan', 'penambahan']) ->order_by('tanggal_dibuat', 'DESC') ->order_by('id', 'DESC') ->limit(1) ->get() ->row(); $tanggalPatokan = null; if ($mutasiPatokan && !empty($mutasiPatokan->tanggal_dibuat)) { $tanggalPatokan = $mutasiPatokan->tanggal_dibuat; } if (empty($tanggalPatokan)) { $tanggalPatokan = $a->tanggal_perolehan; } if (empty($tanggalPatokan)) { $skipped++; $assetDetails['status'] = 'skip'; $assetDetails['reason'] = 'tanggal_patokan_tidak_ditemukan'; $details[] = $assetDetails; continue; } $startMonth = date('Y-m', strtotime(date('Y-m-01', strtotime($tanggalPatokan)) . ' +1 month')); $currentMonth = $period; if ($startMonth > $currentMonth) { $skipped++; $assetDetails['status'] = 'skip'; $assetDetails['reason'] = 'start_next_month_after_acquisition'; $assetDetails['tanggal_patokan'] = $tanggalPatokan; $details[] = $assetDetails; continue; } $nilaiPerolehan = (float)$a->nilai_perolehan; $nilaiResidu = (float)$a->nilai_residu; $susutPerBulan = (float)$a->penyusutan_per_bulan; $masaManfaat = (int)$a->masa_manfaat; $akumulasiMutasi = $this->db ->select('COALESCE(SUM(nilai),0) total', false) ->from('asset_mutations') ->where('asset_id', $a->id) ->where('tipe', 'penyusutan') ->get() ->row(); $akumulasiBerjalan = (float)($akumulasiMutasi ? $akumulasiMutasi->total : 0); $nilaiBukuBerjalan = $nilaiPerolehan - $akumulasiBerjalan; if ($nilaiBukuBerjalan < $nilaiResidu) { $nilaiBukuBerjalan = $nilaiResidu; $akumulasiBerjalan = $nilaiPerolehan - $nilaiBukuBerjalan; } $iterMonth = $startMonth; $assetHasSuccess = false; while ($iterMonth <= $currentMonth) { $monthsUsed = 0; $start = new DateTime(date('Y-m-01', strtotime($tanggalPatokan))); $end = new DateTime(date('Y-m-01', strtotime($iterMonth . '-01'))); $diff = $start->diff($end); $monthsUsed = ($diff->y * 12) + $diff->m; if ($monthsUsed < 0) { $skipped++; $assetDetails['skipped_months'][] = [ 'period' => $iterMonth, 'reason' => 'tanggal_patokan_after_period' ]; $iterMonth = date('Y-m', strtotime($iterMonth . '-01 +1 month')); continue; } if ($masaManfaat > 0 && $monthsUsed >= $masaManfaat) { $skipped++; $assetDetails['skipped_months'][] = [ 'period' => $iterMonth, 'reason' => 'masa_manfaat_finished' ]; break; } $acqDay = (int)date('d', strtotime($tanggalPatokan)); $lastDayOfMonth = (int)date('t', strtotime($iterMonth . '-01')); $dueDay = min($acqDay, $lastDayOfMonth); $dueDateThisMonth = $iterMonth . '-' . str_pad($dueDay, 2, '0', STR_PAD_LEFT); // Untuk bulan berjalan, tetap hormati due date. // Untuk bulan yang sudah lewat, langsung boleh diproses (catch-up). if ($iterMonth === $currentMonth && strtotime($runDate) < strtotime($dueDateThisMonth)) { $skipped++; $assetDetails['skipped_months'][] = [ 'period' => $iterMonth, 'reason' => 'not_due_date_today', 'due_date' => $dueDateThisMonth, 'run_date' => $runDate ]; $iterMonth = date('Y-m', strtotime($iterMonth . '-01 +1 month')); continue; } $startDate = $iterMonth . '-01'; $endDate = date('Y-m-d', strtotime($startDate . ' +1 month')); $already = $this->db ->where('asset_id', $a->id) ->where('tipe', 'penyusutan') ->where('tanggal_dibuat >=', $startDate) ->where('tanggal_dibuat <', $endDate) ->count_all_results('asset_mutations'); if ($already > 0) { $skipped++; $assetDetails['skipped_months'][] = [ 'period' => $iterMonth, 'reason' => 'already_depreciated_this_month' ]; $iterMonth = date('Y-m', strtotime($iterMonth . '-01 +1 month')); continue; } if ($nilaiBukuBerjalan <= $nilaiResidu) { $skipped++; $assetDetails['skipped_months'][] = [ 'period' => $iterMonth, 'reason' => 'nothing_to_depreciate' ]; break; } $maxSusut = max(0, $nilaiBukuBerjalan - $nilaiResidu); $susut = min($susutPerBulan, $maxSusut); if ($susut <= 0) { $skipped++; $assetDetails['skipped_months'][] = [ 'period' => $iterMonth, 'reason' => 'nothing_to_depreciate' ]; break; } $akumulasiBefore = $akumulasiBerjalan; $nilaiBukuBefore = $nilaiBukuBerjalan; $akumulasiBerjalan += $susut; $nilaiBukuBerjalan = $nilaiPerolehan - $akumulasiBerjalan; if ($nilaiBukuBerjalan < $nilaiResidu) { $nilaiBukuBerjalan = $nilaiResidu; $akumulasiBerjalan = $nilaiPerolehan - $nilaiBukuBerjalan; } $this->db->insert('asset_mutations', [ 'asset_id' => $a->id, 'tipe' => 'penyusutan', 'nilai' => $susut, 'keterangan' => 'Penyusutan otomatis periode ' . $iterMonth . ' (cronjob)', 'tanggal_dibuat' => $dueDateThisMonth ]); $no_ref = 'DEP-' . date('YmdHis') . '-' . $a->id . '-' . str_replace('-', '', $iterMonth); $this->db->insert('journals', [ 'tanggal' => $dueDateThisMonth, 'no_ref' => $no_ref, 'keterangan' => 'Penyusutan Asset ' . $a->nama_asset . ' periode ' . $iterMonth, 'ref_type' => 'assets', 'ref_id' => $a->id, 'created_by' => 1 ]); $jid = $this->db->insert_id(); $this->db->insert('journal_details', [ 'journal_id' => $jid, 'account_id' => $akun_beban_penyusutan, 'debit' => $susut, 'kredit' => 0 ]); $this->db->insert('journal_details', [ 'journal_id' => $jid, 'account_id' => $akun_akumulasi_penyusutan, 'debit' => 0, 'kredit' => $susut ]); $success++; $assetHasSuccess = true; $assetDetails['processed_months'][] = [ 'period' => $iterMonth, 'depreciation' => (float)$susut, 'due_date' => $dueDateThisMonth, 'akumulasi_before' => (float)$akumulasiBefore, 'akumulasi_after' => (float)$akumulasiBerjalan, 'nilai_buku_before' => (float)$nilaiBukuBefore, 'nilai_buku_after' => (float)$nilaiBukuBerjalan, 'journal_no_ref' => $no_ref ]; $iterMonth = date('Y-m', strtotime($iterMonth . '-01 +1 month')); } if ($assetHasSuccess) { $this->db->where('id', $a->id)->update('assets', [ 'akumulasi_penyusutan' => $akumulasiBerjalan, 'nilai_buku' => $nilaiBukuBerjalan ]); $assetDetails['status'] = 'success'; $assetDetails['akumulasi_final'] = (float)$akumulasiBerjalan; $assetDetails['nilai_buku_final'] = (float)$nilaiBukuBerjalan; } else { $assetDetails['status'] = 'skip'; if (empty($assetDetails['reason'])) { $assetDetails['reason'] = 'no_eligible_month_to_process'; } } $details[] = $assetDetails; } if ($this->db->trans_status() === FALSE) { throw new Exception('Transaction failed'); } $this->db->trans_commit(); echo json_encode([ 'status' => true, 'period' => $period, 'processed' => $processed, 'success' => $success, 'skipped' => $skipped, 'errors' => $errors, 'message' => 'Depreciation cron executed', 'details' => $details ]); } catch (Exception $e) { $this->db->trans_rollback(); $errors++; echo json_encode([ 'status' => false, 'period' => $period, 'processed' => $processed, 'success' => $success, 'skipped' => $skipped, 'errors' => $errors, 'message' => $e->getMessage() ]); } } }