diff --git a/application/controllers/Dashboard.php b/application/controllers/Dashboard.php index 6d47b8e..19cca35 100644 --- a/application/controllers/Dashboard.php +++ b/application/controllers/Dashboard.php @@ -1,92 +1,44 @@ load->library('session'); $this->load->library('PerformanceCacheService'); - - // Cek apakah user sudah login - if (!$this->session->userdata('logged_in')) { - redirect('auth'); - } } public function index() { - $companyId=(int)$this->companycontext->id(); - $summary=$this->performancecacheservice->remember('dashboard:summary:'.$companyId,300,function()use($companyId){ - $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(); - $row['net_profit']=(float)$row['total_revenue']-(float)$row['total_expense'];return$row; - }); - goto dashboard_summary_ready; - $this->db->select(' - 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; + $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); - if ($r->tipe == 'expense') { - $saldo = $r->debit - $r->kredit; - $expense += $saldo; - } - } - - $summary = [ - 'total_revenue' => $revenue, - 'total_expense' => $expense, - 'net_profit' => $revenue - $expense - ]; - - dashboard_summary_ready: - $today = date('Y-m'); - $ai_insight = $this->db - ->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 - ]; + $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); @@ -95,197 +47,302 @@ class Dashboard extends MY_Controller { public function get_activity() { - $this->db->select('activity_logs.*, users.nama'); - $this->db->from('activity_logs'); - $this->db->join('users','users.id = activity_logs.user_id','left'); - - 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); + $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 = [ - "active_menu" => "dashboard" - ]; - + $data = array('active_menu' => 'dashboard'); $this->load->view('partials/header', $data); $this->load->view('dashboard/activity_logs', $data); $this->load->view('partials/footer'); - } 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'); - $start = $this->input->post('start'); - $length = $this->input->post('length'); - $search = $this->input->post('search')['value'] ?? ''; + $this->activityQuery($search); + $recordsFiltered = (int) $this->db->count_all_results(); - // ========================= - // 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->activityQuery($search); $this->db->order_by('activity_logs.id', 'DESC'); - - // ========================= - // LIMIT - // ========================= - if ($length != -1) { - $this->db->limit($length, $start); + if ($length !== -1) { + $this->db->limit(max(1, $length), $start); } + $rows = $this->db->get()->result(); - $query = $this->db->get(); - $data = $query->result(); - - // ========================= - // TOTAL RECORD - // ========================= - $recordsTotal = $this->db->count_all('activity_logs'); - - // ========================= - // FORMAT OUTPUT - // ========================= - $result = []; - - foreach ($data as $row) { - $result[] = [ - 'created_at' => $row->created_at, - 'nama' => $row->nama ?? 'System', - 'module' => $row->module, - 'action' => $row->action, + $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 - ]; + 'status' => $row->status, + 'ip_address' => $row->ip_address, + ); } - echo json_encode([ - "draw" => intval($draw), - "recordsTotal" => $recordsTotal, - "recordsFiltered" => $recordsFiltered, - "data" => $result - ]); + return $this->json(array( + 'draw' => $draw, + 'recordsTotal' => $recordsTotal, + 'recordsFiltered' => $recordsFiltered, + 'data' => $result, + )); } public function get_chart() { - $year = $this->input->get('year'); - - // 🔥 DEFAULT: tahun berjalan - if (empty($year)) { - $year = date('Y'); + $year = (int) $this->input->get('year'); + if ($year < 2000 || $year > 2200) { + $year = (int) date('Y'); } - $this->db->select(' - journals.tanggal, - accounts.tipe, - SUM(journal_details.debit) as total_debit, - SUM(journal_details.kredit) as total_kredit - '); - $this->db->from('journal_details'); - $this->db->join('journals', 'journals.id = journal_details.journal_id', 'left'); - $this->db->join('accounts', 'accounts.id = journal_details.account_id', 'left'); - $this->db->where_in('journals.status', array('posted', 'reversed')); - $this->db->where('accounts.kategori', 'laba_rugi'); - $this->db->where('accounts.is_active', 1); + $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(); - // 🔥 pakai tahun default / filter - $this->db->where('YEAR(journals.tanggal)', $year); - - $this->db->group_by(['journals.tanggal', 'accounts.tipe']); - $this->db->order_by('journals.tanggal', 'ASC'); - - $rows = $this->db->get()->result(); - - $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); + $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; } } - $labels = []; - $revenue = []; - $expense = []; - - 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, + return $this->json(array( + 'year' => $year, + 'labels' => $months, 'revenue' => $revenue, - 'expense' => $expense - ]); + 'expense' => $expense, + )); } public function get_years() { - $this->db->select('DISTINCT YEAR(tanggal) as year'); - $this->db->from('journals'); - $this->db->where_in('status', array('posted', 'reversed')); - $this->db->order_by('year', 'DESC'); - - $data = $this->db->get()->result(); - - echo json_encode($data); + $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_tempo0.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)); + } } diff --git a/application/views/auth/login.php b/application/views/auth/login.php index 3a477a2..3c16b95 100644 --- a/application/views/auth/login.php +++ b/application/views/auth/login.php @@ -43,7 +43,8 @@ $authNotice = trim((string) $this->session->flashdata('auth_notice')); background: radial-gradient(circle at 8% 12%, rgba(255, 138, 0, .13), transparent 28rem), 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; } @@ -145,17 +146,17 @@ $authNotice = trim((string) $this->session->flashdata('auth_notice')); max-width: 470px; margin: 0; color: #fff; - font-size: clamp(35px, 4.3vw, 54px); - line-height: 1.08; - letter-spacing: -.045em; + font-size: clamp(27px, 2.6vw, 32px); + line-height: 1.2; + letter-spacing: -.035em; } .brand-copy p { max-width: 450px; margin: 22px 0 0; color: rgba(255, 255, 255, .72); - font-size: 16px; - line-height: 1.75; + font-size: 13px; + line-height: 1.7; } .brand-footer { @@ -181,7 +182,7 @@ $authNotice = trim((string) $this->session->flashdata('auth_notice')); .form-heading h2 { margin: 0; color: var(--navy-950); - font-size: 31px; + font-size: 22px; line-height: 1.2; letter-spacing: -.03em; } @@ -189,7 +190,7 @@ $authNotice = trim((string) $this->session->flashdata('auth_notice')); .form-heading p { margin: 10px 0 0; color: var(--slate-500); - font-size: 14px; + font-size: 13px; 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; } .form-panel { align-items: flex-start; padding: 30px 22px; } .mobile-logo { margin-bottom: 42px; } - .form-heading h2 { font-size: 28px; } + .form-heading h2 { font-size: 21px; } } @media (prefers-reduced-motion: reduce) { diff --git a/application/views/dashboard/layout.php b/application/views/dashboard/layout.php index 0425ef1..57621fe 100644 --- a/application/views/dashboard/layout.php +++ b/application/views/dashboard/layout.php @@ -1,849 +1,264 @@ -session->userdata('role'); ?> + -/* ====================================================== - DASHBOARD CLEAN - Orange Theme (Minimal & Elegant) -====================================================== */ -:root { - /* Orange Theme Colors */ - --orange-primary: #ff8c00; - --orange-light: #ffd4a3; - --orange-dark: #e67700; - --orange-gradient: linear-gradient(135deg, #ff8c00 0%, #ff7b00 50%, #e67700 100%); - - /* Neutral Colors */ - --white: #ffffff; - --light-bg: #f8fafc; - --border-light: #e2e8f0; - --text-dark: #1e293b; - --text-muted: #64748b; - - /* Shadows - Subtle */ - --shadow-sm: 0 2px 8px rgba(0,0,0,0.06); - --shadow-md: 0 8px 24px rgba(0,0,0,0.08); - --shadow-lg: 0 16px 40px rgba(0,0,0,0.10); -} +$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 'Belum ada pembanding'; + $positive = (float) $value >= 0; + $good = $inverse ? !$positive : $positive; + return ' ' . number_format(abs((float) $value), 1, ',', '.') + . '% dari tahun lalu'; +}; +?> + -/* Modern Card - Clean & Minimal */ -.modern-card { - background: var(--white); - border: 1px solid var(--border-light); - border-radius: 16px; - box-shadow: var(--shadow-sm); - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.modern-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 3px; - background: var(--orange-gradient); - transform: scaleX(0); - transition: transform 0.3s ease; -} - -.modern-card:hover { - box-shadow: var(--shadow-lg); - transform: translateY(-2px); -} - -.modern-card:hover::before { - transform: scaleX(1); -} - -/* ====================================================== - SUMMARY CARDS - Orange Accents -====================================================== */ -.summary-card { - background: var(--white); - border: 1px solid var(--border-light); - border-radius: 16px; - padding: 24px; - transition: all 0.3s ease; - cursor: pointer; - position: relative; -} - -.summary-card:hover { - box-shadow: var(--shadow-md); - transform: translateY(-4px); -} - -.summary-icon { - width: 60px; - height: 60px; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - font-size: 22px; - color: var(--white); - flex-shrink: 0; - box-shadow: 0 6px 20px rgba(255,140,0,0.3); - transition: all 0.3s ease; -} - -/* Orange-based icon colors - subtle variations */ -.summary-card:nth-child(1) .summary-icon { background: var(--orange-gradient); } -.summary-card:nth-child(2) .summary-icon { - background: linear-gradient(135deg, var(--orange-light) 0%, var(--orange-primary) 100%); -} -.summary-card:nth-child(3) .summary-icon { - background: linear-gradient(135deg, #ff9500 0%, var(--orange-primary) 100%); -} -.summary-card:nth-child(4) .summary-icon { - background: linear-gradient(135deg, var(--orange-dark) 0%, #d66a00 100%); -} -.summary-card:nth-child(5) .summary-icon { - background: linear-gradient(135deg, #ffb366 0%, var(--orange-primary) 100%); -} -.summary-card:nth-child(6) .summary-icon { - background: linear-gradient(135deg, #ffcc80 0%, #ffad42 100%); -} -.summary-card:nth-child(7) .summary-icon { - background: linear-gradient(135deg, #ffd180 0%, #ffab40 100%); -} - -/* Quick Action Cards */ -.quick-action .summary-card { - text-decoration: none; - color: inherit; - transition: all 0.3s ease; -} - -.quick-action .summary-card:hover .summary-icon { - transform: scale(1.1); - box-shadow: 0 10px 30px rgba(255,140,0,0.4); -} - -/* ====================================================== - AI INSIGHT - Clean Cards -====================================================== */ -.ai-box { - display: flex; - flex-direction: column; - gap: 12px; - max-height: 340px; - overflow-y: auto; -} - -.ai-box::-webkit-scrollbar { - width: 4px; -} - -.ai-box::-webkit-scrollbar-track { - background: #f1f5f9; - border-radius: 10px; -} - -.ai-box::-webkit-scrollbar-thumb { - background: var(--orange-light); - border-radius: 10px; -} - -.ai-item { - padding: 16px 20px; - border-radius: 12px; - font-size: 14px; - line-height: 1.6; - border-left: 4px solid var(--orange-primary); - background: #fef7ee; - box-shadow: var(--shadow-sm); - transition: all 0.2s ease; - border: 1px solid #fee2c7; -} - -.ai-item:hover { - box-shadow: var(--shadow-md); - transform: translateX(4px); -} - -.ai-item i { - font-size: 16px; - margin-top: 1px; - flex-shrink: 0; - opacity: 0.9; - color: var(--orange-primary); - margin-right: 12px; -} - -.ai-success { - border-left-color: #10b981; - background: #f0fdf4; - border-color: #bbf7d0; -} - -.ai-success i { - color: #10b981; -} - -.ai-warning { - border-left-color: #f59e0b; - background: #fef3c7; - border-color: #fed7aa; -} - -.ai-warning i { - color: #f59e0b; -} - -.ai-info { - border-left-color: #3b82f6; - background: #eff6ff; - border-color: #bfdbfe; -} - -.ai-info i { - color: #3b82f6; -} - -/* ====================================================== - TABLE CLEAN & MODERN -====================================================== */ -.modern-table-dashboard { - border-radius: 12px; - overflow: hidden; - box-shadow: var(--shadow-sm); - border: none; - background: var(--white); -} - -.modern-table-dashboard thead th { - background: linear-gradient(135deg, #fef7ee 0%, #fff8f0 100%); - border: none; - padding: 18px 16px; - font-weight: 600; - color: var(--text-dark); - font-size: 0.95rem; -} - -.modern-table-dashboard tbody tr { - transition: all 0.2s ease; - border: none; -} - -.modern-table-dashboard tbody tr:hover { - background: rgba(255,140,0,0.04); -} - -.modern-table-dashboard td { - padding: 18px 16px; - border-color: #f8fafc; - vertical-align: middle; -} - -/* Modern Badges */ -.badge { - padding: 8px 16px; - border-radius: 20px; - font-weight: 600; - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.3px; - box-shadow: var(--shadow-sm); -} - -/* ====================================================== - SUBTLE ANIMATIONS -====================================================== */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.modern-card, -.summary-card { - animation: fadeInUp 0.6s ease forwards; -} - -.modern-card:nth-child(1) { animation-delay: 0.1s; } -.modern-card:nth-child(2) { animation-delay: 0.15s; } -.modern-card:nth-child(3) { animation-delay: 0.2s; } -.modern-card:nth-child(4) { animation-delay: 0.25s; } - -/* ====================================================== - RESPONSIVE - Clean Mobile -====================================================== */ -@media (max-width: 768px) { - .summary-card { - padding: 20px; - margin-bottom: 20px; - } - - .summary-icon { - width: 52px; - height: 52px; - font-size: 20px; - } - - .ai-item { - padding: 14px 16px; - font-size: 13.5px; - } - - .modern-table-dashboard thead th, - .modern-table-dashboard td { - padding: 14px 12px; - font-size: 0.9rem; - } - - .modern-card, - .summary-card { - margin-bottom: 16px; - } -} - -/* Button enhancements */ -.btn-outline-primary { - border-color: var(--orange-primary); - color: var(--orange-primary); -} - -.btn-outline-primary:hover { - background: var(--orange-primary); - border-color: var(--orange-primary); - color: white; -} - -/* Chart container */ -.chart-container { - height: 300px; - position: relative; -} - - - -
- - - -
-
-
-
-
-
-
Rp
- Total Pemasukan -
-
+
+
+
+ Ringkasan perusahaan +

