==========================================
// 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" ? (
) : (
)}
)}
{/* HEADER SECTION */}
{/* CORE LAYOUT WITH LEFT NAVIGATION */}
{/* NAV BAR */}
Modul Utama
setActiveTab("dashboard")}
className={`w-full text-left px-3 py-2.5 rounded transition-all duration-150 flex items-center space-x-2 whitespace-nowrap ${activeTab === "dashboard" ? "bg-[#E31E24] text-white font-bold" : "text-gray-400 hover:bg-neutral-800 hover:text-white"}`}
>
📊 Dashboard
{["Super Admin", "Admin", "Cashier"].includes(currentUser.role) && (
setActiveTab("pos")}
className={`w-full text-left px-3 py-2.5 rounded transition-all duration-150 flex items-center space-x-2 whitespace-nowrap ${activeTab === "pos" ? "bg-[#E31E24] text-white font-bold" : "text-gray-400 hover:bg-neutral-800 hover:text-white"}`}
>
🛒 POS Jualan & Kasir
)}
setActiveTab("repair")}
className={`w-full text-left px-3 py-2.5 rounded transition-all duration-150 flex items-center space-x-2 whitespace-nowrap ${activeTab === "repair" ? "bg-[#E31E24] text-white font-bold" : "text-gray-400 hover:bg-neutral-800 hover:text-white"}`}
>
🔧 Repair Management
{["Super Admin", "Admin"].includes(currentUser.role) && (
setActiveTab("inventory")}
className={`w-full text-left px-3 py-2.5 rounded transition-all duration-150 flex items-center space-x-2 whitespace-nowrap ${activeTab === "inventory" ? "bg-[#E31E24] text-white font-bold" : "text-gray-400 hover:bg-neutral-800 hover:text-white"}`}
>
📦 Inventory & Parts
)}
{["Super Admin", "Admin", "Cashier"].includes(currentUser.role) && (
setActiveTab("customers")}
className={`w-full text-left px-3 py-2.5 rounded transition-all duration-150 flex items-center space-x-2 whitespace-nowrap ${activeTab === "customers" ? "bg-[#E31E24] text-white font-bold" : "text-gray-400 hover:bg-neutral-800 hover:text-white"}`}
>
👥 Database Pelanggan
)}
{["Super Admin", "Admin"].includes(currentUser.role) && (
setActiveTab("accounting")}
className={`w-full text-left px-3 py-2.5 rounded transition-all duration-150 flex items-center space-x-2 whitespace-nowrap ${activeTab === "accounting" ? "bg-[#E31E24] text-white font-bold" : "text-gray-400 hover:bg-neutral-800 hover:text-white"}`}
>
⚖️ Accounting & GL
)}
{["Super Admin"].includes(currentUser.role) && (
setActiveTab("hr")}
className={`w-full text-left px-3 py-2.5 rounded transition-all duration-150 flex items-center space-x-2 whitespace-nowrap ${activeTab === "hr" ? "bg-[#E31E24] text-white font-bold" : "text-gray-400 hover:bg-neutral-800 hover:text-white"}`}
>
💼 HR & Commission
)}
setActiveTab("architecture")}
className={`w-full text-left px-3 py-2.5 rounded transition-all duration-150 flex items-center space-x-2 whitespace-nowrap ${activeTab === "architecture" ? "bg-[#E31E24] text-white font-bold" : "text-gray-400 hover:bg-neutral-800 hover:text-white"}`}
>
🗄️ Seni Bina Sistem
{/* MAIN BODY CONTAINER */}
{/* ==========================================
TAB 1: DASHBOARD
========================================== */}
{activeTab === "dashboard" && (
Ringkasan Prestasi Perniagaan
Status langsung transaksi kewangan, inventory dan tiket pembaikan hari ini.
{
showToast("Menyambung semula pangkalan data & memuat semula data live!");
}}
className="bg-neutral-800 text-xs px-3 py-2 rounded text-white border border-neutral-700 hover:bg-neutral-700 transition"
>
🔄 Segarkan Data
{/* 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
setActiveTab("repair")} className="text-xs text-[#E31E24] font-bold hover:underline">Urus Semua Tiket ↗
{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.
setActiveTab("pos")} className="bg-[#E31E24] hover:bg-red-700 text-white text-xs font-bold px-4 py-2 rounded transition">
Uji POS Sekarang
)}
{/* ==========================================
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)}
handleAddToCart(item)}
className="bg-[#E31E24] hover:bg-red-700 text-white text-[10px] font-bold px-2 py-1.5 rounded transition"
>
+ Tambah
))}
{/* RIGHT: CART & BILLING CHECKOUT */}
Bakul Belian (Cart)
setCart([])} className="text-xs text-neutral-500 hover:text-red-500">Kosongkan
{/* SELECT CUSTOMER FOR LOYALTY POINTS */}
Pilih Pelanggan (Loyalty)
setSelectedPosCust(e.target.value)}
className="w-full bg-neutral-900 border border-neutral-700 rounded text-xs p-2 text-white focus:outline-none focus:border-[#E31E24]"
>
-- Walk-in / Bukan Ahli --
{customers.map(c => (
{c.name} ({c.phone})
))}
{/* 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
updateCartQty(item.id, item.quantity - 1)} className="bg-neutral-800 px-2 py-1 rounded hover:bg-neutral-700 text-white">-
{item.quantity}
updateCartQty(item.id, item.quantity + 1)} className="bg-neutral-800 px-2 py-1 rounded hover:bg-neutral-700 text-white">+
RM {itemTotal.toFixed(2)}
);
})
)}
{/* DISCOUNTS & OFFERS */}
{/* 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)}
Bayar & Cetak Invois (Auto Posting)
)}
{/* ==========================================
TAB 3: REPAIR MANAGEMENT
========================================== */}
{activeTab === "repair" && (
Repair Management System (Tiket & Diagnos)
Daftar peranti rosak, agih tugasan teknikal, jejak proses pembaikan telefon & laptop.
setShowRepairModal(true)}
className="bg-[#E31E24] hover:bg-red-700 text-white font-bold text-xs px-4 py-2 rounded transition"
>
+ Daftar Tiket Repair Baru
{/* SEARCH & FILTERS */}
{/* REPAIRS LIST TABLE */}
ID Tiket / Tarikh
Pelanggan
Peranti (Brand/Model)
Punca Kerosakan
Teknisi Ditugaskan
Kos & Deposit
Status Repair
Tindakan
{repairs
.filter(r => r.id.toLowerCase().includes(searchRepair.toLowerCase()) || r.customerName.toLowerCase().includes(searchRepair.toLowerCase()) || r.model.toLowerCase().includes(searchRepair.toLowerCase()))
.map(rep => (
{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}
updateRepairStatus(rep.id, e.target.value)}
className={`rounded text-[11px] font-bold p-1.5 border focus:outline-none ${
rep.status === "Received" ? "bg-neutral-800 border-neutral-700 text-white" :
rep.status === "Diagnosing" ? "bg-indigo-950 border-indigo-800 text-indigo-300" :
rep.status === "In Progress" ? "bg-blue-950 border-blue-800 text-blue-300" :
rep.status === "Waiting Parts" ? "bg-amber-950 border-amber-800 text-amber-300" :
rep.status === "Ready Collection" ? "bg-purple-950 border-purple-800 text-purple-300" :
rep.status === "Completed" ? "bg-green-950 border-green-800 text-green-300 cursor-not-allowed" :
"bg-red-950 border-red-800 text-red-300"
}`}
disabled={rep.status === "Completed" || rep.status === "Cancelled"}
>
Received
Diagnosing
Waiting Parts
In Progress
Ready Collection
Completed (Auto Invoice)
Cancelled
{
// Cetak Resit Repair Mock
setPrintInvoiceData({
invoiceId: rep.id,
date: rep.dateReceived,
customer: { name: rep.customerName, phone: rep.customerPhone },
items: [
{ name: `Repair Service: ${rep.deviceBrand} ${rep.model}`, sellingPrice: rep.cost, quantity: 1 }
],
subtotal: rep.cost,
discount: 0,
tax: 0,
total: rep.cost,
paymentMethod: "Deposit paid: RM" + rep.deposit,
isRepair: true,
repairData: rep
});
setShowInvoiceModal(true);
}}
className="bg-neutral-800 text-gray-300 hover:text-white px-2 py-1 rounded hover:bg-neutral-700 transition"
title="Cetak Resit"
>
🖨️ Cetak
💬 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.
setShowStockModal(true)}
className="bg-[#E31E24] hover:bg-red-700 text-white font-bold text-xs px-4 py-2 rounded transition"
>
+ Tambah Stok / Alat Ganti Baru
{/* 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 */}
SKU
Nama Alat Ganti / Barang
Kategori
Harga Kos (RM)
Harga Jual (RM)
Kuantiti Baki
Kedudukan Rak
Supplier Utama
Tindakan
{inventory
.filter(item => item.name.toLowerCase().includes(searchInventory.toLowerCase()) || item.sku.toLowerCase().includes(searchInventory.toLowerCase()))
.map(item => (
{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}
{
// Simulate Stock Adjustment
const adjustAmt = prompt("Masukkan jumlah tambahan stok (boleh negatif untuk tolak):");
if (adjustAmt !== null) {
const num = parseInt(adjustAmt);
if (!isNaN(num)) {
setInventory(inventory.map(i => i.id === item.id ? { ...i, qty: i.qty + num } : i));
showToast(`Stok ${item.sku} diselaraskan sebanyak ${num} unit.`);
}
}
}}
className="bg-neutral-800 text-white hover:bg-neutral-700 px-2.5 py-1 rounded transition text-[11px]"
>
Adjust 🔄
))}
)}
{/* ==========================================
TAB 5: CUSTOMER DATABASE
========================================== */}
{activeTab === "customers" && (
Pangkalan Data Pelanggan & Kesetiaan (CRM)
Pendaftaran pelanggan, sejarah pembaikan, mata ganjaran (loyalty points), dan pautan WhatsApp.
setShowCustModal(true)}
className="bg-[#E31E24] hover:bg-red-700 text-white font-bold text-xs px-4 py-2 rounded transition"
>
+ Daftar Pelanggan Baru
{/* 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.
setShowLedgerModal(true)}
className="bg-[#E31E24] hover:bg-red-700 text-white font-bold text-xs px-4 py-2 rounded transition"
>
+ Rekod Jurnal Manual (Expenses/Capital)
{/* 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 */}
Ref ID
Tarikh
Butiran (Description)
Debit (Akaun Penerima)
Kredit (Akaun Pemberi)
Amaun (RM)
{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 (
{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)}
{
const newComm = prompt("Masukkan jumlah komisen manual untuk ditambah (RM):");
if (newComm) {
const amt = parseFloat(newComm);
if (!isNaN(amt)) {
setStaff(staff.map(s => s.id === member.id ? { ...s, commissions: s.commissions + amt } : s));
showToast(`Komisen ${member.name} dinaikkan sebanyak RM${amt}!`);
}
}
}}
className="w-full text-center py-2 bg-neutral-800 hover:bg-neutral-700 text-white rounded text-xs transition"
>
Pemberian Komisen Tambahan 💰
))}
)}
{/* ==========================================
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 */}
© 2026 REPAIRTECH MOBILE JENJAROM. Hak Cipta Terpelihara.
Sistem Pengurusan & Perakaunan Kedai Telefon Pintar Malaysia • Dibangunkan Setaraf Piawaian Kewangan SQL Account.
{/* ==========================================
MODALS SECTION (INTERACTIVE FORMS)
========================================== */}
{/* MODAL 1: REGISTER CUSTOMER */}
{showCustModal && (
Daftar Pelanggan Baru
setShowCustModal(false)} className="text-gray-400 hover:text-white">✕
)}
{/* MODAL 2: REGISTER REPAIR TIKET */}
{showRepairModal && (
Daftar Tiket Pembaikan Baru
setShowRepairModal(false)} className="text-gray-400 hover:text-white">✕
No Telefon Pelanggan *
setRepairForm({ ...repairForm, customerPhone: e.target.value })}
className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none focus:border-[#E31E24]"
/>
Jenama Peranti (Brand) *
setRepairForm({ ...repairForm, deviceBrand: e.target.value })}
className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none"
required
>
-- Pilih Jenama --
Apple (iPhone/iPad/Mac)
Samsung
Xiaomi / Redmi
Oppo
Vivo
Asus (ROG/Laptop)
HP Laptop
Lenovo Laptop
Model Peranti *
setRepairForm({ ...repairForm, model: e.target.value })}
className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none"
/>
IMEI / Serial No
setRepairForm({ ...repairForm, serial: e.target.value })}
className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none"
/>
Kata Laluan Peranti (Password)
setRepairForm({ ...repairForm, password: e.target.value })}
className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none"
/>
Punca & Huraian Masalah Kerosakan *
setRepairForm({ ...repairForm, problem: e.target.value })}
className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none h-16"
/>
Teknisi Ditugaskan
setRepairForm({ ...repairForm, technician: e.target.value })}
className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none"
>
Zul (Senior Tech)
Razi (Tech Assistant)
Anggaran Tempoh Waranti
setRepairForm({ ...repairForm, warranty: e.target.value })}
className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none"
/>
Anggaran Kos Sebenar (RM) *
setRepairForm({ ...repairForm, cost: e.target.value })}
className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none"
/>
Deposit Diterima (RM)
setRepairForm({ ...repairForm, deposit: e.target.value })}
className="w-full bg-neutral-900 border border-neutral-700 rounded p-2 text-white focus:outline-none"
/>
setShowRepairModal(false)} className="bg-neutral-800 text-white px-4 py-2 rounded">Batal
Daftar & Cetak Resit
)}
{/* MODAL 3: INVENTORY STOCK IN */}
{showStockModal && (
)}
{/* MODAL 4: MANUAL JOURNAL ENTRY */}
{showLedgerModal && (
)}
{/* MODAL 5: CUSTOMER INVOICE & RECIEPT PRINT PREVIEW */}
{showInvoiceModal && printInvoiceData && (
RESIT PEMBAYARAN KOS / TRANSAKSI
setShowInvoiceModal(false)} className="text-gray-500 hover:text-black font-extrabold">✕
{/* PRINT BODY */}
REPAIRTECH MOBILE
Pakar Repair Telefon & Laptop Dipercayai
Jenjarom Hub, Selangor, Malaysia
Tel: 012-345 6789
No Ref: {printInvoiceData.invoiceId}
Tarikh: {printInvoiceData.date}
Pelanggan: {printInvoiceData.customer?.name || "Walk-in Customer"}
No Tel: {printInvoiceData.customer?.phone || "N/A"}
{/* LIST ITEMS */}
Item / Perkara
Amaun
{printInvoiceData.items.map((item, index) => (
{item.quantity}x {item.name}
RM {(item.sellingPrice * item.quantity).toFixed(2)}
))}
{/* CALCULATIVE SUMMARY */}
Subtotal:
RM {printInvoiceData.subtotal.toFixed(2)}
{printInvoiceData.discount > 0 && (
Diskaun:
- RM {printInvoiceData.discount.toFixed(2)}
)}
SST (6%):
RM {printInvoiceData.tax.toFixed(2)}
Jumlah Bersih:
RM {printInvoiceData.total.toFixed(2)}
Terima Kasih Atas Sokongan Anda!
Sila simpan resit ini sebagai rujukan waranti anda.
Sistem Auto Posting Diperkasakan oleh REPAIRTECH ERP
{
window.print();
}}
className="flex-1 bg-neutral-900 text-white font-bold py-2 rounded text-xs hover:bg-black transition"
>
Cetak Fizikal / Simpan PDF 🖨️
setShowInvoiceModal(false)}
className="bg-neutral-200 text-neutral-800 font-bold py-2 px-4 rounded text-xs hover:bg-neutral-300 transition"
>
Tutup
)}
);
}