update user company UI CSS

This commit is contained in:
Wian Drs
2026-09-11 23:35:24 +07:00
parent b011272f28
commit e43bda8d6b
6 changed files with 1026 additions and 1067 deletions
+290 -233
View File
@@ -1,92 +1,44 @@
<?php <?php
defined('BASEPATH') OR exit('No direct script access allowed'); defined('BASEPATH') OR exit('No direct script access allowed');
class Dashboard extends MY_Controller { class Dashboard extends MY_Controller
{
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->load->library('session');
$this->load->library('PerformanceCacheService'); $this->load->library('PerformanceCacheService');
// Cek apakah user sudah login
if (!$this->session->userdata('logged_in')) {
redirect('auth');
}
} }
public function index() public function index()
{ {
$companyId=(int)$this->companycontext->id(); $companyId = (int) $this->companycontext->id();
$summary=$this->performancecacheservice->remember('dashboard:summary:'.$companyId,300,function()use($companyId){ $year = (int) date('Y');
$row=$this->db->query("SELECT COALESCE(SUM(IF(a.tipe='revenue',d.kredit-d.debit,0)),0) total_revenue,COALESCE(SUM(IF(a.tipe='expense',d.debit-d.kredit,0)),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",array($companyId))->row_array(); $summary = $this->performancecacheservice->remember(
$row['net_profit']=(float)$row['total_revenue']-(float)$row['total_expense'];return$row; 'dashboard:summary:' . $companyId . ':' . $year,
}); 300,
goto dashboard_summary_ready; function () use ($companyId, $year) {
$this->db->select(' return $this->financialSummary($companyId, $year);
accounts.id,
accounts.kode_akun,
accounts.nama_akun,
accounts.tipe,
COALESCE(SUM(journal_details.debit),0) as debit,
COALESCE(SUM(journal_details.kredit),0) as kredit
');
$this->db->from('accounts');
$this->db->join('journal_details','accounts.id = journal_details.account_id','left');
$this->db->join('journals','journals.id = journal_details.journal_id','left');
$this->db->where("(journals.status IN ('posted','reversed') OR journal_details.id IS NULL)", null, false);
$this->db->where('accounts.kategori','laba_rugi');
$this->db->where('accounts.is_active',1);
$this->db->group_by('accounts.id');
$this->db->order_by('accounts.kode_akun','ASC');
$rows = $this->db->get()->result();
$revenue = 0;
$expense = 0;
foreach ($rows as $r) {
// 🔥 NORMALISASI SALDO BERDASARKAN POSISI
if ($r->tipe == 'revenue') {
$saldo = $r->kredit - $r->debit;
$revenue += $saldo;
} }
);
$previous = $this->financialSummary($companyId, $year - 1);
$overview = $this->operationalOverview($companyId);
if ($r->tipe == 'expense') { $data = array(
$saldo = $r->debit - $r->kredit; 'active_menu' => 'dashboard',
$expense += $saldo; 'summary' => $summary,
} 'comparison' => array(
} 'revenue' => $this->percentageChange($summary['total_revenue'], $previous['total_revenue']),
'expense' => $this->percentageChange($summary['total_expense'], $previous['total_expense']),
$summary = [ 'profit' => $this->percentageChange($summary['net_profit'], $previous['net_profit']),
'total_revenue' => $revenue, ),
'total_expense' => $expense, 'overview' => $overview,
'net_profit' => $revenue - $expense 'ai_insight' => $this->insights($companyId),
]; 'dashboard_year' => $year,
'quick_links' => $this->quickLinks(),
dashboard_summary_ready: 'show_financial' => is_master_admin_user()
$today = date('Y-m'); || check_permission('reports', 'can_view')
$ai_insight = $this->db || check_permission('jurnal', 'can_view'),
->select('type, severity, message, meta, created_at') );
->where('period', $today)
->order_by("
CASE severity
WHEN 'critical' THEN 1
WHEN 'warning' THEN 2
WHEN 'info' THEN 3
END
", '', false)
->order_by('created_at', 'DESC')
->limit(20)
->get('ai_insights')
->result();
$data = [
"active_menu" => "dashboard",
"summary" => $summary,
"ai_insight" => $ai_insight
];
$this->load->view('partials/header', $data); $this->load->view('partials/header', $data);
$this->load->view('dashboard/layout', $data); $this->load->view('dashboard/layout', $data);
@@ -95,197 +47,302 @@ class Dashboard extends MY_Controller {
public function get_activity() public function get_activity()
{ {
$this->db->select('activity_logs.*, users.nama'); $this->activityQuery();
$this->db->from('activity_logs'); $rows = $this->db->order_by('activity_logs.created_at', 'DESC')->limit(10)->get()->result();
$this->db->join('users','users.id = activity_logs.user_id','left'); return $this->json($rows);
if ($this->session->userdata('role') != 'Admin') {
$this->db->where('user_id',$this->session->userdata('user_id'));
}
$this->db->order_by('created_at','DESC');
$this->db->limit(10);
$data = $this->db->get()->result();
echo json_encode($data);
} }
public function log_activity() public function log_activity()
{ {
$data = [ $data = array('active_menu' => 'dashboard');
"active_menu" => "dashboard"
];
$this->load->view('partials/header', $data); $this->load->view('partials/header', $data);
$this->load->view('dashboard/activity_logs', $data); $this->load->view('dashboard/activity_logs', $data);
$this->load->view('partials/footer'); $this->load->view('partials/footer');
} }
public function get_activity_datatable() public function get_activity_datatable()
{ {
$this->load->database(); $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'] ?? '')) : '';
$draw = $this->input->post('draw'); $this->activityQuery($search);
$start = $this->input->post('start'); $recordsFiltered = (int) $this->db->count_all_results();
$length = $this->input->post('length');
$search = $this->input->post('search')['value'] ?? '';
// ========================= $this->activityQuery($search);
// BASE QUERY
// =========================
$this->db->select('
activity_logs.*,
users.nama
');
$this->db->from('activity_logs');
$this->db->join('users', 'users.id = activity_logs.user_id', 'left');
// =========================
// SEARCH
// =========================
if (!empty($search)) {
$this->db->group_start();
$this->db->like('activity_logs.module', $search);
$this->db->or_like('activity_logs.action', $search);
$this->db->or_like('activity_logs.description', $search);
$this->db->or_like('activity_logs.status', $search);
$this->db->or_like('users.nama', $search);
$this->db->group_end();
}
// =========================
// COUNT FILTERED
// =========================
$filtered_query = clone $this->db;
$recordsFiltered = $filtered_query->count_all_results('', false);
// =========================
// ORDER
// =========================
$this->db->order_by('activity_logs.id', 'DESC'); $this->db->order_by('activity_logs.id', 'DESC');
if ($length !== -1) {
// ========================= $this->db->limit(max(1, $length), $start);
// LIMIT
// =========================
if ($length != -1) {
$this->db->limit($length, $start);
} }
$rows = $this->db->get()->result();
$query = $this->db->get(); $this->activityQuery();
$data = $query->result(); $recordsTotal = (int) $this->db->count_all_results();
$result = array();
// ========================= foreach ($rows as $row) {
// TOTAL RECORD $result[] = array(
// ========================= 'created_at' => $row->created_at,
$recordsTotal = $this->db->count_all('activity_logs'); 'nama' => $row->nama ?: 'System',
'module' => $row->module,
// ========================= 'action' => $row->action,
// FORMAT OUTPUT
// =========================
$result = [];
foreach ($data as $row) {
$result[] = [
'created_at' => $row->created_at,
'nama' => $row->nama ?? 'System',
'module' => $row->module,
'action' => $row->action,
'description' => $row->description, 'description' => $row->description,
'status' => $row->status, 'status' => $row->status,
'ip_address' => $row->ip_address 'ip_address' => $row->ip_address,
]; );
} }
echo json_encode([ return $this->json(array(
"draw" => intval($draw), 'draw' => $draw,
"recordsTotal" => $recordsTotal, 'recordsTotal' => $recordsTotal,
"recordsFiltered" => $recordsFiltered, 'recordsFiltered' => $recordsFiltered,
"data" => $result 'data' => $result,
]); ));
} }
public function get_chart() public function get_chart()
{ {
$year = $this->input->get('year'); $year = (int) $this->input->get('year');
if ($year < 2000 || $year > 2200) {
// 🔥 DEFAULT: tahun berjalan $year = (int) date('Y');
if (empty($year)) {
$year = date('Y');
} }
$this->db->select(' $companyId = (int) $this->companycontext->id();
journals.tanggal, $rows = $this->db->query(
accounts.tipe, "SELECT MONTH(j.tanggal) month_no,
SUM(journal_details.debit) as total_debit, COALESCE(SUM(CASE WHEN a.tipe='revenue' THEN d.kredit-d.debit ELSE 0 END),0) revenue,
SUM(journal_details.kredit) as total_kredit COALESCE(SUM(CASE WHEN a.tipe='expense' THEN d.debit-d.kredit ELSE 0 END),0) expense
'); FROM journal_details d
$this->db->from('journal_details'); JOIN journals j ON j.id=d.journal_id
$this->db->join('journals', 'journals.id = journal_details.journal_id', 'left'); JOIN accounts a ON a.id=d.account_id
$this->db->join('accounts', 'accounts.id = journal_details.account_id', 'left'); WHERE j.company_id=? AND j.status IN('posted','reversed')
$this->db->where_in('journals.status', array('posted', 'reversed')); AND a.kategori='laba_rugi' AND a.is_active=1 AND YEAR(j.tanggal)=?
$this->db->where('accounts.kategori', 'laba_rugi'); GROUP BY MONTH(j.tanggal)
$this->db->where('accounts.is_active', 1); ORDER BY month_no",
array($companyId, $year)
)->result();
// 🔥 pakai tahun default / filter $months = array('Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des');
$this->db->where('YEAR(journals.tanggal)', $year); $revenue = array_fill(0, 12, 0);
$expense = array_fill(0, 12, 0);
$this->db->group_by(['journals.tanggal', 'accounts.tipe']); foreach ($rows as $row) {
$this->db->order_by('journals.tanggal', 'ASC'); $index = (int) $row->month_no - 1;
if ($index >= 0 && $index < 12) {
$rows = $this->db->get()->result(); $revenue[$index] = (float) $row->revenue;
$expense[$index] = (float) $row->expense;
$map = [];
foreach ($rows as $r) {
$date = $r->tanggal;
if (!isset($map[$date])) {
$map[$date] = [
'revenue' => 0,
'expense' => 0
];
}
if ($r->tipe == 'revenue') {
$map[$date]['revenue'] += ($r->total_kredit - $r->total_debit);
}
if ($r->tipe == 'expense') {
$map[$date]['expense'] += ($r->total_debit - $r->total_kredit);
} }
} }
$labels = []; return $this->json(array(
$revenue = []; 'year' => $year,
$expense = []; 'labels' => $months,
foreach ($map as $date => $val) {
$labels[] = $date;
$revenue[] = (float)$val['revenue'];
$expense[] = (float)$val['expense'];
}
echo json_encode([
'year' => $year, // optional biar debugging enak
'labels' => $labels,
'revenue' => $revenue, 'revenue' => $revenue,
'expense' => $expense 'expense' => $expense,
]); ));
} }
public function get_years() public function get_years()
{ {
$this->db->select('DISTINCT YEAR(tanggal) as year'); $this->db->select('DISTINCT YEAR(tanggal) year', false)
$this->db->from('journals'); ->from('journals')
$this->db->where_in('status', array('posted', 'reversed')); ->where('company_id', (int) $this->companycontext->id())
$this->db->order_by('year', 'DESC'); ->where_in('status', array('posted', 'reversed'))
->order_by('year', 'DESC');
$data = $this->db->get()->result(); $years = $this->db->get()->result();
if (!$years) {
echo json_encode($data); $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));
}
} }
+10 -9
View File
@@ -43,7 +43,8 @@ $authNotice = trim((string) $this->session->flashdata('auth_notice'));
background: background:
radial-gradient(circle at 8% 12%, rgba(255, 138, 0, .13), transparent 28rem), radial-gradient(circle at 8% 12%, rgba(255, 138, 0, .13), transparent 28rem),
linear-gradient(135deg, #f8fafc 0%, #fff8ef 100%); linear-gradient(135deg, #f8fafc 0%, #fff8ef 100%);
font-family: Inter, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 13px;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
@@ -145,17 +146,17 @@ $authNotice = trim((string) $this->session->flashdata('auth_notice'));
max-width: 470px; max-width: 470px;
margin: 0; margin: 0;
color: #fff; color: #fff;
font-size: clamp(35px, 4.3vw, 54px); font-size: clamp(27px, 2.6vw, 32px);
line-height: 1.08; line-height: 1.2;
letter-spacing: -.045em; letter-spacing: -.035em;
} }
.brand-copy p { .brand-copy p {
max-width: 450px; max-width: 450px;
margin: 22px 0 0; margin: 22px 0 0;
color: rgba(255, 255, 255, .72); color: rgba(255, 255, 255, .72);
font-size: 16px; font-size: 13px;
line-height: 1.75; line-height: 1.7;
} }
.brand-footer { .brand-footer {
@@ -181,7 +182,7 @@ $authNotice = trim((string) $this->session->flashdata('auth_notice'));
.form-heading h2 { .form-heading h2 {
margin: 0; margin: 0;
color: var(--navy-950); color: var(--navy-950);
font-size: 31px; font-size: 22px;
line-height: 1.2; line-height: 1.2;
letter-spacing: -.03em; letter-spacing: -.03em;
} }
@@ -189,7 +190,7 @@ $authNotice = trim((string) $this->session->flashdata('auth_notice'));
.form-heading p { .form-heading p {
margin: 10px 0 0; margin: 10px 0 0;
color: var(--slate-500); color: var(--slate-500);
font-size: 14px; font-size: 13px;
line-height: 1.6; line-height: 1.6;
} }
@@ -337,7 +338,7 @@ $authNotice = trim((string) $this->session->flashdata('auth_notice'));
.auth-shell { width: 100%; min-height: 100vh; min-height: 100dvh; border: 0; border-radius: 0; box-shadow: none; } .auth-shell { width: 100%; min-height: 100vh; min-height: 100dvh; border: 0; border-radius: 0; box-shadow: none; }
.form-panel { align-items: flex-start; padding: 30px 22px; } .form-panel { align-items: flex-start; padding: 30px 22px; }
.mobile-logo { margin-bottom: 42px; } .mobile-logo { margin-bottom: 42px; }
.form-heading h2 { font-size: 28px; } .form-heading h2 { font-size: 21px; }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
<link rel="stylesheet" href="<?=base_url('assets/css/inventory-professional.css')?>?v=<?=filemtime(FCPATH.'assets/css/inventory-professional.css')?>"> <link rel="stylesheet" href="<?=base_url('assets/css/inventory-professional.css')?>?v=<?=filemtime(FCPATH.'assets/css/inventory-professional.css')?>">
<section class="inventory-hero mb-3"> <section class="inventory-hero mb-3">
<div><span class="inventory-kicker">PERSEDIAAN TERINTEGRASI</span><h2 class="mb-1"><?=html_escape($inventory_title??'Persediaan')?></h2><p class="mb-0"><?=html_escape($inventory_subtitle??'Kontrol barang, gudang, dan nilai persediaan dalam satu alur.')?></p></div> <div><span class="inventory-kicker">PERSEDIAAN TERINTEGRASI</span><h2 class="mb-1 text-light"><?=html_escape($inventory_title??'Persediaan')?></h2><p class="mb-0"><?=html_escape($inventory_subtitle??'Kontrol barang, gudang, dan nilai persediaan dalam satu alur.')?></p></div>
<a class="btn btn-light inventory-scan-link" href="<?=base_url('inventoryprofessional')?>#scanner"><i class="bi bi-qr-code-scan"></i> Scan Barcode</a> <a class="btn btn-light inventory-scan-link" href="<?=base_url('inventoryprofessional')?>#scanner"><i class="bi bi-qr-code-scan"></i> Scan Barcode</a>
</section> </section>
<nav class="inventory-module-nav mb-4" aria-label="Menu Persediaan"> <nav class="inventory-module-nav mb-4" aria-label="Menu Persediaan">
+195 -4
View File
@@ -1,4 +1,3 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
:root { :root {
--brand: #ee7800; --brand: #ee7800;
--brand-dark: #c85d00; --brand-dark: #c85d00;
@@ -23,7 +22,7 @@ body.app-v2 {
margin: 0; margin: 0;
background: var(--canvas); background: var(--canvas);
color: var(--ink); color: var(--ink);
font-family: Inter, "Segoe UI", sans-serif; font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 12px; font-size: 12px;
overflow-x: hidden; overflow-x: hidden;
} }
@@ -840,8 +839,8 @@ body.app-v2 {
} }
} }
.nav-badge.nav-badge-warning { .nav-badge.nav-badge-warning {
background: #f2b705; background: #ee7800;
color: #302400; color: #fff;
font-weight: 700; font-weight: 700;
} }
.nav-badge.nav-badge-danger { .nav-badge.nav-badge-danger {
@@ -899,3 +898,195 @@ body.app-v2 {
border-radius: 8px; border-radius: 8px;
text-decoration: none; text-decoration: none;
} }
/* ================================================================
UI FOUNDATION 2026
Standar tipografi dan warna lintas modul. Selector dibuat eksplisit
supaya view lama tetap mengikuti sistem visual yang sama.
================================================================ */
:root {
--ui-font: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--ui-font-size: 13px;
--ui-table-font-size: 11px;
--ui-heading-1: 20px;
--ui-heading-2: 18px;
--ui-heading-3: 17px;
--ui-heading-4: 16px;
--ui-heading-5: 14px;
--ui-heading-6: 13px;
--warning: #ee7800;
--warning-dark: #c85d00;
--warning-soft: #fff2e5;
--warning-border: #ffc68f;
--bs-warning: #ee7800;
--bs-warning-rgb: 238, 120, 0;
}
html body.app-v2,
html body.app-v2 button,
html body.app-v2 input,
html body.app-v2 select,
html body.app-v2 textarea,
html body.app-v2 .select2-container,
html body.app-v2 .swal2-container {
font-family: var(--ui-font) !important;
}
html body.app-v2 {
font-size: var(--ui-font-size) !important;
line-height: 1.5;
}
html body.app-v2 .app-content h1 { font-size: var(--ui-heading-1) !important; }
html body.app-v2 .app-content h2 { font-size: var(--ui-heading-2) !important; }
html body.app-v2 .app-content h3 { font-size: var(--ui-heading-3) !important; }
html body.app-v2 .app-content h4 { font-size: var(--ui-heading-4) !important; }
html body.app-v2 .app-content h5 { font-size: var(--ui-heading-5) !important; }
html body.app-v2 .app-content h6 { font-size: var(--ui-heading-6) !important; }
html body.app-v2 .app-content h1,
html body.app-v2 .app-content h2,
html body.app-v2 .app-content h3,
html body.app-v2 .app-content h4,
html body.app-v2 .app-content h5,
html body.app-v2 .app-content h6 {
color: #1d2939;
font-weight: 700;
line-height: 1.3;
}
html body.app-v2 .app-topbar h1 {
font-size: 16px !important;
letter-spacing: -0.01em;
}
html body.app-v2 .card-header,
html body.app-v2 .modal-title {
font-size: 14px !important;
line-height: 1.4;
}
html body.app-v2 .form-label,
html body.app-v2 label {
font-size: 12px;
}
html body.app-v2 .form-control,
html body.app-v2 .form-select,
html body.app-v2 .select2-selection__rendered {
font-size: 12.5px !important;
}
html body.app-v2 table,
html body.app-v2 table.table,
html body.app-v2 table.dataTable,
html body.app-v2 table.modern-table,
html body.app-v2 table.inventory-table,
html body.app-v2 table.tech-table {
font-size: var(--ui-table-font-size) !important;
}
html body.app-v2 table th,
html body.app-v2 table td,
html body.app-v2 .table th,
html body.app-v2 .table td,
html body.app-v2 table.dataTable th,
html body.app-v2 table.dataTable td {
font-size: var(--ui-table-font-size) !important;
line-height: 1.45;
}
html body.app-v2 table .btn,
html body.app-v2 table .badge,
html body.app-v2 table small {
font-size: var(--ui-table-font-size) !important;
}
html body.app-v2 .btn-warning,
html body.app-v2 .bg-warning,
html body.app-v2 .text-bg-warning,
html body.app-v2 .badge.bg-warning,
html body.app-v2 .nav-badge-warning {
border-color: var(--warning) !important;
background: var(--warning) !important;
color: #fff !important;
}
html body.app-v2 .btn-warning:hover,
html body.app-v2 .btn-warning:focus,
html body.app-v2 .btn-warning:active {
border-color: var(--warning-dark) !important;
background: var(--warning-dark) !important;
color: #fff !important;
}
html body.app-v2 .btn-outline-warning {
border-color: var(--warning) !important;
color: var(--warning-dark) !important;
background: #fff !important;
}
html body.app-v2 .btn-outline-warning:hover,
html body.app-v2 .btn-outline-warning:focus {
border-color: var(--warning) !important;
background: var(--warning) !important;
color: #fff !important;
}
html body.app-v2 .alert-warning,
html body.app-v2 .bg-warning-subtle,
html body.app-v2 .tech-warning,
html body.app-v2 .ai-warning,
html body.app-v2 .status-warning,
html body.app-v2 .status-pending,
html body.app-v2 .status-submitted,
html body.app-v2 .status-reserved {
border-color: var(--warning-border) !important;
background: var(--warning-soft) !important;
color: #8a4500 !important;
}
html body.app-v2 .step-badge.warning {
background: var(--warning) !important;
color: #fff !important;
}
html body.app-v2 .text-warning {
color: var(--warning-dark) !important;
}
html body.app-v2 .border-warning {
border-color: var(--warning-border) !important;
}
html body.app-v2 .swal2-icon.swal2-warning {
border-color: var(--warning) !important;
color: var(--warning) !important;
}
html body.app-v2 .swal2-confirm {
background: var(--brand) !important;
}
/* Aksen modul lama memakai satu warna brand yang sama. */
html body.app-v2 .tech-shell {
--tech-orange: var(--brand) !important;
}
html body.app-v2 .sales-section-nav a.active {
background: var(--brand) !important;
}
html body.app-v2 .line-type-switch input:checked + span {
border-color: var(--brand) !important;
}
html body.app-v2 .company-settings-hero {
background: linear-gradient(125deg, #172b4d, #24456f 62%, var(--brand)) !important;
}
@media (max-width: 767.98px) {
html body.app-v2 { font-size: var(--ui-font-size) !important; }
html body.app-v2 .app-content h1 { font-size: 19px !important; }
html body.app-v2 .app-content h2 { font-size: 17px !important; }
}
+295
View File
@@ -0,0 +1,295 @@
.dashboard-shell {
--dash-navy: #172b4d;
--dash-muted: #667085;
--dash-line: #e4e7ec;
--dash-orange: #ee7800;
display: grid;
gap: 16px;
padding-bottom: 18px;
}
.dashboard-intro {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 22px 24px;
border: 1px solid rgba(23, 43, 77, .16);
border-radius: 16px;
color: #fff;
background:
radial-gradient(circle at 88% 20%, rgba(238, 120, 0, .34), transparent 23rem),
linear-gradient(125deg, #142642, #213b62);
box-shadow: 0 10px 26px rgba(23, 43, 77, .12);
}
.dashboard-eyebrow {
display: block;
margin-bottom: 5px;
color: #ffbf7d;
font-size: 10px;
font-weight: 800;
letter-spacing: .15em;
text-transform: uppercase;
}
.dashboard-intro h2 {
margin: 0 0 5px;
color: #fff !important;
font-size: 20px !important;
}
.dashboard-intro p {
margin: 0;
color: rgba(255, 255, 255, .72);
font-size: 12px;
}
.dashboard-period {
min-width: 175px;
display: flex;
align-items: center;
gap: 11px;
padding: 10px 13px;
border: 1px solid rgba(255, 255, 255, .18);
border-radius: 11px;
background: rgba(255, 255, 255, .09);
}
.dashboard-period > i { color: #ffab51; font-size: 18px; }
.dashboard-period span, .dashboard-period small, .dashboard-period strong { display: block; }
.dashboard-period small { color: rgba(255, 255, 255, .62); font-size: 10px; }
.dashboard-period strong { color: #fff; font-size: 12px; }
.dashboard-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.metric-card,
.dashboard-card,
.dashboard-section,
.operational-card {
border: 1px solid var(--dash-line);
border-radius: 14px;
background: #fff;
box-shadow: 0 5px 18px rgba(16, 24, 40, .045);
}
.metric-card {
min-width: 0;
padding: 16px;
}
.metric-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 13px;
}
.metric-top small { color: #98a2b3; font-size: 10px; }
.metric-icon,
.operational-icon,
.attention-status {
display: grid;
place-items: center;
flex: 0 0 auto;
}
.metric-icon {
width: 34px;
height: 34px;
border-radius: 9px;
font-size: 15px;
}
.metric-revenue .metric-icon { color: #087443; background: #e4f8ef; }
.metric-expense .metric-icon { color: #b54708; background: #fff0df; }
.metric-profit .metric-icon { color: #175cd3; background: #eaf2ff; }
.metric-receivable .metric-icon { color: #6938ef; background: #f1edff; }
.metric-label {
display: block;
margin-bottom: 4px;
color: var(--dash-muted);
font-size: 11px;
font-weight: 600;
}
.metric-card > strong {
display: block;
overflow: hidden;
margin-bottom: 9px;
color: #1d2939;
font-size: clamp(16px, 1.45vw, 21px);
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.metric-card > a {
color: #6941c6;
font-size: 10px;
font-weight: 700;
text-decoration: none;
}
.metric-comparison { font-size: 10px; font-weight: 650; }
.metric-comparison.is-good { color: #078045; }
.metric-comparison.is-bad { color: #c4323d; }
.metric-comparison.is-neutral { color: #98a2b3; font-weight: 500; }
.operational-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.operational-card {
position: relative;
min-width: 0;
display: flex;
align-items: center;
gap: 11px;
padding: 13px;
color: #344054;
text-decoration: none;
transition: border-color .16s ease, box-shadow .16s ease;
}
.operational-card:hover {
color: #1d2939;
border-color: #ffc68f;
box-shadow: 0 8px 22px rgba(16, 24, 40, .08);
}
.operational-icon { width: 38px; height: 38px; border-radius: 10px; font-size: 16px; }
.operational-icon.orange { color: #b54708; background: #fff2e5; }
.operational-icon.blue { color: #175cd3; background: #eff6ff; }
.operational-icon.green { color: #087443; background: #e8f8f0; }
.operational-icon.purple { color: #6941c6; background: #f2efff; }
.operational-card > span:nth-child(2) { min-width: 0; }
.operational-card small, .operational-card strong, .operational-card em { display: block; }
.operational-card small { color: var(--dash-muted); font-size: 10px; }
.operational-card strong { overflow: hidden; color: #1d2939; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.operational-card em { color: #98a2b3; font-size: 9px; font-style: normal; }
.attention-badge {
position: absolute;
top: 8px;
right: 8px;
padding: 2px 6px;
border-radius: 999px;
color: #fff;
background: var(--dash-orange);
font-size: 8px;
font-weight: 700;
}
.dashboard-section { padding: 15px 16px; }
.section-heading, .dashboard-card-header { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
.section-heading { margin-bottom: 12px; }
.section-heading h3, .dashboard-card-header h3 { margin: 0; color: #1d2939; font-size: 14px !important; }
.section-heading p, .dashboard-card-header p { margin: 3px 0 0; color: var(--dash-muted); font-size: 10px; }
.dashboard-quick-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 9px;
}
.dashboard-quick-grid > a {
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 11px;
border: 1px solid #eaecf0;
border-radius: 10px;
color: #344054;
background: #fbfcfd;
text-decoration: none;
}
.dashboard-quick-grid > a:hover { border-color: #ffc68f; background: #fff9f3; }
.dashboard-quick-grid > a > i:first-child { color: var(--dash-orange); font-size: 17px; }
.dashboard-quick-grid span { min-width: 0; flex: 1; }
.dashboard-quick-grid strong, .dashboard-quick-grid small { display: block; }
.dashboard-quick-grid strong { font-size: 11px; }
.dashboard-quick-grid small { overflow: hidden; color: var(--dash-muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
.quick-arrow { color: #98a2b3; font-size: 11px; }
.dashboard-main-grid,
.dashboard-lower-grid {
display: grid;
grid-template-columns: minmax(0, 1.65fr) minmax(300px, .75fr);
gap: 12px;
}
.dashboard-lower-grid { grid-template-columns: minmax(0, 1.55fr) minmax(300px, .85fr); }
.dashboard-card { min-width: 0; overflow: hidden; }
.dashboard-card-header { min-height: 64px; padding: 13px 16px; border-bottom: 1px solid #eaecf0; }
.dashboard-card-header .form-select { width: 105px; min-height: 34px; }
.chart-container { position: relative; height: 300px; padding: 16px 16px 8px; }
.chart-message { position: absolute; inset: 0; place-items: center; color: var(--dash-muted); background: #fff; }
.chart-message:not([hidden]) { display: grid; }
.attention-list { display: grid; }
.attention-list > a { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 12px 15px; border-bottom: 1px solid #f0f1f3; color: #344054; text-decoration: none; }
.attention-list > a:last-child { border-bottom: 0; }
.attention-list > a:hover { background: #fafbfc; }
.attention-status { width: 32px; height: 32px; border-radius: 9px; }
.attention-status.danger { color: #b42318; background: #feeceb; }
.attention-status.warning { color: #b54708; background: #fff2e5; }
.attention-status.clear { color: #087443; background: #e9f8f1; }
.attention-list strong, .attention-list small { display: block; }
.attention-list strong { color: #344054; font-size: 11px; }
.attention-list small { color: var(--dash-muted); font-size: 9px; }
.attention-list > a > b { color: #475467; font-size: 12px; }
.section-link { color: var(--dash-orange); font-size: 10px; font-weight: 700; text-decoration: none; white-space: nowrap; }
.dashboard-table-wrap { margin: 0 !important; border: 0 !important; border-radius: 0 !important; }
.dashboard-table td:first-child { white-space: nowrap; }
.activity-time { color: #475467; font-weight: 650; }
.module-chip { display: inline-block; padding: 3px 7px; border-radius: 6px; color: #344054; background: #f2f4f7; font-weight: 650; }
.dashboard-table .badge { padding: 4px 7px; border-radius: 999px; text-transform: capitalize; }
.dashboard-empty { padding: 32px !important; color: #98a2b3 !important; text-align: center; }
.insight-icon { width: 31px; height: 31px; display: grid; place-items: center; border-radius: 9px; color: #b54708; background: #fff2e5; }
.insight-list { max-height: 340px; overflow: auto; }
.insight-item { display: flex; align-items: flex-start; gap: 9px; padding: 11px 14px; border-bottom: 1px solid #f0f1f3; font-size: 10px; line-height: 1.55; }
.insight-item:last-child { border-bottom: 0; }
.insight-item > i { margin-top: 2px; }
.insight-item.critical > i { color: #d92d20; }
.insight-item.warning > i { color: var(--dash-orange); }
.insight-item.info > i { color: #175cd3; }
.dashboard-empty-state { min-height: 210px; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 5px; padding: 24px; color: #98a2b3; text-align: center; }
.dashboard-empty-state > i { font-size: 25px; color: #ee7800; }
.dashboard-empty-state strong { color: #475467; font-size: 11px; }
.dashboard-empty-state span { font-size: 9px; }
@media (max-width: 1199.98px) {
.dashboard-metrics, .operational-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.dashboard-main-grid, .dashboard-lower-grid { grid-template-columns: 1fr; }
}
@media (max-width: 767.98px) {
.dashboard-shell { gap: 12px; }
.dashboard-intro { align-items: flex-start; flex-direction: column; padding: 18px; }
.dashboard-period { min-width: 0; width: 100%; }
.dashboard-metrics, .operational-grid { grid-template-columns: 1fr 1fr; gap: 8px; }
.metric-card { padding: 13px; }
.metric-card > strong { font-size: 16px; }
.dashboard-quick-grid { display: flex; overflow-x: auto; padding-bottom: 4px; }
.dashboard-quick-grid > a { min-width: 205px; }
.chart-container { height: 265px; padding: 12px 8px 6px; }
}
@media (max-width: 479.98px) {
.dashboard-metrics, .operational-grid { grid-template-columns: 1fr; }
.dashboard-card-header { align-items: flex-start; }
}