========================================== // SYSTEM INITIAL DATA & MOCK DATABASES // ========================================== const INITIAL_CUSTOMERS = [ { id: "CUST-001", name: "Ahmad Zulhelmi", phone: "0123456789", email: "ahmad.zul@gmail.com", address: "No 12, Jalan Putera, Jenjarom", dateJoined: "2026-01-10", repairCount: 3, purchaseCount: 5, points: 350 }, { id: "CUST-002", name: "Siti Fatimah", phone: "0198765432", email: "siti.f@yahoo.com", address: "Lot 204, Kampung Medan, Jenjarom", dateJoined: "2026-02-14", repairCount: 1, purchaseCount: 2, points: 120 }, { id: "CUST-003", name: "Tan Wei Shen", phone: "0173334455", email: "weishen.tan@hotmail.com", address: "No 55, Taman Melati, Jenjarom", dateJoined: "2026-03-01", repairCount: 2, purchaseCount: 8, points: 540 } ]; const INITIAL_INVENTORY = [ { id: "INV-001", sku: "LCD-IP13-ORI", name: "iPhone 13 Pro Max LCD Original", category: "LCD", costPrice: 450, sellingPrice: 750, supplier: "Apex Mobile Supply", qty: 12, minStock: 3, rack: "A1-L2" }, { id: "INV-002", sku: "BATT-IP11-PREM", name: "iPhone 11 Premium Battery (High Cap)", category: "Battery", costPrice: 65, sellingPrice: 150, supplier: "TechParts Distributors", qty: 25, minStock: 5, rack: "B3-L1" }, { id: "INV-003", sku: "CHG-S22-OEM", name: "Samsung Galaxy S22 Charging Port Flex", category: "Charging Port", costPrice: 35, sellingPrice: 90, supplier: "Global Mobile Spares", qty: 2, minStock: 5, rack: "C2-L3" }, // Low stock alert { id: "INV-004", sku: "CASE-IP14-MAG", name: "iPhone 14 Magsafe Silicon Case Black", category: "Accessories", costPrice: 15, sellingPrice: 45, supplier: "China Direct Import", qty: 40, minStock: 10, rack: "D1-L1" }, { id: "INV-005", sku: "HYD-CURVE-CLEAR", name: "Hydrogel Screen Protector Curved Clear", category: "Hydrogel", costPrice: 5, sellingPrice: 35, supplier: "M-Shield Malaysia", qty: 120, minStock: 20, rack: "A2-L1" } ]; const INITIAL_REPAIRS = [ { id: "REP-2026-0001", customerName: "Ahmad Zulhelmi", customerPhone: "0123456789", deviceBrand: "Apple", model: "iPhone 13 Pro Max", serial: "IMEI-88231992312", password: "UnlockCode: 2580", problem: "Skrin retak teruk selepas terjatuh dan sentuhan tidak berfungsi.", technician: "Zul (Senior Tech)", status: "In Progress", cost: 750, deposit: 150, dateReceived: "2026-06-18", dateCompleted: "", warranty: "3 Months Warranty", timeline: [ { date: "2026-06-18 10:00", text: "Device received and registered by Cashier." }, { date: "2026-06-18 14:30", text: "Diagnosed by Zul: Screen replacement required. Quotation RM750 approved by customer." }, { date: "2026-06-19 09:00", text: "Waiting for Original LCD Stock assignment." }, { date: "2026-06-19 11:15", text: "Status updated to In Progress. Replacement in action." } ] }, { id: "REP-2026-0002", customerName: "Siti Fatimah", customerPhone: "0198765432", deviceBrand: "Samsung", model: "Galaxy S22 Ultra", serial: "IMEI-99238472911", password: "No password (Pattern: L Shape)", problem: "Tidak boleh cas. Longgar di bahagian port cas.", technician: "Razi (Tech Assistant)", status: "Waiting Parts", cost: 180, deposit: 50, dateReceived: "2026-06-19", dateCompleted: "", warranty: "1 Month Warranty", timeline: [ { date: "2026-06-19 11:00", text: "Device received and registered." }, { date: "2026-06-19 15:00", text: "Diagnosed by Razi: Port pin damaged. Stock in hand is empty, awaiting supplier supply delivery." } ] }, { id: "REP-2026-0003", customerName: "Tan Wei Shen", customerPhone: "0173334455", deviceBrand: "Asus", model: "ROG Strix G15 (Laptop)", serial: "SN-ASUS-992812", password: "Windows: admin123", problem: "Overheating dan kipas berbunyi sangat bising.", technician: "Zul (Senior Tech)", status: "Ready Collection", cost: 120, deposit: 0, dateReceived: "2026-06-17", dateCompleted: "2026-06-19", warranty: "1 Month Warranty", timeline: [ { date: "2026-06-17 12:00", text: "Laptop received for thermal service and fan issues." }, { date: "2026-06-18 10:00", text: "Diagnosed: Heavy dust clogging and thermal paste dryout. Fan bearing needs lubrication." }, { date: "2026-06-18 16:00", text: "Servicing done, thermal paste replaced with Honeywell PTM7950, fan lubricated." }, { date: "2026-06-19 10:00", text: "Passed benchmark test, temperature down by 22c. Ready for collection." } ] } ]; const INITIAL_STAFF = [ { id: "STF-001", name: "Zulkifli bin Ramli", role: "Technician", baseSalary: 2800, commissions: 480, attendance: "98%", completedRepairs: 45, rating: 4.8 }, { id: "STF-002", name: "Razi bin Roslan", role: "Technician", baseSalary: 1800, commissions: 210, attendance: "95%", completedRepairs: 22, rating: 4.5 }, { id: "STF-003", name: "Nur Amira", role: "Cashier", baseSalary: 1700, commissions: 50, attendance: "100%", completedRepairs: 0, rating: 4.9 } ]; // ========================================== // DOUBLE ENTRY ACCOUNTING CHART OF ACCOUNTS (COA) // ========================================== const INITIAL_COA = { Assets: [ { code: "1010", name: "Cash in Hand", balance: 1450.00 }, { code: "1020", name: "Maybank Operating Account", balance: 45200.00 }, { code: "1200", name: "Inventory Asset Value", balance: 9400.00 }, { code: "1300", name: "Accounts Receivable (A/R)", balance: 1020.00 }, { code: "1500", name: "Shop Equipment & Tools", balance: 12000.00 } ], Liabilities: [ { code: "2010", name: "Accounts Payable (A/P) - Suppliers", balance: 1850.00 }, { code: "2100", name: "Customer Deposits Received", balance: 200.00 }, { code: "2200", name: "Business Micro-Loan (SME)", balance: 10000.00 } ], Equity: [ { code: "3010", name: "Owner Capital (Modal)", balance: 50000.00 }, { code: "3020", name: "Retained Earnings", balance: 6020.00 } ], Income: [ { code: "4010", name: "Repair Service Income", balance: 11250.00 }, { code: "4020", name: "Sparepart & Accessory Sales", balance: 6540.00 }, { code: "4030", name: "Hydrogel Protection Installation", balance: 1890.00 } ], Expenses: [ { code: "5010", name: "Cost of Goods Sold (COGS)", balance: 4500.00 }, { code: "5020", name: "Staff Salary & Commission", balance: 6300.00 }, { code: "5030", name: "Shop Rental (Jenjarom Hub)", balance: 2200.00 }, { code: "5040", name: "Utilities (TNB & Syabas & Unifi)", balance: 680.00 }, { code: "5050", name: "Marketing & Facebook Ads", balance: 450.00 }, { code: "5060", name: "Miscellaneous Shop Expenses", balance: 180.00 } ] }; const INITIAL_TRANSACTIONS = [ { id: "TX-1001", date: "2026-06-19", ref: "REP-2026-0003", desc: "Received balance of Repair Asus ROG", debit: "1010", credit: "1300", amount: 120 }, { id: "TX-1002", date: "2026-06-19", ref: "POS-99328", desc: "Sold iPhone 14 Silicon Case & Hydrogel", debit: "1020", credit: "4020", amount: 80 }, { id: "TX-1003", date: "2026-06-18", ref: "BILL-5542", desc: "Purchased battery stock from supplier", debit: "1200", credit: "2010", amount: 650 }, { id: "TX-1004", date: "2026-06-17", ref: "RENT-JUN26", desc: "Monthly Rent Payment", debit: "5030", credit: "1020", amount: 2200 } ]; export default function App() { // ========================================== // STATE MANAGEMENT // ========================================== const [currentUser, setCurrentUser] = useState({ name: "Super Admin", role: "Super Admin" }); const [activeTab, setActiveTab] = useState("dashboard"); const [customers, setCustomers] = useState(INITIAL_CUSTOMERS); const [inventory, setInventory] = useState(INITIAL_INVENTORY); const [repairs, setRepairs] = useState(INITIAL_REPAIRS); const [staff, setStaff] = useState(INITIAL_STAFF); const [coa, setCoa] = useState(INITIAL_COA); const [transactions, setTransactions] = useState(INITIAL_TRANSACTIONS); // Modal / Form States const [showCustModal, setShowCustModal] = useState(false); const [custForm, setCustForm] = useState({ name: "", phone: "", email: "", address: "" }); const [showRepairModal, setShowRepairModal] = useState(false); const [repairForm, setRepairForm] = useState({ customerPhone: "", deviceBrand: "", model: "", serial: "", password: "", problem: "", technician: "Zul (Senior Tech)", cost: "", deposit: "0", warranty: "3 Months Warranty" }); const [showStockModal, setShowStockModal] = useState(false); const [stockForm, setStockForm] = useState({ sku: "", name: "", category: "LCD", costPrice: "", sellingPrice: "", supplier: "", qty: "", minStock: "", rack: "" }); const [showLedgerModal, setShowLedgerModal] = useState(false); const [ledgerForm, setLedgerForm] = useState({ date: new Date().toISOString().split('T')[0], ref: "", desc: "", debit: "1010", credit: "4010", amount: "" }); // POS / Cart State const [cart, setCart] = useState([]); const [posDiscount, setPosDiscount] = useState(0); const [selectedPosCust, setSelectedPosCust] = useState(""); const [paymentMethod, setPaymentMethod] = useState("Cash"); // Filter/Search states const [searchCust, setSearchCust] = useState(""); const [searchInventory, setSearchInventory] = useState(""); const [searchRepair, setSearchRepair] = useState(""); // Print/Invoicing temporary storage const [printInvoiceData, setPrintInvoiceData] = useState(null); const [showInvoiceModal, setShowInvoiceModal] = useState(false); // Custom UI Notifications const [notification, setNotification] = useState(null); const showToast = (message, type = "success") => { setNotification({ message, type }); setTimeout(() => { setNotification(null); }, 4000); }; // Helper Auto Generate IDs const generateRepairId = () => { const year = new Date().getFullYear(); const count = repairs.length + 1; return `REP-${year}-${String(count).padStart(4, '0')}`; }; // ========================================== // LOGICS & MUTATIONS // ========================================== // 1. Add Customer const handleAddCustomer = (e) => { e.preventDefault(); if (!custForm.name || !custForm.phone) { showToast("Sila isikan nama dan nombor telefon!", "error"); return; } const newCust = { id: `CUST-${String(customers.length + 1).padStart(3, '0')}`, ...custForm, dateJoined: new Date().toISOString().split('T')[0], repairCount: 0, purchaseCount: 0, points: 20 // Welcome points }; setCustomers([newCust, ...customers]); setShowCustModal(false); setCustForm({ name: "", phone: "", email: "", address: "" }); showToast(`Pelanggan ${newCust.name} berjaya didaftarkan!`); }; // 2. Add Repair Job & Automation const handleAddRepair = (e) => { e.preventDefault(); if (!repairForm.customerPhone || !repairForm.model || !repairForm.problem) { showToast("Sila lengkapkan maklumat penting pembaikan!", "error"); return; } // Cari atau daftar pelanggan automatik let cust = customers.find(c => c.phone === repairForm.customerPhone); let custName = "Walk-in Customer"; if (cust) { custName = cust.name; } else { // Daftar pelanggan walk-in automatik custName = `Pelanggan Baru (${repairForm.customerPhone})`; const newAutoCust = { id: `CUST-${String(customers.length + 1).padStart(3, '0')}`, name: custName, phone: repairForm.customerPhone, email: "auto@repairtech.my", address: "Jenjarom", dateJoined: new Date().toISOString().split('T')[0], repairCount: 1, purchaseCount: 0, points: 10 }; setCustomers(prev => [...prev, newAutoCust]); } const newRepId = generateRepairId(); const costVal = parseFloat(repairForm.cost) || 0; const depVal = parseFloat(repairForm.deposit) || 0; const newRepair = { id: newRepId, customerName: custName, customerPhone: repairForm.customerPhone, deviceBrand: repairForm.deviceBrand, model: repairForm.model, serial: repairForm.serial || "N/A", password: repairForm.password || "Tiada", problem: repairForm.problem, technician: repairForm.technician, status: "Received", cost: costVal, deposit: depVal, dateReceived: new Date().toISOString().split('T')[0], dateCompleted: "", warranty: repairForm.warranty, timeline: [ { date: new Date().toISOString().replace('T', ' ').substring(0, 16), text: `Repair Job registered. Status: Received. Deposit RM${depVal} paid.` } ] }; setRepairs([newRepair, ...repairs]); // AUTO ACCOUNTING TRANSACTION FOR DEPOSIT if (depVal > 0) { const newTx = { id: `TX-${transactions.length + 1001}`, date: new Date().toISOString().split('T')[0], ref: newRepId, desc: `Deposit received for device repair ${repairForm.model}`, debit: "1010", // Debit Cash credit: "2100", // Credit Deposits Received (Liability) amount: depVal }; setTransactions([newTx, ...transactions]); // Kemaskini baki COA secara live updateCoaBalance("1010", depVal, "add"); updateCoaBalance("2100", depVal, "add"); } // Update Customer Repair Count if (cust) { setCustomers(customers.map(c => c.id === cust.id ? { ...c, repairCount: c.repairCount + 1, points: c.points + 50 } : c)); } setShowRepairModal(false); setRepairForm({ customerPhone: "", deviceBrand: "", model: "", serial: "", password: "", problem: "", technician: "Zul (Senior Tech)", cost: "", deposit: "0", warranty: "3 Months Warranty" }); showToast(`Tiket ${newRepId} didaftarkan & WhatsApp auto-notifikasi dijana!`); }; // 3. Update Repair Status & Auto-Accounting on Completed const updateRepairStatus = (repairId, newStatus) => { const updated = repairs.map(rep => { if (rep.id === repairId) { const completedDate = newStatus === "Completed" ? new Date().toISOString().split('T')[0] : rep.dateCompleted; const timelineEntry = { date: new Date().toISOString().replace('T', ' ').substring(0, 16), text: `Status updated to [${newStatus}] oleh ${currentUser.name}.` }; // AUTO-INVOICING & FINANCIAL JOURNAL POSTING ON COMPLETION if (newStatus === "Completed" && rep.status !== "Completed") { const finalReceivable = rep.cost - rep.deposit; // Debit Cash (baki bayaran) & Debit Deposit (kontra deposit) -> Credit Repair Income // Kita permudahkan double entry journal di sini: const tx1 = { id: `TX-${transactions.length + 1001}`, date: new Date().toISOString().split('T')[0], ref: rep.id, desc: `Final Payment for Repair Job: ${rep.model}`, debit: "1010", // Tunai dibayar credit: "4010", // Pendapatan repair amount: finalReceivable }; setTransactions(prev => [tx1, ...prev]); updateCoaBalance("1010", finalReceivable, "add"); updateCoaBalance("4010", rep.cost, "add"); if (rep.deposit > 0) { // Kontra balik liability deposit ke income const tx2 = { id: `TX-${transactions.length + 1002}`, date: new Date().toISOString().split('T')[0], ref: rep.id, desc: `Realize Deposit to Revenue for repair completion`, debit: "2100", // Kurangkan liability deposit credit: "4010", // Masuk repair income amount: rep.deposit }; setTransactions(prev => [tx2, ...prev]); updateCoaBalance("2100", rep.deposit, "sub"); } showToast(`Invois & Resit Auto dijana untuk ${rep.id}. Jurnal Am telah dikemaskini.`); } return { ...rep, status: newStatus, dateCompleted: completedDate, timeline: [...rep.timeline, timelineEntry] }; } return rep; }); setRepairs(updated); }; // Helper kemaskini akaun const updateCoaBalance = (code, amount, action) => { setCoa(prev => { const copy = { ...prev }; Object.keys(copy).forEach(category => { copy[category] = copy[category].map(acc => { if (acc.code === code) { const currentBal = parseFloat(acc.balance); return { ...acc, balance: action === "add" ? currentBal + amount : currentBal - amount }; } return acc; }); }); return copy; }); }; // 4. Inventory Stock In / Adjustments const handleAddStock = (e) => { e.preventDefault(); if (!stockForm.sku || !stockForm.name || !stockForm.qty || !stockForm.costPrice) { showToast("Sila masukkan maklumat lengkap stok!", "error"); return; } const newStock = { id: `INV-${String(inventory.length + 1).padStart(3, '0')}`, sku: stockForm.sku.toUpperCase(), name: stockForm.name, category: stockForm.category, costPrice: parseFloat(stockForm.costPrice), sellingPrice: parseFloat(stockForm.sellingPrice), supplier: stockForm.supplier || "Supplier Tempatan", qty: parseInt(stockForm.qty), minStock: parseInt(stockForm.minStock) || 5, rack: stockForm.rack || "N/A" }; setInventory([newStock, ...inventory]); // Auto Journal Entry for Inventory Purchase const totalCostValue = newStock.costPrice * newStock.qty; const newPurchaseTx = { id: `TX-${transactions.length + 1001}`, date: new Date().toISOString().split('T')[0], ref: `STOCK-IN-${newStock.sku}`, desc: `Auto Stock-In: Purchased ${newStock.qty}x ${newStock.name}`, debit: "1200", // Inventory Asset credit: "2010", // Accounts Payable (Sebab beli dari supplier) amount: totalCostValue }; setTransactions([newPurchaseTx, ...transactions]); updateCoaBalance("1200", totalCostValue, "add"); updateCoaBalance("2010", totalCostValue, "add"); setShowStockModal(false); setStockForm({ sku: "", name: "", category: "LCD", costPrice: "", sellingPrice: "", supplier: "", qty: "", minStock: "", rack: "" }); showToast(`Stok ${newStock.sku} berjaya didaftarkan. Lejar Am dikemaskini.`); }; // 5. POS Checkout Logic & Auto Stock Deduction const handleAddToCart = (item) => { if (item.qty <= 0) { showToast(`Stok untuk ${item.name} telah habis!`, "error"); return; } const existing = cart.find(cartItem => cartItem.id === item.id); if (existing) { if (existing.quantity >= item.qty) { showToast("Had stok dicapai untuk item ini di kedai.", "warning"); return; } setCart(cart.map(cartItem => cartItem.id === item.id ? { ...cartItem, quantity: cartItem.quantity + 1 } : cartItem)); } else { setCart([...cart, { ...item, quantity: 1 }]); } }; const updateCartQty = (id, newQty) => { if (newQty <= 0) { setCart(cart.filter(item => item.id !== id)); return; } const invItem = inventory.find(i => i.id === id); if (newQty > invItem.qty) { showToast(`Stok maksimum hanya ada ${invItem.qty} unit.`, "warning"); return; } setCart(cart.map(item => item.id === id ? { ...item, quantity: newQty } : item)); }; const handlePOSCheckout = () => { if (cart.length === 0) { showToast("Bakul belian kosong!", "error"); return; } const subtotal = cart.reduce((acc, item) => acc + (item.sellingPrice * item.quantity), 0); const tax = subtotal * 0.06; // SST 6% Malaysia const finalTotal = subtotal - posDiscount + tax; // Deduct Stock & Record Inventory Movement const updatedInventory = inventory.map(invItem => { const cartItem = cart.find(c => c.id === invItem.id); if (cartItem) { return { ...invItem, qty: invItem.qty - cartItem.quantity }; } return invItem; }); setInventory(updatedInventory); // Auto Journal Entry for POS Sale const checkoutRef = `POS-${Math.floor(10000 + Math.random() * 90000)}`; const totalCostOfGoodsSold = cart.reduce((acc, item) => acc + (item.costPrice * item.quantity), 0); const saleTx = { id: `TX-${transactions.length + 1001}`, date: new Date().toISOString().split('T')[0], ref: checkoutRef, desc: `POS Product Sales (${paymentMethod})`, debit: paymentMethod === "Cash" ? "1010" : "1020", // Cash or Bank credit: "4020", // Spareparts / Acc Income amount: finalTotal }; const cogsTx = { id: `TX-${transactions.length + 1002}`, date: new Date().toISOString().split('T')[0], ref: checkoutRef, desc: `COGS Auto Posting - Ref: ${checkoutRef}`, debit: "5010", // COGS Expense credit: "1200", // Inventory Asset (Reduced) amount: totalCostOfGoodsSold }; setTransactions([saleTx, cogsTx, ...transactions]); // Update COA updateCoaBalance(paymentMethod === "Cash" ? "1010" : "1020", finalTotal, "add"); updateCoaBalance("4020", finalTotal, "add"); updateCoaBalance("5010", totalCostOfGoodsSold, "add"); updateCoaBalance("1200", totalCostOfGoodsSold, "sub"); // Loyalty Points for Customer if (selectedPosCust) { setCustomers(customers.map(c => { if (c.id === selectedPosCust) { const pointsEarned = Math.floor(finalTotal / 10); return { ...c, purchaseCount: c.purchaseCount + 1, points: c.points + pointsEarned }; } return c; })); } // Set Print Data for Invoicing Modal setPrintInvoiceData({ invoiceId: checkoutRef, date: new Date().toLocaleString(), customer: selectedPosCust ? customers.find(c => c.id === selectedPosCust) : { name: "Walk-in Customer", phone: "N/A" }, items: cart, subtotal, discount: posDiscount, tax, total: finalTotal, paymentMethod }); setShowInvoiceModal(true); setCart([]); setPosDiscount(0); setSelectedPosCust(""); showToast(`Transaksi POS ${checkoutRef} selesai!`); }; // 6. Manual Accounting Journal Entry const handleManualJournal = (e) => { e.preventDefault(); const amt = parseFloat(ledgerForm.amount); if (!amt || amt <= 0) { showToast("Sila masukkan nilai amaun yang sah!", "error"); return; } if (ledgerForm.debit === ledgerForm.credit) { showToast("Akaun Debit dan Kredit mestilah berbeza!", "error"); return; } const manualTx = { id: `TX-${transactions.length + 1001}`, date: ledgerForm.date, ref: ledgerForm.ref || `JE-${Math.floor(1000 + Math.random() * 9000)}`, desc: ledgerForm.desc, debit: ledgerForm.debit, credit: ledgerForm.credit, amount: amt }; setTransactions([manualTx, ...transactions]); updateCoaBalance(ledgerForm.debit, amt, "add"); updateCoaBalance(ledgerForm.credit, amt, "add"); setShowLedgerModal(false); setLedgerForm({ date: new Date().toISOString().split('T')[0], ref: "", desc: "", debit: "1010", credit: "4010", amount: "" }); showToast("Catatan jurnal manual telah dimasukkan ke dalam Lejar Am."); }; // ========================================== // CALCULATIVE METRICS & KPI (Dashboard & Reports) // ========================================== // Sales calculations const todayDateStr = new Date().toISOString().split('T')[0]; const todayTransactions = transactions.filter(t => t.date === todayDateStr); const todaySales = todayTransactions .filter(t => ["4010", "4020", "4030"].includes(t.credit)) .reduce((acc, t) => acc + t.amount, 0); const monthSales = transactions .filter(t => t.date.substring(0, 7) === todayDateStr.substring(0, 7)) .filter(t => ["4010", "4020", "4030"].includes(t.credit)) .reduce((acc, t) => acc + t.amount, 0); const totalAssetValue = Object.values(coa).flatMap(arr => arr).filter(a => a.code.startsWith("1")).reduce((acc, a) => acc + a.balance, 0); const totalLiabilityValue = Object.values(coa).flatMap(arr => arr).filter(a => a.code.startsWith("2")).reduce((acc, a) => acc + a.balance, 0); const totalIncome = Object.values(coa).flatMap(arr => arr).filter(a => a.code.startsWith("4")).reduce((acc, a) => acc + a.balance, 0); const totalExpense = Object.values(coa).flatMap(arr => arr).filter(a => a.code.startsWith("5")).reduce((acc, a) => acc + a.balance, 0); const netProfit = totalIncome - totalExpense; const lowStockAlerts = inventory.filter(item => item.qty <= item.minStock); const pendingRepairs = repairs.filter(r => !["Completed", "Cancelled", "Ready Collection"].includes(r.status)); return (
{/* GLOBAL TOAST NOTIFICATION */} {notification && (
{notification.type === "error" ? ( ) : ( )}

{notification.message}

)} {/* HEADER SECTION */}
RT

