-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Open
Description
<title>Caisse Tactile & Scanner</title>
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
🖨️ Imprimer le ticket
<script>
/* --- VARIABLES GLOBALES --- */
let cart = [];
let currentInput = "";
let html5QrCode = null;
let isScanning = false;
// Données persistantes
let inventory = JSON.parse(localStorage.getItem('bw_inventory')) || [
{code: "123456", name: "Coca-Cola", price: 2.50, tax: 21, stock: 50},
{code: "789012", name: "Croissant", price: 1.20, tax: 6, stock: 20}
];
let salesHistory = JSON.parse(localStorage.getItem('bw_sales')) || [];
let storeConfig = JSON.parse(localStorage.getItem('bw_config')) || {
name: "MON MAGASIN", addr: "Rue du Commerce 10", tax: "BE000.000.000"
};
/* --- INITIALISATION --- */
window.onload = function() {
updateTicketHeader();
updateDate();
setInterval(updateDate, 60000);
renderInventory();
};
function updateDate() {
const now = new Date();
document.getElementById('ticketDate').innerText = "Date: " + now.toLocaleString('fr-BE');
}
function updateTicketHeader() {
document.getElementById('ticketStoreName').innerText = storeConfig.name;
document.getElementById('ticketStoreAddr').innerText = storeConfig.addr;
document.getElementById('ticketStoreTax').innerText = "TVA: " + storeConfig.tax;
// Prefill config inputs
document.getElementById('confName').value = storeConfig.name;
document.getElementById('confAddr').value = storeConfig.addr;
document.getElementById('confTax').value = storeConfig.tax;
}
/* --- LOGIQUE ECRAN & PAVE --- */
function appendNumber(n) {
if (n === '.' && currentInput.includes('.')) return;
currentInput += n;
document.getElementById('display').innerText = currentInput;
}
function clearDisplay() {
currentInput = "";
document.getElementById('display').innerText = "0";
}
/* --- LOGIQUE SCANNER CAMERA --- */
function toggleScanner() {
if (isScanning) {
stopScanner();
} else {
startScanner();
}
}
function startScanner() {
const readerDiv = document.getElementById('reader');
readerDiv.style.display = "block";
html5QrCode = new Html5Qrcode("reader");
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
// facingMode: "environment" force la caméra arrière (idéal S24 Ultra)
html5QrCode.start({ facingMode: "environment" }, config, onScanSuccess)
.then(() => {
isScanning = true;
document.getElementById('statusMsg').innerText = "Scan en cours...";
})
.catch(err => {
alert("Erreur caméra : " + err + "\nAssurez-vous d'être en HTTPS ou localhost.");
readerDiv.style.display = "none";
});
}
function stopScanner() {
if (html5QrCode) {
html5QrCode.stop().then(() => {
document.getElementById('reader').style.display = "none";
isScanning = false;
document.getElementById('statusMsg').innerText = "Prêt";
html5QrCode.clear();
});
}
}
function onScanSuccess(decodedText, decodedResult) {
// Jouer un petit son (hack base64 court)
// En production, utiliser un vrai fichier mp3
const audio = new Audio('https://actions.google.com/sounds/v1/alarms/beep_short.ogg');
audio.play().catch(e => console.log("Audio blocké"));
handleBarcode(decodedText);
// Pause temporaire pour éviter les doubles scans
html5QrCode.pause();
setTimeout(() => html5QrCode.resume(), 1000);
}
function handleBarcode(code) {
const product = inventory.find(p => p.code === code);
if (product) {
addToCart(product.name, product.price, product.tax, true, code);
document.getElementById('statusMsg').innerText = "Ajouté: " + product.name;
} else {
alert("Produit inconnu: " + code);
// Possibilité d'ouvrir la modale d'ajout ici
}
}
/* --- LOGIQUE PANIER & TVA --- */
// Règle demandée: TVA calculée sur le prix TTC (Prix * Taux / 100)
function calculateTaxAmount(price, rate) {
return price * (rate / 100);
}
function addManualItem(taxRate) {
if (!currentInput) return;
const price = parseFloat(currentInput);
addToCart("Article Manuel", price, taxRate, false, null);
clearDisplay();
}
function addToCart(name, price, taxRate, isStock, code) {
if (isStock && code) {
const itemIndex = inventory.findIndex(p => p.code === code);
if (inventory[itemIndex].stock <= 0) {
alert("Stock épuisé pour " + name);
return;
}
}
const taxAmount = calculateTaxAmount(price, taxRate);
cart.push({
name: name,
price: price,
taxRate: taxRate,
taxAmount: taxAmount,
isStock: isStock,
code: code
});
renderTicket();
}
function removeFromCart(index) {
cart.splice(index, 1);
renderTicket();
}
function renderTicket() {
const list = document.getElementById('ticketItems');
list.innerHTML = "";
let total = 0;
cart.forEach((item, index) => {
total += item.price;
const div = document.createElement('div');
div.className = 'ticket-row';
div.innerHTML = `
${item.name}
${item.price.toFixed(2)}
❌
`;
list.appendChild(div);
});
document.getElementById('ticketTotal').innerText = total.toFixed(2) + " €";
}
/* --- PAIEMENT --- */
function processPayment() {
if (cart.length === 0) return;
// Déduire les stocks
cart.forEach(item => {
if (item.isStock && item.code) {
const idx = inventory.findIndex(p => p.code === item.code);
if (idx > -1) inventory[idx].stock--;
}
});
localStorage.setItem('bw_inventory', JSON.stringify(inventory));
renderInventory();
// Sauvegarder la vente
const total = cart.reduce((acc, item) => acc + item.price, 0);
salesHistory.push({
date: new Date().toISOString(),
items: [...cart],
total: total
});
localStorage.setItem('bw_sales', JSON.stringify(salesHistory));
// Animation impression
window.print(); // Lance l'impression automatique du navigateur
// Reset
cart = [];
renderTicket();
currentInput = "";
updateDisplay();
}
/* --- GESTION --- */
function addProduct() {
const code = document.getElementById('prodCode').value || "MAN-" + Date.now();
const name = document.getElementById('prodName').value;
const price = parseFloat(document.getElementById('prodPrice').value);
const stock = parseInt(document.getElementById('prodStock').value);
const tax = parseInt(document.getElementById('prodTax').value);
if (name && price) {
// Check if exists
const existingIdx = inventory.findIndex(p => p.code === code);
if (existingIdx > -1) {
inventory[existingIdx] = {code, name, price, tax, stock};
} else {
inventory.push({code, name, price, tax, stock});
}
localStorage.setItem('bw_inventory', JSON.stringify(inventory));
renderInventory();
// Reset champs
document.getElementById('prodCode').value = "";
document.getElementById('prodName').value = "";
document.getElementById('prodPrice').value = "";
}
}
function renderInventory() {
const tbody = document.querySelector('#inventoryTable tbody');
tbody.innerHTML = "";
inventory.forEach((p, i) => {
tbody.innerHTML += `
${p.code}
${p.name}
${p.price}
${p.stock}
Del
`;
});
}
function deleteProd(i) {
inventory.splice(i, 1);
localStorage.setItem('bw_inventory', JSON.stringify(inventory));
renderInventory();
}
function generateZReport() {
let total = 0;
let taxes = {0: 0, 6: 0, 21: 0};
salesHistory.forEach(sale => {
total += sale.total;
sale.items.forEach(item => {
if (taxes[item.taxRate] !== undefined) {
taxes[item.taxRate] += item.taxAmount;
}
});
});
const html = `
<style>
/* --- DESIGN NOIR ET BLANC & RESET --- */
:root {
--bg-color: #ffffff;
--text-color: #000000;
--border-color: #000000;
--highlight: #e0e0e0;
}
* { box-sizing: border-box; }
body {
font-family: 'Courier New', Courier, monospace;
margin: 0; padding: 0;
background-color: var(--bg-color);
color: var(--text-color);
height: 100vh;
display: flex;
flex-direction: column; /* Mobile first logic adjusted */
}
/* --- LAYOUT --- */
.container {
display: flex;
height: 100%;
overflow: hidden;
}
/* GAUCHE: TICKET (VISUALISATION) */
.left-panel {
width: 35%;
border-right: 3px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 15px;
background: #fff;
box-shadow: 5px 0 15px rgba(0,0,0,0.1);
z-index: 10;
}
/* Style Papier Ticket */
.ticket-paper {
background: white;
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.ticket-header {
text-align: center;
border-bottom: 2px dashed var(--border-color);
padding-bottom: 15px;
margin-bottom: 10px;
}
.ticket-header h2 { margin: 5px 0; text-transform: uppercase; }
.ticket-header p { margin: 2px 0; font-size: 12px; }
.ticket-body {
flex-grow: 1;
overflow-y: auto;
border-bottom: 2px dashed var(--border-color);
}
.ticket-row {
display: flex;
justify-content: space-between;
font-size: 14px;
margin-bottom: 5px;
}
.ticket-row span:first-child { font-weight: bold; }
.ticket-footer {
padding-top: 15px;
}
.total-line {
display: flex;
justify-content: space-between;
font-size: 22px;
font-weight: bold;
border-top: 2px solid black;
border-bottom: 2px solid black;
padding: 10px 0;
margin-bottom: 10px;
}
.print-btn {
width: 100%;
background: black;
color: white;
border: none;
padding: 10px;
font-family: inherit;
cursor: pointer;
text-transform: uppercase;
margin-top: 10px;
}
/* DROITE: COMMANDES */
.right-panel {
width: 65%;
display: flex;
flex-direction: column;
padding: 10px;
background: #f4f4f4;
}
/* SCANNER AREA */
#reader {
width: 100%;
background: black;
display: none; /* Caché par défaut */
margin-bottom: 10px;
border: 2px solid black;
}
/* ECRAN PRINCIPAL */
.screen {
background: white;
border: 3px solid black;
height: 70px;
font-size: 40px;
text-align: right;
padding: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.screen small { font-size: 14px; color: #555; }
/* BOUTONS ADMIN */
.admin-bar { display: flex; gap: 5px; margin-bottom: 10px; flex-wrap: wrap; }
.btn-top {
flex: 1; padding: 8px; border: 2px solid black; background: white; cursor: pointer; font-weight: bold;
min-width: 100px;
}
.btn-scan { background: black; color: white; }
/* GRILLE CONTROLES */
.control-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 10px;
flex-grow: 1;
}
.numpad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.actions { display: grid; grid-template-columns: 1fr; gap: 8px; }
button.key {
font-size: 22px; background: white; border: 2px solid black; cursor: pointer; border-radius: 4px;
}
button.key:active { background: black; color: white; }
.btn-tva { background: #ddd; font-weight: bold; font-size: 16px; }
.btn-pay { background: black; color: white; font-size: 24px; font-weight: bold; }
/* IMPRESSION CSS */
@media print {
.right-panel, .admin-bar, .modal, .print-btn, .delete-btn { display: none !important; }
.left-panel { width: 100%; border: none; box-shadow: none; position: absolute; top: 0; left: 0; }
.container { display: block; }
body { height: auto; overflow: visible; }
}
/* MODALES */
.modal {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.8); z-index: 100;
}
.modal-content {
background: white; width: 90%; max-width: 600px; margin: 50px auto;
padding: 20px; border: 4px solid black; max-height: 90vh; overflow-y: auto;
}
.close { float: right; font-size: 30px; cursor: pointer; }
input, select { padding: 8px; border: 2px solid black; margin: 5px 0; width: 100%; font-family: inherit; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; }
th, td { border: 1px solid black; padding: 5px; text-align: left; }
</style>
MAGASIN
Adresse...
TVA: ...
Date: --/--/----
TOTAL À PAYER
0.00 €
Merci de votre visite !
***
***
<div class="right-panel">
<div id="reader"></div>
<div class="admin-bar">
<button class="btn-top btn-scan" onclick="toggleScanner()">📷 SCANNER (S24)</button>
<button class="btn-top" onclick="openModal('inventoryModal')">📦 Stock</button>
<button class="btn-top" onclick="generateZReport()">📄 Rapport Z</button>
<button class="btn-top" onclick="openModal('storeModal')">⚙️ Config</button>
</div>
<div class="screen">
<small id="statusMsg">Prêt</small>
<span id="display">0</span>
</div>
<div class="control-grid">
<div class="numpad">
<button class="key" onclick="appendNumber('7')">7</button>
<button class="key" onclick="appendNumber('8')">8</button>
<button class="key" onclick="appendNumber('9')">9</button>
<button class="key" onclick="appendNumber('4')">4</button>
<button class="key" onclick="appendNumber('5')">5</button>
<button class="key" onclick="appendNumber('6')">6</button>
<button class="key" onclick="appendNumber('1')">1</button>
<button class="key" onclick="appendNumber('2')">2</button>
<button class="key" onclick="appendNumber('3')">3</button>
<button class="key" onclick="appendNumber('0')">0</button>
<button class="key" onclick="appendNumber('.')">.</button>
<button class="key" onclick="clearDisplay()">C</button>
</div>
<div class="actions">
<button class="key btn-tva" onclick="addManualItem(21)">TVA 21%</button>
<button class="key btn-tva" onclick="addManualItem(6)">TVA 6%</button>
<button class="key btn-tva" onclick="addManualItem(0)">TVA 0%</button>
<button class="key btn-pay" onclick="processPayment()">PAYER</button>
</div>
</div>
</div>
×
TVA 21%
TVA 6%
TVA 0%
Ajouter / Mettre à jour
Gestion Stock & Codes-Barres
| Code | Nom | Prix | Stock | Action |
|---|
×
Rapport Z (Fin de journée)
CLÔTURER LA JOURNÉE
×
Configuration Magasin
Nom: Adresse: TVA: SauvegarderCA Total: ${total.toFixed(2)} €
TVA 21% (Montant): ${taxes[21].toFixed(2)} €
TVA 6% (Montant): ${taxes[6].toFixed(2)} €
TVA 0% (Montant): ${taxes[0].toFixed(2)} €
Calcul: Prix x Taux (inclus)
`; document.getElementById('zContent').innerHTML = html; openModal('zReportModal'); } function resetDay() { if (confirm("Clôturer la journée et effacer l'historique ?")) { salesHistory = []; localStorage.setItem('bw_sales', JSON.stringify(salesHistory)); closeModal('zReportModal'); alert("Journée clôturée !"); } } function saveConfig() { storeConfig.name = document.getElementById('confName').value; storeConfig.addr = document.getElementById('confAddr').value; storeConfig.tax = document.getElementById('confTax').value; localStorage.setItem('bw_config', JSON.stringify(storeConfig)); updateTicketHeader(); closeModal('storeModal'); } /* --- MODALES HELPER --- */ function openModal(id) { document.getElementById(id).style.display = "block"; } function closeModal(id) { document.getElementById(id).style.display = "none"; } </script>Metadata
Metadata
Assignees
Labels
No labels