v1.4.0
- [CUSTOMER] access time 정보 입력 / 조회 추가. closing days 정보 입력 / 조회 추가. - [MAP] 아직 access 할 수 없는 곳은 남은 시간 표시. 상태 기준을 색깔에서 d_status 값으로 변경. - [SHORT INFO] access time, closing days 정보 표시. - [FORECAST] SHORT INFO 클릭은 right pannel 이벤트에서 제외.
This commit is contained in:
parent
c0cb2f00a8
commit
9925b9eaae
|
|
@ -113,10 +113,18 @@ class CustomerService extends CONF
|
||||||
// =====================================
|
// =====================================
|
||||||
else {
|
else {
|
||||||
|
|
||||||
|
// 주소/우편번호가 바뀌면 좌표 초기화
|
||||||
|
$this->handleGeoReset($beforeData, $data);
|
||||||
|
|
||||||
$setParts = [];
|
$setParts = [];
|
||||||
|
|
||||||
foreach ($data as $k => $v) {
|
foreach ($data as $k => $v) {
|
||||||
|
|
||||||
|
if ($v === null) {
|
||||||
|
$setParts[] = "{$k}=NULL";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$vEsc = mysqli_real_escape_string(
|
$vEsc = mysqli_real_escape_string(
|
||||||
$connect,
|
$connect,
|
||||||
(string)$v
|
(string)$v
|
||||||
|
|
@ -814,6 +822,14 @@ class CustomerService extends CONF
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// c_time_restricted: Y/N 로 강제 (MIS 전용 필드)
|
||||||
|
// - hasCustomerChanges() 의 $businessFields 에 없으므로 ERP outbox 로 안 나감
|
||||||
|
if (isset($data['c_time_restricted'])) {
|
||||||
|
$v = $data['c_time_restricted'];
|
||||||
|
$data['c_time_restricted'] =
|
||||||
|
($v === 'Y' || $v === '1' || $v === 1 || $v === true) ? 'Y' : 'N';
|
||||||
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -954,3 +970,60 @@ class CustomerService extends CONF
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
* 이 파일을 "직접 호출"했을 때만 동작하는 경량 디스패처.
|
||||||
|
* (다른 코드에서 require 될 땐 위 CustomerService 클래스만 제공)
|
||||||
|
*
|
||||||
|
* action = patchTimeRestricted
|
||||||
|
* - c_time_restricted (Y|N) 만 patchCustomer() 로 갱신
|
||||||
|
* - c_time_restricted 는 hasCustomerChanges()의 $businessFields 에 없으므로
|
||||||
|
* ERP outbox 로 나가지 않음
|
||||||
|
* - 입력: POST c_uid, value(Y|N) / 출력: { ok, value }
|
||||||
|
* ===================================================================== */
|
||||||
|
$__self = basename(__FILE__);
|
||||||
|
$__script = isset($_SERVER['SCRIPT_FILENAME']) ? basename($_SERVER['SCRIPT_FILENAME']) : '';
|
||||||
|
if ($__script === $__self) {
|
||||||
|
|
||||||
|
if (is_file(getenv("DOCUMENT_ROOT")."/include/session_include.php")) {
|
||||||
|
include_once getenv("DOCUMENT_ROOT")."/include/session_include.php";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if ($action === 'patchTimeRestricted') {
|
||||||
|
|
||||||
|
$c_uid = intval($_POST['c_uid'] ?? $_GET['c_uid'] ?? 0);
|
||||||
|
$value = ((($_POST['value'] ?? $_GET['value'] ?? '')) === 'Y') ? 'Y' : 'N';
|
||||||
|
|
||||||
|
if ($c_uid <= 0) {
|
||||||
|
echo json_encode(['ok' => false, 'msg' => 'invalid c_uid']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $_SESSION['ss_INITIAL'] ?? ($_SESSION['ss_UID'] ?? 'system');
|
||||||
|
|
||||||
|
$svc = new CustomerService();
|
||||||
|
$svc->patchCustomer(
|
||||||
|
$c_uid,
|
||||||
|
[
|
||||||
|
'c_time_restricted' => $value,
|
||||||
|
'c_updatedby' => $user,
|
||||||
|
],
|
||||||
|
['skip_after' => true] // 부수효과(runAfterChange) 생략
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'value' => $value]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['ok' => false, 'msg' => 'unknown action']);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['ok' => false, 'msg' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/* =====================================================================
|
||||||
|
* customer_access.css
|
||||||
|
* Access Time / Closing Days 표시(칩) + 클릭 편집 박스 공용 스타일
|
||||||
|
* - customer_detail.php, map.php, shortInfo_lib.php, customer_access_display.php 공용
|
||||||
|
* - 클래스 기준(스코프 없음)이라 어느 화면에 넣어도 동일하게 적용됨
|
||||||
|
* ===================================================================== */
|
||||||
|
|
||||||
|
/* 필드 박스 (흰색) */
|
||||||
|
.at-field, .cd-field {
|
||||||
|
background:#fff; border:1px solid #ccc; border-radius:4px;
|
||||||
|
padding:8px 10px; min-height:36px; height:100%; box-sizing:border-box;
|
||||||
|
}
|
||||||
|
/* td 안에서 박스가 셀 높이를 채우도록 (td height:100% 는 auto 로 계산되므로 1px 시드) */
|
||||||
|
.td-text-info:has(> .box-clickable) { height:1px; }
|
||||||
|
|
||||||
|
/* Access Time */
|
||||||
|
.at-wrap { display:flex; flex-direction:column; gap:2px; }
|
||||||
|
.at-row { display:flex; align-items:center; gap:8px; line-height:1.6; }
|
||||||
|
.at-row.at-off { opacity:.55; }
|
||||||
|
.at-dow { display:inline-block; width:36px; font-weight:600; font-size:14px; color:#444; }
|
||||||
|
.at-dow.at-wknd { color:#c0392b; }
|
||||||
|
.at-win { display:flex; flex-wrap:wrap; gap:5px; }
|
||||||
|
.at-chip {
|
||||||
|
display:inline-block; padding:1px 8px; border-radius:4px;
|
||||||
|
background:#eef3fb; border:1px solid #cfddf2; color:#23548f;
|
||||||
|
font-size:14px; white-space:nowrap;
|
||||||
|
}
|
||||||
|
.at-dash { color:#bbb; font-size:14px; }
|
||||||
|
|
||||||
|
/* Closing Days */
|
||||||
|
.cd-wrap { display:flex; flex-direction:column; gap:5px; }
|
||||||
|
.cd-chip {
|
||||||
|
display:inline-flex; align-items:center; gap:6px;
|
||||||
|
padding:2px 9px; border-radius:4px; align-self:flex-start;
|
||||||
|
background:#fbf1f1; border:1px solid #e8c9c9; color:#a23a3a;
|
||||||
|
font-size:14px; white-space:nowrap;
|
||||||
|
}
|
||||||
|
.cd-range { font-weight:600; }
|
||||||
|
.cd-note { color:#8a6d3b; font-style:italic; font-size:13px; }
|
||||||
|
.cd-empty { color:#bbb; font-size:14px; }
|
||||||
|
|
||||||
|
/* 박스 전체 클릭 → 수정 팝업 */
|
||||||
|
.box-clickable { cursor:pointer; position:relative; transition:border-color .12s, background-color .12s; }
|
||||||
|
.box-clickable:hover { border-color:#7fb069; background:#fafdf7; }
|
||||||
|
.box-clickable::after {
|
||||||
|
content:"\270E"; position:absolute; top:5px; right:8px;
|
||||||
|
font-size:13px; color:#bbb; line-height:1;
|
||||||
|
}
|
||||||
|
.box-clickable:hover::after { color:#7fb069; }
|
||||||
|
|
||||||
|
/* "지금 닫힘" 경고 배지 (마커/Info버튼/모달 Access Time 공용) */
|
||||||
|
.closed-badge {
|
||||||
|
display:inline-flex; align-items:center; justify-content:center;
|
||||||
|
min-width:18px; height:18px; padding:0 5px; margin-left:6px;
|
||||||
|
background:#e53935; color:#fff; border-radius:9px;
|
||||||
|
font-size:13px; font-weight:700; line-height:1; vertical-align:middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 오늘 요일 강조 (주황 하이라이트 + 붉은 테두리) */
|
||||||
|
.at-row.at-today {
|
||||||
|
background:#fff6da; border:1px solid #e53935; border-radius:4px;
|
||||||
|
padding:1px 6px; margin:0 -7px;
|
||||||
|
opacity:1 !important; /* 오늘은 휴무여도 흐리게 하지 않음 */
|
||||||
|
}
|
||||||
|
|
@ -207,6 +207,7 @@ class API extends CONF {
|
||||||
"qty" => $rstOptionPoint['d_quantity'],
|
"qty" => $rstOptionPoint['d_quantity'],
|
||||||
"estqty" => $rstOptionPoint['d_estquantity'],
|
"estqty" => $rstOptionPoint['d_estquantity'],
|
||||||
"color" => $color,
|
"color" => $color,
|
||||||
|
"d_status" => $rstOptionPoint['d_status'],
|
||||||
"comment" => $rstOptionPoint['c_comment_ri'],
|
"comment" => $rstOptionPoint['c_comment_ri'],
|
||||||
"container_location" => stripslashes($rstOptionPoint['c_location']),
|
"container_location" => stripslashes($rstOptionPoint['c_location']),
|
||||||
"last_pickup_date" => $rstOptionPoint['c_lastpickupdate'],
|
"last_pickup_date" => $rstOptionPoint['c_lastpickupdate'],
|
||||||
|
|
@ -704,6 +705,7 @@ class API extends CONF {
|
||||||
"qty" => $rstOptionPoint['d_quantity'],
|
"qty" => $rstOptionPoint['d_quantity'],
|
||||||
"estqty" => $rstOptionPoint['d_estquantity'],
|
"estqty" => $rstOptionPoint['d_estquantity'],
|
||||||
"color" => $color,
|
"color" => $color,
|
||||||
|
"d_status" => $rstOptionPoint['d_status'],
|
||||||
"comment" => $rstOptionPoint['c_comment_ri'],
|
"comment" => $rstOptionPoint['c_comment_ri'],
|
||||||
"container_location" => stripslashes($rstOptionPoint['c_location']),
|
"container_location" => stripslashes($rstOptionPoint['c_location']),
|
||||||
"last_pickup_date" => $rstOptionPoint['c_lastpickupdate'],
|
"last_pickup_date" => $rstOptionPoint['c_lastpickupdate'],
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,46 @@ if ($mode == "update") {
|
||||||
$containerList[] = $row;
|
$containerList[] = $row;
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
// Access Time (요일별 수거 가능 시간창)
|
||||||
|
// $accessTimeMap[dow] = [ {open, close, note, seq}, ... ]
|
||||||
|
// ===============================
|
||||||
|
$accessTimeMap = [];
|
||||||
|
$qAT = "
|
||||||
|
SELECT ca_uid, 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 ASC, ca_seq ASC
|
||||||
|
";
|
||||||
|
$rAT = mysqli_query($jdb->DBConn, $qAT);
|
||||||
|
if ($rAT) {
|
||||||
|
while ($row = mysqli_fetch_array($rAT, MYSQLI_ASSOC)) {
|
||||||
|
$accessTimeMap[(int)$row['ca_day_of_week']][] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
// Closing Days (휴무 날짜 구간)
|
||||||
|
// 지나간 구간(ccd_end_date < 오늘)은 화면에 표시하지 않음
|
||||||
|
// ===============================
|
||||||
|
$closingDays = [];
|
||||||
|
$qCD = "
|
||||||
|
SELECT ccd_uid, ccd_start_date, ccd_end_date, ccd_note
|
||||||
|
FROM tbl_customer_closing_days
|
||||||
|
WHERE ccd_customer_uid = '{$c_uid}'
|
||||||
|
AND ccd_end_date >= CURDATE()
|
||||||
|
ORDER BY ccd_start_date ASC
|
||||||
|
";
|
||||||
|
$rCD = mysqli_query($jdb->DBConn, $qCD);
|
||||||
|
if ($rCD) {
|
||||||
|
while ($row = mysqli_fetch_array($rCD, MYSQLI_ASSOC)) {
|
||||||
|
$closingDays[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restricted 체크박스 상태 (c_time_restricted = 'Y'/'N')
|
||||||
|
$isRestrictedChecked = ($c_time_restricted == 'Y') ? 'checked' : '';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$c_salescommissiondateSTR = "NOT YET";
|
$c_salescommissiondateSTR = "NOT YET";
|
||||||
|
|
@ -1373,6 +1413,9 @@ $(document).ready(function()
|
||||||
<div class="h3-title-section">
|
<div class="h3-title-section">
|
||||||
<h3><span style="color:var(--color-primary);">Restaurant</span> Information</h3>
|
<h3><span style="color:var(--color-primary);">Restaurant</span> Information</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/customer_access.css">
|
||||||
|
|
||||||
<table class="tb-info-box">
|
<table class="tb-info-box">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-title-info">Account No</td>
|
<td class="td-title-info">Account No</td>
|
||||||
|
|
@ -1554,6 +1597,62 @@ $(document).ready(function()
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="td-title-info">Access Time<div style="display: inline; float:right; padding-right: 30px;">Restricted <input type="checkbox" id="atna" name="c_time_restricted" value="Y" <?=$isRestrictedChecked?>></div></td>
|
||||||
|
<td class="td-text-info">
|
||||||
|
<?php
|
||||||
|
$dowNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
||||||
|
if (!function_exists('fmtAT')) { function fmtAT($t){ return $t ? substr($t,0,5) : ''; } }
|
||||||
|
?>
|
||||||
|
<div class="at-field box-clickable" data-popup="accesstime" title="클릭하여 수정">
|
||||||
|
<div class="at-wrap" id="atWrap">
|
||||||
|
<?php for($d=0; $d<7; $d++):
|
||||||
|
$wins = isset($accessTimeMap[$d]) ? $accessTimeMap[$d] : [];
|
||||||
|
$isWeekend = ($d == 0 || $d == 6);
|
||||||
|
?>
|
||||||
|
<div class="at-row<?= empty($wins) ? ' at-off' : '' ?>">
|
||||||
|
<span class="at-dow<?= $isWeekend ? ' at-wknd' : '' ?>"><?=$dowNames[$d]?></span>
|
||||||
|
<span class="at-win">
|
||||||
|
<?php if (empty($wins)): ?>
|
||||||
|
<span class="at-dash">—</span>
|
||||||
|
<?php else: foreach($wins as $w):
|
||||||
|
$o = fmtAT($w['ca_open_time']);
|
||||||
|
$c = fmtAT($w['ca_close_time']);
|
||||||
|
$label = $c ? ($o.'–'.$c) : ($o.' →');
|
||||||
|
?>
|
||||||
|
<span class="at-chip"<?= !empty($w['ca_note']) ? ' title="'.htmlspecialchars($w['ca_note'], ENT_QUOTES).'"' : '' ?>><?=$label?></span>
|
||||||
|
<?php endforeach; endif; ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="td-title-info">Closing Days</td>
|
||||||
|
<td class="td-text-info">
|
||||||
|
<div class="cd-field box-clickable" data-popup="closingdays" title="클릭하여 수정">
|
||||||
|
<div class="cd-wrap" id="cdWrap">
|
||||||
|
<?php if (empty($closingDays)): ?>
|
||||||
|
<span class="cd-empty">—</span>
|
||||||
|
<?php else: foreach($closingDays as $cd):
|
||||||
|
$s = $cd['ccd_start_date'];
|
||||||
|
$e = $cd['ccd_end_date'];
|
||||||
|
$range = ($s === $e) ? $s : ($s.' ~ '.$e);
|
||||||
|
?>
|
||||||
|
<span class="cd-chip">
|
||||||
|
<i class="bi bi-calendar-x"></i>
|
||||||
|
<span class="cd-range"><?=$range?></span>
|
||||||
|
<?php if (!empty($cd['ccd_note'])): ?>
|
||||||
|
<em class="cd-note"><?=htmlspecialchars($cd['ccd_note'])?></em>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<?php endforeach; endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-title-info">Comment</td>
|
<td class="td-title-info">Comment</td>
|
||||||
<td class="td-text-info" colspan="3">
|
<td class="td-text-info" colspan="3">
|
||||||
|
|
@ -1982,10 +2081,80 @@ $(document).ready(function()
|
||||||
</FORM>
|
</FORM>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ==============================================================
|
||||||
|
Access Time / Closing Days 공용 팝업 모달
|
||||||
|
- 외부 PHP를 iframe 으로 로드 (다른 화면에서도 동일 파일 재사용)
|
||||||
|
- /lib/customer_access_time_popup.php
|
||||||
|
- /lib/customer_closing_days_popup.php
|
||||||
|
- 저장 완료 시 자식 iframe 에서 window.parent.closeCustomerPopup(true) 호출
|
||||||
|
============================================================== -->
|
||||||
|
<div class="modal fade" id="customerPopupModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="customerPopupTitle">Edit</h4>
|
||||||
|
<button type="button" class="btn" data-bs-dismiss="modal" aria-label="Close" style="color:#fff;"><i class="bi bi-x"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="padding:0;">
|
||||||
|
<iframe id="customerPopupFrame" src="about:blank" title="Edit popup"
|
||||||
|
style="width:100%; height:70vh; border:0; display:block;"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const C_UID = "<?=$c_uid?>";
|
||||||
|
const GO_STR = "<?=$goStr?>";
|
||||||
|
|
||||||
|
const popupConfig = {
|
||||||
|
accesstime: { url: '/lib/customer_access_time_popup.php', title: 'Access Time' },
|
||||||
|
closingdays: { url: '/lib/customer_closing_days_popup.php', title: 'Closing Days' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalEl = document.getElementById('customerPopupModal');
|
||||||
|
const popupModal = new bootstrap.Modal(modalEl);
|
||||||
|
const frameEl = document.getElementById('customerPopupFrame');
|
||||||
|
const titleEl = document.getElementById('customerPopupTitle');
|
||||||
|
|
||||||
|
// 변경 발생 여부 (자식이 closeCustomerPopup(true) 호출 시 true)
|
||||||
|
let popupChanged = false;
|
||||||
|
|
||||||
|
document.querySelectorAll('.box-clickable[data-popup]').forEach(function (box) {
|
||||||
|
box.addEventListener('click', function () {
|
||||||
|
const key = box.getAttribute('data-popup');
|
||||||
|
const cfg = popupConfig[key];
|
||||||
|
if (!cfg) return;
|
||||||
|
|
||||||
|
popupChanged = false;
|
||||||
|
titleEl.innerText = cfg.title;
|
||||||
|
frameEl.src = cfg.url
|
||||||
|
+ '?c_uid=' + encodeURIComponent(C_UID)
|
||||||
|
+ '&goStr=' + encodeURIComponent(GO_STR);
|
||||||
|
popupModal.show();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// iframe 비우기 (다음 오픈 시 깨끗하게)
|
||||||
|
modalEl.addEventListener('hidden.bs.modal', function () {
|
||||||
|
frameEl.src = 'about:blank';
|
||||||
|
if (popupChanged) location.reload(); // 변경됐으면 부모 새로고침해서 갱신된 칩 반영
|
||||||
|
});
|
||||||
|
|
||||||
|
// 자식 iframe(팝업 PHP)에서 호출하는 전역 핸들러
|
||||||
|
// 저장 성공 후: window.parent.closeCustomerPopup(true);
|
||||||
|
// 단순 닫기 : window.parent.closeCustomerPopup(false);
|
||||||
|
window.closeCustomerPopup = function (changed) {
|
||||||
|
if (changed) popupChanged = true;
|
||||||
|
popupModal.hide();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<?
|
<?
|
||||||
|
|
||||||
if ($_SESSION['ss_LEVEL'] == "1" || $_SESSION['ss_LEVEL'] == "5" || $_SESSION['ss_LEVEL'] == "6" || $_SESSION['ss_LEVEL'] == "7" ) {
|
if ($_SESSION['ss_LEVEL'] == "1" || $_SESSION['ss_LEVEL'] == "5" || $_SESSION['ss_LEVEL'] == "6" || $_SESSION['ss_LEVEL'] == "7" ) {
|
||||||
$add_query = " ORDER BY n_uid DESC";
|
$add_query = " ORDER BY n_uid DESC";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1429,6 +1429,10 @@ $(document).on("click", "a.restaurant-link", function(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).on("click", "a.customerShortInfo", function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
let openedCid = null;
|
let openedCid = null;
|
||||||
let panelOpen = false;
|
let panelOpen = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -606,6 +606,9 @@ function popup(){
|
||||||
</div>
|
</div>
|
||||||
<!-- HISTORY DETAIL MODAL Ends -->
|
<!-- HISTORY DETAIL MODAL Ends -->
|
||||||
|
|
||||||
|
<!-- Access Time / Closing Days 표시 스타일 (customer_detail 과 동일) -->
|
||||||
|
<link rel="stylesheet" href="/assets/css/customer_access.css">
|
||||||
|
|
||||||
<!-- INFO MODAL-->
|
<!-- INFO MODAL-->
|
||||||
<div class="modal fade top map in" id="map-modal-info" tabindex="-1" role="dialog">
|
<div class="modal fade top map in" id="map-modal-info" tabindex="-1" role="dialog">
|
||||||
<div id="modal-info" class="modal-dialog" role="document">
|
<div id="modal-info" class="modal-dialog" role="document">
|
||||||
|
|
@ -620,33 +623,33 @@ function popup(){
|
||||||
<form name="customerShortInfo" id="customerShortInfo" method="POST" novalidate="novalidate">
|
<form name="customerShortInfo" id="customerShortInfo" method="POST" novalidate="novalidate">
|
||||||
<input type="hidden" name="actionStr" value="SHORTINFO">
|
<input type="hidden" name="actionStr" value="SHORTINFO">
|
||||||
<input type="hidden" name="actionPage" value="">
|
<input type="hidden" name="actionPage" value="">
|
||||||
<input type="hidden" name="c_uid" class="c_uid" value="3888">
|
<input type="hidden" name="c_uid" class="c_uid" value="">
|
||||||
<input type="hidden" name="c_accountno" class="c_accountno" value="">
|
<input type="hidden" name="c_accountno" class="c_accountno" value="">
|
||||||
<input type="hidden" name="mode" class="mode" value="update">
|
<input type="hidden" name="mode" class="mode" value="update">
|
||||||
<table class="tb-info-box">
|
<table class="tb-info-box">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-title-info">Restaurant Name</td>
|
<td class="td-title-info">Restaurant Name</td>
|
||||||
<td colspan="2" class="td-text-info shortinfo-store-name">Name</td>
|
<td colspan="2" class="td-text-info shortinfo-store-name"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-title-info">Payment Type</td>
|
<td class="td-title-info">Payment Type</td>
|
||||||
<td colspan="2" class="td-text-info shortinfo-payment-type">Cheque</td>
|
<td colspan="2" class="td-text-info shortinfo-payment-type"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-title-info">Pay Status</td>
|
<td class="td-title-info">Pay Status</td>
|
||||||
<td colspan="2" class="td-text-info shortinfo-payment-string">Paid</td>
|
<td colspan="2" class="td-text-info shortinfo-payment-string"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-title-info">Comment</td>
|
<td class="td-title-info">Comment</td>
|
||||||
<td colspan="2" class="td-text-info">
|
<td colspan="2" class="td-text-info">
|
||||||
<textarea id="c_comment_ri" class="textarea shortinfo-comment" name="c_comment_ri" rows="4" cols="20">매주 수요일 정기 픽업, 11시 이후 방문</textarea>
|
<textarea id="c_comment_ri" class="textarea shortinfo-comment" name="c_comment_ri" rows="4" cols="20"></textarea>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-title-info">Container Location</td>
|
<td class="td-title-info">Container Location</td>
|
||||||
<td colspan="2" class="td-text-info">
|
<td colspan="2" class="td-text-info">
|
||||||
<textarea id="c_location" class="textarea shortinfo-container-location" name="c_location" rows="2" cols="20">Inside Rest</textarea>
|
<textarea id="c_location" class="textarea shortinfo-container-location" name="c_location" rows="2" cols="20"></textarea>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- <tr>
|
<!-- <tr>
|
||||||
|
|
@ -661,6 +664,7 @@ function popup(){
|
||||||
</td>
|
</td>
|
||||||
</tr> -->
|
</tr> -->
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tbody id="mapAccessClosing"></tbody>
|
||||||
</table>
|
</table>
|
||||||
<br>
|
<br>
|
||||||
<div class="text-center grid-layout-col-2">
|
<div class="text-center grid-layout-col-2">
|
||||||
|
|
@ -686,6 +690,82 @@ function popup(){
|
||||||
</div>
|
</div>
|
||||||
<!-- INFO DETAIL MODAL Ends -->
|
<!-- INFO DETAIL MODAL Ends -->
|
||||||
|
|
||||||
|
<!-- ==============================================================
|
||||||
|
Access Time / Closing Days 공용 편집 팝업 모달 (Bootstrap 3)
|
||||||
|
- 외부 PHP 를 iframe 으로 로드 (customer_detail 과 동일 팝업 재사용)
|
||||||
|
- 저장 완료 시 자식이 window.parent.closeCustomerPopup(true) 호출
|
||||||
|
============================================================== -->
|
||||||
|
<!-- ==============================================================
|
||||||
|
Access Time / Closing Days 공용 편집 팝업 (순수 오버레이)
|
||||||
|
- Bootstrap 모달을 쓰지 않음: BS3/BS5 focus-trap 충돌(무한재귀) 회피
|
||||||
|
- 외부 PHP 를 iframe 으로 로드 (customer_detail 과 동일 팝업 재사용)
|
||||||
|
- 저장 완료 시 자식이 window.parent.closeCustomerPopup(true) 호출
|
||||||
|
============================================================== -->
|
||||||
|
<div id="customerPopupOverlay" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; z-index:100000; background:rgba(0,0,0,0.5);">
|
||||||
|
<div id="customerPopupBox" style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:92%; max-width:760px; background:#fff; border-radius:6px; overflow:hidden; box-shadow:0 10px 40px rgba(0,0,0,0.35);">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; padding:10px 14px; background:#2A9B56; color:#fff;">
|
||||||
|
<h4 id="customerPopupTitle" style="margin:0; font-size:16px;">Edit</h4>
|
||||||
|
<button type="button" id="customerPopupClose" style="background:none; border:none; color:#fff; font-size:24px; line-height:1; cursor:pointer;">×</button>
|
||||||
|
</div>
|
||||||
|
<iframe id="customerPopupFrame" src="about:blank" title="Edit popup" style="width:100%; height:70vh; border:0; display:block;"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var popupConfig = {
|
||||||
|
accesstime: { url: '/lib/customer_access_time_popup.php', title: 'Access Time' },
|
||||||
|
closingdays: { url: '/lib/customer_closing_days_popup.php', title: 'Closing Days' }
|
||||||
|
};
|
||||||
|
var popupChanged = false;
|
||||||
|
|
||||||
|
// 클리핑/스택 방지: 오버레이를 body 직속으로 이동
|
||||||
|
jQuery('#customerPopupOverlay').appendTo(document.body);
|
||||||
|
|
||||||
|
// 현재 INFO MODAL 의 c_uid
|
||||||
|
function currentCuid() { return jQuery('#customerShortInfo .c_uid').val() || ''; }
|
||||||
|
|
||||||
|
// 표시 조각 로드 (모달 열릴 때 / 저장 후 갱신)
|
||||||
|
window.loadCustomerAccess = function (cuid) {
|
||||||
|
cuid = cuid || currentCuid();
|
||||||
|
if (!cuid) { jQuery('#mapAccessClosing').empty(); return; }
|
||||||
|
jQuery('#mapAccessClosing').load('/lib/customer_access_display.php?c_uid=' + encodeURIComponent(cuid));
|
||||||
|
};
|
||||||
|
|
||||||
|
function openPopup(key) {
|
||||||
|
var cfg = popupConfig[key];
|
||||||
|
if (!cfg) return;
|
||||||
|
var cuid = currentCuid();
|
||||||
|
if (!cuid) { alert('No customer selected.'); return; }
|
||||||
|
popupChanged = false;
|
||||||
|
jQuery('#customerPopupTitle').text(cfg.title);
|
||||||
|
jQuery('#customerPopupFrame').attr('src', cfg.url + '?c_uid=' + encodeURIComponent(cuid) + '&goStr=');
|
||||||
|
jQuery('#customerPopupOverlay').css('display', 'block');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePopup() {
|
||||||
|
jQuery('#customerPopupOverlay').css('display', 'none');
|
||||||
|
jQuery('#customerPopupFrame').attr('src', 'about:blank');
|
||||||
|
if (popupChanged) { window.loadCustomerAccess(); popupChanged = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 박스 클릭 → 편집 팝업 (동적 주입이라 위임 바인딩)
|
||||||
|
jQuery(document).on('click', '#mapAccessClosing .box-clickable[data-popup]', function () {
|
||||||
|
openPopup(jQuery(this).data('popup'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 닫기: X 버튼 / 배경 클릭(박스 내부 제외)
|
||||||
|
jQuery('#customerPopupClose').on('click', closePopup);
|
||||||
|
jQuery('#customerPopupOverlay').on('click', function (e) { if (e.target === this) closePopup(); });
|
||||||
|
|
||||||
|
// 자식 iframe(팝업 PHP)에서 호출하는 전역 핸들러
|
||||||
|
window.closeCustomerPopup = function (changed) {
|
||||||
|
if (changed) popupChanged = true;
|
||||||
|
closePopup();
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- NOTE MODAL-->
|
<!-- NOTE MODAL-->
|
||||||
<div class="modal fade top map" id="map-modal-note" tabindex="-1" role="dialog">
|
<div class="modal fade top map" id="map-modal-note" tabindex="-1" role="dialog">
|
||||||
<div id="modal-note" class="modal-dialog" role="document">
|
<div id="modal-note" class="modal-dialog" role="document">
|
||||||
|
|
@ -1798,6 +1878,15 @@ function popup(){
|
||||||
clusterer = null;
|
clusterer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 닫힘 배지 마커 정리
|
||||||
|
if (typeof closedBadgeMarker !== 'undefined' && closedBadgeMarker) {
|
||||||
|
Object.keys(closedBadgeMarker).forEach(function (idx) {
|
||||||
|
if (closedBadgeMarker[idx]) closedBadgeMarker[idx].setMap(null);
|
||||||
|
});
|
||||||
|
closedBadgeMarker = {};
|
||||||
|
}
|
||||||
|
if (window.closedNowSet) window.closedNowSet = new Set();
|
||||||
|
|
||||||
marker = [];
|
marker = [];
|
||||||
overlay = [];
|
overlay = [];
|
||||||
let pointGeocoder = new google.maps.Geocoder();
|
let pointGeocoder = new google.maps.Geocoder();
|
||||||
|
|
@ -1819,6 +1908,8 @@ function popup(){
|
||||||
ordertype: json.result[i]['ordertype'],
|
ordertype: json.result[i]['ordertype'],
|
||||||
customerid: json.result[i]['id'],
|
customerid: json.result[i]['id'],
|
||||||
type: json.result[i]['type'],
|
type: json.result[i]['type'],
|
||||||
|
d_status: json.result[i]['d_status'],
|
||||||
|
schedule: json.result[i]['schedule'],
|
||||||
colour: "transparency",
|
colour: "transparency",
|
||||||
color: json.result[i]['color'],
|
color: json.result[i]['color'],
|
||||||
lat: json.result[i]['lat'],
|
lat: json.result[i]['lat'],
|
||||||
|
|
@ -1876,7 +1967,7 @@ function popup(){
|
||||||
position: location,
|
position: location,
|
||||||
map: map,
|
map: map,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
animation: (property.rnote && /\S/.test(property.rnote) && property.color !== "#7B7A7A") ? google.maps.Animation.BOUNCE : null
|
animation: (property.rnote && /\S/.test(property.rnote) && property.d_status !== 'F') ? google.maps.Animation.BOUNCE : null
|
||||||
});
|
});
|
||||||
|
|
||||||
marker[i].customType = property.type; //'flag' 또는 'map-marker'
|
marker[i].customType = property.type; //'flag' 또는 'map-marker'
|
||||||
|
|
@ -1902,11 +1993,11 @@ function popup(){
|
||||||
markerwindow[i].close();
|
markerwindow[i].close();
|
||||||
});
|
});
|
||||||
|
|
||||||
let content = buildContent(property);
|
|
||||||
marker[i].addListener('click', function() {
|
marker[i].addListener('click', function() {
|
||||||
if(overlay[i]) {
|
if(overlay[i]) {
|
||||||
overlay[i].setMap(null);
|
overlay[i].setMap(null);
|
||||||
}
|
}
|
||||||
|
let content = buildContent(property); // 클릭 시점 생성 → closedNowSet(닫힘 배지) 반영
|
||||||
overlay[i] = new CustomOverlay(map, marker[i].getPosition(), content.innerHTML, i);
|
overlay[i] = new CustomOverlay(map, marker[i].getPosition(), content.innerHTML, i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1947,20 +2038,43 @@ function popup(){
|
||||||
const size = Math.min(50, 30 + Math.log(count) * 10);
|
const size = Math.min(50, 30 + Math.log(count) * 10);
|
||||||
const textColor = (String(topColor).toUpperCase() === "#FFFDD5") ? "#000" : "#fff";
|
const textColor = (String(topColor).toUpperCase() === "#FFFDD5") ? "#000" : "#fff";
|
||||||
|
|
||||||
|
// 클러스터 내 닫힘 마커: 남은시간 최대값을 알약으로 표시(느낌표 최소화).
|
||||||
|
// 시간이 있는 닫힘이 하나도 없고 "오늘 더 안 열림"만 있으면 !
|
||||||
|
const closedProps = markers.map(m => m.property).filter(p => isClosedNowProp(p));
|
||||||
|
const closedMins = closedProps
|
||||||
|
.map(p => closedMinutesFor(p.customerid))
|
||||||
|
.filter(v => v != null && v > 0);
|
||||||
|
const clusterMaxLabel = closedMins.length ? remainLabel(Math.max.apply(null, closedMins)) : null;
|
||||||
|
|
||||||
|
let W = 34;
|
||||||
|
let closedBadgeSvg = '';
|
||||||
|
if (clusterMaxLabel) {
|
||||||
|
const pw = Math.max(22, 12 + clusterMaxLabel.length * 7);
|
||||||
|
W = Math.max(34, 21 + pw + 2);
|
||||||
|
closedBadgeSvg =
|
||||||
|
`<rect x="21" y="0.5" width="${pw}" height="15" rx="7.5" fill="#f0ad4e" stroke="#ffffff" stroke-width="1"/>
|
||||||
|
<text x="${21 + pw / 2}" y="12" text-anchor="middle" font-size="11" font-weight="bold" font-family="${BADGE_FONT}" fill="#5a3d00">${clusterMaxLabel}</text>`;
|
||||||
|
} else if (closedProps.length > 0) {
|
||||||
|
closedBadgeSvg =
|
||||||
|
`<circle cx="27" cy="7" r="6.5" fill="#e53935" stroke="#fff" stroke-width="1.5"/>
|
||||||
|
<text x="27" y="10.5" text-anchor="middle" font-size="9" font-weight="bold" font-family="${BADGE_FONT}" fill="#fff">!</text>`;
|
||||||
|
}
|
||||||
|
|
||||||
return new google.maps.Marker({
|
return new google.maps.Marker({
|
||||||
position,
|
position,
|
||||||
icon: {
|
icon: {
|
||||||
url:
|
url:
|
||||||
"data:image/svg+xml;charset=UTF-8," +
|
"data:image/svg+xml;charset=UTF-8," +
|
||||||
encodeURIComponent(`
|
encodeURIComponent(`
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30">
|
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="34">
|
||||||
<circle cx="15" cy="15" r="13"
|
<circle cx="15" cy="15" r="13"
|
||||||
fill="${topColor}" />
|
fill="${topColor}" />
|
||||||
<text x="15" y="20" text-anchor="middle"
|
<text x="15" y="20" text-anchor="middle"
|
||||||
font-size="12" fill="${textColor}">${count}</text>
|
font-size="12" font-family="${BADGE_FONT}" fill="${textColor}">${count}</text>
|
||||||
|
${closedBadgeSvg}
|
||||||
</svg>
|
</svg>
|
||||||
`),
|
`),
|
||||||
scaledSize: new google.maps.Size(30, 30),
|
scaledSize: new google.maps.Size(W, 34),
|
||||||
anchor: new google.maps.Point(15, 15),
|
anchor: new google.maps.Point(15, 15),
|
||||||
},
|
},
|
||||||
zIndex: Number(google.maps.Marker.MAX_ZINDEX) + count,
|
zIndex: Number(google.maps.Marker.MAX_ZINDEX) + count,
|
||||||
|
|
@ -1970,7 +2084,7 @@ function popup(){
|
||||||
markers.some(m =>
|
markers.some(m =>
|
||||||
m.property &&
|
m.property &&
|
||||||
m.property.rnote && /\S/.test(m.property.rnote) &&
|
m.property.rnote && /\S/.test(m.property.rnote) &&
|
||||||
m.property.color !== "#7B7A7A"
|
m.property.d_status !== 'F'
|
||||||
)
|
)
|
||||||
) ? google.maps.Animation.BOUNCE : null,
|
) ? google.maps.Animation.BOUNCE : null,
|
||||||
});
|
});
|
||||||
|
|
@ -2016,14 +2130,24 @@ function popup(){
|
||||||
|
|
||||||
let html =
|
let html =
|
||||||
"<div style='min-width:220px;max-height:240px;overflow:auto'>";
|
"<div style='min-width:220px;max-height:240px;overflow:auto'>";
|
||||||
|
|
||||||
markersInCluster.forEach((m) => {
|
markersInCluster.forEach((m) => {
|
||||||
const p = m.property || {};
|
const p = m.property || {};
|
||||||
const hasRnote = p.rnote && /\S/.test(p.rnote);
|
const hasRnote = p.rnote && /\S/.test(p.rnote);
|
||||||
|
const isClosed = isClosedNowProp(p);
|
||||||
|
const openInLbl = isClosed ? remainLabel(closedMinutesFor(p.customerid)) : null;
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="cluster-item" data-marker-index="${p.index}" style="display:flex; align-items:center; margin-bottom:4px; cursor:pointer;">
|
<div class="cluster-item" data-marker-index="${p.index}" style="display:flex; align-items:center; margin-bottom:4px; cursor:pointer;">
|
||||||
<span style="width:15px; height:15px; margin-right:6px; background-color:${p.color || "#ccc"}; display:inline-block; border:1px solid #999;"></span>
|
<span style="width:15px; height:15px; margin-right:6px; background-color:${p.color || "#ccc"}; display:inline-block; border:1px solid #999;"></span>
|
||||||
<strong>${p.store ?? "—"}</strong>
|
<strong>${p.store ?? "—"}</strong>
|
||||||
|
${
|
||||||
|
isClosed
|
||||||
|
? (openInLbl
|
||||||
|
? `<span title="Opens in ${openInLbl}" style="margin-left:6px; display:inline-flex; align-items:center; padding:0 7px; height:16px; border-radius:8px; background:#f0ad4e; color:#5a3d00; font-size:11px; font-weight:bold;">${openInLbl}</span>`
|
||||||
|
: `<span title="Closed today" style="margin-left:6px; display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; border-radius:8px; background:#e53935; color:#fff; font-size:11px; font-weight:bold;">!</span>`)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
${
|
${
|
||||||
hasRnote
|
hasRnote
|
||||||
? `<span style="margin-left:6px; padding:2px 4px; background:#FF9800; color:white; border-radius:3px; font-size:10px;">
|
? `<span style="margin-left:6px; padding:2px 4px; background:#FF9800; color:white; border-radius:3px; font-size:10px;">
|
||||||
|
|
@ -2073,14 +2197,18 @@ function popup(){
|
||||||
clusterer.addListener('clusteringend', () => {
|
clusterer.addListener('clusteringend', () => {
|
||||||
marker.forEach(m => {
|
marker.forEach(m => {
|
||||||
if (!m.getMap()) return;
|
if (!m.getMap()) return;
|
||||||
if (m.property?.rnote && /\S/.test(m.property.rnote) && m.property.color !== "#7B7A7A") {
|
if (m.property?.rnote && /\S/.test(m.property.rnote) && m.property.d_status !== 'F') {
|
||||||
m.setAnimation(google.maps.Animation.BOUNCE);
|
m.setAnimation(google.maps.Animation.BOUNCE);
|
||||||
} else {
|
} else {
|
||||||
m.setAnimation(null);
|
m.setAnimation(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// 클러스터로 숨겨진 마커의 닫힘 배지도 같이 숨김/표시
|
||||||
|
if (typeof syncClosedBadges === 'function') syncClosedBadges();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyClosedBadges();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showInfoWindow(position, title, quantity, infoType) {
|
function showInfoWindow(position, title, quantity, infoType) {
|
||||||
|
|
@ -2245,8 +2373,8 @@ function popup(){
|
||||||
markerwindow[parseInt(json.markerIndex)].close();
|
markerwindow[parseInt(json.markerIndex)].close();
|
||||||
});
|
});
|
||||||
|
|
||||||
let content = buildContent(json.property);
|
|
||||||
marker[parseInt(json.markerIndex)].addListener('click', function() {
|
marker[parseInt(json.markerIndex)].addListener('click', function() {
|
||||||
|
let content = buildContent(json.property); // 클릭 시점 생성 → closedNowSet 반영
|
||||||
overlay[parseInt(json.markerIndex)] = new CustomOverlay(map, marker[parseInt(json.markerIndex)].getPosition(), content.innerHTML, parseInt(json.markerIndex));
|
overlay[parseInt(json.markerIndex)] = new CustomOverlay(map, marker[parseInt(json.markerIndex)].getPosition(), content.innerHTML, parseInt(json.markerIndex));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2362,6 +2490,10 @@ function popup(){
|
||||||
jQuery("#customerShortInfo #c_comment_ri,#customerShortInfo #c_location").val(loadingTxt).attr("disabled","disabled");
|
jQuery("#customerShortInfo #c_comment_ri,#customerShortInfo #c_location").val(loadingTxt).attr("disabled","disabled");
|
||||||
jQuery("#customerShortInfo .btn-sub").attr("disabled","disabled");
|
jQuery("#customerShortInfo .btn-sub").attr("disabled","disabled");
|
||||||
|
|
||||||
|
// Access Time / Closing Days 표시 조각 로드
|
||||||
|
jQuery("#customerShortInfo .c_uid").val(id);
|
||||||
|
if (typeof loadCustomerAccess === 'function') loadCustomerAccess(id);
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
"id" : id
|
"id" : id
|
||||||
}
|
}
|
||||||
|
|
@ -2801,7 +2933,7 @@ function popup(){
|
||||||
"quantity" : quantity_array
|
"quantity" : quantity_array
|
||||||
}
|
}
|
||||||
|
|
||||||
api('inqNearby', data, rstInqPoint);
|
api('inqNearby', data, rstInqPoint);
|
||||||
|
|
||||||
smoothZoom(map, 14, map.getZoom());
|
smoothZoom(map, 14, map.getZoom());
|
||||||
offLoading();
|
offLoading();
|
||||||
|
|
@ -2924,9 +3056,140 @@ function popup(){
|
||||||
jQuery("#map-modal-history tbody").html(output);
|
jQuery("#map-modal-history tbody").html(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 지금 닫힘(closed now) 고객 집합 + 상세(다음 오픈까지 남은 분 등)
|
||||||
|
window.closedNowSet = window.closedNowSet || new Set();
|
||||||
|
window.closedNowInfo = window.closedNowInfo || {}; // { customerid: {open, next_open, minutes_until_open, ...} }
|
||||||
|
|
||||||
|
// 고객의 "다음 오픈까지 남은 분" (없으면 null)
|
||||||
|
function closedMinutesFor(cid) {
|
||||||
|
var d = window.closedNowInfo && window.closedNowInfo[String(cid)];
|
||||||
|
return (d && typeof d.minutes_until_open === 'number') ? d.minutes_until_open : null;
|
||||||
|
}
|
||||||
|
// 남은 분 → 짧은 라벨. 오늘 더 안 열면(null/0 이하) null → '!' 로 표시됨
|
||||||
|
function remainLabel(mins) {
|
||||||
|
if (mins == null || mins <= 0) return null;
|
||||||
|
if (mins < 60) return mins + 'm';
|
||||||
|
var h = Math.floor(mins / 60), m = mins % 60;
|
||||||
|
return m ? (h + 'h ' + m + 'm') : (h + 'h');
|
||||||
|
}
|
||||||
|
// 뱃지 텍스트 폰트 (HTML 배지와 통일). 실제 UI 폰트에 맞게 바꿔도 됨.
|
||||||
|
var BADGE_FONT = "Arial, Helvetica, sans-serif";
|
||||||
|
|
||||||
|
// 남은시간 앰버 알약 배지 SVG
|
||||||
|
function timeBadgeSvg(label) {
|
||||||
|
var w = Math.max(22, 12 + label.length * 7);
|
||||||
|
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="18">' +
|
||||||
|
'<rect x="0.5" y="0.5" width="' + (w - 1) + '" height="17" rx="8.5" fill="#f0ad4e" stroke="#ffffff" stroke-width="1"/>' +
|
||||||
|
'<text x="' + (w / 2) + '" y="13" text-anchor="middle" font-size="12" font-weight="bold" font-family="' + BADGE_FONT + '" fill="#5a3d00">' + label + '</text>' +
|
||||||
|
'</svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 닫힘 배지(빨간 원 + 흰 !) 아이콘
|
||||||
|
var CLOSED_BADGE_SVG = 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20">' +
|
||||||
|
'<circle cx="10" cy="10" r="8.5" fill="#e53935" stroke="#ffffff" stroke-width="2"/>' +
|
||||||
|
'<text x="10" y="14.5" text-anchor="middle" font-size="13" font-weight="bold" font-family="' + BADGE_FONT + '" fill="#ffffff">!</text>' +
|
||||||
|
'</svg>');
|
||||||
|
|
||||||
|
// 깃발 대비 배지 위치.
|
||||||
|
// 깃발 심볼은 anchor (0,0) → latlng 이 깃발 좌상단, 깃발은 우/하로 그려짐(약 17x20px).
|
||||||
|
// 배지 아이콘 20x20 기준, 배지 중심 화면오프셋 = (10 - x, 10 - y).
|
||||||
|
// 아래 값 = 배지 중심이 latlng 기준 (17, -1) → 깃발 배너 우상단.
|
||||||
|
// 깃발과 안 맞으면 x/y 두 숫자만 조정.
|
||||||
|
var BADGE_ANCHOR = { x: -7, y: 11 };
|
||||||
|
|
||||||
|
var closedBadgeMarker = {}; // markerArrayIndex -> 배지 google.maps.Marker
|
||||||
|
|
||||||
|
function makeClosedBadge(pos, label) {
|
||||||
|
var icon;
|
||||||
|
if (label) {
|
||||||
|
var w = Math.max(22, 12 + label.length * 7);
|
||||||
|
icon = {
|
||||||
|
url: timeBadgeSvg(label),
|
||||||
|
scaledSize: new google.maps.Size(w, 18),
|
||||||
|
anchor: new google.maps.Point(-6, 15), // 깃발 우상단, 알약 좌측 기준
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
icon = {
|
||||||
|
url: CLOSED_BADGE_SVG,
|
||||||
|
scaledSize: new google.maps.Size(20, 20),
|
||||||
|
anchor: new google.maps.Point(BADGE_ANCHOR.x, BADGE_ANCHOR.y),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return new google.maps.Marker({
|
||||||
|
position: pos,
|
||||||
|
map: map,
|
||||||
|
clickable: false,
|
||||||
|
zIndex: Number(google.maps.Marker.MAX_ZINDEX) + 1,
|
||||||
|
icon: icon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 클러스터로 숨겨진(부모 마커가 map 에서 빠진) 배지는 같이 숨김
|
||||||
|
function syncClosedBadges() {
|
||||||
|
Object.keys(closedBadgeMarker).forEach(function (idx) {
|
||||||
|
var b = closedBadgeMarker[idx];
|
||||||
|
var m = marker[idx];
|
||||||
|
if (b && m) b.setMap(m.getMap() ? map : null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// "지금 닫힘" 표시 대상인지 (Finished = d_status 'F' 는 제외)
|
||||||
|
function isClosedNowProp(p) {
|
||||||
|
return !!(p && p.d_status !== 'F' &&
|
||||||
|
window.closedNowSet && window.closedNowSet.has(String(p.customerid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마커들의 고객 id 로 "지금 닫힘" 조회 → 개별 배지 + 클러스터 재렌더
|
||||||
|
function applyClosedBadges() {
|
||||||
|
var ids = [];
|
||||||
|
marker.forEach(function (m) {
|
||||||
|
if (m && m.property && m.property.customerid != null) ids.push(String(m.property.customerid));
|
||||||
|
});
|
||||||
|
ids = Array.from(new Set(ids));
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
|
||||||
|
jQuery.ajax({
|
||||||
|
url: '/lib/customer_open_status.php',
|
||||||
|
method: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
data: { ids: ids.join(',') },
|
||||||
|
success: function (res) {
|
||||||
|
window.closedNowSet = new Set(((res && res.closed) ? res.closed : []).map(String));
|
||||||
|
window.closedNowInfo = (res && res.detail) ? res.detail : {};
|
||||||
|
|
||||||
|
// 기존 배지 제거
|
||||||
|
Object.keys(closedBadgeMarker).forEach(function (idx) {
|
||||||
|
if (closedBadgeMarker[idx]) closedBadgeMarker[idx].setMap(null);
|
||||||
|
});
|
||||||
|
closedBadgeMarker = {};
|
||||||
|
|
||||||
|
// 닫힌 마커마다 배지 생성 (오늘 이따 열면 남은시간, 아니면 !)
|
||||||
|
marker.forEach(function (m, idx) {
|
||||||
|
if (!m || !m.property) return;
|
||||||
|
if (isClosedNowProp(m.property)) {
|
||||||
|
var lbl = remainLabel(closedMinutesFor(m.property.customerid));
|
||||||
|
closedBadgeMarker[idx] = makeClosedBadge(m.getPosition(), lbl);
|
||||||
|
m.setTitle(lbl ? ('Opens in ' + lbl) : 'Closed today');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
syncClosedBadges();
|
||||||
|
|
||||||
|
// 클러스터 아이콘도 ! 반영되도록 재렌더
|
||||||
|
if (typeof clusterer !== 'undefined' && clusterer && typeof clusterer.render === 'function') {
|
||||||
|
clusterer.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function buildContent(property) {
|
function buildContent(property) {
|
||||||
let content = document.createElement("div");
|
let content = document.createElement("div");
|
||||||
const chipsHtml = renderContainerChips(property.containers);
|
const chipsHtml = renderContainerChips(property.containers);
|
||||||
|
const closedBadge = isClosedNowProp(property)
|
||||||
|
? '<span class="closed-badge">!</span>' : '';
|
||||||
|
|
||||||
content.classList.add("property");
|
content.classList.add("property");
|
||||||
let innerHtml = `
|
let innerHtml = `
|
||||||
|
|
@ -3002,7 +3265,7 @@ function popup(){
|
||||||
data-target="#map-modal-info"
|
data-target="#map-modal-info"
|
||||||
onClick="showShortInfo(${property.customerid});">
|
onClick="showShortInfo(${property.customerid});">
|
||||||
<i class="fa-solid fa-question mapBtn-ico"></i>
|
<i class="fa-solid fa-question mapBtn-ico"></i>
|
||||||
<div class="mapBtn-txt">Info</div>
|
<div class="mapBtn-txt">Info ${closedBadge}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?php
|
||||||
|
/* =====================================================================
|
||||||
|
* /lib/customer_access_display.php
|
||||||
|
* - Access Time / Closing Days 읽기 전용 표시 조각 (두 개의 <tr> 반환)
|
||||||
|
* - customer_detail.php 와 동일한 칩 스타일 (CSS 는 부모 화면이 보유)
|
||||||
|
* - AJAX 로 모달 등에 주입해서 재사용 (map.php 등)
|
||||||
|
* - 각 박스는 .box-clickable[data-popup] 이라 부모의 클릭 핸들러가 편집 팝업을 연다
|
||||||
|
* ===================================================================== */
|
||||||
|
|
||||||
|
include getenv("DOCUMENT_ROOT")."/include/session_include.php";
|
||||||
|
require_once getenv("DOCUMENT_ROOT")."/assets/dbCon.php";
|
||||||
|
require_once getenv("DOCUMENT_ROOT")."/lib/customer_open_status.php";
|
||||||
|
|
||||||
|
$c_uid = isset($_GET['c_uid']) ? intval($_GET['c_uid']) : 0;
|
||||||
|
|
||||||
|
// 지금 닫힘 여부 (Access Time 제목 옆 경고 배지용)
|
||||||
|
$openStat = ($c_uid > 0) ? customerIsOpenNow($c_uid) : ['restricted'=>false,'open'=>true,'reason'=>''];
|
||||||
|
$closedNow = (!empty($openStat['restricted']) && empty($openStat['open']));
|
||||||
|
|
||||||
|
// Restricted 토글 현재값
|
||||||
|
$isRestricted = false;
|
||||||
|
if ($c_uid > 0) {
|
||||||
|
$rR = qry("SELECT c_time_restricted AS f FROM tbl_customer WHERE c_uid = $c_uid LIMIT 1");
|
||||||
|
$rowR = fetch_array($rR);
|
||||||
|
$isRestricted = ($rowR && $rowR['f'] === 'Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
$accessTimeMap = [];
|
||||||
|
$closingDays = [];
|
||||||
|
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)) { $accessTimeMap[(int)$row['ca_day_of_week']][] = $row; }
|
||||||
|
|
||||||
|
// 지나간 휴무(종료일 < 오늘)는 제외
|
||||||
|
$res2 = qry("SELECT ccd_start_date, ccd_end_date, ccd_note
|
||||||
|
FROM tbl_customer_closing_days
|
||||||
|
WHERE ccd_customer_uid = $c_uid AND ccd_end_date >= CURDATE()
|
||||||
|
ORDER BY ccd_start_date");
|
||||||
|
while ($row = fetch_array($res2)) { $closingDays[] = $row; }
|
||||||
|
}
|
||||||
|
|
||||||
|
$dowNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
||||||
|
$todayDow = (int)date('w'); // 0=Sun .. 6=Sat (서버 기준, open-status 와 동일)
|
||||||
|
if (!function_exists('fmtAccT')) { function fmtAccT($t){ return $t ? substr($t, 0, 5) : ''; } }
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="td-title-info">Access Time<?php if ($closedNow): ?> <span class="closed-badge" title="Closed right now">!</span><?php endif; ?><span class="restricted-toggle" style="float:right; font-weight:normal; font-size:13px;">Restricted <input type="checkbox" class="ca-restricted-chk" data-cuid="<?=$c_uid?>" <?= $isRestricted ? 'checked' : '' ?>></span></td>
|
||||||
|
<td colspan="2" class="td-text-info">
|
||||||
|
<div class="at-field box-clickable" data-popup="accesstime" title="Click to edit">
|
||||||
|
<div class="at-wrap">
|
||||||
|
<?php for ($d = 0; $d < 7; $d++):
|
||||||
|
$wins = isset($accessTimeMap[$d]) ? $accessTimeMap[$d] : [];
|
||||||
|
?>
|
||||||
|
<div class="at-row<?= empty($wins) ? ' at-off' : '' ?><?= ($d === $todayDow) ? ' at-today' : '' ?>">
|
||||||
|
<span class="at-dow<?= ($d == 0 || $d == 6) ? ' at-wknd' : '' ?>"><?=$dowNames[$d]?></span>
|
||||||
|
<span class="at-win">
|
||||||
|
<?php if (empty($wins)): ?>
|
||||||
|
<span class="at-dash">—</span>
|
||||||
|
<?php else: foreach ($wins as $w):
|
||||||
|
$o = fmtAccT($w['ca_open_time']);
|
||||||
|
$c = fmtAccT($w['ca_close_time']);
|
||||||
|
$label = $c ? ($o.'–'.$c) : ($o.' →');
|
||||||
|
?>
|
||||||
|
<span class="at-chip"<?= !empty($w['ca_note']) ? ' title="'.htmlspecialchars($w['ca_note'], ENT_QUOTES).'"' : '' ?>><?=$label?></span>
|
||||||
|
<?php endforeach; endif; ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="td-title-info">Closing Days</td>
|
||||||
|
<td colspan="2" class="td-text-info">
|
||||||
|
<div class="cd-field box-clickable" data-popup="closingdays" title="Click to edit">
|
||||||
|
<div class="cd-wrap">
|
||||||
|
<?php if (empty($closingDays)): ?>
|
||||||
|
<span class="cd-empty">—</span>
|
||||||
|
<?php else: foreach ($closingDays as $cd):
|
||||||
|
$s = $cd['ccd_start_date'];
|
||||||
|
$e = $cd['ccd_end_date'];
|
||||||
|
$range = ($s === $e) ? $s : ($s.' ~ '.$e);
|
||||||
|
?>
|
||||||
|
<span class="cd-chip">
|
||||||
|
<span class="cd-range"><?=$range?></span>
|
||||||
|
<?php if (!empty($cd['ccd_note'])): ?><em class="cd-note"><?=htmlspecialchars($cd['ccd_note'])?></em><?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<?php endforeach; endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* Restricted 체크박스 → c_time_restricted 즉시 저장 (map / shortInfo 공용)
|
||||||
|
- 조각이 .load() 로 주입될 때마다 이 스크립트가 실행되지만,
|
||||||
|
window 플래그로 문서 위임 바인딩은 한 번만 건다. */
|
||||||
|
if (!window.__caRestrictedBound) {
|
||||||
|
window.__caRestrictedBound = true;
|
||||||
|
jQuery(document).on('change', '.ca-restricted-chk', function () {
|
||||||
|
var $chk = jQuery(this);
|
||||||
|
var cuid = $chk.data('cuid');
|
||||||
|
var val = this.checked ? 'Y' : 'N';
|
||||||
|
if (!cuid) return;
|
||||||
|
$chk.prop('disabled', true);
|
||||||
|
jQuery.ajax({
|
||||||
|
url: '/assets/api/customer.php',
|
||||||
|
method: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
data: { action: 'patchTimeRestricted', c_uid: cuid, value: val },
|
||||||
|
success: function (res) {
|
||||||
|
if (res && res.ok) {
|
||||||
|
// 표시 조각 다시 로드 → "!" 배지/상태 갱신
|
||||||
|
var $tb = $chk.closest('tbody');
|
||||||
|
if ($tb.attr('id')) {
|
||||||
|
$tb.load('/lib/customer_access_display.php?c_uid=' + encodeURIComponent(cuid));
|
||||||
|
} else {
|
||||||
|
$chk.prop('disabled', false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$chk.prop('checked', !$chk.prop('checked')).prop('disabled', false);
|
||||||
|
alert('Failed to update Restricted.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$chk.prop('checked', !$chk.prop('checked')).prop('disabled', false);
|
||||||
|
alert('Failed to update Restricted.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
<?php
|
||||||
|
/* =====================================================================
|
||||||
|
* /lib/customer_closing_days_popup.php
|
||||||
|
* - Customer Closing Days (closure date ranges) 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" within current/future only; past ranges are not shown and are left untouched
|
||||||
|
* ===================================================================== */
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
$clean = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$start = trim($r['start'] ?? '');
|
||||||
|
$end = trim($r['end'] ?? '');
|
||||||
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $start)) continue; // start date required
|
||||||
|
if ($end === '') $end = $start; // empty end = same day
|
||||||
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $end)) continue;
|
||||||
|
if ($end < $start) { $tmp = $start; $start = $end; $end = $tmp; } // auto-fix if reversed
|
||||||
|
|
||||||
|
$note = isset($r['note']) ? trim($r['note']) : '';
|
||||||
|
$note = utf8_cut($note, 200);
|
||||||
|
|
||||||
|
$clean[] = ['start' => $start, 'end' => $end, 'note' => $note];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($clean, function ($a, $b) { return strcmp($a['start'], $b['start']); });
|
||||||
|
|
||||||
|
$uEsc = addslashes($user);
|
||||||
|
|
||||||
|
// full replace
|
||||||
|
// full replace — 현재/미래만 관리, 과거 행은 건드리지 않음
|
||||||
|
qry("DELETE FROM tbl_customer_closing_days
|
||||||
|
WHERE ccd_customer_uid = " . $c_uid . " AND ccd_end_date >= CURDATE()");
|
||||||
|
|
||||||
|
foreach ($clean as $r) {
|
||||||
|
$noteV = ($r['note'] === '') ? "NULL" : "'" . addslashes($r['note']) . "'";
|
||||||
|
qry("INSERT INTO tbl_customer_closing_days
|
||||||
|
(ccd_customer_uid, ccd_start_date, ccd_end_date, ccd_note, ccd_createdby, ccd_created_at)
|
||||||
|
VALUES
|
||||||
|
($c_uid, '{$r['start']}', '{$r['end']}', $noteV, '$uEsc', NOW())");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
* GET: load existing (current/future only — past is not shown)
|
||||||
|
* ===================================================================== */
|
||||||
|
$existing = [];
|
||||||
|
if ($c_uid > 0) {
|
||||||
|
$res = qry("SELECT ccd_start_date, ccd_end_date, ccd_note
|
||||||
|
FROM tbl_customer_closing_days
|
||||||
|
WHERE ccd_customer_uid = $c_uid AND ccd_end_date >= CURDATE()
|
||||||
|
ORDER BY ccd_start_date");
|
||||||
|
while ($row = fetch_array($res)) {
|
||||||
|
$existing[] = [
|
||||||
|
'start' => $row['ccd_start_date'],
|
||||||
|
'end' => $row['ccd_end_date'],
|
||||||
|
'note' => $row['ccd_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>Closing Days</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; }
|
||||||
|
.toolbar { display:flex; gap:8px; flex-wrap:wrap; margin-bottom: 10px; }
|
||||||
|
.add { border: 1px solid #7fb069; background: #7fb069; color: #fff; border-radius: 4px; font-size: 13px; padding: 6px 12px; cursor: pointer; }
|
||||||
|
.add:hover { background: #6da356; }
|
||||||
|
.rows { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.empty { color: #bbb; font-size: 13px; padding: 6px 0; }
|
||||||
|
.row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 4px 0; }
|
||||||
|
.row input { padding: 5px 7px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; }
|
||||||
|
.row input[type=date] { width: 150px; }
|
||||||
|
.row input.note { flex: 1; min-width: 120px; }
|
||||||
|
.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>Closing Days</h2>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<button type="button" class="add" id="addBtn">+ Add closing</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rows" id="rows"></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 existing = <?=$existingJson?> || [];
|
||||||
|
const C_UID = <?=$c_uid?>;
|
||||||
|
|
||||||
|
const rowsEl = document.getElementById('rows');
|
||||||
|
|
||||||
|
function esc(s){ return (s||'').replace(/"/g,'"'); }
|
||||||
|
|
||||||
|
function makeRow(d) {
|
||||||
|
d = d || { start: '', end: '', note: '' };
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'row';
|
||||||
|
row.innerHTML =
|
||||||
|
'<input type="date" class="start" value="' + (d.start || '') + '">' +
|
||||||
|
'<span class="tilde">~</span>' +
|
||||||
|
'<input type="date" class="end" value="' + (d.end || '') + '">' +
|
||||||
|
'<input type="text" class="note" maxlength="200" placeholder="Note (optional)" value="' + esc(d.note) + '">' +
|
||||||
|
'<button type="button" class="rm" title="Delete">×</button>';
|
||||||
|
row.querySelector('.rm').addEventListener('click', function () { row.remove(); refreshEmpty(); });
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshEmpty() {
|
||||||
|
const has = rowsEl.querySelector('.row');
|
||||||
|
let emptyEl = rowsEl.querySelector('.empty');
|
||||||
|
if (!has && !emptyEl) {
|
||||||
|
emptyEl = document.createElement('div');
|
||||||
|
emptyEl.className = 'empty';
|
||||||
|
emptyEl.textContent = 'No closing days.';
|
||||||
|
rowsEl.appendChild(emptyEl);
|
||||||
|
} else if (has && emptyEl) {
|
||||||
|
emptyEl.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.forEach(function (d) { rowsEl.appendChild(makeRow(d)); });
|
||||||
|
refreshEmpty();
|
||||||
|
|
||||||
|
document.getElementById('addBtn').addEventListener('click', function () { rowsEl.appendChild(makeRow()); refreshEmpty(); });
|
||||||
|
|
||||||
|
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 = false;
|
||||||
|
document.querySelectorAll('.row').forEach(function (row) {
|
||||||
|
const start = row.querySelector('.start').value.trim();
|
||||||
|
let end = row.querySelector('.end').value.trim();
|
||||||
|
const note = row.querySelector('.note').value.trim();
|
||||||
|
if (!start) return;
|
||||||
|
if (!end) end = start;
|
||||||
|
if (end < start) bad = true;
|
||||||
|
rows.push({ start: start, end: end, note: note });
|
||||||
|
});
|
||||||
|
if (bad) { alert('Some rows have an end date earlier than the start date.'); 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>
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?php
|
||||||
|
/* =====================================================================
|
||||||
|
* /lib/customer_open_status.php
|
||||||
|
* "지금 이 고객이 오픈인가?" 판정 — 라이브러리 + 엔드포인트 겸용
|
||||||
|
*
|
||||||
|
* [라이브러리로 쓸 때] 다른 파일에서 include/require 하면 함수만 제공됨
|
||||||
|
* require_once getenv("DOCUMENT_ROOT")."/lib/customer_open_status.php";
|
||||||
|
* $st = customerIsOpenNow($c_uid); // 단일
|
||||||
|
* $map = customersOpenStatus([13013,13014]); // 배치
|
||||||
|
* (호출 전에 dbCon.php(qry/fetch_array) 가 로드돼 있어야 함)
|
||||||
|
*
|
||||||
|
* [엔드포인트로 쓸 때] 이 파일을 직접 호출하면 JSON 반환
|
||||||
|
* POST/GET ids=13013,13014,... → { "closed":[...], "detail":{...} }
|
||||||
|
*
|
||||||
|
* 판정 규칙:
|
||||||
|
* · c_time_restricted != 'Y' → open=true (경고 없음)
|
||||||
|
* · tbl_customer_accesstime 데이터 없음 → open=true (항상 열림)
|
||||||
|
* · 오늘이 휴무(closing day) → open=false (closing_day)
|
||||||
|
* · 오늘 요일에 창이 하나도 없음 → open=false (day_off)
|
||||||
|
* · 현재 시각이 어떤 창(open~close)에 포함 → open=true (close=NULL 이면 오픈엔드)
|
||||||
|
* · 그 외 → open=false (outside_hours)
|
||||||
|
* ===================================================================== */
|
||||||
|
|
||||||
|
if (!function_exists('customersOpenStatus')) {
|
||||||
|
|
||||||
|
function customersOpenStatus($ids, $now = null) {
|
||||||
|
$ids = array_values(array_unique(array_filter(
|
||||||
|
array_map('intval', (array)$ids),
|
||||||
|
function ($x) { return $x > 0; })));
|
||||||
|
$out = [];
|
||||||
|
if (empty($ids)) return $out;
|
||||||
|
|
||||||
|
if ($now === null) $now = time();
|
||||||
|
$dow = (int)date('w', $now); // 0=Sun .. 6=Sat
|
||||||
|
$today = date('Y-m-d', $now);
|
||||||
|
$hm = date('H:i:s', $now);
|
||||||
|
$inList = implode(',', $ids);
|
||||||
|
|
||||||
|
// 기본값: 제한 없음(열림)
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$out[$id] = ['restricted' => false, 'open' => true, 'reason' => 'no_restriction'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// c_time_restricted='Y' 인 고객만 대상 (1차 게이트)
|
||||||
|
$restrictedFlag = [];
|
||||||
|
$r = qry("SELECT c_uid AS id FROM tbl_customer
|
||||||
|
WHERE c_uid IN ($inList) AND c_time_restricted = 'Y'");
|
||||||
|
while ($row = fetch_array($r)) { $restrictedFlag[(int)$row['id']] = true; }
|
||||||
|
|
||||||
|
// 액세스타임 사용 여부
|
||||||
|
$usesAT = [];
|
||||||
|
$r = qry("SELECT DISTINCT ca_customer_uid AS id
|
||||||
|
FROM tbl_customer_accesstime
|
||||||
|
WHERE ca_customer_uid IN ($inList) AND ca_status = 'A'");
|
||||||
|
while ($row = fetch_array($r)) { $usesAT[(int)$row['id']] = true; }
|
||||||
|
|
||||||
|
// 오늘 휴무인 고객
|
||||||
|
$closedToday = [];
|
||||||
|
$r = qry("SELECT DISTINCT ccd_customer_uid AS id
|
||||||
|
FROM tbl_customer_closing_days
|
||||||
|
WHERE ccd_customer_uid IN ($inList)
|
||||||
|
AND '$today' BETWEEN ccd_start_date AND ccd_end_date");
|
||||||
|
while ($row = fetch_array($r)) { $closedToday[(int)$row['id']] = true; }
|
||||||
|
|
||||||
|
// 오늘 요일의 창
|
||||||
|
$windows = []; // id => [ [open, close], ... ]
|
||||||
|
$r = qry("SELECT ca_customer_uid AS id, ca_open_time AS o, ca_close_time AS c
|
||||||
|
FROM tbl_customer_accesstime
|
||||||
|
WHERE ca_customer_uid IN ($inList) AND ca_status = 'A' AND ca_day_of_week = $dow");
|
||||||
|
while ($row = fetch_array($r)) { $windows[(int)$row['id']][] = [$row['o'], $row['c']]; }
|
||||||
|
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
// (1) c_time_restricted='Y' 아니면 신경 안 씀 → 항상 열림
|
||||||
|
if (empty($restrictedFlag[$id])) {
|
||||||
|
$out[$id] = ['restricted' => false, 'open' => true, 'reason' => 'not_restricted'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// (2) 액세스타임 데이터가 없으면 항상 열림
|
||||||
|
if (empty($usesAT[$id])) {
|
||||||
|
$out[$id] = ['restricted' => false, 'open' => true, 'reason' => 'no_accesstime'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 오늘 휴무
|
||||||
|
if (!empty($closedToday[$id])) {
|
||||||
|
$out[$id] = ['restricted' => true, 'open' => false, 'reason' => 'closing_day'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 오늘 요일 창 없음 → 휴무
|
||||||
|
$wins = isset($windows[$id]) ? $windows[$id] : [];
|
||||||
|
if (empty($wins)) {
|
||||||
|
$out[$id] = ['restricted' => true, 'open' => false, 'reason' => 'day_off'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$open = false;
|
||||||
|
$nextOpen = null; // 오늘 아직 안 열린(현재보다 늦은) 창의 오픈시간 중 가장 이른 것
|
||||||
|
foreach ($wins as $w) {
|
||||||
|
$o = $w[0]; $c = $w[1];
|
||||||
|
if ($o === null || $o === '') continue;
|
||||||
|
if ($c === null || $c === '') { // 오픈엔드
|
||||||
|
if ($hm >= $o) { $open = true; }
|
||||||
|
} else {
|
||||||
|
if ($hm >= $o && $hm <= $c) { $open = true; }
|
||||||
|
}
|
||||||
|
if ($o > $hm) { // 오늘 이따 여는 창
|
||||||
|
if ($nextOpen === null || $o < $nextOpen) $nextOpen = $o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$entry = ['restricted' => true, 'open' => $open, 'reason' => ($open ? '' : 'outside_hours')];
|
||||||
|
if (!$open && $nextOpen !== null) {
|
||||||
|
$entry['next_open'] = substr($nextOpen, 0, 5); // 'HH:MM'
|
||||||
|
$secs = strtotime("$today $nextOpen") - $now;
|
||||||
|
$entry['minutes_until_open'] = max(0, (int)round($secs / 60));
|
||||||
|
}
|
||||||
|
$out[$id] = $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 단일 고객
|
||||||
|
function customerIsOpenNow($id, $now = null) {
|
||||||
|
$r = customersOpenStatus([$id], $now);
|
||||||
|
$id = intval($id);
|
||||||
|
return isset($r[$id]) ? $r[$id]
|
||||||
|
: ['restricted' => false, 'open' => true, 'reason' => 'no_restriction'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
* 이 파일을 "직접 호출"했을 때만 엔드포인트로 동작.
|
||||||
|
* (다른 파일에서 include/require 될 땐 위 함수만 제공하고 여기는 건너뜀)
|
||||||
|
* ------------------------------------------------------------------- */
|
||||||
|
$__self = basename(__FILE__);
|
||||||
|
$__script = isset($_SERVER['SCRIPT_FILENAME']) ? basename($_SERVER['SCRIPT_FILENAME']) : '';
|
||||||
|
if ($__script === $__self) {
|
||||||
|
|
||||||
|
include getenv("DOCUMENT_ROOT")."/include/session_include.php";
|
||||||
|
require_once getenv("DOCUMENT_ROOT")."/assets/dbCon.php";
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$idsRaw = isset($_POST['ids']) ? $_POST['ids'] : (isset($_GET['ids']) ? $_GET['ids'] : '');
|
||||||
|
$ids = is_array($idsRaw) ? $idsRaw : array_filter(explode(',', (string)$idsRaw), 'strlen');
|
||||||
|
|
||||||
|
$detail = customersOpenStatus($ids);
|
||||||
|
|
||||||
|
$closed = [];
|
||||||
|
foreach ($detail as $id => $st) {
|
||||||
|
if (!empty($st['restricted']) && empty($st['open'])) $closed[] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['closed' => $closed, 'detail' => $detail]);
|
||||||
|
}
|
||||||
|
|
@ -137,6 +137,9 @@ if ($c_paymenttype =="CA") {
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<!-- Access Time / Closing Days (표시 조각 AJAX 주입, 클릭 시 편집 팝업) -->
|
||||||
|
<tbody id="siAccessClosing"></tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
<br>
|
<br>
|
||||||
<div class="text-center grid-layout-col-2">
|
<div class="text-center grid-layout-col-2">
|
||||||
|
|
@ -160,6 +163,21 @@ if ($c_paymenttype =="CA") {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ==============================================================
|
||||||
|
Access Time / Closing Days 편집 오버레이 (순수 오버레이)
|
||||||
|
- Bootstrap 모달 미사용: BS3/BS5 focus-trap 충돌 회피
|
||||||
|
- customer_access_time_popup.php / customer_closing_days_popup.php 재사용
|
||||||
|
============================================================== -->
|
||||||
|
<div id="siAccessPopupOverlay" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; z-index:100000; background:rgba(0,0,0,0.5);">
|
||||||
|
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:92%; max-width:760px; background:#fff; border-radius:6px; overflow:hidden; box-shadow:0 10px 40px rgba(0,0,0,0.35);">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; padding:10px 14px; background:#2A9B56; color:#fff;">
|
||||||
|
<h4 id="siAccessPopupTitle" style="margin:0; font-size:16px;">Edit</h4>
|
||||||
|
<button type="button" id="siAccessPopupClose" style="background:none; border:none; color:#fff; font-size:24px; line-height:1; cursor:pointer;">×</button>
|
||||||
|
</div>
|
||||||
|
<iframe id="siAccessPopupFrame" src="about:blank" title="Edit popup" style="width:100%; height:70vh; border:0; display:block;"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|
@ -277,9 +295,57 @@ function popup() {
|
||||||
const url = `/lib/customer_image_upload_popup.php?c_uid=<?=$c_uid?>&goStr=<?=$goStr?>`;
|
const url = `/lib/customer_image_upload_popup.php?c_uid=<?=$c_uid?>&goStr=<?=$goStr?>`;
|
||||||
window.open(url, "uploadImage", "width=600,height=750,scrollbars=yes");
|
window.open(url, "uploadImage", "width=600,height=750,scrollbars=yes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----- Access Time / Closing Days 표시 + 편집 오버레이 ----- */
|
||||||
|
(function () {
|
||||||
|
var C_UID = "<?=$c_uid?>";
|
||||||
|
var popupConfig = {
|
||||||
|
accesstime: { url: '/lib/customer_access_time_popup.php', title: 'Access Time' },
|
||||||
|
closingdays: { url: '/lib/customer_closing_days_popup.php', title: 'Closing Days' }
|
||||||
|
};
|
||||||
|
var popupChanged = false;
|
||||||
|
|
||||||
|
// 이전 로드분 오버레이 제거 후 body 직속으로 이동 (중복/클리핑 방지)
|
||||||
|
jQuery('body > #siAccessPopupOverlay').remove();
|
||||||
|
var $overlay = jQuery('#siAccessPopupOverlay').appendTo(document.body);
|
||||||
|
|
||||||
|
function loadDisplay() {
|
||||||
|
if (!C_UID) { jQuery('#siAccessClosing').empty(); return; }
|
||||||
|
jQuery('#siAccessClosing').load('/lib/customer_access_display.php?c_uid=' + encodeURIComponent(C_UID));
|
||||||
|
}
|
||||||
|
loadDisplay();
|
||||||
|
|
||||||
|
function openPopup(key) {
|
||||||
|
var cfg = popupConfig[key];
|
||||||
|
if (!cfg || !C_UID) return;
|
||||||
|
popupChanged = false;
|
||||||
|
jQuery('#siAccessPopupTitle').text(cfg.title);
|
||||||
|
jQuery('#siAccessPopupFrame').attr('src', cfg.url + '?c_uid=' + encodeURIComponent(C_UID) + '&goStr=');
|
||||||
|
$overlay.css('display', 'block');
|
||||||
|
}
|
||||||
|
function closePopup() {
|
||||||
|
$overlay.css('display', 'none');
|
||||||
|
jQuery('#siAccessPopupFrame').attr('src', 'about:blank');
|
||||||
|
if (popupChanged) { loadDisplay(); popupChanged = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
jQuery(document).on('click', '#siAccessClosing .box-clickable[data-popup]', function () {
|
||||||
|
openPopup(jQuery(this).data('popup'));
|
||||||
|
});
|
||||||
|
jQuery('#siAccessPopupClose').on('click', closePopup);
|
||||||
|
$overlay.on('click', function (e) { if (e.target === this) closePopup(); });
|
||||||
|
|
||||||
|
// 자식 iframe(팝업 PHP)에서 호출
|
||||||
|
window.closeCustomerPopup = function (changed) {
|
||||||
|
if (changed) popupChanged = true;
|
||||||
|
closePopup();
|
||||||
|
};
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/customer_access.css">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.error{
|
.error{
|
||||||
color: red;
|
color: red;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue