goiintra/public_html/lib/customer_access_time_popup.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,'&quot;'); }
/* ---- 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 = '&times;';
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>