Skip to content

Nicolas.html #604

@zakk1976

Description

@zakk1976
<title>Caisse Tactile & Scanner</title> <script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
<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 !
***
🖨️ Imprimer le ticket
<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>
×

Gestion Stock & Codes-Barres

TVA 21% TVA 6% TVA 0% Ajouter / Mettre à jour
CodeNomPrixStockAction
×

Rapport Z (Fin de journée)

CLÔTURER LA JOURNÉE
×

Configuration Magasin

Nom: Adresse: TVA: Sauvegarder

<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 = `

CA 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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions