feat: 워크스페이스 탭 자동복원 + 주문 입금확인 토글 + 메뉴 개명(전화접수→주문접수)
종량제3 세션 작업. - 워크스페이스 탭 계정별 저장·자동복원(member_workspace_tabs 자동생성, pref/workspace-tabs) - 주문접수 관리 입금확인(so_paid) 수동 토글(phoneOrderMarkPaid, order/phone/manage/paid) - 메뉴 '전화 접수 관리' → '주문접수관리' 개명(피드백 #35) + 시드/개명 SQL Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -788,6 +788,16 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 박스수량 입력 중 Enter → 다음 봉투의 박스수량 입력칸으로 이동(#39)
|
||||
selectedBody.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Enter' || !event.target.classList.contains('item-qty-box')) return;
|
||||
event.preventDefault();
|
||||
const inputs = Array.from(selectedBody.querySelectorAll('.item-qty-box'));
|
||||
const next = inputs[inputs.indexOf(event.target) + 1];
|
||||
if (next) { next.focus(); next.select(); }
|
||||
else { event.target.blur(); }
|
||||
});
|
||||
|
||||
referenceRows.forEach((row) => {
|
||||
row.addEventListener('click', (event) => {
|
||||
const button = event.target.closest('.js-toggle-bag');
|
||||
|
||||
@@ -191,18 +191,33 @@ if ($effectiveLgIdx) {
|
||||
|
||||
var STORE_KEY = 'jrj_ws_tabs';
|
||||
var WS_OWNER = '<?= (string) (session()->get('mb_idx') ?? '') ?>'; // 탭 저장 소유자(로그인 사용자) 식별
|
||||
var WS_SAVE_URL = '<?= site_url('bag/pref/workspace-tabs') ?>';
|
||||
var WS_CSRF_NAME = '<?= csrf_token() ?>';
|
||||
var wsCsrfHash = '<?= csrf_hash() ?>';
|
||||
var wsSaveTimer = null;
|
||||
function wsStateObj() {
|
||||
return {
|
||||
owner: WS_OWNER,
|
||||
tabs: order.map(function (id) { return { url: tabs[id].url, title: tabs[id].title }; }),
|
||||
layout: layout,
|
||||
focused: focused,
|
||||
vRatio: vRatio,
|
||||
hRatio: hRatio,
|
||||
slots: slots.map(function (id) { return (id && tabs[id]) ? tabs[id].url : null; })
|
||||
};
|
||||
}
|
||||
function persist() {
|
||||
try {
|
||||
sessionStorage.setItem(STORE_KEY, JSON.stringify({
|
||||
owner: WS_OWNER,
|
||||
tabs: order.map(function (id) { return { url: tabs[id].url, title: tabs[id].title }; }),
|
||||
layout: layout,
|
||||
focused: focused,
|
||||
vRatio: vRatio,
|
||||
hRatio: hRatio,
|
||||
slots: slots.map(function (id) { return (id && tabs[id]) ? tabs[id].url : null; })
|
||||
}));
|
||||
} catch (e) {}
|
||||
var obj = wsStateObj();
|
||||
var json = JSON.stringify(obj);
|
||||
try { sessionStorage.setItem(STORE_KEY, json); } catch (e) {}
|
||||
// 계정(DB)에 저장 → 다음 로그인/다른 기기에서도 자동 복원 (연속 변경은 디바운스)
|
||||
clearTimeout(wsSaveTimer);
|
||||
wsSaveTimer = setTimeout(function () {
|
||||
var b = new URLSearchParams();
|
||||
b.set(WS_CSRF_NAME, wsCsrfHash); b.set('state', json);
|
||||
fetch(WS_SAVE_URL, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: b.toString() })
|
||||
.then(function (r) { return r.json(); }).then(function (d) { if (d && d.csrf) wsCsrfHash = d.csrf; }).catch(function () {});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 분할 칸 헤더·빈칸 안내 요소 4개 미리 생성
|
||||
@@ -560,11 +575,19 @@ if ($effectiveLgIdx) {
|
||||
});
|
||||
});
|
||||
|
||||
// 첫 화면: 세션 저장 복원(레이아웃·분할 배치 포함), 없으면 대시보드 1분할
|
||||
// 첫 화면: ① 계정(DB) 저장 자동복원 → ② 세션 저장 → ③ 없으면 대시보드 1분할
|
||||
var SERVER_STATE = <?= $savedWorkspaceJson ?? 'null' ?>;
|
||||
(function restore() {
|
||||
var saved = null;
|
||||
try { saved = JSON.parse(sessionStorage.getItem(STORE_KEY) || 'null'); } catch (e) {}
|
||||
if (saved && saved.owner !== WS_OWNER) { try { sessionStorage.removeItem(STORE_KEY); } catch (e) {} saved = null; }
|
||||
// ① 계정 DB에 저장된 상태 우선(다음 로그인·다른 기기에서도 복원)
|
||||
if (SERVER_STATE && SERVER_STATE.tabs && SERVER_STATE.tabs.length) {
|
||||
saved = SERVER_STATE;
|
||||
}
|
||||
// ② 없으면 이 브라우저 세션 저장 사용
|
||||
if (!saved) {
|
||||
try { saved = JSON.parse(sessionStorage.getItem(STORE_KEY) || 'null'); } catch (e) {}
|
||||
if (saved && saved.owner !== WS_OWNER) { try { sessionStorage.removeItem(STORE_KEY); } catch (e) {} saved = null; }
|
||||
}
|
||||
if (saved && saved.tabs && saved.tabs.length) {
|
||||
saved.tabs.forEach(function (t) { if (t && t.url) openTab(t.url, t.title, { noFocus: true }); });
|
||||
layout = (saved.layout === 'lr' || saved.layout === 'tb' || saved.layout === 'quad') ? saved.layout : 'single';
|
||||
|
||||
@@ -187,7 +187,7 @@ unset($g);
|
||||
<th class="text-right">금액</th>
|
||||
<th class="text-right">수량</th>
|
||||
<th class="text-right">금액</th>
|
||||
<th class="text-right">판매수량</th>
|
||||
<th class="text-right">판매수량(팩)</th>
|
||||
<th class="text-right">금액</th>
|
||||
<th class="text-center">B</th>
|
||||
<th class="text-center">P</th>
|
||||
@@ -212,7 +212,7 @@ unset($g);
|
||||
<td class="text-right px-2"><?= number_format($r['box']) ?></td>
|
||||
<td class="text-right px-2"><?= number_format($boxPrice) ?></td>
|
||||
<td class="text-right px-2"><?= number_format($r['price']) ?></td>
|
||||
<td><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-qty-input" name="item_qty[]" type="number" min="0" value="0"/></td>
|
||||
<td><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-qty-input item-pack-input" type="number" min="0" value="0"/><input type="hidden" name="item_qty[]" class="item-qty-hidden" value="0"/></td>
|
||||
<td class="text-right px-2 item-amount-cell">0</td>
|
||||
<td class="text-center item-box-cell">0</td>
|
||||
<td class="text-center item-pack-cell">0</td>
|
||||
@@ -330,12 +330,18 @@ unset($g);
|
||||
}
|
||||
|
||||
function calcRow(row) {
|
||||
const qtyInput = row.querySelector('.item-qty-input');
|
||||
const qty = parseInt(qtyInput.value || '0', 10) || 0;
|
||||
const qtyInput = row.querySelector('.item-pack-input');
|
||||
const hiddenQty = row.querySelector('.item-qty-hidden');
|
||||
const packQty = parseInt(qtyInput.value || '0', 10) || 0; // 입력값 = 팩 수
|
||||
const unitPrice = parseInt(row.dataset.unitPrice || '0', 10) || 0;
|
||||
const boxSheets = parseInt(row.dataset.boxSheets || '0', 10) || 0;
|
||||
const packSheets = parseInt(row.dataset.packSheets || '0', 10) || 0;
|
||||
|
||||
// 팩당 낱장 수(팩 매수 미설정 봉투는 1:1로 처리)
|
||||
const mult = packSheets > 0 ? packSheets : 1;
|
||||
const qty = packQty * mult; // 실제 낱장 수 → 서버로 전송
|
||||
if (hiddenQty) hiddenQty.value = String(qty);
|
||||
|
||||
let box = 0;
|
||||
let pack = 0;
|
||||
let sheet = qty;
|
||||
@@ -359,14 +365,14 @@ unset($g);
|
||||
row.querySelector('.item-pack-cell').textContent = pack ? nf(pack) : '';
|
||||
row.querySelector('.item-sheet-cell').textContent = sheet ? nf(sheet) : '';
|
||||
|
||||
return { qty, amount, box, pack, sheet };
|
||||
return { packQty, amount, box, pack, sheet };
|
||||
}
|
||||
|
||||
function recalcAllRows() {
|
||||
let sumQty = 0, sumAmount = 0, sumBox = 0, sumPack = 0, sumSheet = 0;
|
||||
document.querySelectorAll('.order-row').forEach((row) => {
|
||||
const r = calcRow(row);
|
||||
sumQty += r.qty;
|
||||
sumQty += r.packQty;
|
||||
sumAmount += r.amount;
|
||||
sumBox += r.box;
|
||||
sumPack += r.pack;
|
||||
|
||||
@@ -69,12 +69,14 @@
|
||||
<input type="date" lang="ko" name="so_delivery_date" id="detail-delivery-date-input" class="border border-gray-300 rounded px-2 py-0.5 text-sm"/>
|
||||
</div>
|
||||
<div><span class="font-semibold text-gray-700 inline-block w-20">상태</span> <span id="detail-status">-</span></div>
|
||||
<div><span class="font-semibold text-gray-700 inline-block w-20">입금상태</span> <span id="detail-paid-status" class="font-semibold">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 px-1">
|
||||
<span class="text-sm font-semibold text-gray-700">접수 품목 내역</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" id="btn-toggle-paid" class="border border-emerald-300 text-emerald-700 px-3 py-1 rounded-sm text-sm hover:bg-emerald-50" disabled>입금확인</button>
|
||||
<button type="submit" id="btn-save" class="bg-btn-search text-white px-3 py-1 rounded-sm text-sm shadow hover:opacity-90" disabled>주문 수정 저장</button>
|
||||
<button type="button" id="btn-receipt" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50" disabled>입금자 영수증 출력</button>
|
||||
<button type="submit" id="btn-cancel-order" form="order-cancel-form" class="border border-red-300 text-red-600 px-3 py-1 rounded-sm text-sm hover:bg-red-50" disabled>주문 취소</button>
|
||||
@@ -238,6 +240,8 @@
|
||||
const btnSave = document.getElementById('btn-save');
|
||||
const btnCancel = document.getElementById('btn-cancel-order');
|
||||
const btnReceipt = document.getElementById('btn-receipt');
|
||||
const btnTogglePaid = document.getElementById('btn-toggle-paid');
|
||||
const paidApi = new URL('<?= base_url('bag/order/phone/manage/paid') ?>', window.location.href).pathname;
|
||||
const receiptBase = '<?= base_url('bag/order/phone/receipt') ?>';
|
||||
btnReceipt?.addEventListener('click', () => {
|
||||
const id = inputSoIdx.value;
|
||||
@@ -252,6 +256,43 @@
|
||||
|
||||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||||
|
||||
function paintPaidButton(order) {
|
||||
if (!btnTogglePaid) return;
|
||||
const paid = order && Number(order.so_paid) === 1;
|
||||
btnTogglePaid.textContent = paid ? '입금확인 취소' : '입금확인';
|
||||
btnTogglePaid.className = paid
|
||||
? 'border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50'
|
||||
: 'border border-emerald-300 text-emerald-700 px-3 py-1 rounded-sm text-sm hover:bg-emerald-50';
|
||||
}
|
||||
|
||||
btnTogglePaid?.addEventListener('click', async () => {
|
||||
const id = inputSoIdx.value;
|
||||
if (!id) return;
|
||||
const o = orderMap.get(String(id));
|
||||
if (!o) return;
|
||||
const nextPaid = Number(o.so_paid) === 1 ? 0 : 1;
|
||||
if (nextPaid === 0 && !confirm('입금확인을 취소하면 이 주문은 포장(스캔)이 차단됩니다.\n계속할까요?')) return;
|
||||
const body = new URLSearchParams();
|
||||
body.set(csrfName, csrfHash);
|
||||
body.set('so_idx', String(id));
|
||||
body.set('paid', String(nextPaid));
|
||||
btnTogglePaid.disabled = true;
|
||||
try {
|
||||
const res = await fetch(paidApi, { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body });
|
||||
const data = await res.json();
|
||||
if (data && data.csrf) csrfHash = data.csrf;
|
||||
if (!data || !data.ok) { alert((data && data.message) || '처리에 실패했습니다.'); btnTogglePaid.disabled = false; return; }
|
||||
o.so_paid = nextPaid;
|
||||
setHeader(o);
|
||||
paintPaidButton(o);
|
||||
btnTogglePaid.disabled = false;
|
||||
renderList(); // 리스트의 입금 표시 갱신
|
||||
} catch (e) {
|
||||
alert('네트워크 오류로 처리하지 못했습니다.');
|
||||
btnTogglePaid.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function setHeader(order) {
|
||||
var t = function (id, v) { document.getElementById(id).textContent = (v === undefined || v === null || v === '') ? '-' : v; };
|
||||
t('detail-so-no', order ? displayNo(order) : '');
|
||||
@@ -263,6 +304,12 @@
|
||||
t('detail-shop-addr', order ? order.ds_addr : '');
|
||||
t('detail-order-date', order ? order.so_order_date : '');
|
||||
t('detail-status', order ? ((order.so_status === 'cancelled') ? '취소' : '정상') : '');
|
||||
var ps = document.getElementById('detail-paid-status');
|
||||
if (ps) {
|
||||
var paid = order && Number(order.so_paid) === 1;
|
||||
ps.textContent = order ? (paid ? '입금완료' : '미입금') : '-';
|
||||
ps.style.color = order ? (paid ? '#059669' : '#e11d48') : '';
|
||||
}
|
||||
// 배달일 — 수정 가능한 date 입력
|
||||
var dd = document.getElementById('detail-delivery-date-input');
|
||||
if (dd) {
|
||||
@@ -346,6 +393,7 @@
|
||||
btnSave.disabled = isCancelled;
|
||||
btnCancel.disabled = isCancelled;
|
||||
if (btnReceipt) btnReceipt.disabled = false;
|
||||
if (btnTogglePaid) { btnTogglePaid.disabled = isCancelled; paintPaidButton(order); }
|
||||
|
||||
renderPacking(order); // 포장 명세 탭 동기화
|
||||
|
||||
|
||||
Reference in New Issue
Block a user