|
|
|
|
|
const state = { |
|
|
currentModule: 'dashboard', |
|
|
currentSubModule: null, |
|
|
locationFilter: 'all', |
|
|
data: { |
|
|
financeAccounting: { |
|
|
accountsPayable: { |
|
|
invoices: [ |
|
|
{ invoice_id: 'INV-AP-001', vendor: 'PT Pakan Berkah', vendor_id: 'VEN-001', po_number: 'PO-2025-001', invoice_no: 'INV/PKB/2025/001', invoice_date: '2025-11-01', due_date: '2025-11-15', amount: 750000000, currency: 'IDR', status: 'Approved', payment_status: 'Unpaid', grn_matched: true }, |
|
|
{ invoice_id: 'INV-AP-002', vendor: 'CV Ternak Maju', vendor_id: 'VEN-002', po_number: 'PO-2025-002', invoice_no: 'INV/TM/2025/002', invoice_date: '2025-11-03', due_date: '2025-11-17', amount: 4200000000, currency: 'IDR', status: 'In Transit', payment_status: 'Unpaid', grn_matched: false }, |
|
|
{ invoice_id: 'INV-AP-003', vendor: 'PT Medika Veteriner', vendor_id: 'VEN-003', po_number: 'PO-2025-003', invoice_no: 'INV/MV/2025/003', invoice_date: '2025-10-28', due_date: '2025-11-11', amount: 125000000, currency: 'IDR', status: 'Received', payment_status: 'Unpaid', grn_matched: true }, |
|
|
{ invoice_id: 'INV-AP-004', vendor: 'PT Pakan Berkah', vendor_id: 'VEN-001', po_number: 'PO-2025-004', invoice_no: 'INV/PKB/2025/004', invoice_date: '2025-10-20', due_date: '2025-11-03', amount: 450000000, currency: 'IDR', status: 'Received', payment_status: 'Paid', grn_matched: true, paid_date: '2025-11-02' }, |
|
|
{ invoice_id: 'INV-AP-005', vendor: 'CV Ternak Maju', vendor_id: 'VEN-002', po_number: 'PO-2025-005', invoice_no: 'INV/TM/2025/005', invoice_date: '2025-09-15', due_date: '2025-09-29', amount: 320000000, currency: 'IDR', status: 'Received', payment_status: 'Overdue', grn_matched: true, days_overdue: 41 } |
|
|
], |
|
|
summary: { total_payable: 5845000000, paid: 450000000, unpaid: 5395000000, overdue: 320000000 } |
|
|
}, |
|
|
accountsReceivable: { |
|
|
invoices: [ |
|
|
{ invoice_id: 'INV-AR-001', customer: 'PT Daging Prima', customer_id: 'CUST-001', invoice_no: 'INV/DP/2025/001', invoice_date: '2025-11-01', due_date: '2025-11-15', amount: 3200000000, currency: 'IDR', status: 'Delivered', payment_status: 'Unpaid', days_outstanding: 9 }, |
|
|
{ invoice_id: 'INV-AR-002', customer: 'CV Karkas Jaya', customer_id: 'CUST-002', invoice_no: 'INV/KJ/2025/002', invoice_date: '2025-10-28', due_date: '2025-11-11', amount: 2100000000, currency: 'IDR', status: 'Delivered', payment_status: 'Unpaid', days_outstanding: 12 }, |
|
|
{ invoice_id: 'INV-AR-003', customer: 'PT Daging Prima', customer_id: 'CUST-001', invoice_no: 'INV/DP/2025/003', invoice_date: '2025-10-15', due_date: '2025-10-29', amount: 1800000000, currency: 'IDR', status: 'Delivered', payment_status: 'Paid', paid_date: '2025-10-28' }, |
|
|
{ invoice_id: 'INV-AR-004', customer: 'CV Karkas Jaya', customer_id: 'CUST-002', invoice_no: 'INV/KJ/2025/004', invoice_date: '2025-09-20', due_date: '2025-10-04', amount: 1600000000, currency: 'IDR', status: 'Delivered', payment_status: 'Overdue', days_overdue: 37 } |
|
|
], |
|
|
summary: { total_receivable: 8700000000, collected: 1800000000, outstanding: 6900000000, overdue: 1600000000, dso: 42 } |
|
|
}, |
|
|
assets: { |
|
|
fixedAssets: [ |
|
|
{ asset_id: 'ASSET-001', name: 'Gudang Pakan Lampung', category: 'Building', location: 'Lampung Selatan', acquisition_date: '2020-03-15', original_cost: 5000000000, useful_life: 20, accumulated_depreciation: 1250000000, book_value: 3750000000, status: 'Active', depreciation_method: 'Straight Line' }, |
|
|
{ asset_id: 'ASSET-002', name: 'Kandang Utama Lampung', category: 'Building', location: 'Lampung Selatan', acquisition_date: '2019-06-10', original_cost: 8000000000, useful_life: 20, accumulated_depreciation: 2400000000, book_value: 5600000000, status: 'Active', depreciation_method: 'Straight Line' }, |
|
|
{ asset_id: 'ASSET-003', name: 'Forklift Hidraulis #1', category: 'Equipment', location: 'Lampung Selatan', acquisition_date: '2021-08-20', original_cost: 450000000, useful_life: 10, accumulated_depreciation: 67500000, book_value: 382500000, status: 'Active', depreciation_method: 'Straight Line' }, |
|
|
{ asset_id: 'ASSET-004', name: 'Skala Digital Otomatis', category: 'Equipment', location: 'Medan', acquisition_date: '2022-02-14', original_cost: 185000000, useful_life: 5, accumulated_depreciation: 55500000, book_value: 129500000, status: 'Active', depreciation_method: 'Straight Line' }, |
|
|
{ asset_id: 'ASSET-005', name: 'Kendaraan Dinas Pickup #1', category: 'Vehicle', location: 'Lampung Selatan', acquisition_date: '2021-11-05', original_cost: 320000000, useful_life: 5, accumulated_depreciation: 96000000, book_value: 224000000, status: 'Active', depreciation_method: 'Straight Line' } |
|
|
], |
|
|
summary: { total_assets: 16086000000, total_depreciation: 3869000000, net_book_value: 10386000000 } |
|
|
}, |
|
|
cashBank: { |
|
|
bankAccounts: [ |
|
|
{ account_id: 'BANK-001', bank_name: 'Bank BCA', account_number: '0012345678', account_type: 'Checking', balance: 5200000000, currency: 'IDR', status: 'Active', last_reconciled: '2025-11-09' }, |
|
|
{ account_id: 'BANK-002', bank_name: 'Bank Mandiri', account_number: '0087654321', account_type: 'Savings', balance: 2500000000, currency: 'IDR', status: 'Active', last_reconciled: '2025-11-09' }, |
|
|
{ account_id: 'BANK-003', bank_name: 'Bank BNI', account_number: '0011223344', account_type: 'Checking', balance: 1000000000, currency: 'IDR', status: 'Active', last_reconciled: '2025-11-09' } |
|
|
], |
|
|
cash_on_hand: 50000000, |
|
|
total_liquid_assets: 8750000000 |
|
|
}, |
|
|
budgeting: { |
|
|
budgets: [ |
|
|
{ budget_id: 'BUD-2025-001', department: 'Operations - Lampung', category: 'Feed Cost', budgeted_amount: 15000000000, spent_amount: 12450000000, remaining: 2550000000, variance_percent: -17.0, status: 'On Track' }, |
|
|
{ budget_id: 'BUD-2025-002', department: 'Operations - Medan', category: 'Feed Cost', budgeted_amount: 8000000000, spent_amount: 6200000000, remaining: 1800000000, variance_percent: -22.5, status: 'On Track' }, |
|
|
{ budget_id: 'BUD-2025-003', department: 'Animal Health', category: 'Veterinary & Drugs', budgeted_amount: 2000000000, spent_amount: 1850000000, remaining: 150000000, variance_percent: -7.5, status: 'Caution' }, |
|
|
{ budget_id: 'BUD-2025-004', department: 'HR & Payroll', category: 'Employee Salaries', budgeted_amount: 5400000000, spent_amount: 5400000000, remaining: 0, variance_percent: 0, status: 'Fully Spent' }, |
|
|
{ budget_id: 'BUD-2025-005', department: 'Logistics', category: 'Transportation', budgeted_amount: 1200000000, spent_amount: 540000000, remaining: 660000000, variance_percent: -55.0, status: 'On Track' } |
|
|
], |
|
|
summary: { total_budget: 31600000000, total_spent: 26440000000, total_remaining: 5160000000, utilization_percent: 83.7 } |
|
|
}, |
|
|
generalLedger: { |
|
|
accounts: [ |
|
|
{ account_code: '1100', account_name: 'Cash & Bank', account_type: 'Asset', balance: 8750000000 }, |
|
|
{ account_code: '1200', account_name: 'Accounts Receivable', account_type: 'Asset', balance: 6900000000 }, |
|
|
{ account_code: '1300', account_name: 'Inventory', account_type: 'Asset', balance: 18500000000 }, |
|
|
{ account_code: '1500', account_name: 'Fixed Assets (Net)', account_type: 'Asset', balance: 10386000000 }, |
|
|
{ account_code: '2100', account_name: 'Accounts Payable', account_type: 'Liability', balance: 5395000000 }, |
|
|
{ account_code: '2200', account_name: 'Short-term Loans', account_type: 'Liability', balance: 5000000000 }, |
|
|
{ account_code: '3100', account_name: 'Equity', account_type: 'Equity', balance: 45236000000 } |
|
|
] |
|
|
}, |
|
|
reports: { |
|
|
incomeStatement: { period: 'Nov 1 - Nov 9, 2025', revenue: 12100000000, cogs: 8470000000, gross_profit: 3630000000, gross_margin_percent: 30.0, operating_expenses: 1820000000, operating_profit: 1810000000, net_profit: 1627750000 }, |
|
|
balanceSheet: { period: '2025-11-09', total_assets: 44536000000, total_liabilities: 10395000000, total_equity: 45236000000 } |
|
|
} |
|
|
}, |
|
|
livestock: [ |
|
|
{ id: 'C001', tag: 'RFID-8450', breed: 'Brahman', weight: 425, age_months: 18, location: 'Lampung-Pen A1', health: 'Good', adg: 1.32 }, |
|
|
{ id: 'C002', tag: 'RFID-8451', breed: 'Simmental', weight: 398, age_months: 16, location: 'Lampung-Pen A1', health: 'Good', adg: 1.28 }, |
|
|
{ id: 'C003', tag: 'RFID-8452', breed: 'Limousin', weight: 412, age_months: 17, location: 'Lampung-Pen A2', health: 'Under Treatment', adg: 0.95 }, |
|
|
{ id: 'C004', tag: 'RFID-8453', breed: 'Brahman', weight: 445, age_months: 19, location: 'Medan-Pen B1', health: 'Good', adg: 1.41 }, |
|
|
{ id: 'C005', tag: 'RFID-8454', breed: 'Angus Cross', weight: 388, age_months: 15, location: 'Medan-Pen B1', health: 'Good', adg: 1.22 } |
|
|
], |
|
|
inventory: [ |
|
|
{ sku: 'FEED-001', name: 'Jagung Giling', category: 'Pakan', quantity: 45000, unit: 'kg', location: 'Lampung-WH1', reorder_point: 20000 }, |
|
|
{ sku: 'FEED-002', name: 'Konsentrat Protein', category: 'Pakan', quantity: 12000, unit: 'kg', location: 'Lampung-WH1', reorder_point: 5000 }, |
|
|
{ sku: 'MED-001', name: 'Vaksin PMK', category: 'Obat', quantity: 450, unit: 'dosis', location: 'Med Storage', reorder_point: 200 }, |
|
|
{ sku: 'MED-002', name: 'Antibiotik Broad Spectrum', category: 'Obat', quantity: 85, unit: 'botol', location: 'Med Storage', reorder_point: 50 }, |
|
|
{ sku: 'SUPP-001', name: 'Vitamin Premix', category: 'Suplemen', quantity: 2400, unit: 'kg', location: 'Lampung-WH1', reorder_point: 1000 } |
|
|
], |
|
|
purchaseOrders: [ |
|
|
{ po_no: 'PO-2025-001', vendor: 'PT Pakan Berkah', item: 'Jagung Giling', quantity: 50000, unit: 'kg', value: 'IDR 750M', status: 'Approved', delivery_date: '2025-11-15' }, |
|
|
{ po_no: 'PO-2025-002', vendor: 'CV Ternak Maju', item: 'Sapi Bakalan', quantity: 200, unit: 'ekor', value: 'IDR 4.2B', status: 'In Transit', delivery_date: '2025-11-12' }, |
|
|
{ po_no: 'PO-2025-003', vendor: 'PT Medika Veteriner', item: 'Vaksin & Obat', quantity: 1, unit: 'paket', value: 'IDR 125M', status: 'Pending', delivery_date: '2025-11-20' } |
|
|
], |
|
|
employees: [ |
|
|
{ emp_id: 'EMP-001', name: 'Budi Santoso', position: 'Farm Manager', location: 'Lampung', department: 'Operations', status: 'Active' }, |
|
|
{ emp_id: 'EMP-002', name: 'Dr. Siti Rahayu', position: 'Veterinarian', location: 'Lampung', department: 'Animal Health', status: 'Active' }, |
|
|
{ emp_id: 'EMP-003', name: 'Ahmad Fauzi', position: 'Warehouse Supervisor', location: 'Lampung', department: 'Logistics', status: 'Active' }, |
|
|
{ emp_id: 'EMP-004', name: 'Dewi Lestari', position: 'Finance Manager', location: 'Jakarta', department: 'Finance', status: 'Active' }, |
|
|
{ emp_id: 'EMP-005', name: 'Rudi Hermawan', position: 'Procurement Officer', location: 'Jakarta', department: 'Procurement', status: 'Active' } |
|
|
], |
|
|
healthRecords: [ |
|
|
{ mrn: 'MRN-00123', cattle_id: 'C003', date: '2025-11-08', diagnosis: 'Mild Respiratory Infection', treatment: 'Antibiotik 10ml IM', status: 'Under Treatment', vet: 'Dr. Siti Rahayu' }, |
|
|
{ mrn: 'MRN-00124', cattle_id: 'C027', date: '2025-11-09', diagnosis: 'Routine Vaccination', treatment: 'Vaksin PMK', status: 'Completed', vet: 'Dr. Siti Rahayu' }, |
|
|
{ mrn: 'MRN-00125', cattle_id: 'C089', date: '2025-11-09', diagnosis: 'Lameness', treatment: 'Anti-inflammatory, Rest', status: 'Under Observation', vet: 'Dr. Ahmad Kusuma' } |
|
|
], |
|
|
vendors: [ |
|
|
{ vendor_id: 'VEN-001', name: 'PT Pakan Berkah', category: 'Feed Supplier', otif: '94.5%', rating: 4.5 }, |
|
|
{ vendor_id: 'VEN-002', name: 'CV Ternak Maju', category: 'Livestock Supplier', otif: '92.0%', rating: 4.2 }, |
|
|
{ vendor_id: 'VEN-003', name: 'PT Medika Veteriner', category: 'Medical Supplies', otif: '96.8%', rating: 4.8 } |
|
|
], |
|
|
customers: [ |
|
|
{ customer_id: 'CUST-001', name: 'PT Daging Prima', type: 'Butcher/Processor', location: 'Jakarta', total_orders: 24, lifetime_value: 'IDR 12.5B' }, |
|
|
{ customer_id: 'CUST-002', name: 'CV Karkas Jaya', type: 'Distributor', location: 'Surabaya', total_orders: 18, lifetime_value: 'IDR 8.2B' } |
|
|
] |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
function init() { |
|
|
setupEventListeners(); |
|
|
loadModule('dashboard'); |
|
|
updateLastSync(); |
|
|
setInterval(updateLastSync, 60000); |
|
|
} |
|
|
|
|
|
|
|
|
function setupEventListeners() { |
|
|
|
|
|
document.getElementById('menuToggle').addEventListener('click', () => { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const mainContent = document.getElementById('mainContent'); |
|
|
sidebar.classList.toggle('collapsed'); |
|
|
mainContent.classList.toggle('expanded'); |
|
|
}); |
|
|
|
|
|
|
|
|
const logoLink = document.querySelector('.logo-link'); |
|
|
if (logoLink) { |
|
|
logoLink.addEventListener('click', (e) => { |
|
|
e.preventDefault(); |
|
|
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active')); |
|
|
document.querySelector('.nav-item[data-module="dashboard"]').classList.add('active'); |
|
|
loadModule('dashboard'); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
document.querySelectorAll('.nav-item').forEach(item => { |
|
|
item.addEventListener('click', (e) => { |
|
|
e.preventDefault(); |
|
|
const module = item.dataset.module; |
|
|
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active')); |
|
|
item.classList.add('active'); |
|
|
loadModule(module); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('locationFilter').addEventListener('change', (e) => { |
|
|
state.locationFilter = e.target.value; |
|
|
loadModule(state.currentModule); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('modalClose').addEventListener('click', closeModal); |
|
|
document.getElementById('modal').addEventListener('click', (e) => { |
|
|
if (e.target.id === 'modal') closeModal(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function loadModule(module) { |
|
|
state.currentModule = module; |
|
|
const content = document.getElementById('mainContent'); |
|
|
|
|
|
switch(module) { |
|
|
case 'dashboard': |
|
|
content.innerHTML = renderDashboard(); |
|
|
initDashboardCharts(); |
|
|
break; |
|
|
case 'finance': |
|
|
content.innerHTML = renderFinance(); |
|
|
initFinanceCharts(); |
|
|
break; |
|
|
case 'inventory': |
|
|
content.innerHTML = renderInventory(); |
|
|
break; |
|
|
case 'warehouse': |
|
|
content.innerHTML = renderWarehouse(); |
|
|
break; |
|
|
case 'procurement': |
|
|
content.innerHTML = renderProcurement(); |
|
|
break; |
|
|
case 'hr': |
|
|
content.innerHTML = renderHR(); |
|
|
break; |
|
|
case 'crm': |
|
|
content.innerHTML = renderCRM(); |
|
|
initCRMCharts(); |
|
|
break; |
|
|
case 'scm': |
|
|
content.innerHTML = renderSCM(); |
|
|
break; |
|
|
case 'feedlot': |
|
|
content.innerHTML = renderFeedlot(); |
|
|
initFeedlotCharts(); |
|
|
break; |
|
|
case 'bi': |
|
|
content.innerHTML = renderBI(); |
|
|
initBICharts(); |
|
|
break; |
|
|
} |
|
|
|
|
|
setupModuleListeners(); |
|
|
} |
|
|
|
|
|
|
|
|
function renderDashboard() { |
|
|
return ` |
|
|
<div class="breadcrumb"> |
|
|
<a href="#">Home</a> / <span>Dashboard</span> |
|
|
</div> |
|
|
|
|
|
<div class="page-header"> |
|
|
<h1 class="page-title">Dashboard</h1> |
|
|
<p class="page-subtitle">Welcome back! Here's what's happening with your business today.</p> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Revenue</span> |
|
|
<span class="stat-icon">💰</span> |
|
|
</div> |
|
|
<div class="stat-value">IDR 45.2B</div> |
|
|
<div class="stat-change">+12.5% from last month</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Expenses</span> |
|
|
<span class="stat-icon">📊</span> |
|
|
</div> |
|
|
<div class="stat-value">IDR 32.8B</div> |
|
|
<div class="stat-change">+8.3% from last month</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Net Profit</span> |
|
|
<span class="stat-icon">📈</span> |
|
|
</div> |
|
|
<div class="stat-value">IDR 12.4B</div> |
|
|
<div class="stat-change">+24.1% from last month</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Cattle</span> |
|
|
<span class="stat-icon">🐄</span> |
|
|
</div> |
|
|
<div class="stat-value">8,450</div> |
|
|
<div class="stat-change">+234 this month</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Active Employees</span> |
|
|
<span class="stat-icon">👥</span> |
|
|
</div> |
|
|
<div class="stat-value">342</div> |
|
|
<div class="stat-change">+8 new hires</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Stock Value</span> |
|
|
<span class="stat-icon">📦</span> |
|
|
</div> |
|
|
<div class="stat-value">IDR 18.5B</div> |
|
|
<div class="stat-change">+5.2% from last week</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Health Alerts</h3> |
|
|
<button class="btn btn-secondary btn-sm" onclick="loadModule('feedlot')">View All</button> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;"> |
|
|
<span style="font-size: 20px;">🚨</span> |
|
|
<div style="flex: 1;"> |
|
|
<strong>Sapi C089 dalam perawatan (Medan)</strong> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">2025-11-10 14:30 - High severity</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;"> |
|
|
<span style="font-size: 20px;">⚠️</span> |
|
|
<div style="flex: 1;"> |
|
|
<strong>Feed konsentrat di bawah reorder point</strong> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">2025-11-10 13:45 - Medium severity</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px;"> |
|
|
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;"> |
|
|
<span style="font-size: 20px;">💰</span> |
|
|
<div style="flex: 1;"> |
|
|
<strong>AP Rp 320M overdue (INV-AP-005)</strong> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">2025-11-10 09:00 - High severity</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-2"> |
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">Revenue Trend</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="revenueChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">Cattle Growth</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="cattleChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Recent Activity</h3> |
|
|
<button class="btn btn-secondary btn-sm">View All</button> |
|
|
</div> |
|
|
<div class="activity-feed"> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<strong>PO Created</strong> - PO-2025-006 created for PT Pakan Berkah |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">2025-11-10 10:15</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<strong>Payment Received</strong> - Rp 1.8B from PT Daging Prima (INV-AR-003) |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">2025-11-09 16:30</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<strong>Asset Maintenance</strong> - Forklift Hidraulis #1 scheduled maintenance |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">2025-11-08 08:00</div> |
|
|
</div> |
|
|
<div style="padding: 12px;"> |
|
|
<strong>New cattle shipment</strong> - 200 heads arrived at Medan facility |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">2025-11-07 14:20</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Quick Actions</h3> |
|
|
</div> |
|
|
<div class="grid-2"> |
|
|
<button class="btn btn-primary" onclick="loadModule('procurement'); showToast('Opening PO creation...')" style="padding: 20px; font-size: 16px;"> |
|
|
<span style="font-size: 24px; margin-right: 8px;">📋</span> Create Purchase Order |
|
|
</button> |
|
|
<button class="btn btn-primary" onclick="loadModule('inventory')" style="padding: 20px; font-size: 16px;"> |
|
|
<span style="font-size: 24px; margin-right: 8px;">📦</span> View Inventory |
|
|
</button> |
|
|
<button class="btn btn-primary" onclick="loadModule('finance'); state.currentSubModule='cashbank'; loadModule('finance');" style="padding: 20px; font-size: 16px;"> |
|
|
<span style="font-size: 24px; margin-right: 8px;">🏦</span> Check Cash Balance |
|
|
</button> |
|
|
<button class="btn btn-primary" onclick="loadModule('feedlot')" style="padding: 20px; font-size: 16px;"> |
|
|
<span style="font-size: 24px; margin-right: 8px;">🚨</span> View Health Alerts |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderGeneralLedger() { |
|
|
const accounts = state.data.financeAccounting.generalLedger.accounts; |
|
|
const totalAssets = accounts.filter(a => a.account_type === 'Asset').reduce((sum, a) => sum + a.balance, 0); |
|
|
const totalLiabilities = accounts.filter(a => a.account_type === 'Liability').reduce((sum, a) => sum + a.balance, 0); |
|
|
const totalEquity = accounts.filter(a => a.account_type === 'Equity').reduce((sum, a) => sum + a.balance, 0); |
|
|
|
|
|
return ` |
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Assets</span> |
|
|
<span class="stat-icon">📊</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(totalAssets)}</div> |
|
|
<div class="stat-change">${accounts.filter(a => a.account_type === 'Asset').length} accounts</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Liabilities</span> |
|
|
<span class="stat-icon">📉</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(totalLiabilities)}</div> |
|
|
<div class="stat-change">${accounts.filter(a => a.account_type === 'Liability').length} accounts</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Equity</span> |
|
|
<span class="stat-icon">💼</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(totalEquity)}</div> |
|
|
<div class="stat-change">${accounts.filter(a => a.account_type === 'Equity').length} accounts</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Current Ratio</span> |
|
|
<span class="stat-icon">📈</span> |
|
|
</div> |
|
|
<div class="stat-value">1.42</div> |
|
|
<div class="stat-change">Healthy position</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Chart of Accounts</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-secondary btn-sm">Export</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Account', 'Account creation form would appear here')">+ New Account</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Account Code</th> |
|
|
<th>Account Name</th> |
|
|
<th>Type</th> |
|
|
<th>Balance (IDR)</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${accounts.map(acc => ` |
|
|
<tr onclick="showModal('Account Details', 'Detailed transactions for ${acc.account_name} would appear here')"> |
|
|
<td><strong>${acc.account_code}</strong></td> |
|
|
<td>${acc.account_name}</td> |
|
|
<td><span class="status-badge status-info">${acc.account_type}</span></td> |
|
|
<td style="text-align: right;"><strong>${formatCurrency(acc.balance)}</strong></td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-2"> |
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Financial Ratios</h3> |
|
|
</div> |
|
|
<div style="padding: 20px; background: var(--color-bg-1); border-radius: 8px; margin-bottom: 16px;"> |
|
|
<div style="font-size: 14px; color: var(--color-text-secondary); margin-bottom: 8px;">Debt-to-Equity Ratio</div> |
|
|
<div style="font-size: 28px; font-weight: 700;">0.23</div> |
|
|
<div style="font-size: 12px; color: var(--color-success); margin-top: 4px;">✓ Low leverage</div> |
|
|
</div> |
|
|
<div style="padding: 20px; background: var(--color-bg-2); border-radius: 8px;"> |
|
|
<div style="font-size: 14px; color: var(--color-text-secondary); margin-bottom: 8px;">Working Capital</div> |
|
|
<div style="font-size: 28px; font-weight: 700;">${formatCurrency(totalAssets - totalLiabilities)}</div> |
|
|
<div style="font-size: 12px; color: var(--color-success); margin-top: 4px;">✓ Strong position</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Quick Stats</h3> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Total Accounts</span> |
|
|
<strong>${accounts.length}</strong> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Asset Accounts</span> |
|
|
<strong>${accounts.filter(a => a.account_type === 'Asset').length}</strong> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Liability Accounts</span> |
|
|
<strong>${accounts.filter(a => a.account_type === 'Liability').length}</strong> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px;"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Equity Accounts</span> |
|
|
<strong>${accounts.filter(a => a.account_type === 'Equity').length}</strong> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderAccountsPayable() { |
|
|
const ap = state.data.financeAccounting.accountsPayable; |
|
|
const invoices = ap.invoices; |
|
|
|
|
|
return ` |
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Payable</span> |
|
|
<span class="stat-icon">📤</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(ap.summary.total_payable)}</div> |
|
|
<div class="stat-change">${invoices.length} open invoices</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Unpaid</span> |
|
|
<span class="stat-icon">⏳</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(ap.summary.unpaid)}</div> |
|
|
<div class="stat-change">${invoices.filter(i => i.payment_status === 'Unpaid').length} invoices</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Overdue</span> |
|
|
<span class="stat-icon">🚨</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(ap.summary.overdue)}</div> |
|
|
<div class="stat-change class="negative">${invoices.filter(i => i.payment_status === 'Overdue').length} invoice overdue</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Paid This Month</span> |
|
|
<span class="stat-icon">✅</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(ap.summary.paid)}</div> |
|
|
<div class="stat-change">${invoices.filter(i => i.payment_status === 'Paid').length} invoice paid</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">AP Aging Analysis</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="apAgingChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Vendor Invoices</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-secondary btn-sm">Export</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Invoice', 'Vendor invoice entry form would appear here')">+ New Invoice</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Invoice ID</th> |
|
|
<th>Vendor</th> |
|
|
<th>PO Number</th> |
|
|
<th>Invoice Date</th> |
|
|
<th>Due Date</th> |
|
|
<th>Amount</th> |
|
|
<th>Status</th> |
|
|
<th>Payment</th> |
|
|
<th>Actions</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${invoices.map(inv => { |
|
|
const daysOverdue = inv.days_overdue || 0; |
|
|
const statusBadge = inv.payment_status === 'Paid' ? 'status-good' : inv.payment_status === 'Overdue' ? 'status-error' : 'status-warning'; |
|
|
return ` |
|
|
<tr onclick="showAPInvoiceDetail('${inv.invoice_id}')"> |
|
|
<td><strong>${inv.invoice_id}</strong></td> |
|
|
<td>${inv.vendor}</td> |
|
|
<td>${inv.po_number}</td> |
|
|
<td>${inv.invoice_date}</td> |
|
|
<td>${inv.due_date}</td> |
|
|
<td style="text-align: right;"><strong>${formatCurrency(inv.amount)}</strong></td> |
|
|
<td><span class="status-badge status-info">${inv.status}</span></td> |
|
|
<td><span class="status-badge ${statusBadge}">${inv.payment_status}${daysOverdue > 0 ? ` ⚠️` : ''}</span></td> |
|
|
<td> |
|
|
${inv.payment_status !== 'Paid' ? '<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); showModal(\'Record Payment\', \'Payment form for ' + inv.invoice_id + ' would appear here\')">Pay</button>' : ''} |
|
|
</td> |
|
|
</tr> |
|
|
`; |
|
|
}).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function initDashboardCharts() { |
|
|
|
|
|
const revenueCtx = document.getElementById('revenueChart'); |
|
|
if (revenueCtx) { |
|
|
new Chart(revenueCtx, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov'], |
|
|
datasets: [{ |
|
|
label: 'Revenue (Billion IDR)', |
|
|
data: [32, 35, 38, 36, 39, 41, 40, 42, 43, 44, 45.2], |
|
|
borderColor: '#1FB8CD', |
|
|
backgroundColor: 'rgba(31, 184, 205, 0.1)', |
|
|
tension: 0.4, |
|
|
fill: true |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { display: false } |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const cattleCtx = document.getElementById('cattleChart'); |
|
|
if (cattleCtx) { |
|
|
new Chart(cattleCtx, { |
|
|
type: 'bar', |
|
|
data: { |
|
|
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov'], |
|
|
datasets: [{ |
|
|
label: 'Total Cattle', |
|
|
data: [7200, 7350, 7500, 7680, 7820, 7950, 8100, 8200, 8300, 8400, 8450], |
|
|
backgroundColor: '#FFC185' |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { display: false } |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function renderFinance() { |
|
|
state.currentSubModule = state.currentSubModule || 'gl'; |
|
|
const subModule = state.currentSubModule; |
|
|
return ` |
|
|
<div class="breadcrumb"> |
|
|
<a href="#">Home</a> / <span>Finance & Accounting</span> |
|
|
</div> |
|
|
|
|
|
<div class="page-header"> |
|
|
<h1 class="page-title">Finance & Accounting</h1> |
|
|
<p class="page-subtitle">Manage your financial operations and reporting</p> |
|
|
</div> |
|
|
|
|
|
<div class="sub-nav"> |
|
|
<button class="sub-nav-item ${subModule === 'gl' ? 'active' : ''}" data-subnav="gl">General Ledger</button> |
|
|
<button class="sub-nav-item ${subModule === 'ap' ? 'active' : ''}" data-subnav="ap">Accounts Payable</button> |
|
|
<button class="sub-nav-item ${subModule === 'ar' ? 'active' : ''}" data-subnav="ar">Accounts Receivable</button> |
|
|
<button class="sub-nav-item ${subModule === 'assets' ? 'active' : ''}" data-subnav="assets">Assets</button> |
|
|
<button class="sub-nav-item ${subModule === 'cashbank' ? 'active' : ''}" data-subnav="cashbank">Cash & Bank</button> |
|
|
<button class="sub-nav-item ${subModule === 'budget' ? 'active' : ''}" data-subnav="budget">Budgeting</button> |
|
|
<button class="sub-nav-item ${subModule === 'reports' ? 'active' : ''}" data-subnav="reports">Reports</button> |
|
|
</div> |
|
|
|
|
|
<div id="financeSubContent"> |
|
|
${renderFinanceSubModule(subModule)} |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderFinanceSubModule(subModule) { |
|
|
switch(subModule) { |
|
|
case 'gl': |
|
|
return renderGeneralLedger(); |
|
|
case 'ap': |
|
|
return renderAccountsPayable(); |
|
|
case 'ar': |
|
|
return renderAccountsReceivable(); |
|
|
case 'assets': |
|
|
return renderAssets(); |
|
|
case 'cashbank': |
|
|
return renderCashBank(); |
|
|
case 'budget': |
|
|
return renderBudgeting(); |
|
|
case 'reports': |
|
|
return renderFinanceReports(); |
|
|
default: |
|
|
return renderGeneralLedger(); |
|
|
} |
|
|
} |
|
|
|
|
|
function renderGeneralLedger() { |
|
|
const glData = { |
|
|
accounts: [ |
|
|
{ account_code: '1100', account_name: 'Cash & Bank', account_type: 'Asset', balance: 8750000000, prior_balance: 8200000000, change: 550000000 }, |
|
|
{ account_code: '1200', account_name: 'Accounts Receivable', account_type: 'Asset', balance: 6900000000, prior_balance: 5400000000, change: 1500000000 }, |
|
|
{ account_code: '1300', account_name: 'Inventory', account_type: 'Asset', balance: 18500000000, prior_balance: 17800000000, change: 700000000 }, |
|
|
{ account_code: '1500', account_name: 'Fixed Assets (Net)', account_type: 'Asset', balance: 10386000000, prior_balance: 10512000000, change: -126000000 }, |
|
|
{ account_code: '2100', account_name: 'Accounts Payable', account_type: 'Liability', balance: 5395000000, prior_balance: 4200000000, change: 1195000000 }, |
|
|
{ account_code: '2200', account_name: 'Short-term Loans', account_type: 'Liability', balance: 5000000000, prior_balance: 5000000000, change: 0 }, |
|
|
{ account_code: '3100', account_name: 'Equity', account_type: 'Equity', balance: 45236000000, prior_balance: 42112000000, change: 3124000000 } |
|
|
] |
|
|
}; |
|
|
|
|
|
const accounts = glData.accounts; |
|
|
const totalAssets = accounts.filter(a => a.account_type === 'Asset').reduce((sum, a) => sum + a.balance, 0); |
|
|
const totalLiabilities = accounts.filter(a => a.account_type === 'Liability').reduce((sum, a) => sum + a.balance, 0); |
|
|
const totalEquity = accounts.filter(a => a.account_type === 'Equity').reduce((sum, a) => sum + a.balance, 0); |
|
|
const currentRatio = 1.42; |
|
|
|
|
|
function getTypeBadgeStyle(type) { |
|
|
if (type === 'Asset') return 'background: #E3F2FD; color: #0066CC; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500;'; |
|
|
if (type === 'Liability') return 'background: #FFEBEE; color: #CC0000; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500;'; |
|
|
if (type === 'Equity') return 'background: #E8F5E9; color: #008000; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500;'; |
|
|
return ''; |
|
|
} |
|
|
|
|
|
function formatChange(change) { |
|
|
if (change > 0) return `<span style="color: #22c55e; font-weight: 600;">+${formatCurrency(change)}</span>`; |
|
|
if (change < 0) return `<span style="color: #ef4444; font-weight: 600;">${formatCurrency(change)}</span>`; |
|
|
return `<span style="color: #6b7280; font-weight: 600;">0</span>`; |
|
|
} |
|
|
|
|
|
return ` |
|
|
<div style="margin-bottom: 16px;"> |
|
|
<h2 style="font-size: 24px; font-weight: 600; margin-bottom: 4px;">General Ledger & Chart of Accounts</h2> |
|
|
<p style="color: var(--color-text-secondary); font-size: 14px;">Financial Position as of 2025-11-10</p> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Assets</span> |
|
|
<span class="stat-icon">📊</span> |
|
|
</div> |
|
|
<div class="stat-value" style="color: #0066CC;">${formatCurrency(totalAssets)}</div> |
|
|
<div class="stat-change" style="color: var(--color-text-secondary);">${accounts.filter(a => a.account_type === 'Asset').length} accounts</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Liabilities</span> |
|
|
<span class="stat-icon">📉</span> |
|
|
</div> |
|
|
<div class="stat-value" style="color: #0066CC;">${formatCurrency(totalLiabilities)}</div> |
|
|
<div class="stat-change" style="color: var(--color-text-secondary);">${accounts.filter(a => a.account_type === 'Liability').length} accounts</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Equity</span> |
|
|
<span class="stat-icon">💼</span> |
|
|
</div> |
|
|
<div class="stat-value" style="color: #0066CC;">${formatCurrency(totalEquity)}</div> |
|
|
<div class="stat-change" style="color: var(--color-text-secondary);">${accounts.filter(a => a.account_type === 'Equity').length} accounts</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Current Ratio</span> |
|
|
<span class="stat-icon">📈</span> |
|
|
</div> |
|
|
<div class="stat-value" style="color: #0066CC;">${currentRatio.toFixed(2)}</div> |
|
|
<div class="stat-change" style="color: var(--color-text-secondary);">Healthy position</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Chart of Accounts</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('Exporting to PDF...')">📄 Export PDF</button> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('Exporting to Excel...')">📊 Export Excel</button> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('Printing...')">🖨️ Print</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div style="margin-bottom: 16px; display: flex; gap: 8px; align-items: center;"> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('Filter: All accounts')">All</button> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('Filter: Assets only')">Assets</button> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('Filter: Liabilities only')">Liabilities</button> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('Filter: Equity only')">Equity</button> |
|
|
<input type="text" placeholder="Search accounts..." class="form-control" style="width: 200px; margin-left: auto;"> |
|
|
</div> |
|
|
|
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th style="text-align: left;">Account Code</th> |
|
|
<th style="text-align: left;">Account Name</th> |
|
|
<th style="text-align: center;">Account Type</th> |
|
|
<th style="text-align: right;">Balance</th> |
|
|
<th style="text-align: right;">Prior Balance</th> |
|
|
<th style="text-align: right;">Change</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${accounts.map(acc => ` |
|
|
<tr onclick="showModal('Account Details', 'Detailed transactions for ${acc.account_name} would appear here')" style="cursor: pointer;"> |
|
|
<td style="text-align: left;"><strong style="font-family: monospace; color: #1a5490;">${acc.account_code}</strong></td> |
|
|
<td style="text-align: left;">${acc.account_name}</td> |
|
|
<td style="text-align: center;"><span style="${getTypeBadgeStyle(acc.account_type)}">${acc.account_type}</span></td> |
|
|
<td style="text-align: right;"><strong>${formatCurrency(acc.balance)}</strong></td> |
|
|
<td style="text-align: right; color: #6b7280;">${formatCurrency(acc.prior_balance)}</td> |
|
|
<td style="text-align: right;">${formatChange(acc.change)}</td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-2"> |
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Financial Ratios</h3> |
|
|
</div> |
|
|
<div style="padding: 20px; background: var(--color-bg-1); border-radius: 8px; margin-bottom: 16px;"> |
|
|
<div style="font-size: 14px; color: var(--color-text-secondary); margin-bottom: 8px;">Debt-to-Equity Ratio</div> |
|
|
<div style="font-size: 28px; font-weight: 700;">0.23</div> |
|
|
<div style="font-size: 12px; color: var(--color-success); margin-top: 4px;">✓ Low leverage</div> |
|
|
</div> |
|
|
<div style="padding: 20px; background: var(--color-bg-2); border-radius: 8px;"> |
|
|
<div style="font-size: 14px; color: var(--color-text-secondary); margin-bottom: 8px;">Working Capital</div> |
|
|
<div style="font-size: 28px; font-weight: 700;">${formatCurrency(totalAssets - totalLiabilities)}</div> |
|
|
<div style="font-size: 12px; color: var(--color-success); margin-top: 4px;">✓ Strong position</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Quick Stats</h3> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Total Accounts</span> |
|
|
<strong>${accounts.length}</strong> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Asset Accounts</span> |
|
|
<strong>${accounts.filter(a => a.account_type === 'Asset').length}</strong> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Liability Accounts</span> |
|
|
<strong>${accounts.filter(a => a.account_type === 'Liability').length}</strong> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px;"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Equity Accounts</span> |
|
|
<strong>${accounts.filter(a => a.account_type === 'Equity').length}</strong> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function showAPInvoiceDetail(invoiceId) { |
|
|
const invoice = state.data.financeAccounting.accountsPayable.invoices.find(i => i.invoice_id === invoiceId); |
|
|
if (invoice) { |
|
|
const content = ` |
|
|
<div style="margin-bottom: 16px;"> |
|
|
<h4>Invoice Information</h4> |
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;"> |
|
|
<div><strong>Invoice ID:</strong> ${invoice.invoice_id}</div> |
|
|
<div><strong>Invoice No:</strong> ${invoice.invoice_no}</div> |
|
|
<div><strong>Vendor:</strong> ${invoice.vendor}</div> |
|
|
<div><strong>PO Reference:</strong> ${invoice.po_number}</div> |
|
|
<div><strong>Invoice Date:</strong> ${invoice.invoice_date}</div> |
|
|
<div><strong>Due Date:</strong> ${invoice.due_date}</div> |
|
|
<div><strong>Amount:</strong> ${formatCurrency(invoice.amount)}</div> |
|
|
<div><strong>Status:</strong> ${invoice.status}</div> |
|
|
<div><strong>Payment Status:</strong> ${invoice.payment_status}</div> |
|
|
<div><strong>GRN Matched:</strong> ${invoice.grn_matched ? 'Yes ✓' : 'No ✗'}</div> |
|
|
</div> |
|
|
<hr style="margin: 16px 0; border: none; border-top: 1px solid var(--color-border);"> |
|
|
<h4>3-Way Match Status</h4> |
|
|
<div style="padding: 12px; background: var(--color-bg-1); border-radius: 8px; margin-top: 8px;"> |
|
|
<div style="margin-bottom: 8px;">✅ PO Match: Verified</div> |
|
|
<div style="margin-bottom: 8px;">${invoice.grn_matched ? '✅' : '⏳'} GRN Match: ${invoice.grn_matched ? 'Verified' : 'Pending'}</div> |
|
|
<div>✅ Invoice Match: Verified</div> |
|
|
</div> |
|
|
</div> |
|
|
<div style="display: flex; gap: 8px;"> |
|
|
<button class="btn btn-primary" onclick="closeModal()">Close</button> |
|
|
${invoice.payment_status !== 'Paid' ? '<button class="btn btn-secondary" onclick="showToast(\'Payment recorded\')">Record Payment</button>' : ''} |
|
|
<button class="btn btn-secondary" onclick="showToast(\'Email sent\')">Email Vendor</button> |
|
|
</div> |
|
|
`; |
|
|
showModal('Invoice Details: ' + invoice.invoice_id, content); |
|
|
} |
|
|
} |
|
|
|
|
|
function showARInvoiceDetail(invoiceId) { |
|
|
const invoice = state.data.financeAccounting.accountsReceivable.invoices.find(i => i.invoice_id === invoiceId); |
|
|
if (invoice) { |
|
|
const content = ` |
|
|
<div style="margin-bottom: 16px;"> |
|
|
<h4>Invoice Information</h4> |
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;"> |
|
|
<div><strong>Invoice ID:</strong> ${invoice.invoice_id}</div> |
|
|
<div><strong>Invoice No:</strong> ${invoice.invoice_no}</div> |
|
|
<div><strong>Customer:</strong> ${invoice.customer}</div> |
|
|
<div><strong>Invoice Date:</strong> ${invoice.invoice_date}</div> |
|
|
<div><strong>Due Date:</strong> ${invoice.due_date}</div> |
|
|
<div><strong>Amount:</strong> ${formatCurrency(invoice.amount)}</div> |
|
|
<div><strong>Status:</strong> ${invoice.status}</div> |
|
|
<div><strong>Payment Status:</strong> ${invoice.payment_status}</div> |
|
|
${invoice.days_overdue ? `<div><strong>Days Overdue:</strong> <span style="color: var(--color-error);">${invoice.days_overdue}</span></div>` : ''} |
|
|
${invoice.days_outstanding ? `<div><strong>Days Outstanding:</strong> ${invoice.days_outstanding}</div>` : ''} |
|
|
</div> |
|
|
<hr style="margin: 16px 0; border: none; border-top: 1px solid var(--color-border);"> |
|
|
<h4>Collection Status</h4> |
|
|
<div style="padding: 12px; background: var(--color-bg-2); border-radius: 8px; margin-top: 8px;"> |
|
|
${invoice.payment_status === 'Paid' ? 'Payment received and recorded' : invoice.payment_status === 'Overdue' ? 'Follow-up required - payment overdue' : 'Monitoring for payment'} |
|
|
</div> |
|
|
</div> |
|
|
<div style="display: flex; gap: 8px;"> |
|
|
<button class="btn btn-primary" onclick="closeModal()">Close</button> |
|
|
${invoice.payment_status !== 'Paid' ? '<button class="btn btn-secondary" onclick="showToast(\'Reminder sent\')">Send Reminder</button>' : ''} |
|
|
${invoice.payment_status !== 'Paid' ? '<button class="btn btn-secondary" onclick="showToast(\'Payment recorded\')">Record Payment</button>' : ''} |
|
|
<button class="btn btn-secondary" onclick="showToast(\'Invoice printed\')">Print</button> |
|
|
</div> |
|
|
`; |
|
|
showModal('Invoice Details: ' + invoice.invoice_id, content); |
|
|
} |
|
|
} |
|
|
|
|
|
function showAssetDetail(assetId) { |
|
|
const asset = state.data.financeAccounting.assets.fixedAssets.find(a => a.asset_id === assetId); |
|
|
if (asset) { |
|
|
const monthlyDepreciation = asset.original_cost / (asset.useful_life * 12); |
|
|
const content = ` |
|
|
<div style="margin-bottom: 16px;"> |
|
|
<h4>Asset Information</h4> |
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;"> |
|
|
<div><strong>Asset ID:</strong> ${asset.asset_id}</div> |
|
|
<div><strong>Name:</strong> ${asset.name}</div> |
|
|
<div><strong>Category:</strong> ${asset.category}</div> |
|
|
<div><strong>Location:</strong> ${asset.location}</div> |
|
|
<div><strong>Acquisition Date:</strong> ${asset.acquisition_date}</div> |
|
|
<div><strong>Status:</strong> ${asset.status}</div> |
|
|
<div><strong>Original Cost:</strong> ${formatCurrency(asset.original_cost)}</div> |
|
|
<div><strong>Useful Life:</strong> ${asset.useful_life} years</div> |
|
|
<div><strong>Method:</strong> ${asset.depreciation_method}</div> |
|
|
<div><strong>Accumulated Depr:</strong> ${formatCurrency(asset.accumulated_depreciation)}</div> |
|
|
<div><strong>Book Value:</strong> ${formatCurrency(asset.book_value)}</div> |
|
|
<div><strong>Monthly Depr:</strong> ${formatCurrency(monthlyDepreciation)}</div> |
|
|
</div> |
|
|
<hr style="margin: 16px 0; border: none; border-top: 1px solid var(--color-border);"> |
|
|
<h4>Depreciation Summary</h4> |
|
|
<div style="padding: 12px; background: var(--color-bg-3); border-radius: 8px; margin-top: 8px;"> |
|
|
Depreciation Rate: ${((asset.accumulated_depreciation / asset.original_cost) * 100).toFixed(1)}% of original cost<br> |
|
|
Remaining Life: ${(asset.useful_life - (asset.accumulated_depreciation / (asset.original_cost / asset.useful_life))).toFixed(1)} years |
|
|
</div> |
|
|
</div> |
|
|
<div style="display: flex; gap: 8px;"> |
|
|
<button class="btn btn-primary" onclick="closeModal()">Close</button> |
|
|
<button class="btn btn-secondary">Edit Asset</button> |
|
|
<button class="btn btn-secondary" onclick="showToast(\'Maintenance recorded\')">Add Maintenance</button> |
|
|
</div> |
|
|
`; |
|
|
showModal('Asset Details: ' + asset.name, content); |
|
|
} |
|
|
} |
|
|
|
|
|
function formatCurrency(amount) { |
|
|
if (amount >= 1000000000) { |
|
|
return 'IDR ' + (amount / 1000000000).toFixed(2) + 'B'; |
|
|
} else if (amount >= 1000000) { |
|
|
return 'IDR ' + (amount / 1000000).toFixed(0) + 'M'; |
|
|
} else { |
|
|
return 'IDR ' + amount.toLocaleString('id-ID'); |
|
|
} |
|
|
} |
|
|
|
|
|
function renderAccountsReceivable() { |
|
|
const ar = state.data.financeAccounting.accountsReceivable; |
|
|
const invoices = ar.invoices; |
|
|
|
|
|
return ` |
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Receivable</span> |
|
|
<span class="stat-icon">📥</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(ar.summary.total_receivable)}</div> |
|
|
<div class="stat-change">${invoices.length} customer invoices</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Outstanding</span> |
|
|
<span class="stat-icon">⏳</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(ar.summary.outstanding)}</div> |
|
|
<div class="stat-change">${invoices.filter(i => i.payment_status === 'Unpaid').length} invoices</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Overdue</span> |
|
|
<span class="stat-icon">🚨</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(ar.summary.overdue)}</div> |
|
|
<div class="stat-change class="negative">${invoices.filter(i => i.payment_status === 'Overdue').length} invoice overdue</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">DSO (Days)</span> |
|
|
<span class="stat-icon">📊</span> |
|
|
</div> |
|
|
<div class="stat-value">${ar.summary.dso}</div> |
|
|
<div class="stat-change">Days Sales Outstanding</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">AR Aging Analysis</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="arAgingChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Customer Invoices</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-secondary btn-sm">Export</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Invoice', 'Customer invoice form would appear here')">+ New Invoice</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Invoice ID</th> |
|
|
<th>Customer</th> |
|
|
<th>Invoice Date</th> |
|
|
<th>Due Date</th> |
|
|
<th>Amount</th> |
|
|
<th>Status</th> |
|
|
<th>Payment</th> |
|
|
<th>Actions</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${invoices.map(inv => { |
|
|
const daysOverdue = inv.days_overdue || 0; |
|
|
const statusBadge = inv.payment_status === 'Paid' ? 'status-good' : inv.payment_status === 'Overdue' ? 'status-error' : 'status-warning'; |
|
|
return ` |
|
|
<tr onclick="showARInvoiceDetail('${inv.invoice_id}')"> |
|
|
<td><strong>${inv.invoice_id}</strong></td> |
|
|
<td>${inv.customer}</td> |
|
|
<td>${inv.invoice_date}</td> |
|
|
<td>${inv.due_date}</td> |
|
|
<td style="text-align: right;"><strong>${formatCurrency(inv.amount)}</strong></td> |
|
|
<td><span class="status-badge status-info">${inv.status}</span></td> |
|
|
<td><span class="status-badge ${statusBadge}">${inv.payment_status}${daysOverdue > 0 ? ` (${daysOverdue}d)` : ''}</span></td> |
|
|
<td> |
|
|
${inv.payment_status !== 'Paid' ? '<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); showModal(\'Record Payment\', \'Payment form for ' + inv.invoice_id + ' would appear here\')">Record Payment</button>' : ''} |
|
|
</td> |
|
|
</tr> |
|
|
`; |
|
|
}).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderAssets() { |
|
|
const assets = state.data.financeAccounting.assets; |
|
|
const fixedAssets = assets.fixedAssets; |
|
|
|
|
|
return ` |
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Assets Value</span> |
|
|
<span class="stat-icon">🏢</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(assets.summary.total_assets)}</div> |
|
|
<div class="stat-change">Original cost</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Net Book Value</span> |
|
|
<span class="stat-icon">💎</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(assets.summary.net_book_value)}</div> |
|
|
<div class="stat-change">After depreciation</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Accumulated Depreciation</span> |
|
|
<span class="stat-icon">📉</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(assets.summary.total_depreciation)}</div> |
|
|
<div class="stat-change">${((assets.summary.total_depreciation / assets.summary.total_assets) * 100).toFixed(1)}% of original</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Active Assets</span> |
|
|
<span class="stat-icon">✅</span> |
|
|
</div> |
|
|
<div class="stat-value">${fixedAssets.filter(a => a.status === 'Active').length}</div> |
|
|
<div class="stat-change">Total items</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">Assets by Category</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="assetsCategoryChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Fixed Asset Register</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-secondary btn-sm">Export</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Asset', 'Asset registration form would appear here')">+ New Asset</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Asset ID</th> |
|
|
<th>Name</th> |
|
|
<th>Category</th> |
|
|
<th>Location</th> |
|
|
<th>Acquisition Date</th> |
|
|
<th>Original Cost</th> |
|
|
<th>Acc. Depreciation</th> |
|
|
<th>Book Value</th> |
|
|
<th>Status</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${fixedAssets.map(asset => ` |
|
|
<tr onclick="showAssetDetail('${asset.asset_id}')"> |
|
|
<td><strong>${asset.asset_id}</strong></td> |
|
|
<td>${asset.name}</td> |
|
|
<td><span class="status-badge status-info">${asset.category}</span></td> |
|
|
<td>${asset.location}</td> |
|
|
<td>${asset.acquisition_date}</td> |
|
|
<td style="text-align: right;">${formatCurrency(asset.original_cost)}</td> |
|
|
<td style="text-align: right;">${formatCurrency(asset.accumulated_depreciation)}</td> |
|
|
<td style="text-align: right;"><strong>${formatCurrency(asset.book_value)}</strong></td> |
|
|
<td><span class="status-badge status-good">${asset.status}</span></td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderCashBank() { |
|
|
const cb = state.data.financeAccounting.cashBank; |
|
|
const accounts = cb.bankAccounts; |
|
|
|
|
|
return ` |
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Liquid Assets</span> |
|
|
<span class="stat-icon">💵</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(cb.total_liquid_assets)}</div> |
|
|
<div class="stat-change">Cash + Bank</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Bank Accounts</span> |
|
|
<span class="stat-icon">🏦</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(cb.total_liquid_assets - cb.cash_on_hand)}</div> |
|
|
<div class="stat-change">${accounts.length} accounts</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Cash on Hand</span> |
|
|
<span class="stat-icon">💰</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(cb.cash_on_hand)}</div> |
|
|
<div class="stat-change">Physical cash</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Last Reconciliation</span> |
|
|
<span class="stat-icon">✅</span> |
|
|
</div> |
|
|
<div class="stat-value">${accounts[0].last_reconciled}</div> |
|
|
<div class="stat-change">All accounts</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">Cash Flow Forecast (30 Days)</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="cashFlowChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-2"> |
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Bank Accounts</h3> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Bank Account', 'Bank account setup form would appear here')">+ Add Account</button> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Bank</th> |
|
|
<th>Account Number</th> |
|
|
<th>Type</th> |
|
|
<th>Balance</th> |
|
|
<th>Status</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${accounts.map(acc => ` |
|
|
<tr onclick="showModal('Bank Account Details', 'Transaction history for ${acc.bank_name} would appear here')"> |
|
|
<td><strong>${acc.bank_name}</strong></td> |
|
|
<td>${acc.account_number}</td> |
|
|
<td>${acc.account_type}</td> |
|
|
<td style="text-align: right;"><strong>${formatCurrency(acc.balance)}</strong></td> |
|
|
<td><span class="status-badge status-good">${acc.status}</span></td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Recent Transactions</h3> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;"> |
|
|
<strong>Customer Payment</strong> |
|
|
<span style="color: var(--color-success); font-weight: 600;">+IDR 1.8B</span> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">2025-11-09 | Bank BCA</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;"> |
|
|
<strong>Vendor Payment</strong> |
|
|
<span style="color: var(--color-error); font-weight: 600;">-IDR 750M</span> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">2025-11-08 | Bank Mandiri</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;"> |
|
|
<strong>Payroll Transfer</strong> |
|
|
<span style="color: var(--color-error); font-weight: 600;">-IDR 450M</span> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">2025-11-07 | Bank BCA</div> |
|
|
</div> |
|
|
<div style="padding: 12px;"> |
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;"> |
|
|
<strong>Bank Interest</strong> |
|
|
<span style="color: var(--color-success); font-weight: 600;">+IDR 12.5M</span> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">2025-11-05 | Bank Mandiri</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderBudgeting() { |
|
|
const budgeting = state.data.financeAccounting.budgeting; |
|
|
const budgets = budgeting.budgets; |
|
|
|
|
|
return ` |
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Budget</span> |
|
|
<span class="stat-icon">📊</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(budgeting.summary.total_budget)}</div> |
|
|
<div class="stat-change">FY 2025</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Spent</span> |
|
|
<span class="stat-icon">💸</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(budgeting.summary.total_spent)}</div> |
|
|
<div class="stat-change">${budgeting.summary.utilization_percent.toFixed(1)}% utilized</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Remaining</span> |
|
|
<span class="stat-icon">💰</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(budgeting.summary.total_remaining)}</div> |
|
|
<div class="stat-change">${(100 - budgeting.summary.utilization_percent).toFixed(1)}% available</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Budget Items</span> |
|
|
<span class="stat-icon">📋</span> |
|
|
</div> |
|
|
<div class="stat-value">${budgets.length}</div> |
|
|
<div class="stat-change">${budgets.filter(b => b.status === 'Caution' || b.status === 'Fully Spent').length} need attention</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">Budget vs Actual by Department</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="budgetVsActualChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Budget Analysis</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-secondary btn-sm">Export</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Budget', 'Budget creation form would appear here')">+ New Budget</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Department</th> |
|
|
<th>Category</th> |
|
|
<th>Budgeted</th> |
|
|
<th>Spent</th> |
|
|
<th>Remaining</th> |
|
|
<th>Variance %</th> |
|
|
<th>Status</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${budgets.map(budget => { |
|
|
const statusBadge = budget.status === 'On Track' ? 'status-good' : budget.status === 'Caution' ? 'status-warning' : 'status-error'; |
|
|
return ` |
|
|
<tr onclick="showModal('Budget Details', 'Detailed breakdown for ${budget.department} would appear here')"> |
|
|
<td><strong>${budget.department}</strong></td> |
|
|
<td>${budget.category}</td> |
|
|
<td style="text-align: right;">${formatCurrency(budget.budgeted_amount)}</td> |
|
|
<td style="text-align: right;">${formatCurrency(budget.spent_amount)}</td> |
|
|
<td style="text-align: right;">${formatCurrency(budget.remaining)}</td> |
|
|
<td style="text-align: right;"><strong>${Math.abs(budget.variance_percent).toFixed(1)}%</strong></td> |
|
|
<td><span class="status-badge ${statusBadge}">${budget.status}</span></td> |
|
|
</tr> |
|
|
`; |
|
|
}).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderFinanceReports() { |
|
|
const reports = state.data.financeAccounting.reports; |
|
|
const is = reports.incomeStatement; |
|
|
const bs = reports.balanceSheet; |
|
|
|
|
|
return ` |
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Revenue (MTD)</span> |
|
|
<span class="stat-icon">💰</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(is.revenue)}</div> |
|
|
<div class="stat-change">${is.period}</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Gross Profit</span> |
|
|
<span class="stat-icon">📊</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(is.gross_profit)}</div> |
|
|
<div class="stat-change">${is.gross_margin_percent}% margin</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Net Profit</span> |
|
|
<span class="stat-icon">💎</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(is.net_profit)}</div> |
|
|
<div class="stat-change">${((is.net_profit / is.revenue) * 100).toFixed(1)}% of revenue</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Assets</span> |
|
|
<span class="stat-icon">🏢</span> |
|
|
</div> |
|
|
<div class="stat-value">${formatCurrency(bs.total_assets)}</div> |
|
|
<div class="stat-change">Balance Sheet</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-2"> |
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Income Statement</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('PDF exported')">📄 Export PDF</button> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('Excel exported')">📊 Export Excel</button> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 16px; background: var(--color-bg-1); border-radius: 8px;"> |
|
|
<div style="margin-bottom: 12px; font-weight: 600; font-size: 16px;">Period: ${is.period}</div> |
|
|
<table style="width: 100%; font-size: 14px;"> |
|
|
<tr style="border-bottom: 1px solid var(--color-border);"> |
|
|
<td style="padding: 8px 0;">Revenue</td> |
|
|
<td style="padding: 8px 0; text-align: right; font-weight: 600;">${formatCurrency(is.revenue)}</td> |
|
|
</tr> |
|
|
<tr style="border-bottom: 1px solid var(--color-border);"> |
|
|
<td style="padding: 8px 0;">Cost of Goods Sold</td> |
|
|
<td style="padding: 8px 0; text-align: right;">(${formatCurrency(is.cogs)})</td> |
|
|
</tr> |
|
|
<tr style="border-bottom: 2px solid var(--color-border);"> |
|
|
<td style="padding: 8px 0; font-weight: 600;">Gross Profit</td> |
|
|
<td style="padding: 8px 0; text-align: right; font-weight: 600;">${formatCurrency(is.gross_profit)}</td> |
|
|
</tr> |
|
|
<tr style="border-bottom: 1px solid var(--color-border);"> |
|
|
<td style="padding: 8px 0;">Operating Expenses</td> |
|
|
<td style="padding: 8px 0; text-align: right;">(${formatCurrency(is.operating_expenses)})</td> |
|
|
</tr> |
|
|
<tr style="border-bottom: 2px solid var(--color-border);"> |
|
|
<td style="padding: 8px 0; font-weight: 600;">Operating Profit</td> |
|
|
<td style="padding: 8px 0; text-align: right; font-weight: 600;">${formatCurrency(is.operating_profit)}</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<td style="padding: 8px 0; font-weight: 700; font-size: 16px;">Net Profit</td> |
|
|
<td style="padding: 8px 0; text-align: right; font-weight: 700; font-size: 16px; color: var(--color-success);">${formatCurrency(is.net_profit)}</td> |
|
|
</tr> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Balance Sheet</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('PDF exported')">📄 Export PDF</button> |
|
|
<button class="btn btn-secondary btn-sm" onclick="showToast('Excel exported')">📊 Export Excel</button> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 16px; background: var(--color-bg-2); border-radius: 8px;"> |
|
|
<div style="margin-bottom: 12px; font-weight: 600; font-size: 16px;">As of: ${bs.period}</div> |
|
|
<table style="width: 100%; font-size: 14px;"> |
|
|
<tr style="border-bottom: 2px solid var(--color-border);"> |
|
|
<td style="padding: 8px 0; font-weight: 700;">ASSETS</td> |
|
|
<td style="padding: 8px 0; text-align: right; font-weight: 700;">${formatCurrency(bs.total_assets)}</td> |
|
|
</tr> |
|
|
<tr style="border-bottom: 1px solid var(--color-border);"> |
|
|
<td style="padding: 8px 0; padding-left: 16px;">Current Assets</td> |
|
|
<td style="padding: 8px 0; text-align: right;">${formatCurrency(8750000000 + 6900000000 + 18500000000)}</td> |
|
|
</tr> |
|
|
<tr style="border-bottom: 2px solid var(--color-border);"> |
|
|
<td style="padding: 8px 0; padding-left: 16px;">Fixed Assets (Net)</td> |
|
|
<td style="padding: 8px 0; text-align: right;">${formatCurrency(10386000000)}</td> |
|
|
</tr> |
|
|
<tr style="border-bottom: 2px solid var(--color-border);"> |
|
|
<td style="padding: 8px 0; font-weight: 700;">LIABILITIES</td> |
|
|
<td style="padding: 8px 0; text-align: right; font-weight: 700;">${formatCurrency(bs.total_liabilities)}</td> |
|
|
</tr> |
|
|
<tr style="border-bottom: 2px solid var(--color-border);"> |
|
|
<td style="padding: 8px 0; font-weight: 700;">EQUITY</td> |
|
|
<td style="padding: 8px 0; text-align: right; font-weight: 700;">${formatCurrency(bs.total_equity)}</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<td style="padding: 8px 0; font-weight: 700; font-size: 16px;">Total Liab. + Equity</td> |
|
|
<td style="padding: 8px 0; text-align: right; font-weight: 700; font-size: 16px;">${formatCurrency(bs.total_liabilities + bs.total_equity)}</td> |
|
|
</tr> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Available Reports</h3> |
|
|
</div> |
|
|
<div class="grid-3"> |
|
|
<div style="padding: 16px; border: 1px solid var(--color-border); border-radius: 8px; cursor: pointer;" onclick="showToast('Generating P&L Statement...')"> |
|
|
<div style="font-size: 24px; margin-bottom: 8px;">📊</div> |
|
|
<div style="font-weight: 600; margin-bottom: 4px;">Profit & Loss Statement</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Monthly/Quarterly/YTD</div> |
|
|
</div> |
|
|
<div style="padding: 16px; border: 1px solid var(--color-border); border-radius: 8px; cursor: pointer;" onclick="showToast('Generating Balance Sheet...')"> |
|
|
<div style="font-size: 24px; margin-bottom: 8px;">🏢</div> |
|
|
<div style="font-weight: 600; margin-bottom: 4px;">Balance Sheet</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Assets, Liabilities, Equity</div> |
|
|
</div> |
|
|
<div style="padding: 16px; border: 1px solid var(--color-border); border-radius: 8px; cursor: pointer;" onclick="showToast('Generating Cash Flow...')"> |
|
|
<div style="font-size: 24px; margin-bottom: 8px;">💵</div> |
|
|
<div style="font-weight: 600; margin-bottom: 4px;">Cash Flow Statement</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Operating/Investing/Financing</div> |
|
|
</div> |
|
|
<div style="padding: 16px; border: 1px solid var(--color-border); border-radius: 8px; cursor: pointer;" onclick="showToast('Generating AP Aging...')"> |
|
|
<div style="font-size: 24px; margin-bottom: 8px;">📤</div> |
|
|
<div style="font-weight: 600; margin-bottom: 4px;">AP Aging Report</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Payables by due date</div> |
|
|
</div> |
|
|
<div style="padding: 16px; border: 1px solid var(--color-border); border-radius: 8px; cursor: pointer;" onclick="showToast('Generating AR Aging...')"> |
|
|
<div style="font-size: 24px; margin-bottom: 8px;">📥</div> |
|
|
<div style="font-weight: 600; margin-bottom: 4px;">AR Aging Report</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Receivables by due date</div> |
|
|
</div> |
|
|
<div style="padding: 16px; border: 1px solid var(--color-border); border-radius: 8px; cursor: pointer;" onclick="showToast('Generating Trial Balance...')"> |
|
|
<div style="font-size: 24px; margin-bottom: 8px;">⚖️</div> |
|
|
<div style="font-weight: 600; margin-bottom: 4px;">Trial Balance</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">All account balances</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function initFinanceCharts() { |
|
|
const subModule = state.currentSubModule || 'gl'; |
|
|
|
|
|
if (subModule === 'ap') { |
|
|
const apAgingCtx = document.getElementById('apAgingChart'); |
|
|
if (apAgingCtx) { |
|
|
new Chart(apAgingCtx, { |
|
|
type: 'bar', |
|
|
data: { |
|
|
labels: ['Current', '1-30 Days', '31-60 Days', '60+ Days'], |
|
|
datasets: [{ |
|
|
label: 'Amount (IDR Billion)', |
|
|
data: [4.2, 0.75, 0.125, 0.32], |
|
|
backgroundColor: ['#1FB8CD', '#FFC185', '#D2BA4C', '#B4413C'] |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { legend: { display: false } } |
|
|
} |
|
|
}); |
|
|
} |
|
|
} else if (subModule === 'ar') { |
|
|
const arAgingCtx = document.getElementById('arAgingChart'); |
|
|
if (arAgingCtx) { |
|
|
new Chart(arAgingCtx, { |
|
|
type: 'bar', |
|
|
data: { |
|
|
labels: ['Current', '1-30 Days', '31-60 Days', '60+ Days'], |
|
|
datasets: [{ |
|
|
label: 'Amount (IDR Billion)', |
|
|
data: [3.2, 2.1, 0, 1.6], |
|
|
backgroundColor: ['#1FB8CD', '#FFC185', '#D2BA4C', '#B4413C'] |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { legend: { display: false } } |
|
|
} |
|
|
}); |
|
|
} |
|
|
} else if (subModule === 'assets') { |
|
|
const assetsCategoryCtx = document.getElementById('assetsCategoryChart'); |
|
|
if (assetsCategoryCtx) { |
|
|
new Chart(assetsCategoryCtx, { |
|
|
type: 'doughnut', |
|
|
data: { |
|
|
labels: ['Building', 'Equipment', 'Vehicle'], |
|
|
datasets: [{ |
|
|
data: [9.35, 0.512, 0.224], |
|
|
backgroundColor: ['#1FB8CD', '#FFC185', '#B4413C'] |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { legend: { position: 'right' } } |
|
|
} |
|
|
}); |
|
|
} |
|
|
} else if (subModule === 'cashbank') { |
|
|
const cashFlowCtx = document.getElementById('cashFlowChart'); |
|
|
if (cashFlowCtx) { |
|
|
new Chart(cashFlowCtx, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'], |
|
|
datasets: [{ |
|
|
label: 'Projected Balance (IDR Billion)', |
|
|
data: [8.75, 9.2, 8.9, 9.5], |
|
|
borderColor: '#1FB8CD', |
|
|
backgroundColor: 'rgba(31, 184, 205, 0.1)', |
|
|
tension: 0.4, |
|
|
fill: true |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { legend: { display: false } } |
|
|
} |
|
|
}); |
|
|
} |
|
|
} else if (subModule === 'budget') { |
|
|
const budgetCtx = document.getElementById('budgetVsActualChart'); |
|
|
new Chart(budgetCtx, { |
|
|
type: 'bar', |
|
|
data: { |
|
|
labels: ['Ops Lampung', 'Ops Medan', 'Animal Health', 'HR Payroll', 'Logistics'], |
|
|
datasets: [ |
|
|
{ |
|
|
label: 'Budget (IDR Billion)', |
|
|
data: [15, 8, 2, 5.4, 1.2], |
|
|
backgroundColor: '#B4413C' |
|
|
}, |
|
|
{ |
|
|
label: 'Actual (IDR Billion)', |
|
|
data: [12.45, 6.2, 1.85, 5.4, 0.54], |
|
|
backgroundColor: '#1FB8CD' |
|
|
} |
|
|
] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { legend: { display: true, position: 'top' } } |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function renderInventory() { |
|
|
const items = state.data.inventory; |
|
|
return ` |
|
|
<div class="breadcrumb"> |
|
|
<a href="#">Home</a> / <span>Inventory Management</span> |
|
|
</div> |
|
|
|
|
|
<div class="page-header"> |
|
|
<h1 class="page-title">Inventory Management</h1> |
|
|
<p class="page-subtitle">Track and manage your inventory across all locations</p> |
|
|
</div> |
|
|
|
|
|
<div class="sub-nav"> |
|
|
<button class="sub-nav-item active" data-subnav="overview">Stock Overview</button> |
|
|
<button class="sub-nav-item" data-subnav="items">Items</button> |
|
|
<button class="sub-nav-item" data-subnav="warehouses">Warehouses</button> |
|
|
<button class="sub-nav-item" data-subnav="movements">Movements</button> |
|
|
<button class="sub-nav-item" data-subnav="batch">Batch/Lot</button> |
|
|
<button class="sub-nav-item" data-subnav="qc">Quality Control</button> |
|
|
<button class="sub-nav-item" data-subnav="reports">Reports</button> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Stock Value</span> |
|
|
<span class="stat-icon">💎</span> |
|
|
</div> |
|
|
<div class="stat-value">IDR 18.5B</div> |
|
|
<div class="stat-change">Across all locations</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Items</span> |
|
|
<span class="stat-icon">📦</span> |
|
|
</div> |
|
|
<div class="stat-value">${items.length}</div> |
|
|
<div class="stat-change">Active SKUs</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Low Stock Items</span> |
|
|
<span class="stat-icon">⚠️</span> |
|
|
</div> |
|
|
<div class="stat-value">${items.filter(i => i.quantity < i.reorder_point).length}</div> |
|
|
<div class="stat-change class="negative">Need reorder</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Stock Movements</span> |
|
|
<span class="stat-icon">📊</span> |
|
|
</div> |
|
|
<div class="stat-value">342</div> |
|
|
<div class="stat-change">This month</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Inventory Items</h3> |
|
|
<div class="section-actions"> |
|
|
<input type="text" placeholder="Search items..." class="form-control" style="width: 200px;"> |
|
|
<button class="btn btn-secondary btn-sm">Export</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Item', 'Item creation form would appear here')">+ New Item</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>SKU</th> |
|
|
<th>Name</th> |
|
|
<th>Category</th> |
|
|
<th>Quantity</th> |
|
|
<th>Unit</th> |
|
|
<th>Location</th> |
|
|
<th>Status</th> |
|
|
<th>Actions</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${items.map(item => ` |
|
|
<tr onclick="showInventoryDetail('${item.sku}')"> |
|
|
<td><strong>${item.sku}</strong></td> |
|
|
<td>${item.name}</td> |
|
|
<td><span class="status-badge status-info">${item.category}</span></td> |
|
|
<td>${item.quantity.toLocaleString()}</td> |
|
|
<td>${item.unit}</td> |
|
|
<td>${item.location}</td> |
|
|
<td>${item.quantity < item.reorder_point ? '<span class="status-badge status-error">Low Stock</span>' : '<span class="status-badge status-good">In Stock</span>'}</td> |
|
|
<td> |
|
|
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); alert('Edit ${item.sku}');">Edit</button> |
|
|
</td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
function renderWarehouse() { |
|
|
return ` |
|
|
<div class="breadcrumb"> |
|
|
<a href="#">Home</a> / <span>Warehouse Management</span> |
|
|
</div> |
|
|
|
|
|
<div class="page-header"> |
|
|
<h1 class="page-title">Warehouse Management</h1> |
|
|
<p class="page-subtitle">Optimize warehouse operations and logistics</p> |
|
|
</div> |
|
|
|
|
|
<div class="sub-nav"> |
|
|
<button class="sub-nav-item active" data-subnav="layout">Layout</button> |
|
|
<button class="sub-nav-item" data-subnav="putaway">Putaway</button> |
|
|
<button class="sub-nav-item" data-subnav="picking">Picking</button> |
|
|
<button class="sub-nav-item" data-subnav="packing">Packing</button> |
|
|
<button class="sub-nav-item" data-subnav="shipping">Shipping</button> |
|
|
<button class="sub-nav-item" data-subnav="labor">Labor</button> |
|
|
<button class="sub-nav-item" data-subnav="reports">Reports</button> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Storage Utilization</span> |
|
|
<span class="stat-icon">📊</span> |
|
|
</div> |
|
|
<div class="stat-value">78%</div> |
|
|
<div class="stat-change">Of total capacity</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Active Orders</span> |
|
|
<span class="stat-icon">📋</span> |
|
|
</div> |
|
|
<div class="stat-value">45</div> |
|
|
<div class="stat-change">Being processed</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Picking Tasks</span> |
|
|
<span class="stat-icon">✅</span> |
|
|
</div> |
|
|
<div class="stat-value">23</div> |
|
|
<div class="stat-change">Pending completion</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Shipments Today</span> |
|
|
<span class="stat-icon">🚚</span> |
|
|
</div> |
|
|
<div class="stat-value">12</div> |
|
|
<div class="stat-change">8 completed</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-2"> |
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Warehouse Zones</h3> |
|
|
</div> |
|
|
<div style="padding: 20px; text-align: center; background: var(--color-bg-1); border-radius: 8px; margin-bottom: 16px;"> |
|
|
<div style="font-size: 24px; margin-bottom: 8px;">🏭</div> |
|
|
<div style="font-weight: 600;">Zone A - Feed Storage</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Capacity: 85% | Items: 145</div> |
|
|
</div> |
|
|
<div style="padding: 20px; text-align: center; background: var(--color-bg-2); border-radius: 8px; margin-bottom: 16px;"> |
|
|
<div style="font-size: 24px; margin-bottom: 8px;">💊</div> |
|
|
<div style="font-weight: 600;">Zone B - Medical Storage</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Capacity: 45% | Items: 67</div> |
|
|
</div> |
|
|
<div style="padding: 20px; text-align: center; background: var(--color-bg-3); border-radius: 8px;"> |
|
|
<div style="font-size: 24px; margin-bottom: 8px;">📦</div> |
|
|
<div style="font-weight: 600;">Zone C - General Supplies</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Capacity: 62% | Items: 98</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Today's Activity</h3> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Receipts</span> |
|
|
<strong>8 orders</strong> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">2,500 items received</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Putaway Tasks</span> |
|
|
<strong>15 completed</strong> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">3 pending</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Picks Completed</span> |
|
|
<strong>45 orders</strong> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">95% accuracy rate</div> |
|
|
</div> |
|
|
<div style="padding: 12px;"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Shipments</span> |
|
|
<strong>12 dispatched</strong> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary); margin-top: 4px;">On-time: 100%</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
function renderProcurement() { |
|
|
const pos = state.data.purchaseOrders; |
|
|
return ` |
|
|
<div class="breadcrumb"> |
|
|
<a href="#">Home</a> / <span>Procurement & PPIC</span> |
|
|
</div> |
|
|
|
|
|
<div class="page-header"> |
|
|
<h1 class="page-title">Procurement & PPIC</h1> |
|
|
<p class="page-subtitle">Manage purchasing and production planning</p> |
|
|
</div> |
|
|
|
|
|
<div class="sub-nav"> |
|
|
<button class="sub-nav-item active" data-subnav="vendors">Vendors</button> |
|
|
<button class="sub-nav-item" data-subnav="pr">Purchase Requisitions</button> |
|
|
<button class="sub-nav-item" data-subnav="rfq">RFQ</button> |
|
|
<button class="sub-nav-item" data-subnav="po">Purchase Orders</button> |
|
|
<button class="sub-nav-item" data-subnav="mrp">MRP</button> |
|
|
<button class="sub-nav-item" data-subnav="production">Production</button> |
|
|
<button class="sub-nav-item" data-subnav="reports">Reports</button> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Open POs</span> |
|
|
<span class="stat-icon">📋</span> |
|
|
</div> |
|
|
<div class="stat-value">${pos.length}</div> |
|
|
<div class="stat-change">Total value: IDR 5.1B</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Active Vendors</span> |
|
|
<span class="stat-icon">🏢</span> |
|
|
</div> |
|
|
<div class="stat-value">${state.data.vendors.length}</div> |
|
|
<div class="stat-change">Avg rating: 4.5/5</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Pending Approvals</span> |
|
|
<span class="stat-icon">⏳</span> |
|
|
</div> |
|
|
<div class="stat-value">12</div> |
|
|
<div class="stat-change">Awaiting review</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">OTIF Performance</span> |
|
|
<span class="stat-icon">✅</span> |
|
|
</div> |
|
|
<div class="stat-value">94.5%</div> |
|
|
<div class="stat-change">On-time in-full</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Purchase Orders</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-secondary btn-sm">Export</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Purchase Order', 'PO creation form would appear here')">+ New PO</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>PO Number</th> |
|
|
<th>Vendor</th> |
|
|
<th>Item</th> |
|
|
<th>Quantity</th> |
|
|
<th>Value</th> |
|
|
<th>Status</th> |
|
|
<th>Delivery Date</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${pos.map(po => ` |
|
|
<tr onclick="showModal('PO Details', 'Purchase order ${po.po_no} details would appear here')"> |
|
|
<td><strong>${po.po_no}</strong></td> |
|
|
<td>${po.vendor}</td> |
|
|
<td>${po.item}</td> |
|
|
<td>${po.quantity.toLocaleString()} ${po.unit}</td> |
|
|
<td>${po.value}</td> |
|
|
<td><span class="status-badge ${po.status === 'Approved' ? 'status-good' : po.status === 'Pending' ? 'status-warning' : 'status-info'}">${po.status}</span></td> |
|
|
<td>${po.delivery_date}</td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Vendor Performance</h3> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Vendor ID</th> |
|
|
<th>Name</th> |
|
|
<th>Category</th> |
|
|
<th>OTIF %</th> |
|
|
<th>Rating</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${state.data.vendors.map(v => ` |
|
|
<tr onclick="showModal('Vendor Details', 'Vendor ${v.name} details would appear here')"> |
|
|
<td><strong>${v.vendor_id}</strong></td> |
|
|
<td>${v.name}</td> |
|
|
<td>${v.category}</td> |
|
|
<td>${v.otif}</td> |
|
|
<td>⭐ ${v.rating}/5.0</td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
function renderHR() { |
|
|
const employees = state.data.employees; |
|
|
return ` |
|
|
<div class="breadcrumb"> |
|
|
<a href="#">Home</a> / <span>Human Resources</span> |
|
|
</div> |
|
|
|
|
|
<div class="page-header"> |
|
|
<h1 class="page-title">Human Resources</h1> |
|
|
<p class="page-subtitle">Manage your workforce and HR operations</p> |
|
|
</div> |
|
|
|
|
|
<div class="sub-nav"> |
|
|
<button class="sub-nav-item active" data-subnav="employees">Employees</button> |
|
|
<button class="sub-nav-item" data-subnav="recruitment">Recruitment</button> |
|
|
<button class="sub-nav-item" data-subnav="attendance">Attendance</button> |
|
|
<button class="sub-nav-item" data-subnav="payroll">Payroll</button> |
|
|
<button class="sub-nav-item" data-subnav="performance">Performance</button> |
|
|
<button class="sub-nav-item" data-subnav="training">Training</button> |
|
|
<button class="sub-nav-item" data-subnav="safety">Safety</button> |
|
|
<button class="sub-nav-item" data-subnav="reports">Reports</button> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Employees</span> |
|
|
<span class="stat-icon">👥</span> |
|
|
</div> |
|
|
<div class="stat-value">342</div> |
|
|
<div class="stat-change">+8 this month</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Attendance Rate</span> |
|
|
<span class="stat-icon">📅</span> |
|
|
</div> |
|
|
<div class="stat-value">96.8%</div> |
|
|
<div class="stat-change">Above target</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Open Positions</span> |
|
|
<span class="stat-icon">📢</span> |
|
|
</div> |
|
|
<div class="stat-value">7</div> |
|
|
<div class="stat-change">Active recruitment</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Training Completion</span> |
|
|
<span class="stat-icon">🎓</span> |
|
|
</div> |
|
|
<div class="stat-value">87%</div> |
|
|
<div class="stat-change">This quarter</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Employee Directory</h3> |
|
|
<div class="section-actions"> |
|
|
<input type="text" placeholder="Search employees..." class="form-control" style="width: 200px;"> |
|
|
<button class="btn btn-secondary btn-sm">Export</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Employee', 'Employee registration form would appear here')">+ New Employee</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Employee ID</th> |
|
|
<th>Name</th> |
|
|
<th>Position</th> |
|
|
<th>Department</th> |
|
|
<th>Location</th> |
|
|
<th>Status</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${employees.map(emp => ` |
|
|
<tr onclick="showModal('Employee Profile', 'Profile for ${emp.name} would appear here')"> |
|
|
<td><strong>${emp.emp_id}</strong></td> |
|
|
<td>${emp.name}</td> |
|
|
<td>${emp.position}</td> |
|
|
<td>${emp.department}</td> |
|
|
<td>${emp.location}</td> |
|
|
<td><span class="status-badge status-good">${emp.status}</span></td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
function renderCRM() { |
|
|
const customers = state.data.customers; |
|
|
return ` |
|
|
<div class="breadcrumb"> |
|
|
<a href="#">Home</a> / <span>Customer Relationship Management</span> |
|
|
</div> |
|
|
|
|
|
<div class="page-header"> |
|
|
<h1 class="page-title">Customer Relationship Management</h1> |
|
|
<p class="page-subtitle">Manage customer relationships and sales pipeline</p> |
|
|
</div> |
|
|
|
|
|
<div class="sub-nav"> |
|
|
<button class="sub-nav-item active" data-subnav="customers">Customers</button> |
|
|
<button class="sub-nav-item" data-subnav="leads">Leads</button> |
|
|
<button class="sub-nav-item" data-subnav="opportunities">Opportunities</button> |
|
|
<button class="sub-nav-item" data-subnav="quotations">Quotations</button> |
|
|
<button class="sub-nav-item" data-subnav="sales">Sales Orders</button> |
|
|
<button class="sub-nav-item" data-subnav="service">Service</button> |
|
|
<button class="sub-nav-item" data-subnav="reports">Reports</button> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Customers</span> |
|
|
<span class="stat-icon">🤝</span> |
|
|
</div> |
|
|
<div class="stat-value">${customers.length}</div> |
|
|
<div class="stat-change">+2 new this month</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Active Opportunities</span> |
|
|
<span class="stat-icon">💼</span> |
|
|
</div> |
|
|
<div class="stat-value">18</div> |
|
|
<div class="stat-change">IDR 8.5B pipeline</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Win Rate</span> |
|
|
<span class="stat-icon">🎯</span> |
|
|
</div> |
|
|
<div class="stat-value">68%</div> |
|
|
<div class="stat-change">Last 90 days</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Customer Satisfaction</span> |
|
|
<span class="stat-icon">⭐</span> |
|
|
</div> |
|
|
<div class="stat-value">4.7/5</div> |
|
|
<div class="stat-change">Based on surveys</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">Sales Pipeline</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="pipelineChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Customer List</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-secondary btn-sm">Export</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Customer', 'Customer creation form would appear here')">+ New Customer</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Customer ID</th> |
|
|
<th>Name</th> |
|
|
<th>Type</th> |
|
|
<th>Location</th> |
|
|
<th>Total Orders</th> |
|
|
<th>Lifetime Value</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${customers.map(cust => ` |
|
|
<tr onclick="showModal('Customer Profile', '360-degree view for ${cust.name} would appear here')"> |
|
|
<td><strong>${cust.customer_id}</strong></td> |
|
|
<td>${cust.name}</td> |
|
|
<td>${cust.type}</td> |
|
|
<td>${cust.location}</td> |
|
|
<td>${cust.total_orders}</td> |
|
|
<td>${cust.lifetime_value}</td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function initCRMCharts() { |
|
|
const pipelineCtx = document.getElementById('pipelineChart'); |
|
|
if (pipelineCtx) { |
|
|
new Chart(pipelineCtx, { |
|
|
type: 'doughnut', |
|
|
data: { |
|
|
labels: ['Prospecting', 'Qualification', 'Proposal', 'Negotiation', 'Closed Won'], |
|
|
datasets: [{ |
|
|
data: [12, 8, 6, 4, 10], |
|
|
backgroundColor: ['#1FB8CD', '#FFC185', '#B4413C', '#ECEBD5', '#5D878F'] |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { position: 'right' } |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function renderSCM() { |
|
|
return ` |
|
|
<div class="breadcrumb"> |
|
|
<a href="#">Home</a> / <span>Supply Chain Management</span> |
|
|
</div> |
|
|
|
|
|
<div class="page-header"> |
|
|
<h1 class="page-title">Supply Chain Management</h1> |
|
|
<p class="page-subtitle">Optimize your end-to-end supply chain</p> |
|
|
</div> |
|
|
|
|
|
<div class="sub-nav"> |
|
|
<button class="sub-nav-item active" data-subnav="planning">Planning</button> |
|
|
<button class="sub-nav-item" data-subnav="distribution">Distribution</button> |
|
|
<button class="sub-nav-item" data-subnav="suppliers">Suppliers</button> |
|
|
<button class="sub-nav-item" data-subnav="traceability">Traceability</button> |
|
|
<button class="sub-nav-item" data-subnav="logistics">Logistics</button> |
|
|
<button class="sub-nav-item" data-subnav="returns">Returns</button> |
|
|
<button class="sub-nav-item" data-subnav="reports">Reports</button> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Supply Chain Efficiency</span> |
|
|
<span class="stat-icon">⚡</span> |
|
|
</div> |
|
|
<div class="stat-value">92%</div> |
|
|
<div class="stat-change">+3% vs last month</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">On-Time Delivery</span> |
|
|
<span class="stat-icon">🚚</span> |
|
|
</div> |
|
|
<div class="stat-value">94.5%</div> |
|
|
<div class="stat-change">Above target</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Active Shipments</span> |
|
|
<span class="stat-icon">📦</span> |
|
|
</div> |
|
|
<div class="stat-value">28</div> |
|
|
<div class="stat-change">In transit</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Perfect Order Rate</span> |
|
|
<span class="stat-icon">✅</span> |
|
|
</div> |
|
|
<div class="stat-value">96.2%</div> |
|
|
<div class="stat-change">This quarter</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-2"> |
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Shipment Tracking</h3> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;"> |
|
|
<strong>SHIP-2025-0234</strong> |
|
|
<span class="status-badge status-info">In Transit</span> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Lampung → Jakarta | ETA: Nov 12, 2025</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;"> |
|
|
<strong>SHIP-2025-0235</strong> |
|
|
<span class="status-badge status-warning">Delayed</span> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Medan → Surabaya | ETA: Nov 13, 2025</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;"> |
|
|
<strong>SHIP-2025-0236</strong> |
|
|
<span class="status-badge status-good">Delivered</span> |
|
|
</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Lampung → Bandung | Delivered: Nov 10, 2025</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Supply Chain KPIs</h3> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Order Cycle Time</span> |
|
|
<strong>3.2 days</strong> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Cash-to-Cash Cycle</span> |
|
|
<strong>42 days</strong> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-bottom: 1px solid var(--color-border);"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Inventory Turnover</span> |
|
|
<strong>8.5x</strong> |
|
|
</div> |
|
|
</div> |
|
|
<div style="padding: 12px;"> |
|
|
<div style="display: flex; justify-content: space-between;"> |
|
|
<span>Supply Chain Cost Ratio</span> |
|
|
<strong>18.5%</strong> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
function renderFeedlot() { |
|
|
const cattle = state.data.livestock; |
|
|
const healthRecords = state.data.healthRecords; |
|
|
|
|
|
return ` |
|
|
<div class="breadcrumb"> |
|
|
<a href="#">Home</a> / <span>Feedlot Operations</span> |
|
|
</div> |
|
|
|
|
|
<div class="page-header"> |
|
|
<h1 class="page-title">Feedlot Operations</h1> |
|
|
<p class="page-subtitle">Comprehensive livestock management system</p> |
|
|
</div> |
|
|
|
|
|
<div class="sub-nav"> |
|
|
<button class="sub-nav-item active" data-subnav="registry">Cattle Registry</button> |
|
|
<button class="sub-nav-item" data-subnav="weight">Weight Tracking</button> |
|
|
<button class="sub-nav-item" data-subnav="feed">Feed Management</button> |
|
|
<button class="sub-nav-item" data-subnav="health">Animal Health</button> |
|
|
<button class="sub-nav-item" data-subnav="breeding">Breeding</button> |
|
|
<button class="sub-nav-item" data-subnav="facility">Facility</button> |
|
|
<button class="sub-nav-item" data-subnav="reports">Reports</button> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Total Cattle</span> |
|
|
<span class="stat-icon">🐄</span> |
|
|
</div> |
|
|
<div class="stat-value">8,450</div> |
|
|
<div class="stat-change">+234 this month</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Average Daily Gain</span> |
|
|
<span class="stat-icon">📈</span> |
|
|
</div> |
|
|
<div class="stat-value">1.28 kg</div> |
|
|
<div class="stat-change">Above target (1.2 kg)</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Feed Conversion Ratio</span> |
|
|
<span class="stat-icon">🌾</span> |
|
|
</div> |
|
|
<div class="stat-value">6.2:1</div> |
|
|
<div class="stat-change">Efficient</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Health Rate</span> |
|
|
<span class="stat-icon">💊</span> |
|
|
</div> |
|
|
<div class="stat-value">99.2%</div> |
|
|
<div class="stat-change">${healthRecords.filter(h => h.status === 'Under Treatment').length} under treatment</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Harvest Ready</span> |
|
|
<span class="stat-icon">✅</span> |
|
|
</div> |
|
|
<div class="stat-value">145</div> |
|
|
<div class="stat-change">Target weight reached</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Mortality Rate</span> |
|
|
<span class="stat-icon">📊</span> |
|
|
</div> |
|
|
<div class="stat-value">0.8%</div> |
|
|
<div class="stat-change">Below industry avg</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">Weight Growth Trend</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="weightChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Cattle Registry</h3> |
|
|
<div class="section-actions"> |
|
|
<input type="text" placeholder="Search by ID or Tag..." class="form-control" style="width: 220px;"> |
|
|
<select class="form-control" style="width: 150px;"> |
|
|
<option>All Breeds</option> |
|
|
<option>Brahman</option> |
|
|
<option>Simmental</option> |
|
|
<option>Limousin</option> |
|
|
<option>Angus Cross</option> |
|
|
</select> |
|
|
<button class="btn btn-secondary btn-sm">Export</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('Register New Cattle', 'Cattle registration form would appear here')">+ New Cattle</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Cattle ID</th> |
|
|
<th>RFID Tag</th> |
|
|
<th>Breed</th> |
|
|
<th>Weight (kg)</th> |
|
|
<th>Age (months)</th> |
|
|
<th>Location</th> |
|
|
<th>ADG (kg/day)</th> |
|
|
<th>Health Status</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${cattle.map(c => ` |
|
|
<tr onclick="showCattleDetail('${c.id}')"> |
|
|
<td><strong>${c.id}</strong></td> |
|
|
<td>${c.tag}</td> |
|
|
<td>${c.breed}</td> |
|
|
<td>${c.weight}</td> |
|
|
<td>${c.age_months}</td> |
|
|
<td>${c.location}</td> |
|
|
<td>${c.adg}</td> |
|
|
<td><span class="status-badge ${c.health === 'Good' ? 'status-good' : 'status-warning'}">${c.health}</span></td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Recent Health Records</h3> |
|
|
<div class="section-actions"> |
|
|
<button class="btn btn-primary btn-sm" onclick="showModal('New Health Record', 'Health record entry form would appear here')">+ New Record</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="data-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>MRN</th> |
|
|
<th>Cattle ID</th> |
|
|
<th>Date</th> |
|
|
<th>Diagnosis</th> |
|
|
<th>Treatment</th> |
|
|
<th>Veterinarian</th> |
|
|
<th>Status</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
${healthRecords.map(h => ` |
|
|
<tr onclick="showModal('Health Record Details', 'Complete medical record for MRN ${h.mrn} would appear here')"> |
|
|
<td><strong>${h.mrn}</strong></td> |
|
|
<td>${h.cattle_id}</td> |
|
|
<td>${h.date}</td> |
|
|
<td>${h.diagnosis}</td> |
|
|
<td>${h.treatment}</td> |
|
|
<td>${h.vet}</td> |
|
|
<td><span class="status-badge ${h.status === 'Completed' ? 'status-good' : 'status-warning'}">${h.status}</span></td> |
|
|
</tr> |
|
|
`).join('')} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function initFeedlotCharts() { |
|
|
const weightCtx = document.getElementById('weightChart'); |
|
|
if (weightCtx) { |
|
|
new Chart(weightCtx, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4', 'Week 5', 'Week 6', 'Week 7', 'Week 8'], |
|
|
datasets: [ |
|
|
{ |
|
|
label: 'Actual Weight (kg)', |
|
|
data: [320, 335, 352, 368, 385, 401, 415, 428], |
|
|
borderColor: '#1FB8CD', |
|
|
backgroundColor: 'rgba(31, 184, 205, 0.1)', |
|
|
tension: 0.4 |
|
|
}, |
|
|
{ |
|
|
label: 'Target Weight (kg)', |
|
|
data: [320, 332, 344, 356, 368, 380, 392, 404], |
|
|
borderColor: '#B4413C', |
|
|
borderDash: [5, 5], |
|
|
backgroundColor: 'transparent', |
|
|
tension: 0.4 |
|
|
} |
|
|
] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { display: true, position: 'top' } |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function renderBI() { |
|
|
return ` |
|
|
<div class="breadcrumb"> |
|
|
<a href="#">Home</a> / <span>Business Intelligence</span> |
|
|
</div> |
|
|
|
|
|
<div class="page-header"> |
|
|
<h1 class="page-title">Business Intelligence & Analytics</h1> |
|
|
<p class="page-subtitle">Data-driven insights for better decision making</p> |
|
|
</div> |
|
|
|
|
|
<div class="sub-nav"> |
|
|
<button class="sub-nav-item active" data-subnav="dashboards">Dashboards</button> |
|
|
<button class="sub-nav-item" data-subnav="reports">Reports</button> |
|
|
<button class="sub-nav-item" data-subnav="analytics">Analytics</button> |
|
|
<button class="sub-nav-item" data-subnav="predictions">Predictions</button> |
|
|
<button class="sub-nav-item" data-subnav="alerts">Alerts</button> |
|
|
<button class="sub-nav-item" data-subnav="export">Data Export</button> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Revenue Growth</span> |
|
|
<span class="stat-icon">📈</span> |
|
|
</div> |
|
|
<div class="stat-value">+12.5%</div> |
|
|
<div class="stat-change">Year over year</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Profit Margin</span> |
|
|
<span class="stat-icon">💹</span> |
|
|
</div> |
|
|
<div class="stat-value">27.4%</div> |
|
|
<div class="stat-change">+2.1% vs target</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">ROI</span> |
|
|
<span class="stat-icon">💰</span> |
|
|
</div> |
|
|
<div class="stat-value">34.8%</div> |
|
|
<div class="stat-change">On feedlot operations</div> |
|
|
</div> |
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<span class="stat-title">Efficiency Score</span> |
|
|
<span class="stat-icon">⚡</span> |
|
|
</div> |
|
|
<div class="stat-value">92/100</div> |
|
|
<div class="stat-change">Industry leading</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-2"> |
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">Revenue by Category</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="revenueByCategory"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="chart-container"> |
|
|
<h3 class="section-title" style="margin-bottom: 16px;">Cost Analysis</h3> |
|
|
<div class="chart-wrapper"> |
|
|
<canvas id="costAnalysis"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Predictive Analytics</h3> |
|
|
</div> |
|
|
<div class="grid-2"> |
|
|
<div style="padding: 20px; background: var(--color-bg-1); border-radius: 8px; margin-bottom: 16px;"> |
|
|
<div style="font-size: 18px; font-weight: 600; margin-bottom: 12px;">🔮 Disease Outbreak Prediction</div> |
|
|
<div style="font-size: 24px; font-weight: 700; margin-bottom: 8px;">Low Risk</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Based on environmental factors, vaccination coverage, and historical data</div> |
|
|
<div style="margin-top: 12px; font-size: 14px;">Confidence: <strong>87%</strong></div> |
|
|
</div> |
|
|
|
|
|
<div style="padding: 20px; background: var(--color-bg-2); border-radius: 8px; margin-bottom: 16px;"> |
|
|
<div style="font-size: 18px; font-weight: 600; margin-bottom: 12px;">📊 Weight Gain Forecast</div> |
|
|
<div style="font-size: 24px; font-weight: 700; margin-bottom: 8px;">1.32 kg/day</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Expected ADG for next 30 days based on feed quality and weather patterns</div> |
|
|
<div style="margin-top: 12px; font-size: 14px;">Confidence: <strong>92%</strong></div> |
|
|
</div> |
|
|
|
|
|
<div style="padding: 20px; background: var(--color-bg-3); border-radius: 8px;"> |
|
|
<div style="font-size: 18px; font-weight: 600; margin-bottom: 12px;">🌾 Feed Demand Forecast</div> |
|
|
<div style="font-size: 24px; font-weight: 700; margin-bottom: 8px;">52,000 kg</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Projected feed requirement for next week across all facilities</div> |
|
|
<div style="margin-top: 12px; font-size: 14px;">Confidence: <strong>89%</strong></div> |
|
|
</div> |
|
|
|
|
|
<div style="padding: 20px; background: var(--color-bg-4); border-radius: 8px;"> |
|
|
<div style="font-size: 18px; font-weight: 600; margin-bottom: 12px;">💹 Price Trend Prediction</div> |
|
|
<div style="font-size: 24px; font-weight: 700; margin-bottom: 8px;">+5.2%</div> |
|
|
<div style="font-size: 12px; color: var(--color-text-secondary);">Expected cattle price increase in next quarter based on market analysis</div> |
|
|
<div style="margin-top: 12px; font-size: 14px;">Confidence: <strong>78%</strong></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="content-section"> |
|
|
<div class="section-header"> |
|
|
<h3 class="section-title">Active Alerts & Recommendations</h3> |
|
|
</div> |
|
|
<div style="padding: 12px; border-left: 3px solid #f59e0b; background: rgba(245, 158, 11, 0.1); margin-bottom: 12px; border-radius: 4px;"> |
|
|
<div style="font-weight: 600; margin-bottom: 4px;">⚠️ Inventory Alert</div> |
|
|
<div style="font-size: 14px; color: var(--color-text-secondary);">Vitamin Premix stock below reorder point. Recommend ordering 5,000 kg.</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-left: 3px solid #3b82f6; background: rgba(59, 130, 246, 0.1); margin-bottom: 12px; border-radius: 4px;"> |
|
|
<div style="font-weight: 600; margin-bottom: 4px;">💡 Optimization Opportunity</div> |
|
|
<div style="font-size: 14px; color: var(--color-text-secondary);">Feed cost can be reduced by 8% by switching to alternative supplier without quality impact.</div> |
|
|
</div> |
|
|
<div style="padding: 12px; border-left: 3px solid #10b981; background: rgba(16, 185, 129, 0.1); border-radius: 4px;"> |
|
|
<div style="font-weight: 600; margin-bottom: 4px;">✅ Performance Achievement</div> |
|
|
<div style="font-size: 14px; color: var(--color-text-secondary);">Medan facility achieved 98% health rate this month, exceeding target by 3%.</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function initBICharts() { |
|
|
const revByCat = document.getElementById('revenueByCategory'); |
|
|
if (revByCat) { |
|
|
new Chart(revByCat, { |
|
|
type: 'pie', |
|
|
data: { |
|
|
labels: ['Cattle Sales', 'Feed Sales', 'Breeding Services', 'Consulting', 'Other'], |
|
|
datasets: [{ |
|
|
data: [65, 20, 8, 5, 2], |
|
|
backgroundColor: ['#1FB8CD', '#FFC185', '#B4413C', '#ECEBD5', '#5D878F'] |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { position: 'right' } |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
const costAnal = document.getElementById('costAnalysis'); |
|
|
if (costAnal) { |
|
|
new Chart(costAnal, { |
|
|
type: 'doughnut', |
|
|
data: { |
|
|
labels: ['Feed', 'Labor', 'Veterinary', 'Transport', 'Utilities', 'Other'], |
|
|
datasets: [{ |
|
|
data: [45, 25, 12, 8, 6, 4], |
|
|
backgroundColor: ['#DB4545', '#D2BA4C', '#964325', '#944454', '#13343B', '#5D878F'] |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { position: 'right' } |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function setupModuleListeners() { |
|
|
|
|
|
document.querySelectorAll('.sub-nav-item').forEach(item => { |
|
|
item.addEventListener('click', (e) => { |
|
|
if (state.currentModule === 'finance') { |
|
|
e.preventDefault(); |
|
|
document.querySelectorAll('.sub-nav-item').forEach(i => i.classList.remove('active')); |
|
|
item.classList.add('active'); |
|
|
state.currentSubModule = item.dataset.subnav; |
|
|
const subContent = document.getElementById('financeSubContent'); |
|
|
if (subContent) { |
|
|
subContent.innerHTML = renderFinanceSubModule(state.currentSubModule); |
|
|
initFinanceCharts(); |
|
|
} |
|
|
} else { |
|
|
document.querySelectorAll('.sub-nav-item').forEach(i => i.classList.remove('active')); |
|
|
item.classList.add('active'); |
|
|
const subModule = item.dataset.subnav; |
|
|
showToast(`Switched to ${item.textContent}`); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
function showModal(title, content) { |
|
|
const modal = document.getElementById('modal'); |
|
|
const modalTitle = document.getElementById('modalTitle'); |
|
|
const modalBody = document.getElementById('modalBody'); |
|
|
|
|
|
modalTitle.textContent = title; |
|
|
modalBody.innerHTML = `<p>${content}</p>`; |
|
|
modal.classList.add('active'); |
|
|
} |
|
|
|
|
|
function closeModal() { |
|
|
document.getElementById('modal').classList.remove('active'); |
|
|
} |
|
|
|
|
|
function showToast(message) { |
|
|
const toast = document.getElementById('toast'); |
|
|
const toastMessage = document.getElementById('toastMessage'); |
|
|
|
|
|
toastMessage.textContent = message; |
|
|
toast.classList.add('show'); |
|
|
|
|
|
setTimeout(() => { |
|
|
toast.classList.remove('show'); |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
function updateLastSync() { |
|
|
const lastSyncEl = document.getElementById('lastSync'); |
|
|
if (lastSyncEl) { |
|
|
const now = new Date(); |
|
|
lastSyncEl.textContent = now.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); |
|
|
} |
|
|
} |
|
|
|
|
|
function showInventoryDetail(sku) { |
|
|
const item = state.data.inventory.find(i => i.sku === sku); |
|
|
if (item) { |
|
|
const content = ` |
|
|
<div style="margin-bottom: 16px;"> |
|
|
<strong>SKU:</strong> ${item.sku}<br> |
|
|
<strong>Name:</strong> ${item.name}<br> |
|
|
<strong>Category:</strong> ${item.category}<br> |
|
|
<strong>Quantity:</strong> ${item.quantity.toLocaleString()} ${item.unit}<br> |
|
|
<strong>Location:</strong> ${item.location}<br> |
|
|
<strong>Reorder Point:</strong> ${item.reorder_point.toLocaleString()} ${item.unit}<br> |
|
|
<strong>Status:</strong> ${item.quantity < item.reorder_point ? 'Low Stock - Reorder Required' : 'In Stock'} |
|
|
</div> |
|
|
<button class="btn btn-primary" onclick="closeModal()">Close</button> |
|
|
`; |
|
|
showModal('Item Details: ' + item.name, content); |
|
|
} |
|
|
} |
|
|
|
|
|
function showCattleDetail(id) { |
|
|
const cattle = state.data.livestock.find(c => c.id === id); |
|
|
if (cattle) { |
|
|
const content = ` |
|
|
<div style="margin-bottom: 16px;"> |
|
|
<h4>Individual Cattle Profile</h4> |
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;"> |
|
|
<div><strong>Cattle ID:</strong> ${cattle.id}</div> |
|
|
<div><strong>RFID Tag:</strong> ${cattle.tag}</div> |
|
|
<div><strong>Breed:</strong> ${cattle.breed}</div> |
|
|
<div><strong>Weight:</strong> ${cattle.weight} kg</div> |
|
|
<div><strong>Age:</strong> ${cattle.age_months} months</div> |
|
|
<div><strong>Location:</strong> ${cattle.location}</div> |
|
|
<div><strong>ADG:</strong> ${cattle.adg} kg/day</div> |
|
|
<div><strong>Health Status:</strong> ${cattle.health}</div> |
|
|
</div> |
|
|
<hr style="margin: 16px 0; border: none; border-top: 1px solid var(--color-border);"> |
|
|
<h4>Weight History</h4> |
|
|
<p style="color: var(--color-text-secondary); font-size: 14px;">Last 8 weeks of weight measurements</p> |
|
|
<div style="padding: 8px; background: var(--color-bg-1); border-radius: 4px; margin-top: 8px;"> |
|
|
Entry weights would be displayed here in a chart or table format |
|
|
</div> |
|
|
<hr style="margin: 16px 0; border: none; border-top: 1px solid var(--color-border);"> |
|
|
<h4>Health Records</h4> |
|
|
<p style="color: var(--color-text-secondary); font-size: 14px;">Vaccination history, treatments, and medical notes</p> |
|
|
<div style="padding: 8px; background: var(--color-bg-2); border-radius: 4px; margin-top: 8px;"> |
|
|
Medical history for this animal would be displayed here |
|
|
</div> |
|
|
</div> |
|
|
<div style="display: flex; gap: 8px;"> |
|
|
<button class="btn btn-primary" onclick="closeModal()">Close</button> |
|
|
<button class="btn btn-secondary">Edit Details</button> |
|
|
<button class="btn btn-secondary">Add Health Record</button> |
|
|
</div> |
|
|
`; |
|
|
showModal('Cattle Profile: ' + cattle.id, content); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const chatbot = { |
|
|
isOpen: false, |
|
|
conversationHistory: [], |
|
|
|
|
|
init() { |
|
|
this.setupEventListeners(); |
|
|
this.showWelcomeNotification(); |
|
|
}, |
|
|
|
|
|
setupEventListeners() { |
|
|
const toggleBtn = document.getElementById('chatbotToggle'); |
|
|
const closeBtn = document.getElementById('chatCloseBtn'); |
|
|
const minimizeBtn = document.getElementById('chatMinimizeBtn'); |
|
|
const clearBtn = document.getElementById('chatClearBtn'); |
|
|
const sendBtn = document.getElementById('chatbotSendBtn'); |
|
|
const input = document.getElementById('chatbotInput'); |
|
|
|
|
|
toggleBtn.addEventListener('click', () => this.toggleChat()); |
|
|
closeBtn.addEventListener('click', () => this.closeChat()); |
|
|
minimizeBtn.addEventListener('click', () => this.closeChat()); |
|
|
clearBtn.addEventListener('click', () => this.clearConversation()); |
|
|
sendBtn.addEventListener('click', () => this.sendMessage()); |
|
|
input.addEventListener('keypress', (e) => { |
|
|
if (e.key === 'Enter') this.sendMessage(); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.quick-action-btn').forEach(btn => { |
|
|
btn.addEventListener('click', (e) => { |
|
|
const action = e.target.dataset.action; |
|
|
this.handleQuickAction(action); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.suggestion-btn').forEach(btn => { |
|
|
btn.addEventListener('click', (e) => { |
|
|
const question = e.target.textContent; |
|
|
this.askQuestion(question); |
|
|
}); |
|
|
}); |
|
|
}, |
|
|
|
|
|
showWelcomeNotification() { |
|
|
const badge = document.getElementById('chatNotificationBadge'); |
|
|
badge.style.display = 'block'; |
|
|
}, |
|
|
|
|
|
toggleChat() { |
|
|
const widget = document.getElementById('chatbotWidget'); |
|
|
const badge = document.getElementById('chatNotificationBadge'); |
|
|
|
|
|
this.isOpen = !this.isOpen; |
|
|
widget.classList.toggle('active'); |
|
|
|
|
|
if (this.isOpen) { |
|
|
badge.style.display = 'none'; |
|
|
document.getElementById('chatbotInput').focus(); |
|
|
} |
|
|
}, |
|
|
|
|
|
closeChat() { |
|
|
const widget = document.getElementById('chatbotWidget'); |
|
|
widget.classList.remove('active'); |
|
|
this.isOpen = false; |
|
|
}, |
|
|
|
|
|
clearConversation() { |
|
|
if (confirm('Hapus semua percakapan?')) { |
|
|
this.conversationHistory = []; |
|
|
const messagesContainer = document.getElementById('chatbotMessages'); |
|
|
messagesContainer.innerHTML = ''; |
|
|
document.querySelector('.chatbot-welcome').style.display = 'block'; |
|
|
showToast('Percakapan telah dihapus'); |
|
|
} |
|
|
}, |
|
|
|
|
|
sendMessage() { |
|
|
const input = document.getElementById('chatbotInput'); |
|
|
const message = input.value.trim(); |
|
|
|
|
|
if (!message) return; |
|
|
|
|
|
this.addMessage(message, 'user'); |
|
|
input.value = ''; |
|
|
|
|
|
|
|
|
document.querySelector('.chatbot-welcome').style.display = 'none'; |
|
|
|
|
|
|
|
|
this.showTyping(); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
this.hideTyping(); |
|
|
this.processQuery(message); |
|
|
}, 1000 + Math.random() * 1000); |
|
|
}, |
|
|
|
|
|
askQuestion(question) { |
|
|
document.getElementById('chatbotInput').value = question; |
|
|
this.sendMessage(); |
|
|
}, |
|
|
|
|
|
handleQuickAction(action) { |
|
|
const queries = { |
|
|
dashboard: 'Tampilkan ringkasan dashboard sistem', |
|
|
inventory: 'Cek status inventory semua lokasi', |
|
|
health: 'Tampilkan health alerts saat ini', |
|
|
financial: 'Berikan ringkasan keuangan bulan ini' |
|
|
}; |
|
|
|
|
|
this.askQuestion(queries[action]); |
|
|
}, |
|
|
|
|
|
addMessage(text, sender) { |
|
|
const messagesContainer = document.getElementById('chatbotMessages'); |
|
|
const messageDiv = document.createElement('div'); |
|
|
messageDiv.className = `chat-message ${sender}`; |
|
|
|
|
|
const avatar = document.createElement('div'); |
|
|
avatar.className = `message-avatar ${sender}`; |
|
|
avatar.textContent = sender === 'bot' ? '🤖' : '👤'; |
|
|
|
|
|
const content = document.createElement('div'); |
|
|
content.className = 'message-content'; |
|
|
|
|
|
const bubble = document.createElement('div'); |
|
|
bubble.className = 'message-bubble'; |
|
|
bubble.innerHTML = text; |
|
|
|
|
|
const time = document.createElement('div'); |
|
|
time.className = 'message-time'; |
|
|
const now = new Date(); |
|
|
time.textContent = now.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); |
|
|
|
|
|
content.appendChild(bubble); |
|
|
content.appendChild(time); |
|
|
|
|
|
messageDiv.appendChild(avatar); |
|
|
messageDiv.appendChild(content); |
|
|
|
|
|
messagesContainer.appendChild(messageDiv); |
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight; |
|
|
|
|
|
this.conversationHistory.push({ text, sender, timestamp: now }); |
|
|
|
|
|
return content; |
|
|
}, |
|
|
|
|
|
showTyping() { |
|
|
const messagesContainer = document.getElementById('chatbotMessages'); |
|
|
const typingDiv = document.createElement('div'); |
|
|
typingDiv.className = 'chat-message bot'; |
|
|
typingDiv.id = 'typingIndicator'; |
|
|
|
|
|
const avatar = document.createElement('div'); |
|
|
avatar.className = 'message-avatar bot'; |
|
|
avatar.textContent = '🤖'; |
|
|
|
|
|
const typing = document.createElement('div'); |
|
|
typing.className = 'typing-indicator'; |
|
|
typing.innerHTML = '<div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div>'; |
|
|
|
|
|
typingDiv.appendChild(avatar); |
|
|
typingDiv.appendChild(typing); |
|
|
messagesContainer.appendChild(typingDiv); |
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight; |
|
|
}, |
|
|
|
|
|
hideTyping() { |
|
|
const typing = document.getElementById('typingIndicator'); |
|
|
if (typing) typing.remove(); |
|
|
}, |
|
|
|
|
|
processQuery(query) { |
|
|
const lowerQuery = query.toLowerCase(); |
|
|
let response = ''; |
|
|
|
|
|
|
|
|
if (lowerQuery.match(/berapa.*sapi|total.*cattle|jumlah.*sapi/)) { |
|
|
response = this.getCattleCountResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/sapi.*sakit|sick.*cattle|kesehatan.*sapi/)) { |
|
|
response = this.getSickCattleResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/sapi.*panen|harvest.*ready|siap.*panen/)) { |
|
|
response = this.getHarvestReadyResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/adg|average.*daily.*gain|pertambahan.*bobot/)) { |
|
|
response = this.getADGResponse(); |
|
|
} |
|
|
|
|
|
|
|
|
else if (lowerQuery.match(/stok.*jagung|corn.*stock|jagung/)) { |
|
|
response = this.getCornStockResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/stock.*out|stok.*habis|inventory.*critical/)) { |
|
|
response = this.getCriticalStockResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/inventory.*status|status.*inventory|cek.*inventory/)) { |
|
|
response = this.getInventoryStatusResponse(); |
|
|
} |
|
|
|
|
|
|
|
|
else if (lowerQuery.match(/status.*po|po.*status|purchase.*order/)) { |
|
|
response = this.getPOStatusResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/po.*pending|pending.*po/)) { |
|
|
response = this.getPendingPOResponse(); |
|
|
} |
|
|
|
|
|
|
|
|
else if (lowerQuery.match(/revenue|pendapatan|penjualan.*bulan/)) { |
|
|
response = this.getRevenueResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/profit|laba|margin/)) { |
|
|
response = this.getProfitResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/cash.*balance|saldo.*kas/)) { |
|
|
response = this.getCashBalanceResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/financial.*summary|ringkasan.*keuangan/)) { |
|
|
response = this.getFinancialSummaryResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/ap.*balance|accounts.*payable|hutang/)) { |
|
|
response = this.getAPBalanceResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/ar.*balance|accounts.*receivable|piutang/)) { |
|
|
response = this.getARBalanceResponse(); |
|
|
} |
|
|
else if (lowerQuery.match(/asset.*value|nilai.*aset/)) { |
|
|
response = this.getAssetValueResponse(); |
|
|
} |
|
|
|
|
|
|
|
|
else if (lowerQuery.match(/karyawan|employee|attendance/)) { |
|
|
response = this.getEmployeeResponse(); |
|
|
} |
|
|
|
|
|
|
|
|
else if (lowerQuery.match(/dashboard|ringkasan.*sistem|summary/)) { |
|
|
response = this.getDashboardSummaryResponse(); |
|
|
} |
|
|
|
|
|
|
|
|
else if (lowerQuery.match(/generate.*report|buat.*laporan|laporan/)) { |
|
|
response = this.getReportGenerationResponse(); |
|
|
} |
|
|
|
|
|
|
|
|
else if (lowerQuery.match(/health.*alert|alert.*kesehatan/)) { |
|
|
response = this.getHealthAlertsResponse(); |
|
|
} |
|
|
|
|
|
|
|
|
else { |
|
|
response = this.getDefaultResponse(query); |
|
|
} |
|
|
|
|
|
const messageContent = this.addMessage(response, 'bot'); |
|
|
this.addActionButtons(messageContent, lowerQuery); |
|
|
}, |
|
|
|
|
|
getCattleCountResponse() { |
|
|
return `<strong>📊 Total Sapi - Semua Lokasi</strong><br><br> |
|
|
📍 <strong>Lampung:</strong> 5,200 ekor<br> |
|
|
📍 <strong>Medan:</strong> 3,250 ekor<br> |
|
|
<strong>Total: 8,450 ekor</strong><br><br> |
|
|
📈 <strong>Trend:</strong> +234 ekor bulan ini (+2.8%)<br> |
|
|
✅ <strong>Health Rate:</strong> 99.2%<br> |
|
|
🎯 <strong>Harvest Ready:</strong> 145 ekor`; |
|
|
}, |
|
|
|
|
|
getSickCattleResponse() { |
|
|
return `<strong>🏥 Sapi Sakit - Minggu Ini (Nov 3-9, 2025)</strong><br><br> |
|
|
📍 <strong>Lampung:</strong> 5 sapi<br> |
|
|
• 3 respirasi<br> |
|
|
• 2 digestif<br><br> |
|
|
📍 <strong>Medan:</strong> 2 sapi<br> |
|
|
• 1 mastitis<br> |
|
|
• 1 lameness<br><br> |
|
|
<strong>Total: 7 ekor</strong> (0.08% dari populasi)<br> |
|
|
🔔 <strong>1 case butuh immediate attention</strong> - sapi C089 di Medan<br> |
|
|
📊 <strong>Trend:</strong> Naik 2 kasus vs minggu lalu`; |
|
|
}, |
|
|
|
|
|
getHarvestReadyResponse() { |
|
|
return `<strong>✅ Sapi Siap Panen</strong><br><br> |
|
|
<strong>Total: 145 ekor</strong> telah mencapai target weight<br><br> |
|
|
📍 Lampung: 92 ekor (avg 485 kg)<br> |
|
|
📍 Medan: 53 ekor (avg 478 kg)<br><br> |
|
|
📅 <strong>Estimasi Revenue:</strong> IDR 950M<br> |
|
|
💡 <strong>Rekomendasi:</strong> Harvest dalam 7-10 hari untuk optimal margin`; |
|
|
}, |
|
|
|
|
|
getADGResponse() { |
|
|
return `<strong>📈 Average Daily Gain (ADG) Analysis</strong><br><br> |
|
|
<strong>ADG Rata-rata: 1.28 kg/hari</strong><br> |
|
|
🎯 Target: 1.2 kg/hari ✅<br><br> |
|
|
📍 Lampung: 1.32 kg/hari<br> |
|
|
📍 Medan: 1.22 kg/hari<br><br> |
|
|
<strong>Top Performers (ADG > 1.4):</strong><br> |
|
|
• C004: 1.41 kg/hari (Brahman)<br> |
|
|
• C012: 1.38 kg/hari (Simmental)<br><br> |
|
|
⚠️ <strong>Low ADG (<1.0):</strong> 12 sapi need review`; |
|
|
}, |
|
|
|
|
|
getCornStockResponse() { |
|
|
return `<strong>🌽 Jagung Giling (FEED-001) - Stock Summary</strong><br><br> |
|
|
📍 <strong>Lampung-WH1:</strong> 45,000 kg (Stock Aman ✓)<br> |
|
|
📍 <strong>Medan-WH1:</strong> 12,500 kg (⚠️ Cukup untuk 2.1 hari)<br> |
|
|
<strong>Total: 57,500 kg</strong><br><br> |
|
|
📈 <strong>Daily Consumption:</strong> ~5,800 kg<br> |
|
|
🔮 <strong>Forecast:</strong> Medan stock habis dalam <strong>2.1 hari</strong><br><br> |
|
|
💡 <strong>Rekomendasi:</strong> Transfer 20,000 kg dari Lampung atau order baru segera`; |
|
|
}, |
|
|
|
|
|
getCriticalStockResponse() { |
|
|
return `<strong>⚠️ Critical Stock Alert</strong><br><br> |
|
|
<table class="data-table-chat"> |
|
|
<tr><th>Item</th><th>Stock</th><th>Days Left</th></tr> |
|
|
<tr><td>Jagung (Medan)</td><td>12,500 kg</td><td class="metric-highlight">2.1</td></tr> |
|
|
<tr><td>Antibiotik</td><td>85 btl</td><td class="metric-highlight">4.2</td></tr> |
|
|
<tr><td>Vitamin Premix</td><td>2,400 kg</td><td class="metric-highlight">5.8</td></tr> |
|
|
</table><br> |
|
|
🚨 <strong>Action Required:</strong> 3 items perlu immediate reorder`; |
|
|
}, |
|
|
|
|
|
getInventoryStatusResponse() { |
|
|
return `<strong>📦 Inventory Status - All Locations</strong><br><br> |
|
|
<strong>Total Stock Value: IDR 18.5B</strong><br><br> |
|
|
✅ <strong>In Stock:</strong> 287 SKUs<br> |
|
|
⚠️ <strong>Low Stock:</strong> 23 SKUs<br> |
|
|
🚨 <strong>Stock Out:</strong> 0 SKUs<br><br> |
|
|
<strong>By Category:</strong><br> |
|
|
• Pakan: IDR 12.2B (66%)<br> |
|
|
• Obat: IDR 3.8B (21%)<br> |
|
|
• Suplemen: IDR 2.5B (13%)<br><br> |
|
|
📊 <strong>Inventory Turnover:</strong> 8.5x/year`; |
|
|
}, |
|
|
|
|
|
getPOStatusResponse() { |
|
|
return `<strong>📋 Purchase Order Status</strong><br><br> |
|
|
<table class="data-table-chat"> |
|
|
<tr><th>PO Number</th><th>Status</th><th>Value</th></tr> |
|
|
<tr><td>PO-2025-001</td><td><span class="status-badge status-good">Approved</span></td><td>IDR 750M</td></tr> |
|
|
<tr><td>PO-2025-002</td><td><span class="status-badge status-info">In Transit</span></td><td>IDR 4.2B</td></tr> |
|
|
<tr><td>PO-2025-003</td><td><span class="status-badge status-warning">Pending</span></td><td>IDR 125M</td></tr> |
|
|
</table><br> |
|
|
<strong>Total Open POs: 3</strong><br> |
|
|
Total Value: IDR 5.1B`; |
|
|
}, |
|
|
|
|
|
getPendingPOResponse() { |
|
|
return `<strong>⏳ Pending Purchase Orders</strong><br><br> |
|
|
<strong>PO-2025-003</strong><br> |
|
|
Vendor: PT Medika Veteriner<br> |
|
|
Item: Vaksin & Obat<br> |
|
|
Value: IDR 125M<br> |
|
|
Status: Awaiting approval<br> |
|
|
Expected Delivery: Nov 20, 2025<br><br> |
|
|
💡 <strong>Action:</strong> Review dan approve untuk avoid stock out`; |
|
|
}, |
|
|
|
|
|
getRevenueResponse() { |
|
|
return `<strong>💰 Revenue Bulan Ini (November 2025)</strong><br><br> |
|
|
<strong class="metric-highlight">Total: IDR 45.2B</strong><br> |
|
|
📈 +12.5% vs Oktober (IDR 40.2B)<br><br> |
|
|
<strong>By Category:</strong><br> |
|
|
• Cattle Sales: IDR 29.4B (65%)<br> |
|
|
• Feed Sales: IDR 9.0B (20%)<br> |
|
|
• Breeding: IDR 3.6B (8%)<br> |
|
|
• Other: IDR 3.2B (7%)<br><br> |
|
|
🎯 <strong>Target:</strong> IDR 42B ✅ (108% achieved)`; |
|
|
}, |
|
|
|
|
|
getProfitResponse() { |
|
|
return `<strong>📊 Profitability Analysis - November 2025</strong><br><br> |
|
|
<strong>Revenue:</strong> IDR 45.2B<br> |
|
|
<strong>COGS:</strong> IDR 32.8B<br> |
|
|
<strong class="metric-highlight">Gross Profit: IDR 12.4B</strong><br> |
|
|
<strong>Gross Margin: 27.4%</strong><br><br> |
|
|
<strong>By Location:</strong><br> |
|
|
📍 Lampung: 29.2% margin<br> |
|
|
📍 Medan: 24.1% margin<br><br> |
|
|
📈 Margin improvement: +2.1% vs target`; |
|
|
}, |
|
|
|
|
|
getCashBalanceResponse() { |
|
|
return `<strong>🏦 Cash & Bank Balance</strong><br><br> |
|
|
<strong class="metric-highlight">Available Cash: IDR 8.5B</strong><br><br> |
|
|
💵 Cash in Hand: IDR 450M<br> |
|
|
🏦 Bank Accounts: IDR 8.05B<br><br> |
|
|
<strong>Upcoming:</strong><br> |
|
|
📥 Receivables (30d): IDR 12.2B<br> |
|
|
📤 Payables (30d): IDR 8.8B<br><br> |
|
|
✅ <strong>Net Position:</strong> Healthy`; |
|
|
}, |
|
|
|
|
|
getAPBalanceResponse() { |
|
|
const ap = state.data.financeAccounting.accountsPayable; |
|
|
return `<strong>📤 Accounts Payable Summary</strong><br><br> |
|
|
<strong class="metric-highlight">Total Payable: ${formatCurrency(ap.summary.total_payable)}</strong><br><br> |
|
|
✅ Paid: ${formatCurrency(ap.summary.paid)}<br> |
|
|
⏳ Unpaid: ${formatCurrency(ap.summary.unpaid)}<br> |
|
|
🚨 Overdue: ${formatCurrency(ap.summary.overdue)}<br><br> |
|
|
<strong>Recent Invoices:</strong><br> |
|
|
${ap.invoices.slice(0, 3).map(inv => `• ${inv.vendor}: ${formatCurrency(inv.amount)} - ${inv.payment_status}`).join('<br>')}<br><br> |
|
|
💡 ${ap.invoices.filter(i => i.payment_status === 'Overdue').length} invoice(s) need immediate attention`; |
|
|
}, |
|
|
|
|
|
getARBalanceResponse() { |
|
|
const ar = state.data.financeAccounting.accountsReceivable; |
|
|
return `<strong>📥 Accounts Receivable Summary</strong><br><br> |
|
|
<strong class="metric-highlight">Total Receivable: ${formatCurrency(ar.summary.total_receivable)}</strong><br><br> |
|
|
✅ Collected: ${formatCurrency(ar.summary.collected)}<br> |
|
|
⏳ Outstanding: ${formatCurrency(ar.summary.outstanding)}<br> |
|
|
🚨 Overdue: ${formatCurrency(ar.summary.overdue)}<br><br> |
|
|
📊 <strong>DSO:</strong> ${ar.summary.dso} days<br><br> |
|
|
<strong>Top Outstanding:</strong><br> |
|
|
${ar.invoices.filter(i => i.payment_status !== 'Paid').slice(0, 3).map(inv => `• ${inv.customer}: ${formatCurrency(inv.amount)}`).join('<br>')}`; |
|
|
}, |
|
|
|
|
|
getAssetValueResponse() { |
|
|
const assets = state.data.financeAccounting.assets; |
|
|
return `<strong>🏢 Fixed Assets Summary</strong><br><br> |
|
|
<strong>Total Original Cost: ${formatCurrency(assets.summary.total_assets)}</strong><br> |
|
|
<strong class="metric-highlight">Net Book Value: ${formatCurrency(assets.summary.net_book_value)}</strong><br><br> |
|
|
📉 Accumulated Depreciation: ${formatCurrency(assets.summary.total_depreciation)}<br> |
|
|
✅ Active Assets: ${assets.fixedAssets.filter(a => a.status === 'Active').length}<br><br> |
|
|
<strong>By Category:</strong><br> |
|
|
• Building: ${assets.fixedAssets.filter(a => a.category === 'Building').length} assets<br> |
|
|
• Equipment: ${assets.fixedAssets.filter(a => a.category === 'Equipment').length} assets<br> |
|
|
• Vehicle: ${assets.fixedAssets.filter(a => a.category === 'Vehicle').length} assets`; |
|
|
}, |
|
|
|
|
|
getFinancialSummaryResponse() { |
|
|
return `<strong>💹 Financial Summary - November 2025</strong><br><br> |
|
|
<strong>Revenue:</strong> IDR 45.2B (+12.5%)<br> |
|
|
<strong>Expenses:</strong> IDR 32.8B (+8.3%)<br> |
|
|
<strong class="metric-highlight">Net Profit: IDR 12.4B</strong><br> |
|
|
<strong>Profit Margin: 27.4%</strong><br><br> |
|
|
<strong>Key Metrics:</strong><br> |
|
|
• ROI: 34.8%<br> |
|
|
• Cash Balance: IDR 8.5B<br> |
|
|
• AR Outstanding: IDR 12.2B<br> |
|
|
• AP Outstanding: IDR 8.8B<br><br> |
|
|
✅ All metrics above target`; |
|
|
}, |
|
|
|
|
|
getEmployeeResponse() { |
|
|
return `<strong>👥 Employee & Attendance Summary</strong><br><br> |
|
|
<strong>Total Employees: 342</strong><br> |
|
|
✅ Active: 342<br> |
|
|
📅 New Hires (Nov): 8<br><br> |
|
|
<strong>Attendance Rate: 96.8%</strong><br> |
|
|
🎯 Target: 95% ✅<br><br> |
|
|
<strong>By Department:</strong><br> |
|
|
• Operations: 185<br> |
|
|
• Animal Health: 45<br> |
|
|
• Logistics: 52<br> |
|
|
• Finance: 28<br> |
|
|
• Other: 32`; |
|
|
}, |
|
|
|
|
|
getDashboardSummaryResponse() { |
|
|
return `<strong>🎯 Dashboard Summary - JJAA ERP System</strong><br><br> |
|
|
<strong>Business Highlights:</strong><br> |
|
|
💰 Revenue: IDR 45.2B (+12.5%)<br> |
|
|
🐄 Total Cattle: 8,450 (+234)<br> |
|
|
👥 Employees: 342 (96.8% attendance)<br> |
|
|
📦 Stock Value: IDR 18.5B<br><br> |
|
|
<strong>Key Alerts:</strong><br> |
|
|
🚨 8 health alerts (3 critical)<br> |
|
|
⚠️ 23 low stock items<br> |
|
|
⏳ 12 pending PO approvals<br><br> |
|
|
<strong>Performance:</strong><br> |
|
|
✅ ADG: 1.28 kg/day (target: 1.2)<br> |
|
|
✅ Health Rate: 99.2%<br> |
|
|
✅ Profit Margin: 27.4%`; |
|
|
}, |
|
|
|
|
|
getHealthAlertsResponse() { |
|
|
return `<strong>🚨 Health Alerts - Current Status</strong><br><br> |
|
|
<strong>Critical (3):</strong><br> |
|
|
🔴 Sapi C089 - Lameness (immediate attention)<br> |
|
|
🔴 Sapi C145 - High fever (monitoring)<br> |
|
|
🔴 Sapi C203 - Respiratory distress<br><br> |
|
|
<strong>Under Treatment (5):</strong><br> |
|
|
🟡 5 sapi receiving antibiotics<br><br> |
|
|
<strong>Vaccination Due (12):</strong><br> |
|
|
🟢 12 sapi scheduled for PMK vaccine<br><br> |
|
|
📊 <strong>Overall Health Rate: 99.2%</strong>`; |
|
|
}, |
|
|
|
|
|
getReportGenerationResponse() { |
|
|
return `<strong>📄 Report Generation Ready</strong><br><br> |
|
|
Pilih report yang ingin Anda generate:<br><br> |
|
|
<strong>Available Reports:</strong><br> |
|
|
• Weekly Health Summary<br> |
|
|
• Monthly Profitability Report<br> |
|
|
• Inventory Valuation Report<br> |
|
|
• FCR Analysis Report<br> |
|
|
• Vendor Performance Report<br><br> |
|
|
💡 Klik tombol di bawah untuk generate report yang dipilih`; |
|
|
}, |
|
|
|
|
|
getDefaultResponse(query) { |
|
|
return `Terima kasih atas pertanyaan Anda: "<em>${query}</em>"<br><br> |
|
|
Saya dapat membantu Anda dengan:<br> |
|
|
🐄 Data livestock & kesehatan sapi<br> |
|
|
📦 Status inventory & stock<br> |
|
|
💰 Informasi keuangan & revenue<br> |
|
|
📋 Purchase order & procurement<br> |
|
|
👥 Data karyawan & attendance<br> |
|
|
📊 Report generation<br><br> |
|
|
Silakan coba pertanyaan yang lebih spesifik atau pilih dari suggested questions di atas.`; |
|
|
}, |
|
|
|
|
|
addActionButtons(messageContent, query) { |
|
|
const actionsDiv = document.createElement('div'); |
|
|
actionsDiv.className = 'message-actions'; |
|
|
|
|
|
|
|
|
if (query.match(/inventory|stok|stock/)) { |
|
|
actionsDiv.innerHTML = ` |
|
|
<button class="action-btn" onclick="loadModule('inventory')">📦 View in System</button> |
|
|
<button class="action-btn" onclick="showToast('Export started')">⬇️ Export CSV</button> |
|
|
`; |
|
|
} |
|
|
else if (query.match(/cattle|sapi|livestock/)) { |
|
|
actionsDiv.innerHTML = ` |
|
|
<button class="action-btn" onclick="loadModule('feedlot')">🐄 View in System</button> |
|
|
<button class="action-btn" onclick="showToast('Report generated')">📄 Generate Report</button> |
|
|
`; |
|
|
} |
|
|
else if (query.match(/po|purchase|procurement/)) { |
|
|
actionsDiv.innerHTML = ` |
|
|
<button class="action-btn" onclick="loadModule('procurement')">🛒 View in System</button> |
|
|
<button class="action-btn" onclick="showToast('Approval sent')">✅ Approve</button> |
|
|
`; |
|
|
} |
|
|
else if (query.match(/revenue|profit|financial|keuangan/)) { |
|
|
actionsDiv.innerHTML = ` |
|
|
<button class="action-btn" onclick="loadModule('finance')">💰 View in System</button> |
|
|
<button class="action-btn" onclick="showToast('Report exported')">📊 Export Report</button> |
|
|
`; |
|
|
} |
|
|
else if (query.match(/dashboard|summary/)) { |
|
|
actionsDiv.innerHTML = ` |
|
|
<button class="action-btn" onclick="loadModule('dashboard'); chatbot.closeChat();">🏠 Go to Dashboard</button> |
|
|
`; |
|
|
} |
|
|
else if (query.match(/report|laporan/)) { |
|
|
actionsDiv.innerHTML = ` |
|
|
<button class="action-btn" onclick="loadModule('bi')">📊 View BI Module</button> |
|
|
<button class="action-btn" onclick="showToast('PDF report generated')">📄 Download PDF</button> |
|
|
<button class="action-btn" onclick="showToast('Report emailed')">📧 Email Report</button> |
|
|
`; |
|
|
} |
|
|
|
|
|
if (actionsDiv.innerHTML) { |
|
|
messageContent.appendChild(actionsDiv); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
init(); |
|
|
chatbot.init(); |
|
|
}); |