Phần Mềm

  • QL

    <html lang="vi"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Quản Lý Hàng Hóa - CameGPS</title> <style> :root{--accent:#0d6efd;--danger:#dc3545;--muted:#666} body{font-family:Inter, Arial, sans-serif;margin:0;background:#f3f6fb;color:#111} .app{max-width:1100px;margin:20px auto;padding:20px} .card{background:#fff;border-radius:12px;padding:16px;box-shadow:0 6px 18px rgba(20,30,60,0.06);margin-bottom:16px} h1{margin:0 0 8px;font-size:20px} .grid{display:grid;gap:12px} .grid.cols-3{grid-template-columns:1fr 120px 120px} input,select,button{padding:8px;border-radius:8px;border:1px solid #e1e7ef} button{cursor:pointer} table{width:100%;border-collapse:collapse;margin-top:12px} th,td{padding:8px;border-bottom:1px solid #eef3fb;text-align:left} .muted{color:var(--muted)} .row{display:flex;gap:8px;align-items:center} .actions button{padding:6px 8px} .small{font-size:13px} .badge{display:inline-block;padding:4px 8px;border-radius:999px;background:#f1f6ff;color:var(--accent);font-weight:600} </style> </head> <body> <div class="app"> <div class="card"> <h1>📦 Quản Lý Hàng Hóa (Mobile + Desktop)</h1> <div class="muted small">Tính năng: Thêm / Sửa / Xóa hàng · Nhập / Bán (ghi giao dịch) · Tồn kho tự tính · Cảnh báo tồn kho thấp · Nhập / xuất CSV · Đồng bộ Google Drive</div> </div> <div class="card"> <h2>Thêm / Cập nhật hàng</h2> <div class="grid cols-3"> <input id="name" placeholder="Tên hàng hóa" /> <input id="sku" placeholder="Mã (SKU) - tùy chọn" /> <input id="price" type="number" placeholder="Giá (VNĐ)" /> </div> <div class="grid cols-3" style="margin-top:8px"> <input id="qty" type="number" placeholder="Số lượng" /> <input id="minStock" type="number" placeholder="Mức cảnh báo tồn kho" /> <select id="category"><option value="">Danh mục</option><option>Phụ kiện</option><option>Camera</option><option>Phụ tùng</option></select> </div> <div style="margin-top:8px" class="row"> <button onclick="saveItem()" style="background:var(--accent);color:#fff">Lưu hàng</button> <button onclick="clearForm()">Xóa form</button> <div class="muted small">Đang chỉnh sửa: <span id="editing">-</span></div> </div> </div> <div class="card"> <div style="display:flex;justify-content:space-between;align-items:center"> <h2>Danh sách hàng</h2> <div class="row"> <input id="search" placeholder="Tìm theo tên hoặc SKU" oninput="render()" /> <select id="filterCat" onchange="render()"><option value="">Tất cả</option><option>Phụ kiện</option><option>Camera</option><option>Phụ tùng</option></select> <button onclick="exportCSV()">Xuất CSV</button> <input type="file" id="csvfile" accept=".csv" onchange="importCSV(event)" style="display:none" /> <button onclick="document.getElementById('csvfile').click()">Nhập CSV</button> </div> </div> <table> <thead> <tr><th>Tên</th><th>SKU</th><th>SL</th><th>Giá</th><th>Đơn vị</th><th>Min</th><th>Ghi chú</th><th>Hành động</th></tr> </thead> <tbody id="list"></tbody> </table> </div> <div class="card"> <h2>Nhập - Bán hàng (Ghi giao dịch)</h2> <div class="grid" style="grid-template-columns:1fr 120px 120px 120px;align-items:end"> <select id="txSku"></select> <input id="txQty" type="number" placeholder="Số lượng" /> <select id="txType"><option value="in">Nhập hàng</option><option value="out">Bán hàng</option></select> <button onclick="recordTransaction()">Ghi giao dịch</button> </div> <div style="margin-top:12px"> <h3 class="small">Lịch sử giao dịch</h3> <table> <thead><tr><th>Ngày</th><th>SKU</th><th>Tên</th><th>Loại</th><th>Số lượng</th><th>Ghi chú</th></tr></thead> <tbody id="txList"></tbody> </table> </div> </div> <div class="card"> <h2>Đồng bộ Google Drive (tùy chọn)</h2> <div class="muted small">Hướng dẫn ngắn: bạn cần tạo OAuth Client ID trên Google Cloud Console, bật Drive API và dán CLIENT_ID vào chỗ bên dưới. Ứng dụng sẽ lưu một file JSON (inventory.json) vào Drive để sao lưu / tải lại dữ liệu.</div> <div style="margin-top:8px" class="row"> <input id="googleClientId" placeholder="GOOGLE_CLIENT_ID (dán vào đây)" style="width:360px" /> <button onclick="initGapi()">Kết nối Google</button> <button onclick="saveToDrive()">Lưu lên Drive</button> <button onclick="loadFromDrive()">Tải từ Drive</button> </div> <div id="gstatus" class="muted small" style="margin-top:8px">Chưa kết nối</div> </div> </div> <script> // --- Dữ liệu --- let items = JSON.parse(localStorage.getItem('inventory_items') || '[]'); let transactions = JSON.parse(localStorage.getItem('inventory_tx') || '[]'); let editingIndex = -1; // --- Utility --- function saveLocal(){ localStorage.setItem('inventory_items', JSON.stringify(items)); localStorage.setItem('inventory_tx', JSON.stringify(transactions)); render(); populateTxSelect(); } function formatDate(d){return new Date(d).toLocaleString();} // --- Item CRUD --- function saveItem(){ const name = document.getElementById('name').value.trim(); const sku = document.getElementById('sku').value.trim() || 'SKU-'+Math.random().toString(36).slice(2,8).toUpperCase(); const price = Number(document.getElementById('price').value) || 0; const qty = Number(document.getElementById('qty').value) || 0; const minStock = Number(document.getElementById('minStock').value) || 0; const category = document.getElementById('category').value; if(!name) return alert('Vui lòng nhập tên hàng'); const item = {name,sku,price,qty,minStock,category,note:''}; if(editingIndex >=0){ // update existing by index items[editingIndex] = {...items[editingIndex], ...item}; editingIndex = -1; document.getElementById('editing').innerText = '-'; } else { // prevent duplicate SKU if(items.find(i=>i.sku===sku)) return alert('SKU đã tồn tại, hãy đổi mã hoặc sửa hàng hiện có'); items.push(item); } clearForm(); saveLocal(); } function editItem(index){ const it = items[index]; editingIndex = index; document.getElementById('name').value = it.name; document.getElementById('sku').value = it.sku; document.getElementById('price').value = it.price; document.getElementById('qty').value = it.qty; document.getElementById('minStock').value = it.minStock; document.getElementById('category').value = it.category || ''; document.getElementById('editing').innerText = `${it.name} (idx ${index})`; window.scrollTo({top:0,behavior:'smooth'}); } function deleteItem(index){ if(!confirm('Xác nhận xóa hàng này?')) return; items.splice(index,1); saveLocal(); } function clearForm(){ document.getElementById('name').value=''; document.getElementById('sku').value=''; document.getElementById('price').value=''; document.getElementById('qty').value=''; document.getElementById('minStock').value=''; document.getElementById('category').value=''; } // --- Render --- function render(){ const list = document.getElementById('list'); const search = document.getElementById('search').value.toLowerCase(); const filterCat = document.getElementById('filterCat').value; list.innerHTML=''; items.forEach((it,idx)=>{ if(search && !(it.name.toLowerCase().includes(search) || (it.sku||'').toLowerCase().includes(search))) return; if(filterCat && it.category!==filterCat) return; const low = it.minStock>0 && it.qty<=it.minStock; list.innerHTML += `<tr> <td>${it.name}</td> <td>${it.sku}</td> <td>${it.qty} ${low?'Tồn thấp':''}</td> <td>${Number(it.price).toLocaleString()}₫</td> <td>${it.unit||'Cái'}</td> <td>${it.minStock||'-'}</td> <td>${it.note||'-'}</td> <td class="actions"> <button onclick="editItem(${idx})">Sửa</button> <button onclick="deleteItem(${idx})" style="background:var(--danger);color:#fff">Xóa</button> <button onclick="quickIn(${idx})">Nhập</button> <button onclick="quickOut(${idx})">Bán</button> </td> </tr>`; }); } // --- Quick in/out --- function quickIn(index){ const q = Number(prompt('Số lượng nhập:', '1'))||0; if(q<=0) return; transactions.push({date:Date.now(),sku:items[index].sku,type:'in',qty:q,note:'Nhập nhanh'}); items[index].qty += q; saveLocal(); } function quickOut(index){ const q = Number(prompt('Số lượng bán:', '1'))||0; if(q<=0) return; if(items[index].qty - q < 0 && !confirm('Số lượng sẽ âm, tiếp tục?')) return; transactions.push({date:Date.now(),sku:items[index].sku,type:'out',qty:q,note:'Bán nhanh'}); items[index].qty -= q; saveLocal(); } // --- Transaction handling --- function populateTxSelect(){ const sel = document.getElementById('txSku'); sel.innerHTML=''; items.forEach(it=>{ const opt = document.createElement('option'); opt.value = it.sku; opt.text = `${it.sku} — ${it.name}`; sel.appendChild(opt); }); } function recordTransaction(){ const sku = document.getElementById('txSku').value; const qty = Number(document.getElementById('txQty').value)||0; const type = document.getElementById('txType').value; if(!sku || qty<=0) return alert('Chọn SKU và số lượng > 0'); const item = items.find(i=>i.sku===sku); if(!item) return alert('SKU không tìm thấy'); if(type==='out' && item.qty - qty < 0 && !confirm('Số lượng sẽ âm, tiếp tục?')) return; transactions.push({date:Date.now(),sku,type,qty,note:''}); item.qty += (type==='in'? qty : -qty); document.getElementById('txQty').value=''; saveLocal(); } function renderTx(){ const txList = document.getElementById('txList'); txList.innerHTML=''; const recent = transactions.slice().reverse(); recent.forEach(tx=>{ const it = items.find(i=>i.sku===tx.sku) || {name:'-'}; txList.innerHTML += `<tr><td>${formatDate(tx.date)}</td><td>${tx.sku}</td><td>${it.name}</td><td>${tx.type}</td><td>${tx.qty}</td><td>${tx.note||'-'}</td></tr>`; }); } // --- CSV export/import --- function exportCSV(){ let csv = 'name,sku,price,qty,minStock,category,noten'; items.forEach(it=>{csv += `"${it.name}","${it.sku}",${it.price},${it.qty},${it.minStock},"${it.category || ''}","${(it.note||'').replace(/"/g,'""')}"n`}); const blob = new Blob([csv],{type:'text/csv;charset=utf-8;'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'inventory.csv'; a.click(); URL.revokeObjectURL(url); } function importCSV(e){ const f = e.target.files[0]; if(!f) return; const reader = new FileReader(); reader.onload = ()=>{ const text = reader.result; const lines = text.split(/r?n/).filter(Boolean); const data = lines.slice(1).map(l=>{ const cols = l.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/); // crude CSV parse return {name:cols[0].replace(/^"|"$/g,''), sku:cols[1].replace(/^"|"$/g,''), price:Number(cols[2])||0, qty:Number(cols[3])||0, minStock:Number(cols[4])||0, category:cols[5].replace(/^"|"$/g,''), note:cols[6]?cols[6].replace(/^"|"$/g,''):''}; }); // merge or add data.forEach(d=>{ const idx = items.findIndex(i=>i.sku===d.sku); if(idx>=0) items[idx] = {...items[idx],...d}; else items.push(d); }); saveLocal(); alert('Nhập CSV xong'); }; reader.readAsText(f,'utf-8'); e.target.value=''; } // --- Google Drive sync (client-side) --- // NOTE: You must create OAuth Client ID in Google Cloud Console, enable Drive API and paste CLIENT_ID below. // This implementation uses simple file create & list to save a JSON file named 'inventory_backup.json'. let gapiInited = false; let googleAuth = null; let CLIENT_ID = ''; function initGapi(){ CLIENT_ID = document.getElementById('googleClientId').value.trim(); if(!CLIENT_ID) return alert('Dán GOOGLE_CLIENT_ID vào ô input trước khi kết nối'); loadGapiClient(()=>{ gapiInited = true; document.getElementById('gstatus').innerText = 'GAPI đã sẵn sàng. Hãy đăng nhập.'; signIn(); }); } function loadGapiClient(cb){ if(window.gapi) return cb(); const s = document.createElement('script'); s.src = 'https://apis.google.com/js/api.js'; s.onload = ()=>{ gapi.load('client:auth2', async ()=>{ await gapi.client.init({apiKey: null, clientId: CLIENT_ID, scope:'https://www.googleapis.com/auth/drive.file'}); googleAuth = gapi.auth2.getAuthInstance(); cb(); }); }; document.head.appendChild(s); } async function signIn(){ if(!googleAuth) return alert('Chưa khởi tạo GAPI'); try{ await googleAuth.signIn(); document.getElementById('gstatus').innerText = 'Đã đăng nhập: ' + googleAuth.currentUser.get().getBasicProfile().getEmail(); }catch(e){console.error(e); alert('Không thể đăng nhập Google: '+e.message)} } async function saveToDrive(){ if(!gapiInited) return alert('Chưa kết nối Google'); const content = JSON.stringify({items,transactions}); const metadata = {name:'inventory_backup.json', mimeType:'application/json'}; const boundary = '-------314159265358979323846'; const delimiter = 'rn--' + boundary + 'rn'; const closeDelimiter = 'rn--' + boundary + '--'; const multipartRequestBody = delimiter + 'Content-Type: application/json; charset=UTF-8rnrn' + JSON.stringify(metadata) + delimiter + 'Content-Type: application/jsonrnrn' + content + closeDelimiter; try{ const response = await gapi.client.request({ path: '/upload/drive/v3/files', method: 'POST', params: {uploadType: 'multipart'}, headers: {'Content-Type': 'multipart/related; boundary="' + boundary + '"'}, body: multipartRequestBody }); alert('Lưu lên Drive thành công'); }catch(e){console.error(e); alert('Lưu thất bại: '+ (e.message||e.result||e))} } async function loadFromDrive(){ if(!gapiInited) return alert('Chưa kết nối Google'); try{ const res = await gapi.client.drive.files.list({q:"name='inventory_backup.json' and trashed=false", fields:'files(id,name,modifiedTime)'}); if(!res.result.files || res.result.files.length===0) return alert('Không tìm thấy file inventory_backup.json trên Drive'); const file = res.result.files[0]; const contentRes = await gapi.client.request({path:`/drive/v3/files/${file.id}?alt=media`}); const data = typeof contentRes.result === 'string' ? JSON.parse(contentRes.result) : contentRes.result; items = data.items || []; transactions = data.transactions || []; saveLocal(); alert('Tải dữ liệu từ Drive xong (file: '+file.name+')'); }catch(e){console.error(e); alert('Tải thất bại: '+(e.message||e.result||e))} } // --- Init --- function boot(){ saveLocal(); renderTx(); populateTxSelect(); render(); setInterval(()=>{render();renderTx()}, 1000*10); } // run boot(); </script> </body> </html>
    Ngày: 16-11-2025
  • Kiki auto

    Ngày: 25-02-2025

logo-70mai-camegps
logo-protrack-camegps
logo-vietmap-camegps
camera-giam-sat-4g-tai-hue
brand_5
brand_6
CameGPS.com
Số 10 Nguyễn Khuyến, P. Phú Nhuận, Tp. Huế
Tổng đài hỗ trợ:0986864070

Từ 8h00 - 17h00 các ngày thứ 2 đến chủ nhật

camegps@gmail.com

Phương thức thanh toán: payment