Giới thiệu dự án

Quản lý kho hàng (Theo dõi Nhập kho, Xuất kho và kiểm tra Hàng tồn) là bài toán cốt lõi của bất kỳ cửa hàng hay doanh nghiệp nhỏ nào. Thay vì phải trả chi phí vận hành máy chủ (Server) hay cơ sở dữ liệu đám mây phức tạp, xu hướng hiện nay là tận dụng tối đa sức mạnh của trình duyệt web thông qua công nghệ IndexedDB.

Trong bài viết này, mình sẽ hướng dẫn các bạn xây dựng một Phần mềm Quản lý Kho hàng mini (Serverless) chỉ với một file HTML duy nhất. Bạn có thể nhúng trực tiếp vào Blogspot để sử dụng cá nhân hoặc chia sẻ cho khách hàng.

Tại sao lại chọn IndexedDB?

  • Hoàn toàn miễn phí: Không cần thuê hosting có database (MySQL, SQL Server), tiết kiệm tối đa chi phí.

  • Dung lượng lưu trữ lớn: Khác với localStorage bị giới hạn ở mức 5MB, IndexedDB cho phép lưu trữ từ vài trăm MB đến vài GB dữ liệu tùy thuộc vào ổ cứng của người dùng.

  • An toàn và bảo mật: Dữ liệu được lưu trữ trực tiếp trên trình duyệt của máy bạn, không sợ bị rò rỉ lên mạng internet.

  • Hoạt động Offline: Không cần kết nối mạng internet vẫn có thể nhập/xuất kho bình thường.

Hướng dẫn nhúng code vào Blogspot

  1. Truy cập vào trang quản trị Blogspot của bạn Chọn Bài đăng mới (New Post).

  2. Tại thanh công cụ, chuyển từ chế độ Chế độ xem soạn thảo (Compose View) sang Chế độ xem HTML (HTML View).

  3. Sao chép toàn bộ đoạn code phía dưới và dán vào.

  4. Nhấn Xuất bản (Publish).

Toàn bộ mã nguồn ứng dụng (Full Source Code)

