小さなお店用のPOSシステム「MisePOS」

【更新履歴】

 ・2026/2/20 バージョン1.0公開。
 ・2026/2/21 バージョン1.1公開。

画像
レジ画面(商品選択)
画像
決済リスト画面(簡単に削除)
画像
商品の登録画面
画像
レシート読み取り画面

・小規模な小売店向けの売上入力システムです。
・出力された売上データのcsvファイルは、
 分析ツールの「Mise」でも使えます。

・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>MISE POS — レジ入力</title>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&family=M+PLUS+Rounded+1c:wght@400;500;700;800&display=swap" rel="stylesheet">
<style>
:root {
  --bg:     #fff8f2;
  --bg2:    #fff2e6;
  --surface:#ffffff;
  --peach:  #ff8c69;
  --peach-l:#ffe4d9;
  --coral:  #ff6b6b;
  --mint:   #6bcb8b;
  --mint-l: #d9f5e4;
  --sky:    #5bb8f5;
  --sky-l:  #d9f0ff;
  --lemon:  #ffd166;
  --lemon-l:#fff5cc;
  --purple: #b388f7;
  --purple-l:#ede0ff;
  --gray:   #aaa09a;
  --text:   #3d2c22;
  --text2:  #8a7a70;
  --border: #f0d8c8;
  --shadow: 0 4px 18px rgba(255,100,50,.10);
  --r: 20px;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}

body {
  background: var(--bg);
  color: var(--text);
  font-family: 'M PLUS Rounded 1c', sans-serif;
  font-size: 15px;
  height: 100vh;
  margin: 0;
  padding: 0;
  overflow: hidden;
  user-select: none;
  display: flex;
  flex-direction: column;
}

