import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { LayoutDashboard, Boxes, Users, Lock, ArrowDownLeft, FileCheck2, Truck, Receipt, BookOpen, ShieldCheck, ShieldAlert, Crown, Warehouse, Plus, Search, Printer, X, CheckCircle2, AlertTriangle, Database, LogOut, ChevronRight, Beaker, FileText, Building, UserCheck, Calendar, DollarSign, Copy, ExternalLink, KeyRound, LogIn, UserPlus, HelpCircle, Info } from 'lucide-react'; const ROMAN_MONTHS = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII']; const formatRp = (num) => { return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(num || 0); }; const generateDocNumber = (prefix, partnerName) => { const cleanName = (partnerName || 'PGS').split(' ')[1] || (partnerName || 'PGS').split(' ')[0] || 'PGS'; const now = new Date(); const rom = ROMAN_MONTHS[now.getMonth()]; const dateNum = String(now.getDate()).padStart(2, '0'); const year = now.getFullYear(); const seq = String(Math.floor(Math.random() * 899) + 100); return `${prefix}-PGS/${cleanName.toUpperCase().replace(/[^A-Z]/g, '')}/${rom}-${dateNum}/${year}/${seq}`; }; const DEFAULT_USERS = [ { id: 'usr-mgmt-01', email: 'direksi@pratachem.com', password: 'password123', name: 'Cia Liu', title: 'Assistant Director & Management', role: 'management' }, { id: 'usr-adm-01', email: 'admin.sales@pratachem.com', password: 'password123', name: 'Siti Rahma', title: 'Admin & Sales Office', role: 'admin' }, { id: 'usr-gdg-01', email: 'gudang@pratachem.com', password: 'password123', name: 'Budi Santoso', title: 'Kepala Gudang Tangerang', role: 'gudang' } ]; const INITIAL_CUSTOMERS = [ { id: 'cust-001', name: 'PT. MAYORA INDAH TBK', address: 'Kawasan Industri Jatake, Jl. Industri Raya No. 18, Tangerang, Banten', npwp: '01.345.678.9-056.000', pic: 'Herman Susanto (Procurement Head)', responsible: 'Andre Atmadja (Direksi)', top: 'TOP 30 Hari', creditLimit: 500000000, emailFinance: 'invoicing.jatake@mayora.co.id', phoneFinance: '021-590-8888' }, { id: 'cust-002', name: 'PT. INDOFOOD CBP SUKSES MAKMUR TBK', address: 'Kawasan Industri Indotaisei Sektor IA, Kalihurip, Cikampek, Karawang', npwp: '01.234.567.8-092.000', pic: 'Bambang Irawan (Purchasing Manager)', responsible: 'Axton Salim (Direksi)', top: 'TOP 30 Hari', creditLimit: 750000000, emailFinance: 'ap.chemicals@icbp.indofood.co.id', phoneFinance: '0264-351-234' }, { id: 'cust-003', name: 'PT. WINGS SURYA', address: 'Jl. Kalisosok Kidul No. 2, Krembangan, Kota Surabaya, Jawa Timur', npwp: '01.111.222.3-604.000', pic: 'Stefanus Wijaya (Raw Materials Buyer)', responsible: 'Ferdinand Katuari (Direksi)', top: 'TOP 14 Hari', creditLimit: 300000000, emailFinance: 'finance.wings@wingscorp.com', phoneFinance: '031-353-3333' } ]; const INITIAL_SUPPLIERS = [ { id: 'supp-001', name: 'SINOCHEM HEBEI CO., LTD.', address: 'No. 88 Lianmeng Road, Shijiazhuang, Hebei 050061, China', npwp: '02.888.777.6-033.000', pic: 'Mr. Chen Wei (Overseas Sales Dir)', responsible: 'Dr. Zhang Yong (President)', top: 'TOP 30 Hari', emailFinance: 'export.finance@sinochem.hebei.com', phoneFinance: '+86 311 8899 1122' }, { id: 'supp-002', name: 'ROQUETTE FRERES FRANCE', address: '1 Rue de la Haute Loge, 62136 Lestrem, France', npwp: '02.555.444.3-033.000', pic: 'Jean-Paul Sartre (Asia Pacific Mgr)', responsible: 'Edouard Roquette (Chairman)', top: 'TOP 60 Hari (LC)', emailFinance: 'ap.invoicing@roquette.com', phoneFinance: '+33 3 21 63 36 00' }, { id: 'supp-003', name: 'TTCA WEIFANG CO., LTD.', address: 'Weifang Coastal Economic Development Zone, Shandong, China', npwp: '02.333.222.1-033.000', pic: 'Ms. Wang Lin (Export Division)', responsible: 'Liu Baojun (General Manager)', top: 'Cash Advance / T/T', emailFinance: 'finance@ttca.cn', phoneFinance: '+86 536 8877 665' } ]; const INITIAL_CHEMICALS = [ { id: 'chem-001', sku: 'PRCF0001', warehouse: 'C', category: 'F', name: 'Citric Acid Anhydrous', origin: 'TTCA Weifang, China', form: 'Powder', packageType: 'Zak', weightPerUnit: 25, pricePerKg: 28500, bookedKg: 500, batches: [ { id: 'b-01', lotNumber: 'LOT-TTCA-2026-08', origin: 'TTCA Weifang', inflowDate: '2026-08-10', expDate: '2028-08-09', pkgCount: 40, remainingKg: 1000 }, { id: 'b-02', lotNumber: 'LOT-TTCA-2026-09', origin: 'TTCA Weifang', inflowDate: '2026-09-01', expDate: '2028-09-01', pkgCount: 60, remainingKg: 1500 } ] }, { id: 'chem-002', sku: 'PRTF0002', warehouse: 'T', category: 'F', name: 'High Fructose Corn Syrup 55%', origin: 'Roquette Freres, France', form: 'Liquid', packageType: 'Jerigen', weightPerUnit: 25, pricePerKg: 32000, bookedKg: 250, batches: [ { id: 'b-03', lotNumber: 'LOT-RQ-2026-014', origin: 'Roquette France', inflowDate: '2026-09-12', expDate: '2027-09-11', pkgCount: 30, remainingKg: 750 } ] }, { id: 'chem-003', sku: 'PRTI0003', warehouse: 'T', category: 'I', name: 'Trichloroisocyanuric Acid (TCCA 90%)', origin: 'Sinochem Hebei, China', form: 'Granular', packageType: 'Drum', weightPerUnit: 50, pricePerKg: 46000, bookedKg: 0, batches: [ { id: 'b-04', lotNumber: 'LOT-SINO-2026-55', origin: 'Sinochem Hebei', inflowDate: '2026-09-15', expDate: '2029-09-14', pkgCount: 40, remainingKg: 2000 } ] } ]; export default function App() { const [usersList, setUsersList] = useState(() => { try { const saved = localStorage.getItem('pratachem_users'); return saved ? JSON.parse(saved) : DEFAULT_USERS; } catch { return DEFAULT_USERS; } }); const [currentUser, setCurrentUser] = useState(() => { try { const saved = localStorage.getItem('pratachem_session'); return saved ? JSON.parse(saved) : null; } catch { return null; } }); const [activeTab, setActiveTab] = useState('dashboard'); const [authTab, setAuthTab] = useState('login'); const [toast, setToast] = useState(null); const [customers, setCustomers] = useState(INITIAL_CUSTOMERS); const [suppliers, setSuppliers] = useState(INITIAL_SUPPLIERS); const [chemicals, setChemicals] = useState(INITIAL_CHEMICALS); const [supplierPOs, setSupplierPOs] = useState([ { id: 'po-1', poNumber: 'PO-PGS/SINOCHEM/IX-20/2026/001', date: '2026-09-20', supplier: 'SINOCHEM HEBEI CO., LTD.', chemId: 'chem-003', chemName: 'Trichloroisocyanuric Acid (TCCA 90%)', packageCount: 40, totalKg: 2000, pricePerKg: 42000, subtotal: 84000000, ppn: 9240000, grandTotal: 93240000, status: 'Received' } ]); const [quotations, setQuotations] = useState([ { id: 'quo-1', quoNumber: 'Quo-PGS/MAYORA/IX-24/2026/001', date: '2026-09-24', customer: 'PT. MAYORA INDAH TBK', customerId: 'cust-001', customerPO: 'PO-MYR/09/2026/881', chemId: 'chem-002', chemName: 'High Fructose Corn Syrup 55%', packageCount: 10, totalKg: 250, pricePerKg: 34500, terms: 'TOP 30 Hari', address: 'Kawasan Industri Jatake, Tangerang', status: 'Shipped', dispatchId: 'sj-1' } ]); const [dispatches, setDispatches] = useState([ { id: 'sj-1', sjNumber: 'SJ-PGS/MAYORA/IX-26/2026/001', quoId: 'quo-1', customer: 'PT. MAYORA INDAH TBK', customerPO: 'PO-MYR/09/2026/881', chemId: 'chem-002', chemName: 'High Fructose Corn Syrup 55%', packageCount: 10, packageType: 'Jerigen', totalKg: 250, address: 'Kawasan Industri Jatake, Tangerang', driverName: 'Septian (Deliveree CDE)', driverPhone: '0812-9988-7711', nopol: 'B 1583 VOR', pickupTime: '2026-09-26 10:00', status: 'Delivered', podReceiver: 'Herman Susanto (QC Pass)', podCompletionTime: '2026-09-26 14:30', podNotes: 'Segel 10 jerigen utuh dan lengkap', invoiceId: 'inv-1' } ]); const [invoices, setInvoices] = useState([ { id: 'inv-1', invNumber: 'INV-PGS/MAYORA/IX-26/2026/001', dispatchId: 'sj-1', sjNumber: 'SJ-PGS/MAYORA/IX-26/2026/001', customer: 'PT. MAYORA INDAH TBK', customerPO: 'PO-MYR/09/2026/881', date: '2026-09-26', dueDate: '2026-10-26', terms: 'TOP 30 Hari', totalKg: 250, pricePerKg: 34500, subtotal: 8625000, ppn: 948750, grandTotal: 9573750, fakturPajak: '010.002-26.12345678', status: 'Unpaid' } ]); const [logbook, setLogbook] = useState([ { id: 'log-1', type: 'Stok in', time: '21/09/2026, 09.15', driverInfo: 'Andi (B 2020 TOR)', chemInfo: 'Citric Acid Anhydrous (PRCF0001)', qty: '40 Zak (1.000 Kg)', condition: 'Segel karung utuh, bersih, lolos uji QC', officer: 'Budi Santoso' }, { id: 'log-2', type: 'Stok out', time: '26/09/2026, 10.30', driverInfo: 'Septian (B 1583 VOR)', chemInfo: 'High Fructose Corn Syrup 55% (PRTF0002)', qty: '10 Jerigen (250 Kg)', condition: 'Segel drum rapat, muat selesai tanpa tumpah', officer: 'Budi Santoso' } ]); const [modalType, setModalType] = useState(null); const [selectedBatchChem, setSelectedBatchChem] = useState(null); const [printDoc, setPrintDoc] = useState(null); const [activeQuoForDispatch, setActiveQuoForDispatch] = useState(null); const [activeDispatchForPod, setActiveDispatchForPod] = useState(null); const [activeDispatchForInv, setActiveDispatchForInv] = useState(null); const [appDialog, setAppDialog] = useState(null); const [supabaseConfig, setSupabaseConfig] = useState({ url: '', anonKey: '', isConnected: true }); const showToast = (message, type = 'success') => { setToast({ message, type }); setTimeout(() => setToast(null), 3500); }; const triggerDialog = (title, message, isConfirm = false, onConfirm = null) => { setAppDialog({ title, message, isConfirm, onConfirm }); }; const closeDialog = () => { setAppDialog(null); }; const handleLoginSubmit = (e) => { e.preventDefault(); const email = e.target.email.value.trim().toLowerCase(); const password = e.target.password.value; const matched = usersList.find( (u) => u.email.toLowerCase() === email && u.password === password ); if (matched) { setCurrentUser(matched); localStorage.setItem('pratachem_session', JSON.stringify(matched)); showToast(`Login berhasil sebagai ${matched.name} (${matched.role.toUpperCase()})`); if (matched.role !== 'management' && activeTab === 'suppliers') { setActiveTab('dashboard'); } } else { triggerDialog( 'Gagal Masuk', 'Kombinasi email atau kata sandi tidak cocok. Silakan periksa kembali atau daftarkan akun baru Anda.' ); } }; const handleRegisterSubmit = (e) => { e.preventDefault(); const name = e.target.regName.value.trim(); const email = e.target.regEmail.value.trim().toLowerCase(); const role = e.target.regRole.value; const password = e.target.regPass.value; if (!name || !email || !password) { triggerDialog('Pendaftaran Tidak Lengkap', 'Harap isi semua kolom pendaftaran.'); return; } if (usersList.some((u) => u.email.toLowerCase() === email)) { triggerDialog( 'Email Sudah Terdaftar', 'Email tersebut sudah terdaftar di sistem ERP Pratachem. Silakan gunakan menu Masuk (Login).' ); setAuthTab('login'); return; } let title = 'Staf Operasional'; if (role === 'management') title = 'Direksi & Management Pratachem'; else if (role === 'admin') title = 'Admin & Sales Office'; else if (role === 'gudang') title = 'Kepala / Staf Gudang'; const newUser = { id: 'usr-' + Date.now(), name, email, role, title, password }; const updatedUsers = [...usersList, newUser]; setUsersList(updatedUsers); localStorage.setItem('pratachem_users', JSON.stringify(updatedUsers)); setCurrentUser(newUser); localStorage.setItem('pratachem_session', JSON.stringify(newUser)); showToast(`Pendaftaran berhasil! Selamat datang, ${name}`); }; const handleLogout = () => { triggerDialog( 'Konfirmasi Keluar', 'Apakah Anda yakin ingin keluar dari sesi akun ERP Pratachem saat ini?', true, () => { setCurrentUser(null); localStorage.removeItem('pratachem_session'); setActiveTab('dashboard'); showToast('Anda telah berhasil keluar dari akun.'); } ); }; const inventoryStats = useMemo(() => { let totalPhysicalKg = 0; let totalBookedKg = 0; let tngCount = 0; let ckrCount = 0; chemicals.forEach((c) => { const chemPhysical = (c.batches || []).reduce((acc, b) => acc + (Number(b.remainingKg) || 0), 0); totalPhysicalKg += chemPhysical; totalBookedKg += Number(c.bookedKg) || 0; if (c.warehouse === 'T') tngCount++; if (c.warehouse === 'C') ckrCount++; }); const totalFreeKg = Math.max(0, totalPhysicalKg - totalBookedKg); return { skuCount: chemicals.length, totalPhysicalKg, totalBookedKg, totalFreeKg, tngCount, ckrCount }; }, [chemicals]); if (!currentUser) { return (