HTML
<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Phần Mềm Quản Lý Kho Hàng Xuất Nhập Tồn</title>
    <style>
        :root {
            --primary-color: #2c3e50;
            --secondary-color: #27ae60;
            --danger-color: #c0392b;
            --light-bg: #f5f6fa;
            --border-color: #dcdde1;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            margin: 0;
            padding: 20px;
            background-color: var(--light-bg);
            color: #2f3640;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
        }

        h1, h2, h3 {
            color: var(--primary-color);
            text-align: center;
        }

        .grid-layout {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }

        .card {
            background: #fff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.05);
            border: 1px solid var(--border-color);
        }

        .form-group {
            margin-bottom: 15px;
        }

        .form-group label {
            display: block;
            margin-bottom: 5px;
            font-weight: 600;
        }

        .form-group input, .form-group select {
            width: 100%;
            padding: 10px;
            border: 1px solid var(--border-color);
            border-radius: 4px;
            box-sizing: border-box;
            font-size: 14px;
        }

        button {
            background-color: var(--primary-color);
            color: white;
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: 600;
            width: 100%;
            font-size: 14px;
            transition: background 0.3s;
        }

        button:hover {
            opacity: 0.9;
        }

        .btn-success { background-color: var(--secondary-color); }
        .btn-danger { background-color: var(--danger-color); }

        .table-responsive {
            overflow-x: auto;
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.05);
            margin-top: 20px;
        }

        table {
            width: 100%;
            border-collapse: collapse;
            text-align: left;
        }

        th, td {
            padding: 12px 15px;
            border-bottom: 1px solid var(--border-color);
        }

        th {
            background-color: var(--primary-color);
            color: white;
        }

        tr:hover {
            background-color: #f8f9fa;
        }

        .badge {
            padding: 5px 10px;
            border-radius: 20px;
            font-size: 12px;
            font-weight: bold;
            display: inline-block;
        }

        .badge-nhap { background-color: #d4edda; color: #155724; }
        .badge-xuat { background-color: #f8d7da; color: #721c24; }
        
        .alert-warning {
            color: var(--danger-color);
            font-weight: bold;
        }
    </style>
</head>
<body>

<div class="container">
    <h1>HỆ THỐNG QUẢN LÝ XUẤT NHẬP TỒN KHO HÀNG</h1>
    <p style="text-align: center; color: #7f8c8d;">Giải pháp lưu trữ Offline bảo mật bằng công nghệ HTML5 IndexedDB</p>
    <hr style="border: 0; border-top: 1px solid var(--border-color); margin: 20px 0;">

    <div class="grid-layout">
        <!-- Khối 1: Thêm sản phẩm mới -->
        <div class="card">
            <h3>1. Đăng Ký Mã Sản Phẩm</h3>
            <form id="product-form">
                <div class="form-group">
                    <label>Mã sản phẩm (SKU):</label>
                    <input type="text" id="prod-id" placeholder="VD: SP001" required>
                </div>
                <div class="form-group">
                    <label>Tên sản phẩm:</label>
                    <input type="text" id="prod-name" placeholder="VD: Điện thoại iPhone 15" required>
                </div>
                <button type="submit" class="btn-success">Thêm Sản Phẩm Vào Danh Mục</button>
            </form>
        </div>

        <!-- Khối 2: Thực hiện Nhập/Xuất kho -->
        <div class="card">
            <h3>2. Phát Sinh Giao Dịch Kho</h3>
            <form id="transaction-form">
                <div class="form-group">
                    <label>Chọn sản phẩm:</label>
                    <select id="tx-prod-id" required>
                        <option value="">-- Chọn sản phẩm --</option>
                    </select>
                </div>
                <div class="form-group">
                    <label>Loại giao dịch:</label>
                    <select id="tx-type" required>
                        <option value="NHAP">Nhập Kho (+)</option>
                        <option value="XUAT">Xuất Kho (-)</option>
                    </select>
                </div>
                <div class="form-group">
                    <label>Số lượng:</label>
                    <input type="number" id="tx-qty" min="1" placeholder="Số lượng giao dịch" required>
                </div>
                <button type="submit">Xác Nhận Thực Hiện</button>
            </form>
        </div>
    </div>

    <!-- Khối Báo Cáo Xuất Nhập Tồn -->
    <div class="card">
        <h3>BÁO CÁO XUẤT - NHẬP - TỒN KHO THỰC TẾ</h3>
        <div class="table-responsive">
            <table>
                <thead>
                    <tr>
                        <th>Mã Sản Phẩm</th>
                        <th>Tên Sản Phẩm</th>
                        <th>Tổng Nhập</th>
                        <th>Tổng Xuất</th>
                        <th>Tồn Kho Hiện Tại</th>
                        <th>Trạng Thái</th>
                    </tr>
                </thead>
                <tbody id="inventory-table-body">
                    <!-- Dữ liệu render bằng JS -->
                </tbody>
            </table>
        </div>
    </div>

    <!-- Khối Nhật ký giao dịch gần đây -->
    <div class="card" style="margin-top: 20px;">
        <h3>NHẬT KÝ GIAO DỊCH GẦN ĐÂY</h3>
        <div class="table-responsive">
            <table>
                <thead>
                    <tr>
                        <th>Thời Gian</th>
                        <th>Mã SP</th>
                        <th>Loại</th>
                        <th>Số Lượng</th>
                    </tr>
                </thead>
                <tbody id="history-table-body">
                    <!-- Dữ liệu render bằng JS -->
                </tbody>
            </table>
        </div>
    </div>
</div>

<script>
    // --- 1. KHỞI TẠO CƠ SỞ DỮ LIỆU INDEXEDDB ---
    const DB_NAME = 'WarehouseDB';
    const DB_VERSION = 1;
    let db;

    const request = indexedDB.open(DB_NAME, DB_VERSION);

    request.onerror = (event) => console.error("Lỗi mở Database:", event.target.error);

    request.onupgradeneeded = (event) => {
        db = event.target.result;
        // Tạo bảng chứa danh mục sản phẩm
        if (!db.objectStoreNames.contains('products')) {
            db.createObjectStore('products', { keyPath: 'id' });
        }
        // Tạo bảng chứa lịch sử giao dịch nhập xuất
        if (!db.objectStoreNames.contains('transactions')) {
            db.createObjectStore('transactions', { keyPath: 'id', autoIncrement: true });
        }
    };

    request.onsuccess = (event) => {
        db = event.target.result;
        initApp();
    };

    function initApp() {
        updateProductDropdown();
        renderInventoryReport();
        renderHistoryTable();
    }

    // --- 2. XỬ LÝ SỰ KIỆN THÊM SẢN PHẨM MỚI ---
    document.getElementById('product-form').addEventListener('submit', function(e) {
        e.preventDefault();
        const id = document.getElementById('prod-id').value.trim().toUpperCase();
        const name = document.getElementById('prod-name').value.trim();

        const transaction = db.transaction(['products'], 'readwrite');
        const store = transaction.objectStore('products');
        
        // Kiểm tra trùng mã sản phẩm trước khi thêm
        const checkRequest = store.get(id);
        checkRequest.onsuccess = function() {
            if (checkRequest.result) {
                alert("Mã sản phẩm này đã tồn tại trong danh mục!");
                return;
            }
            
            store.add({ id, name });
            transaction.oncomplete = () => {
                alert("Thêm sản phẩm thành công!");
                document.getElementById('product-form').reset();
                initApp();
            };
        };
    });

    // --- 3. XỬ LÝ SỰ KIỆN GIAO DỊCH NHẬP XUẤT ---
    document.getElementById('transaction-form').addEventListener('submit', function(e) {
        e.preventDefault();
        const productId = document.getElementById('tx-prod-id').value;
        const type = document.getElementById('tx-type').value;
        const qty = parseInt(document.getElementById('tx-qty').value);
        const timestamp = new Date().toLocaleString('vi-VN');

        if (type === 'XUAT') {
            // Kiểm tra lượng hàng tồn kho trước khi cho phép xuất
            calculateCurrentStock(productId, (currentStock) => {
                if (qty > currentStock) {
                    alert(`Không thể xuất hàng! Số lượng xuất (${qty}) vượt quá số lượng tồn kho hiện tại (${currentStock}).`);
                } else {
                    saveTransaction(productId, type, qty, timestamp);
                }
            });
        } else {
            saveTransaction(productId, type, qty, timestamp);
        }
    });

    function saveTransaction(productId, type, qty, timestamp) {
        const transaction = db.transaction(['transactions'], 'readwrite');
        const store = transaction.objectStore('transactions');
        store.add({ productId, type, qty, timestamp });
        
        transaction.oncomplete = () => {
            alert("Ghi nhận giao dịch thành công!");
            document.getElementById('transaction-form').reset();
            initApp();
        };
    }

    // --- 4. HÀM TRỢ GIÚP TÍNH TOÁN VÀ RENDER DỮ LIỆU ---
    function updateProductDropdown() {
        const dropdown = document.getElementById('tx-prod-id');
        dropdown.innerHTML = '<option value="">-- Chọn sản phẩm --</option>';

        const transaction = db.transaction(['products'], 'readonly');
        const store = transaction.objectStore('products');
        
        store.openCursor().onsuccess = (event) => {
            const cursor = event.target.result;
            if (cursor) {
                const option = document.createElement('option');
                option.value = cursor.value.id;
                option.textContent = `${cursor.value.id} - ${cursor.value.name}`;
                dropdown.appendChild(option);
                cursor.continue();
            }
        };
    }

    function calculateCurrentStock(productId, callback) {
        const transaction = db.transaction(['transactions'], 'readonly');
        const store = transaction.objectStore('transactions');
        let stock = 0;

        store.openCursor().onsuccess = (event) => {
            const cursor = event.target.result;
            if (cursor) {
                if (cursor.value.productId === productId) {
                    if (cursor.value.type === 'NHAP') stock += cursor.value.qty;
                    if (cursor.value.type === 'XUAT') stock -= cursor.value.qty;
                }
                cursor.continue();
            } else {
                callback(stock);
            }
        };
    }

    function renderInventoryReport() {
        const tbody = document.getElementById('inventory-table-body');
        tbody.innerHTML = '';

        const prodTx = db.transaction(['products'], 'readonly');
        const prodStore = prodTx.objectStore('products');
        
        const productsList = [];
        prodStore.openCursor().onsuccess = (e) => {
            const cursor = e.target.result;
            if(cursor) {
                productsList.push({...cursor.value});
                cursor.continue();
            } else {
                // Sau khi lấy hết sản phẩm, tính toán nhập xuất từ bảng transactions
                const txTx = db.transaction(['transactions'], 'readonly');
                const txStore = txTx.objectStore('transactions');
                const txList = [];
                
                txStore.openCursor().onsuccess = (e2) => {
                    const cursor2 = e2.target.result;
                    if(cursor2) {
                        txList.push(cursor2.value);
                        cursor2.continue();
                    } else {
                        // Tạo báo cáo tổng hợp
                        productsList.forEach(prod => {
                            let totalNhap = 0;
                            let totalXuat = 0;
                            
                            txList.forEach(tx => {
                                if(tx.productId === prod.id) {
                                    if(tx.type === 'NHAP') totalNhap += tx.qty;
                                    if(tx.type === 'XUAT') totalXuat += tx.qty;
                                }
                            });
                            
                            let tonKho = totalNhap - totalXuat;
                            let statusBadge = tonKho <= 5 ? '<span class="alert-warning">Sắp hết hàng!</span>' : '<span style="color:green;">An toàn</span>';

                            const row = `<tr>
                                <td><b>${prod.id}</b></td>
                                <td>${prod.name}</td>
                                <td style="color: #27ae60;">+${totalNhap}</td>
                                <td style="color: #c0392b;">-${totalXuat}</td>
                                <td><mark><b>${tonKho}</b></mark></td>
                                <td>${statusBadge}</td>
                            </tr>`;
                            tbody.innerHTML += row;
                        });
                    }
                };
            }
        };
    }

    function renderHistoryTable() {
        const tbody = document.getElementById('history-table-body');
        tbody.innerHTML = '';

        const transaction = db.transaction(['transactions'], 'readonly');
        const store = transaction.objectStore('transactions');
        const items = [];

        store.openCursor().onsuccess = (event) => {
            const cursor = event.target.result;
            if (cursor) {
                items.push(cursor.value);
                cursor.continue();
            } else {
                // Hiển thị các giao dịch mới nhất lên đầu
                items.reverse().slice(0, 10).forEach(item => {
                    const badgeClass = item.type === 'NHAP' ? 'badge-nhap' : 'badge-xuat';
                    const textType = item.type === 'NHAP' ? 'Nhập Kho' : 'Xuất Kho';
                    const row = `<tr>
                        <td>${item.timestamp}</td>
                        <td>${item.productId}</td>
                        <td><span class="badge ${badgeClass}">${textType}</span></td>
                        <td><b>${item.qty}</b></td>
                    </tr>`;
                    tbody.innerHTML += row;
                });
            }
        };
    }
</script>

</body>
</html>

Các tính năng nổi bật của đoạn code trên

  1. Chống xuất âm kho: Hệ thống tự động kiểm tra lượng hàng tồn thực tế, nếu số lượng hàng yêu cầu xuất lớn hơn số lượng trong kho, chương trình sẽ tự động chặn lại và gửi cảnh báo.

  2. Cảnh báo thông minh: Tại bảng thống kê, nếu bất kỳ sản phẩm nào có số lượng tồn kho bằng hoặc nhỏ hơn , hệ thống sẽ hiển thị dòng chữ đỏ Sắp hết hàng! để nhắc nhở bạn nhập thêm hàng.

  3. Báo cáo real-time: Mọi thao tác thêm sản phẩm, nhập kho hay xuất kho đều cập nhật tức thì lên bảng hiển thị mà không cần phải tải lại trang blog.

Chúc các bạn tích hợp thành công ứng dụng quản lý kho tiện lợi này vào Blogspot của mình! Nếu có bất kỳ thắc mắc nào trong quá trình chạy code, hãy để lại bình luận phía dưới nhé.

Phần Mềm Quản Lý Kho Hàng Xuất Nhập Tồn

HỆ THỐNG QUẢN LÝ XUẤT NHẬP TỒN KHO HÀNG

Giải pháp lưu trữ Offline bảo mật bằng công nghệ HTML5 IndexedDB


1. Đăng Ký Mã Sản Phẩm

2. Phát Sinh Giao Dịch Kho

BÁO CÁO XUẤT - NHẬP - TỒN KHO THỰC TẾ

Mã Sản Phẩm Tên Sản Phẩm Tổng Nhập Tổng Xuất Tồn Kho Hiện Tại Trạng Thái

NHẬT KÝ GIAO DỊCH GẦN ĐÂY

Thời Gian Mã SP Loại Số Lượng