POS開発 UIテンプレート集 — POS固有
POS画面に特化したコピペテンプレート
各テンプレートは HTML・CSS・JavaScript の3ブロックで構成されています。
使いたいテンプレートの各ブロックをコピーし、プロジェクトの該当ファイルに貼り付けてください。
クラス名やIDは必要に応じて変更してください。
8. 商品カードグリッド
使い方: 商品データをグリッド表示するテンプレートです。
各カードに
data-id と data-price を設定し、「カートに追加」ボタンで商品情報を取得します。
グリッドのカラム数は minmax() の値で調整可能。画像は実際のパスに差し替えてください。
HTML
<!-- 商品カードグリッド --> <div class="product-grid"> <div class="product-card" data-id="001" data-price="350"> <div class="product-img">画像</div> <div class="product-info"> <p class="product-name">コーヒー(S)</p> <p class="product-price">¥350</p> </div> <button class="add-to-cart-btn">カートに追加</button> </div> <div class="product-card" data-id="002" data-price="500"> <div class="product-img">画像</div> <div class="product-info"> <p class="product-name">サンドイッチ</p> <p class="product-price">¥500</p> </div> <button class="add-to-cart-btn">カートに追加</button> </div> <!-- 商品カードを増やす場合はここにコピー --> </div>
CSS
/* 商品カードグリッド */ .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; } .product-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; transition: box-shadow 0.2s; } .product-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .product-img { height: 100px; background: #f5f5f5; display: flex; align-items: center; justify-content: center; color: #aaa; } .product-info { padding: 12px; } .product-name { font-weight: bold; font-size: 14px; margin-bottom: 4px; } .product-price { color: #e94560; font-weight: bold; } .add-to-cart-btn { width: 100%; padding: 10px; background: #e94560; color: #fff; border: none; cursor: pointer; font-size: 13px; transition: background 0.2s; } .add-to-cart-btn:hover { background: #c73a52; }
JavaScript
// 商品カード — カートに追加(イベント委譲) document.querySelector('.product-grid').addEventListener('click', (e) => { const btn = e.target.closest('.add-to-cart-btn'); if (!btn) return; const card = btn.closest('.product-card'); const item = { id: card.dataset.id, name: card.querySelector('.product-name').textContent, price: Number(card.dataset.price), quantity: 1 }; console.log('カートに追加:', item); // ここでカート配列に追加する処理を書く // 例: cart.push(item); renderCart(); });
// React: products を props で受け取り、onAdd で親に通知 function ProductGrid({ products, onAdd }) { return ( <div className="product-grid"> {products.map((p) => ( <div key={p.id} className="product-card"> <div className="product-img">画像</div> <div className="product-info"> <p className="product-name">{p.name}</p> <p className="product-price">¥{p.price.toLocaleString()}</p> </div> {/* クロージャで商品オブジェクトを直接渡せる */} <button className="add-to-cart-btn" onClick={() => onAdd({ ...p, quantity: 1 })} > カートに追加 </button> </div> ))} </div> ); } // 使い方 const products = [ { id: '001', name: 'コーヒー(S)', price: 350 }, { id: '002', name: 'サンドイッチ', price: 500 }, ]; <ProductGrid products={products} onAdd={(item) => console.log('追加:', item)} />
9. ショッピングカート
使い方: カート内の商品を一覧表示し、数量変更・削除・合計計算を行うテンプレートです。
cart 配列にデータを追加して renderCart() を呼ぶと画面が更新されます。
税率は TAX_RATE 定数で変更可能。商品カードグリッドと組み合わせて使ってください。
HTML
<!-- ショッピングカート --> <div class="cart"> <h3>カート</h3> <table class="cart-table"> <thead> <tr> <th>商品</th><th>単価</th><th>数量</th><th>小計</th><th></th> </tr> </thead> <tbody id="cart-body"> <!-- JSで動的に描画 --> </tbody> </table> <div class="cart-summary"> <p>小計: <span id="cart-subtotal">¥0</span></p> <p>消費税: <span id="cart-tax">¥0</span></p> <p class="cart-total">合計: <span id="cart-total">¥0</span></p> </div> </div>
CSS
/* ショッピングカート */ .cart-table { width: 100%; border-collapse: collapse; font-size: 14px; } .cart-table th { background: #f5f5f5; padding: 10px; text-align: left; font-size: 13px; color: #666; } .cart-table td { padding: 10px; border-bottom: 1px solid #eee; } .qty-control { display: flex; align-items: center; gap: 6px; } .qty-btn { width: 28px; height: 28px; border: 1px solid #ddd; border-radius: 50%; background: #fff; cursor: pointer; font-size: 16px; line-height: 1; } .qty-btn:hover { background: #f0f0f0; } .remove-btn { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 18px; } .cart-summary { text-align: right; padding: 16px 10px; font-size: 14px; } .cart-total { font-size: 20px; font-weight: bold; color: #e94560; }
JavaScript
// ショッピングカート const TAX_RATE = 0.10; // 消費税率 10% let cart = []; function addToCart(id, name, price) { const existing = cart.find(item => item.id === id); if (existing) { existing.quantity++; } else { cart.push({ id, name, price, quantity: 1 }); } renderCart(); } function updateQuantity(id, delta) { const item = cart.find(i => i.id === id); if (!item) return; item.quantity += delta; if (item.quantity < 1) cart = cart.filter(i => i.id !== id); renderCart(); } function removeFromCart(id) { cart = cart.filter(i => i.id !== id); renderCart(); } function renderCart() { const tbody = document.getElementById('cart-body'); let subtotal = 0; tbody.innerHTML = cart.map(item => { const itemTotal = item.price * item.quantity; subtotal += itemTotal; return `<tr> <td>${item.name}</td> <td>¥${item.price.toLocaleString()}</td> <td> <div class="qty-control"> <button class="qty-btn" onclick="updateQuantity('${item.id}', -1)">-</button> <span>${item.quantity}</span> <button class="qty-btn" onclick="updateQuantity('${item.id}', 1)">+</button> </div> </td> <td>¥${itemTotal.toLocaleString()}</td> <td><button class="remove-btn" onclick="removeFromCart('${item.id}')">×</button></td> </tr>`; }).join(''); const tax = Math.floor(subtotal * TAX_RATE); document.getElementById('cart-subtotal').textContent = `¥${subtotal.toLocaleString()}`; document.getElementById('cart-tax').textContent = `¥${tax.toLocaleString()}`; document.getElementById('cart-total').textContent = `¥${(subtotal + tax).toLocaleString()}`; }
// React: useReducer でカート操作を集約。render は state から自動導出 const TAX_RATE = 0.10; function cartReducer(cart, action) { switch (action.type) { case 'ADD': { const existing = cart.find((i) => i.id === action.item.id); return existing ? cart.map((i) => i.id === action.item.id ? { ...i, quantity: i.quantity + 1 } : i) : [...cart, { ...action.item, quantity: 1 }]; } case 'UPDATE_QTY': return cart .map((i) => i.id === action.id ? { ...i, quantity: i.quantity + action.delta } : i) .filter((i) => i.quantity > 0); case 'REMOVE': return cart.filter((i) => i.id !== action.id); default: return cart; } } function Cart() { const [cart, dispatch] = useReducer(cartReducer, []); // 集計は毎回導出(useMemo でキャッシュ可) const subtotal = cart.reduce((s, i) => s + i.price * i.quantity, 0); const tax = Math.floor(subtotal * TAX_RATE); const total = subtotal + tax; return ( <div className="cart"> <h3>カート</h3> <table className="cart-table"> <tbody> {cart.map((item) => ( <tr key={item.id}> <td>{item.name}</td> <td>¥{item.price.toLocaleString()}</td> <td> <div className="qty-control"> <button className="qty-btn" onClick={() => dispatch({ type: 'UPDATE_QTY', id: item.id, delta: -1 })}>−</button> <span>{item.quantity}</span> <button className="qty-btn" onClick={() => dispatch({ type: 'UPDATE_QTY', id: item.id, delta: +1 })}>+</button> </div> </td> <td>¥{(item.price * item.quantity).toLocaleString()}</td> <td> <button className="remove-btn" onClick={() => dispatch({ type: 'REMOVE', id: item.id })}>×</button> </td> </tr> ))} </tbody> </table> <div className="cart-summary"> <p>小計: ¥{subtotal.toLocaleString()}</p> <p>消費税: ¥{tax.toLocaleString()}</p> <p className="cart-total">合計: ¥{total.toLocaleString()}</p> </div> </div> ); }
10. テンキー入力
使い方: タッチスクリーン対応の数値入力パッドです。
ボタンサイズは最低60pxで、タッチ操作に最適化されています。
POS画面では「金額入力」「数量入力」「会員番号入力」に使えます。
onNumpadEnter コールバックで確定時の処理を実装してください。
HTML
<!-- テンキー入力 --> <div class="numpad"> <div class="numpad-display" id="numpadDisplay">0</div> <div class="numpad-keys"> <button class="numpad-btn" data-num="7">7</button> <button class="numpad-btn" data-num="8">8</button> <button class="numpad-btn" data-num="9">9</button> <button class="numpad-btn" data-num="4">4</button> <button class="numpad-btn" data-num="5">5</button> <button class="numpad-btn" data-num="6">6</button> <button class="numpad-btn" data-num="1">1</button> <button class="numpad-btn" data-num="2">2</button> <button class="numpad-btn" data-num="3">3</button> <button class="numpad-btn" data-num="00">00</button> <button class="numpad-btn" data-num="0">0</button> <button class="numpad-btn numpad-backspace" data-action="backspace">⌫</button> <button class="numpad-btn numpad-clear" data-action="clear">C</button> <button class="numpad-btn numpad-enter" data-action="enter" style="grid-column: span 2">確定</button> </div> </div>
CSS
/* テンキー */ .numpad { max-width: 280px; background: #f9f9f9; border-radius: 8px; padding: 16px; } .numpad-display { background: #1e1e2e; color: #cdd6f4; font-size: 28px; text-align: right; padding: 12px 16px; border-radius: 6px; margin-bottom: 12px; font-family: "Cascadia Code", monospace; } .numpad-keys { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .numpad-btn { padding: 16px; font-size: 20px; border: 1px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; min-height: 60px; transition: background 0.1s; user-select: none; -webkit-tap-highlight-color: transparent; } .numpad-btn:active { background: #e0e0e0; } .numpad-clear { color: #e74c3c; } .numpad-enter { background: #e94560; color: #fff; border-color: #e94560; } .numpad-enter:active { background: #c73a52; }
JavaScript
// テンキー入力 let numpadValue = '0'; const numpadDisplay = document.getElementById('numpadDisplay'); function updateNumpadDisplay() { const num = parseInt(numpadValue) || 0; numpadDisplay.textContent = num.toLocaleString(); } // 確定時のコールバック(用途に応じて書き換え) function onNumpadEnter(value) { console.log('入力確定:', value); } document.querySelector('.numpad-keys').addEventListener('click', (e) => { const btn = e.target.closest('.numpad-btn'); if (!btn) return; const num = btn.dataset.num; const action = btn.dataset.action; if (num) { numpadValue = numpadValue === '0' ? num : numpadValue + num; } else if (action === 'backspace') { numpadValue = numpadValue.slice(0, -1) || '0'; } else if (action === 'clear') { numpadValue = '0'; } else if (action === 'enter') { onNumpadEnter(parseInt(numpadValue) || 0); numpadValue = '0'; } updateNumpadDisplay(); });
// React: 入力値は state で保持。onEnter コールバックで親に確定値を渡す function NumPad({ onEnter }) { const [value, setValue] = useState('0'); const press = (key) => { if (key === 'clear') return setValue('0'); if (key === 'backspace') return setValue((v) => v.slice(0, -1) || '0'); if (key === 'enter') { onEnter(parseInt(value) || 0); return setValue('0'); } setValue((v) => v === '0' ? key : v + key); }; const display = (parseInt(value) || 0).toLocaleString(); return ( <div className="numpad"> <div className="numpad-display">{display}</div> <div className="numpad-keys"> {['7','8','9','4','5','6','1','2','3','00','0'].map((n) => ( <button key={n} className="numpad-btn" onClick={() => press(n)}>{n}</button> ))} <button className="numpad-btn numpad-backspace" onClick={() => press('backspace')}>⌫</button> <button className="numpad-btn numpad-clear" onClick={() => press('clear')}>C</button> <button className="numpad-btn numpad-enter" style={{ gridColumn: 'span 2' }} onClick={() => press('enter')}>確定</button> </div> </div> ); } // 使い方: <NumPad onEnter={(v) => console.log('確定:', v)} />
11. レシート表示
使い方: レシート風のレイアウトで注文内容を表示します。
generateReceipt(order) にオーダーデータを渡すとレシートを生成します。
window.print() で印刷可能。印刷時はレシート部分のみが出力されるCSS付き。
感熱紙サイズ(幅80mm)に合わせた max-width: 320px になっています。
HTML
<!-- レシート表示 --> <div class="receipt" id="receipt"> <div class="receipt-header"> <h3>SHOP NAME</h3> <p>〒000-0000 東京都...</p> <p>TEL: 03-0000-0000</p> </div> <div class="receipt-divider"></div> <p class="receipt-date" id="receipt-date"></p> <div class="receipt-divider"></div> <div id="receipt-items"> <!-- JSで動的に描画 --> </div> <div class="receipt-divider"></div> <div class="receipt-totals" id="receipt-totals"></div> <div class="receipt-divider"></div> <div class="receipt-footer"> <p>ありがとうございました</p> </div> </div> <button onclick="window.print()" class="print-btn">レシートを印刷</button>
CSS
/* レシート表示 */ .receipt { max-width: 320px; margin: 0 auto; background: #fff; padding: 20px; font-family: "Cascadia Code", monospace; font-size: 13px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); } .receipt-header { text-align: center; } .receipt-header h3 { font-size: 18px; margin-bottom: 4px; } .receipt-header p { font-size: 11px; color: #666; } .receipt-divider { border-top: 1px dashed #aaa; margin: 10px 0; } .receipt-date { font-size: 12px; color: #666; } .receipt-item { display: flex; justify-content: space-between; margin-bottom: 4px; } .receipt-totals .receipt-item { font-size: 13px; } .receipt-totals .total-line { font-size: 18px; font-weight: bold; margin-top: 4px; } .receipt-footer { text-align: center; font-size: 12px; } .print-btn { display: block; margin: 16px auto; padding: 8px 24px; background: #333; color: #fff; border: none; border-radius: 4px; cursor: pointer; } /* 印刷時はレシートだけ表示 */ @media print { body * { visibility: hidden; } .receipt, .receipt * { visibility: visible; } .receipt { position: absolute; top: 0; left: 0; box-shadow: none; } .print-btn { display: none; } }
JavaScript
// レシート生成 function generateReceipt(order) { // order = { items: [{name, price, quantity}], taxRate: 0.10 } const now = new Date(); document.getElementById('receipt-date').textContent = now.toLocaleString('ja-JP'); // 商品リスト const itemsEl = document.getElementById('receipt-items'); let subtotal = 0; itemsEl.innerHTML = order.items.map(item => { const total = item.price * item.quantity; subtotal += total; return `<div class="receipt-item"> <span>${item.name} x${item.quantity}</span> <span>¥${total.toLocaleString()}</span> </div>`; }).join(''); // 合計 const tax = Math.floor(subtotal * (order.taxRate || 0.10)); document.getElementById('receipt-totals').innerHTML = ` <div class="receipt-item"><span>小計</span><span>¥${subtotal.toLocaleString()}</span></div> <div class="receipt-item"><span>消費税</span><span>¥${tax.toLocaleString()}</span></div> <div class="receipt-item total-line"><span>合計</span><span>¥${(subtotal + tax).toLocaleString()}</span></div> `; } // 使用例: // generateReceipt({ // items: [ // { name: 'コーヒー', price: 350, quantity: 2 }, // { name: 'サンドイッチ', price: 500, quantity: 1 } // ], // taxRate: 0.10 // });
// React: order を props で受け取るだけ。innerHTML 構築は不要 function Receipt({ order }) { const subtotal = order.items.reduce( (s, i) => s + i.price * i.quantity, 0 ); const tax = Math.floor(subtotal * (order.taxRate ?? 0.10)); const total = subtotal + tax; const date = new Date().toLocaleString('ja-JP'); return ( <> <div className="receipt"> <div className="receipt-header"> <h3>SHOP NAME</h3> <p>〒000-0000 東京都...</p> <p>TEL: 03-0000-0000</p> </div> <div className="receipt-divider"/> <p className="receipt-date">{date}</p> <div className="receipt-divider"/> <div> {order.items.map((item, i) => ( <div key={i} className="receipt-item"> <span>{item.name} x{item.quantity}</span> <span>¥{(item.price * item.quantity).toLocaleString()}</span> </div> ))} </div> <div className="receipt-divider"/> <div className="receipt-totals"> <div className="receipt-item"><span>小計</span><span>¥{subtotal.toLocaleString()}</span></div> <div className="receipt-item"><span>消費税</span><span>¥{tax.toLocaleString()}</span></div> <div className="receipt-item total-line"><span>合計</span><span>¥{total.toLocaleString()}</span></div> </div> </div> <button className="print-btn" onClick={() => window.print()}> レシートを印刷 </button> </> ); }
12. バーコード入力
使い方: バーコードスキャナーからの高速入力を検知するテンプレートです。
スキャナーは高速にキーストロークを送信し最後にEnterを押します(通常50ms以内)。
手動入力とスキャナー入力を
SCAN_THRESHOLD(ms)で判別します。
onBarcodeScanned コールバックで商品検索処理を実装してください。
HTML
<!-- バーコード入力 --> <div class="barcode-input-wrapper"> <label for="barcodeInput">バーコード</label> <div class="barcode-field"> <span class="barcode-icon">☷</span> <input type="text" id="barcodeInput" placeholder="スキャンまたは入力..." autocomplete="off" autofocus> <span class="barcode-status" id="barcodeStatus"></span> </div> </div>
CSS
/* バーコード入力 */ .barcode-input-wrapper { max-width: 400px; } .barcode-input-wrapper label { display: block; font-size: 13px; color: #666; margin-bottom: 4px; } .barcode-field { display: flex; align-items: center; border: 2px solid #ddd; border-radius: 6px; padding: 0 12px; transition: border-color 0.2s; } .barcode-field:focus-within { border-color: #e94560; } .barcode-icon { font-size: 20px; color: #999; margin-right: 8px; } .barcode-field input { flex: 1; border: none; outline: none; padding: 12px 0; font-size: 16px; font-family: "Cascadia Code", monospace; } .barcode-status { font-size: 12px; padding: 2px 8px; border-radius: 10px; } .barcode-status.scanned { background: #2ecc71; color: #fff; }
JavaScript
// バーコードスキャナー入力検知 const SCAN_THRESHOLD = 50; // ms — この時間内の入力はスキャナーと判定 let lastKeyTime = 0; let isScanning = false; const barcodeInput = document.getElementById('barcodeInput'); const barcodeStatus = document.getElementById('barcodeStatus'); // スキャン完了時のコールバック function onBarcodeScanned(code) { console.log('スキャン:', code); barcodeStatus.textContent = 'スキャン完了'; barcodeStatus.className = 'barcode-status scanned'; setTimeout(() => { barcodeStatus.textContent = ''; barcodeStatus.className = 'barcode-status'; }, 2000); // ここで商品検索処理を実装 } barcodeInput.addEventListener('keydown', (e) => { const now = Date.now(); if (now - lastKeyTime < SCAN_THRESHOLD) isScanning = true; lastKeyTime = now; if (e.key === 'Enter') { e.preventDefault(); const code = barcodeInput.value.trim(); if (code) { onBarcodeScanned(code); barcodeInput.value = ''; } isScanning = false; } }); // ページ読み込み時に自動フォーカス barcodeInput.focus();
// React: 入力値 / ステータスを state、useRef + useEffect で autofocus function BarcodeInput({ onScan }) { const [value, setValue] = useState(''); const [scanned, setScanned] = useState(false); const inputRef = useRef(null); // マウント時に自動フォーカス useEffect(() => { inputRef.current?.focus(); }, []); // スキャン完了表示を 2 秒で消す useEffect(() => { if (!scanned) return; const t = setTimeout(() => setScanned(false), 2000); return () => clearTimeout(t); }, [scanned]); const handleKeyDown = (e) => { if (e.key !== 'Enter') return; e.preventDefault(); const code = value.trim(); if (!code) return; onScan(code); setValue(''); setScanned(true); }; return ( <div className="barcode-input-wrapper"> <label>バーコード</label> <div className="barcode-field"> <span className="barcode-icon">⚏</span> <input ref={inputRef} type="text" value={value} onChange={(e) => setValue(e.target.value)} onKeyDown={handleKeyDown} placeholder="スキャンまたは入力..." autoComplete="off" /> <span className={`barcode-status ${scanned ? 'scanned' : ''}`}> {scanned ? 'スキャン完了' : ''} </span> </div> </div> ); }
13. 会計画面レイアウト
使い方: POS会計画面の2カラムレイアウトです。
左側に商品リスト、右側に合計と支払い方法を配置します。
640px以下で1カラムに切り替わるレスポンシブ対応。
商品カード・カート・テンキーテンプレートと組み合わせて使えます。
HTML
<!-- 会計画面レイアウト --> <div class="checkout-layout"> <!-- 左: 商品リスト --> <div class="checkout-items"> <h3>注文内容</h3> <table class="checkout-table"> <thead> <tr><th>商品</th><th>数量</th><th>小計</th></tr> </thead> <tbody id="checkout-body"> <tr><td>コーヒー(S)</td><td>2</td><td>¥700</td></tr> <tr><td>サンドイッチ</td><td>1</td><td>¥500</td></tr> </tbody> </table> </div> <!-- 右: 会計サマリー --> <div class="checkout-summary"> <div class="checkout-totals"> <div class="checkout-row"><span>小計</span><span>¥1,200</span></div> <div class="checkout-row"><span>消費税(10%)</span><span>¥120</span></div> <div class="checkout-row total"><span>合計</span><span>¥1,320</span></div> </div> <div class="payment-methods"> <p>支払い方法</p> <div class="payment-buttons"> <button class="payment-btn active" data-method="cash">現金</button> <button class="payment-btn" data-method="card">カード</button> <button class="payment-btn" data-method="epay">電子マネー</button> </div> </div> <button class="checkout-btn">会計する</button> </div> </div>
CSS
/* 会計画面レイアウト */ .checkout-layout { display: grid; grid-template-columns: 1fr 360px; gap: 24px; align-items: start; } .checkout-items { background: #fff; border-radius: 8px; padding: 20px; } .checkout-table { width: 100%; border-collapse: collapse; margin-top: 12px; } .checkout-table th, .checkout-table td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; font-size: 14px; } .checkout-table th { color: #888; font-size: 13px; } .checkout-summary { background: #fff; border-radius: 8px; padding: 20px; position: sticky; top: 70px; } .checkout-row { display: flex; justify-content: space-between; padding: 8px 0; font-size: 14px; } .checkout-row.total { font-size: 24px; font-weight: bold; color: #e94560; border-top: 2px solid #333; margin-top: 8px; padding-top: 12px; } .payment-methods { margin: 20px 0; } .payment-methods p { font-size: 13px; color: #666; margin-bottom: 8px; } .payment-buttons { display: flex; gap: 8px; } .payment-btn { flex: 1; padding: 10px; border: 2px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; font-size: 13px; transition: border-color 0.2s; } .payment-btn.active { border-color: #e94560; color: #e94560; font-weight: bold; } .checkout-btn { width: 100%; padding: 16px; background: #e94560; color: #fff; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: background 0.2s; } .checkout-btn:hover { background: #c73a52; } @media (max-width: 640px) { .checkout-layout { grid-template-columns: 1fr; } .checkout-summary { position: static; } }
JavaScript
// 支払い方法の切り替え document.querySelectorAll('.payment-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.payment-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); }); }); // 会計ボタン document.querySelector('.checkout-btn').addEventListener('click', () => { const method = document.querySelector('.payment-btn.active').dataset.method; console.log('会計処理:', method); // ここに会計処理を実装 // 例: レシート生成、在庫更新、履歴保存など });
// React: 会計画面は上記コンポーネント (Cart / NumPad / Receipt) を組み合わせる // 支払い方法は state、合計は props から導出 const TAX_RATE = 0.10; function Checkout({ items, onSubmit }) { const [method, setMethod] = useState('cash'); const subtotal = items.reduce((s, i) => s + i.price * i.quantity, 0); const tax = Math.floor(subtotal * TAX_RATE); const total = subtotal + tax; const methods = [ { id: 'cash', label: '現金' }, { id: 'card', label: 'カード' }, { id: 'epay', label: '電子マネー' }, ]; return ( <div className="checkout-layout"> <div className="checkout-items"> <h3>注文内容</h3> <table className="checkout-table"> <thead> <tr><th>商品</th><th>数量</th><th>小計</th></tr> </thead> <tbody> {items.map((item) => ( <tr key={item.id}> <td>{item.name}</td> <td>{item.quantity}</td> <td>¥{(item.price * item.quantity).toLocaleString()}</td> </tr> ))} </tbody> </table> </div> <div className="checkout-summary"> <div className="checkout-totals"> <div className="checkout-row"><span>小計</span><span>¥{subtotal.toLocaleString()}</span></div> <div className="checkout-row"><span>消費税(10%)</span><span>¥{tax.toLocaleString()}</span></div> <div className="checkout-row total"><span>合計</span><span>¥{total.toLocaleString()}</span></div> </div> <div className="payment-methods"> <p>支払い方法</p> <div className="payment-buttons"> {methods.map((m) => ( <button key={m.id} className={`payment-btn ${method === m.id ? 'active' : ''}`} onClick={() => setMethod(m.id)} > {m.label} </button> ))} </div> </div> <button className="checkout-btn" onClick={() => onSubmit({ method, total })}> 会計する </button> </div> </div> ); }