Files
2026-09-15 10:38:43 +07:00

440 lines
17 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Jurnal extends MY_Controller {
public function __construct()
{
parent::__construct();
$this->load->model('DynamicModel', 'dm');
$this->load->library('JournalService');
$this->load->library('NumberingService');
$this->load->library('JournalWorkflowService');
if (!$this->session->userdata('logged_in')) {
redirect('auth');
}
}
public function index()
{
// if($this->session->userdata('role') != 'Admin'){
// show_error('Akses ditolak', 403);
// }
$data = ["active_menu" => "jurnal"];
$this->load->view('partials/header', $data);
$this->load->view('jurnal/layout');
$this->load->view('partials/footer');
}
// =========================
// GET DATA DATATABLE (SUDAH ADA TOTAL)
// =========================
public function get_data()
{
$draw = $_POST['draw'];
$start = $_POST['start'];
$length = $_POST['length'];
$search = $_POST['search']['value'];
$status = (string) $this->input->post('status', true);
$allowedStatuses = array('draft', 'submitted', 'approved', 'posted', 'rejected', 'reversed');
if (!in_array($status, $allowedStatuses, true)) $status = '';
// ================= TOTAL ALL
$totalAll = $this->db->where('company_id',$this->companycontext->id())->count_all_results('journals');
// ================= QUERY FILTERED (HITUNG)
$this->db->from('journals')->where('company_id',$this->companycontext->id());
if ($status !== '') {
$this->db->where('status', $status);
}
if(!empty($search)){
$this->db->group_start();
$this->db->like('no_ref', $search);
$this->db->or_like('keterangan', $search);
$this->db->or_like('tanggal', $search);
$this->db->group_end();
}
$filtered = $this->db->count_all_results();
// ================= QUERY DATA (AMBIL DATA)
$this->db->select('
journals.id,
journals.tanggal,
journals.no_ref,
journals.keterangan,
journals.status,
journals.is_reversed,
journals.ref_type,
journals.created_by,
journals.rejection_reason,
COALESCE(SUM(journal_details.debit),0) as total_debit,
COALESCE(SUM(journal_details.kredit),0) as total_kredit
');
$this->db->from('journals');
$this->db->join('journal_details','journal_details.journal_id = journals.id','LEFT');
$this->db->where('journals.company_id',$this->companycontext->id());
if ($status !== '') {
$this->db->where('journals.status', $status);
}
if(!empty($search)){
$this->db->group_start();
$this->db->like('journals.no_ref', $search);
$this->db->or_like('journals.keterangan', $search);
$this->db->or_like('journals.tanggal', $search);
$this->db->group_end();
}
$this->db->group_by('journals.id');
$this->db->order_by('journals.tanggal','DESC');
$this->db->order_by('journals.created_at','DESC');
$this->db->limit($length, $start);
$query = $this->db->get()->result();
// ================= FORMAT
$data = [];
$no = $start;
foreach ($query as $row) {
$no++;
$isAdmin = is_master_admin_user() || $this->session->userdata('role') === 'Admin';
$isOwner = (int)$row->created_by === (int)$this->session->userdata('user_id');
$aksi = '<button class="btn btn-sm btn-info btn-detail" data-id="'.$row->id.'">Detail</button> ';
if ($row->status === 'draft') {
$aksi .= '<button class="btn btn-sm btn-primary btn-submit" data-id="'.$row->id.'">Submit</button> ';
if ($isAdmin) $aksi .= '<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Hapus</button> ';
}
if ($row->status === 'rejected' && ($isOwner || $isAdmin) && in_array((string)$row->ref_type, array('', 'manual_journal'), true)) {
$aksi .= '<button class="btn btn-sm btn-warning btn-edit-rejected" data-id="'.$row->id.'">Edit & Koreksi</button> ';
if ($isAdmin) $aksi .= '<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Hapus</button> ';
}
if ($isAdmin && $row->status === 'submitted') {
$aksi .= '<button class="btn btn-sm btn-success btn-approve" data-id="'.$row->id.'">Approve</button> ';
$aksi .= '<button class="btn btn-sm btn-outline-danger btn-reject" data-id="'.$row->id.'">Reject</button> ';
}
if ($isAdmin && $row->status === 'posted') {
$aksi .= '<button class="btn btn-sm btn-outline-warning btn-reverse" data-id="'.$row->id.'">Reverse</button>';
}
$statusClass = array('draft'=>'secondary','submitted'=>'info','approved'=>'primary','posted'=>'success','rejected'=>'danger','reversed'=>'warning');
$badge = '<span class="badge bg-'.(isset($statusClass[$row->status]) ? $statusClass[$row->status] : 'secondary').'">'.htmlspecialchars(strtoupper($row->status), ENT_QUOTES, 'UTF-8').'</span>';
$data[] = [
$no,
$row->tanggal,
$row->no_ref,
$row->keterangan,
number_format($row->total_debit,0,',','.'),
number_format($row->total_kredit,0,',','.'),
$badge,
$aksi
];
}
echo json_encode([
"draw" => $draw,
"recordsTotal" => $totalAll,
"recordsFiltered" => $filtered,
"data" => $data
]);
}
// =========================
// GET ACCOUNTS
// =========================
public function get_accounts()
{
$data = $this->db->where(['company_id'=>$this->companycontext->id(),'is_active'=>1,'allow_posting'=>1])->order_by('kode_akun','ASC')->get('accounts')->result();
echo json_encode($data);
}
// =========================
// SAVE
// =========================
public function save()
{
$tanggal = (string) $this->input->post('tanggal', true);
$keterangan = trim((string) $this->input->post('keterangan', true));
$accounts = (array) $this->input->post('account_id');
$debit = (array) $this->input->post('debit');
$kredit = (array) $this->input->post('kredit');
try {
$noRef = $this->numberingservice->next('journal', $tanggal);
$lines = array();
foreach ($accounts as $index => $accountId) {
$lineDebit = isset($debit[$index]) ? $this->parseNominal($debit[$index]) : 0;
$lineCredit = isset($kredit[$index]) ? $this->parseNominal($kredit[$index]) : 0;
if ($lineDebit <= 0 && $lineCredit <= 0) continue;
$lines[] = array(
'account_id' => $accountId,
'debit' => $lineDebit,
'kredit' => $lineCredit
);
}
$journalId = $this->journalservice->create(array(
'tanggal' => $tanggal,
'no_ref' => $noRef,
'keterangan' => $keterangan,
'ref_type' => 'manual_journal',
'created_by' => (int) $this->session->userdata('user_id')
), $lines);
log_activity('Jurnal', 'create', 'Membuat jurnal No Ref: ' . $noRef, 'success');
return json_response(true, 'Jurnal berhasil disimpan.', array(
'journal_id' => $journalId,
'no_ref' => $noRef
));
} catch (Throwable $exception) {
return business_exception_response($exception);
}
}
public function update_rejected($id)
{
if (strtoupper($this->input->method()) !== 'POST') return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
try {
$accounts=(array)$this->input->post('account_id');$debit=(array)$this->input->post('debit');$kredit=(array)$this->input->post('kredit');$lines=array();
foreach($accounts as$index=>$accountId){$d=isset($debit[$index])?$this->parseNominal($debit[$index]):0;$k=isset($kredit[$index])?$this->parseNominal($kredit[$index]):0;if($d<=0&&$k<=0)continue;$lines[]=array('account_id'=>(int)$accountId,'debit'=>$d,'kredit'=>$k);}
$journal=$this->journalservice->reviseRejected((int)$id,array('tanggal'=>(string)$this->input->post('tanggal',true),'keterangan'=>trim((string)$this->input->post('keterangan',true))),$lines,(int)$this->session->userdata('user_id'));
log_activity('Jurnal','revise_rejected','Mengoreksi jurnal ditolak '.$journal->no_ref.' menjadi Draft.','success');
return json_response(true,'Koreksi jurnal berhasil disimpan sebagai Draft dan dapat diajukan kembali.',array('journal_id'=>$journal->id));
} catch (Throwable $exception) { return business_exception_response($exception); }
}
/** Menerima nominal raw maupun berformat 20,000 / 20.000 / 20.000,50. */
private function parseNominal($value)
{
$value = trim(str_ireplace(array('Rp', ' '), '', (string) $value));
if ($value === '') return 0.0;
$comma = strrpos($value, ','); $dot = strrpos($value, '.');
if ($comma !== false && $dot !== false) {
$value = $comma > $dot ? str_replace(',', '.', str_replace('.', '', $value)) : str_replace(',', '', $value);
} elseif ($comma !== false) {
$decimal = strlen($value) - $comma - 1;
$value = $decimal > 0 && $decimal <= 2 ? str_replace(',', '.', $value) : str_replace(',', '', $value);
} elseif ($dot !== false && strlen($value) - $dot - 1 === 3) {
$value = str_replace('.', '', $value);
}
return is_numeric($value) ? round((float) $value, 2) : 0.0;
}
// =========================
// DETAIL
// =========================
public function detail($id)
{
$header = $this->db->get_where('journals',['id'=>$id,'company_id'=>$this->companycontext->id()])->row();
if (!$header) return json_response(false, 'Jurnal tidak ditemukan.', array(), 404);
$detail = $this->db
->select('accounts.kode_akun, accounts.nama_akun, journal_details.*')
->from('journal_details')
->join('accounts','accounts.id = journal_details.account_id')
->where('journal_id',$id)
->get()->result();
// print_r($header);
$userid = $header->created_by;
$user = $this->db
->select('nama')
->where('id', $userid)
->get('users')
->row();
$history = $this->db
->select('journal_status_histories.*, users.nama changed_by_name')
->from('journal_status_histories')
->join('users', 'users.id = journal_status_histories.changed_by', 'left')
->where('journal_id', $id)
->order_by('journal_status_histories.id', 'ASC')
->get()->result();
echo json_encode([
'header'=>$header,
'detail'=>$detail,
'user' =>$user,
'history'=>$history
]);
}
// =========================
// DELETE
// =========================
public function delete($id)
{
if (strtoupper($this->input->method()) !== 'POST') {
return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
}
$this->requireAdmin();
$journal = $this->db->get_where('journals', ['id' => $id])->row();
// VALIDASI
if (!$journal) {
echo json_encode([
'status' => false,
'message' => 'Jurnal tidak ditemukan'
]);
return;
}
if (!in_array($journal->status, array('draft', 'rejected'), true)) {
return json_response(false, 'Hanya jurnal draft atau rejected yang dapat dihapus.', array(), 422);
}
$this->db->trans_start();
// =========================
// HANDLE STOCK LOGS
// =========================
if ($journal->ref_type == 'stock_logs') {
$stock = $this->db
->select('item_id, warehouse_id')
->where('id', $journal->ref_id)
->get('stock_logs')
->row();
if ($stock) {
// hapus log stok
$this->db->delete('stock_logs', ['id' => $journal->ref_id]);
// update stok ulang (pakai item_id saja biar gak ganggu sistem lama)
$this->update_item_stok($stock->item_id);
}
}
// =========================
// DELETE JOURNAL
// =========================
$this->db->delete('journal_details', ['journal_id' => $id]);
$this->db->delete('journals', ['id' => $id]);
$this->db->trans_complete();
// =========================
// VALIDASI TRANSAKSI
// =========================
if (!$this->db->trans_status()) {
echo json_encode([
'status' => false,
'message' => 'Gagal hapus jurnal'
]);
return;
}
// ================= LOG ACTIVITY =================
log_activity(
'Jurnal',
'delete',
'Menghapus jurnal no_ref: ' . $journal->no_ref . ' ket: ' . $journal->keterangan,
'success'
);
echo json_encode([
'status' => true, // ✅ FIX
'message' => 'Jurnal berhasil dihapus',
'journal' => $journal->ref_type
]);
}
public function submit($id)
{
return $this->workflowAction(function () use ($id) {
return $this->journalworkflowservice->submit($id, $this->session->userdata('user_id'));
}, 'Jurnal berhasil diajukan.');
}
public function approve($id)
{
$this->requireAdmin();
return $this->workflowAction(function () use ($id) {
return $this->journalworkflowservice->approve($id, $this->session->userdata('user_id'));
}, 'Approval jurnal berhasil diproses. Setelah approval final, jurnal otomatis diposting.');
}
public function post($id)
{
$this->requireAdmin();
return $this->workflowAction(function () use ($id) {
return $this->journalworkflowservice->post($id, $this->session->userdata('user_id'));
}, 'Jurnal berhasil diposting.');
}
public function reject($id)
{
$this->requireAdmin();
$reason = $this->input->post('reason', true);
return $this->workflowAction(function () use ($id, $reason) {
return $this->journalworkflowservice->reject($id, $this->session->userdata('user_id'), $reason);
}, 'Jurnal berhasil ditolak.');
}
public function reverse($id)
{
$this->requireAdmin();
$date = $this->input->post('date', true);
$reason = $this->input->post('reason', true);
return $this->workflowAction(function () use ($id, $date, $reason) {
return $this->journalworkflowservice->reverse($id, $date, $this->session->userdata('user_id'), $reason);
}, 'Reversal jurnal berhasil dibuat.');
}
private function workflowAction(callable $callback, $message)
{
if (strtoupper($this->input->method()) !== 'POST') {
return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
}
try {
$result = $callback();
log_activity('Jurnal', 'workflow', $message, 'success');
return json_response(true, $message, array('result' => $result));
} catch (Throwable $exception) {
return business_exception_response($exception);
}
}
public function get_base_jurnal()
{
echo json_encode(
$this->db->order_by('kode','ASC')->get('base_journal')->result()
);
}
public function get_base_jurnal_detail($id)
{
echo json_encode(
$this->db->where('base_journal_id',$id)
->get('base_journal_detail')
->result()
);
}
private function update_item_stok($item_id)
{
$stok = $this->db->query("
SELECT COALESCE(SUM(qty * IF(tipe='masuk',1,-1)),0) as stok
FROM stock_logs
WHERE item_id = ?
", [$item_id])->row()->stok;
$this->db->where('id', $item_id)
->update('items', [
'stok' => $stok
]);
}
}