/** * 口座の利用履歴の表(マイページ `mypage/index.html` と 学園祭ゲストページ `guest/` で共用) * * データは api の `history`(→ GakuensaiAccount::history)。 * - 購入は「店名/商品」、入出金は種別(チャージ・返金・カード合算)を出す * - ★ 増える向きかどうかはサーバーが決めた `direction` をそのまま使う(画面で再計算しない)。 * 合算は同じ1レコードが、送金元では「送金」、送金先では「受取」になる * - 取消は消さずに取り消し線で残す * - スマホのゲストは、行に `device` / `this_device` があれば「この端末/ほかの端末」を出す * * - 本部の「商品の返金」があれば、表の上に店・商品・理由のお知らせを出す(→ gsRenderRefundNotices) * * @param {HTMLTableElement} table 3列(時刻・内容・金額)の表 * @param {Array} history * @param {{guest?: boolean}} [options] guest = 前払い(ゲスト)。お知らせの文言(残高に戻す/引落から差し引く)に使う */ function gsRenderAccountHistory(table, history, options) { gsRenderRefundNotices(table, history, !!(options && options.guest)); const yen = (n) => Number(n || 0).toLocaleString('ja-JP'); const KIND_LABEL = { charge: 'チャージ', refund: '返金', adjust: '調整' }; const kindLabel = (p) => p.kind === 'transfer' ? (p.direction === 'in' ? 'カード合算(受取)' : 'カード合算(送金)') : (KIND_LABEL[p.kind] || p.kind); const td = (tr, text, className) => { const cell = document.createElement('td'); if (className) cell.className = className; cell.textContent = text; tr.appendChild(cell); return cell; }; const tbody = table.querySelector('tbody'); tbody.innerHTML = ''; history.forEach((p) => { const tr = document.createElement('tr'); const voided = p.status === 'voided'; if (voided) tr.classList.add('voided'); td(tr, String(p.paid_at).slice(11, 16)); const where = document.createElement('td'); const head = document.createElement('div'); head.className = 'shop'; const sub = document.createElement('div'); sub.className = 'detail'; if (p.kind === 'purchase') { head.textContent = p.shop_name || p.group_name || ''; sub.textContent = (p.items || []).map((l) => l.item_name + '×' + l.count).join('、') || '商品代'; } else { head.textContent = kindLabel(p); // 合算は相手のカード番号も出す(どちらの画面でも対応が追えるように) if (p.other_party) sub.textContent = (p.direction === 'in' ? '送金元 ' : '送金先 ') + p.other_party; } where.appendChild(head); if (sub.textContent) where.appendChild(sub); // スマホのゲストだけ:どの端末のQRで行ったか(api/guest.php が付ける。マイページには来ない) if (p.device) { const via = document.createElement('div'); via.className = 'via' + (p.this_device ? ' this' : ''); via.textContent = p.this_device ? 'この端末' : 'ほかの端末(' + p.device + ')'; where.appendChild(via); } tr.appendChild(where); const plus = p.direction === 'in'; const amount = td(tr, (plus ? '+' : '') + yen(p.total) + '円', 'num' + (plus ? ' plus' : '')); if (voided) { const note = document.createElement('span'); note.className = 'void-note'; note.textContent = '取消'; amount.appendChild(note); } tbody.appendChild(tr); }); if (!history.length) { const tr = document.createElement('tr'); const cell = document.createElement('td'); cell.colSpan = 3; cell.className = 'empty'; cell.textContent = 'まだ利用がありません'; tr.appendChild(cell); tbody.appendChild(tr); } } /** * 商品の返金のお知らせ(履歴の表の上)。データは history の行の `item_refund`(店・商品・理由。→ GakuensaiAccount::history)。 * 取り消された返金は出さない。お知らせの箱は表の入れ物(.table-wrap)の直前に1つだけ作って使い回す。 */ function gsRenderRefundNotices(table, history, guest) { const yen = (n) => Number(n || 0).toLocaleString('ja-JP'); const anchor = table.closest('.table-wrap') || table; let box = anchor.previousElementSibling; if (!box || !box.classList.contains('refund-notices')) { box = document.createElement('div'); box.className = 'refund-notices'; anchor.parentNode.insertBefore(box, anchor); } box.innerHTML = ''; const refunds = (history || []).filter((p) => p.item_refund && p.status !== 'voided'); box.classList.toggle('hidden', !refunds.length); refunds.forEach((p) => { const r = p.item_refund; const item = document.createElement('div'); item.className = 'refund-notice'; const head = document.createElement('p'); head.className = 'refund-notice-head'; head.textContent = '返金のお知らせ'; const body = document.createElement('p'); const what = r.items.map((l) => '「' + l.item_name + '」' + l.count + '個').join('、'); body.textContent = (r.shop || 'お店') + 'の' + what + 'の代金 ' + yen(p.total) + '円を返金しました。' + (guest ? '残高に戻しています。' : 'ご利用額から差し引いています。'); item.append(head, body); if (r.reason) { const reason = document.createElement('p'); reason.className = 'refund-notice-reason'; reason.textContent = '理由:' + r.reason; item.appendChild(reason); } // 固定の案内(依頼元の指定。返金の理由によらず出す) const ask = document.createElement('p'); ask.className = 'refund-notice-ask'; ask.textContent = 'まだ召し上がっていない方は念のため廃棄をしていただくようお願いします。'; item.appendChild(ask); box.appendChild(item); }); }