352 lines
14 KiB
PHP
352 lines
14 KiB
PHP
<?php
|
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
|
|
|
class Dashboard extends MY_Controller
|
|
{
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
$this->load->library('PerformanceCacheService');
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
$companyId = (int) $this->companycontext->id();
|
|
$year = (int) date('Y');
|
|
$summary = $this->performancecacheservice->remember(
|
|
'dashboard:summary:' . $companyId . ':' . $year,
|
|
300,
|
|
function () use ($companyId, $year) {
|
|
return $this->financialSummary($companyId, $year);
|
|
}
|
|
);
|
|
$previous = $this->financialSummary($companyId, $year - 1);
|
|
$overview = $this->operationalOverview($companyId);
|
|
|
|
$data = array(
|
|
'active_menu' => 'dashboard',
|
|
'summary' => $summary,
|
|
'comparison' => array(
|
|
'revenue' => $this->percentageChange($summary['total_revenue'], $previous['total_revenue']),
|
|
'expense' => $this->percentageChange($summary['total_expense'], $previous['total_expense']),
|
|
'profit' => $this->percentageChange($summary['net_profit'], $previous['net_profit']),
|
|
),
|
|
'overview' => $overview,
|
|
'ai_insight' => $this->insights($companyId),
|
|
'dashboard_year' => $year,
|
|
'quick_links' => $this->quickLinks(),
|
|
'show_financial' => is_master_admin_user()
|
|
|| check_permission('reports', 'can_view')
|
|
|| check_permission('jurnal', 'can_view'),
|
|
);
|
|
|
|
$this->load->view('partials/header', $data);
|
|
$this->load->view('dashboard/layout', $data);
|
|
$this->load->view('partials/footer');
|
|
}
|
|
|
|
public function get_activity()
|
|
{
|
|
$this->activityQuery();
|
|
$rows = $this->db->order_by('activity_logs.created_at', 'DESC')->limit(10)->get()->result();
|
|
return $this->json($rows);
|
|
}
|
|
|
|
public function log_activity()
|
|
{
|
|
$data = array(
|
|
'active_menu' => 'dashboard',
|
|
'use_datatables' => true,
|
|
);
|
|
$this->load->view('partials/header', $data);
|
|
$this->load->view('dashboard/activity_logs', $data);
|
|
$this->load->view('partials/footer');
|
|
}
|
|
|
|
public function get_activity_datatable()
|
|
{
|
|
$draw = (int) $this->input->post('draw');
|
|
$start = max(0, (int) $this->input->post('start'));
|
|
$length = (int) $this->input->post('length');
|
|
$searchInput = $this->input->post('search');
|
|
$search = is_array($searchInput) ? trim((string) ($searchInput['value'] ?? '')) : '';
|
|
|
|
$this->activityQuery($search);
|
|
$recordsFiltered = (int) $this->db->count_all_results();
|
|
|
|
$this->activityQuery($search);
|
|
$this->db->order_by('activity_logs.id', 'DESC');
|
|
if ($length !== -1) {
|
|
$this->db->limit(max(1, $length), $start);
|
|
}
|
|
$rows = $this->db->get()->result();
|
|
|
|
$this->activityQuery();
|
|
$recordsTotal = (int) $this->db->count_all_results();
|
|
$result = array();
|
|
foreach ($rows as $row) {
|
|
$result[] = array(
|
|
'created_at' => $row->created_at,
|
|
'nama' => $row->nama ?: 'System',
|
|
'module' => $row->module,
|
|
'action' => $row->action,
|
|
'description' => $row->description,
|
|
'status' => $row->status,
|
|
'ip_address' => $row->ip_address,
|
|
);
|
|
}
|
|
|
|
return $this->json(array(
|
|
'draw' => $draw,
|
|
'recordsTotal' => $recordsTotal,
|
|
'recordsFiltered' => $recordsFiltered,
|
|
'data' => $result,
|
|
));
|
|
}
|
|
|
|
public function get_chart()
|
|
{
|
|
$year = (int) $this->input->get('year');
|
|
if ($year < 2000 || $year > 2200) {
|
|
$year = (int) date('Y');
|
|
}
|
|
|
|
$companyId = (int) $this->companycontext->id();
|
|
$rows = $this->db->query(
|
|
"SELECT MONTH(j.tanggal) month_no,
|
|
COALESCE(SUM(CASE WHEN a.tipe='revenue' THEN d.kredit-d.debit ELSE 0 END),0) revenue,
|
|
COALESCE(SUM(CASE WHEN a.tipe='expense' THEN d.debit-d.kredit ELSE 0 END),0) expense
|
|
FROM journal_details d
|
|
JOIN journals j ON j.id=d.journal_id
|
|
JOIN accounts a ON a.id=d.account_id
|
|
WHERE j.company_id=? AND j.status IN('posted','reversed')
|
|
AND a.kategori='laba_rugi' AND a.is_active=1 AND YEAR(j.tanggal)=?
|
|
GROUP BY MONTH(j.tanggal)
|
|
ORDER BY month_no",
|
|
array($companyId, $year)
|
|
)->result();
|
|
|
|
$months = array('Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des');
|
|
$revenue = array_fill(0, 12, 0);
|
|
$expense = array_fill(0, 12, 0);
|
|
foreach ($rows as $row) {
|
|
$index = (int) $row->month_no - 1;
|
|
if ($index >= 0 && $index < 12) {
|
|
$revenue[$index] = (float) $row->revenue;
|
|
$expense[$index] = (float) $row->expense;
|
|
}
|
|
}
|
|
|
|
return $this->json(array(
|
|
'year' => $year,
|
|
'labels' => $months,
|
|
'revenue' => $revenue,
|
|
'expense' => $expense,
|
|
));
|
|
}
|
|
|
|
public function get_years()
|
|
{
|
|
$this->db->select('DISTINCT YEAR(tanggal) year', false)
|
|
->from('journals')
|
|
->where('company_id', (int) $this->companycontext->id())
|
|
->where_in('status', array('posted', 'reversed'))
|
|
->order_by('year', 'DESC');
|
|
$years = $this->db->get()->result();
|
|
if (!$years) {
|
|
$years = array((object) array('year' => (int) date('Y')));
|
|
}
|
|
return $this->json($years);
|
|
}
|
|
|
|
private function financialSummary($companyId, $year)
|
|
{
|
|
$row = $this->db->query(
|
|
"SELECT
|
|
COALESCE(SUM(CASE WHEN a.tipe='revenue' THEN d.kredit-d.debit ELSE 0 END),0) total_revenue,
|
|
COALESCE(SUM(CASE WHEN a.tipe='expense' THEN d.debit-d.kredit ELSE 0 END),0) total_expense
|
|
FROM journal_details d
|
|
JOIN journals j ON j.id=d.journal_id
|
|
JOIN accounts a ON a.id=d.account_id
|
|
WHERE j.company_id=? AND j.status IN('posted','reversed')
|
|
AND a.kategori='laba_rugi' AND a.is_active=1 AND YEAR(j.tanggal)=?",
|
|
array((int) $companyId, (int) $year)
|
|
)->row_array();
|
|
|
|
$revenue = (float) ($row['total_revenue'] ?? 0);
|
|
$expense = (float) ($row['total_expense'] ?? 0);
|
|
return array(
|
|
'total_revenue' => $revenue,
|
|
'total_expense' => $expense,
|
|
'net_profit' => $revenue - $expense,
|
|
);
|
|
}
|
|
|
|
private function operationalOverview($companyId)
|
|
{
|
|
$result = array(
|
|
'receivable_amount' => 0,
|
|
'receivable_count' => 0,
|
|
'receivable_overdue' => 0,
|
|
'payable_amount' => 0,
|
|
'payable_count' => 0,
|
|
'payable_overdue' => 0,
|
|
'inventory_value' => 0,
|
|
'active_items' => 0,
|
|
'pending_items' => 0,
|
|
'pending_approvals' => 0,
|
|
);
|
|
|
|
if ($this->db->table_exists('invoices')) {
|
|
$whereDeleted = $this->db->field_exists('deleted_at', 'invoices') ? ' AND deleted_at IS NULL' : '';
|
|
$row = $this->db->query(
|
|
"SELECT COUNT(*) total,
|
|
COALESCE(SUM(sisa_piutang),0) amount,
|
|
SUM(CASE WHEN jatuh_tempo<CURDATE() THEN 1 ELSE 0 END) overdue
|
|
FROM invoices
|
|
WHERE company_id=? AND workflow_status='posted' AND sisa_piutang>0.001" . $whereDeleted,
|
|
array($companyId)
|
|
)->row();
|
|
if ($row) {
|
|
$result['receivable_count'] = (int) $row->total;
|
|
$result['receivable_amount'] = (float) $row->amount;
|
|
$result['receivable_overdue'] = (int) $row->overdue;
|
|
}
|
|
}
|
|
|
|
if ($this->db->table_exists('supplier_invoices')) {
|
|
$companyFilter = $this->db->field_exists('company_id', 'supplier_invoices')
|
|
? ' AND company_id=?'
|
|
: '';
|
|
$bindings = $companyFilter === '' ? array() : array($companyId);
|
|
$row = $this->db->query(
|
|
"SELECT COUNT(*) total,
|
|
COALESCE(SUM(balance),0) amount,
|
|
SUM(CASE WHEN due_date<=CURDATE() THEN 1 ELSE 0 END) overdue
|
|
FROM supplier_invoices
|
|
WHERE status='partial' AND balance>0.001" . $companyFilter,
|
|
$bindings
|
|
)->row();
|
|
if ($row) {
|
|
$result['payable_count'] = (int) $row->total;
|
|
$result['payable_amount'] = (float) $row->amount;
|
|
$result['payable_overdue'] = (int) $row->overdue;
|
|
}
|
|
}
|
|
|
|
if ($this->db->table_exists('items')) {
|
|
$result['active_items'] = (int) $this->db
|
|
->where(array('company_id' => $companyId, 'status' => 'active'))
|
|
->count_all_results('items');
|
|
|
|
if ($this->db->table_exists('item_barcodes')) {
|
|
$pending = $this->db->query(
|
|
"SELECT COUNT(DISTINCT i.id) total
|
|
FROM items i
|
|
LEFT JOIN item_barcodes b ON b.item_id=i.id AND b.status='pending'
|
|
WHERE i.company_id=? AND (i.status='draft' OR b.id IS NOT NULL)",
|
|
array($companyId)
|
|
)->row();
|
|
$result['pending_items'] = (int) ($pending ? $pending->total : 0);
|
|
} else {
|
|
$result['pending_items'] = (int) $this->db
|
|
->where(array('company_id' => $companyId, 'status' => 'draft'))
|
|
->count_all_results('items');
|
|
}
|
|
}
|
|
|
|
if ($this->db->table_exists('inventory_ledger') && $this->db->field_exists('value', 'inventory_ledger')) {
|
|
$inventory = $this->db->query(
|
|
"SELECT COALESCE(SUM(CASE WHEN l.direction='in' THEN l.value ELSE -l.value END),0) amount
|
|
FROM inventory_ledger l JOIN items i ON i.id=l.item_id WHERE i.company_id=?",
|
|
array($companyId)
|
|
)->row();
|
|
$result['inventory_value'] = (float) ($inventory ? $inventory->amount : 0);
|
|
}
|
|
|
|
if ($this->db->table_exists('approval_requests')
|
|
&& (is_master_admin_user() || check_permission('approvals', 'can_view'))) {
|
|
$result['pending_approvals'] = (int) $this->db
|
|
->where('status', 'pending')
|
|
->count_all_results('approval_requests');
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function insights($companyId)
|
|
{
|
|
if (!$this->db->table_exists('ai_insights')) {
|
|
return array();
|
|
}
|
|
$this->db->select('type,severity,message,meta,created_at')
|
|
->where('period', date('Y-m'));
|
|
if ($this->db->field_exists('company_id', 'ai_insights')) {
|
|
$this->db->where('company_id', $companyId);
|
|
}
|
|
return $this->db
|
|
->order_by("CASE severity WHEN 'critical' THEN 1 WHEN 'warning' THEN 2 ELSE 3 END", '', false)
|
|
->order_by('created_at', 'DESC')
|
|
->limit(8)
|
|
->get('ai_insights')
|
|
->result();
|
|
}
|
|
|
|
private function quickLinks()
|
|
{
|
|
$definitions = array(
|
|
array('feature' => 'invoices', 'action' => 'can_create', 'label' => 'Buat Invoice', 'description' => 'Catat tagihan penjualan', 'icon' => 'bi-receipt-cutoff', 'url' => 'invoices/create'),
|
|
array('feature' => 'purchases', 'action' => 'can_create', 'label' => 'Purchase Workflow', 'description' => 'Buat permintaan pembelian', 'icon' => 'bi-cart-plus', 'url' => 'purchases'),
|
|
array('feature' => 'jurnal', 'action' => 'can_create', 'label' => 'Jurnal Umum', 'description' => 'Catat jurnal manual', 'icon' => 'bi-journal-plus', 'url' => 'jurnal'),
|
|
array('feature' => 'items', 'action' => 'can_view', 'label' => 'Scan Persediaan', 'description' => 'Periksa barcode dan stok', 'icon' => 'bi-qr-code-scan', 'url' => 'inventoryprofessional#scanner'),
|
|
);
|
|
|
|
$links = array();
|
|
foreach ($definitions as $definition) {
|
|
if (is_master_admin_user() || check_permission($definition['feature'], $definition['action'])) {
|
|
$links[] = $definition;
|
|
}
|
|
}
|
|
return $links;
|
|
}
|
|
|
|
private function activityQuery($search = '')
|
|
{
|
|
$this->db->select('activity_logs.*,users.nama')
|
|
->from('activity_logs')
|
|
->join('users', 'users.id=activity_logs.user_id', 'left');
|
|
|
|
if ($this->db->field_exists('company_id', 'activity_logs')) {
|
|
$this->db->where('activity_logs.company_id', (int) $this->companycontext->id());
|
|
} elseif (!is_master_admin_user()) {
|
|
$this->db->where('activity_logs.user_id', (int) $this->session->userdata('user_id'));
|
|
}
|
|
|
|
if ($search !== '') {
|
|
$this->db->group_start()
|
|
->like('activity_logs.module', $search)
|
|
->or_like('activity_logs.action', $search)
|
|
->or_like('activity_logs.description', $search)
|
|
->or_like('activity_logs.status', $search)
|
|
->or_like('users.nama', $search)
|
|
->group_end();
|
|
}
|
|
}
|
|
|
|
private function percentageChange($current, $previous)
|
|
{
|
|
$previous = (float) $previous;
|
|
if (abs($previous) < 0.01) {
|
|
return null;
|
|
}
|
|
return round((((float) $current - $previous) / abs($previous)) * 100, 1);
|
|
}
|
|
|
|
private function json($payload)
|
|
{
|
|
return $this->output
|
|
->set_content_type('application/json', 'utf-8')
|
|
->set_output(json_encode($payload, JSON_UNESCAPED_UNICODE));
|
|
}
|
|
}
|