bank, laporan bank, budgeting badge

This commit is contained in:
Wian Drs
2026-09-17 00:51:53 +07:00
parent e4d6245a3d
commit 2a77402c60
11 changed files with 82 additions and 16 deletions
+4 -1
View File
@@ -11,7 +11,10 @@ class Budgets extends MY_Controller
$listAccount=(int)$this->input->get('list_account_id'); $listAccount=(int)$this->input->get('list_account_id');
$accounts=$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('kode_akun')->get('accounts')->result(); $accounts=$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('kode_akun')->get('accounts')->result();
$budgetAccounts=$this->db->select('a.id,a.kode_akun,a.nama_akun,a.tipe')->distinct()->from('accounts a')->join('account_budget_entries e','e.account_id=a.id')->where(array('e.company_id'=>$company,'e.is_active'=>1))->order_by('a.kode_akun')->get()->result(); $budgetAccounts=$this->db->select('a.id,a.kode_akun,a.nama_akun,a.tipe')->distinct()->from('accounts a')->join('account_budget_entries e','e.account_id=a.id')->where(array('e.company_id'=>$company,'e.is_active'=>1))->order_by('a.kode_akun')->get()->result();
$data=array('active_menu'=>'budgets','active_tab'=>$tab,'accounts'=>$accounts,'budget_accounts'=>$budgetAccounts,'budget_account_count'=>count($budgetAccounts),'entries'=>$this->budgetservice->entries($listAccount),'reminders'=>$this->budgetservice->currentReminders(),'list_account_id'=>$listAccount,'filters'=>$filters,'report_rows'=>$tab==='accounts'?$this->budgetservice->report($filters):array(),'can_create'=>$this->allowed('can_create'),'can_update'=>$this->allowed('can_update'),'can_delete'=>$this->allowed('can_delete'),'can_export'=>$this->allowed('can_export')); $entries=$this->budgetservice->entries($listAccount);$reportRows=$tab==='accounts'?$this->budgetservice->report($filters):array();
$listTotal=array_sum(array_map(function($entry){return(float)$entry->amount;},$entries));
$accountTotal=array_sum(array_map(function($row){return(float)$row['budget'];},$reportRows));
$data=array('active_menu'=>'budgets','active_tab'=>$tab,'accounts'=>$accounts,'budget_accounts'=>$budgetAccounts,'budget_account_count'=>count($budgetAccounts),'entries'=>$entries,'list_total_budget'=>$listTotal,'reminders'=>$this->budgetservice->currentReminders(),'list_account_id'=>$listAccount,'filters'=>$filters,'report_rows'=>$reportRows,'account_total_budget'=>$accountTotal,'can_create'=>$this->allowed('can_create'),'can_update'=>$this->allowed('can_update'),'can_delete'=>$this->allowed('can_delete'),'can_export'=>$this->allowed('can_export'));
$this->load->view('partials/header',$data);$this->load->view('budgets/index',$data);$this->load->view('partials/footer'); $this->load->view('partials/header',$data);$this->load->view('budgets/index',$data);$this->load->view('partials/footer');
} }
public function save_entry() public function save_entry()
+1 -1
View File
@@ -49,7 +49,7 @@ class Companies extends MY_Admin_Controller
if($type==='savings')$maturity=null; if($type==='savings')$maturity=null;
if($maturity!==null&&$maturity<$opened)throw new RuntimeException('Tanggal jatuh tempo tidak boleh sebelum tanggal pembukaan.'); if($maturity!==null&&$maturity<$opened)throw new RuntimeException('Tanggal jatuh tempo tidak boleh sebelum tanggal pembukaan.');
$coa=$this->db->get_where('accounts',array('id'=>$gl,'company_id'=>$company,'tipe'=>'asset','is_active'=>1,'allow_posting'=>1))->row();if(!$coa)throw new RuntimeException('Akun COA bank tidak valid atau bukan akun Aktiva aktif.'); $coa=$this->db->get_where('accounts',array('id'=>$gl,'company_id'=>$company,'tipe'=>'asset','is_active'=>1,'allow_posting'=>1))->row();if(!$coa)throw new RuntimeException('Akun COA bank tidak valid atau bukan akun Aktiva aktif.');
$row=array('company_id'=>$company,'bank_name'=>trim((string)$this->input->post('bank_name',true)),'account_number'=>trim((string)$this->input->post('account_number',true)),'account_holder'=>trim((string)$this->input->post('account_holder',true)),'product_name'=>trim((string)$this->input->post('product_name',true)),'account_type'=>$type,'interest_rate'=>$interest,'opened_at'=>$opened,'maturity_date'=>$maturity,'currency'=>strtoupper(trim((string)$this->input->post('currency',true))),'gl_account_id'=>$gl,'is_primary'=>$this->input->post('is_primary')?1:0,'updated_at'=>date('Y-m-d H:i:s')); $row=array('company_id'=>$company,'bank_name'=>trim((string)$this->input->post('bank_name',true)),'account_number'=>trim((string)$this->input->post('account_number',true)),'account_holder'=>trim((string)$this->input->post('account_holder',true)),'product_name'=>trim((string)$this->input->post('product_name',true)),'account_type'=>$type,'interest_rate'=>$interest,'opened_at'=>$opened,'maturity_date'=>$maturity,'currency'=>strtoupper(trim((string)$this->input->post('currency',true))),'gl_account_id'=>$gl,'is_primary'=>$this->input->post('is_primary')?1:0,'show_on_invoice'=>$this->input->post('show_on_invoice')?1:0,'updated_at'=>date('Y-m-d H:i:s'));
if($row['bank_name']===''||$row['account_number']===''||$row['account_holder']===''||$row['product_name']==='')throw new RuntimeException('Nama bank, nomor rekening, pemilik, dan produk bank wajib diisi.'); if($row['bank_name']===''||$row['account_number']===''||$row['account_holder']===''||$row['product_name']==='')throw new RuntimeException('Nama bank, nomor rekening, pemilik, dan produk bank wajib diisi.');
if(!$this->db->get_where('currencies',array('code'=>$row['currency'],'is_active'=>1))->row())throw new RuntimeException('Mata uang rekening tidak valid.'); if(!$this->db->get_where('currencies',array('code'=>$row['currency'],'is_active'=>1))->row())throw new RuntimeException('Mata uang rekening tidak valid.');
$this->db->where(array('company_id'=>$company,'account_number'=>$row['account_number']));if($id)$this->db->where('id !=',$id);if($this->db->count_all_results('company_bank_accounts'))throw new RuntimeException('Nomor rekening sudah terdaftar pada perusahaan ini.'); $this->db->where(array('company_id'=>$company,'account_number'=>$row['account_number']));if($id)$this->db->where('id !=',$id);if($this->db->count_all_results('company_bank_accounts'))throw new RuntimeException('Nomor rekening sudah terdaftar pada perusahaan ini.');
+10 -1
View File
@@ -123,7 +123,16 @@ class Invoices extends MY_Controller
} }
private function addPdfCompanySettings(array&$data) private function addPdfCompanySettings(array&$data)
{ {
$data['pdf_company']=$this->db->get_where('companies',array('id'=>$this->company()))->row();$currency=!empty($data['invoice']->transaction_currency)?$data['invoice']->transaction_currency:($data['pdf_company']->base_currency??'IDR');$data['pdf_currency']=$this->db->get_where('currencies',array('code'=>$currency))->row();$data['pdf_bank_accounts']=$this->db->table_exists('company_bank_accounts')?$this->db->where(array('company_id'=>$this->company(),'is_active'=>1))->order_by('is_primary','DESC')->order_by('id','ASC')->get('company_bank_accounts')->result():array();$data['pdf_logo_data']='';$path=$data['pdf_company']->logo_path??'';if($path!==''&&is_file(FCPATH.$path)){$mime=function_exists('mime_content_type')?mime_content_type(FCPATH.$path):'image/png';$data['pdf_logo_data']='data:'.$mime.';base64,'.base64_encode(file_get_contents(FCPATH.$path));} $data['pdf_company']=$this->db->get_where('companies',array('id'=>$this->company()))->row();
$currency=!empty($data['invoice']->transaction_currency)?$data['invoice']->transaction_currency:($data['pdf_company']->base_currency??'IDR');
$data['pdf_currency']=$this->db->get_where('currencies',array('code'=>$currency))->row();
$data['pdf_bank_accounts']=array();
if($this->db->table_exists('company_bank_accounts')){
$this->db->where(array('company_id'=>$this->company(),'is_active'=>1));
if($this->db->field_exists('show_on_invoice','company_bank_accounts'))$this->db->where('show_on_invoice',1);
$data['pdf_bank_accounts']=$this->db->order_by('is_primary','DESC')->order_by('id','ASC')->get('company_bank_accounts')->result();
}
$data['pdf_logo_data']='';$path=$data['pdf_company']->logo_path??'';if($path!==''&&is_file(FCPATH.$path)){$mime=function_exists('mime_content_type')?mime_content_type(FCPATH.$path):'image/png';$data['pdf_logo_data']='data:'.$mime.';base64,'.base64_encode(file_get_contents(FCPATH.$path));}
} }
private function mutation($permission,callable$callback,$message) private function mutation($permission,callable$callback,$message)
{ {
@@ -4,7 +4,7 @@ class Professionalreports extends MY_Controller
{ {
public function __construct(){parent::__construct();$this->requirePermission('reports');$this->load->database();$this->load->library(array('ProfessionalReportService','CompanyContext','JobQueueService'));} public function __construct(){parent::__construct();$this->requirePermission('reports');$this->load->database();$this->load->library(array('ProfessionalReportService','CompanyContext','JobQueueService'));}
private function filter(){$to=$this->input->get_post('to')?:date('Y-m-d');return array('from'=>$this->input->get_post('from')?:date('Y-m-01',strtotime($to)),'to'=>$to,'account_id'=>(int)$this->input->get_post('account_id'));} private function filter(){$to=$this->input->get_post('to')?:date('Y-m-d');return array('from'=>$this->input->get_post('from')?:date('Y-m-01',strtotime($to)),'to'=>$to,'account_id'=>(int)$this->input->get_post('account_id'));}
private function reportFilter($code,array$f){if($code==='balance_sheet')$f['from']='1900-01-01';return$f;} private function reportFilter($code,array$f){if(in_array($code,array('balance_sheet','bank_account_balances'),true))$f['from']='1900-01-01';return$f;}
public function index(){$code=$this->input->get('report')?:'trial_balance';$f=$this->filter();$reportFilters=$this->reportFilter($code,$f);$rows=$this->professionalreportservice->run($code,$reportFilters);$summary=array();if($code==='cash_flow')$summary=$this->professionalreportservice->cashFlowSummary($reportFilters,$rows);elseif($code==='general_ledger')$summary=$this->professionalreportservice->ledgerSummary($reportFilters);elseif($code==='receivable_reconciliation')$summary=$this->professionalreportservice->receivableSummary($reportFilters,$rows);$d=array('active_menu'=>'professional_reports','report'=>$code,'filters'=>$f,'rows'=>$rows,'report_summary'=>$summary,'profit_loss_rows'=>$code==='balance_sheet'?$this->professionalreportservice->run('profit_loss',$reportFilters):array());$d['accounts']=$this->db->where('company_id',$this->companycontext->id())->order_by('kode_akun')->get('accounts')->result();$d['company']=$this->db->get_where('companies',array('id'=>$this->companycontext->id()))->row();$this->load->view('partials/header',$d);$this->load->view('professional_reports/index',$d);$this->load->view('partials/footer');} public function index(){$code=$this->input->get('report')?:'trial_balance';$f=$this->filter();$reportFilters=$this->reportFilter($code,$f);$rows=$this->professionalreportservice->run($code,$reportFilters);$summary=array();if($code==='cash_flow')$summary=$this->professionalreportservice->cashFlowSummary($reportFilters,$rows);elseif($code==='general_ledger')$summary=$this->professionalreportservice->ledgerSummary($reportFilters);elseif($code==='receivable_reconciliation')$summary=$this->professionalreportservice->receivableSummary($reportFilters,$rows);$d=array('active_menu'=>'professional_reports','report'=>$code,'filters'=>$f,'rows'=>$rows,'report_summary'=>$summary,'profit_loss_rows'=>$code==='balance_sheet'?$this->professionalreportservice->run('profit_loss',$reportFilters):array());$d['accounts']=$this->db->where('company_id',$this->companycontext->id())->order_by('kode_akun')->get('accounts')->result();$d['company']=$this->db->get_where('companies',array('id'=>$this->companycontext->id()))->row();$this->load->view('partials/header',$d);$this->load->view('professional_reports/index',$d);$this->load->view('partials/footer');}
public function export($code){$f=$this->reportFilter($code,$this->filter());$rows=$this->professionalreportservice->run($code,$f);$this->output->set_content_type('text/csv')->set_header('Content-Disposition: attachment; filename="'.$code.'-'.$f['to'].'.csv"');$h=fopen('php://output','w');if($rows){fputcsv($h,array_keys($rows[0]));foreach($rows as$r)fputcsv($h,$r);}fclose($h);} public function export($code){$f=$this->reportFilter($code,$this->filter());$rows=$this->professionalreportservice->run($code,$f);$this->output->set_content_type('text/csv')->set_header('Content-Disposition: attachment; filename="'.$code.'-'.$f['to'].'.csv"');$h=fopen('php://output','w');if($rows){fputcsv($h,array_keys($rows[0]));foreach($rows as$r)fputcsv($h,$r);}fclose($h);}
public function save_filter(){$this->db->insert('saved_report_filters',array('user_id'=>$this->session->userdata('user_id'),'report_code'=>$this->input->post('report_code'),'name'=>$this->input->post('name',true),'filter_json'=>json_encode($this->filter()),'created_at'=>date('Y-m-d H:i:s'),'company_id'=>$this->companycontext->id()));redirect('professionalreports?report='.$this->input->post('report_code'));} public function save_filter(){$this->db->insert('saved_report_filters',array('user_id'=>$this->session->userdata('user_id'),'report_code'=>$this->input->post('report_code'),'name'=>$this->input->post('name',true),'filter_json'=>json_encode($this->filter()),'created_at'=>date('Y-m-d H:i:s'),'company_id'=>$this->companycontext->id()));redirect('professionalreports?report='.$this->input->post('report_code'));}
+6 -1
View File
@@ -89,7 +89,12 @@ class Sales extends MY_Controller
private function decodeRows(){$rows=json_decode((string)$this->input->post('rows'),true);if(!is_array($rows))throw new BusinessException('Detail barang tidak valid.');return$rows;} private function decodeRows(){$rows=json_decode((string)$this->input->post('rows'),true);if(!is_array($rows))throw new BusinessException('Detail barang tidak valid.');return$rows;}
private function action($fn,$message){if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode tidak diizinkan.',array(),405);try{return json_response(true,$message,array('result'=>$fn()));}catch(Throwable$e){return business_exception_response($e);}} private function action($fn,$message){if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode tidak diizinkan.',array(),405);try{return json_response(true,$message,array('result'=>$fn()));}catch(Throwable$e){return business_exception_response($e);}}
private function jsonDetail($fn){try{return json_response(true,'Detail ditemukan.',$fn());}catch(Throwable$e){return business_exception_response($e);}} private function jsonDetail($fn){try{return json_response(true,'Detail ditemukan.',$fn());}catch(Throwable$e){return business_exception_response($e);}}
private function pdf($view,$data,$name){$company=(int)$this->companycontext->id();$data['pdf_company']=$this->db->get_where('companies',array('id'=>$company))->row();$currency=!empty($data['invoice']->transaction_currency)?$data['invoice']->transaction_currency:($data['pdf_company']->base_currency??'IDR');$data['pdf_currency']=$this->db->get_where('currencies',array('code'=>$currency))->row();$data['pdf_bank_accounts']=$this->db->table_exists('company_bank_accounts')?$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('is_primary','DESC')->order_by('id')->get('company_bank_accounts')->result():array();$data['pdf_logo_data']='';$path=$data['pdf_company']->logo_path??'';if($path!==''&&is_file(FCPATH.$path)){$mime=function_exists('mime_content_type')?mime_content_type(FCPATH.$path):'image/png';$data['pdf_logo_data']='data:'.$mime.';base64,'.base64_encode(file_get_contents(FCPATH.$path));}require_once FCPATH.'vendor/autoload.php';$html=$this->load->view($view,$data,true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','portrait');$pdf->render();$pdf->stream($name,array('Attachment'=>false));} private function pdf($view,$data,$name)
{
$company=(int)$this->companycontext->id();$data['pdf_company']=$this->db->get_where('companies',array('id'=>$company))->row();$currency=!empty($data['invoice']->transaction_currency)?$data['invoice']->transaction_currency:($data['pdf_company']->base_currency??'IDR');$data['pdf_currency']=$this->db->get_where('currencies',array('code'=>$currency))->row();$data['pdf_bank_accounts']=array();
if($this->db->table_exists('company_bank_accounts')){$this->db->where(array('company_id'=>$company,'is_active'=>1));if($this->db->field_exists('show_on_invoice','company_bank_accounts'))$this->db->where('show_on_invoice',1);$data['pdf_bank_accounts']=$this->db->order_by('is_primary','DESC')->order_by('id')->get('company_bank_accounts')->result();}
$data['pdf_logo_data']='';$path=$data['pdf_company']->logo_path??'';if($path!==''&&is_file(FCPATH.$path)){$mime=function_exists('mime_content_type')?mime_content_type(FCPATH.$path):'image/png';$data['pdf_logo_data']='data:'.$mime.';base64,'.base64_encode(file_get_contents(FCPATH.$path));}require_once FCPATH.'vendor/autoload.php';$html=$this->load->view($view,$data,true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','portrait');$pdf->render();$pdf->stream($name,array('Attachment'=>false));
}
private function uploads($field,$required=false) private function uploads($field,$required=false)
{ {
if(empty($_FILES[$field])||empty($_FILES[$field]['name'])){if($required)throw new BusinessException('Bukti pembayaran gambar atau PDF wajib diunggah.');return array();}$files=$_FILES[$field];$names=is_array($files['name'])?$files['name']:array($files['name']);$saved=array();if(count($names)>10)throw new BusinessException('Maksimal 10 file.');$dir=FCPATH.'uploads/accounting/sales/';if(!is_dir($dir)&&!mkdir($dir,0755,true))throw new BusinessException('Folder lampiran tidak dapat dibuat.');$finfo=new finfo(FILEINFO_MIME_TYPE);$allowed=array('image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','application/pdf'=>'pdf'); if(empty($_FILES[$field])||empty($_FILES[$field]['name'])){if($required)throw new BusinessException('Bukti pembayaran gambar atau PDF wajib diunggah.');return array();}$files=$_FILES[$field];$names=is_array($files['name'])?$files['name']:array($files['name']);$saved=array();if(count($names)>10)throw new BusinessException('Maksimal 10 file.');$dir=FCPATH.'uploads/accounting/sales/';if(!is_dir($dir)&&!mkdir($dir,0755,true))throw new BusinessException('Folder lampiran tidak dapat dibuat.');$finfo=new finfo(FILEINFO_MIME_TYPE);$allowed=array('image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','application/pdf'=>'pdf');
+2 -2
View File
@@ -17,7 +17,7 @@ class NavigationService
array('key'=>'fixed_assets','label'=>'Aset Tetap','icon'=>'bi-building-gear','url'=>'fixedassets','feature'=>'asset'), array('key'=>'fixed_assets','label'=>'Aset Tetap','icon'=>'bi-building-gear','url'=>'fixedassets','feature'=>'asset'),
array('key'=>'accounting','label'=>'Accounting','icon'=>'bi-journal-text','feature'=>'jurnal','children'=>array(array('key'=>'jurnal','label'=>'Jurnal Umum','url'=>'jurnal'),array('key'=>'accounts','label'=>'Daftar Akun','url'=>'accounts'),array('key'=>'basejurnal','label'=>'Base Jurnal','url'=>'basejurnal'),array('key'=>'accounting_periods','label'=>'Periode Accounting','url'=>'accountingperiods'))), array('key'=>'accounting','label'=>'Accounting','icon'=>'bi-journal-text','feature'=>'jurnal','children'=>array(array('key'=>'jurnal','label'=>'Jurnal Umum','url'=>'jurnal'),array('key'=>'accounts','label'=>'Daftar Akun','url'=>'accounts'),array('key'=>'basejurnal','label'=>'Base Jurnal','url'=>'basejurnal'),array('key'=>'accounting_periods','label'=>'Periode Accounting','url'=>'accountingperiods'))),
array('key'=>'professional_reports','label'=>'Laporan Profesional','icon'=>'bi-bar-chart','url'=>'professionalreports','feature'=>'reports'), array('key'=>'professional_reports','label'=>'Laporan Profesional','icon'=>'bi-bar-chart','url'=>'professionalreports','feature'=>'reports'),
array('key'=>'budgets','label'=>'Budget','icon'=>'bi-bullseye','url'=>'budgets','feature'=>'budgets','badge'=>$this->budgetApprovalBadge(),'badge_class'=>'nav-badge-warning','badge_title'=>'Pengingat pembayaran budget yang perlu diperhatikan'), array('key'=>'budgets','label'=>'Budget','icon'=>'bi-bullseye','url'=>'budgets','feature'=>'budgets','badge'=>$this->budgetDueBadge(),'badge_class'=>'nav-badge-danger','badge_title'=>'Budget jatuh tempo atau terlambat yang belum diselesaikan'),
array('key'=>'hr','label'=>'HR & Payroll','icon'=>'bi-people','feature'=>array('hr_organization','hr_employees','hr_schedules','hr_attendance','hr_leave','payroll','employees'),'badge'=>$this->leaveApprovalBadge()+$this->manualAttendanceBadge(),'badge_class'=>'nav-badge-warning','badge_title'=>'Pengajuan HR menunggu tindakan','children'=>array(array('key'=>'employees','label'=>'Karyawan','url'=>$employeeUrl,'feature'=>array('hr_employees','hr_organization','employees'),'active_keys'=>array('employees','organization_master')),array('key'=>'attendance_monitoring','label'=>'Absen & Kehadiran','url'=>$attendanceUrl,'feature'=>array('hr_schedules','hr_attendance','hr_leave','employees'),'active_keys'=>array('generate_schedule','attendance_monitoring','attendance_monthly','shift','holidays','leave_requests'),'badge'=>$this->leaveApprovalBadge()+$this->manualAttendanceBadge(),'badge_class'=>'nav-badge-warning','badge_title'=>'Pengajuan absensi atau cuti menunggu tindakan'),array('key'=>'hr_payroll','label'=>'Payroll','url'=>'hrpayroll','feature'=>array('payroll','employees')))), array('key'=>'hr','label'=>'HR & Payroll','icon'=>'bi-people','feature'=>array('hr_organization','hr_employees','hr_schedules','hr_attendance','hr_leave','payroll','employees'),'badge'=>$this->leaveApprovalBadge()+$this->manualAttendanceBadge(),'badge_class'=>'nav-badge-warning','badge_title'=>'Pengajuan HR menunggu tindakan','children'=>array(array('key'=>'employees','label'=>'Karyawan','url'=>$employeeUrl,'feature'=>array('hr_employees','hr_organization','employees'),'active_keys'=>array('employees','organization_master')),array('key'=>'attendance_monitoring','label'=>'Absen & Kehadiran','url'=>$attendanceUrl,'feature'=>array('hr_schedules','hr_attendance','hr_leave','employees'),'active_keys'=>array('generate_schedule','attendance_monitoring','attendance_monthly','shift','holidays','leave_requests'),'badge'=>$this->leaveApprovalBadge()+$this->manualAttendanceBadge(),'badge_class'=>'nav-badge-warning','badge_title'=>'Pengajuan absensi atau cuti menunggu tindakan'),array('key'=>'hr_payroll','label'=>'Payroll','url'=>'hrpayroll','feature'=>array('payroll','employees')))),
array('key'=>'companies','label'=>'Company Setting','icon'=>'bi-buildings','url'=>'companies','feature'=>'companies','admin'=>true), array('key'=>'companies','label'=>'Company Setting','icon'=>'bi-buildings','url'=>'companies','feature'=>'companies','admin'=>true),
array('key'=>'settings','label'=>'Pengaturan','icon'=>'bi-gear','feature'=>'setting','admin'=>true,'children'=>array(array('key'=>'users','label'=>'Pengguna','url'=>'users'),array('key'=>'roles','label'=>'Role & Permission','url'=>'roles'))) array('key'=>'settings','label'=>'Pengaturan','icon'=>'bi-gear','feature'=>'setting','admin'=>true,'children'=>array(array('key'=>'users','label'=>'Pengguna','url'=>'users'),array('key'=>'roles','label'=>'Role & Permission','url'=>'roles')))
@@ -27,7 +27,7 @@ class NavigationService
private function leaveApprovalBadge(){if($this->leaveBadge!==null)return$this->leaveBadge;if(!$this->CI->db->table_exists('k_leave_requests')||!$this->CI->db->field_exists('workflow_status','k_leave_requests'))return$this->leaveBadge=0;$this->CI->db->where('workflow_status','submitted');if($this->CI->db->field_exists('company_id','k_leave_requests'))$this->CI->db->where('company_id',(int)$this->CI->session->userdata('company_id'));return$this->leaveBadge=(int)$this->CI->db->count_all_results('k_leave_requests');} private function leaveApprovalBadge(){if($this->leaveBadge!==null)return$this->leaveBadge;if(!$this->CI->db->table_exists('k_leave_requests')||!$this->CI->db->field_exists('workflow_status','k_leave_requests'))return$this->leaveBadge=0;$this->CI->db->where('workflow_status','submitted');if($this->CI->db->field_exists('company_id','k_leave_requests'))$this->CI->db->where('company_id',(int)$this->CI->session->userdata('company_id'));return$this->leaveBadge=(int)$this->CI->db->count_all_results('k_leave_requests');}
private function manualAttendanceBadge(){if($this->attendanceBadge!==null)return$this->attendanceBadge;if(!$this->CI->db->table_exists('manual_attendance_requests'))return$this->attendanceBadge=0;$this->CI->db->where('status','submitted')->where('company_id',(int)$this->CI->session->userdata('company_id'));return$this->attendanceBadge=(int)$this->CI->db->count_all_results('manual_attendance_requests');} private function manualAttendanceBadge(){if($this->attendanceBadge!==null)return$this->attendanceBadge;if(!$this->CI->db->table_exists('manual_attendance_requests'))return$this->attendanceBadge=0;$this->CI->db->where('status','submitted')->where('company_id',(int)$this->CI->session->userdata('company_id'));return$this->attendanceBadge=(int)$this->CI->db->count_all_results('manual_attendance_requests');}
private function salesBadge($name){if($this->salesBadges===null){$c=(int)$this->CI->session->userdata('company_id');$this->salesBadges=array('draft'=>0,'approval'=>0,'delivery'=>0,'receivable'=>0,'overdue'=>0,'return'=>0);if($this->CI->db->table_exists('invoices')){$where=$this->CI->db->field_exists('company_id','invoices')?' AND company_id='.(int)$c:'';$r=$this->CI->db->query("SELECT SUM(workflow_status='draft') draft_count,SUM(workflow_status='submitted') approval_count,SUM(workflow_status='posted' AND sisa_piutang>0) receivable_count,SUM(workflow_status='posted' AND sisa_piutang>0 AND jatuh_tempo<CURDATE()) overdue_count FROM invoices WHERE deleted_at IS NULL".$where)->row();if($r){$this->salesBadges['draft']=(int)$r->draft_count;$this->salesBadges['approval']=(int)$r->approval_count;$this->salesBadges['receivable']=(int)$r->receivable_count;$this->salesBadges['overdue']=(int)$r->overdue_count;}}if($this->CI->db->table_exists('sales_deliveries')){$this->CI->db->where('status','draft');if($this->CI->db->field_exists('company_id','sales_deliveries'))$this->CI->db->where('company_id',$c);$this->salesBadges['delivery']=(int)$this->CI->db->count_all_results('sales_deliveries');}if($this->CI->db->table_exists('sales_returns')){$this->CI->db->where_in('status',array('draft','submitted','approved','received'));if($this->CI->db->field_exists('company_id','sales_returns'))$this->CI->db->where('company_id',$c);$this->salesBadges['return']=(int)$this->CI->db->count_all_results('sales_returns');}}if($name==='all')return$this->salesBadges['draft']+$this->salesBadges['approval']+$this->salesBadges['delivery']+$this->salesBadges['receivable']+$this->salesBadges['return'];return$this->salesBadges[$name]??0;} private function salesBadge($name){if($this->salesBadges===null){$c=(int)$this->CI->session->userdata('company_id');$this->salesBadges=array('draft'=>0,'approval'=>0,'delivery'=>0,'receivable'=>0,'overdue'=>0,'return'=>0);if($this->CI->db->table_exists('invoices')){$where=$this->CI->db->field_exists('company_id','invoices')?' AND company_id='.(int)$c:'';$r=$this->CI->db->query("SELECT SUM(workflow_status='draft') draft_count,SUM(workflow_status='submitted') approval_count,SUM(workflow_status='posted' AND sisa_piutang>0) receivable_count,SUM(workflow_status='posted' AND sisa_piutang>0 AND jatuh_tempo<CURDATE()) overdue_count FROM invoices WHERE deleted_at IS NULL".$where)->row();if($r){$this->salesBadges['draft']=(int)$r->draft_count;$this->salesBadges['approval']=(int)$r->approval_count;$this->salesBadges['receivable']=(int)$r->receivable_count;$this->salesBadges['overdue']=(int)$r->overdue_count;}}if($this->CI->db->table_exists('sales_deliveries')){$this->CI->db->where('status','draft');if($this->CI->db->field_exists('company_id','sales_deliveries'))$this->CI->db->where('company_id',$c);$this->salesBadges['delivery']=(int)$this->CI->db->count_all_results('sales_deliveries');}if($this->CI->db->table_exists('sales_returns')){$this->CI->db->where_in('status',array('draft','submitted','approved','received'));if($this->CI->db->field_exists('company_id','sales_returns'))$this->CI->db->where('company_id',$c);$this->salesBadges['return']=(int)$this->CI->db->count_all_results('sales_returns');}}if($name==='all')return$this->salesBadges['draft']+$this->salesBadges['approval']+$this->salesBadges['delivery']+$this->salesBadges['receivable']+$this->salesBadges['return'];return$this->salesBadges[$name]??0;}
private function budgetApprovalBadge(){if($this->budgetBadge!==null)return$this->budgetBadge;if(!$this->allowed('budgets'))return$this->budgetBadge=0;$company=(int)$this->CI->session->userdata('company_id');$total=0;if($this->CI->db->table_exists('budgets')&&(is_master_admin_user()||check_permission('budgets','can_approve'))){$this->CI->db->where('status','submitted');if($this->CI->db->field_exists('company_id','budgets'))$this->CI->db->where('company_id',$company);$total+=(int)$this->CI->db->count_all_results('budgets');}if($this->CI->db->table_exists('account_budget_entries')&&$this->CI->db->table_exists('account_budget_reminder_completions')){$period=date('Y-m');$row=$this->CI->db->query("SELECT COUNT(*) total FROM account_budget_entries e WHERE e.company_id=? AND e.is_active=1 AND e.reminder_enabled=1 AND ((e.entry_type='recurring' AND e.period<=?) OR (e.entry_type='override' AND e.period=?)) AND LEAST(e.reminder_day,DAY(LAST_DAY(CURDATE())))<=LEAST(DAY(LAST_DAY(CURDATE())),DAY(CURDATE())+7) AND NOT EXISTS(SELECT 1 FROM account_budget_reminder_completions c WHERE c.company_id=e.company_id AND c.budget_entry_id=e.id AND c.period=?)",array($company,$period,$period,$period))->row();$total+=(int)($row?$row->total:0);}return$this->budgetBadge=$total;} private function budgetDueBadge(){if($this->budgetBadge!==null)return$this->budgetBadge;if(!$this->allowed('budgets')||!$this->CI->db->table_exists('account_budget_entries')||!$this->CI->db->table_exists('account_budget_reminder_completions'))return$this->budgetBadge=0;$company=(int)$this->CI->session->userdata('company_id');$period=date('Y-m');$row=$this->CI->db->query("SELECT COUNT(*) total FROM account_budget_entries e WHERE e.company_id=? AND e.is_active=1 AND e.reminder_enabled=1 AND ((e.entry_type='recurring' AND e.period<=?) OR (e.entry_type='override' AND e.period=?)) AND LEAST(e.reminder_day,DAY(LAST_DAY(CURDATE())))<=DAY(CURDATE()) AND NOT EXISTS(SELECT 1 FROM account_budget_reminder_completions c WHERE c.company_id=e.company_id AND c.budget_entry_id=e.id AND c.period=?)",array($company,$period,$period,$period))->row();return$this->budgetBadge=(int)($row?$row->total:0);}
private function pendingItemBadge(){if($this->pendingBadge!==null)return$this->pendingBadge;if(!$this->CI->db->table_exists('items'))return$this->pendingBadge=0;$company=(int)$this->CI->session->userdata('company_id');if($this->CI->db->table_exists('item_barcodes'))return$this->pendingBadge=(int)$this->CI->db->query("SELECT COUNT(DISTINCT i.id) total FROM items i LEFT JOIN item_barcodes ib ON ib.item_id=i.id AND ib.status='pending' WHERE i.company_id=? AND EXISTS(SELECT 1 FROM goods_receipt_lines grl WHERE grl.item_id=i.id) AND(i.status='draft' OR ib.id IS NOT NULL)",array($company))->row()->total;return$this->pendingBadge=(int)$this->CI->db->where(array('status'=>'draft','company_id'=>$company))->count_all_results('items');} private function pendingItemBadge(){if($this->pendingBadge!==null)return$this->pendingBadge;if(!$this->CI->db->table_exists('items'))return$this->pendingBadge=0;$company=(int)$this->CI->session->userdata('company_id');if($this->CI->db->table_exists('item_barcodes'))return$this->pendingBadge=(int)$this->CI->db->query("SELECT COUNT(DISTINCT i.id) total FROM items i LEFT JOIN item_barcodes ib ON ib.item_id=i.id AND ib.status='pending' WHERE i.company_id=? AND EXISTS(SELECT 1 FROM goods_receipt_lines grl WHERE grl.item_id=i.id) AND(i.status='draft' OR ib.id IS NOT NULL)",array($company))->row()->total;return$this->pendingBadge=(int)$this->CI->db->where(array('status'=>'draft','company_id'=>$company))->count_all_results('items');}
private function inventoryDocumentBadge(){if($this->inventoryDocumentBadge!==null)return$this->inventoryDocumentBadge;if(!$this->CI->db->table_exists('stock_documents'))return$this->inventoryDocumentBadge=0;$this->CI->db->where_in('status',array('submitted','approved'));if($this->CI->db->field_exists('company_id','stock_documents'))$this->CI->db->where('company_id',(int)$this->CI->session->userdata('company_id'));return$this->inventoryDocumentBadge=(int)$this->CI->db->count_all_results('stock_documents');} private function inventoryDocumentBadge(){if($this->inventoryDocumentBadge!==null)return$this->inventoryDocumentBadge;if(!$this->CI->db->table_exists('stock_documents'))return$this->inventoryDocumentBadge=0;$this->CI->db->where_in('status',array('submitted','approved'));if($this->CI->db->field_exists('company_id','stock_documents'))$this->CI->db->where('company_id',(int)$this->CI->session->userdata('company_id'));return$this->inventoryDocumentBadge=(int)$this->CI->db->count_all_results('stock_documents');}
private function technicianActionBadge(){ private function technicianActionBadge(){
@@ -50,6 +50,17 @@ class ProfessionalReportService
public function purchaseReturns(array$f){return$this->CI->db->select('r.id record_id,r.return_no,r.return_date,s.supplier_code,s.name supplier,po.po_no,r.problem_category,r.requested_resolution,r.status,r.amount,r.journal_id')->from('purchase_returns r')->join('suppliers s','s.id=r.supplier_id')->join('purchase_orders po','po.id=r.purchase_order_id','left')->where('r.company_id',$this->CI->companycontext->id())->where('r.return_date >=',$f['from'])->where('r.return_date <=',$f['to'])->order_by('r.return_date')->order_by('r.id')->get()->result_array();} public function purchaseReturns(array$f){return$this->CI->db->select('r.id record_id,r.return_no,r.return_date,s.supplier_code,s.name supplier,po.po_no,r.problem_category,r.requested_resolution,r.status,r.amount,r.journal_id')->from('purchase_returns r')->join('suppliers s','s.id=r.supplier_id')->join('purchase_orders po','po.id=r.purchase_order_id','left')->where('r.company_id',$this->CI->companycontext->id())->where('r.return_date >=',$f['from'])->where('r.return_date <=',$f['to'])->order_by('r.return_date')->order_by('r.id')->get()->result_array();}
public function supplierRefunds(array$f,$outstanding=false){$this->CI->db->select('r.id record_id,r.refund_no,r.claim_date,r.expected_date,s.supplier_code,s.name supplier,po.po_no,si.internal_no invoice_supplier,r.source_type,r.status,r.claim_amount,r.received_amount,r.balance,r.entitlement_journal_id journal_id')->from('supplier_refund_claims r')->join('suppliers s','s.id=r.supplier_id')->join('purchase_orders po','po.id=r.purchase_order_id','left')->join('supplier_invoices si','si.id=r.supplier_invoice_id','left')->where('r.company_id',$this->CI->companycontext->id())->where('r.claim_date >=',$f['from'])->where('r.claim_date <=',$f['to']);if($outstanding)$this->CI->db->where('r.balance >',0)->where_not_in('r.status',array('draft','rejected','cancelled','reversed','reconciled'));return$this->CI->db->order_by('r.claim_date')->order_by('r.id')->get()->result_array();} public function supplierRefunds(array$f,$outstanding=false){$this->CI->db->select('r.id record_id,r.refund_no,r.claim_date,r.expected_date,s.supplier_code,s.name supplier,po.po_no,si.internal_no invoice_supplier,r.source_type,r.status,r.claim_amount,r.received_amount,r.balance,r.entitlement_journal_id journal_id')->from('supplier_refund_claims r')->join('suppliers s','s.id=r.supplier_id')->join('purchase_orders po','po.id=r.purchase_order_id','left')->join('supplier_invoices si','si.id=r.supplier_invoice_id','left')->where('r.company_id',$this->CI->companycontext->id())->where('r.claim_date >=',$f['from'])->where('r.claim_date <=',$f['to']);if($outstanding)$this->CI->db->where('r.balance >',0)->where_not_in('r.status',array('draft','rejected','cancelled','reversed','reconciled'));return$this->CI->db->order_by('r.claim_date')->order_by('r.id')->get()->result_array();}
public function supplierDebitNotes(array$f){return$this->CI->db->select('d.id record_id,d.debit_note_no,d.note_date,s.supplier_code,s.name supplier,po.po_no,si.internal_no invoice_supplier,d.correction_type,d.status,d.base_amount,d.tax_amount,d.amount,d.journal_id')->from('supplier_debit_notes d')->join('suppliers s','s.id=d.supplier_id')->join('purchase_orders po','po.id=d.purchase_order_id','left')->join('supplier_invoices si','si.id=d.supplier_invoice_id','left')->where('d.company_id',$this->CI->companycontext->id())->where('d.note_date >=',$f['from'])->where('d.note_date <=',$f['to'])->order_by('d.note_date')->order_by('d.id')->get()->result_array();} public function supplierDebitNotes(array$f){return$this->CI->db->select('d.id record_id,d.debit_note_no,d.note_date,s.supplier_code,s.name supplier,po.po_no,si.internal_no invoice_supplier,d.correction_type,d.status,d.base_amount,d.tax_amount,d.amount,d.journal_id')->from('supplier_debit_notes d')->join('suppliers s','s.id=d.supplier_id')->join('purchase_orders po','po.id=d.purchase_order_id','left')->join('supplier_invoices si','si.id=d.supplier_invoice_id','left')->where('d.company_id',$this->CI->companycontext->id())->where('d.note_date >=',$f['from'])->where('d.note_date <=',$f['to'])->order_by('d.note_date')->order_by('d.id')->get()->result_array();}
public function bankAccountBalances(array$f)
{
if(!$this->CI->db->table_exists('company_bank_accounts'))return array();
$company=(int)$this->CI->companycontext->id();
$invoiceColumn=$this->CI->db->field_exists('show_on_invoice','company_bank_accounts')?'b.show_on_invoice':'b.is_active AS show_on_invoice';
$sql="SELECT b.id bank_account_id,b.bank_name,b.account_number,b.account_holder,b.product_name,b.account_type,b.currency,b.is_primary,b.is_active,{$invoiceColumn},b.opened_at,b.maturity_date,a.id gl_account_id,a.kode_akun,a.nama_akun,COALESCE(x.closing_balance,0) closing_balance,(SELECT COUNT(*) FROM company_bank_accounts bx WHERE bx.company_id=b.company_id AND bx.gl_account_id=b.gl_account_id AND b.gl_account_id IS NOT NULL) linked_account_count FROM company_bank_accounts b LEFT JOIN accounts a ON a.id=b.gl_account_id AND a.company_id=b.company_id LEFT JOIN(SELECT d.account_id,SUM(d.debit-d.kredit) closing_balance FROM journal_details d JOIN journals j ON j.id=d.journal_id WHERE j.company_id=? AND j.status IN('posted','reversed') AND j.tanggal<=? GROUP BY d.account_id)x ON x.account_id=b.gl_account_id WHERE b.company_id=?";
$bind=array($company,$f['to'],$company);if(!empty($f['account_id'])){$sql.=' AND b.gl_account_id=?';$bind[]=(int)$f['account_id'];}$sql.=' ORDER BY b.bank_name ASC,b.account_number ASC,b.id ASC';
$rows=$this->CI->db->query($sql,$bind)->result_array();
foreach($rows as&$row){$row['closing_balance']=(float)$row['closing_balance'];$row['account_type_label']=$row['account_type']==='deposit'?'Deposito':'Tabungan';$row['account_status']=$row['is_active']?'Aktif':'Nonaktif';$row['invoice_status']=$row['show_on_invoice']?'Ditampilkan':'Tidak ditampilkan';$row['mapping_status']=empty($row['gl_account_id'])?'Belum terhubung':((int)$row['linked_account_count']>1?'COA dipakai '.(int)$row['linked_account_count'].' rekening':'Terhubung');}unset($row);
return$rows;
}
public function salesReport($code,array$f){$c=$this->CI->companycontext->id();if($code==='revenue_recognition')return$this->revenueRecognition($f);if($code==='receivable_reconciliation')return$this->receivableReconciliation($f);if($code==='sales_summary')return$this->CI->db->select('i.id record_id,i.no_invoice,i.tanggal,c.nama customer,i.invoice_type,i.recognition_policy,i.status,i.total,i.total_bayar,i.sisa_piutang')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->order_by('i.tanggal')->get()->result_array();if($code==='sales_by_customer')return$this->CI->db->select('c.id customer_id,c.nama customer,COUNT(i.id) invoice_count,SUM(i.total) sales,SUM(i.total_bayar) paid,SUM(i.sisa_piutang) balance',false)->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->group_by('c.id')->order_by('sales','DESC')->get()->result_array();if($code==='sales_by_item')return$this->CI->db->select('d.items_id item_id,it.kode_detail AS kode_barang,it.nama_barang,SUM(d.qty) qty,SUM(d.net_amount) net_sales,SUM(d.tax_amount) tax,SUM(d.subtotal) total',false)->from('invoice_details d')->join('invoices i','i.id=d.invoice_id')->join('items it','it.id=d.items_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->group_by('d.items_id')->order_by('net_sales','DESC')->get()->result_array();if($code==='sales_by_barcode')return$this->CI->db->select('i.id record_id,i.no_invoice,i.tanggal,it.kode_detail AS kode_barang,it.nama_barang,b.barcode,b.serial_number,lb.qty,sd.delivery_no')->from('invoice_line_barcodes lb')->join('invoices i','i.id=lb.invoice_id')->join('item_barcodes b','b.id=lb.barcode_id')->join('items it','it.id=b.item_id')->join('sales_deliveries sd','sd.id=lb.sales_delivery_id','left')->where('lb.company_id',$c)->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->order_by('i.tanggal')->get()->result_array();if($code==='running_invoices')return$this->CI->db->select('i.id record_id,i.no_invoice,c.nama customer,i.period_start,i.period_end,i.delivery_status,i.total,i.workflow_status')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.invoice_type'=>'running','i.workflow_status'=>'draft'))->order_by('i.period_end')->get()->result_array();if($code==='sales_returns')return$this->CI->db->select('r.id record_id,r.return_no,r.return_date,c.nama customer,i.no_invoice,d.delivery_no,r.resolution,r.status,SUM(l.net_amount+l.tax_amount) amount',false)->from('sales_returns r')->join('customers c','c.id=r.customer_id')->join('invoices i','i.id=r.invoice_id','left')->join('sales_deliveries d','d.id=r.sales_delivery_id','left')->join('sales_return_lines l','l.sales_return_id=r.id','left')->where('r.company_id',$c)->where('r.return_date >=',$f['from'])->where('r.return_date <=',$f['to'])->group_by('r.id')->order_by('r.return_date')->get()->result_array();if($code==='unbilled_deliveries')return$this->CI->db->select('d.id record_id,d.delivery_no,d.delivery_date,c.nama customer,d.status')->from('sales_deliveries d')->join('customers c','c.id=d.customer_id')->where('d.company_id',$c)->where('d.invoice_id IS NULL',null,false)->where('d.delivery_date >=',$f['from'])->where('d.delivery_date <=',$f['to'])->get()->result_array();throw new BusinessException('Jenis laporan penjualan tidak valid.');} public function salesReport($code,array$f){$c=$this->CI->companycontext->id();if($code==='revenue_recognition')return$this->revenueRecognition($f);if($code==='receivable_reconciliation')return$this->receivableReconciliation($f);if($code==='sales_summary')return$this->CI->db->select('i.id record_id,i.no_invoice,i.tanggal,c.nama customer,i.invoice_type,i.recognition_policy,i.status,i.total,i.total_bayar,i.sisa_piutang')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->order_by('i.tanggal')->get()->result_array();if($code==='sales_by_customer')return$this->CI->db->select('c.id customer_id,c.nama customer,COUNT(i.id) invoice_count,SUM(i.total) sales,SUM(i.total_bayar) paid,SUM(i.sisa_piutang) balance',false)->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->group_by('c.id')->order_by('sales','DESC')->get()->result_array();if($code==='sales_by_item')return$this->CI->db->select('d.items_id item_id,it.kode_detail AS kode_barang,it.nama_barang,SUM(d.qty) qty,SUM(d.net_amount) net_sales,SUM(d.tax_amount) tax,SUM(d.subtotal) total',false)->from('invoice_details d')->join('invoices i','i.id=d.invoice_id')->join('items it','it.id=d.items_id')->where(array('i.company_id'=>$c,'i.workflow_status'=>'posted'))->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->group_by('d.items_id')->order_by('net_sales','DESC')->get()->result_array();if($code==='sales_by_barcode')return$this->CI->db->select('i.id record_id,i.no_invoice,i.tanggal,it.kode_detail AS kode_barang,it.nama_barang,b.barcode,b.serial_number,lb.qty,sd.delivery_no')->from('invoice_line_barcodes lb')->join('invoices i','i.id=lb.invoice_id')->join('item_barcodes b','b.id=lb.barcode_id')->join('items it','it.id=b.item_id')->join('sales_deliveries sd','sd.id=lb.sales_delivery_id','left')->where('lb.company_id',$c)->where('i.tanggal >=',$f['from'])->where('i.tanggal <=',$f['to'])->order_by('i.tanggal')->get()->result_array();if($code==='running_invoices')return$this->CI->db->select('i.id record_id,i.no_invoice,c.nama customer,i.period_start,i.period_end,i.delivery_status,i.total,i.workflow_status')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.company_id'=>$c,'i.invoice_type'=>'running','i.workflow_status'=>'draft'))->order_by('i.period_end')->get()->result_array();if($code==='sales_returns')return$this->CI->db->select('r.id record_id,r.return_no,r.return_date,c.nama customer,i.no_invoice,d.delivery_no,r.resolution,r.status,SUM(l.net_amount+l.tax_amount) amount',false)->from('sales_returns r')->join('customers c','c.id=r.customer_id')->join('invoices i','i.id=r.invoice_id','left')->join('sales_deliveries d','d.id=r.sales_delivery_id','left')->join('sales_return_lines l','l.sales_return_id=r.id','left')->where('r.company_id',$c)->where('r.return_date >=',$f['from'])->where('r.return_date <=',$f['to'])->group_by('r.id')->order_by('r.return_date')->get()->result_array();if($code==='unbilled_deliveries')return$this->CI->db->select('d.id record_id,d.delivery_no,d.delivery_date,c.nama customer,d.status')->from('sales_deliveries d')->join('customers c','c.id=d.customer_id')->where('d.company_id',$c)->where('d.invoice_id IS NULL',null,false)->where('d.delivery_date >=',$f['from'])->where('d.delivery_date <=',$f['to'])->get()->result_array();throw new BusinessException('Jenis laporan penjualan tidak valid.');}
private function revenueRecognition(array$f) private function revenueRecognition(array$f)
{ {
@@ -63,5 +74,5 @@ class ProfessionalReportService
{ {
if($rows===null)$rows=$this->receivableReconciliation($f);$subledger=0;foreach($rows as$row)if(empty($row['reconciliation_adjustment']))$subledger+=(float)$row['remaining_receivable'];$this->CI->load->library('AccountMappingService');$account=(int)$this->CI->accountmappingservice->get('accounts_receivable');$gl=(float)$this->CI->db->query("SELECT COALESCE(SUM(d.debit-d.kredit),0) amount FROM journal_details d JOIN journals j ON j.id=d.journal_id WHERE j.company_id=? AND j.status IN('posted','reversed') AND j.tanggal<=? AND d.account_id=?",array((int)$this->CI->companycontext->id(),$f['to'],$account))->row()->amount;return array('subledger'=>$subledger,'general_ledger'=>$gl,'difference'=>round($gl-$subledger,2),'balanced'=>abs($gl-$subledger)<.01); if($rows===null)$rows=$this->receivableReconciliation($f);$subledger=0;foreach($rows as$row)if(empty($row['reconciliation_adjustment']))$subledger+=(float)$row['remaining_receivable'];$this->CI->load->library('AccountMappingService');$account=(int)$this->CI->accountmappingservice->get('accounts_receivable');$gl=(float)$this->CI->db->query("SELECT COALESCE(SUM(d.debit-d.kredit),0) amount FROM journal_details d JOIN journals j ON j.id=d.journal_id WHERE j.company_id=? AND j.status IN('posted','reversed') AND j.tanggal<=? AND d.account_id=?",array((int)$this->CI->companycontext->id(),$f['to'],$account))->row()->amount;return array('subledger'=>$subledger,'general_ledger'=>$gl,'difference'=>round($gl-$subledger,2),'balanced'=>abs($gl-$subledger)<.01);
} }
public function run($code,array$f){if($code==='trial_balance')return$this->trialBalance($f);if(in_array($code,array('profit_loss','balance_sheet'),true))return$this->statement($code,$f);if(in_array($code,array('general_ledger','journal_register','account_detail'),true))return$this->ledger($f);if($code==='cash_flow')return$this->cashFlow($f);if($code==='changes_equity')return$this->equity($f);if($code==='actual_budget')return$this->actualBudget($f);if($code==='comparative')return$this->comparative($f);if($code==='purchase_returns')return$this->purchaseReturns($f);if($code==='supplier_refunds')return$this->supplierRefunds($f);if($code==='outstanding_supplier_refunds')return$this->supplierRefunds($f,true);if($code==='supplier_debit_notes')return$this->supplierDebitNotes($f);if(in_array($code,array('sales_summary','sales_by_customer','sales_by_item','sales_by_barcode','running_invoices','sales_returns','revenue_recognition','unbilled_deliveries','receivable_reconciliation'),true))return$this->salesReport($code,$f);throw new BusinessException('Jenis laporan tidak valid.');} public function run($code,array$f){if($code==='trial_balance')return$this->trialBalance($f);if(in_array($code,array('profit_loss','balance_sheet'),true))return$this->statement($code,$f);if(in_array($code,array('general_ledger','journal_register','account_detail'),true))return$this->ledger($f);if($code==='cash_flow')return$this->cashFlow($f);if($code==='bank_account_balances')return$this->bankAccountBalances($f);if($code==='changes_equity')return$this->equity($f);if($code==='actual_budget')return$this->actualBudget($f);if($code==='comparative')return$this->comparative($f);if($code==='purchase_returns')return$this->purchaseReturns($f);if($code==='supplier_refunds')return$this->supplierRefunds($f);if($code==='outstanding_supplier_refunds')return$this->supplierRefunds($f,true);if($code==='supplier_debit_notes')return$this->supplierDebitNotes($f);if(in_array($code,array('sales_summary','sales_by_customer','sales_by_item','sales_by_barcode','running_invoices','sales_returns','revenue_recognition','unbilled_deliveries','receivable_reconciliation'),true))return$this->salesReport($code,$f);throw new BusinessException('Jenis laporan tidak valid.');}
} }
@@ -0,0 +1,25 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Migration_Add_invoice_visibility_to_company_bank_accounts extends CI_Migration
{
public function up()
{
if (!$this->db->table_exists('company_bank_accounts')) return;
if (!$this->db->field_exists('show_on_invoice', 'company_bank_accounts')) {
$this->db->query("ALTER TABLE `company_bank_accounts`
ADD `show_on_invoice` TINYINT(1) NOT NULL DEFAULT 1 AFTER `is_primary`");
}
if (!$this->db->query("SHOW INDEX FROM company_bank_accounts WHERE Key_name='idx_company_bank_invoice'")->row()) {
$this->db->query("ALTER TABLE `company_bank_accounts`
ADD KEY `idx_company_bank_invoice` (`company_id`,`is_active`,`show_on_invoice`,`is_primary`)");
}
}
public function down()
{
throw new RuntimeException('Pilihan rekening pada PDF Invoice dipertahankan untuk histori dokumen.');
}
}
+4 -4
View File
@@ -18,7 +18,7 @@ $periodLabel=function($period)use($monthNames){$parts=explode('-',(string)$perio
<?php if($active_tab==='list'):?> <?php if($active_tab==='list'):?>
<section class="budget-card"> <section class="budget-card">
<div class="budget-card-head"><div><h5>Daftar Budget</h5><p>Setiap kebutuhan dibuat sebagai jadwal tersendiri. Beberapa jadwal boleh menggunakan akun yang sama dan semuanya dijumlahkan.</p></div><?php if($can_export):?><div class="btn-group"><button type="button" class="btn btn-outline-danger budget-pdf" data-title="Daftar Budget" data-url="<?=base_url('budgets/export_list/pdf?'.http_build_query(array('list_account_id'=>$list_account_id)))?>"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</button><a class="btn btn-outline-success" href="<?=base_url('budgets/export_list/excel?'.http_build_query(array('list_account_id'=>$list_account_id)))?>"><i class="bi bi-file-earmark-excel me-1"></i>Excel</a></div><?php endif?></div> <div class="budget-card-head"><div><h5>Daftar Budget</h5><p>Setiap kebutuhan dibuat sebagai jadwal tersendiri. Beberapa jadwal boleh menggunakan akun yang sama dan semuanya dijumlahkan.</p></div><?php if($can_export):?><div class="btn-group"><button type="button" class="btn btn-outline-danger budget-pdf" data-title="Daftar Budget" data-url="<?=base_url('budgets/export_list/pdf?'.http_build_query(array('list_account_id'=>$list_account_id)))?>"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</button><a class="btn btn-outline-success" href="<?=base_url('budgets/export_list/excel?'.http_build_query(array('list_account_id'=>$list_account_id)))?>"><i class="bi bi-file-earmark-excel me-1"></i>Excel</a></div><?php endif?></div>
<form class="budget-filter" method="get"><input type="hidden" name="tab" value="list"><div><label class="form-label">Filter Akun</label><select class="form-select" name="list_account_id"><option value="">Semua akun</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>" <?=$list_account_id==$a->id?'selected':''?>><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><button class="btn btn-outline-primary"><i class="bi bi-funnel me-1"></i>Tampilkan</button><?php if($list_account_id):?><a href="<?=base_url('budgets?tab=list')?>" class="btn btn-light">Reset</a><?php endif?></form> <form class="budget-filter" method="get"><input type="hidden" name="tab" value="list"><div><label class="form-label">Filter Akun</label><select class="form-select" name="list_account_id"><option value="">Semua akun</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>" <?=$list_account_id==$a->id?'selected':''?>><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><button class="btn btn-outline-primary"><i class="bi bi-funnel me-1"></i>Tampilkan</button><?php if($list_account_id):?><a href="<?=base_url('budgets?tab=list')?>" class="btn btn-light">Reset</a><?php endif?><div class="budget-filter-total"><span>Total Budget</span><strong>Rp <?=number_format($list_total_budget,2,',','.')?></strong></div></form>
<div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Budget / Akun</th><th>Pola</th><th>Mulai / Bulan</th><th class="text-end">Nominal Budget</th><th>Pengingat</th><th>Keterangan</th><th>Diperbarui</th><th>Aksi</th></tr></thead><tbody> <div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Budget / Akun</th><th>Pola</th><th>Mulai / Bulan</th><th class="text-end">Nominal Budget</th><th>Pengingat</th><th>Keterangan</th><th>Diperbarui</th><th>Aksi</th></tr></thead><tbody>
<?php foreach($entries as$entry):?><tr> <?php foreach($entries as$entry):?><tr>
<td><strong><?=html_escape($entry->title)?></strong><small class="d-block text-muted"><?=html_escape($entry->kode_akun.' - '.$entry->nama_akun.' · '.($accountTypes[$entry->tipe]??ucfirst($entry->tipe)))?></small></td> <td><strong><?=html_escape($entry->title)?></strong><small class="d-block text-muted"><?=html_escape($entry->kode_akun.' - '.$entry->nama_akun.' · '.($accountTypes[$entry->tipe]??ucfirst($entry->tipe)))?></small></td>
@@ -34,7 +34,7 @@ $periodLabel=function($period)use($monthNames){$parts=explode('-',(string)$perio
<?php else:?> <?php else:?>
<section class="budget-card"> <section class="budget-card">
<div class="budget-card-head"><div><h5>Budget Per Akun</h5><p>Hanya akun yang mempunyai budget yang ditampilkan. Budget berulang dihitung otomatis untuk setiap bulan dalam rentang.</p></div><?php if($can_export):?><div class="btn-group"><button type="button" class="btn btn-outline-danger budget-pdf" data-title="Budget Per Akun" data-url="<?=base_url('budgets/export_accounts/pdf?'.http_build_query($filters))?>"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</button><a class="btn btn-outline-success" href="<?=base_url('budgets/export_accounts/excel?'.http_build_query($filters))?>"><i class="bi bi-file-earmark-excel me-1"></i>Excel</a></div><?php endif?></div> <div class="budget-card-head"><div><h5>Budget Per Akun</h5><p>Hanya akun yang mempunyai budget yang ditampilkan. Budget berulang dihitung otomatis untuk setiap bulan dalam rentang.</p></div><?php if($can_export):?><div class="btn-group"><button type="button" class="btn btn-outline-danger budget-pdf" data-title="Budget Per Akun" data-url="<?=base_url('budgets/export_accounts/pdf?'.http_build_query($filters))?>"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</button><a class="btn btn-outline-success" href="<?=base_url('budgets/export_accounts/excel?'.http_build_query($filters))?>"><i class="bi bi-file-earmark-excel me-1"></i>Excel</a></div><?php endif?></div>
<form class="budget-filter budget-period-filter" method="get"><input type="hidden" name="tab" value="accounts"><div><label class="form-label">Dari Bulan</label><input type="month" class="form-control" name="from" value="<?=html_escape($filters['from'])?>" required></div><div><label class="form-label">Sampai Bulan</label><input type="month" class="form-control" name="to" value="<?=html_escape($filters['to'])?>" required></div><div><label class="form-label">Akun</label><select class="form-select" name="account_id"><option value="">Semua akun yang memiliki budget</option><?php foreach($budget_accounts as$a):?><option value="<?=$a->id?>" <?=$filters['account_id']==$a->id?'selected':''?>><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><button class="btn btn-primary"><i class="bi bi-search me-1"></i>Tampilkan</button></form> <form class="budget-filter budget-period-filter" method="get"><input type="hidden" name="tab" value="accounts"><div class="budget-month-field"><label class="form-label">Dari Bulan</label><input type="month" class="form-control" name="from" value="<?=html_escape($filters['from'])?>" required></div><div class="budget-month-field"><label class="form-label">Sampai Bulan</label><input type="month" class="form-control" name="to" value="<?=html_escape($filters['to'])?>" required></div><div class="budget-account-field"><label class="form-label">Akun</label><select class="form-select" name="account_id"><option value="">Semua akun yang memiliki budget</option><?php foreach($budget_accounts as$a):?><option value="<?=$a->id?>" <?=$filters['account_id']==$a->id?'selected':''?>><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><button class="btn btn-primary"><i class="bi bi-search me-1"></i>Tampilkan</button><div class="budget-filter-total"><span>Total Budget</span><strong>Rp <?=number_format($account_total_budget,2,',','.')?></strong></div></form>
<div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Akun</th><th class="text-end">Akumulasi Budget</th><th class="text-end">Realisasi</th><th class="text-end">Komitmen PO</th><th class="text-end">Total Pemakaian</th><th class="text-end">Sisa</th><th class="text-end">%</th><th>Kondisi</th></tr></thead><tbody> <div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Akun</th><th class="text-end">Akumulasi Budget</th><th class="text-end">Realisasi</th><th class="text-end">Komitmen PO</th><th class="text-end">Total Pemakaian</th><th class="text-end">Sisa</th><th class="text-end">%</th><th>Kondisi</th></tr></thead><tbody>
<?php foreach($report_rows as$row):?><tr><td><strong><?=html_escape($row['kode_akun'].' - '.$row['nama_akun'])?></strong><small class="d-block text-muted"><?=html_escape($accountTypes[$row['tipe']]??ucfirst($row['tipe']))?></small></td><?php foreach(array('budget','actual','commitment','usage','remaining')as$key):?><td class="text-end <?=$key==='budget'?'fw-semibold':''?>">Rp <?=number_format($row[$key],2,',','.')?></td><?php endforeach?><td class="text-end"><?=number_format($row['percentage'],2,',','.')?>%</td><td><span class="badge bg-<?=$row['indicator']==='over'?'danger':($row['indicator']==='warning'?'warning text-dark':'success')?>"><?=$row['indicator']==='over'?'Melebihi':($row['indicator']==='warning'?'Mendekati Batas':'Aman')?></span></td></tr><?php endforeach?> <?php foreach($report_rows as$row):?><tr><td><strong><?=html_escape($row['kode_akun'].' - '.$row['nama_akun'])?></strong><small class="d-block text-muted"><?=html_escape($accountTypes[$row['tipe']]??ucfirst($row['tipe']))?></small></td><?php foreach(array('budget','actual','commitment','usage','remaining')as$key):?><td class="text-end <?=$key==='budget'?'fw-semibold':''?>">Rp <?=number_format($row[$key],2,',','.')?></td><?php endforeach?><td class="text-end"><?=number_format($row['percentage'],2,',','.')?>%</td><td><span class="badge bg-<?=$row['indicator']==='over'?'danger':($row['indicator']==='warning'?'warning text-dark':'success')?>"><?=$row['indicator']==='over'?'Melebihi':($row['indicator']==='warning'?'Mendekati Batas':'Aman')?></span></td></tr><?php endforeach?>
<?php if(!$report_rows):?><tr><td colspan="8" class="empty-state"><i class="bi bi-bar-chart"></i><strong>Belum ada budget pada periode ini</strong><span>Buat budget berulang atau budget khusus bulan terlebih dahulu.</span></td></tr><?php endif?> <?php if(!$report_rows):?><tr><td colspan="8" class="empty-state"><i class="bi bi-bar-chart"></i><strong>Belum ada budget pada periode ini</strong><span>Buat budget berulang atau budget khusus bulan terlebih dahulu.</span></td></tr><?php endif?>
@@ -53,9 +53,9 @@ $periodLabel=function($period)use($monthNames){$parts=explode('-',(string)$perio
</div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" id="saveBudget"><i class="bi bi-check2-circle me-1"></i>Simpan Budget</button></div></form></div></div> </div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" id="saveBudget"><i class="bi bi-check2-circle me-1"></i>Simpan Budget</button></div></form></div></div>
<div class="modal fade" id="budgetPdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content budget-pdf-content"><div class="modal-header"><h5 class="modal-title">Pratinjau PDF Budget</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="Pratinjau PDF Budget"></iframe></div></div></div></div> <div class="modal fade" id="budgetPdfModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content budget-pdf-content"><div class="modal-header"><h5 class="modal-title">Pratinjau PDF Budget</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe title="Pratinjau PDF Budget"></iframe></div></div></div></div>
<style> <style>
.budget-page{--budget-blue:#176bff}.budget-hero{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:18px}.budget-hero small{font-size:.7rem;letter-spacing:.15em;color:#758199}.budget-hero h4{margin:2px 0 3px}.budget-hero p,.budget-card-head p{margin:0;color:#69758a}.budget-section-nav{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:8px;padding:8px;margin-bottom:16px;background:#fff;border:1px solid #e4e9f1;border-radius:14px}.budget-section-nav a{display:flex;align-items:center;justify-content:center;gap:9px;min-height:48px;padding:10px 14px;border-radius:9px;color:#526079;text-decoration:none;font-weight:600}.budget-section-nav a:hover{background:#f3f7ff;color:var(--budget-blue)}.budget-section-nav a.active{background:var(--budget-blue);color:#fff;box-shadow:0 5px 14px rgba(23,107,255,.2)}.budget-section-nav b{min-width:22px;padding:2px 6px;border-radius:20px;background:rgba(255,255,255,.22);font-size:.72rem;text-align:center}.budget-section-nav a:not(.active) b{background:#eef2f8}.budget-card{overflow:hidden;background:#fff;border:1px solid #e4e9f1;border-radius:14px;box-shadow:0 6px 22px rgba(22,34,55,.05)}.budget-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:17px 18px;border-bottom:1px solid #e8ecf2}.budget-card-head h5{margin:0 0 3px}.budget-filter{display:flex;align-items:end;gap:10px;padding:14px 18px;background:#fafbfd;border-bottom:1px solid #e8ecf2}.budget-filter>div{min-width:min(360px,100%)}.budget-period-filter{display:grid;grid-template-columns:180px 180px minmax(240px,1fr) auto}.budget-card .table th{padding:12px 14px;background:#f6f8fb;color:#59657a;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}.budget-card .table td{padding:13px 14px}.empty-state{text-align:center!important;padding:54px 20px!important;color:#768196}.empty-state i,.empty-state strong,.empty-state span{display:block}.empty-state i{margin-bottom:8px;font-size:1.8rem}.empty-state strong{color:#344054}.budget-type-options{display:grid;gap:8px}.budget-type-options label{display:flex;gap:10px;padding:12px;border:1px solid #dfe5ee;border-radius:10px;cursor:pointer}.budget-type-options label:has(input:checked){border-color:var(--budget-blue);background:#f2f7ff}.budget-type-options input{margin-top:3px}.budget-type-options span,.budget-type-options small{display:block}.budget-type-options small{margin-top:2px;color:#6f7b8f;font-weight:400}@media(max-width:767.98px){.budget-hero{align-items:flex-start}.budget-section-nav{display:flex;overflow:auto}.budget-section-nav a{flex:1;white-space:nowrap}.budget-filter,.budget-period-filter{display:grid;grid-template-columns:1fr}.budget-filter>div{min-width:0}.budget-card-head{align-items:flex-start}.budget-card .table td,.budget-card .table th{white-space:nowrap}.modal-dialog{margin:.5rem}} .budget-page{--budget-blue:#176bff}.budget-hero{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:18px}.budget-hero small{font-size:.7rem;letter-spacing:.15em;color:#758199}.budget-hero h4{margin:2px 0 3px}.budget-hero p,.budget-card-head p{margin:0;color:#69758a}.budget-section-nav{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:8px;padding:8px;margin-bottom:16px;background:#fff;border:1px solid #e4e9f1;border-radius:14px}.budget-section-nav a{display:flex;align-items:center;justify-content:center;gap:9px;min-height:48px;padding:10px 14px;border-radius:9px;color:#526079;text-decoration:none;font-weight:600}.budget-section-nav a:hover{background:#f3f7ff;color:var(--budget-blue)}.budget-section-nav a.active{background:var(--budget-blue);color:#fff;box-shadow:0 5px 14px rgba(23,107,255,.2)}.budget-section-nav b{min-width:22px;padding:2px 6px;border-radius:20px;background:rgba(255,255,255,.22);font-size:.72rem;text-align:center}.budget-section-nav a:not(.active) b{background:#eef2f8}.budget-card{overflow:hidden;background:#fff;border:1px solid #e4e9f1;border-radius:14px;box-shadow:0 6px 22px rgba(22,34,55,.05)}.budget-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:17px 18px;border-bottom:1px solid #e8ecf2}.budget-card-head h5{margin:0 0 3px}.budget-filter{display:flex;align-items:end;gap:10px;padding:14px 18px;background:#fafbfd;border-bottom:1px solid #e8ecf2;flex-wrap:wrap}.budget-filter:not(.budget-period-filter)>div:not(.budget-filter-total){min-width:min(360px,100%)}.budget-period-filter .budget-month-field{flex:0 0 180px;min-width:180px}.budget-period-filter .budget-account-field{flex:0 1 320px;min-width:240px;max-width:360px}.budget-card .table th{padding:12px 14px;background:#f6f8fb;color:#59657a;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}.budget-card .table td{padding:13px 14px}.empty-state{text-align:center!important;padding:54px 20px!important;color:#768196}.empty-state i,.empty-state strong,.empty-state span{display:block}.empty-state i{margin-bottom:8px;font-size:1.8rem}.empty-state strong{color:#344054}.budget-type-options{display:grid;gap:8px}.budget-type-options label{display:flex;gap:10px;padding:12px;border:1px solid #dfe5ee;border-radius:10px;cursor:pointer}.budget-type-options label:has(input:checked){border-color:var(--budget-blue);background:#f2f7ff}.budget-type-options input{margin-top:3px}.budget-type-options span,.budget-type-options small{display:block}.budget-type-options small{margin-top:2px;color:#6f7b8f;font-weight:400}@media(max-width:767.98px){.budget-hero{align-items:flex-start}.budget-section-nav{display:flex;overflow:auto}.budget-section-nav a{flex:1;white-space:nowrap}.budget-filter,.budget-period-filter{display:grid;grid-template-columns:1fr}.budget-filter>div,.budget-period-filter .budget-month-field,.budget-period-filter .budget-account-field{width:100%;min-width:0;max-width:none}.budget-card-head{align-items:flex-start}.budget-card .table td,.budget-card .table th{white-space:nowrap}.modal-dialog{margin:.5rem}}
.budget-reminder-alert{display:flex;align-items:flex-start;gap:12px;margin-bottom:16px;padding:14px 16px;border:1px solid #ffd89a;border-radius:13px;background:#fff8e8;color:#6c4a09}.reminder-icon{display:grid;place-items:center;width:38px;height:38px;flex:0 0 38px;border-radius:10px;background:#ffedc4;color:#e48300;font-size:1.1rem}.reminder-items{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px}.reminder-items span{padding:4px 9px;border-radius:20px;background:#fff;border:1px solid #f2d79c;font-size:.78rem}.reminder-items span.overdue{border-color:#f2b8b8;background:#fff0f0;color:#b42318}.budget-reminder-option{padding:13px 14px;border:1px solid #dfe5ee;border-radius:10px;background:#fafbfd} .budget-reminder-alert{display:flex;align-items:flex-start;gap:12px;margin-bottom:16px;padding:14px 16px;border:1px solid #ffd89a;border-radius:13px;background:#fff8e8;color:#6c4a09}.reminder-icon{display:grid;place-items:center;width:38px;height:38px;flex:0 0 38px;border-radius:10px;background:#ffedc4;color:#e48300;font-size:1.1rem}.reminder-items{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px}.reminder-items span{padding:4px 9px;border-radius:20px;background:#fff;border:1px solid #f2d79c;font-size:.78rem}.reminder-items span.overdue{border-color:#f2b8b8;background:#fff0f0;color:#b42318}.budget-reminder-option{padding:13px 14px;border:1px solid #dfe5ee;border-radius:10px;background:#fafbfd}
.budget-pdf-content{height:min(90vh,900px)}.budget-pdf-content .modal-body{min-height:0}.budget-pdf-content iframe{width:100%;height:100%;border:0}.budget-card-head .btn-group{flex:0 0 auto}@media(max-width:575.98px){.budget-card-head{flex-direction:column}.budget-card-head .btn-group{width:100%}.budget-card-head .btn-group>*{flex:1}} .budget-filter-total{display:flex;align-items:center;justify-content:flex-end;gap:12px;min-width:0;margin-left:auto;align-self:center;white-space:nowrap}.budget-filter-total span{color:#69758a;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em}.budget-filter-total strong{color:#172b4d;font-size:1.05rem}.budget-pdf-content{height:min(90vh,900px)}.budget-pdf-content .modal-body{min-height:0}.budget-pdf-content iframe{width:100%;height:100%;border:0}.budget-card-head .btn-group{flex:0 0 auto}@media(max-width:575.98px){.budget-card-head{flex-direction:column}.budget-card-head .btn-group{width:100%}.budget-card-head .btn-group>*{flex:1}.budget-filter-total{width:100%;margin-left:0;justify-content:space-between}}
</style> </style>
<script> <script>
document.addEventListener('DOMContentLoaded',function(){ document.addEventListener('DOMContentLoaded',function(){
+5 -1
View File
@@ -28,6 +28,9 @@
<script> <script>
(function($){ (function($){
var form=document.getElementById('bankAccountForm');if(!form)return; var form=document.getElementById('bankAccountForm');if(!form)return;
$('#companyBanks .settings-card-head p').eq(0).text('Pilih sendiri rekening aktif yang boleh ditampilkan pada PDF Invoice.');
$('#companyBanks .settings-card-head p').eq(1).text('Seluruh rekening tetap tersimpan dan tetap muncul pada laporan, termasuk yang nonaktif.');
$(form).find('input[name="is_primary"]').closest('.col-5').after('<div class="col-12"><label class="form-check mb-0"><input class="form-check-input" type="checkbox" name="show_on_invoice" value="1" checked><span class="form-check-label">Tampilkan rekening ini pada PDF Invoice</span></label><small class="d-block text-muted mt-1">Tidak memengaruhi pencatatan maupun Laporan Profesional.</small></div>');
$('[data-bs-target="#companyBanks"]').html('<i class="bi bi-bank"></i> Akun Bank Perusahaan'); $('[data-bs-target="#companyBanks"]').html('<i class="bi bi-bank"></i> Akun Bank Perusahaan');
var glAccounts=<?=json_encode(array_map(function($a){return array('id'=>$a->id,'text'=>$a->kode_akun.' - '.$a->nama_akun);},$bank_gl_accounts),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)?>; var glAccounts=<?=json_encode(array_map(function($a){return array('id'=>$a->id,'text'=>$a->kode_akun.' - '.$a->nama_akun);},$bank_gl_accounts),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)?>;
var coaOptions='<option value="">Pilih akun COA</option>'+glAccounts.map(function(a){return '<option value="'+a.id+'">'+$('<div>').text(a.text).html()+'</option>';}).join(''); var coaOptions='<option value="">Pilih akun COA</option>'+glAccounts.map(function(a){return '<option value="'+a.id+'">'+$('<div>').text(a.text).html()+'</option>';}).join('');
@@ -38,11 +41,12 @@
function accountTypeChanged(){var deposit=form.elements.account_type.value==='deposit';$(form).find('.maturity-field').toggleClass('d-none',!deposit);form.elements.maturity_date.required=deposit;if(!deposit)form.elements.maturity_date.value='';} function accountTypeChanged(){var deposit=form.elements.account_type.value==='deposit';$(form).find('.maturity-field').toggleClass('d-none',!deposit);form.elements.maturity_date.required=deposit;if(!deposit)form.elements.maturity_date.value='';}
$(form.elements.account_type).on('change',accountTypeChanged);accountTypeChanged(); $(form.elements.account_type).on('change',accountTypeChanged);accountTypeChanged();
$('#cancelBankEdit').on('click',function(){$(form.elements.gl_account_id).val('').trigger('change');accountTypeChanged();}); $('#cancelBankEdit').on('click',function(){$(form.elements.gl_account_id).val('').trigger('change');accountTypeChanged();});
$('.edit-bank').on('click',function(){var r=$(this).data('row');form.elements.product_name.value=r.product_name||'';form.elements.account_type.value=r.account_type||'savings';form.elements.interest_rate.value=r.interest_rate||0;form.elements.opened_at.value=r.opened_at||'';form.elements.maturity_date.value=r.maturity_date||'';form.elements.gl_account_id.value=r.gl_account_id||'';$(form.elements.gl_account_id).trigger('change');accountTypeChanged();}); $('.edit-bank').on('click',function(){var r=$(this).data('row');form.elements.product_name.value=r.product_name||'';form.elements.account_type.value=r.account_type||'savings';form.elements.interest_rate.value=r.interest_rate||0;form.elements.opened_at.value=r.opened_at||'';form.elements.maturity_date.value=r.maturity_date||'';form.elements.gl_account_id.value=r.gl_account_id||'';form.elements.show_on_invoice.checked=Number(r.show_on_invoice)===1;$(form.elements.gl_account_id).trigger('change');accountTypeChanged();});
var table=$('#companyBanks table').first(),head=table.find('thead tr');head.html('<th>Bank / Rekening</th><th>Produk</th><th>Mata Uang</th><th>Akun COA</th><th>Status</th><th>Aksi</th>'); var table=$('#companyBanks table').first(),head=table.find('thead tr');head.html('<th>Bank / Rekening</th><th>Produk</th><th>Mata Uang</th><th>Akun COA</th><th>Status</th><th>Aksi</th>');
table.find('tbody tr').each(function(){var tr=$(this),button=tr.find('.edit-bank');if(!button.length){tr.children('td').attr('colspan',6);return;}var r=button.data('row'),cells=tr.children('td'),product=$('<td>').html('<b>'+(r.account_type==='deposit'?'Deposito':'Tabungan')+'</b><small>'+$('<div>').text(r.product_name||'-').html()+' · buka '+(r.opened_at||'-')+(r.maturity_date?' · jatuh tempo '+r.maturity_date:'')+(Number(r.interest_rate)>0?' · bunga '+Number(r.interest_rate).toLocaleString('id-ID')+'%':'')+'</small>'),coa=$('<td>').html(r.kode_akun?'<b>'+$('<div>').text(r.kode_akun).html()+'</b><small>'+$('<div>').text(r.nama_akun||'').html()+'</small>':'<span class="text-warning">Belum terhubung</span>');cells.eq(0).after(product);cells.eq(1).after(coa);tr.data('bankRow',r);}); table.find('tbody tr').each(function(){var tr=$(this),button=tr.find('.edit-bank');if(!button.length){tr.children('td').attr('colspan',6);return;}var r=button.data('row'),cells=tr.children('td'),product=$('<td>').html('<b>'+(r.account_type==='deposit'?'Deposito':'Tabungan')+'</b><small>'+$('<div>').text(r.product_name||'-').html()+' · buka '+(r.opened_at||'-')+(r.maturity_date?' · jatuh tempo '+r.maturity_date:'')+(Number(r.interest_rate)>0?' · bunga '+Number(r.interest_rate).toLocaleString('id-ID')+'%':'')+'</small>'),coa=$('<td>').html(r.kode_akun?'<b>'+$('<div>').text(r.kode_akun).html()+'</b><small>'+$('<div>').text(r.nama_akun||'').html()+'</small>':'<span class="text-warning">Belum terhubung</span>');cells.eq(0).after(product);cells.eq(1).after(coa);tr.data('bankRow',r);});
var filters=$('<div class="row g-2 mb-3 bank-report-filter"><div class="col-md-3"><input class="form-control" id="bankFilterName" placeholder="Cari nama bank"></div><div class="col-md-2"><select class="form-select" id="bankFilterType"><option value="">Semua jenis</option><option value="savings">Tabungan</option><option value="deposit">Deposito</option></select></div><div class="col-md-2"><select class="form-select" id="bankFilterStatus"><option value="">Semua status</option><option value="1">Aktif</option><option value="0">Nonaktif</option></select></div><div class="col-md-2"><input type="date" class="form-control" id="bankFilterFrom" title="Jatuh tempo dari"></div><div class="col-md-2"><input type="date" class="form-control" id="bankFilterTo" title="Jatuh tempo sampai"></div><div class="col-md-1"><a class="btn btn-success w-100" id="bankExport" href="<?=base_url('companies/bank_export')?>" title="Export CSV"><i class="bi bi-file-earmark-spreadsheet"></i></a></div></div>');table.before(filters); var filters=$('<div class="row g-2 mb-3 bank-report-filter"><div class="col-md-3"><input class="form-control" id="bankFilterName" placeholder="Cari nama bank"></div><div class="col-md-2"><select class="form-select" id="bankFilterType"><option value="">Semua jenis</option><option value="savings">Tabungan</option><option value="deposit">Deposito</option></select></div><div class="col-md-2"><select class="form-select" id="bankFilterStatus"><option value="">Semua status</option><option value="1">Aktif</option><option value="0">Nonaktif</option></select></div><div class="col-md-2"><input type="date" class="form-control" id="bankFilterFrom" title="Jatuh tempo dari"></div><div class="col-md-2"><input type="date" class="form-control" id="bankFilterTo" title="Jatuh tempo sampai"></div><div class="col-md-1"><a class="btn btn-success w-100" id="bankExport" href="<?=base_url('companies/bank_export')?>" title="Export CSV"><i class="bi bi-file-earmark-spreadsheet"></i></a></div></div>');table.before(filters);
table.find('tbody tr').each(function(){var tr=$(this),r=tr.data('bankRow');if(!r)return;tr.children('td').eq(4).append(Number(r.show_on_invoice)===1?'<span class="badge text-bg-info ms-1">Tampil di Invoice</span>':'<span class="badge text-bg-light ms-1">Tidak di Invoice</span>');});
function filterBanks(){var name=$('#bankFilterName').val().toLowerCase(),type=$('#bankFilterType').val(),status=$('#bankFilterStatus').val(),from=$('#bankFilterFrom').val(),to=$('#bankFilterTo').val();table.find('tbody tr').each(function(){var tr=$(this),r=tr.data('bankRow');if(!r)return;var match=(!name||String(r.bank_name||'').toLowerCase().includes(name))&&(!type||r.account_type===type)&&(status===''||String(Number(r.is_active))===status)&&(!from||(r.maturity_date&&r.maturity_date>=from))&&(!to||(r.maturity_date&&r.maturity_date<=to));tr.toggle(match);});$('#bankExport').attr('href','<?=base_url('companies/bank_export')?>?'+$.param({bank:$('#bankFilterName').val(),type:type,status:status,maturity_from:from,maturity_to:to}));} function filterBanks(){var name=$('#bankFilterName').val().toLowerCase(),type=$('#bankFilterType').val(),status=$('#bankFilterStatus').val(),from=$('#bankFilterFrom').val(),to=$('#bankFilterTo').val();table.find('tbody tr').each(function(){var tr=$(this),r=tr.data('bankRow');if(!r)return;var match=(!name||String(r.bank_name||'').toLowerCase().includes(name))&&(!type||r.account_type===type)&&(status===''||String(Number(r.is_active))===status)&&(!from||(r.maturity_date&&r.maturity_date>=from))&&(!to||(r.maturity_date&&r.maturity_date<=to));tr.toggle(match);});$('#bankExport').attr('href','<?=base_url('companies/bank_export')?>?'+$.param({bank:$('#bankFilterName').val(),type:type,status:status,maturity_from:from,maturity_to:to}));}
$('.bank-report-filter').on('input change','input,select',filterBanks); $('.bank-report-filter').on('input change','input,select',filterBanks);
if(location.hash==='#companyBanks'){var trigger=document.querySelector('[data-bs-target="#companyBanks"]');if(trigger)bootstrap.Tab.getOrCreateInstance(trigger).show();} if(location.hash==='#companyBanks'){var trigger=document.querySelector('[data-bs-target="#companyBanks"]');if(trigger)bootstrap.Tab.getOrCreateInstance(trigger).show();}
@@ -1,16 +1,16 @@
<?php <?php
$reportNames=array('trial_balance'=>'Neraca Saldo','general_ledger'=>'Buku Besar','profit_loss'=>'Laporan Laba Rugi','balance_sheet'=>'Laporan Neraca','cash_flow'=>'Laporan Arus Kas','changes_equity'=>'Perubahan Ekuitas','journal_register'=>'Register Jurnal','account_detail'=>'Detail Transaksi Akun','actual_budget'=>'Aktual vs Budget','sales_summary'=>'Rekap Penjualan','sales_by_customer'=>'Penjualan per Customer','sales_by_item'=>'Penjualan per Barang','sales_by_barcode'=>'Penjualan per Barcode','running_invoices'=>'Invoice Berjalan Belum Final','sales_returns'=>'Retur Penjualan','revenue_recognition'=>'Pendapatan Diakui vs Ditangguhkan','unbilled_deliveries'=>'Surat Jalan Belum Ditagihkan','receivable_reconciliation'=>'Rekonsiliasi Piutang','purchase_returns'=>'Laporan Retur Pembelian','supplier_refunds'=>'Laporan Refund Supplier','outstanding_supplier_refunds'=>'Outstanding Refund Supplier','supplier_debit_notes'=>'Laporan Debit Note Supplier'); $reportNames=array('trial_balance'=>'Neraca Saldo','general_ledger'=>'Buku Besar','profit_loss'=>'Laporan Laba Rugi','balance_sheet'=>'Laporan Neraca','cash_flow'=>'Laporan Arus Kas','bank_account_balances'=>'Rekening Bank & Saldo','changes_equity'=>'Perubahan Ekuitas','journal_register'=>'Register Jurnal','account_detail'=>'Detail Transaksi Akun','actual_budget'=>'Aktual vs Budget','sales_summary'=>'Rekap Penjualan','sales_by_customer'=>'Penjualan per Customer','sales_by_item'=>'Penjualan per Barang','sales_by_barcode'=>'Penjualan per Barcode','running_invoices'=>'Invoice Berjalan Belum Final','sales_returns'=>'Retur Penjualan','revenue_recognition'=>'Pendapatan Diakui vs Ditangguhkan','unbilled_deliveries'=>'Surat Jalan Belum Ditagihkan','receivable_reconciliation'=>'Rekonsiliasi Piutang','purchase_returns'=>'Laporan Retur Pembelian','supplier_refunds'=>'Laporan Refund Supplier','outstanding_supplier_refunds'=>'Outstanding Refund Supplier','supplier_debit_notes'=>'Laporan Debit Note Supplier');
$money=function($value){return number_format((float)$value,2,',','.');}; $money=function($value){return number_format((float)$value,2,',','.');};
$accountCode=function($value){return preg_replace('/(?:[\.,]0+)$/','',(string)$value);}; $accountCode=function($value){return preg_replace('/(?:[\.,]0+)$/','',(string)$value);};
?> ?>
<div class="container-fluid py-3 professional-report-page"> <div class="container-fluid py-3 professional-report-page">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2"> <div class="d-flex flex-wrap justify-content-between align-items-start gap-2">
<div><h4><?=html_escape(isset($reportNames[$report])?$reportNames[$report]:'Laporan Profesional')?></h4><small><?=html_escape($company->name)?> · <?=$report==='balance_sheet'?'posisi per '.$filters['to']:'periode '.$filters['from'].' s.d. '.$filters['to']?></small></div> <div><h4><?=html_escape(isset($reportNames[$report])?$reportNames[$report]:'Laporan Profesional')?></h4><small><?=html_escape($company->name)?> · <?=in_array($report,array('balance_sheet','bank_account_balances'),true)?'posisi per '.$filters['to']:'periode '.$filters['from'].' s.d. '.$filters['to']?></small></div>
<div><a class="btn btn-success" href="<?=base_url('professionalreports/export/'.$report.'?'.http_build_query($filters))?>">Export Excel/CSV</a> <button class="btn btn-outline-dark" onclick="print()">PDF/Print</button></div> <div><a class="btn btn-success" href="<?=base_url('professionalreports/export/'.$report.'?'.http_build_query($filters))?>">Export Excel/CSV</a> <button class="btn btn-outline-dark" onclick="print()">PDF/Print</button></div>
</div> </div>
<form class="card card-body my-3"><div class="row g-2"> <form class="card card-body my-3"><div class="row g-2">
<div class="col-md-3"><label class="form-label">Tipe Laporan</label><select class="form-select" name="report"><?php foreach($reportNames as$k=>$v):?><option value="<?=$k?>" <?=$report===$k?'selected':''?>><?=$v?></option><?php endforeach?></select></div> <div class="col-md-3"><label class="form-label">Tipe Laporan</label><select class="form-select" name="report"><?php foreach($reportNames as$k=>$v):?><option value="<?=$k?>" <?=$report===$k?'selected':''?>><?=$v?></option><?php endforeach?></select></div>
<div class="col-md-2"><label class="form-label">Dari tanggal</label><input class="form-control" type="date" name="from" value="<?=$filters['from']?>"></div> <?php if(!in_array($report,array('balance_sheet','bank_account_balances'),true)):?><div class="col-md-2"><label class="form-label">Dari tanggal</label><input class="form-control" type="date" name="from" value="<?=$filters['from']?>"></div><?php else:?><input type="hidden" name="from" value="1900-01-01"><?php endif?>
<div class="col-md-2"><label class="form-label">Sampai tanggal</label><input class="form-control" type="date" name="to" value="<?=$filters['to']?>"></div> <div class="col-md-2"><label class="form-label">Sampai tanggal</label><input class="form-control" type="date" name="to" value="<?=$filters['to']?>"></div>
<div class="col-md-3"><label class="form-label">Akun</label><select class="form-select" name="account_id"><option value="">Semua akun</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>" <?=$filters['account_id']==$a->id?'selected':''?>><?=html_escape($a->kode_akun.' '.$a->nama_akun)?></option><?php endforeach?></select></div> <div class="col-md-3"><label class="form-label">Akun</label><select class="form-select" name="account_id"><option value="">Semua akun</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>" <?=$filters['account_id']==$a->id?'selected':''?>><?=html_escape($a->kode_akun.' '.$a->nama_akun)?></option><?php endforeach?></select></div>
<div class="col-md-2 d-flex align-items-end"><button class="btn btn-primary w-100">Tampilkan</button></div> <div class="col-md-2 d-flex align-items-end"><button class="btn btn-primary w-100">Tampilkan</button></div>
@@ -40,6 +40,15 @@ $accountCode=function($value){return preg_replace('/(?:[\.,]0+)$/','',(string)$v
<?php endforeach?> <?php endforeach?>
</div> </div>
<div class="card"><div class="card-header"><b>Detail Transaksi Pembentuk Arus Kas</b><small class="d-block text-muted">Saldo berjalan mencakup seluruh akun kas/bank yang dipilih.</small></div><div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Tanggal</th><th>Referensi</th><th>Keterangan</th><th>Akun</th><th>Aktivitas</th><th class="text-end">Kas Masuk</th><th class="text-end">Kas Keluar</th><th class="text-end">Saldo</th></tr></thead><tbody><?php foreach($rows as$r):?><tr><td><?=html_escape($r['tanggal'])?></td><td><a href="<?=base_url('jurnal?focus='.$r['journal_id'])?>"><?=html_escape($r['no_ref']?:'#'.$r['journal_id'])?></a></td><td><?=html_escape($r['keterangan'])?></td><td><?=html_escape($r['akun'])?></td><td><?=html_escape($r['aktivitas'])?></td><td class="text-end text-success"><?=$money($r['kas_masuk'])?></td><td class="text-end text-danger"><?=$money($r['kas_keluar'])?></td><td class="text-end fw-semibold"><?=$money($r['saldo_berjalan'])?></td></tr><?php endforeach?><?php if(!$rows):?><tr><td colspan="8" class="text-center text-muted py-4">Tidak ada transaksi kas pada periode ini.</td></tr><?php endif?></tbody><tfoot><tr><th colspan="5">Total Mutasi</th><th class="text-end"><?=$money($report_summary['cash_in']??0)?></th><th class="text-end"><?=$money($report_summary['cash_out']??0)?></th><th class="text-end"><?=$money($report_summary['closing_balance']??0)?></th></tr></tfoot></table></div></div> <div class="card"><div class="card-header"><b>Detail Transaksi Pembentuk Arus Kas</b><small class="d-block text-muted">Saldo berjalan mencakup seluruh akun kas/bank yang dipilih.</small></div><div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Tanggal</th><th>Referensi</th><th>Keterangan</th><th>Akun</th><th>Aktivitas</th><th class="text-end">Kas Masuk</th><th class="text-end">Kas Keluar</th><th class="text-end">Saldo</th></tr></thead><tbody><?php foreach($rows as$r):?><tr><td><?=html_escape($r['tanggal'])?></td><td><a href="<?=base_url('jurnal?focus='.$r['journal_id'])?>"><?=html_escape($r['no_ref']?:'#'.$r['journal_id'])?></a></td><td><?=html_escape($r['keterangan'])?></td><td><?=html_escape($r['akun'])?></td><td><?=html_escape($r['aktivitas'])?></td><td class="text-end text-success"><?=$money($r['kas_masuk'])?></td><td class="text-end text-danger"><?=$money($r['kas_keluar'])?></td><td class="text-end fw-semibold"><?=$money($r['saldo_berjalan'])?></td></tr><?php endforeach?><?php if(!$rows):?><tr><td colspan="8" class="text-center text-muted py-4">Tidak ada transaksi kas pada periode ini.</td></tr><?php endif?></tbody><tfoot><tr><th colspan="5">Total Mutasi</th><th class="text-end"><?=$money($report_summary['cash_in']??0)?></th><th class="text-end"><?=$money($report_summary['cash_out']??0)?></th><th class="text-end"><?=$money($report_summary['closing_balance']??0)?></th></tr></tfoot></table></div></div>
<?php elseif($report==='bank_account_balances'):
$mapped=0;$unmapped=0;$uniqueBalances=array();foreach($rows as$r){if(empty($r['gl_account_id']))$unmapped++;else{$mapped++;$uniqueBalances[(int)$r['gl_account_id']]=(float)$r['closing_balance'];}}$totalBankBalance=array_sum($uniqueBalances);
?>
<div class="row g-2 mb-3">
<div class="col-md-4"><div class="card h-100"><div class="card-body"><small class="text-muted">Seluruh Rekening</small><strong class="d-block mt-1"><?=count($rows)?></strong><span class="small text-muted">Aktif maupun nonaktif</span></div></div></div>
<div class="col-md-4"><div class="card h-100"><div class="card-body"><small class="text-muted">Terhubung ke COA</small><strong class="d-block mt-1 text-success"><?=$mapped?></strong><span class="small text-muted"><?=$unmapped?> rekening belum terhubung</span></div></div></div>
<div class="col-md-4"><div class="card h-100"><div class="card-body"><small class="text-muted">Total Saldo COA Unik</small><strong class="d-block mt-1"><?=html_escape($company->base_currency?:'IDR')?> <?=$money($totalBankBalance)?></strong><span class="small text-muted">Tidak menggandakan COA yang dipakai bersama</span></div></div></div>
</div>
<div class="card"><div class="card-header"><b>Daftar Rekening dan Saldo Buku Besar</b><small class="d-block text-muted">Saldo berasal dari jurnal posted/reversed sampai <?=html_escape($filters['to'])?> dalam mata uang dasar perusahaan. Seluruh rekening tetap ditampilkan; pilihan PDF Invoice hanya bersifat informasi.</small></div><div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Bank / Rekening</th><th>Produk</th><th>Mata Uang</th><th>Akun COA</th><th>Status</th><th>PDF Invoice</th><th class="text-end">Saldo Akhir COA</th></tr></thead><tbody><?php foreach($rows as$r):?><tr><td><b><?=html_escape($r['bank_name'])?></b><small class="d-block text-muted"><?=html_escape($r['account_number'].' · a.n. '.$r['account_holder'])?></small></td><td><?=html_escape($r['account_type_label'])?><small class="d-block text-muted"><?=html_escape($r['product_name']?:'-')?></small></td><td><?=html_escape($r['currency'])?></td><td><?php if(!empty($r['gl_account_id'])):?><b><?=html_escape($accountCode($r['kode_akun']))?></b><small class="d-block text-muted"><?=html_escape($r['nama_akun'])?></small><?php if((int)$r['linked_account_count']>1):?><span class="badge bg-warning text-dark mt-1"><?=html_escape($r['mapping_status'])?></span><?php endif?><?php else:?><span class="text-warning">Belum terhubung</span><?php endif?></td><td><span class="badge <?=$r['is_active']?'bg-success':'bg-secondary'?>"><?=html_escape($r['account_status'])?></span><?=$r['is_primary']?'<span class="badge bg-primary ms-1">Utama</span>':''?></td><td><span class="badge <?=$r['show_on_invoice']?'bg-info text-dark':'bg-light text-dark'?>"><?=html_escape($r['invoice_status'])?></span></td><td class="text-end fw-bold"><?=html_escape($company->base_currency?:'IDR')?> <?=$money($r['closing_balance'])?></td></tr><?php endforeach?><?php if(!$rows):?><tr><td colspan="7" class="text-center text-muted py-4">Belum ada rekening perusahaan.</td></tr><?php endif?></tbody></table></div></div>
<?php elseif($report==='general_ledger'): ?> <?php elseif($report==='general_ledger'): ?>
<div class="card mb-3"><div class="card-header"><b>Ringkasan Saldo per Akun</b><small class="d-block text-muted">Saldo awal berasal dari seluruh jurnal sebelum periode, bukan dari pagination tampilan.</small></div><div class="table-responsive"><table class="table table-sm mb-0"><thead><tr><th>Akun</th><th class="text-end">Saldo Awal</th><th class="text-end">Total Debit</th><th class="text-end">Total Kredit</th><th class="text-end">Saldo Akhir</th></tr></thead><tbody><?php foreach($report_summary as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' - '.$r['nama_akun'])?></td><td class="text-end"><?=$money($r['saldo_awal'])?></td><td class="text-end"><?=$money($r['total_debit'])?></td><td class="text-end"><?=$money($r['total_kredit'])?></td><td class="text-end fw-bold"><?=$money($r['saldo_akhir'])?></td></tr><?php endforeach?><?php if(!$report_summary):?><tr><td colspan="5" class="text-center text-muted py-4">Tidak ada saldo atau mutasi akun.</td></tr><?php endif?></tbody></table></div></div> <div class="card mb-3"><div class="card-header"><b>Ringkasan Saldo per Akun</b><small class="d-block text-muted">Saldo awal berasal dari seluruh jurnal sebelum periode, bukan dari pagination tampilan.</small></div><div class="table-responsive"><table class="table table-sm mb-0"><thead><tr><th>Akun</th><th class="text-end">Saldo Awal</th><th class="text-end">Total Debit</th><th class="text-end">Total Kredit</th><th class="text-end">Saldo Akhir</th></tr></thead><tbody><?php foreach($report_summary as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' - '.$r['nama_akun'])?></td><td class="text-end"><?=$money($r['saldo_awal'])?></td><td class="text-end"><?=$money($r['total_debit'])?></td><td class="text-end"><?=$money($r['total_kredit'])?></td><td class="text-end fw-bold"><?=$money($r['saldo_akhir'])?></td></tr><?php endforeach?><?php if(!$report_summary):?><tr><td colspan="5" class="text-center text-muted py-4">Tidak ada saldo atau mutasi akun.</td></tr><?php endif?></tbody></table></div></div>
<div class="card"><div class="card-header"><b>Rincian Buku Besar</b></div><div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Akun</th><th>Tanggal</th><th>Referensi</th><th>Keterangan</th><th class="text-end">Debit</th><th class="text-end">Kredit</th><th class="text-end">Saldo Berjalan</th></tr></thead><tbody><?php foreach($rows as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' - '.$r['nama_akun'])?></td><td><?=html_escape($r['tanggal'])?></td><td><a href="<?=base_url('jurnal?focus='.$r['journal_id'])?>"><?=html_escape($r['no_ref'])?></a></td><td><?=html_escape($r['keterangan'])?></td><td class="text-end"><?=$money($r['debit'])?></td><td class="text-end"><?=$money($r['kredit'])?></td><td class="text-end fw-semibold"><?=$money($r['saldo_berjalan'])?></td></tr><?php endforeach?><?php if(!$rows):?><tr><td colspan="7" class="text-center text-muted py-4">Tidak ada transaksi pada periode ini.</td></tr><?php endif?></tbody></table></div></div> <div class="card"><div class="card-header"><b>Rincian Buku Besar</b></div><div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Akun</th><th>Tanggal</th><th>Referensi</th><th>Keterangan</th><th class="text-end">Debit</th><th class="text-end">Kredit</th><th class="text-end">Saldo Berjalan</th></tr></thead><tbody><?php foreach($rows as$r):?><tr><td><?=html_escape($accountCode($r['kode_akun']).' - '.$r['nama_akun'])?></td><td><?=html_escape($r['tanggal'])?></td><td><a href="<?=base_url('jurnal?focus='.$r['journal_id'])?>"><?=html_escape($r['no_ref'])?></a></td><td><?=html_escape($r['keterangan'])?></td><td class="text-end"><?=$money($r['debit'])?></td><td class="text-end"><?=$money($r['kredit'])?></td><td class="text-end fw-semibold"><?=$money($r['saldo_berjalan'])?></td></tr><?php endforeach?><?php if(!$rows):?><tr><td colspan="7" class="text-center text-muted py-4">Tidak ada transaksi pada periode ini.</td></tr><?php endif?></tbody></table></div></div>