REPAIRTECH MOBILE

Pakar Repair Telefon & Laptop Dipercayai | Jenjarom

{/* ROLE BAR & CURRENT DATE */}
Log Masuk Sebagai:

{currentUser.name} {currentUser.role}

{/* CORE LAYOUT WITH LEFT NAVIGATION */}
{/* NAV BAR */} {/* MAIN BODY CONTAINER */}
{/* ========================================== TAB 1: DASHBOARD ========================================== */} {activeTab === "dashboard" && (

Ringkasan Prestasi Perniagaan

Status langsung transaksi kewangan, inventory dan tiket pembaikan hari ini.

{/* KPI CARDS */}
Jualan Hari Ini

RM {todaySales.toFixed(2)}

▲ Real-time dari POS & Cashier

💵
Jualan Bulan Ini

RM {monthSales.toFixed(2)}

Sasaran bulanan: RM 25,000

📊
Tiket Repair Aktif

{pendingRepairs.length} Tiket

Menunggu tindakan teknikal

🔧
Baki Kas Negara (Net Profit)

= 0 ? "text-green-500" : "text-red-500"}`}> RM {netProfit.toFixed(2)}

Selepas tolak semua kos/belanja

⚖️
{/* CORE DASHBOARD GRAPHICS & ALERTS */}
{/* LOW STOCK ALERTS & CRITICAL ISSUES */}

🚨 Amaran Stok Kurang / Reorder

{lowStockAlerts.length} Item
{lowStockAlerts.length === 0 ? (

Tiada amaran stok buat masa ini.

) : ( lowStockAlerts.map(item => (

{item.name}

SKU: {item.sku} | Rak: {item.rack}

Stok: {item.qty} (Min: {item.minStock})

{ setActiveTab("inventory"); setShowStockModal(true); }}>Reorder ↗

)) )}
{/* CURRENT REPAIR STATUS MONITOR */}

Live Status Pembaikan Pelanggan

{repairs.map(rep => (
{rep.id}

{rep.deviceBrand} {rep.model}

Pelanggan: {rep.customerName}

{rep.status}
Teknikal: {rep.technician} Kos: RM{rep.cost}
))}
{/* QUICK AUTOMATION EXPLANATION BANNER */}

💡 Hubungan Automasi ERP Pintar Aktif

Apabila anda mendaftar jualan di POS, stok automatik dipotong dan lejar kewangan anda dikemaskini tanpa perlu memasukkan rekod secara manual.

)} {/* ========================================== TAB 2: POINT OF SALES (POS) MODULE ========================================== */} {activeTab === "pos" && (

Sistem Kaunter POS (Point of Sales)

Pendaftaran walk-in, belian barang runcit, aksesori dan bayaran baki repair.

{/* LEFT: PRODUCTS LIST & SELECTOR */}
Senarai Produk & Alatan Ganti setSearchInventory(e.target.value)} className="bg-neutral-900 border border-neutral-700 rounded text-xs px-3 py-1.5 focus:outline-none focus:border-[#E31E24] text-white w-48 sm:w-64" />
{inventory .filter(item => item.name.toLowerCase().includes(searchInventory.toLowerCase()) || item.sku.toLowerCase().includes(searchInventory.toLowerCase())) .map(item => (
{item.sku} Stok: {item.qty}

{item.name}

Kategori: {item.category}

RM {item.sellingPrice.toFixed(2)}
))}
{/* RIGHT: CART & BILLING CHECKOUT */}
Bakul Belian (Cart)
{/* SELECT CUSTOMER FOR LOYALTY POINTS */}
{/* CART ITEMS LIST */}
{cart.length === 0 ? (
Bakul kosong. Tambah item di panel kiri.
) : ( cart.map(item => { const itemTotal = item.sellingPrice * item.quantity; return (
{item.name}

RM {item.sellingPrice.toFixed(2)} / unit

{item.quantity}
RM {itemTotal.toFixed(2)}
); }) )}
{/* DISCOUNTS & OFFERS */}
setPosDiscount(parseFloat(e.target.value) || 0)} className="w-full bg-neutral-900 border border-neutral-700 rounded text-xs p-1.5 text-white focus:outline-none focus:border-[#E31E24]" />
{/* SUMMARY BILL */}
Subtotal: RM {cart.reduce((acc, item) => acc + (item.sellingPrice * item.quantity), 0).toFixed(2)}
{posDiscount > 0 && (
Diskaun: - RM {posDiscount.toFixed(2)}
)}
SST (6% Malaysia): RM {(cart.reduce((acc, item) => acc + (item.sellingPrice * item.quantity), 0) * 0.06).toFixed(2)}
Jumlah Bersih: RM {( cart.reduce((acc, item) => acc + (item.sellingPrice * item.quantity), 0) - posDiscount + (cart.reduce((acc, item) => acc + (item.sellingPrice * item.quantity), 0) * 0.06) ).toFixed(2)}
)} {/* ========================================== TAB 3: REPAIR MANAGEMENT ========================================== */} {activeTab === "repair" && (

Repair Management System (Tiket & Diagnos)

Daftar peranti rosak, agih tugasan teknikal, jejak proses pembaikan telefon & laptop.

{/* SEARCH & FILTERS */}
setSearchRepair(e.target.value)} className="bg-neutral-900 border border-neutral-700 rounded text-xs px-3 py-2 text-white focus:outline-none focus:border-[#E31E24] w-full sm:w-80" />
Petunjuk Status: Received In Progress Waiting Parts Ready Completed
{/* REPAIRS LIST TABLE */}
{repairs .filter(r => r.id.toLowerCase().includes(searchRepair.toLowerCase()) || r.customerName.toLowerCase().includes(searchRepair.toLowerCase()) || r.model.toLowerCase().includes(searchRepair.toLowerCase())) .map(rep => ( ))}
ID Tiket / Tarikh Pelanggan Peranti (Brand/Model) Punca Kerosakan Teknisi Ditugaskan Kos & Deposit Status Repair Tindakan
{rep.id}

{rep.dateReceived}

{rep.customerName}

{rep.customerPhone}

{rep.deviceBrand}

{rep.model}

{rep.serial}

{rep.problem}

{rep.password}

{rep.technician}

Kos: RM {rep.cost}

Dep: RM {rep.deposit}

Baki: RM {rep.cost - rep.deposit}

💬 WhatsApp
)} {/* ========================================== TAB 4: INVENTORY MODULE ========================================== */} {activeTab === "inventory" && (

Sistem Inventori, Alat Ganti & Aksesori

Pantau baki perkakasan, pembekal (supplier), harga kos, harga jualan, dan kedudukan rak fizikal.

{/* STATS */}
Nilai Aset Inventori

RM {totalAssetValue.toFixed(2)}

Mengikut harga kos terkumpul spareparts

Kategori Aktif

7 Kategori Utama

LCD, Bateri, Hydrogel, Casing, etc

Supplier Utama Berdaftar

5 Pembekal Tempatan

Urusan Akaun Belum Bayar (A/P)

{/* SEARCH & FILTER */}
setSearchInventory(e.target.value)} className="bg-neutral-900 border border-neutral-700 rounded text-xs px-3 py-2 text-white focus:outline-none focus:border-[#E31E24] w-full sm:w-80" />
{/* INVENTORY TABLE */}
{inventory .filter(item => item.name.toLowerCase().includes(searchInventory.toLowerCase()) || item.sku.toLowerCase().includes(searchInventory.toLowerCase())) .map(item => ( ))}
SKU Nama Alat Ganti / Barang Kategori Harga Kos (RM) Harga Jual (RM) Kuantiti Baki Kedudukan Rak Supplier Utama Tindakan
{item.sku} {item.name} {item.category} RM {item.costPrice.toFixed(2)} RM {item.sellingPrice.toFixed(2)} {item.qty} (Min: {item.minStock}) {item.rack} {item.supplier}
)} {/* ========================================== TAB 5: CUSTOMER DATABASE ========================================== */} {activeTab === "customers" && (

Pangkalan Data Pelanggan & Kesetiaan (CRM)

Pendaftaran pelanggan, sejarah pembaikan, mata ganjaran (loyalty points), dan pautan WhatsApp.

{/* SEARCH */}
setSearchCust(e.target.value)} className="bg-neutral-900 border border-neutral-700 rounded text-xs px-3 py-2 text-white focus:outline-none focus:border-[#E31E24] w-full sm:w-80" />
{/* CUSTOMERS LIST */}
{customers .filter(c => c.name.toLowerCase().includes(searchCust.toLowerCase()) || c.phone.includes(searchCust)) .map(cust => (
{cust.id} 🏆 {cust.points} Points

{cust.name}

📞 {cust.phone}

📧 {cust.email}

📍 {cust.address}

Bil Repair: {cust.repairCount}

Bil Belian: {cust.purchaseCount}

Sembang WhatsApp
))}
)} {/* ========================================== TAB 6: ACCOUNTING MODULE ========================================== */} {activeTab === "accounting" && (

Sistem Perakaunan & Imbangan (Double Entry Ledger)

Automasi jurnal am, lejar, trial balance, untung & rugi (P&L), dan kunci kira-kira.

{/* ACCOUNTS OVERVIEW AND NET REVENUE STATS */}
Aset Semasa

RM {totalAssetValue.toFixed(2)}

Tunai + Bank + Nilai Stok

Liabiliti Terkumpul

RM {totalLiabilityValue.toFixed(2)}

Hutang Pembekal & Deposit

Jumlah Hasil (Revenue)

RM {totalIncome.toFixed(2)}

Pembaikan + Jualan Barang

Jumlah Perbelanjaan

RM {totalExpense.toFixed(2)}

Sewa + Gaji + COGS + Bil

{/* REPORT TABS: TRIAL BALANCE / P&L / JOURNAL ENTRIES */}

Invois & Jurnal Terkini (General Ledger)

{/* GENERAL LEDGER LIST */}
{transactions.map(tx => { // Cari nama akaun debit dan kredit untuk kefahaman user const allAccounts = Object.values(coa).flat(); const debAcc = allAccounts.find(a => a.code === tx.debit)?.name || tx.debit; const credAcc = allAccounts.find(a => a.code === tx.credit)?.name || tx.credit; return ( ); })}
Ref ID Tarikh Butiran (Description) Debit (Akaun Penerima) Kredit (Akaun Pemberi) Amaun (RM)
{tx.ref} {tx.date} {tx.desc} ({tx.debit}) {debAcc} ({tx.credit}) {credAcc} RM {tx.amount.toFixed(2)}
{/* LIVE PROFIT & LOSS REPORT PREVIEW */}

Penyata Untung Rugi (Profit & Loss)

1. Hasil / Pendapatan
{coa.Income.map(i => (
{i.name} RM {i.balance.toFixed(2)}
))}
Jumlah Hasil Terkumpul RM {totalIncome.toFixed(2)}
2. Perbelanjaan / Kos Kos Terlibat
{coa.Expenses.map(e => (
{e.name} RM {e.balance.toFixed(2)}
))}
Jumlah Belanja Terkumpul RM {totalExpense.toFixed(2)}
Untung Bersih (Net Profit) RM {netProfit.toFixed(2)}

Kunci Kira-Kira (Balance Sheet Balance Test)

Aset (Assets)
{coa.Assets.map(a => (
{a.name} RM {a.balance.toFixed(2)}
))}
Jumlah Keseluruhan Aset RM {totalAssetValue.toFixed(2)}
Liabiliti + Ekuiti
{coa.Liabilities.map(l => (
{l.name} RM {l.balance.toFixed(2)}
))} {coa.Equity.map(eq => (
{eq.name} RM {eq.balance.toFixed(2)}
))} {/* Add Current profit to Equity */}
Keuntungan Bersih Semasa RM {netProfit.toFixed(2)}
Jumlah Liabiliti & Ekuiti RM {(totalLiabilityValue + 56020 + netProfit).toFixed(2)}
💡 Sistem Perakaunan RT: Formula Imbangan Kunci Kira-kira sentiasa diuji (Aset = Liabiliti + Ekuiti). Segala rekod jualan atau pembaikan diselaraskan secara langsung di belakang tabir.
)} {/* ========================================== TAB 7: STAFF PERFORMANCE & COMMISSION (HR) ========================================== */} {activeTab === "hr" && (

HR & Prestasi Komisen Kakitangan

Pantau kehadiran, bilangan kerja pembaikan selesai, KPI peratusan kepuasan pelanggan, dan perkiraan komisen bulanan.

{staff.map(member => (
{member.role}

{member.name}

ID Pekerja: {member.id}

👤
Gaji Pokok: RM {member.baseSalary.toFixed(2)}
Komisen Terkumpul: RM {member.commissions.toFixed(2)}
Kadar Kehadiran: {member.attendance}
Kerosakan Selesai: {member.completedRepairs} Peranti
Rating KPI Pelanggan: ⭐ {member.rating} / 5.0
Anggaran Gaji Kasar: RM {(member.baseSalary + member.commissions).toFixed(2)}
))}
)} {/* ========================================== TAB 8: SYSTEM ARCHITECTURE & DATABASE SCHEMAS ========================================== */} {activeTab === "architecture" && (

Seni Bina Sistem & Pangkalan Data (SaaS Blueprint)

Struktur visual, skema pangkalan data MySQL, API endpoints, dan ERD untuk rujukan pembangun.

{/* SYSTEM FLOW & DIRECTIVES */}

Seni Bina Aliran Kerja (System Architecture)

Aplikasi ini menggunakan kombinasi **React Client** untuk kelancaran antaramuka, berinteraksi dengan **Express.js API** di VPS Cloud, dan menyimpan data kewangan bertaraf ACID ke dalam **MySQL Database**.

// 1. Alur Automasi POS & Accounting:

[Pengguna Jual Barang] ➔ [Kurangkan Stok Inventori (Live)] ➔ [Jana Jurnal Am (Debit Cash, Credit Revenue)] ➔ [Jana Invois Jualan (SST 6%)]

// 2. Alur Automasi Repair Tiket:

[Daftar Tiket] ➔ [Terima Deposit (Auto Ledger Deposit Received)] ➔ [Kemaskini Status Teknisi] ➔ [Status Selesai ➔ Auto Jana Invois Akhir & Ambil Baki]

API Endpoints Utama (Express.js Router)

POST /api/v1/auth/login - User sign in & JWT token issue

POST /api/v1/repairs - Create new repair tiket

PUT /api/v1/repairs/:id/status - Change repair status

GET /api/v1/inventory - Fetch live spareparts & stock

POST /api/v1/pos/checkout - Process sales & ledger posting

GET /api/v1/accounting/pl - Retrieve Profit & Loss report

GET /api/v1/accounting/balance-sheet - Balance Sheet Report

{/* MYSQL SCHEMA SCRIPT */}

Skrip SQL Pangkalan Data (MySQL Schema)

Gunakan skema jadual SQL berelasi tinggi di bawah untuk melancarkan pangkalan data MySQL anda di server produksi.

{`-- JADUAL CUSTOMER
CREATE TABLE customers (
  id VARCHAR(30) PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  phone VARCHAR(20) NOT NULL UNIQUE,
  email VARCHAR(100),
  address TEXT,
  date_joined DATE DEFAULT CURRENT_DATE,
  points INT DEFAULT 0
);

-- JADUAL REPAIR TIKET
CREATE TABLE repairs (
  id VARCHAR(30) PRIMARY KEY,
  customer_id VARCHAR(30),
  device_brand VARCHAR(50) NOT NULL,
  model VARCHAR(100) NOT NULL,
  serial_number VARCHAR(100),
  password_device VARCHAR(50),
  problem_desc TEXT,
  technician_assigned VARCHAR(100),
  status VARCHAR(30) DEFAULT 'Received',
  cost DECIMAL(10,2) DEFAULT 0.00,
  deposit DECIMAL(10,2) DEFAULT 0.00,
  date_received DATE DEFAULT CURRENT_DATE,
  date_completed DATE,
  warranty VARCHAR(100),
  FOREIGN KEY (customer_id) REFERENCES customers(id)
);

-- JADUAL INVENTORI SPARE PARTS
CREATE TABLE inventory (
  id VARCHAR(30) PRIMARY KEY,
  sku VARCHAR(50) UNIQUE NOT NULL,
  name VARCHAR(255) NOT NULL,
  category VARCHAR(50),
  cost_price DECIMAL(10,2) NOT NULL,
  selling_price DECIMAL(10,2) NOT NULL,
  supplier VARCHAR(150),
  qty INT DEFAULT 0,
  min_stock INT DEFAULT 5,
  location_rack VARCHAR(50)
);

-- JADUAL JOURNAL TRANSACTIONS (DOUBLE ENTRY)
CREATE TABLE journal_transactions (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tx_date DATE NOT NULL,
  ref_code VARCHAR(50) NOT NULL,
  description TEXT,
  debit_account VARCHAR(10) NOT NULL,
  credit_account VARCHAR(10) NOT NULL,
  amount DECIMAL(10,2) NOT NULL
);`}
                    
)}
{/* FOOTER */} {/* ========================================== MODALS SECTION (INTERACTIVE FORMS) ========================================== */} {/* MODAL 1: REGISTER CUSTOMER */} {showCustModal && (

Daftar Pelanggan Baru

setCustForm({ ...custForm, name: e.target.value })} className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none focus:border-[#E31E24]" />
setCustForm({ ...custForm, phone: e.target.value })} className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none focus:border-[#E31E24]" />
setCustForm({ ...custForm, email: e.target.value })} className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none focus:border-[#E31E24]" />