session->userdata('nama') ?: 'Pengguna')) ?>

+

Pantau kondisi keuangan dan pekerjaan yang membutuhkan perhatian dalam satu tampilan.

-
- -
-
-
-
-
-
Rp
- Total Pengeluaran -
-
+
+ + Periode aktif
-
+ -
-
-
-
-
-
Rp
- Keuntungan Bersih -
-
+ +
+
+
Tahun
+ Pendapatan + + +
+
+
Tahun
+ Pengeluaran + + +
+
+
Tahun
+ Laba Bersih + + +
+
-
+ -
- - -
- - -
- -
-
- -
- -
- -
- -
- Neraca -
- - - Laporan posisi keuangan perusahaan - - -
- -
- -
- -
-
+
- - - - - - -
- -
-
-
-
-
-

Ringkasan Keuangan

- Performa keuangan -
-
- -
-
-
- -
-
-
-
- - -
-
-
- - -
-
- -
-
-
AI Insight
- Rekomendasi otomatis sistem -
-
- - -
- - - -
- -
Belum ada insight hari ini
-
- - - - - - severity == 'critical'){ - $icon = 'bi-x-octagon-fill'; - $colorClass = 'ai-danger'; - } elseif($ai->severity == 'warning'){ - $icon = 'bi-exclamation-triangle-fill'; - $colorClass = 'ai-warning'; - } elseif($ai->severity == 'info'){ - $icon = 'bi-info-circle-fill'; - $colorClass = 'ai-info'; - } elseif($ai->type == 'stock'){ - $icon = 'bi-box-seam-fill'; - $colorClass = 'ai-stock'; - } elseif($ai->type == 'finance'){ - $icon = 'bi-cash-coin'; - $colorClass = 'ai-finance'; - } - ?> - -
- -
- - - -
-
- message; ?> -
- - meta)): ?> -
- - detail tambahan tersedia -
- -
- -
- -
- - - - - -
-
-
-
-
- - - - -
-
-
-
-
-
-

