Files
2026-09-11 23:35:24 +07:00

265 lines
17 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
$hour = (int) date('G');
$greeting = $hour < 11 ? 'Selamat pagi' : ($hour < 15 ? 'Selamat siang' : ($hour < 19 ? 'Selamat sore' : 'Selamat malam'));
$money = static function ($value) {
return 'Rp ' . number_format((float) $value, 0, ',', '.');
};
$trend = static function ($value, $inverse = false) {
if ($value === null) return '<span class="metric-comparison is-neutral">Belum ada pembanding</span>';
$positive = (float) $value >= 0;
$good = $inverse ? !$positive : $positive;
return '<span class="metric-comparison ' . ($good ? 'is-good' : 'is-bad') . '"><i class="bi bi-arrow-'
. ($positive ? 'up' : 'down') . '"></i> ' . number_format(abs((float) $value), 1, ',', '.')
. '% dari tahun lalu</span>';
};
?>
<link rel="stylesheet" href="<?= base_url('assets/css/dashboard-professional.css') . '?v=' . filemtime(FCPATH . 'assets/css/dashboard-professional.css') ?>">
<section class="dashboard-shell">
<header class="dashboard-intro">
<div>
<span class="dashboard-eyebrow">Ringkasan perusahaan</span>
<h2><?= html_escape($greeting . ', ' . ($this->session->userdata('nama') ?: 'Pengguna')) ?></h2>
<p>Pantau kondisi keuangan dan pekerjaan yang membutuhkan perhatian dalam satu tampilan.</p>
</div>
<div class="dashboard-period">
<i class="bi bi-calendar3"></i>
<span><small>Periode aktif</small><strong><?= html_escape(date('d M Y')) ?></strong></span>
</div>
</header>
<?php if ($show_financial): ?>
<div class="dashboard-metrics">
<article class="metric-card metric-revenue">
<div class="metric-top"><span class="metric-icon"><i class="bi bi-arrow-up-right"></i></span><small>Tahun <?= (int) $dashboard_year ?></small></div>
<span class="metric-label">Pendapatan</span>
<strong><?= $money($summary['total_revenue']) ?></strong>
<?= $trend($comparison['revenue']) ?>
</article>
<article class="metric-card metric-expense">
<div class="metric-top"><span class="metric-icon"><i class="bi bi-arrow-down-right"></i></span><small>Tahun <?= (int) $dashboard_year ?></small></div>
<span class="metric-label">Pengeluaran</span>
<strong><?= $money($summary['total_expense']) ?></strong>
<?= $trend($comparison['expense'], true) ?>
</article>
<article class="metric-card metric-profit">
<div class="metric-top"><span class="metric-icon"><i class="bi bi-graph-up-arrow"></i></span><small>Tahun <?= (int) $dashboard_year ?></small></div>
<span class="metric-label">Laba Bersih</span>
<strong class="<?= (float) $summary['net_profit'] < 0 ? 'text-danger' : '' ?>"><?= $money($summary['net_profit']) ?></strong>
<?= $trend($comparison['profit']) ?>
</article>
<article class="metric-card metric-receivable">
<div class="metric-top"><span class="metric-icon"><i class="bi bi-wallet2"></i></span><small><?= (int) $overview['receivable_count'] ?> invoice</small></div>
<span class="metric-label">Piutang Belum Lunas</span>
<strong><?= $money($overview['receivable_amount']) ?></strong>
<a href="<?= base_url('receivables') ?>">Buka daftar piutang <i class="bi bi-arrow-right"></i></a>
</article>
</div>
<?php endif; ?>
<div class="operational-grid">
<a class="operational-card" href="<?= base_url('payables') ?>">
<span class="operational-icon orange"><i class="bi bi-credit-card"></i></span>
<span><small>Hutang supplier</small><strong><?= $money($overview['payable_amount']) ?></strong><em><?= (int) $overview['payable_count'] ?> tagihan terbuka</em></span>
<?php if ($overview['payable_overdue']): ?><b class="attention-badge"><?= (int) $overview['payable_overdue'] ?> jatuh tempo</b><?php endif; ?>
</a>
<a class="operational-card" href="<?= base_url('items') ?>">
<span class="operational-icon blue"><i class="bi bi-box-seam"></i></span>
<span><small>Persediaan aktif</small><strong><?= number_format((int) $overview['active_items'], 0, ',', '.') ?> barang</strong><em><?= (int) $overview['pending_items'] ?> perlu diaktifkan</em></span>
<?php if ($overview['pending_items']): ?><b class="attention-badge"><?= (int) $overview['pending_items'] ?> pending</b><?php endif; ?>
</a>
<a class="operational-card" href="<?= base_url('inventoryprofessional') ?>">
<span class="operational-icon green"><i class="bi bi-boxes"></i></span>
<span><small>Nilai persediaan</small><strong><?= $money($overview['inventory_value']) ?></strong><em>Saldo inventory ledger</em></span>
</a>
<a class="operational-card" href="<?= base_url('approvals') ?>">
<span class="operational-icon purple"><i class="bi bi-clipboard-check"></i></span>
<span><small>Menunggu persetujuan</small><strong><?= number_format((int) $overview['pending_approvals'], 0, ',', '.') ?> dokumen</strong><em>Perlu diperiksa</em></span>
<?php if ($overview['pending_approvals']): ?><b class="attention-badge"><?= (int) $overview['pending_approvals'] ?> baru</b><?php endif; ?>
</a>
</div>
<?php if ($quick_links): ?>
<section class="dashboard-section quick-section">
<div class="section-heading">
<div><h3>Aksi Cepat</h3><p>Jalur singkat menuju pekerjaan yang paling sering digunakan.</p></div>
</div>
<div class="dashboard-quick-grid">
<?php foreach ($quick_links as $link): ?>
<a href="<?= base_url($link['url']) ?>">
<i class="bi <?= html_escape($link['icon']) ?>"></i>
<span><strong><?= html_escape($link['label']) ?></strong><small><?= html_escape($link['description']) ?></small></span>
<i class="bi bi-chevron-right quick-arrow"></i>
</a>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<div class="dashboard-main-grid">
<section class="dashboard-card chart-card">
<div class="dashboard-card-header">
<div><h3>Performa Keuangan</h3><p>Perbandingan pendapatan dan pengeluaran per bulan.</p></div>
<select id="yearFilter" class="form-select form-select-sm" aria-label="Pilih tahun grafik">
<option value="<?= (int) $dashboard_year ?>"><?= (int) $dashboard_year ?></option>
</select>
</div>
<div class="chart-container"><canvas id="financeChart"></canvas><div class="chart-message" id="chartMessage" hidden>Grafik belum dapat dimuat.</div></div>
</section>
<section class="dashboard-card attention-card">
<div class="dashboard-card-header"><div><h3>Perlu Perhatian</h3><p>Prioritas operasional saat ini.</p></div></div>
<div class="attention-list">
<a href="<?= base_url('receivables') ?>">
<span class="attention-status <?= $overview['receivable_overdue'] ? 'danger' : 'clear' ?>"><i class="bi bi-receipt"></i></span>
<span><strong>Piutang jatuh tempo</strong><small><?= $overview['receivable_overdue'] ? (int) $overview['receivable_overdue'] . ' invoice perlu ditagih' : 'Tidak ada invoice lewat jatuh tempo' ?></small></span>
<b><?= (int) $overview['receivable_overdue'] ?></b>
</a>
<a href="<?= base_url('payables') ?>">
<span class="attention-status <?= $overview['payable_overdue'] ? 'warning' : 'clear' ?>"><i class="bi bi-credit-card-2-front"></i></span>
<span><strong>Hutang jatuh tempo</strong><small><?= $overview['payable_overdue'] ? (int) $overview['payable_overdue'] . ' tagihan perlu dibayar' : 'Tidak ada hutang lewat jatuh tempo' ?></small></span>
<b><?= (int) $overview['payable_overdue'] ?></b>
</a>
<a href="<?= base_url('items/draft_items') ?>">
<span class="attention-status <?= $overview['pending_items'] ? 'warning' : 'clear' ?>"><i class="bi bi-box2-heart"></i></span>
<span><strong>Barang pending</strong><small><?= $overview['pending_items'] ? (int) $overview['pending_items'] . ' barang belum siap digunakan' : 'Semua barang sudah ditangani' ?></small></span>
<b><?= (int) $overview['pending_items'] ?></b>
</a>
<a href="<?= base_url('approvals') ?>">
<span class="attention-status <?= $overview['pending_approvals'] ? 'warning' : 'clear' ?>"><i class="bi bi-check2-square"></i></span>
<span><strong>Approval tertunda</strong><small><?= $overview['pending_approvals'] ? (int) $overview['pending_approvals'] . ' dokumen menunggu keputusan' : 'Tidak ada approval tertunda' ?></small></span>
<b><?= (int) $overview['pending_approvals'] ?></b>
</a>
</div>
</section>
</div>
<div class="dashboard-lower-grid">
<section class="dashboard-card">
<div class="dashboard-card-header">
<div><h3>Aktivitas Terbaru</h3><p>Jejak aktivitas terakhir yang tercatat di sistem.</p></div>
<a class="section-link" href="<?= base_url('dashboard/log_activity') ?>">Lihat semua <i class="bi bi-arrow-right"></i></a>
</div>
<div class="table-responsive dashboard-table-wrap">
<table class="table dashboard-table align-middle mb-0">
<thead><tr><th>Waktu</th><th>Modul</th><th>Keterangan</th><th>Pengguna</th><th>Status</th></tr></thead>
<tbody id="activityBody"><tr><td colspan="5" class="dashboard-empty"><span class="spinner-border spinner-border-sm"></span> Memuat aktivitas...</td></tr></tbody>
</table>
</div>
</section>
<section class="dashboard-card insight-card">
<div class="dashboard-card-header"><div><h3>Insight Sistem</h3><p>Informasi penting periode berjalan.</p></div><span class="insight-icon"><i class="bi bi-stars"></i></span></div>
<div class="insight-list">
<?php if (!$ai_insight): ?>
<div class="dashboard-empty-state"><i class="bi bi-lightbulb"></i><strong>Belum ada insight baru</strong><span>Sistem belum menemukan hal yang perlu diinformasikan.</span></div>
<?php else: ?>
<?php foreach ($ai_insight as $insight):
$severity = in_array($insight->severity, array('critical', 'warning', 'info'), true) ? $insight->severity : 'info'; ?>
<article class="insight-item <?= html_escape($severity) ?>">
<i class="bi <?= $severity === 'critical' ? 'bi-exclamation-octagon' : ($severity === 'warning' ? 'bi-exclamation-triangle' : 'bi-info-circle') ?>"></i>
<span><?= html_escape($insight->message) ?></span>
</article>
<?php endforeach; ?>
<?php endif; ?>
</div>
</section>
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>
(function () {
'use strict';
var baseUrl = <?= json_encode(base_url()) ?>;
var currentYear = <?= (int) $dashboard_year ?>;
var chart = null;
function escapeHtml(value) {
var node = document.createElement('div');
node.textContent = value == null ? '-' : String(value);
return node.innerHTML;
}
function moneyCompact(value) {
return new Intl.NumberFormat('id-ID', {notation: 'compact', maximumFractionDigits: 1}).format(Number(value || 0));
}
function loadYears() {
fetch(baseUrl + 'dashboard/get_years', {headers: {'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest'}})
.then(function (response) { return response.json(); })
.then(function (years) {
var select = document.getElementById('yearFilter');
select.innerHTML = years.map(function (item) {
var year = Number(item.year);
return '<option value="' + year + '"' + (year === currentYear ? ' selected' : '') + '>' + year + '</option>';
}).join('');
}).catch(function () {});
}
function loadChart(year) {
var message = document.getElementById('chartMessage');
fetch(baseUrl + 'dashboard/get_chart?year=' + encodeURIComponent(year), {headers: {'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest'}})
.then(function (response) { if (!response.ok) throw new Error('Respons grafik gagal.'); return response.json(); })
.then(function (data) {
if (typeof window.Chart === 'undefined') throw new Error('Library grafik tidak tersedia.');
var canvas = document.getElementById('financeChart');
var context = canvas.getContext('2d');
var revenueGradient = context.createLinearGradient(0, 0, 0, 280);
revenueGradient.addColorStop(0, 'rgba(17, 154, 100, .22)');
revenueGradient.addColorStop(1, 'rgba(17, 154, 100, .01)');
var expenseGradient = context.createLinearGradient(0, 0, 0, 280);
expenseGradient.addColorStop(0, 'rgba(238, 120, 0, .20)');
expenseGradient.addColorStop(1, 'rgba(238, 120, 0, .01)');
if (chart) chart.destroy();
message.hidden = true;
chart = new Chart(context, {
type: 'line',
data: {labels: data.labels || [], datasets: [
{label: 'Pendapatan', data: data.revenue || [], borderColor: '#119a64', backgroundColor: revenueGradient, borderWidth: 2, fill: true, tension: .35, pointRadius: 0, pointHoverRadius: 4},
{label: 'Pengeluaran', data: data.expense || [], borderColor: '#ee7800', backgroundColor: expenseGradient, borderWidth: 2, fill: true, tension: .35, pointRadius: 0, pointHoverRadius: 4}
]},
options: {
responsive: true, maintainAspectRatio: false, interaction: {mode: 'index', intersect: false},
plugins: {legend: {position: 'bottom', align: 'start', labels: {usePointStyle: true, boxWidth: 7, boxHeight: 7, padding: 18, font: {size: 11}}}, tooltip: {padding: 11, callbacks: {label: function (item) { return item.dataset.label + ': Rp ' + Number(item.raw || 0).toLocaleString('id-ID'); }}}},
scales: {x: {grid: {display: false}, ticks: {font: {size: 10}, color: '#667085'}}, y: {beginAtZero: true, border: {display: false}, grid: {color: '#eef1f5'}, ticks: {font: {size: 10}, color: '#667085', callback: moneyCompact}}}
}
});
}).catch(function () { message.hidden = false; });
}
function statusBadge(status) {
var value = String(status || 'success').toLowerCase();
var map = {success: ['success', 'Berhasil'], failed: ['danger', 'Gagal'], pending: ['warning', 'Pending']};
var item = map[value] || ['secondary', value];
return '<span class="badge bg-' + item[0] + '">' + escapeHtml(item[1]) + '</span>';
}
function loadActivity() {
fetch(baseUrl + 'dashboard/get_activity', {headers: {'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest'}})
.then(function (response) { if (!response.ok) throw new Error(); return response.json(); })
.then(function (rows) {
var body = document.getElementById('activityBody');
if (!Array.isArray(rows) || !rows.length) {
body.innerHTML = '<tr><td colspan="5" class="dashboard-empty">Belum ada aktivitas.</td></tr>';
return;
}
body.innerHTML = rows.map(function (item) {
var parsed = new Date(String(item.created_at || '').replace(' ', 'T'));
var time = isNaN(parsed.getTime()) ? '-' : parsed.toLocaleString('id-ID', {day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit'});
return '<tr><td class="activity-time">' + escapeHtml(time) + '</td><td><span class="module-chip">' + escapeHtml(item.module) + '</span></td><td>' + escapeHtml(item.description) + '</td><td>' + escapeHtml(item.nama || 'System') + '</td><td>' + statusBadge(item.status) + '</td></tr>';
}).join('');
}).catch(function () {
document.getElementById('activityBody').innerHTML = '<tr><td colspan="5" class="dashboard-empty text-danger">Aktivitas tidak dapat dimuat.</td></tr>';
});
}
document.getElementById('yearFilter').addEventListener('change', function () { loadChart(this.value); });
loadYears();
loadChart(currentYear);
loadActivity();
}());
</script>