PT. PRATACHEM GLOBALINDO SUKSES

Pratachem Core System Apps, Version 2.0

{authTab === 'login' ? (
Karyawan baru dan belum memiliki akun?
) : (

Peran menentukan hak akses modul dan perlindungan data rahasia supplier.

Sudah memiliki akun terdaftar?
)}
{appDialog && (

{appDialog.title}

{appDialog.message}

)}
); } return (
setActiveTab('dashboard')}>
PRATACHEM ERP v5.2

PT. Pratachem Globalindo Sukses • Chemical Trading & Logistics

{currentUser.name.charAt(0).toUpperCase()}
{currentUser.name}
{currentUser.role.toUpperCase()}
{currentUser.role !== 'gudang' && ( )} {currentUser.role === 'management' && ( )} {currentUser.role !== 'gudang' && ( )} {currentUser.role !== 'gudang' && ( )}
{}
Hak Akses: {currentUser.title}

Sistem ERP & WMS Bahan Kimia Terpadu

{currentUser.role === 'management' ? 'Mode Direksi: Akses penuh termasuk Direktori Rahasia Supplier & Approval Pembukuan.' : currentUser.role === 'admin' ? 'Mode Admin: Input transaksi, PO & Invoice e-Faktur. Data Supplier Prinsipal TERBLOKIR TOTAL.' : 'Mode Lapangan Gudang: Verifikasi armada supir Deliveree, potong batch FIFO riil, dan catat fisik gerbang.'}

