387 lines
16 KiB
PHP
387 lines
16 KiB
PHP
<?php
|
|
/* =====================================================================
|
|
* /lib/customer_access_time_popup.php
|
|
* - Customer Access Time (weekly pickup-availability windows) editor popup
|
|
* - Opened as a modal (iframe) from customer_detail.php; reusable on other screens
|
|
* - On successful save, notifies parent via window.parent.closeCustomerPopup(true)
|
|
* - Save = "full replace": delete the customer's active rows, re-insert current input
|
|
* - Time is chosen via hour/minute selects (minute step = 5) so granularity is
|
|
* guaranteed regardless of the browser's native time picker
|
|
* ===================================================================== */
|
|
|
|
include getenv("DOCUMENT_ROOT")."/include/session_include.php";
|
|
require_once getenv("DOCUMENT_ROOT")."/assets/dbCon.php";
|
|
|
|
$c_uid = isset($_REQUEST['c_uid']) ? intval($_REQUEST['c_uid']) : 0;
|
|
$goStr = $_GET['goStr'] ?? '';
|
|
$user = $_SESSION['ss_INITIAL'] ?? ($_SESSION['ss_UID'] ?? 'system');
|
|
|
|
// mbstring 미설치 환경 대비: UTF-8 문자 기준 N자로 자르기
|
|
if (!function_exists('utf8_cut')) {
|
|
function utf8_cut($s, $n) {
|
|
if (function_exists('mb_substr')) return mb_substr($s, 0, $n);
|
|
preg_match_all('/./us', (string)$s, $mm);
|
|
return (count($mm[0]) <= $n) ? $s : implode('', array_slice($mm[0], 0, $n));
|
|
}
|
|
}
|
|
|
|
/* =====================================================================
|
|
* AJAX save
|
|
* ===================================================================== */
|
|
if (($_POST['action'] ?? '') === 'save') {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
if ($c_uid <= 0) { echo json_encode(['ok' => false, 'msg' => 'invalid c_uid']); exit; }
|
|
|
|
$rows = json_decode($_POST['data'] ?? '[]', true);
|
|
if (!is_array($rows)) $rows = [];
|
|
|
|
// validate + clean
|
|
$clean = [];
|
|
foreach ($rows as $r) {
|
|
$dow = isset($r['dow']) ? intval($r['dow']) : -1;
|
|
if ($dow < 0 || $dow > 6) continue;
|
|
|
|
$open = trim($r['open'] ?? '');
|
|
if (!preg_match('/^\d{2}:\d{2}$/', $open)) continue; // start time required
|
|
|
|
$close = trim($r['close'] ?? '');
|
|
if ($close !== '' && !preg_match('/^\d{2}:\d{2}$/', $close)) continue; // end optional (open-ended)
|
|
|
|
$note = isset($r['note']) ? trim($r['note']) : '';
|
|
$note = utf8_cut($note, 200);
|
|
|
|
$clean[] = ['dow' => $dow, 'open' => $open, 'close' => $close, 'note' => $note];
|
|
}
|
|
|
|
// sort by dow then start time, assign seq within each dow
|
|
usort($clean, function ($a, $b) {
|
|
if ($a['dow'] !== $b['dow']) return $a['dow'] - $b['dow'];
|
|
return strcmp($a['open'], $b['open']);
|
|
});
|
|
|
|
$uEsc = addslashes($user);
|
|
|
|
// full replace
|
|
qry("DELETE FROM tbl_customer_accesstime WHERE ca_customer_uid = " . $c_uid);
|
|
|
|
$seqByDow = [];
|
|
foreach ($clean as $r) {
|
|
$d = $r['dow'];
|
|
$seqByDow[$d] = isset($seqByDow[$d]) ? $seqByDow[$d] + 1 : 1;
|
|
$seq = $seqByDow[$d];
|
|
|
|
$openV = "'" . $r['open'] . ":00'";
|
|
$closeV = ($r['close'] === '') ? "NULL" : "'" . $r['close'] . ":00'";
|
|
$noteV = ($r['note'] === '') ? "NULL" : "'" . addslashes($r['note']) . "'";
|
|
|
|
qry("INSERT INTO tbl_customer_accesstime
|
|
(ca_customer_uid, ca_day_of_week, ca_seq, ca_open_time, ca_close_time, ca_note,
|
|
ca_status, ca_createdby, ca_created_at)
|
|
VALUES
|
|
($c_uid, $d, $seq, $openV, $closeV, $noteV, 'A', '$uEsc', NOW())");
|
|
}
|
|
|
|
echo json_encode(['ok' => true]);
|
|
exit;
|
|
}
|
|
|
|
/* =====================================================================
|
|
* GET: load existing
|
|
* ===================================================================== */
|
|
$existing = []; // dow => [ {open, close, note}, ... ]
|
|
if ($c_uid > 0) {
|
|
$res = qry("SELECT ca_day_of_week, ca_seq, ca_open_time, ca_close_time, ca_note
|
|
FROM tbl_customer_accesstime
|
|
WHERE ca_customer_uid = $c_uid AND ca_status = 'A'
|
|
ORDER BY ca_day_of_week, ca_seq");
|
|
while ($row = fetch_array($res)) {
|
|
$existing[(int)$row['ca_day_of_week']][] = [
|
|
'open' => substr($row['ca_open_time'], 0, 5),
|
|
'close' => $row['ca_close_time'] ? substr($row['ca_close_time'], 0, 5) : '',
|
|
'note' => $row['ca_note'],
|
|
];
|
|
}
|
|
}
|
|
$existingJson = json_encode($existing, JSON_UNESCAPED_UNICODE);
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Access Time</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { font-family: -apple-system, "Segoe UI", Arial, sans-serif; margin: 0; padding: 14px 16px 80px; color: #333; font-size: 14px; background:#fff; }
|
|
h2 { font-size: 16px; margin: 0 0 10px; }
|
|
|
|
/* time selects */
|
|
.tf { display: inline-flex; align-items: center; gap: 2px; }
|
|
.tf select { padding: 5px 4px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; background: #fff; }
|
|
.tf-colon { color: #999; }
|
|
|
|
/* Quick fill */
|
|
.quick { border: 1px solid #cfe0cf; background: #f4f9f1; border-radius: 8px; padding: 10px 12px; margin-bottom: 14px; }
|
|
.quick-line { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
.quick-line:last-child { margin-bottom: 0; }
|
|
.quick input.note { padding: 5px 7px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; flex: 1; min-width: 90px; max-width: 200px; }
|
|
.qday { border: 1px solid #bcd1bc; background: #fff; color: #4a7a3a; border-radius: 14px; padding: 2px 11px; font-size: 13px; cursor: pointer; user-select: none; }
|
|
.qday.on { background: #7fb069; border-color: #7fb069; color: #fff; }
|
|
.qday.wknd { color: #c0392b; }
|
|
.qday.wknd.on { color: #fff; }
|
|
.qbtn { border: 1px solid #cfcfcf; background: #fff; border-radius: 4px; font-size: 12px; padding: 4px 10px; cursor: pointer; color: #555; }
|
|
.qbtn:hover { background: #eee; }
|
|
.qbtn.primary { background: #7fb069; border-color: #7fb069; color: #fff; }
|
|
.qbtn.primary:hover { background: #6da356; }
|
|
|
|
/* Per-day list */
|
|
.day { border: 1px solid #e3e3e3; border-radius: 6px; margin-bottom: 8px; }
|
|
.day-head { display: flex; align-items: center; justify-content: space-between; padding: 7px 10px; background: #f6f8f3; border-bottom: 1px solid #eee; }
|
|
.day-name { font-weight: 600; }
|
|
.day-name.wknd { color: #c0392b; }
|
|
.add-win { border: 1px solid #cfcfcf; background: #fff; border-radius: 4px; font-size: 12px; padding: 2px 8px; cursor: pointer; color: #555; }
|
|
.add-win:hover { background: #7fb069; color: #fff; border-color: #7fb069; }
|
|
.wins { padding: 6px 10px; display: flex; flex-direction: column; gap: 6px; }
|
|
.empty { color: #bbb; font-size: 13px; padding: 4px 0; }
|
|
.win { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
.win input.note { padding: 5px 7px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; flex: 1; min-width: 90px; }
|
|
.tilde { color: #999; }
|
|
.rm { border: none; background: #fbecec; color: #c0392b; border-radius: 4px; width: 26px; height: 28px; cursor: pointer; font-size: 15px; line-height: 1; }
|
|
.rm:hover { background: #c0392b; color: #fff; }
|
|
|
|
.footer { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; border-top: 1px solid #e3e3e3; padding: 10px 16px; display: flex; justify-content: flex-end; gap: 8px; }
|
|
.btn { padding: 8px 18px; border-radius: 4px; border: 1px solid #ccc; background: #fff; cursor: pointer; font-size: 14px; }
|
|
.btn-save { background: #7fb069; border-color: #7fb069; color: #fff; }
|
|
.btn-save:hover { background: #6da356; }
|
|
.btn:disabled { opacity: .6; cursor: default; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h2>Access Time</h2>
|
|
|
|
<!-- Quick fill: enter a time once and add it to multiple days at once -->
|
|
<div class="quick">
|
|
<div class="quick-line">
|
|
<span id="qOpenField"></span>
|
|
<span class="tilde">~</span>
|
|
<span id="qCloseField"></span>
|
|
<input type="text" id="qNote" class="note" maxlength="200" placeholder="Note (optional)">
|
|
</div>
|
|
<div class="quick-line" id="qDays"></div>
|
|
<div class="quick-line">
|
|
<button type="button" class="qbtn" id="qWeekday">Weekdays</button>
|
|
<button type="button" class="qbtn" id="qWeekend">Weekend</button>
|
|
<button type="button" class="qbtn" id="qAll">Everyday</button>
|
|
<button type="button" class="qbtn" id="qNone">Unselect</button>
|
|
<button type="button" class="qbtn primary" id="qApply">Apply</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="days"></div>
|
|
|
|
<div class="footer">
|
|
<button type="button" class="btn" id="closeBtn">Close</button>
|
|
<button type="button" class="btn btn-save" id="saveBtn">Save</button>
|
|
</div>
|
|
|
|
<script>
|
|
const DOW_NAMES = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
|
const MIN_STEP = 5; // minute step (change to 15 if desired)
|
|
const existing = <?=$existingJson?> || {};
|
|
const C_UID = <?=$c_uid?>;
|
|
|
|
const daysEl = document.getElementById('days');
|
|
|
|
function pad(n){ return String(n).padStart(2,'0'); }
|
|
function esc(s){ return (s||'').replace(/"/g,'"'); }
|
|
|
|
/* ---- hour/minute select field. Reads value via the hidden input.className ---- */
|
|
function timeField(cls, value) {
|
|
value = value || '';
|
|
const wrap = document.createElement('span');
|
|
wrap.className = 'tf';
|
|
|
|
const selH = document.createElement('select');
|
|
const selM = document.createElement('select');
|
|
const hid = document.createElement('input');
|
|
hid.type = 'hidden';
|
|
hid.className = cls;
|
|
hid.value = value;
|
|
|
|
// "--" (empty) option
|
|
let o = document.createElement('option'); o.value = ''; o.textContent = '--'; selH.appendChild(o);
|
|
o = document.createElement('option'); o.value = ''; o.textContent = '--'; selM.appendChild(o);
|
|
|
|
for (let h = 0; h < 24; h++) { const op = document.createElement('option'); op.value = pad(h); op.textContent = pad(h); selH.appendChild(op); }
|
|
const mins = [];
|
|
for (let m = 0; m < 60; m += MIN_STEP) mins.push(pad(m));
|
|
mins.forEach(function (m) { const op = document.createElement('option'); op.value = m; op.textContent = m; selM.appendChild(op); });
|
|
|
|
// 새 필드(빈 값)는 분을 00으로 기본 세팅. 시(hour)를 고르면 HH:00 으로 확정됨
|
|
let hh = '', mm = '00';
|
|
if (/^\d{2}:\d{2}$/.test(value)) {
|
|
hh = value.slice(0, 2); mm = value.slice(3, 5);
|
|
if (mins.indexOf(mm) === -1) { const op = document.createElement('option'); op.value = mm; op.textContent = mm; selM.appendChild(op); } // keep legacy minute
|
|
}
|
|
selH.value = hh; selM.value = mm;
|
|
|
|
function sync() {
|
|
const h = selH.value, m = selM.value;
|
|
hid.value = (h !== '' && m !== '') ? (h + ':' + m) : '';
|
|
}
|
|
selH.addEventListener('change', sync);
|
|
selM.addEventListener('change', sync);
|
|
|
|
const colon = document.createElement('span'); colon.className = 'tf-colon'; colon.textContent = ':';
|
|
wrap.appendChild(selH); wrap.appendChild(colon); wrap.appendChild(selM); wrap.appendChild(hid);
|
|
return wrap;
|
|
}
|
|
|
|
function winRow(w) {
|
|
w = w || { open: '', close: '', note: '' };
|
|
const div = document.createElement('div');
|
|
div.className = 'win';
|
|
|
|
div.appendChild(timeField('open', w.open));
|
|
const tilde = document.createElement('span'); tilde.className = 'tilde'; tilde.textContent = '~'; div.appendChild(tilde);
|
|
div.appendChild(timeField('close', w.close));
|
|
|
|
const note = document.createElement('input');
|
|
note.type = 'text'; note.className = 'note'; note.maxLength = 200;
|
|
note.placeholder = 'Note (optional)'; note.value = w.note || '';
|
|
div.appendChild(note);
|
|
|
|
const rm = document.createElement('button');
|
|
rm.type = 'button'; rm.className = 'rm'; rm.title = 'Delete'; rm.innerHTML = '×';
|
|
rm.addEventListener('click', function () { const wins = div.parentElement; div.remove(); refreshEmpty(wins); });
|
|
div.appendChild(rm);
|
|
|
|
return div;
|
|
}
|
|
|
|
function refreshEmpty(winsEl) {
|
|
const hasRow = winsEl.querySelector('.win');
|
|
let emptyEl = winsEl.querySelector('.empty');
|
|
if (!hasRow && !emptyEl) {
|
|
emptyEl = document.createElement('div');
|
|
emptyEl.className = 'empty';
|
|
emptyEl.textContent = 'Closed (no hours)';
|
|
winsEl.appendChild(emptyEl);
|
|
} else if (hasRow && emptyEl) {
|
|
emptyEl.remove();
|
|
}
|
|
}
|
|
|
|
const winsByDow = {};
|
|
|
|
function addWindowToDay(d, w) {
|
|
const wins = winsByDow[d];
|
|
wins.appendChild(winRow(w));
|
|
refreshEmpty(wins);
|
|
}
|
|
|
|
for (let d = 0; d < 7; d++) {
|
|
const day = document.createElement('div');
|
|
day.className = 'day';
|
|
day.dataset.dow = d;
|
|
|
|
const head = document.createElement('div');
|
|
head.className = 'day-head';
|
|
head.innerHTML =
|
|
'<span class="day-name ' + ((d === 0 || d === 6) ? 'wknd' : '') + '">' + DOW_NAMES[d] + '</span>' +
|
|
'<button type="button" class="add-win">+ Add time</button>';
|
|
|
|
const wins = document.createElement('div');
|
|
wins.className = 'wins';
|
|
winsByDow[d] = wins;
|
|
|
|
(existing[d] || []).forEach(function (w) { wins.appendChild(winRow(w)); });
|
|
refreshEmpty(wins);
|
|
|
|
head.querySelector('.add-win').addEventListener('click', function () { addWindowToDay(d); });
|
|
|
|
day.appendChild(head);
|
|
day.appendChild(wins);
|
|
daysEl.appendChild(day);
|
|
}
|
|
|
|
/* ----- Quick fill ----- */
|
|
document.getElementById('qOpenField').appendChild(timeField('qOpen', ''));
|
|
document.getElementById('qCloseField').appendChild(timeField('qClose', ''));
|
|
|
|
const qDaysEl = document.getElementById('qDays');
|
|
for (let d = 0; d < 7; d++) {
|
|
const chip = document.createElement('span');
|
|
chip.className = 'qday' + ((d === 0 || d === 6) ? ' wknd' : '');
|
|
chip.dataset.dow = d;
|
|
chip.textContent = DOW_NAMES[d];
|
|
chip.addEventListener('click', function () { chip.classList.toggle('on'); });
|
|
qDaysEl.appendChild(chip);
|
|
}
|
|
function setDays(list) {
|
|
qDaysEl.querySelectorAll('.qday').forEach(function (c) {
|
|
c.classList.toggle('on', list.includes(parseInt(c.dataset.dow, 10)));
|
|
});
|
|
}
|
|
document.getElementById('qWeekday').addEventListener('click', function () { setDays([1,2,3,4,5]); });
|
|
document.getElementById('qWeekend').addEventListener('click', function () { setDays([0,6]); });
|
|
document.getElementById('qAll').addEventListener('click', function () { setDays([0,1,2,3,4,5,6]); });
|
|
document.getElementById('qNone').addEventListener('click', function () { setDays([]); });
|
|
|
|
document.getElementById('qApply').addEventListener('click', function () {
|
|
const open = document.querySelector('.qOpen').value.trim();
|
|
const close = document.querySelector('.qClose').value.trim();
|
|
const note = document.getElementById('qNote').value.trim();
|
|
if (!open) { alert('Enter a start time.'); return; }
|
|
if (close && close < open) { alert('End time is earlier than start time.'); return; }
|
|
const targets = [];
|
|
qDaysEl.querySelectorAll('.qday.on').forEach(function (c) { targets.push(parseInt(c.dataset.dow, 10)); });
|
|
if (targets.length === 0) { alert('Select day(s) to apply.'); return; }
|
|
targets.forEach(function (d) { addWindowToDay(d, { open: open, close: close, note: note }); });
|
|
});
|
|
|
|
/* ----- Close / Save ----- */
|
|
function closeUp(changed) {
|
|
if (window.parent && window.parent.closeCustomerPopup) {
|
|
window.parent.closeCustomerPopup(!!changed);
|
|
} else {
|
|
if (changed) location.reload(); else history.back();
|
|
}
|
|
}
|
|
document.getElementById('closeBtn').addEventListener('click', function () { closeUp(false); });
|
|
|
|
document.getElementById('saveBtn').addEventListener('click', function () {
|
|
const btn = this;
|
|
const rows = [];
|
|
let bad = '';
|
|
for (let d = 0; d < 7; d++) {
|
|
winsByDow[d].querySelectorAll('.win').forEach(function (win) {
|
|
const open = win.querySelector('.open').value.trim();
|
|
const close = win.querySelector('.close').value.trim();
|
|
const note = win.querySelector('.note').value.trim();
|
|
if (!open) return;
|
|
if (close && close < open) { if (!bad) bad = DOW_NAMES[d] + ': end time is earlier than start time.'; return; }
|
|
rows.push({ dow: d, open: open, close: close, note: note });
|
|
});
|
|
}
|
|
if (bad) { alert(bad); return; }
|
|
|
|
btn.disabled = true;
|
|
const body = new URLSearchParams();
|
|
body.set('action', 'save');
|
|
body.set('c_uid', C_UID);
|
|
body.set('data', JSON.stringify(rows));
|
|
|
|
fetch(location.href, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (res) {
|
|
if (res && res.ok) { closeUp(true); }
|
|
else { alert('Save failed: ' + (res && res.msg ? res.msg : 'unknown')); btn.disabled = false; }
|
|
})
|
|
.catch(function (e) { alert('Save error: ' + e); btn.disabled = false; });
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|