Aktivitas Terbaru

- Aktivitas terakhir di sistem -
- - Lihat Semua + - -
- - - - - - - - - - - -
WaktuModulKeteranganPenggunaStatus
-
- +
-
+ + + +
+
+
+

Performa Keuangan

Perbandingan pendapatan dan pengeluaran per bulan.

+ +
+
+
+ +
+

Perlu Perhatian

Prioritas operasional saat ini.

+ +
-
-
+
+
+
+

Aktivitas Terbaru

Jejak aktivitas terakhir yang tercatat di sistem.

+ Lihat semua +
+
+ + + +
WaktuModulKeteranganPenggunaStatus
Memuat aktivitas...
+
+
- +
+

Insight Sistem

Informasi penting periode berjalan.

+
+ +
Belum ada insight baruSistem belum menemukan hal yang perlu diinformasikan.
+ + severity, array('critical', 'warning', 'info'), true) ? $insight->severity : 'info'; ?> +
+ + message) ?> +
+ + +
+
+
+ + - - \ No newline at end of file + document.getElementById('yearFilter').addEventListener('change', function () { loadChart(this.value); }); + loadYears(); + loadChart(currentYear); + loadActivity(); +}()); + diff --git a/application/views/inventory_professional/_module_nav.php b/application/views/inventory_professional/_module_nav.php index b7ad1ff..de407f1 100644 --- a/application/views/inventory_professional/_module_nav.php +++ b/application/views/inventory_professional/_module_nav.php @@ -1,6 +1,6 @@
-
PERSEDIAAN TERINTEGRASI

+
PERSEDIAAN TERINTEGRASI

Scan Barcode