{currentUser.role !== 'gudang' && ( )}
{} {activeTab === 'dashboard' && (
Total SKU Bahan
{inventoryStats.skuCount} SKU
{inventoryStats.tngCount} Tangerang • {inventoryStats.ckrCount} Cikarang
Stok Fisik Gudang
{inventoryStats.totalPhysicalKg.toLocaleString('id-ID')} Kg
Stok riil di atas rak & pallet
Stok Terkunci (Booked)
{inventoryStats.totalBookedKg.toLocaleString('id-ID')} Kg
Terkunci PO penawaran aktif
Free Stok (Siap Jual)
{inventoryStats.totalFreeKg.toLocaleString('id-ID')} Kg
Aman ditawarkan ke customer

Pengiriman Aktif Deliveree

Pencocokan armada fisik dan pemotongan stok FIFO otomatis

{dispatches.map((d) => (
{d.customer} {d.status}
{d.sjNumber} • {d.chemName} ({d.totalKg} Kg)
Supir: {d.driverName} ({d.nopol})
))}

Audit Logbook Gerbang

Pencatatan fisik harian

{logbook.slice(0, 3).map((l) => (
{l.type.toUpperCase()} {l.time}
{l.chemInfo}
{l.driverInfo} • {l.qty}
))}
)} {} {activeTab === 'inventory' && (

Master Barang & Struktur SKU Cerdas SKU Matrix

Komoditas Bahan Baku Kimia Non-Pompa • Manajemen Lapisan Batch FIFO Strict

{chemicals.map((chem) => { const physicalKg = (chem.batches || []).reduce((acc, b) => acc + (Number(b.remainingKg) || 0), 0); const freeKg = Math.max(0, physicalKg - (chem.bookedKg || 0)); return ( ); })}
Kode SKU Nama Bahan Baku Asal & Bentuk Kemasan Stok Fisik Booked Free Stok Batch Lots
{chem.name}
{chem.origin}
{chem.form}
{chem.packageType} (@ {chem.weightPerUnit} Kg) {physicalKg.toLocaleString('id-ID')} Kg {chem.bookedKg || 0} Kg {freeKg.toLocaleString('id-ID')} Kg
)} {} {activeTab === 'customers' && currentUser.role !== 'gudang' && (

Master Data Customer (Pelanggan Resmi) PRD Bab 08

Basis Data Tagihan e-Faktur 16 Digit • Pengawasan Plafon Piutang (Credit Limit)

Direktori Rekanan Pembeli Terdaftar Total: {customers.length} Customer
{customers.map((c) => ( ))}
Nama Perusahaan NPWP (16 Digit) Alamat Penagihan & Drop PIC & Penanggung Jawab Terms Plafon Piutang Kontak Finance
{c.name} {c.npwp} {c.address}
{c.pic}
P.J: {c.responsible}
{c.top} {formatRp(c.creditLimit)}
{c.phoneFinance}
{c.emailFinance}
)} {} {activeTab === 'suppliers' && ( currentUser.role === 'management' ? (

Master Data Supplier (Prinsipal / Pabrik) Strictly Confidential

Rahasia Dagang Pratachem • Tersembunyi Total dari Akun Admin Kantor & Tim Gudang

Ketentuan Proteksi Data Pemasok (PRD Bab 02 & Bab 09):

Data prinsipal, kontak representatif pabrik, dan rekening vendor adalah rahasia operasional manajemen. Pengguna dengan hak akses Admin Kantor dan Tim Gudang hanya dapat memilih nama vendor di formulir PO tanpa dapat melihat direktori atau profil lengkap ini.

Direktori Prinsipal & Pabrik Rekanan Total: {suppliers.length} Supplier
{suppliers.map((s) => ( ))}
Nama Pabrik / Supplier NPWP Pemasok Alamat Pabrik / Kantor PIC Representative TOP Email & No. HP Finance
{s.name} {s.npwp} {s.address}
{s.pic}
P.J: {s.responsible}
{s.top}
{s.phoneFinance}
{s.emailFinance}
) : (

AKSES DITOLAK: STRICTLY CONFIDENTIAL

Berdasarkan ketentuan PRD Bab 02 & Bab 09, modul Master Supplier memuat rahasia dagang perusahaan dan hanya dapat diakses oleh akun Management (Direksi & Owner).

) )} {} {activeTab === 'inbound' && (

Inbound & Penerbitan PO Resmi Supplier

Format PO-PGS Cetak Canon MG2500 (A4) • Penerimaan Batch FIFO & Verifikasi Fisik

{currentUser.role !== 'gudang' && ( )}
Daftar PO-PGS ke Pabrik Kimia Rumus: PO-PGS/[Supplier]/[Romawi]-[Tgl]/[Tahun]/[No]
{supplierPOs.map((po) => ( ))}
No. PO Resmi Tanggal Supplier Barang & Volume Total (+11% PPN) Status Aksi Dokumen
{po.poNumber} {po.date} {po.supplier}
{po.chemName}
{po.packageCount} Kemasan ({po.totalKg} Kg)
{formatRp(po.grandTotal)} {po.status}
)} {} {activeTab === 'sales' && currentUser.role !== 'gudang' && (

Penawaran Harga & Kunci Stok (Booked)

PO Customer Mengunci Free Stok • Mencegah Terjadinya Overselling Antartim Sales

Daftar Dokumen Penawaran (Quo-PGS) Rumus: Quo-PGS/[Customer]/[Romawi]-[Tgl]/[Tahun]/[No]
{quotations.map((quo) => ( ))}
No. Penawaran Customer & PO Ref Komoditas Kimia Kuantiti Booked Status Stok Tindakan Logistik
{quo.quoNumber}
{quo.customer}
Ref: {quo.customerPO}
{quo.chemName}
{quo.packageCount} Wadah ({quo.totalKg} Kg)
{quo.totalKg} Kg {quo.status === 'Shipped' ? 'Surat Jalan Terbit' : 'Terkunci (Booked)'} {quo.dispatchId ? ( Surat Jalan Terbit ) : ( )}
)} {} {activeTab === 'deliveree' && (

Dispatch Deliveree & Surat Jalan Continuous Form

Potong Stok Fisik FIFO saat Validasi Muat • Rekap POD Manual Teks (Tanpa File Upload) • Cetak Khusus EPSON LX-300+

Antrean Surat Jalan & Ekspedisi Deliveree Rumus: SJ-PGS/[Customer]/[Romawi]-[Tgl]/[Tahun]/[No]
{dispatches.map((dsp) => ( ))}
No. Surat Jalan Customer & PO Armada & Supir Barang & Volume Status Kirim Konfirmasi POD Manual Aksi / Cetak
{dsp.sjNumber}
{dsp.customer}
PO: {dsp.customerPO}
{dsp.driverName}
{dsp.nopol} • Telp: {dsp.driverPhone}
{dsp.chemName}
{dsp.packageCount} {dsp.packageType} ({dsp.totalKg} Kg)
{dsp.status} {dsp.status === 'Delivered' ? (
Diterima: {dsp.podReceiver}
{dsp.podCompletionTime}
) : ( )}
{dsp.status === 'Delivered' && !dsp.invoiceId && currentUser.role !== 'gudang' && ( )}
)} {} {activeTab === 'invoices' && currentUser.role !== 'gudang' && (

Invoicing & Faktur Pajak Ditjen Pajak

Diterbitkan Otomatis dari Surat Jalan Selesai • PPN 11% • Rekening Resmi BCA 308-1962-888

Rekapitulasi Tagihan Customer (INV-PGS) Rumus: INV-PGS/[Customer]/[Romawi]-[Tgl]/[Tahun]/[No]
{invoices.map((inv) => ( ))}
No. Invoice Tagihan Ref. Surat Jalan Customer & Terms No. Faktur Pajak (16 Digit) Subtotal DPP Total (+11% PPN) Status Aksi / Cetak
{inv.invNumber} {inv.sjNumber}
{inv.customer}
{inv.terms} • Jt. Tempo: {inv.dueDate}
{inv.fakturPajak} {formatRp(inv.subtotal)} {formatRp(inv.grandTotal)} {inv.status}
)} {} {activeTab === 'logbook' && (

Buku Transaksi Harian Gudang (Audit Trail Gerbang)

Catatan Masuk & Keluar Fisik Gerbang Real-Time: Supir, Plat Nomor, dan Kondisi Kemasan

{logbook.map((lg) => ( ))}
Tipe Mutasi Waktu Transaksi Armada & Supir Nama & Kode Barang Kuantiti Kemasan Catatan Kondisi Fisik Petugas
{lg.type.toUpperCase()} {lg.time} {lg.driverInfo} {lg.chemInfo} {lg.qty} {lg.condition} {lg.officer}
)}
{} {selectedBatchChem && (
{selectedBatchChem.sku}

{selectedBatchChem.name}

Kemasan: {selectedBatchChem.packageType} @ {selectedBatchChem.weightPerUnit} Kg • Asal: {selectedBatchChem.origin}

Prinsip Strict FIFO: Pemotongan barang saat pengiriman Deliveree secara otomatis diprioritaskan dari nomor batch dengan tanggal penerimaan tertua terlebih dahulu.
{(selectedBatchChem.batches || []).map((b, idx) => ( ))}
No. Batch / Lot Tgl Masuk (Urutan FIFO) Expired Date Wadah Sisa Stok (Kg)
{b.lotNumber} {b.inflowDate} {idx === 0 && Prioritas #1} {b.expDate} {b.pkgCount} Kemasan {b.remainingKg} Kg
)} {} {modalType === 'new-chemical' && (

Tambah Komoditas Bahan Kimia Baru

Penomoran SKU Cerdas Otomatis sesuai Kaidah Pratachem

{ e.preventDefault(); const form = e.target; const wh = form.wh.value; const cat = form.cat.value; const name = form.name.value.trim(); const origin = form.origin.value.trim(); const pkgType = form.pkgType.value; const weight = Number(form.weight.value); const price = Number(form.price.value); const seq = String(chemicals.length + 1).padStart(4, '0'); const sku = `PR${wh}${cat}${seq}`; const newChem = { id: 'chem-' + Date.now(), sku, warehouse: wh, category: cat, name, origin, form: form.chemForm.value, packageType: pkgType, weightPerUnit: weight, pricePerKg: price, bookedKg: 0, batches: [] }; setChemicals([...chemicals, newChem]); setModalType(null); showToast(`Bahan kimia ${name} berhasil didaftarkan dengan SKU ${sku}`); }} className="p-5 overflow-y-auto space-y-4 text-xs" >
)} {} {modalType === 'receive-inflow' && (

Penerimaan Barang Masuk (Stok In)

Pencatatan Batch, Kadaluwarsa, dan Audit Fisik Gerbang

{ e.preventDefault(); const form = e.target; const chemId = form.chemId.value; const lot = form.lot.value.trim(); const origin = form.origin.value.trim(); const pkgCount = Number(form.pkgCount.value); const expDate = form.expDate.value; const driver = form.driver.value.trim(); const condition = form.condition.value.trim(); const targetChem = chemicals.find((c) => c.id === chemId); if (!targetChem) return; const addedKg = pkgCount * targetChem.weightPerUnit; const today = new Date().toISOString().split('T')[0]; const newBatch = { id: 'b-' + Date.now(), lotNumber: lot, origin, inflowDate: today, expDate, pkgCount, remainingKg: addedKg }; const updatedChemicals = chemicals.map((c) => { if (c.id === chemId) { return { ...c, batches: [...(c.batches || []), newBatch] }; } return c; }); const newLog = { id: 'log-' + Date.now(), type: 'Stok in', time: new Date().toLocaleString('id-ID'), driverInfo: driver, chemInfo: `${targetChem.name} (${targetChem.sku})`, qty: `${pkgCount} ${targetChem.packageType} (${addedKg} Kg)`, condition, officer: currentUser.name }; setChemicals(updatedChemicals); setLogbook([newLog, ...logbook]); setModalType(null); showToast(`Berhasil menerima ${addedKg} Kg ${targetChem.name} ke Batch FIFO`); }} className="p-5 overflow-y-auto space-y-4 text-xs" >
Audit Gerbang & Fisik Barang (PRD Aturan 2):

*COA & MSDS diverifikasi fisik langsung di gudang tanpa upload file server.

)} {} {modalType === 'new-po' && (

Penerbitan PO Resmi ke Supplier

Pilih Supplier Rekanan • Format PO-PGS Cetak Canon MG2500

{ e.preventDefault(); const form = e.target; const suppName = form.suppName.value; const chemId = form.chemId.value; const count = Number(form.count.value); const price = Number(form.price.value); const chem = chemicals.find((c) => c.id === chemId); if (!chem) return; const totalKg = count * chem.weightPerUnit; const subtotal = totalKg * price; const ppn = Math.round(subtotal * 0.11); const grandTotal = subtotal + ppn; const poNumber = generateDocNumber('PO', suppName); const today = new Date().toISOString().split('T')[0]; const newPO = { id: 'po-' + Date.now(), poNumber, date: today, supplier: suppName, chemId, chemName: chem.name, packageCount: count, totalKg, pricePerKg: price, subtotal, ppn, grandTotal, status: 'Received' }; setSupplierPOs([newPO, ...supplierPOs]); setModalType(null); showToast(`PO ${poNumber} berhasil diterbitkan`); }} className="p-5 overflow-y-auto space-y-4 text-xs" >

Admin hanya memilih nama supplier (profil internal tetap terlindungi)

Pajak Pertambahan Nilai: PPN 11%
Alamat Kirim: Gudang Pondok Bahar, Tangerang
)} {} {modalType === 'new-quotation' && (

Penawaran & Penguncian Stok (Booked)

Mencegah Overselling • Otomatis Mengurangi Free Stok

{ e.preventDefault(); const form = e.target; const custId = form.custId.value; const custPO = form.custPO.value.trim(); const chemId = form.chemId.value; const count = Number(form.count.value); const price = Number(form.price.value); const top = form.top.value; const address = form.address.value.trim(); const targetCust = customers.find((c) => c.id === custId); const targetChem = chemicals.find((c) => c.id === chemId); if (!targetCust || !targetChem) return; const requestedKg = count * targetChem.weightPerUnit; const physicalKg = (targetChem.batches || []).reduce((acc, b) => acc + (Number(b.remainingKg) || 0), 0); const freeKg = Math.max(0, physicalKg - (targetChem.bookedKg || 0)); if (requestedKg > freeKg) { triggerDialog( 'Kuantiti Melebihi Stok Bebas', `Free Stok tidak cukup! Tersedia ${freeKg} Kg, sedangkan pesanan meminta ${requestedKg} Kg.` ); return; } const updatedChemicals = chemicals.map((c) => { if (c.id === chemId) { return { ...c, bookedKg: (c.bookedKg || 0) + requestedKg }; } return c; }); const quoNumber = generateDocNumber('Quo', targetCust.name); const today = new Date().toISOString().split('T')[0]; const newQuo = { id: 'quo-' + Date.now(), quoNumber, date: today, customer: targetCust.name, customerId: custId, customerPO: custPO, chemId, chemName: targetChem.name, packageCount: count, totalKg: requestedKg, pricePerKg: price, terms: top, address, status: 'Booked', dispatchId: null }; setChemicals(updatedChemicals); setQuotations([newQuo, ...quotations]); setModalType(null); showToast(`Booking ${requestedKg} Kg berhasil untuk ${targetCust.name}`); }} className="p-5 overflow-y-auto space-y-4 text-xs" >
)} {} {modalType === 'dispatch-deliveree' && activeQuoForDispatch && (

Dispatch Deliveree & Potong Stok FIFO

Mencocokkan Identitas Armada dan Mengurangi Lapisan Batch Tertua

{ e.preventDefault(); const form = e.target; const driverName = form.driverName.value.trim(); const driverPhone = form.driverPhone.value.trim(); const nopol = form.nopol.value.trim(); const pickupTime = form.pickupTime.value; const quo = activeQuoForDispatch; const targetChem = chemicals.find((c) => c.id === quo.chemId); if (!targetChem) return; let remainingToDeduct = quo.totalKg; const updatedBatches = (targetChem.batches || []).map((b) => { if (remainingToDeduct <= 0) return b; const availableInBatch = Number(b.remainingKg) || 0; if (availableInBatch <= remainingToDeduct) { remainingToDeduct -= availableInBatch; return { ...b, remainingKg: 0 }; } else { const newRem = availableInBatch - remainingToDeduct; remainingToDeduct = 0; return { ...b, remainingKg: newRem }; } }).filter((b) => b.remainingKg > 0); const updatedChemicals = chemicals.map((c) => { if (c.id === targetChem.id) { return { ...c, bookedKg: Math.max(0, (c.bookedKg || 0) - quo.totalKg), batches: updatedBatches }; } return c; }); const sjNumber = generateDocNumber('SJ', quo.customer); const newDispatch = { id: 'sj-' + Date.now(), sjNumber, quoId: quo.id, customer: quo.customer, customerPO: quo.customerPO, chemId: quo.chemId, chemName: quo.chemName, packageCount: quo.packageCount, packageType: targetChem.packageType, totalKg: quo.totalKg, address: quo.address, driverName, driverPhone, nopol, pickupTime, status: 'Shipped', podReceiver: null, podCompletionTime: null, podNotes: null, invoiceId: null }; const updatedQuotations = quotations.map((q) => { if (q.id === quo.id) { return { ...q, dispatchId: newDispatch.id, status: 'Shipped' }; } return q; }); const newLog = { id: 'log-' + Date.now(), type: 'Stok out', time: new Date().toLocaleString('id-ID'), driverInfo: `${driverName} (${nopol})`, chemInfo: `${quo.chemName} (${targetChem.sku})`, qty: `${quo.packageCount} ${targetChem.packageType} (${quo.totalKg} Kg)`, condition: 'Muat selesai, segel utuh, lolos gerbang ekspedisi Deliveree', officer: currentUser.name }; setChemicals(updatedChemicals); setQuotations(updatedQuotations); setDispatches([newDispatch, ...dispatches]); setLogbook([newLog, ...logbook]); setModalType(null); showToast(`Surat Jalan ${sjNumber} terbit. Stok fisik FIFO berhasil dipotong.`); }} className="p-5 overflow-y-auto space-y-4 text-xs" >
{activeQuoForDispatch.customer}
Ref PO: {activeQuoForDispatch.customerPO}
{activeQuoForDispatch.chemName} - {activeQuoForDispatch.totalKg} Kg
Validasi Eksekusi Muat:

Menekan tombol di bawah akan langsung memotong stok fisik gudang secara permanen pada layer batch penerimaan tertua (FIFO).

)} {} {modalType === 'confirm-pod' && activeDispatchForPod && (

Rekap Selesai Pengiriman (POD Teks)

Aturan PRD: Murni Pencatatan Manual Tanpa Upload File

{ e.preventDefault(); const form = e.target; const receiver = form.receiver.value.trim(); const compTime = form.compTime.value; const notes = form.notes.value.trim(); const updatedDispatches = dispatches.map((d) => { if (d.id === activeDispatchForPod.id) { return { ...d, status: 'Delivered', podReceiver: receiver, podCompletionTime: compTime, podNotes: notes }; } return d; }); setDispatches(updatedDispatches); setModalType(null); showToast(`Pengiriman ${activeDispatchForPod.sjNumber} berhasil ditandai selesai`); }} className="p-5 space-y-4 text-xs" >
)} {} {modalType === 'generate-invoice' && activeDispatchForInv && (

Terbitkan Invoice & Faktur Pajak 16 Digit

Standar Ditjen Pajak • Cetak Rekening Resmi BCA 308-1962-888

{ e.preventDefault(); const form = e.target; const faktur = form.faktur.value.trim(); const top = form.top.value; const dueDate = form.dueDate.value; const dsp = activeDispatchForInv; const quo = quotations.find((q) => q.id === dsp.quoId); const price = quo ? quo.pricePerKg : 30000; const subtotal = dsp.totalKg * price; const ppn = Math.round(subtotal * 0.11); const grandTotal = subtotal + ppn; const invNumber = generateDocNumber('INV', dsp.customer); const today = new Date().toISOString().split('T')[0]; const newInvoice = { id: 'inv-' + Date.now(), invNumber, dispatchId: dsp.id, sjNumber: dsp.sjNumber, customer: dsp.customer, customerPO: dsp.customerPO, date: today, dueDate, terms: top, totalKg: dsp.totalKg, pricePerKg: price, subtotal, ppn, grandTotal, fakturPajak: faktur, status: 'Unpaid' }; const updatedDispatches = dispatches.map((d) => { if (d.id === dsp.id) { return { ...d, invoiceId: newInvoice.id }; } return d; }); setDispatches(updatedDispatches); setInvoices([newInvoice, ...invoices]); setModalType(null); showToast(`Invoice ${invNumber} dengan e-Faktur ${faktur} berhasil diterbitkan`); }} className="p-5 overflow-y-auto space-y-4 text-xs" >
{activeDispatchForInv.customer} {activeDispatchForInv.sjNumber}
Barang: {activeDispatchForInv.chemName} ({activeDispatchForInv.totalKg} Kg)

Format resmi e-Faktur: 16 Digit angka

Rekening Pembayaran: Bank BCA 308-1962-888 a/n PT. PRATACHEM GLOBALINDO SUKSES
)} {} {modalType === 'new-customer' && (

Registrasi Master Customer (Pelanggan)

Plafon Piutang & Identitas Resmi Faktur Pajak

{ e.preventDefault(); const form = e.target; const newC = { id: 'cust-' + Date.now(), name: form.name.value.trim().toUpperCase(), address: form.address.value.trim(), npwp: form.npwp.value.trim(), pic: form.pic.value.trim(), responsible: form.responsible.value.trim(), top: form.top.value, creditLimit: Number(form.creditLimit.value), emailFinance: form.emailFinance.value.trim(), phoneFinance: form.phoneFinance.value.trim() }; setCustomers([...customers, newC]); setModalType(null); showToast(`Customer ${newC.name} berhasil disimpan`); }} className="p-5 overflow-y-auto space-y-3.5 text-xs" >