/* ─── Scanline noise overlay ─── */
body::before{content:'';position:fixed;inset:0;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Crect width='1' height='1' fill='%23ff8c69' opacity='.03'/%3E%3C/svg%3E");pointer-events:none;z-index:999;}

/* ─── Layout ─── */
.pos-wrap {
  display: grid;
  grid-template-columns: 1fr 560px;
  flex: 1;
  overflow: hidden;
}
.left{display:flex;flex-direction:column;overflow:hidden;}
.right{background:var(--surface);border-left:2px solid var(--border);display:flex;flex-direction:column;overflow:hidden;}

/* ─── 右パネル内部の2カラム ─── */
.right-body{display:grid;grid-template-columns:1fr 210px;flex:1;overflow:hidden;min-height:0;}
.right-items-col{display:flex;flex-direction:column;overflow:hidden;border-right:2px solid var(--border);}
.right-info-col{display:flex;flex-direction:column;overflow-y:auto;background:var(--bg2);}

/* ─── Header ─── */
header{background:linear-gradient(90deg,var(--peach) 0%,var(--coral) 100%);color:#fff;padding:12px 20px;display:flex;align-items:center;gap:14px;flex-shrink:0;box-shadow:0 3px 14px rgba(255,100,50,.25);}
.logo{font-family:'Nunito',sans-serif;font-size:24px;font-weight:900;letter-spacing:.04em;}
.logo span{opacity:.7;font-size:14px;font-weight:700;margin-left:6px;}
.hdr-datetime{margin-left:auto;display:flex;flex-direction:column;align-items:flex-end;background:rgba(255,255,255,.18);padding:5px 14px;border-radius:12px;gap:0;}
.hdr-date{font-family:'M PLUS Rounded 1c',sans-serif;font-size:12px;font-weight:700;color:rgba(255,255,255,.85);line-height:1.3;}
.hdr-clock{font-family:'Nunito',monospace;font-size:22px;font-weight:800;color:#fff;line-height:1.2;}
.hdr-icons{display:flex;gap:8px;}
.hdr-btn{background:rgba(255,255,255,.2);border:none;color:#fff;border-radius:10px;padding:6px 12px;font-size:13px;font-family:inherit;font-weight:700;cursor:pointer;transition:all .15s;}
.hdr-btn:hover{background:rgba(255,255,255,.35);}

/* ─── Category tabs ─── */
.cat-bar{display:flex;gap:8px;padding:12px 16px;overflow-x:auto;background:var(--bg2);border-bottom:2px solid var(--border);flex-shrink:0;}
.cat-bar::-webkit-scrollbar{height:3px;}
.cat-bar::-webkit-scrollbar-thumb{background:var(--peach-l);border-radius:4px;}
.cat-tab{border:2px solid transparent;background:var(--surface);color:var(--text2);border-radius:14px;padding:8px 16px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;transition:all .18s;font-family:inherit;}
.cat-tab.active{background:var(--peach);color:#fff;border-color:var(--coral);box-shadow:0 3px 10px rgba(255,100,50,.3);}
.cat-tab:hover:not(.active){border-color:var(--peach);color:var(--peach);}

/* ─── Products grid ─── */
.products-area{flex:1;overflow-y:auto;padding:14px 16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:12px;align-content:start;}
.products-area::-webkit-scrollbar{width:5px;}
.products-area::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px;}

.prod-card{background:var(--surface);border:2.5px solid var(--border);border-radius:var(--r);padding:14px 10px 12px;text-align:center;cursor:pointer;transition:all .15s;position:relative;overflow:hidden;}
.prod-card:hover{transform:translateY(-3px);box-shadow:var(--shadow);}
.prod-card:active{transform:scale(.95);}
.prod-card.sold-out{opacity:.4;pointer-events:none;}
.prod-card.sold-out::after{content:'売り切れ';position:absolute;inset:0;background:rgba(255,255,255,.85);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;color:var(--coral);border-radius:var(--r);}
.prod-icon{font-size:40px;line-height:1;margin-bottom:6px;display:block;}
.prod-img{width:56px;height:56px;object-fit:cover;border-radius:12px;margin-bottom:6px;}
.prod-name{font-size:13px;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:4px;}
.prod-price{font-family:'Nunito',sans-serif;font-size:16px;font-weight:900;color:var(--peach);}
.prod-price small{font-size:11px;color:var(--text2);font-weight:600;}
.prod-badge{position:absolute;top:6px;right:6px;background:var(--coral);color:#fff;font-size:9px;font-weight:800;padding:2px 6px;border-radius:8px;}

/* ─── 右サイド: レシート ─── */
.receipt-header{padding:10px 14px 8px;border-bottom:2px solid var(--border);flex-shrink:0;}
.receipt-header h2{font-size:15px;font-weight:800;color:var(--text);margin-bottom:0;}

/* 顧客情報(右カラム用) */
.customer-bar{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:6px;}
.cust-label{font-size:11px;color:var(--text2);font-weight:700;width:100%;margin-top:6px;}
.cust-btn{border:2px solid var(--border);background:var(--surface);border-radius:10px;padding:5px 7px;font-size:12px;font-weight:700;cursor:pointer;transition:all .15s;font-family:inherit;display:flex;align-items:center;gap:3px;}
.cust-btn.active{border-color:currentColor;background:var(--lemon-l);}
.cust-btn:hover:not(.active){border-color:var(--lemon);background:var(--lemon-l);}
.age-bar{display:flex;gap:4px;flex-wrap:wrap;}
.age-btn{border:2px solid var(--border);background:var(--surface);border-radius:8px;padding:4px 5px;font-size:11px;font-weight:700;cursor:pointer;font-family:inherit;text-align:center;transition:all .15s;flex:1;min-width:calc(33% - 4px);}
.age-btn.active{background:var(--sky-l);border-color:var(--sky);color:var(--sky);}
.age-btn:hover:not(.active){border-color:var(--sky);background:var(--sky-l);}

/* レシート items */
.receipt-items{flex:1;overflow-y:scroll;padding:8px 10px;min-height:0;}
.receipt-items::-webkit-scrollbar{width:6px;}
.receipt-items::-webkit-scrollbar-track{background:var(--bg2);border-radius:4px;}
.receipt-items::-webkit-scrollbar-thumb{background:var(--peach);border-radius:4px;}
.receipt-items::-webkit-scrollbar-thumb:hover{background:var(--coral);}
.empty-msg{text-align:center;color:var(--text2);padding:30px 0;font-size:14px;font-weight:700;}
.empty-msg span{font-size:40px;display:block;margin-bottom:8px;}

.receipt-item{display:flex;align-items:center;gap:8px;padding:9px 10px;background:var(--bg);border-radius:14px;margin-bottom:7px;border:2px solid transparent;transition:all .15s;}
.receipt-item:hover{border-color:var(--border);}
.ri-icon{font-size:22px;flex-shrink:0;}
.ri-name{flex:1;font-size:13px;font-weight:700;line-height:1.2;}
.ri-name small{font-size:11px;color:var(--text2);font-weight:600;}
.ri-qty{display:flex;align-items:center;gap:4px;}
.ri-qbtn{width:28px;height:28px;border-radius:10px;border:2px solid var(--border);background:var(--surface);color:var(--text);font-size:16px;font-weight:900;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;transition:all .15s;font-family:'Nunito',sans-serif;}
.ri-qbtn.minus:hover{background:var(--coral);border-color:var(--coral);color:#fff;}
.ri-qbtn.plus:hover{background:var(--mint);border-color:var(--mint);color:#fff;}
.ri-qnum{font-family:'Nunito',sans-serif;font-size:18px;font-weight:900;min-width:26px;text-align:center;}
.ri-amt{font-family:'Nunito',sans-serif;font-size:15px;font-weight:800;color:var(--peach);white-space:nowrap;}
.ri-del{color:var(--text2);background:none;border:none;cursor:pointer;font-size:18px;transition:color .15s;padding:0 2px;}
.ri-del:hover{color:var(--coral);}
.ri-discount{font-size:11px;color:var(--mint);font-weight:700;}

/* 割引バー */
.discount-bar{padding:6px 10px;background:var(--mint-l);border-top:2px dashed var(--border);border-bottom:2px dashed var(--border);display:flex;flex-direction:column;gap:4px;flex-shrink:0;}
.disc-label{font-size:11px;font-weight:700;color:var(--text2);}
.disc-btns{display:flex;gap:4px;flex-wrap:wrap;}
.disc-btn{border:2px solid var(--mint);background:var(--surface);color:var(--mint);border-radius:8px;padding:3px 8px;font-size:11px;font-weight:800;cursor:pointer;font-family:inherit;transition:all .15s;}
.disc-btn.active{background:var(--mint);color:#fff;}
.disc-btn:hover{background:var(--mint);color:#fff;}

/* 合計 */
.total-area{padding:8px 10px;border-top:2px solid var(--border);flex-shrink:0;}
.total-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;font-size:12px;color:var(--text2);}
.total-row.grand{font-size:18px;font-weight:900;color:var(--text);font-family:'Nunito',sans-serif;margin-top:6px;}
.total-row.grand span:last-child{color:var(--peach);}
.tax-badge{font-size:10px;background:var(--lemon-l);border:1.5px solid var(--lemon);color:var(--text2);padding:1px 6px;border-radius:8px;font-weight:700;}

/* ボタン */
.action-btns{display:grid;grid-template-columns:1fr;gap:6px;padding:6px 10px 10px;flex-shrink:0;}
.act-btn{border:none;border-radius:14px;padding:12px 8px;font-size:14px;font-weight:800;cursor:pointer;font-family:inherit;transition:all .18s;display:flex;align-items:center;justify-content:center;gap:5px;}
.act-cancel{background:var(--peach-l);color:var(--coral);}
.act-cancel:hover{background:var(--coral);color:#fff;}
.act-pay{background:linear-gradient(135deg,var(--mint) 0%,#3db870 100%);color:#fff;box-shadow:0 4px 16px rgba(107,203,139,.35);}
.act-pay:hover{transform:translateY(-2px);box-shadow:0 6px 22px rgba(107,203,139,.45);}
.act-pay:active{transform:scale(.97);}
.act-pay:disabled{background:var(--border);color:var(--text2);box-shadow:none;transform:none;cursor:not-allowed;}

/* ─── Modal overlay ─── */
.modal{position:fixed;inset:0;background:rgba(60,40,30,.5);backdrop-filter:blur(4px);z-index:500;display:flex;align-items:center;justify-content:center;animation:fadeIn .2s;}
.modal.hidden{display:none;}
@keyframes fadeIn{from{opacity:0;}to{opacity:1;}}
.modal-box{background:var(--surface);border-radius:28px;padding:28px;max-width:480px;width:90%;box-shadow:0 16px 60px rgba(0,0,0,.2);animation:slideUp .25s;}
@keyframes slideUp{from{transform:translateY(30px);opacity:0;}to{transform:translateY(0);opacity:1;}}
.modal-title{font-size:20px;font-weight:800;margin-bottom:16px;text-align:center;}
.modal-body{font-size:15px;color:var(--text2);text-align:center;margin-bottom:20px;line-height:1.7;}
.modal-btns{display:flex;gap:10px;}
.modal-btns button{flex:1;border:none;border-radius:14px;padding:12px;font-size:15px;font-weight:800;cursor:pointer;font-family:inherit;transition:all .15s;}
.mbtn-cancel{background:var(--bg2);color:var(--text2);}
.mbtn-ok{background:linear-gradient(135deg,var(--peach),var(--coral));color:#fff;}
.mbtn-ok:hover{transform:translateY(-2px);}

/* ─── 決済完了画面 ─── */
.paid-screen{text-align:center;padding:20px;}
.paid-circle{width:80px;height:80px;background:linear-gradient(135deg,var(--mint),#3db870);border-radius:50%;margin:0 auto 16px;display:flex;align-items:center;justify-content:center;font-size:40px;box-shadow:0 6px 24px rgba(107,203,139,.4);animation:pop .4s cubic-bezier(.2,1.4,.4,1);}
@keyframes pop{from{transform:scale(.2);opacity:0;}to{transform:scale(1);opacity:1;}}
.paid-screen h2{font-size:22px;font-weight:900;margin-bottom:6px;}
.paid-amt{font-family:'Nunito',sans-serif;font-size:36px;font-weight:900;color:var(--mint);margin:10px 0;}
.paid-items{background:var(--bg);border-radius:14px;padding:12px 16px;text-align:left;margin:12px 0;font-size:13px;}
.paid-items div{display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px solid var(--border);}
.paid-items div:last-child{border-bottom:none;}

/* ─── 商品登録タブ ─── */
.product-mgmt{padding:16px;overflow-y:auto;flex:1;}
.product-mgmt::-webkit-scrollbar{width:6px;}
.product-mgmt::-webkit-scrollbar-track{background:var(--bg2);border-radius:4px;}
.product-mgmt::-webkit-scrollbar-thumb{background:var(--peach);border-radius:4px;}

.field-group{margin-bottom:14px;}
.field-group label{display:block;font-size:12px;font-weight:800;color:var(--text2);margin-bottom:5px;}
.f-input{width:100%;border:2px solid var(--border);border-radius:12px;padding:10px 14px;font-size:15px;font-family:inherit;font-weight:600;color:var(--text);background:var(--bg);outline:none;transition:border-color .15s;}
.f-input:focus{border-color:var(--peach);}
.f-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
.f-row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}

/* ─── レシートOCRパネル ─── */
.ocr-panel{padding:16px;overflow-y:auto;flex:1;}
.ocr-drop{border:3px dashed var(--border);border-radius:20px;padding:30px;text-align:center;cursor:pointer;transition:all .2s;background:var(--bg);}
.ocr-drop:hover,.ocr-drop.drag{border-color:var(--peach);background:var(--peach-l);}
.ocr-drop input{display:none;}
.ocr-queue{margin-top:14px;display:flex;flex-direction:column;gap:8px;max-height:300px;overflow-y:auto;}
.ocr-item{display:flex;align-items:center;gap:10px;background:var(--surface);border:2px solid var(--border);border-radius:14px;padding:10px 14px;}
.ocr-thumb{width:44px;height:44px;object-fit:cover;border-radius:10px;border:2px solid var(--border);}
.ocr-info{flex:1;}
.ocr-name{font-size:13px;font-weight:700;}
.ocr-status{font-size:11px;color:var(--text2);margin-top:2px;}
.ocr-status.processing{color:var(--sky);}
.ocr-status.done{color:var(--mint);}
.ocr-status.err{color:var(--coral);}

/* ─── 設定/サイドメニュー ─── */
.top-mode-bar{display:flex;gap:0;background:var(--bg2);border-bottom:2px solid var(--border);flex-shrink:0;}
.mode-tab{flex:1;padding:10px;text-align:center;font-size:12px;font-weight:800;color:var(--text2);cursor:pointer;border-bottom:3px solid transparent;transition:all .15s;font-family:inherit;background:none;border-left:none;border-right:none;border-top:none;}
.mode-tab.active{color:var(--peach);border-bottom-color:var(--peach);}

/* ─── 確認トースト ─── */
.toast{position:fixed;top:70px;left:50%;transform:translateX(-50%);background:var(--text);color:#fff;padding:10px 20px;border-radius:20px;font-size:14px;font-weight:700;z-index:600;opacity:0;transition:opacity .25s;pointer-events:none;white-space:nowrap;}
.toast.show{opacity:1;}

/* ─── 商品登録プレビュー ─── */
.prod-preview{display:flex;flex-wrap:wrap;gap:10px;margin-top:14px;}
.add-prod-btn{background:linear-gradient(135deg,var(--peach),var(--coral));color:#fff;border:none;border-radius:14px;padding:12px;font-size:14px;font-weight:800;cursor:pointer;width:100%;font-family:inherit;margin-top:10px;}

/* ─── 右サイドのタブパネル ─── */
.right-panel{display:none;flex-direction:column;height:100%;overflow:hidden;}
.right-panel.active{display:flex;}

/* ─── 特記事項入力 ─── */
.note-area{padding:0 10px 6px;flex-shrink:0;}
.note-input{width:100%;border:2px solid var(--border);border-radius:10px;padding:6px 10px;font-size:12px;font-family:inherit;color:var(--text);background:var(--surface);outline:none;resize:none;height:46px;}
.note-input:focus{border-color:var(--sky);}

/* ─── 決済リストパネル ─── */
.history-panel{padding:0;overflow:hidden;flex:1;display:flex;flex-direction:column;}
.history-toolbar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:var(--bg2);border-bottom:2px solid var(--border);flex-shrink:0;flex-wrap:wrap;}
.history-toolbar .ht-label{font-size:12px;font-weight:700;color:var(--text2);white-space:nowrap;}
.history-date-input{border:2px solid var(--border);border-radius:10px;padding:5px 10px;font-size:13px;font-family:inherit;font-weight:600;color:var(--text);background:var(--surface);outline:none;cursor:pointer;}
.history-date-input:focus{border-color:var(--peach);}
.history-toolbar .ht-btn{border:2px solid var(--border);background:var(--surface);border-radius:10px;padding:5px 10px;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit;color:var(--text2);white-space:nowrap;transition:all .15s;}
.history-toolbar .ht-btn:hover{border-color:var(--peach);color:var(--peach);}
.history-toolbar .ht-btn.danger{border-color:var(--coral);color:var(--coral);}
.history-toolbar .ht-btn.danger:hover{background:var(--coral);color:#fff;}
.history-toolbar .ht-btn.primary{border-color:var(--mint);color:var(--mint);}
.history-toolbar .ht-btn.primary:hover{background:var(--mint);color:#fff;}
.history-summary{padding:6px 14px;background:var(--lemon-l);border-bottom:2px solid var(--border);font-size:12px;font-weight:700;color:var(--text2);flex-shrink:0;display:flex;gap:14px;flex-wrap:wrap;}
.history-summary span{color:var(--text);}
.history-list{flex:1;overflow-y:scroll;padding:10px 12px;}
.history-list::-webkit-scrollbar{width:6px;}
.history-list::-webkit-scrollbar-track{background:var(--bg2);border-radius:4px;}
.history-list::-webkit-scrollbar-thumb{background:var(--peach);border-radius:4px;}
.history-list::-webkit-scrollbar-thumb:hover{background:var(--coral);}
.history-tx{background:var(--surface);border:2px solid var(--border);border-radius:14px;margin-bottom:8px;overflow:hidden;transition:all .15s;}
.history-tx.selected{border-color:var(--peach);background:var(--peach-l);}
.history-tx-header{display:flex;align-items:center;gap:8px;padding:9px 12px;cursor:pointer;}
.history-tx-header:hover{background:rgba(255,140,105,.06);}
.htx-check{width:20px;height:20px;border-radius:6px;border:2px solid var(--border);background:var(--bg);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:12px;transition:all .15s;}
.htx-check.checked{background:var(--peach);border-color:var(--coral);color:#fff;}
.htx-id{font-family:'Nunito',sans-serif;font-size:13px;font-weight:900;color:var(--peach);min-width:70px;}
.htx-time{font-size:12px;color:var(--text2);font-weight:700;}
.htx-cust{font-size:11px;color:var(--sky);font-weight:700;background:var(--sky-l);border-radius:8px;padding:2px 7px;}
.htx-total{font-family:'Nunito',sans-serif;font-size:16px;font-weight:900;color:var(--text);margin-left:auto;}
.htx-expand{font-size:16px;color:var(--text2);transition:transform .15s;flex-shrink:0;}
.htx-expand.open{transform:rotate(180deg);}
.history-tx-body{display:none;padding:0 12px 10px;border-top:1px solid var(--border);}
.history-tx-body.open{display:block;}
.htx-item{display:flex;justify-content:space-between;align-items:center;padding:4px 0;font-size:12px;border-bottom:1px dotted var(--border);}
.htx-item:last-child{border-bottom:none;}
.htx-item-name{color:var(--text);font-weight:700;}
.htx-item-detail{color:var(--text2);}
.htx-item-amt{font-family:'Nunito',sans-serif;font-weight:800;color:var(--peach);}
.htx-note{font-size:11px;color:var(--text2);margin-top:6px;padding:5px 8px;background:var(--bg2);border-radius:8px;}
.htx-disc{font-size:11px;color:var(--mint);font-weight:700;margin-top:4px;}
.history-empty{text-align:center;color:var(--text2);padding:40px 20px;font-size:14px;font-weight:700;}
.history-empty span{font-size:40px;display:block;margin-bottom:8px;}

/* ─── レスポンシブ ─── */
@media (max-width:900px){
  .pos-wrap{display:flex; flex-direction:column;}
  .right{height:55vh; border-left:none; border-top:2px solid var(--border); flex-shrink:0;}
  .left{flex:1;}
  .right-body{grid-template-columns:1fr 180px;}
}
@media (max-width:600px){
  .right-body{grid-template-columns:1fr;}
  .right-info-col{display:none;}
}
</style>
</head>
<body>

<header>
  <div class="logo">🛒 MISE POS<span>入力ツール</span></div>
  <div class="hdr-datetime" id="hdr-datetime">
    <div class="hdr-date" id="hdr-date">----年--月--日(-)</div>
    <div class="hdr-clock" id="clock">--:--</div>
  </div>
  <div class="hdr-icons">
    <button class="hdr-btn" onclick="openSettings()">⚙️ 設定</button>
    <button class="hdr-btn" onclick="exportCSV()">📤 CSV出力</button>
  </div>
</header>

<div class="pos-wrap">

  <div class="left">
    <div class="top-mode-bar">
      <button class="mode-tab active" onclick="setMode('pos',this)">🛒 レジ</button>
      <button class="mode-tab" onclick="setMode('history',this)">📋 決済リスト</button>
      <button class="mode-tab" onclick="setMode('products',this)">📦 商品登録</button>
      <button class="mode-tab" onclick="setMode('ocr',this)">📸 レシートOCR</button>
    </div>

    <div id="panel-pos" style="display:flex;flex-direction:column;flex:1;overflow:hidden;">
      <div class="cat-bar" id="cat-bar">
        <button class="cat-tab active" data-cat="all" onclick="filterCat('all',this)">🏷️ すべて</button>
      </div>
      <div class="products-area" id="products-grid"></div>
    </div>

    <div id="panel-products" style="display:none;flex:1;overflow:hidden;">
      <div class="product-mgmt" id="product-mgmt">
        <h3 style="font-size:16px;font-weight:800;margin-bottom:14px;">📦 商品を登録・編集</h3>
        <div class="f-row">
          <div class="field-group">
            <label>商品名</label>
            <input class="f-input" id="pm-name" placeholder="例: カフェラテ">
          </div>
          <div class="field-group">
            <label>カテゴリ</label>
            <input class="f-input" id="pm-cat" placeholder="例: ドリンク" list="cat-datalist">
            <datalist id="cat-datalist"></datalist>
          </div>
        </div>
        <div class="f-row">
          <div class="field-group">
            <label>価格(税抜)</label>
            <input class="f-input" type="number" id="pm-price" placeholder="500">
          </div>
          <div class="field-group">
            <label>消費税</label>
            <select class="f-input" id="pm-tax">
              <option value="10">10%(標準)</option>
              <option value="8">8%(軽減)</option>
              <option value="0">0%(非課税)</option>
            </select>
          </div>
        </div>
        <div class="f-row">
          <div class="field-group">
            <label>アイコン(絵文字)</label>
            <input class="f-input" id="pm-icon" placeholder="☕">
          </div>
          <div class="field-group">
            <label>商品ID</label>
            <input class="f-input" id="pm-id" placeholder="自動生成">
          </div>
        </div>
        <div class="field-group">
          <label>バーコード / SKU</label>
          <input class="f-input" id="pm-barcode" placeholder="省略可">
        </div>
        <div class="field-group">
          <label>色テーマ</label>
          <div style="display:flex;gap:8px;flex-wrap:wrap;" id="color-picker">
            <div class="color-swatch" data-color="#ffe4d9" style="width:32px;height:32px;background:#ffe4d9;border-radius:10px;border:3px solid transparent;cursor:pointer;" onclick="selectColor(this)"></div>
            <div class="color-swatch" data-color="#d9f5e4" style="width:32px;height:32px;background:#d9f5e4;border-radius:10px;border:3px solid transparent;cursor:pointer;" onclick="selectColor(this)"></div>
            <div class="color-swatch" data-color="#d9f0ff" style="width:32px;height:32px;background:#d9f0ff;border-radius:10px;border:3px solid transparent;cursor:pointer;" onclick="selectColor(this)"></div>
            <div class="color-swatch" data-color="#fff5cc" style="width:32px;height:32px;background:#fff5cc;border-radius:10px;border:3px solid transparent;cursor:pointer;" onclick="selectColor(this)"></div>
            <div class="color-swatch" data-color="#ede0ff" style="width:32px;height:32px;background:#ede0ff;border-radius:10px;border:3px solid transparent;cursor:pointer;" onclick="selectColor(this)"></div>
            <div class="color-swatch" data-color="#ffd9d9" style="width:32px;height:32px;background:#ffd9d9;border-radius:10px;border:3px solid transparent;cursor:pointer;" onclick="selectColor(this)"></div>
          </div>
        </div>
        <button class="add-prod-btn" onclick="saveProduct()">✅ 商品を保存</button>
        <div id="pm-feedback" style="text-align:center;margin-top:10px;font-size:13px;font-weight:700;color:var(--mint);min-height:20px;"></div>

        <h3 style="font-size:15px;font-weight:800;margin:20px 0 12px;">登録済み商品一覧</h3>
        <div id="pm-list" style="display:flex;flex-direction:column;gap:8px;padding-bottom:30px;"></div>
      </div>
    </div>

    <div id="panel-ocr" style="display:none;flex:1;overflow:hidden;">
      <div class="ocr-panel">
        <h3 style="font-size:16px;font-weight:800;margin-bottom:12px;">📸 レシートOCR入力</h3>
        <p style="font-size:13px;color:var(--text2);margin-bottom:14px;line-height:1.7;">レシートの写真・スキャンをドロップすると、ローカルLLM/OCRで読み取ります。まとめて複数ファイルを処理できます。</p>
        <label class="ocr-drop" id="ocr-drop">
          <input type="file" multiple accept="image/*,.pdf" onchange="handleOCRFiles(this.files)" id="ocr-file">
          <div style="font-size:40px;margin-bottom:10px;">📄</div>
          <div style="font-size:15px;font-weight:800;margin-bottom:4px;">ここにドロップ</div>
          <div style="font-size:13px;color:var(--text2);">または クリックして選択</div>
          <div style="font-size:11px;color:var(--text2);margin-top:8px;">対応: JPG / PNG / PDF</div>
        </label>

        <div style="margin-top:14px;display:flex;gap:10px;">
          <select class="f-input" id="ocr-endpoint" style="flex:1;font-size:13px;padding:8px;">
            <option value="local">ローカルLLM(Ollama / llama.cpp)</option>
            <option value="custom">カスタムエンドポイント</option>
          </select>
          <input class="f-input" id="ocr-url" placeholder="http://localhost:11434/api/generate" style="flex:2;font-size:12px;padding:8px;">
        </div>

        <div class="ocr-queue" id="ocr-queue">
          <div style="text-align:center;color:var(--text2);font-size:13px;padding:16px;">レシートをドロップするとここに表示されます</div>
        </div>

        <button class="add-prod-btn" style="margin-top:14px;" onclick="processOCRQueue()">🔍 OCR処理を開始</button>
      </div>
    </div>

    <div id="panel-history" style="display:none;flex:1;overflow:hidden;flex-direction:column;">
      <div class="history-panel">
        <div class="history-toolbar">
          <span class="ht-label">📅 日付</span>
          <input type="date" class="history-date-input" id="history-date" onchange="onHistoryDateChange()">
          <button class="ht-btn" onclick="historyDateOffset(-1)">◀ 前日</button>
          <button class="ht-btn" onclick="historyDateOffset(1)">翌日 ▶</button>
          <button class="ht-btn" onclick="setHistoryDateToday()">今日</button>
          <button class="ht-btn primary" onclick="exportHistoryCSV()" title="表示中の日のCSVを保存">📥 CSV保存</button>
          <button class="ht-btn primary" onclick="document.getElementById('history-csv-input').click()" title="CSVを読み込んで復元">📂 CSV読込</button>
          <input type="file" id="history-csv-input" accept=".csv" style="display:none" onchange="importHistoryCSV(this)">
          <button class="ht-btn danger" id="history-del-btn" onclick="deleteSelectedTx()" style="display:none;">🗑 選択を削除</button>
        </div>
        <div class="history-summary" id="history-summary">
          <span>データなし</span>
        </div>
        <div class="history-list" id="history-list">
          <div class="history-empty"><span>📋</span>決済データがありません</div>
        </div>
      </div>
    </div>
  </div>

  <div class="right">
    <div class="receipt-header">
      <h2>🧾 レシート <span style="font-size:12px;font-weight:600;color:var(--text2);" id="txid-label"></span></h2>
    </div>

    <div class="right-body">

      <div class="right-items-col">
        <div class="receipt-items" id="receipt-items">
          <div class="empty-msg"><span>🛍️</span>商品をタップして追加</div>
        </div>
      </div>

      <div class="right-info-col">

        <div style="padding:8px 10px;border-bottom:2px solid var(--border);flex-shrink:0;">
          <div class="cust-label">👤 顧客種別</div>
          <div class="customer-bar" id="cust-bar"></div>
          <div class="cust-label">🎂 年齢層</div>
          <div class="age-bar" id="age-bar"></div>
        </div>

        <div class="note-area" style="padding-top:8px;">
          <div style="font-size:11px;color:var(--text2);font-weight:700;margin-bottom:4px;">📝 メモ</div>
          <textarea class="note-input" id="tx-note" placeholder="特記事項..."></textarea>
        </div>

        <div class="discount-bar">
          <span class="disc-label">💚 割引</span>
          <div class="disc-btns">
            <button class="disc-btn" data-disc="0" onclick="setDiscount(0,this)">なし</button>
            <button class="disc-btn" data-disc="5" onclick="setDiscount(5,this)">5%</button>
            <button class="disc-btn" data-disc="10" onclick="setDiscount(10,this)">10%</button>
            <button class="disc-btn" data-disc="15" onclick="setDiscount(15,this)">15%</button>
            <button class="disc-btn" data-disc="-1" onclick="openCustomDiscount()">任意</button>
          </div>
        </div>

        <div class="total-area">
          <div class="total-row"><span>小計</span><span id="subtotal"0</span></div>
          <div class="total-row"><span>割引 <span class="tax-badge" id="disc-badge">0%</span></span><span id="disc-amt" style="color:var(--mint);">-¥0</span></div>
          <div class="total-row"><span>消費税</span><span id="tax-amt"0</span></div>
          <div class="total-row grand"><span>合計</span><span id="grand-total"0</span></div>
        </div>

        <div class="action-btns">
          <button class="act-btn act-pay" id="pay-btn" disabled onclick="confirmPay()">✅ 決済する</button>
          <button class="act-btn act-cancel" onclick="confirmCancel()">❌ キャンセル</button>
        </div>

      </div></div></div>
</div>

<div class="modal hidden" id="modal">
  <div class="modal-box" id="modal-box">
    <div class="modal-title" id="modal-title"></div>
    <div class="modal-body" id="modal-body"></div>
    <div class="modal-btns" id="modal-btns"></div>
  </div>
</div>

<div class="toast" id="toast"></div>

<script>
// ═══════════════════════════════════════════════════════
// ─── IndexedDB Utils (ファイルハンドル永続化用) ────────
// ═══════════════════════════════════════════════════════
const DB_NAME = 'mise_pos_db';
const STORE_NAME = 'handles';

function initDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, 1);
    request.onupgradeneeded = (e) => {
      e.target.result.createObjectStore(STORE_NAME);
    };
    request.onsuccess = (e) => resolve(e.target.result);
    request.onerror = () => reject(request.error);
  });
}

async function saveHandle(handle) {
  const db = await initDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, 'readwrite');
    tx.objectStore(STORE_NAME).put(handle, 'csvDirHandle');
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

async function loadHandle() {
  const db = await initDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, 'readonly');
    const request = tx.objectStore(STORE_NAME).get('csvDirHandle');
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(tx.error);
  });
}

async function verifyPermission(fileHandle, readWrite) {
  const options = {};
  if (readWrite) {
    options.mode = 'readwrite';
  }
  // ユーザー操作なしで権限があるかチェック
  if ((await fileHandle.queryPermission(options)) === 'granted') {
    return true;
  }
  return false;
}

// ═══════════════════════════════════════════════════════
// ─── 音声 ───────────────────────────────────────────────
// ═══════════════════════════════════════════════════════
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let actx = null;
function getCtx(){if(!actx)actx=new AudioCtx();return actx;}

function beep(freq=880, dur=80, type='sine', vol=0.3, delay=0){
  try{
    const ctx=getCtx();
    const o=ctx.createOscillator();
    const g=ctx.createGain();
    o.connect(g); g.connect(ctx.destination);
    o.type=type; o.frequency.value=freq;
    g.gain.setValueAtTime(0,ctx.currentTime+delay);
    g.gain.linearRampToValueAtTime(vol,ctx.currentTime+delay+0.01);
    g.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+delay+dur/1000);
    o.start(ctx.currentTime+delay);
    o.stop(ctx.currentTime+delay+dur/1000+0.05);
  }catch(e){}
}
function sfxAdd(){beep(880,70,'sine');}
function sfxRemove(){beep(440,80,'sine');}
function sfxCancel(){beep(330,120,'sawtooth',0.15);}
function sfxPay(){beep(660,60,'sine');setTimeout(()=>beep(880,80,'sine'),80);setTimeout(()=>beep(1100,150,'sine'),170);}
function sfxError(){beep(220,200,'sawtooth',0.2);}
function sfxSave(){beep(700,60,'sine');setTimeout(()=>beep(900,80,'sine'),90);}

// ═══════════════════════════════════════════════════════
// ─── データ ──────────────────────────────────────────────
// ═══════════════════════════════════════════════════════
let products = JSON.parse(localStorage.getItem('mise_products') || 'null') || [
  {id:'P001',name:'カフェラテ',cat:'ドリンク',price:450,tax:10,icon:'☕',color:'#ffe4d9'},
  {id:'P002',name:'抹茶ラテ',cat:'ドリンク',price:480,tax:10,icon:'🍵',color:'#d9f5e4'},
  {id:'P003',name:'クロワッサン',cat:'パン',price:220,tax:8,icon:'🥐',color:'#fff5cc'},
  {id:'P004',name:'チーズケーキ',cat:'スイーツ',price:380,tax:8,icon:'🍰',color:'#ede0ff'},
  {id:'P005',name:'ブレンドコーヒー',cat:'ドリンク',price:350,tax:10,icon:'☕',color:'#d9f0ff'},
  {id:'P006',name:'ホットサンド',cat:'フード',price:600,tax:8,icon:'🥪',color:'#ffd9d9'},
  {id:'P007',name:'レモネード',cat:'ドリンク',price:400,tax:10,icon:'🍋',color:'#fff5cc'},
  {id:'P008',name:'ガトーショコラ',cat:'スイーツ',price:420,tax:8,icon:'🍫',color:'#ede0ff'},
  {id:'P009',name:'オレンジジュース',cat:'ドリンク',price:380,tax:10,icon:'🍊',color:'#ffe4d9'},
  {id:'P010',name:'マフィン',cat:'パン',price:280,tax:8,icon:'🧁',color:'#ffd9d9'},
];
let sales = JSON.parse(localStorage.getItem('mise_sales') || '[]');
let cart = [];
let discountPct = 0;
let selectedCustomer = null;
let selectedAge = null;
let currentCat = 'all';
let selectedColor = '#ffe4d9';
let editingProductId = null;
let ocrQueue = [];
let txCounter = parseInt(localStorage.getItem('mise_txcounter')||'1');

// ─── File System Access API: フォルダハンドル(セッション中のみ保持) ───
let csvDirHandle = null; // FileSystemDirectoryHandle

// ─── 決済リスト状態 ───
let selectedTxIds  = new Set();
let expandedTxIds  = new Set();
let historyViewDate = '';

const customerTypes = [
  {key:'general', label:'一般', icon:'👤'},
  {key:'member',  label:'会員', icon:'🌟'},
  {key:'student', label:'学生', icon:'🎓'},
  {key:'senior',  label:'シニア', icon:'👴'},
  {key:'staff',   label:'スタッフ', icon:'🏷️'},
];
const ageBands = ['〜19','20-29','30-39','40-49','50-59','60+'];

// ═══════════════════════════════════════════════════════
// ─── 初期化 ──────────────────────────────────────────────
// ═══════════════════════════════════════════════════════
async function init(){
  historyViewDate=todayStr();
  renderClock();
  setInterval(renderClock,1000);
  renderCatBar();
  renderProducts();
  renderCustomerBar();
  renderAgeBar();
  renderCart();
  updateTxLabel();
  // 起動時: localStorageに保存されたsetting内容(前回終了時に保存済み)を反映
  applyStoredSettings();
  
  // File System Access API対応ブラウザであれば、保存されたハンドルの復旧を試みる
  if('showDirectoryPicker' in window){
    const storedHandle = await loadHandle().catch(()=>null);
    if(storedHandle){
      try {
        const hasPerm = await verifyPermission(storedHandle, true);
        if(hasPerm){
          csvDirHandle = storedHandle;
          localStorage.setItem('mise_csv_folder_name', csvDirHandle.name);
          await loadSettingFromFolder();
          const disp = document.getElementById('set-folder-display');
          if(disp){
            disp.textContent = '📂 ' + csvDirHandle.name;
            disp.style.color = 'var(--mint)';
          }
        } else {
          showFolderBanner(storedHandle.name);
        }
      } catch(e) {
        showFolderBanner(storedHandle.name || localStorage.getItem('mise_csv_folder_name'));
      }
    } else {
      const lastName = localStorage.getItem('mise_csv_folder_name');
      if(lastName){
        showFolderBanner(lastName);
      }
    }
  }
}

function renderClock(){
  const n=new Date();
  document.getElementById('clock').textContent=
    String(n.getHours()).padStart(2,'0')+':'+String(n.getMinutes()).padStart(2,'0');
  const DOW_JP=['日','月','火','水','木','金','土'];
  document.getElementById('hdr-date').textContent=
    n.getFullYear()+'年'+(n.getMonth()+1)+'月'+n.getDate()+'日('+DOW_JP[n.getDay()]+')';
}

function updateTxLabel(){
  document.getElementById('txid-label').textContent='#'+String(txCounter).padStart(4,'0');
}

// ─── カテゴリバー ─────────────────────────────────────────
function renderCatBar(){
  const cats=['all',...new Set(products.map(p=>p.cat))];
  const bar=document.getElementById('cat-bar');
  bar.innerHTML=cats.map(c=>`<button class="cat-tab${c===currentCat?' active':''}" onclick="filterCat('${c}',this)">
    ${c==='all'?'🏷️ すべて':c}
  </button>`).join('');
  // datalist更新
  const dl=document.getElementById('cat-datalist');
  dl.innerHTML=cats.filter(c=>c!=='all').map(c=>`<option value="${c}">`).join('');
}

function filterCat(cat,btn){
  currentCat=cat;
  document.querySelectorAll('.cat-tab').forEach(b=>b.classList.remove('active'));
  btn.classList.add('active');
  renderProducts();
}

// ─── 商品グリッド ─────────────────────────────────────────
function renderProducts(){
  const filtered = currentCat==='all' ? products : products.filter(p=>p.cat===currentCat);
  const grid=document.getElementById('products-grid');
  if(!filtered.length){grid.innerHTML='<div style="color:var(--text2);text-align:center;padding:40px;grid-column:1/-1;font-weight:700;">商品がありません</div>';return;}
  grid.innerHTML=filtered.map(p=>`
    <div class="prod-card" style="background:${p.color||'#fff'}" onclick="addToCart('${p.id}')">
      ${p.badge?`<div class="prod-badge">${p.badge}</div>`:''}
      <span class="prod-icon">${p.icon||'📦'}</span>
      <div class="prod-name">${p.name}</div>
      <div class="prod-price">¥${Math.round(p.price*(1+p.tax/100)).toLocaleString()} <small>税込</small></div>
    </div>`).join('');
}

// ─── 顧客ボタン ───────────────────────────────────────────
function renderCustomerBar(){
  const bar=document.getElementById('cust-bar');
  bar.innerHTML=customerTypes.map(c=>`
    <button class="cust-btn${selectedCustomer===c.key?' active':''}" onclick="selectCustomer('${c.key}',this)">
      ${c.icon} ${c.label}
    </button>`).join('');
}
function selectCustomer(key,btn){
  selectedCustomer = selectedCustomer===key ? null : key;
  beep(660,40,'sine',0.2);
  renderCustomerBar();
}

function renderAgeBar(){
  const bar=document.getElementById('age-bar');
  bar.innerHTML=ageBands.map(a=>`
    <button class="age-btn${selectedAge===a?' active':''}" onclick="selectAge('${a}',this)">
      ${a}
    </button>`).join('');
}
function selectAge(age,btn){
  selectedAge = selectedAge===age ? null : age;
  beep(660,40,'sine',0.2);
  renderAgeBar();
}

// ═══════════════════════════════════════════════════════
// ─── カート操作 ─────────────────────────────────────────
// ═══════════════════════════════════════════════════════
function addToCart(pid){
  const prod = products.find(p=>p.id===pid);
  if(!prod) return;
  const existing = cart.find(i=>i.pid===pid);
  if(existing){existing.qty++;}
  else{cart.push({pid,qty:1,discount:0});}
  sfxAdd();
  renderCart();
  showToast(`${prod.icon||'📦'} ${prod.name} を追加`);
}

function changeQty(pid, delta){
  const item = cart.find(i=>i.pid===pid);
  if(!item) return;
  item.qty += delta;
  if(item.qty <= 0){
    cart = cart.filter(i=>i.pid!==pid);
    sfxRemove();
  } else {
    delta>0 ? sfxAdd() : sfxRemove();
  }
  renderCart();
}

function removeFromCart(pid){
  cart = cart.filter(i=>i.pid!==pid);
  sfxRemove();
  renderCart();
}

function renderCart(){
  const area=document.getElementById('receipt-items');
  if(!cart.length){
    area.innerHTML='<div class="empty-msg"><span>🛍️</span>商品をタップして追加</div>';
    document.getElementById('pay-btn').disabled=true;
  } else {
    area.innerHTML=cart.map(item=>{
      const p=products.find(pr=>pr.id===item.pid);
      if(!p) return '';
      const unitTax = Math.round(p.price*(p.tax/100));
      const unitFull = p.price + unitTax;
      const total = unitFull * item.qty;
      return `<div class="receipt-item">
        <span class="ri-icon">${p.icon||'📦'}</span>
        <div class="ri-name">${p.name}<br><small>¥${unitFull.toLocaleString()} × ${item.qty}</small></div>
        <div class="ri-qty">
          <button class="ri-qbtn minus" onclick="changeQty('${p.id}',-1)">−</button>
          <span class="ri-qnum">${item.qty}</span>
          <button class="ri-qbtn plus" onclick="changeQty('${p.id}',1)">+</button>
        </div>
        <div class="ri-amt">¥${total.toLocaleString()}</div>
        <button class="ri-del" onclick="removeFromCart('${p.id}')" title="削除">🗑</button>
      </div>`;
    }).join('');
    document.getElementById('pay-btn').disabled=false;
  }
  updateTotals();
}

// ─── 合計計算 ─────────────────────────────────────────────
function calcTotals(){
  let subtotal=0, taxTotal=0;
  cart.forEach(item=>{
    const p=products.find(pr=>pr.id===item.pid);
    if(!p) return;
    subtotal += p.price * item.qty;
    taxTotal += Math.round(p.price*(p.tax/100)) * item.qty;
  });
  const subtotalFull = subtotal + taxTotal;
  const discAmt = Math.round(subtotalFull * discountPct / 100);
  const grand = subtotalFull - discAmt;
  return {subtotalFull, discAmt, taxTotal, grand};
}

function updateTotals(){
  const {subtotalFull, discAmt, taxTotal, grand} = calcTotals();
  document.getElementById('subtotal').textContent = '¥'+subtotalFull.toLocaleString();
  document.getElementById('disc-amt').textContent = '-¥'+discAmt.toLocaleString();
  document.getElementById('disc-badge').textContent = discountPct+'%';
  document.getElementById('tax-amt').textContent = '¥'+taxTotal.toLocaleString();
  document.getElementById('grand-total').textContent = '¥'+grand.toLocaleString();
}

function setDiscount(pct, btn){
  if(pct===-1) return openCustomDiscount();
  discountPct = pct;
  document.querySelectorAll('.disc-btn').forEach(b=>b.classList.remove('active'));
  btn.classList.add('active');
  beep(700,50,'sine',0.2);
  updateTotals();
}

function openCustomDiscount(){
  showModal('任意割引を入力','',`
    <div style="margin-bottom:16px;">
      <input id="custom-disc-input" class="f-input" type="number" min="0" max="100" placeholder="割引率 %" style="font-size:24px;text-align:center;padding:14px;">
    </div>
  `,[
    {label:'キャンセル',cls:'mbtn-cancel',action:closeModal},
    {label:'適用',cls:'mbtn-ok',action:()=>{
      const v=parseInt(document.getElementById('custom-disc-input').value||'0');
      if(v<0||v>100){sfxError();showToast('0〜100の値を入力してください');return;}
      discountPct=v;
      document.querySelectorAll('.disc-btn').forEach(b=>b.classList.remove('active'));
      const custom=document.querySelector('.disc-btn[data-disc="-1"]');
      if(custom){custom.classList.add('active');custom.textContent=v+'%';}
      beep(700,50,'sine',0.2);
      updateTotals();
      closeModal();
    }},
  ]);
}

// ═══════════════════════════════════════════════════════
// ─── 決済・ローカルタイム取得 ─────────────────────────
// ═══════════════════════════════════════════════════════

function getLocalDatetime() {
  const now = new Date();
  const yyyy = now.getFullYear();
  const MM = String(now.getMonth()+1).padStart(2,'0');
  const dd = String(now.getDate()).padStart(2,'0');
  const HH = String(now.getHours()).padStart(2,'0');
  const mm = String(now.getMinutes()).padStart(2,'0');
  const ss = String(now.getSeconds()).padStart(2,'0');
  return {
    datetime: `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`,
    date: `${yyyy}-${MM}-${dd}`,
    hour: now.getHours(),
    dow: now.getDay(),
    month: now.getMonth()+1
  };
}

function confirmPay(){
  if(!cart.length){sfxError();return;}
  const {grand} = calcTotals();
  showModal('決済確認','合計 <b style="font-size:22px;color:var(--mint);">¥'+grand.toLocaleString()+'</b> でよろしいですか?','',[
    {label:'戻る',cls:'mbtn-cancel',action:closeModal},
    {label:'✅ 決済する',cls:'mbtn-ok',action:executePay},
  ]);
}

function executePay(){
  closeModal();
  sfxPay();
  
  const localTime = getLocalDatetime();
  const {subtotalFull, discAmt, taxTotal, grand} = calcTotals();
  const txid = 'TX'+String(txCounter).padStart(4,'0');

  cart.forEach(item=>{
    const p=products.find(pr=>pr.id===item.pid);
    if(!p) return;
    const unitFull = Math.round(p.price*(1+p.tax/100));
    const perItemDisc = Math.round(discountPct/100 * unitFull);
    sales.push({
      txid, 
      datetime: localTime.datetime, 
      date: localTime.date, 
      hour: localTime.hour, 
      dow: localTime.dow, 
      month: localTime.month,
      product_id: p.id,
      product: p.name,
      category: p.cat,
      price: unitFull,
      quantity: item.qty,
      amount: (unitFull - perItemDisc) * item.qty,
      tax: p.tax,
      discount_pct: discountPct,
      customer_type: selectedCustomer||'',
      age_band: selectedAge||'',
      note: document.getElementById('tx-note').value||'',
    });
  });

  localStorage.setItem('mise_sales', JSON.stringify(sales));
  txCounter++;
  localStorage.setItem('mise_txcounter', String(txCounter));

  // フォルダが設定されていれば日別CSVとsetting.txtを自動保存
  if(csvDirHandle){
    saveDailyCSVToFolder(localTime.date).catch(()=>{});
    autoSaveSettingToFolder().catch(()=>{});
  }

  // 決済リストが表示中なら更新
  if(document.getElementById('panel-history').style.display!=='none'){
    renderHistoryList();
  }

  // 完了表示
  const {grand:g2} = calcTotals();
  showModal('✅ 決済完了',`
    <div class="paid-screen">
      <div class="paid-circle">✓</div>
      <h2>ありがとうございます!</h2>
      <div class="paid-amt">¥${grand.toLocaleString()}</div>
      <div class="paid-items">${cart.map(item=>{
        const p=products.find(pr=>pr.id===item.pid);
        if(!p) return '';
        const amt=Math.round(p.price*(1+p.tax/100))*(1-discountPct/100)*item.qty;
        return `<div><span>${p.icon} ${p.name} ×${item.qty}</span><span>¥${Math.round(amt).toLocaleString()}</span></div>`;
      }).join('')}</div>
      <div style="font-size:12px;color:var(--text2);">${txid} · ${localTime.datetime}</div>
    </div>
  `,'',[
    {label:'次のお客様へ',cls:'mbtn-ok',action:()=>{
      closeModal();
      resetCart();
    }},
  ]);
}

function resetCart(){
  cart=[];
  discountPct=0;
  selectedCustomer=null;
  selectedAge=null;
  document.getElementById('tx-note').value='';
  document.querySelectorAll('.disc-btn').forEach(b=>b.classList.remove('active'));
  const d0=document.querySelector('.disc-btn[data-disc="0"]');
  if(d0){d0.classList.add('active');}
  renderCustomerBar();
  renderAgeBar();
  renderCart();
  updateTxLabel();
  showToast('🌟 リセットしました');
}

function confirmCancel(){
  if(!cart.length){sfxCancel();resetCart();return;}
  sfxCancel();
  showModal('❌ 取引をキャンセル','現在のレシートをすべてクリアします。よろしいですか?','',[
    {label:'戻る',cls:'mbtn-cancel',action:closeModal},
    {label:'キャンセル実行',cls:'mbtn-ok',action:()=>{closeModal();resetCart();}},
  ]);
}

// ═══════════════════════════════════════════════════════
// ─── 商品管理 ────────────────────────────────────────────
// ═══════════════════════════════════════════════════════
function selectColor(el){
  document.querySelectorAll('.color-swatch').forEach(s=>s.style.border='3px solid transparent');
  el.style.border='3px solid var(--peach)';
  selectedColor=el.dataset.color;
}

function saveProduct(){
  const name=document.getElementById('pm-name').value.trim();
  const cat=document.getElementById('pm-cat').value.trim();
  const priceVal=document.getElementById('pm-price').value;
  const price=priceVal===''?NaN:parseInt(priceVal); // 0円の登録を許容
  const tax=parseInt(document.getElementById('pm-tax').value||'10');
  const icon=document.getElementById('pm-icon').value.trim()||'📦';
  const id=document.getElementById('pm-id').value.trim();
  const barcode=document.getElementById('pm-barcode').value.trim();

  if(!name||!cat||isNaN(price)){sfxError();showToast('⚠️ 商品名・カテゴリ・価格は必須です');return;}

  if(editingProductId){
    const idx=products.findIndex(p=>p.id===editingProductId);
    if(idx>-1){
      products[idx]={...products[idx],name,cat,price,tax,icon,color:selectedColor,barcode};
    }
    editingProductId=null;
    document.getElementById('pm-id').value='';
  } else {
    const newId=id||'P'+String(Date.now()).slice(-6);
    if(products.find(p=>p.id===newId)){sfxError();showToast('⚠️ IDが重複しています');return;}
    products.push({id:newId,name,cat,price,tax,icon,color:selectedColor,barcode});
  }

  localStorage.setItem('mise_products',JSON.stringify(products));
  sfxSave();
  showToast('✅ 商品を保存しました');
  document.getElementById('pm-feedback').textContent='✅ 保存しました!';
  setTimeout(()=>document.getElementById('pm-feedback').textContent='',2000);

  // フォームリセット
  ['pm-name','pm-cat','pm-price','pm-icon','pm-id','pm-barcode'].forEach(id=>document.getElementById(id).value='');
  document.getElementById('pm-tax').value='10';

  renderCatBar();
  renderProducts();
  renderPMList();
  
  if(csvDirHandle){
    autoSaveSettingToFolder().catch(()=>{});
  }
}

function renderPMList(){
  const list=document.getElementById('pm-list');
  list.innerHTML=products.map(p=>`
    <div style="display:flex;align-items:center;gap:10px;background:${p.color||'#fff8f2'};border-radius:14px;padding:10px 14px;border:2px solid var(--border);">
      <span style="font-size:24px;">${p.icon||'📦'}</span>
      <div style="flex:1;">
        <div style="font-size:14px;font-weight:800;">${p.name}</div>
        <div style="font-size:11px;color:var(--text2);">${p.cat} · ¥${p.price.toLocaleString()} (税${p.tax}%) · ${p.id}</div>
      </div>
      <button style="border:none;background:var(--sky-l);border-radius:10px;padding:6px 10px;font-size:12px;font-weight:700;cursor:pointer;color:var(--sky);" onclick="editProduct('${p.id}')">編集</button>
      <button style="border:none;background:var(--peach-l);border-radius:10px;padding:6px 10px;font-size:12px;font-weight:700;cursor:pointer;color:var(--coral);" onclick="deleteProduct('${p.id}')">削除</button>
    </div>`).join('');
}

function editProduct(id){
  const p=products.find(pr=>pr.id===id);
  if(!p) return;
  editingProductId=id;
  document.getElementById('pm-name').value=p.name;
  document.getElementById('pm-cat').value=p.cat;
  document.getElementById('pm-price').value=p.price;
  document.getElementById('pm-tax').value=p.tax;
  document.getElementById('pm-icon').value=p.icon||'';
  document.getElementById('pm-id').value=p.id;
  document.getElementById('pm-barcode').value=p.barcode||'';
  selectedColor=p.color||'#ffe4d9';
  document.querySelectorAll('.color-swatch').forEach(s=>{
    s.style.border=s.dataset.color===selectedColor?'3px solid var(--peach)':'3px solid transparent';
  });
  setMode('products',null);
  document.getElementById('pm-name').focus();
  sfxSave();
}

function deleteProduct(id){
  const p=products.find(pr=>pr.id===id);
  if(!p) return;
  showModal(`「${p.name}」を削除`,`この商品をリストから削除します。過去の売上データは保持されます。`,'',[
    {label:'キャンセル',cls:'mbtn-cancel',action:closeModal},
    {label:'削除する',cls:'mbtn-ok',action:()=>{
      products=products.filter(pr=>pr.id!==id);
      cart=cart.filter(i=>i.pid!==id);
      localStorage.setItem('mise_products',JSON.stringify(products));
      renderCatBar();renderProducts();renderCart();renderPMList();
      closeModal();sfxCancel();showToast('削除しました');
      if(csvDirHandle) autoSaveSettingToFolder().catch(()=>{});
    }},
  ]);
}

// ═══════════════════════════════════════════════════════
// ─── OCR ─────────────────────────────────────────────────
// ═══════════════════════════════════════════════════════
const ocrDrop=document.getElementById('ocr-drop');
ocrDrop.addEventListener('dragover',e=>{e.preventDefault();ocrDrop.classList.add('drag');});
ocrDrop.addEventListener('dragleave',()=>ocrDrop.classList.remove('drag'));
ocrDrop.addEventListener('drop',e=>{
  e.preventDefault();ocrDrop.classList.remove('drag');
  handleOCRFiles(e.dataTransfer.files);
});

function handleOCRFiles(files){
  Array.from(files).forEach(f=>{
    const reader=new FileReader();
    reader.onload=e=>{
      ocrQueue.push({name:f.name,data:e.target.result,type:f.type,status:'pending'});
      renderOCRQueue();
    };
    reader.readAsDataURL(f);
  });
}

function renderOCRQueue(){
  const q=document.getElementById('ocr-queue');
  if(!ocrQueue.length){q.innerHTML='<div style="text-align:center;color:var(--text2);font-size:13px;padding:16px;">レシートをドロップするとここに表示されます</div>';return;}
  q.innerHTML=ocrQueue.map((item,i)=>`
    <div class="ocr-item">
      ${item.type.startsWith('image')?`<img class="ocr-thumb" src="${item.data}">`:'<div class="ocr-thumb" style="display:flex;align-items:center;justify-content:center;font-size:22px;">📄</div>'}
      <div class="ocr-info">
        <div class="ocr-name">${item.name}</div>
        <div class="ocr-status ${item.status}">${
          item.status==='pending'?'待機中...' :
          item.status==='processing'?'🔍 処理中...' :
          item.status==='done'?'✅ 完了' :
          '❌ エラー'
        }</div>
      </div>
      <button onclick="ocrQueue.splice(${i},1);renderOCRQueue();" style="border:none;background:var(--peach-l);border-radius:8px;padding:5px 9px;color:var(--coral);font-size:12px;cursor:pointer;">✕</button>
    </div>`).join('');
}

async function processOCRQueue(){
  const endpoint=document.getElementById('ocr-url').value||'http://localhost:11434/api/generate';
  let processed=0;
  for(let i=0;i<ocrQueue.length;i++){
    if(ocrQueue[i].status!=='pending') continue;
    ocrQueue[i].status='processing';
    renderOCRQueue();
    try{
      const base64=ocrQueue[i].data.split(',')[1];
      const res=await fetch(endpoint,{
        method:'POST',
        headers:{'Content-Type':'application/json'},
        body:JSON.stringify({
          model:'llava',
          prompt:'このレシートから商品名・数量・金額をJSON形式で抽出してください。例: [{"product":"コーヒー","qty":1,"amount":350}]',
          images:[base64],
          stream:false,
        }),
      });
      if(!res.ok) throw new Error('HTTP '+res.status);
      const data=await res.json();
      const text=data.response||'';
      // JSON抽出試行
      const match=text.match(/\[[\s\S]*?\]/);
      if(match){
        const items=JSON.parse(match[0]);
        const txid = 'OCR'+String(txCounter).padStart(4,'0');
        const localTime = getLocalDatetime();
        
        items.forEach(item=>{
          sales.push({
            txid: txid,
            datetime: localTime.datetime,
            date: localTime.date,
            hour: localTime.hour, dow: localTime.dow, month: localTime.month,
            product_id:'',product:item.product||'不明',category:'OCR',
            price:item.amount||0,quantity:item.qty||1,amount:item.amount||0,
            tax:8,discount_pct:0,customer_type:'',age_band:'',note:'OCR: '+ocrQueue[i].name,
          });
        });
        txCounter++;localStorage.setItem('mise_txcounter',String(txCounter));
        localStorage.setItem('mise_sales',JSON.stringify(sales));
        if(csvDirHandle) autoSaveSettingToFolder().catch(()=>{});
      }
      ocrQueue[i].status='done';
      processed++;
    }catch(e){
      ocrQueue[i].status='err';
      ocrQueue[i].error=e.message;
    }
    renderOCRQueue();
  }
  if(processed>0){sfxPay();showToast(`✅ ${processed}件のレシートを処理しました`);}
  else{sfxError();showToast('⚠️ 処理できませんでした(ローカルLLMが起動しているか確認してください)');}
}

// ═══════════════════════════════════════════════════════
// ─── CSV出力(全データ) ────────────────────────────────
// ═══════════════════════════════════════════════════════
function exportCSV(){
  if(!sales.length){sfxError();showToast('⚠️ 売上データがありません');return;}
  const headers=['txid','datetime','date','hour','dow','month','product_id','product','category','price','quantity','amount','tax','discount_pct','customer_type','age_band','note'];
  const csv=[headers.join(','),...sales.map(r=>headers.map(h=>{
    const v=r[h]!==undefined?r[h]:'';
    const sv=String(v);
    return sv.includes(',')||sv.includes('"')||sv.includes('\n')?`"${sv.replace(/"/g,'""')}"`:sv;
  }).join(','))].join('\n');
  const blob=new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8'});
  const a=document.createElement('a');
  a.href=URL.createObjectURL(blob);
  a.download='MISE_all_sales_'+new Date().toISOString().slice(0,10)+'.csv';
  a.click();
  setTimeout(()=>URL.revokeObjectURL(a.href),5000);
  sfxSave();
  showToast('📤 '+sales.length+'件のデータを出力しました');
}

// ═══════════════════════════════════════════════════════
// ─── 設定 ────────────────────────────────────────────────
// ═══════════════════════════════════════════════════════
function openSettings(){
  const folderName = csvDirHandle ? csvDirHandle.name : localStorage.getItem('mise_csv_folder_name') || '未選択';
  const fsSupported = ('showDirectoryPicker' in window);
  const fsNote = fsSupported
    ? `<div style="font-size:11px;color:var(--text2);margin-top:4px;">✅ File System Access API対応ブラウザです。フォルダを選択するとCSVと全データが自動保存されます。</div>`
    : `<div style="font-size:11px;color:#e05050;margin-top:4px;">⚠️ このブラウザはFile System Access APIに対応していません(Chrome/Edge推奨)。CSVはダウンロード保存のみです。</div>`;
  showModal('⚙️ 設定',`
    <div style="text-align:left;max-height:65vh;overflow-y:auto;padding-right:4px;">
      <div style="margin-bottom:14px;">
        <label style="font-size:12px;font-weight:700;color:var(--text2);display:block;margin-bottom:5px;">🏪 店舗名</label>
        <input id="set-storename" class="f-input" value="${escAttr(localStorage.getItem('mise_storename')||'')}" placeholder="例: カフェ花まる">
      </div>
      <div style="margin-bottom:14px;">
        <label style="font-size:12px;font-weight:700;color:var(--text2);display:block;margin-bottom:5px;">📁 データ保存フォルダ(日別CSV・バックアップ保存先)</label>
        <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
          <div id="set-folder-display" style="flex:1;background:var(--bg2);border:2px solid var(--border);border-radius:10px;padding:7px 12px;font-size:13px;font-weight:700;color:${csvDirHandle?'var(--mint)':'var(--text2)'};">
            ${csvDirHandle ? '📂 ' + escHtml(csvDirHandle.name) : '📁 ' + escHtml(folderName)}
          </div>
          ${fsSupported ? `<button class="disc-btn" style="border-color:var(--sky);color:var(--sky);white-space:nowrap;" onclick="selectCsvFolder()">📂 フォルダ選択</button>` : ''}
        </div>
        ${fsNote}
        <div style="font-size:11px;color:var(--text2);margin-top:4px;">
          デフォルト: HTMLファイルと同じフォルダ。決済後や終了時に自動でこのフォルダへ書き込み・バックアップします。
        </div>
      </div>
      <div style="margin-bottom:14px;">
        <label style="font-size:12px;font-weight:700;color:var(--text2);display:block;margin-bottom:5px;">📋 売上データ(件数: ${sales.length}件)</label>
        <div style="display:flex;gap:8px;flex-wrap:wrap;">
          <button class="disc-btn" onclick="exportCSV();closeModal();">📤 全CSV出力</button>
          <button class="disc-btn" style="border-color:var(--coral);color:var(--coral);" onclick="if(confirm('全データを削除します。\\nこの操作は取り消せません。')){sales=[];localStorage.setItem('mise_sales','[]');closeModal();showToast('🗑 データを削除しました');}">🗑 クリア</button>
        </div>
      </div>
      <div style="margin-bottom:14px;">
        <label style="font-size:12px;font-weight:700;color:var(--text2);display:block;margin-bottom:5px;">🔢 次の取引番号</label>
        <input id="set-txcounter" class="f-input" type="number" value="${txCounter}">
      </div>
      <div style="margin-bottom:4px;">
        <label style="font-size:12px;font-weight:700;color:var(--text2);display:block;margin-bottom:5px;">⚙️ setting.txt 管理(売上含む全バックアップ)</label>
        <div style="display:flex;gap:8px;flex-wrap:wrap;">
          <button class="disc-btn" style="border-color:var(--sky);color:var(--sky);" onclick="saveSettingTxtManual()">💾 全データを保存</button>
          <button class="disc-btn" style="border-color:var(--sky);color:var(--sky);" onclick="document.getElementById('setting-txt-input').click()">📂 データを復元</button>
          <input type="file" id="setting-txt-input" accept=".txt,.json" style="display:none" onchange="loadSettingTxt(this)">
        </div>
        <div style="font-size:11px;color:var(--text2);margin-top:4px;">保存: 売上を含む全データをsetting.txtとして保存 復元: 以前保存したsetting.txtを適用</div>
      </div>
    </div>
  `,'',[
    {label:'閉じる',cls:'mbtn-cancel',action:closeModal},
    {label:'保存',cls:'mbtn-ok',action:()=>{
      const name=document.getElementById('set-storename')?.value||'';
      const tc=parseInt(document.getElementById('set-txcounter')?.value||txCounter);
      localStorage.setItem('mise_storename',name);
      txCounter=tc;
      localStorage.setItem('mise_txcounter',String(txCounter));
      updateTxLabel();
      closeModal();sfxSave();showToast('設定を保存しました');
      if(csvDirHandle) autoSaveSettingToFolder().catch(()=>{});
    }},
  ]);
}

// ═══════════════════════════════════════════════════════
// ─── モード切替 ─────────────────────────────────────────
// ═══════════════════════════════════════════════════════
function setMode(mode, btn){
  document.querySelectorAll('.mode-tab').forEach(b=>b.classList.remove('active'));
  if(btn) btn.classList.add('active');
  else {
    const tabs={'pos':0,'history':1,'products':2,'ocr':3};
    document.querySelectorAll('.mode-tab')[tabs[mode]!==undefined?tabs[mode]:0]?.classList.add('active');
  }
  document.getElementById('panel-pos').style.display=mode==='pos'?'flex':'none';
  document.getElementById('panel-history').style.display=mode==='history'?'flex':'none';
  document.getElementById('panel-products').style.display=mode==='products'?'flex':'none';
  document.getElementById('panel-ocr').style.display=mode==='ocr'?'flex':'none';
  if(mode==='products') renderPMList();
  if(mode==='ocr') renderOCRQueue();
  if(mode==='history') renderHistoryPanel();
}

// ═══════════════════════════════════════════════════════
// ─── 決済リスト ──────────────────────────────────────────
// ═══════════════════════════════════════════════════════

// 今日の日付文字列
function todayStr(){
  const n=new Date();
  return n.getFullYear()+'-'+String(n.getMonth()+1).padStart(2,'0')+'-'+String(n.getDate()).padStart(2,'0');
}

// 決済リストパネルを初期化して描画
function renderHistoryPanel(){
  if(!historyViewDate){
    historyViewDate=todayStr();
  }
  const el=document.getElementById('history-date');
  if(el) el.value=historyViewDate;
  selectedTxIds.clear();
  updateDeleteBtn();
  renderHistoryList();
}

// 日付変更
function onHistoryDateChange(){
  const el=document.getElementById('history-date');
  historyViewDate=el.value||todayStr();
  selectedTxIds.clear();
  expandedTxIds.clear();
  updateDeleteBtn();
  renderHistoryList();
}

// 前日・翌日
function historyDateOffset(delta){
  const d=new Date(historyViewDate+'T00:00:00');
  d.setDate(d.getDate()+delta);
  historyViewDate=d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');
  document.getElementById('history-date').value=historyViewDate;
  selectedTxIds.clear();
  expandedTxIds.clear();
  updateDeleteBtn();
  renderHistoryList();
}

// 今日へ
function setHistoryDateToday(){
  historyViewDate=todayStr();
  document.getElementById('history-date').value=historyViewDate;
  selectedTxIds.clear();
  expandedTxIds.clear();
  updateDeleteBtn();
  renderHistoryList();
}

// 日付の売上データを取引ごとにまとめる
function groupSalesByTx(dateStr){
  const dayRows=sales.filter(r=>r.date===dateStr);
  // txidでグルーピング
  const map=new Map();
  dayRows.forEach(r=>{
    if(!map.has(r.txid)){
      map.set(r.txid,{
        txid:r.txid,
        datetime:r.datetime,
        date:r.date,
        customer_type:r.customer_type||'',
        age_band:r.age_band||'',
        discount_pct:r.discount_pct||0,
        note:r.note||'',
        items:[],
        total:0,
      });
    }
    const tx=map.get(r.txid);
    tx.items.push({
      product_id:r.product_id,
      product:r.product,
      category:r.category,
      price:r.price,
      quantity:r.quantity,
      amount:r.amount,
      tax:r.tax,
    });
    tx.total+=r.amount;
    // 最初の行のnoteを使用
    if(!tx.note&&r.note) tx.note=r.note;
  });
  return [...map.values()].sort((a,b)=>a.datetime.localeCompare(b.datetime));
}

// 顧客種別ラベル変換
function custLabel(key){
  const map={general:'一般',member:'会員',student:'学生',senior:'シニア',staff:'スタッフ'};
  return map[key]||key||'';
}

// 決済リスト描画
function renderHistoryList(){
  const listEl=document.getElementById('history-list');
  const summaryEl=document.getElementById('history-summary');
  const txGroups=groupSalesByTx(historyViewDate);

  if(!txGroups.length){
    listEl.innerHTML='<div class="history-empty"><span>📋</span>この日の決済データがありません</div>';
    summaryEl.innerHTML='<span>0件 ¥0</span>';
    return;
  }

  // サマリー計算
  const totalAmt=txGroups.reduce((s,tx)=>s+tx.total,0);
  const totalQty=txGroups.reduce((s,tx)=>s+tx.items.reduce((q,i)=>q+i.quantity,0),0);
  const selectedCount=txGroups.filter(tx=>selectedTxIds.has(tx.txid)).length;
  const selectedAmt=txGroups.filter(tx=>selectedTxIds.has(tx.txid)).reduce((s,tx)=>s+tx.total,0);

  summaryEl.innerHTML=
    `<span>${txGroups.length}件</span>` +
    `<span>合計: <span style="color:var(--peach);font-family:'Nunito',sans-serif;">¥${totalAmt.toLocaleString()}</span></span>` +
    `<span>点数: ${totalQty}点</span>` +
    (selectedCount>0
      ? `<span style="color:var(--coral);">選択中: ${selectedCount}件 ¥${selectedAmt.toLocaleString()}</span>`
      : '');

  listEl.innerHTML=txGroups.map(tx=>{
    const isSelected=selectedTxIds.has(tx.txid);
    const isExpanded=expandedTxIds.has(tx.txid);
    const timeStr=tx.datetime?tx.datetime.slice(11,16):'--:--';
    const custStr=tx.customer_type?`<span class="htx-cust">${custLabel(tx.customer_type)}${tx.age_band?' '+tx.age_band:''}</span>`:'';
    const discStr=tx.discount_pct>0?`<div class="htx-disc">割引: ${tx.discount_pct}%</div>`:'';
    const noteStr=tx.note?`<div class="htx-note">📝 ${escHtml(tx.note)}</div>`:'';
    const itemsHtml=tx.items.map(it=>`
      <div class="htx-item">
        <span class="htx-item-name">${escHtml(it.product)}</span>
        <span class="htx-item-detail">${it.quantity}点 × ¥${it.price.toLocaleString()}</span>
        <span class="htx-item-amt">¥${it.amount.toLocaleString()}</span>
      </div>`).join('');
    return `<div class="history-tx${isSelected?' selected':''}" data-txid="${escAttr(tx.txid)}">
      <div class="history-tx-header" onclick="toggleTxExpand('${escAttr(tx.txid)}',event)">
        <div class="htx-check${isSelected?' checked':''}" onclick="toggleTxSelect('${escAttr(tx.txid)}',event)" title="選択">${isSelected?'✓':''}</div>
        <span class="htx-id">${escHtml(tx.txid)}</span>
        <span class="htx-time">${timeStr}</span>
        ${custStr}
        <span class="htx-total">¥${tx.total.toLocaleString()}</span>
        <span class="htx-expand${isExpanded?' open':''}">▾</span>
      </div>
      <div class="history-tx-body${isExpanded?' open':''}">
        ${itemsHtml}
        ${discStr}
        ${noteStr}
      </div>
    </div>`;
  }).join('');
}

// HTML特殊文字エスケープ
function escHtml(s){
  return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function escAttr(s){
  return String(s||'').replace(/'/g,"\\'").replace(/"/g,'&quot;');
}

// 取引の展開・折りたたみ
function toggleTxExpand(txid,e){
  e.stopPropagation();
  if(expandedTxIds.has(txid)){expandedTxIds.delete(txid);}
  else{expandedTxIds.add(txid);}
  renderHistoryList();
}

// 取引の選択切り替え
function toggleTxSelect(txid,e){
  e.stopPropagation();
  if(selectedTxIds.has(txid)){selectedTxIds.delete(txid);}
  else{selectedTxIds.add(txid);}
  updateDeleteBtn();
  renderHistoryList();
}

// 削除ボタン表示制御
function updateDeleteBtn(){
  const btn=document.getElementById('history-del-btn');
  if(btn) btn.style.display=selectedTxIds.size>0?'inline-flex':'none';
}

// 選択した取引を削除
function deleteSelectedTx(){
  if(!selectedTxIds.size){sfxError();return;}
  const count=selectedTxIds.size;
  showModal('🗑 取引を削除',
    `選択した <b>${count}件</b> の取引を削除します。<br>この操作は取り消せません。`,
    '',
    [
      {label:'キャンセル',cls:'mbtn-cancel',action:closeModal},
      {label:'削除する',cls:'mbtn-ok',action:()=>{
        const txidsToDelete=new Set(selectedTxIds);
        sales=sales.filter(r=>!txidsToDelete.has(r.txid));
        localStorage.setItem('mise_sales',JSON.stringify(sales));
        selectedTxIds.clear();
        expandedTxIds.clear();
        updateDeleteBtn();
        closeModal();
        sfxCancel();
        showToast(`🗑 ${count}件の取引を削除しました`);
        renderHistoryList();
        if(csvDirHandle) autoSaveSettingToFolder().catch(()=>{});
      }},
    ]
  );
}

// 表示中の日の決済リストをCSVに保存(日別ファイル名)
async function exportHistoryCSV(){
  const dateStr = historyViewDate || todayStr();
  const dayRows = sales.filter(r=>r.date===dateStr);
  if(!dayRows.length){ sfxError(); showToast('⚠️ この日の売上データがありません'); return; }

  if(csvDirHandle){
    // フォルダへ書き込み
    const ok = await saveDailyCSVToFolder(dateStr);
    if(ok){
      sfxSave();
      showToast(`📥 ${dateStr} のCSV(${dayRows.length}件)をフォルダに保存しました`);
      return;
    } else {
      showToast('⚠️ フォルダへの書き込みに失敗。ダウンロードします');
    }
  }
  // フォルダ未選択 or 書き込み失敗 → ダウンロード
  const headers=['txid','datetime','date','hour','dow','month','product_id','product','category','price','quantity','amount','tax','discount_pct','customer_type','age_band','note'];
  const csv=[headers.join(','),...dayRows.map(r=>headers.map(h=>{
    const v=r[h]!==undefined?r[h]:'';
    const sv=String(v);
    return sv.includes(',')||sv.includes('"')||sv.includes('\n')?`"${sv.replace(/"/g,'""')}"`:sv;
  }).join(','))].join('\n');
  const blob=new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8'});
  const a=document.createElement('a');
  a.href=URL.createObjectURL(blob);
  a.download='MISE_sales_'+dateStr+'.csv';
  a.click();
  setTimeout(()=>URL.revokeObjectURL(a.href),5000);
  sfxSave();
  showToast(`📥 ${dateStr} のCSV(${dayRows.length}件)をダウンロードしました`);
}

// CSVを読み込んで売上データに追加(重複txidは上書き)
function importHistoryCSV(input){
  const file=input.files[0];
  if(!file){return;}
  const reader=new FileReader();
  reader.onload=e=>{
    try{
      const text=e.target.result.replace(/^\uFEFF/,''); // BOM除去
      const lines=text.split(/\r?\n/).filter(l=>l.trim());
      if(lines.length<2){sfxError();showToast('⚠️ 有効なデータがありません');return;}
      const headers=splitCSVLine(lines[0]);
      const required=['txid','datetime','date','amount'];
      const missing=required.filter(h=>!headers.includes(h));
      if(missing.length){sfxError();showToast('⚠️ 列が不足: '+missing.join(','));return;}
      const newRows=[];
      for(let i=1;i<lines.length;i++){
        const cells=splitCSVLine(lines[i]);
        if(cells.length<2) continue;
        const row={};
        headers.forEach((h,idx)=>{row[h]=cells[idx]!==undefined?cells[idx]:''});
        // 数値変換
        ['hour','dow','month','price','quantity','amount','tax','discount_pct'].forEach(k=>{
          if(row[k]!==undefined&&row[k]!==''){const n=parseFloat(row[k]);if(!isNaN(n))row[k]=n;}
        });
        if(!row.txid||!row.date) continue;
        newRows.push(row);
      }
      if(!newRows.length){sfxError();showToast('⚠️ 有効な行がありませんでした');return;}
      // 読み込んだCSVに含まれるtxidを全て既存から削除してから追加(上書き)
      const importTxIds=new Set(newRows.map(r=>r.txid));
      sales=sales.filter(r=>!importTxIds.has(r.txid));
      sales.push(...newRows);
      // 日付・時刻でソート
      sales.sort((a,b)=>(a.datetime||a.date||'').localeCompare(b.datetime||b.date||''));
      localStorage.setItem('mise_sales',JSON.stringify(sales));
      sfxSave();
      showToast(`📂 ${newRows.length}件を読み込みました(${importTxIds.size}取引)`);
      if(newRows[0]&&newRows[0].date){
        historyViewDate=newRows[0].date;
        document.getElementById('history-date').value=historyViewDate;
      }
      renderHistoryList();
      if(csvDirHandle) autoSaveSettingToFolder().catch(()=>{});
    }catch(err){
      sfxError();
      showToast('⚠️ 読込エラー: '+err.message);
    }
    // inputリセット(同じファイルを再選択できるように)
    input.value='';
  };
  reader.readAsText(file,'UTF-8');
}

// CSVの1行をパース(ダブルクォート対応)
function splitCSVLine(line){
  const res=[];let cur='',inQ=false;
  for(let i=0;i<line.length;i++){
    const c=line[i];
    if(c==='"'){
      if(inQ&&line[i+1]==='"'){cur+='"';i++;}
      else{inQ=!inQ;}
    } else if(c===','&&!inQ){
      res.push(cur);cur='';
    } else {
      cur+=c;
    }
  }
  res.push(cur);
  return res;
}

// ═══════════════════════════════════════════════════════
// ─── File System Access API ──────────────────────────
// ═══════════════════════════════════════════════════════

// フォルダ選択(設定から呼び出し)
async function selectCsvFolder(){
  if(!('showDirectoryPicker' in window)){
    showToast('⚠️ このブラウザはFile System Access APIに対応していません');
    return;
  }
  try{
    const handle = await window.showDirectoryPicker({mode:'readwrite'});
    csvDirHandle = handle;
    await saveHandle(handle); // IndexedDBに保存して永続化
    localStorage.setItem('mise_csv_folder_name', handle.name);
    showToast('📂 フォルダを設定しました: ' + handle.name);
    sfxSave();
    // 設定モーダル内の表示を更新
    const disp = document.getElementById('set-folder-display');
    if(disp){
      disp.textContent = '📂 ' + handle.name;
      disp.style.color = 'var(--mint)';
    }
    // setting.txtを新フォルダに即保存
    await autoSaveSettingToFolder();
    hideFolderBanner();
  }catch(e){
    if(e.name !== 'AbortError'){
      sfxError();
      showToast('⚠️ フォルダ選択エラー: ' + e.message);
    }
  }
}

// フォルダへのファイル書き込みユーティリティ
// @returns {boolean} 成功したかどうか
async function writeToDirHandle(dirHandle, fileName, textContent){
  try{
    const fileHandle = await dirHandle.getFileHandle(fileName, {create:true});
    const writable = await fileHandle.createWritable();
    await writable.write(textContent);
    await writable.close();
    return true;
  }catch(e){
    console.error('writeToDirHandle error:', e);
    return false;
  }
}

// フォルダからファイルを読み込むユーティリティ
// @returns {string|null} ファイルの内容(エラー時はnull)
async function readFromDirHandle(dirHandle, fileName){
  try{
    const fileHandle = await dirHandle.getFileHandle(fileName, {create:false});
    const file = await fileHandle.getFile();
    return await file.text();
  }catch(e){
    if(e.name === 'NotFoundError') return null; // ファイルが無い
    console.error('readFromDirHandle error:', e);
    return null;
  }
}

// setting.txt をフォルダに自動保存(サイレント)
async function autoSaveSettingToFolder(){
  if(!csvDirHandle) return;
  try{
    const settings = collectSettings();
    const json = JSON.stringify(settings, null, 2);
    const ok = await writeToDirHandle(csvDirHandle, 'setting.txt', json);
    if(!ok) console.warn('setting.txt の自動保存に失敗しました');
  }catch(e){
    console.warn('autoSaveSettingToFolder:', e);
  }
}

// 日別CSVをフォルダに書き込む(決済後・CSV保存ボタン押下時に呼ぶ)
// @param {string} dateStr YYYY-MM-DD
async function saveDailyCSVToFolder(dateStr){
  if(!csvDirHandle) return false;
  const dayRows = sales.filter(r=>r.date===dateStr);
  if(!dayRows.length) return false;
  const headers=['txid','datetime','date','hour','dow','month','product_id','product','category','price','quantity','amount','tax','discount_pct','customer_type','age_band','note'];
  const csv=[headers.join(','),...dayRows.map(r=>headers.map(h=>{
    const v=r[h]!==undefined?r[h]:'';
    const sv=String(v);
    return sv.includes(',')||sv.includes('"')||sv.includes('\n')?`"${sv.replace(/"/g,'""')}"`:sv;
  }).join(','))].join('\n');
  const fileName = 'MISE_sales_' + dateStr + '.csv';
  const ok = await writeToDirHandle(csvDirHandle, fileName, '\uFEFF' + csv);
  return ok;
}

// 起動時に前回フォルダからsetting.txtを読み込む案内バナーを表示
function showFolderBanner(folderName){
  // 既存バナーがあれば削除
  const old = document.getElementById('folder-banner');
  if(old) old.remove();
  const banner = document.createElement('div');
  banner.id = 'folder-banner';
  banner.style.cssText = 'background:linear-gradient(90deg,var(--sky),#3a9de0);color:#fff;padding:8px 16px;font-size:13px;font-weight:700;z-index:700;display:flex;align-items:center;gap:10px;box-shadow:0 3px 12px rgba(0,0,0,.15);flex-shrink:0;';
  banner.innerHTML = `
    <span>📂 前回のフォルダ「<b>${escHtml(folderName)}</b>」を再接続すると全データを自動読み込みします</span>
    <button onclick="reconnectFolder()" style="border:none;background:rgba(255,255,255,.25);color:#fff;border-radius:8px;padding:4px 12px;font-size:12px;font-weight:800;cursor:pointer;font-family:inherit;">📂 フォルダを再接続</button>
    <button onclick="hideFolderBanner()" style="border:none;background:none;color:rgba(255,255,255,.7);font-size:18px;cursor:pointer;margin-left:auto;line-height:1;">✕</button>
  `;
  document.body.insertBefore(banner, document.body.firstChild);
}

function hideFolderBanner(){
  const b = document.getElementById('folder-banner');
  if(b) b.remove();
}

// フォルダを再接続(バナーのボタンから呼ぶ)
async function reconnectFolder(){
  const storedHandle = await loadHandle().catch(()=>null);
  if(storedHandle){
    try {
      // ユーザー操作のタイミングで権限を要求
      if((await storedHandle.requestPermission({mode:'readwrite'})) === 'granted'){
        csvDirHandle = storedHandle;
        localStorage.setItem('mise_csv_folder_name', csvDirHandle.name);
        hideFolderBanner();
        await loadSettingFromFolder();
        showToast('📂 フォルダから設定と売上データを読み込みました');
        return;
      }
    } catch(e) {
      console.warn('Permission request failed:', e);
    }
  }
  // 保存されたハンドルが無い、または権限要求に失敗した場合は再選択させる
  await selectCsvFolder();
  if(csvDirHandle){
    await loadSettingFromFolder();
  }
}

// フォルダから setting.txt を読み込んで設定を反映
async function loadSettingFromFolder(){
  if(!csvDirHandle) return;
  const text = await readFromDirHandle(csvDirHandle, 'setting.txt');
  if(!text){ return; } // ファイルなし=初回利用
  try{
    const settings = JSON.parse(text);
    applySettings(settings);
    showToast('📂 データをフォルダから自動読み込みしました');
  }catch(e){
    console.warn('loadSettingFromFolder parse error:', e);
  }
}

// ═══════════════════════════════════════════════════════
// ─── setting.txt 保存・読み込み ──────────────────────────
// ═══════════════════════════════════════════════════════
function collectSettings(){
  return {
    version: 2,
    storename: localStorage.getItem('mise_storename')||'',
    csv_folder_name: csvDirHandle ? csvDirHandle.name : (localStorage.getItem('mise_csv_folder_name')||''),
    txcounter: txCounter,
    products: products,
    sales: sales, // 追加: 売上データを含める
    saved_at: new Date().toISOString(),
  };
}

// 設定オブジェクトをアプリに反映(localStorageにも保存)
function applySettings(settings){
  if(!settings||typeof settings!=='object') return;
  if(settings.storename !== undefined){
    localStorage.setItem('mise_storename', settings.storename);
    const storenameEl = document.getElementById('set-storename');
    if(storenameEl) storenameEl.value = settings.storename;
  }
  if(settings.csv_folder_name !== undefined && settings.csv_folder_name){
    localStorage.setItem('mise_csv_folder_name', settings.csv_folder_name);
  }
  if(settings.txcounter !== undefined){
    const tc = parseInt(settings.txcounter);
    if(!isNaN(tc) && tc > 0){
      txCounter = tc;
      localStorage.setItem('mise_txcounter', String(txCounter));
      updateTxLabel();
    }
  }
  if(Array.isArray(settings.products) && settings.products.length > 0){
    products = settings.products;
    localStorage.setItem('mise_products', JSON.stringify(products));
    renderCatBar();
    renderProducts();
  }
  // 追加: 売上データの復元
  if(Array.isArray(settings.sales)){
    sales = settings.sales;
    localStorage.setItem('mise_sales', JSON.stringify(sales));
    if(document.getElementById('panel-history').style.display!=='none'){
      renderHistoryList();
    }
  }
}

// localStorageに保存された前回の設定を起動時に反映
function applyStoredSettings(){
  // txcounterはすでにlet宣言時に読み込み済みだが念のため再確認
  const tc = parseInt(localStorage.getItem('mise_txcounter')||'1');
  if(!isNaN(tc) && tc >= 1){ txCounter = tc; updateTxLabel(); }
  // productsやsalesはすでに読み込み済み
}

// 手動で setting.txt を保存(設定画面から呼び出し)
async function saveSettingTxtManual(){
  const settings = collectSettings();
  const json = JSON.stringify(settings, null, 2);

  if(csvDirHandle){
    // フォルダが選択されていればフォルダへ書き込み
    const ok = await writeToDirHandle(csvDirHandle, 'setting.txt', json);
    if(ok){
      sfxSave();
      showToast('💾 データをフォルダにバックアップしました');
      return;
    } else {
      showToast('⚠️ フォルダへの書き込みに失敗。ダウンロードします');
    }
  }
  // フォルダ未選択またはフォルダ書き込み失敗 → ダウンロード
  const blob = new Blob([json], {type:'application/json;charset=utf-8'});
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'setting.txt';
  a.click();
  setTimeout(()=>URL.revokeObjectURL(a.href), 5000);
  sfxSave();
  showToast('💾 データをダウンロードしました');
}

function loadSettingTxt(input){
  const file = input.files[0];
  if(!file){ return; }
  const reader = new FileReader();
  reader.onload = e => {
    try{
      const settings = JSON.parse(e.target.result);
      if(!settings||typeof settings!=='object'){ throw new Error('不正なフォーマット'); }
      applySettings(settings);
      sfxSave();
      showToast('📂 データを復元しました');
      closeModal();
      setTimeout(openSettings, 200);
    }catch(err){
      sfxError();
      showToast('⚠️ 読込エラー: ' + err.message);
    }
    input.value = '';
  };
  reader.readAsText(file, 'UTF-8');
}

// ─── ページ離脱時: setting.txt をフォルダへ自動保存 ─────────
window.addEventListener('beforeunload', ()=>{
  // localStorage に最終設定を保存(同期)
  localStorage.setItem('mise_txcounter', String(txCounter));
  localStorage.setItem('mise_last_saved', new Date().toISOString());
  // フォルダが選択されている場合は非同期で試みる(保証はできないがChrome系では動作)
  if(csvDirHandle){
    autoSaveSettingToFolder().catch(()=>{});
  }
});

// 30秒ごとに自動保存(フォルダが設定されている場合)
setInterval(async ()=>{
  if(csvDirHandle){
    await autoSaveSettingToFolder();
    // 今日の日別CSVも更新
    if(sales.filter(r=>r.date===todayStr()).length > 0){
      await saveDailyCSVToFolder(todayStr());
    }
  }
}, 30000);


function showModal(title, body, extra, buttons){
  document.getElementById('modal-title').innerHTML=title;
  document.getElementById('modal-body').innerHTML=body+extra;
  document.getElementById('modal-btns').innerHTML=buttons.map((b,i)=>`
    <button class="${b.cls}" id="mbtn-${i}">${b.label}</button>`).join('');
  buttons.forEach((b,i)=>document.getElementById('mbtn-'+i).onclick=b.action);
  document.getElementById('modal').classList.remove('hidden');
}
function closeModal(){document.getElementById('modal').classList.add('hidden');}
document.getElementById('modal').addEventListener('click',e=>{
  if(e.target===document.getElementById('modal')) closeModal();
});

// ─── トースト ─────────────────────────────────────────────
let toastTimer;
function showToast(msg){
  const t=document.getElementById('toast');
  t.textContent=msg;
  t.classList.add('show');
  clearTimeout(toastTimer);
  toastTimer=setTimeout(()=>t.classList.remove('show'),2200);
}

// ─── 起動 ─────────────────────────────────────────────────
init();
</script>
</body>
</html>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
小さなお店用のPOSシステム「MisePOS」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1