File: /var/www/test/Installationlist/form.php
<?php
// ============================================================
// form.php ─ 電子裝機單(第一頁)
// ============================================================
ob_start(); // 捕捉所有意外輸出,防止污染 JSON
session_start();
// ── OTP 寄信 API(fetch 呼叫時 action=send_otp)─────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'send_otp') {
ob_clean(); // 清掉任何已緩衝輸出
header('Content-Type: application/json; charset=utf-8');
$email = trim($_POST['email'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => '電子信箱格式錯誤']);
exit;
}
// 60 秒冷卻
$lastSent = $_SESSION['otp_sent_at'] ?? 0;
if (time() - $lastSent < 60) {
$remain = 60 - (time() - $lastSent);
echo json_encode(['success' => false, 'message' => "請等待 {$remain} 秒後再重新發送"]);
exit;
}
// 產生 6 碼驗證碼
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$_SESSION['otp_code'] = $otp;
$_SESSION['otp_email'] = $email;
$_SESSION['otp_expires'] = time() + 300;
$_SESSION['otp_sent_at'] = time();
$_SESSION['otp_verified']= false;
$fromEmail = 'service@ysnet.com.tw';
$subject = '亞訊寬頻 ─ 電子信箱驗證碼';
$headers = "From: {$fromEmail}\r\nReply-To: {$fromEmail}\r\nContent-Type: text/plain; charset=UTF-8\r\n";
$message = "親愛的用戶您好,\n\n您的電子信箱驗證碼為:\n\n {$otp}\n\n請於 5 分鐘內輸入此驗證碼完成驗證。\n若非本人操作,請忽略此封信件。\n\n亞訊寬頻 客服中心\nservice@ysnet.com.tw";
if (mail($email, $subject, $message, $headers)) {
echo json_encode(['success' => true, 'message' => '驗證碼已寄出']);
} else {
unset($_SESSION['otp_code'], $_SESSION['otp_email'],
$_SESSION['otp_expires'], $_SESSION['otp_sent_at']);
echo json_encode(['success' => false, 'message' => '寄信失敗,請確認信箱或聯繫客服']);
}
ob_end_flush();
exit;
}
// ── OTP 驗證 API(fetch 呼叫時 action=verify_otp)────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'verify_otp') {
ob_clean();
header('Content-Type: application/json; charset=utf-8');
$entered = trim($_POST['otp'] ?? '');
$email = trim($_POST['email'] ?? '');
if (!isset($_SESSION['otp_code']) || time() > ($_SESSION['otp_expires'] ?? 0)) {
echo json_encode(['success' => false, 'message' => '驗證碼已過期,請重新發送']);
ob_end_flush(); exit;
}
if ($_SESSION['otp_email'] !== $email) {
echo json_encode(['success' => false, 'message' => '信箱不符,請重新發送驗證碼']);
ob_end_flush(); exit;
}
if ($_SESSION['otp_code'] === $entered) {
$_SESSION['otp_verified'] = true;
echo json_encode(['success' => true, 'message' => '驗證成功']);
} else {
echo json_encode(['success' => false, 'message' => '驗證碼錯誤']);
}
ob_end_flush();
exit;
}
// ── 正常頁面流程 ───────────────────────────────────────────
// 第一次帶參數進來:存入 Session 後重導向到乾淨 URL
// from_preview=1 時雖然也帶 user_id,但不應覆蓋 Session,直接略過
if (isset($_GET['user_id']) && !isset($_GET['from_preview'])) {
$user_id = trim($_GET['user_id']);
$enname = trim($_GET['ENname'] ?? $_GET['NName'] ?? $_GET['enname'] ?? '');
$enphone = trim($_GET['ENphone'] ?? '');
if (empty($user_id)) {
http_response_code(400);
die('<p style="color:red;font-size:20px">缺少 user_id 參數</p>');
}
// 存入 Session(所有 FileMaker 傳入的欄位)
$_SESSION['form_user_id'] = $user_id;
$_SESSION['form_enname'] = $enname;
$_SESSION['form_name'] = trim($_GET['name'] ?? '');
$_SESSION['form_member'] = trim($_GET['member'] ?? '');
$_SESSION['form_speed'] = trim($_GET['speed'] ?? '');
$_SESSION['form_season'] = trim($_GET['season'] ?? '');
$_SESSION['form_halfyear'] = trim($_GET['halfyear'] ?? '');
$_SESSION['form_year'] = trim($_GET['year'] ?? '');
$_SESSION['form_twoyear'] = trim($_GET['twoyear'] ?? '');
$_SESSION['form_email'] = trim($_GET['email'] ?? '');
$_SESSION['form_homenumber'] = trim($_GET['homenumber'] ?? '');
$_SESSION['form_phone'] = trim($_GET['phone'] ?? '');
$_SESSION['form_memberaddress']= trim($_GET['memberaddress'] ?? '');
$_SESSION['form_basenumber'] = trim($_GET['basenumber'] ?? '');
// PPPoE 或靜態 IP(FileMaker 的 If 條件二選一傳入)
$_SESSION['form_pppoeid'] = trim($_GET['pppoeid'] ?? '');
$_SESSION['form_pppoepw'] = trim($_GET['pppoepw'] ?? '');
$_SESSION['form_ipaddress'] = trim($_GET['ipaddress'] ?? '');
$_SESSION['form_fistcount'] = trim($_GET['fistcount'] ?? '');
$_SESSION['form_remark'] = trim($_GET['remark'] ?? '');
// ENphone(選填)
$_SESSION['form_enphone'] = trim($_GET['ENphone'] ?? '');
// endday(到期日,FileMaker 可能未補零,例:2026/6/17 → 需轉為 2026-06-17)
$enddayRaw = trim($_GET['endday'] ?? '');
$enddayNormalized = '';
if ($enddayRaw !== '') {
// 支援 2026/6/17、2026-6-17、2026/06/17 等格式
$parts = preg_split('/[\/\-]/', $enddayRaw);
if (count($parts) === 3) {
$y = (int)$parts[0];
$m = (int)$parts[1];
$d = (int)$parts[2];
if ($y > 0 && $m >= 1 && $m <= 12 && $d >= 1 && $d <= 31) {
$enddayNormalized = sprintf('%04d-%02d-%02d', $y, $m, $d);
}
}
}
$_SESSION['form_endday'] = $enddayNormalized;
// 重導向到乾淨 URL
header('Location: form.php');
exit;
}
$user_id = $_SESSION['form_user_id'] ?? '';
$enname = $_SESSION['form_enname'] ?? '';
$enphone = $_SESSION['form_enphone'] ?? '';
if (empty($user_id)) {
// 若 user_id 空但有 from_preview 參數,代表 Session 被清了
if (!empty($_GET['from_preview'])) {
die('<p style="color:red;font-size:18px;padding:20px">
Session 已失效,請重新從 FileMaker 開啟連結<br>
<small style="color:#888">(長時間無操作或瀏覽器重啟可能導致 Session 遺失)</small>
</p>');
}
http_response_code(400);
die('<p style="color:red;font-size:20px">請透過正確連結開啟此頁面</p>');
}
// ── 從 Session 讀取 FileMaker 傳入的資料(不再查詢 MySQL)────
$name = $_SESSION['form_name'] ?? '';
$member = $_SESSION['form_member'] ?? '';
$speed = $_SESSION['form_speed'] ?? '';
$season = $_SESSION['form_season'] ?? '';
$halfyear = $_SESSION['form_halfyear'] ?? '';
$year = $_SESSION['form_year'] ?? '';
$twoyear = $_SESSION['form_twoyear'] ?? '';
$email = $_SESSION['form_email'] ?? '';
$homenumber = $_SESSION['form_homenumber'] ?? '';
$phone = $_SESSION['form_phone'] ?? '';
$memberaddress = $_SESSION['form_memberaddress'] ?? '';
$basenumber = $_SESSION['form_basenumber'] ?? '';
$pppoeid = $_SESSION['form_pppoeid'] ?? '';
$pppoepw = $_SESSION['form_pppoepw'] ?? '';
$ipaddress = $_SESSION['form_ipaddress'] ?? '';
$fistcount = $_SESSION['form_fistcount'] ?? '';
$remark = $_SESSION['form_remark'] ?? '';
$endday = $_SESSION['form_endday'] ?? '';
// useraddress 已合併到 memberaddress(FileMaker 側做拼接),此處設為空字串
$useraddress = '';
$isSubscription = (strpos($speed, '定期') !== false) ? 1 : 0;
$plan_options = [
"season" => !empty($season) ? ["price" => (int)$season, "label" => "季繳"] : null,
"halfyear" => !empty($halfyear) ? ["price" => (int)$halfyear, "label" => "半年繳"] : null,
"year" => !empty($year) ? ["price" => (int)$year, "label" => "一年繳"] : null,
"twoyear" => !empty($twoyear) ? ["price" => (int)$twoyear, "label" => "兩年繳"] : null,
];
// 今日日期
$todayStr = (new DateTime())->format('Y-m-d');
$todayDisplay = (new DateTime())->format('Y年m月d日');
?>
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="format-detection" content="telephone=no">
<title>裝機申請書</title>
<!-- Tabler Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.19.0/dist/tabler-icons.min.css">
<style>
/* ── 基礎 reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--green: #1D9E75;
--green-lt: #E1F5EE;
--green-dk: #0F6E56;
--green-deep: #085041;
--amber: #FAEEDA;
--amber-txt: #633806;
--red: #E24B4A;
--red-lt: #FCEBEB;
--gray-bg: #F5F5F5;
--gray-bd: rgba(0,0,0,.15);
--gray-bd2: rgba(0,0,0,.28);
--text: #111;
--text-sec: #666;
--text-tert: #999;
--radius-md: 8px;
--radius-lg: 12px;
}
body {
font-family: "Microsoft JhengHei", "PingFang TC", sans-serif;
font-size: 15px;
background: var(--gray-bg);
color: var(--text);
padding-bottom: 80px;
}
/* ── 頂部 Bar ── */
.topbar {
position: sticky; top: 0; z-index: 100;
background: #fff;
border-bottom: 0.5px solid var(--gray-bd);
padding: 11px 16px;
display: flex; align-items: center; gap: 10px;
}
.topbar-id {
font-size: 13px; color: var(--text-sec);
}
.topbar-id span {
font-family: monospace; font-size: 14px;
color: var(--text); font-weight: 500;
}
.topbar-title {
flex: 1; text-align: center;
font-size: 16px; font-weight: 500; color: var(--text);
}
.topbar-remark {
font-size: 13px; padding: 6px 12px;
border: 0.5px solid var(--gray-bd2);
border-radius: var(--radius-md);
background: none; cursor: pointer; color: var(--text-sec);
font-family: inherit;
}
/* ── 步驟 Tab ── */
.step-tabs {
background: #fff;
border-bottom: 0.5px solid var(--gray-bd);
display: flex; overflow-x: auto;
}
.step-tabs::-webkit-scrollbar { display: none; }
.step-tab {
flex: 1; min-width: 68px;
padding: 9px 4px 7px;
text-align: center; font-size: 12px;
color: var(--text-sec); cursor: pointer;
border-bottom: 2px solid transparent;
transition: all .15s; white-space: nowrap;
}
.step-tab .dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--gray-bd2);
margin: 0 auto 4px; transition: all .15s;
}
.step-tab.active { color: var(--green); border-bottom-color: var(--green); }
.step-tab.active .dot { background: var(--green); }
.step-tab.done .dot { background: #9FE1CB; }
/* ── 通用卡片 ── */
.card {
background: #fff;
border-radius: var(--radius-lg);
border: 0.5px solid var(--gray-bd);
overflow: hidden;
margin-bottom: 12px;
}
.card-hdr {
display: flex; align-items: center; gap: 8px;
padding: 10px 14px;
border-bottom: 0.5px solid var(--gray-bd);
background: var(--gray-bg);
}
.card-hdr i { font-size: 16px; color: var(--green); }
.card-hdr-txt { font-size: 13px; font-weight: 500; }
.card-hdr-sub {
margin-left: auto; font-size: 11px;
background: var(--green-lt); color: var(--green-deep);
padding: 2px 8px; border-radius: 20px;
}
/* ── 行 (label + value) ── */
.field-row {
display: flex; align-items: stretch;
border-bottom: 0.5px solid var(--gray-bd);
}
.field-row:last-child { border-bottom: none; }
.field-lbl {
width: 80px; flex-shrink: 0;
font-size: 12px; color: var(--text-sec);
padding: 10px 10px 10px 14px;
background: var(--gray-bg);
display: flex; align-items: center; justify-content: center;
text-align: center; line-height: 1.4;
}
.field-val {
flex: 1; padding: 8px 12px;
font-size: 14px; color: var(--text);
display: flex; align-items: center; flex-wrap: wrap; gap: 4px;
}
.field-val.readonly { color: var(--text-sec); font-family: monospace; font-size: 13px; }
.field-val.green { color: var(--green-dk); font-weight: 500; }
/* ── Input ── */
.f-input {
width: 100%; font-size: 15px;
background: transparent; border: none; outline: none;
color: var(--text); font-family: inherit;
padding: 2px 0;
}
.f-input::placeholder { color: #bbb; }
.f-input:focus { outline: none; }
.hint {
font-size: 11px; margin-top: 2px; width: 100%;
}
.hint-ok { color: var(--green); }
.hint-err { color: var(--red); }
/* ── 設備晶片 ── */
.chip-grid { display: flex; flex-wrap: wrap; gap: 7px; padding: 12px 14px; }
.chip {
padding: 6px 12px; border-radius: 20px;
font-size: 13px;
border: 1px solid var(--gray-bd2);
color: var(--text-sec);
cursor: pointer; background: #fff;
display: flex; align-items: center; gap: 4px;
transition: all .12s;
user-select: none;
}
.chip.selected {
background: var(--green-lt);
border-color: var(--green);
color: var(--green-deep); font-weight: 500;
}
.chip i { font-size: 14px; }
/* ── 借用設備群組 ── */
.use-group {
border-bottom: 0.5px solid var(--gray-bd);
padding: 10px 14px;
}
.use-group:last-child { border-bottom: none; }
.use-group-header {
display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
}
.use-select {
flex: 1; font-size: 14px; padding: 6px 8px;
border: 0.5px solid var(--gray-bd2);
border-radius: var(--radius-md);
background: #fff; color: var(--text);
font-family: inherit;
}
.use-fields {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
margin-top: 6px;
}
.use-field-box {
border: 0.5px solid var(--gray-bd2);
border-radius: var(--radius-md);
padding: 6px 10px;
}
.use-field-lbl { font-size: 11px; color: var(--text-tert); margin-bottom: 2px; }
.use-field-input {
width: 100%; font-size: 14px; background: transparent;
border: none; outline: none; color: var(--text); font-family: inherit;
}
.use-field-input::placeholder { color: #ccc; }
.add-btn {
font-size: 13px; padding: 7px 14px;
border: 0.5px solid var(--gray-bd2);
border-radius: var(--radius-md);
background: none; cursor: pointer;
color: var(--text-sec); font-family: inherit;
margin: 8px 14px;
display: flex; align-items: center; gap: 4px;
}
.add-btn:active { background: var(--gray-bg); }
.rm-btn {
width: 26px; height: 26px; border-radius: 50%;
border: 0.5px solid var(--gray-bd2);
background: none; cursor: pointer;
display: flex; align-items: center; justify-content: center;
color: var(--text-tert); font-size: 16px; flex-shrink: 0;
}
/* ── 費率卡片 ── */
.plan-grid {
display: grid; grid-template-columns: 1fr 1fr;
gap: 8px; padding: 12px 14px;
}
.plan-card {
border: 1px solid var(--gray-bd2);
border-radius: var(--radius-md);
padding: 10px 12px; cursor: pointer;
background: #fff; transition: all .12s;
}
.plan-card.selected {
border-color: var(--green);
background: var(--green-lt);
}
.plan-name { font-size: 13px; font-weight: 500; color: var(--text); }
.plan-price {
font-size: 18px; font-weight: 500;
color: var(--green); margin-top: 2px;
}
.plan-price span { font-size: 11px; font-weight: 400; color: var(--text-sec); }
/* 加費項目 */
.extra-row {
display: flex; align-items: center; gap: 8px;
padding: 9px 14px;
border-bottom: 0.5px solid var(--gray-bd);
}
.extra-row:last-child { border-bottom: none; }
.extra-select {
flex: 1; font-size: 13px; padding: 6px 8px;
border: 0.5px solid var(--gray-bd2);
border-radius: var(--radius-md);
background: #fff; color: var(--text); font-family: inherit;
}
.extra-amount {
width: 80px; font-size: 14px;
padding: 6px 8px; text-align: right;
border: 0.5px solid var(--gray-bd2);
border-radius: var(--radius-md);
background: #fff; color: var(--text);
font-family: inherit;
}
/* 總計 & 押金摘要 */
.total-row {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 14px;
}
.total-lbl { font-size: 14px; color: var(--text-sec); }
.total-amt { font-size: 22px; font-weight: 500; color: var(--green); }
.deposit-row {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 14px;
border-top: 0.5px solid var(--gray-bd);
font-size: 13px;
}
.deposit-lbl { color: var(--text-sec); }
.deposit-amt { font-weight: 500; color: #BA7517; }
/* ── 日期 ── */
.date-row {
display: flex; align-items: center;
padding: 10px 14px;
border-bottom: 0.5px solid var(--gray-bd);
}
.date-row:last-child { border-bottom: none; }
.date-lbl { font-size: 13px; color: var(--text-sec); flex: 1; }
.date-val {
font-size: 14px; font-weight: 500; color: var(--text);
}
.date-val.accent { color: var(--green-dk); }
.date-val.big { font-size: 16px; }
/* 贈送群組 */
.gift-group {
display: flex; align-items: center; gap: 8px;
padding: 9px 14px;
border-bottom: 0.5px solid var(--gray-bd);
}
.gift-group:last-child { border-bottom: none; }
.gift-select {
flex: 1; font-size: 14px; padding: 6px 8px;
border: 0.5px solid var(--gray-bd2);
border-radius: var(--radius-md);
background: #fff; color: var(--text); font-family: inherit;
}
.gift-qty {
width: 60px; font-size: 14px; padding: 6px 8px;
border: 0.5px solid var(--gray-bd2);
border-radius: var(--radius-md);
background: #fff; color: var(--text); font-family: inherit;
}
/* ── 底部 ── */
.bottom-bar {
position: sticky; bottom: 0;
background: #fff;
border-top: 0.5px solid var(--gray-bd);
padding: 10px 16px;
display: flex; align-items: center; justify-content: space-between;
}
.bottom-summary {}
.bottom-summary-lbl { font-size: 11px; color: var(--text-sec); }
.bottom-summary-main { font-size: 13px; color: var(--text-sec); margin-top: 1px; }
.bottom-summary-main span { color: var(--green); font-weight: 500; font-size: 15px; }
.bottom-nav { display: flex; gap: 8px; }
.nav-btn {
padding: 9px 14px;
border: 0.5px solid var(--gray-bd2);
border-radius: var(--radius-md);
background: none; cursor: pointer;
font-size: 14px; color: var(--text-sec);
font-family: inherit;
}
.nav-btn.primary {
background: var(--green); border-color: var(--green);
color: #fff; font-weight: 500;
padding: 9px 20px;
}
.nav-btn:disabled { opacity: .4; cursor: not-allowed; }
/* ── 步驟頁面 ── */
.step-page { display: none; padding: 12px 16px 0; }
.step-page.active { display: block; }
/* ── 備註 Modal ── */
.modal-bg {
display: none;
position: fixed; inset: 0; z-index: 9999;
background: rgba(0,0,0,.35);
align-items: center; justify-content: center;
}
.modal-bg.open { display: flex; }
.modal-box {
background: #fff; border-radius: var(--radius-lg);
padding: 20px; width: 88vw; max-width: 360px;
position: relative;
}
.modal-title { font-size: 15px; font-weight: 500; margin-bottom: 12px; }
.modal-close {
position: absolute; top: 10px; right: 12px;
font-size: 22px; background: none; border: none;
cursor: pointer; color: var(--text-sec);
line-height: 1;
}
.modal-content {
font-size: 14px; line-height: 1.8;
color: var(--text-sec); white-space: pre-line;
}
/* 信箱驗證 modal */
.otp-box {
border: 0.5px solid var(--gray-bd2);
border-radius: var(--radius-md);
padding: 12px; margin-top: 12px;
background: #F9FFF9;
}
.otp-input {
width: 100%; font-size: 22px; letter-spacing: 8px;
text-align: center; padding: 10px;
border: 1.5px solid var(--gray-bd2);
border-radius: var(--radius-md); margin: 8px 0;
font-family: monospace;
}
.otp-btn {
width: 100%; padding: 10px;
background: var(--green); color: #fff;
border: none; border-radius: var(--radius-md);
font-size: 14px; cursor: pointer; font-family: inherit;
}
.otp-resend {
text-align: center; font-size: 12px;
color: var(--green); margin-top: 8px;
cursor: pointer;
}
/* 警示列 */
.warn-bar {
background: var(--amber); border-radius: var(--radius-md);
padding: 10px 14px; display: flex; gap: 8px;
font-size: 12px; color: var(--amber-txt);
margin-bottom: 12px; align-items: flex-start;
}
.warn-bar i { font-size: 15px; flex-shrink: 0; }
/* 首裝優惠顯示 */
.fistcount-box {
padding: 10px 14px;
font-size: 13px; color: var(--text-sec); line-height: 1.7;
}
</style>
</head>
<body>
<?php if (!empty($dbError)): ?>
<div style="background:#FCEBEB;color:#A32D2D;padding:12px 16px;font-size:14px">
<i class="ti ti-alert-circle"></i> <?= htmlspecialchars($dbError) ?>
</div>
<?php endif; ?>
<!-- ── 頂部 Bar ── -->
<div class="topbar">
<button class="topbar-remark" id="remark-btn" type="button">
<i class="ti ti-notes"></i> 備註
</button>
<div class="topbar-title">裝機申請書</div>
<div style="text-align:right;line-height:1.5">
<div style="font-size:11px;color:var(--text-sec)">帳號 <span style="font-family:monospace;font-size:12px;color:var(--text)"><?= htmlspecialchars($pppoeid) ?></span></div>
<div style="font-size:11px;color:var(--text-sec)">密碼 <span style="font-family:monospace;font-size:12px;color:var(--text)"><?= htmlspecialchars($pppoepw) ?></span></div>
</div>
</div>
<!-- ── 步驟 Tab ── -->
<div class="step-tabs" id="step-tabs">
<div class="step-tab active" onclick="goStep(0)"><div class="dot"></div>申請人</div>
<div class="step-tab" onclick="goStep(1)"><div class="dot"></div>設備</div>
<div class="step-tab" onclick="goStep(2)"><div class="dot"></div>費率</div>
<div class="step-tab" onclick="goStep(3)"><div class="dot"></div>日期</div>
</div>
<!-- ════════════════════════════════════════
STEP 0 ─ 申請人資訊
═════════════════════════════════════════ -->
<div class="step-page active" id="page-0">
<!-- 系統帶入 -->
<div class="card">
<div class="card-hdr">
<i class="ti ti-database"></i>
<span class="card-hdr-txt">系統帶入資料</span>
<span class="card-hdr-sub">自動填入</span>
</div>
<div class="field-row">
<div class="field-lbl">社區</div>
<div class="field-val readonly"><?= htmlspecialchars($member) ?></div>
</div>
<div class="field-row">
<div class="field-lbl">裝機地址</div>
<div class="field-val readonly" style="font-size:13px">
<?= htmlspecialchars($memberaddress) ?>
<?= htmlspecialchars($useraddress) ?>
</div>
</div>
<div class="field-row">
<div class="field-lbl">申請速率</div>
<div class="field-val green"><?= htmlspecialchars($speed) ?> Mbps</div>
</div>
<div class="field-row">
<div class="field-lbl">據點</div>
<div class="field-val readonly"><?= htmlspecialchars($basenumber) ?></div>
</div>
<div class="field-row">
<div class="field-lbl">PPPoE<br>帳號</div>
<div class="field-val readonly" style="font-family:monospace;font-size:13px">
<?= htmlspecialchars($pppoeid) ?>
</div>
</div>
<div class="field-row">
<div class="field-lbl">PPPoE<br>密碼</div>
<div class="field-val readonly" style="font-family:monospace;font-size:13px">
<?= htmlspecialchars($pppoepw) ?>
</div>
</div>
<?php if (!empty($fistcount)): ?>
<div class="field-row">
<div class="field-lbl">首裝優惠</div>
<div class="field-val">
<div class="fistcount-box" style="padding:0">
<?= nl2br(htmlspecialchars($fistcount)) ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- 申請人填寫 -->
<div class="card">
<div class="card-hdr">
<i class="ti ti-user"></i>
<span class="card-hdr-txt">申請人填寫</span>
</div>
<div class="field-row">
<div class="field-lbl">姓名</div>
<div class="field-val">
<input class="f-input" type="text" id="applicant_name" name="applicant_name"
value="<?= htmlspecialchars($name) ?>" placeholder="請輸入姓名">
</div>
</div>
<div class="field-row">
<div class="field-lbl">證件類型</div>
<div class="field-val">
<select id="id-type" style="font-size:14px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);padding:5px 8px;background:#fff;font-family:inherit;width:100%">
<option value="twid">身分證字號</option>
<option value="taxid">統一編號</option>
<option value="arc">舊式居留證</option>
</select>
</div>
</div>
<div class="field-row">
<div class="field-lbl">證件號碼</div>
<div class="field-val" style="flex-direction:column;align-items:flex-start">
<input class="f-input" type="text" id="applicant_usernumber" name="applicant_usernumber"
placeholder="請輸入證件號碼" maxlength="10"
inputmode="latin" autocomplete="off">
<div class="hint" id="twid-hint"></div>
</div>
</div>
<div class="field-row">
<div class="field-lbl">行動電話</div>
<div class="field-val" style="flex-direction:column;align-items:flex-start">
<input class="f-input" type="tel" id="applicant_phone" name="applicant_phone"
value="<?= htmlspecialchars($phone) ?>" placeholder="0912-345-678" maxlength="12">
<div class="hint" id="phone-hint"></div>
</div>
</div>
<div class="field-row">
<div class="field-lbl">住宅電話</div>
<div class="field-val" style="flex-direction:column;align-items:flex-start">
<input class="f-input" type="tel" id="applicant_homenumber" name="applicant_homenumber"
value="<?= htmlspecialchars($homenumber) ?>" placeholder="02-xxxxxxxx" maxlength="12">
<div class="hint" id="home-hint"></div>
</div>
</div>
<div class="field-row" id="email-row">
<div class="field-lbl">電子信箱</div>
<div class="field-val" style="flex-direction:column;align-items:flex-start">
<div style="display:flex;width:100%;gap:6px;align-items:center">
<input class="f-input" type="email" id="applicant_email" name="applicant_email"
value="<?= htmlspecialchars($email) ?>"
placeholder="example@mail.com" style="flex:1">
<button type="button" id="send-otp-btn"
style="font-size:12px;padding:5px 10px;border:0.5px solid var(--green);border-radius:var(--radius-md);background:none;cursor:pointer;color:var(--green);white-space:nowrap;font-family:inherit">
發送驗證
</button>
</div>
<div class="hint" id="email-hint"></div>
<!-- OTP 輸入區 (預設隱藏) -->
<div class="otp-box" id="otp-box" style="display:none;width:100%">
<div style="font-size:12px;color:var(--text-sec)">
驗證碼已寄送至 <strong id="otp-target-email"></strong>,請於 5 分鐘內輸入
</div>
<input class="otp-input" type="text" id="otp-input"
maxlength="6" placeholder="000000" inputmode="numeric">
<button class="otp-btn" type="button" onclick="verifyOTP()">確認驗證碼</button>
<div style="display:flex;gap:8px;margin-top:8px">
<div class="otp-resend" id="otp-resend" onclick="sendOTP(true)" style="flex:1">重新發送</div>
<div style="flex:1;text-align:center;font-size:12px;color:var(--red);cursor:pointer"
onclick="cancelOTP()">取消重新填寫</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /page-0 -->
<!-- ════════════════════════════════════════
STEP 1 ─ 設備資訊
═════════════════════════════════════════ -->
<div class="step-page" id="page-1">
<!-- 室內資訊箱設備 -->
<div class="card">
<div class="card-hdr">
<i class="ti ti-server"></i>
<span class="card-hdr-txt">室內資訊箱設備</span>
</div>
<div id="info-container"></div>
<button type="button" class="add-btn" id="add-info-btn" onclick="addInfoGroup()">
<i class="ti ti-plus" style="font-size:14px"></i> 新增設備
</button>
</div>
<!-- 室內網路設備 -->
<div class="card">
<div class="card-hdr">
<i class="ti ti-wifi"></i>
<span class="card-hdr-txt">室內網路設備</span>
</div>
<div id="router-container"></div>
<button type="button" class="add-btn" id="add-router-btn" onclick="addRouterGroup()">
<i class="ti ti-plus" style="font-size:14px"></i> 新增設備
</button>
</div>
<!-- 使用設備 -->
<div class="card">
<div class="card-hdr">
<i class="ti ti-package"></i>
<span class="card-hdr-txt">使用設備</span>
</div>
<div id="use-container"></div>
<button type="button" class="add-btn" id="add-use-btn" onclick="addUseGroup()">
<i class="ti ti-plus" style="font-size:14px"></i> 新增設備
</button>
</div>
</div><!-- /page-1 -->
<!-- ════════════════════════════════════════
STEP 2 ─ 費率
═════════════════════════════════════════ -->
<div class="step-page" id="page-2">
<div class="card">
<div class="card-hdr">
<i class="ti ti-receipt"></i>
<span class="card-hdr-txt">選擇繳費方案</span>
<?php if ($endday && !$isSubscription): ?>
<span style="margin-left:auto;font-size:11px;color:var(--text-tert)">(選填,已有到期日)</span>
<?php endif; ?>
</div>
<?php if ($isSubscription): ?>
<div style="padding:14px">
<div class="plan-card selected" id="plan-sub"
data-key="subscription" data-price="0"
onclick="selectPlan(this)">
<div class="plan-name">定期定額月扣</div>
<div class="plan-price">信用卡扣款</div>
</div>
</div>
<?php else: ?>
<div class="plan-grid" id="plan-grid">
<?php
$planMap = [
'season' => '季繳',
'halfyear' => '半年繳',
'year' => '一年繳',
'twoyear' => '兩年繳',
];
foreach ($planMap as $key => $label):
if (!empty($plan_options[$key])):
$price = number_format((int)$plan_options[$key]['price']);
?>
<div class="plan-card" data-key="<?= $key ?>" data-price="<?= (int)$plan_options[$key]['price'] ?>"
onclick="selectPlan(this)">
<div class="plan-name"><?= $label ?></div>
<div class="plan-price">$<?= $price ?><span>/期</span></div>
</div>
<?php endif; endforeach; ?>
</div>
<!-- 取消選擇:避免誤點方案後無法取消 -->
<div id="plan-clear-wrap" style="display:none;padding:0 14px 14px">
<button type="button" onclick="clearPlanSelection()"
style="width:100%;padding:9px;font-size:13px;color:var(--text-sec);background:none;border:0.5px dashed var(--gray-bd2);border-radius:var(--radius-md);cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:5px">
<i class="ti ti-x" style="font-size:13px"></i> 取消選擇方案
</button>
</div>
<?php endif; ?>
</div>
<!-- 加收項目 -->
<div class="card">
<div class="card-hdr">
<i class="ti ti-plus"></i>
<span class="card-hdr-txt">加收項目</span>
<button type="button" onclick="addExtraRow()"
style="margin-left:auto;font-size:12px;padding:4px 10px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:none;cursor:pointer;color:var(--text-sec);font-family:inherit">
+ 新增
</button>
</div>
<div id="extra-container"></div>
</div>
<!-- 費用合計 -->
<div class="card">
<div class="total-row">
<span class="total-lbl">應繳費用總計</span>
<?php if($isSubscription): ?>
<span class="total-amt" id="total-display" style="font-size:13px;color:var(--green-dk)">信用卡定期定額繳費</span>
<?php else: ?>
<span class="total-amt" id="total-display">$0</span>
<?php endif; ?>
</div>
<div id="deposit-summary" style="display:none">
<div class="deposit-row">
<span class="deposit-lbl">借用設備押金</span>
<span class="deposit-amt" id="deposit-display">$0</span>
</div>
</div>
</div>
</div><!-- /page-2 -->
<!-- ════════════════════════════════════════
STEP 3 ─ 日期
═════════════════════════════════════════ -->
<div class="step-page" id="page-3">
<div class="card">
<div class="card-hdr">
<i class="ti ti-calendar"></i>
<span class="card-hdr-txt">裝機與租期</span>
</div>
<div class="date-row">
<span class="date-lbl">裝機日</span>
<span class="date-val"><?= $todayDisplay ?></span>
</div>
<div class="date-row">
<span class="date-lbl">起租日</span>
<span class="date-val accent" id="rent-start">—</span>
</div>
<div class="date-row">
<span class="date-lbl">到期日</span>
<span class="date-val accent" id="rent-end">—</span>
</div>
</div>
<!-- 贈送月份(定期定額不顯示) -->
<div class="card" id="gift-card" <?php if($isSubscription): ?>style="display:none"<?php endif; ?>>
<div class="card-hdr">
<i class="ti ti-gift"></i>
<span class="card-hdr-txt">贈送月份</span>
<button type="button" onclick="addGiftGroup()"
style="margin-left:auto;font-size:12px;padding:4px 10px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:none;cursor:pointer;color:var(--text-sec);font-family:inherit">
+ 新增
</button>
</div>
<div id="gift-container">
<div class="gift-empty" style="padding:12px 14px;font-size:13px;color:var(--text-tert)">
目前無贈送月份
</div>
</div>
</div>
<!-- 含贈送後到期日(無贈送時隱藏) -->
<div class="card" id="real-end-card" style="display:none">
<div class="date-row" style="background:var(--green-lt);border-radius:var(--radius-lg)">
<span class="date-lbl" style="font-weight:500;color:var(--green-deep)">含贈送後到期</span>
<span class="date-val big accent" id="real-end">—</span>
</div>
</div>
</div><!-- /page-3 -->
<!-- ── 底部 ── -->
<div class="bottom-bar">
<div class="bottom-summary">
<div class="bottom-summary-lbl">目前費用</div>
<div class="bottom-summary-main"><span id="bar-total">$0</span></div>
<div id="bar-deposit-wrap" style="display:none;font-size:11px;color:#BA7517;margin-top:2px">
押金 <span id="bar-deposit">$0</span>
</div>
</div>
<div class="bottom-nav">
<button class="nav-btn" id="prev-btn" onclick="navigate(-1)" disabled>
<i class="ti ti-arrow-left"></i>
</button>
<button class="nav-btn primary" id="next-btn" onclick="navigate(1)">
下一步 <i class="ti ti-arrow-right"></i>
</button>
</div>
</div>
<!-- ── 備註 Modal ── -->
<div class="modal-bg" id="remark-modal">
<div class="modal-box">
<button class="modal-close" onclick="document.getElementById('remark-modal').classList.remove('open')">×</button>
<div class="modal-title"><i class="ti ti-notes" style="color:var(--green)"></i> 備註</div>
<div class="modal-content" id="remark-content"><?= htmlspecialchars($remark ?: '無備註') ?></div>
</div>
</div>
<!-- ── Hidden form (送出用) ── -->
<form id="submit-form" method="POST" action="preview.php" style="display:none">
<input type="hidden" name="user_id" value="<?= htmlspecialchars($user_id) ?>">
<input type="hidden" name="member" value="<?= htmlspecialchars($member) ?>">
<input type="hidden" name="memberaddress" value="<?= htmlspecialchars($memberaddress) ?>">
<input type="hidden" name="useraddress" value="<?= htmlspecialchars($useraddress) ?>">
<input type="hidden" name="speed" value="<?= htmlspecialchars($speed) ?>">
<input type="hidden" name="basenumber" value="<?= htmlspecialchars($basenumber) ?>">
<input type="hidden" name="pppoeid" value="<?= htmlspecialchars($pppoeid) ?>">
<input type="hidden" name="pppoepw" value="<?= htmlspecialchars($pppoepw) ?>">
<input type="hidden" name="fistcount" value="<?= htmlspecialchars($fistcount) ?>">
<!-- JS 填入 -->
<input type="hidden" name="applicant_name" id="f_name">
<input type="hidden" name="applicant_usernumber" id="f_id">
<input type="hidden" name="enname" value="<?= htmlspecialchars($enname) ?>">
<input type="hidden" name="enphone" value="<?= htmlspecialchars($enphone) ?>">
<input type="hidden" name="id_type" id="f_idtype">
<input type="hidden" name="applicant_phone" id="f_phone">
<input type="hidden" name="applicant_homenumber" id="f_home">
<input type="hidden" name="applicant_email" id="f_email">
<input type="hidden" name="info_devices" id="f_info">
<input type="hidden" name="info_qty" id="f_info_qty">
<input type="hidden" name="router_devices" id="f_router">
<input type="hidden" name="router_qty" id="f_router_qty">
<input type="hidden" name="use_devices" id="f_use">
<input type="hidden" name="fee_type" id="f_fee_type">
<input type="hidden" name="fee_amount" id="f_fee_amount">
<input type="hidden" name="extra_fees" id="f_extra">
<input type="hidden" name="total_fee" id="f_total">
<input type="hidden" name="deposit_total" id="f_deposit">
<input type="hidden" name="install_date" value="<?= $todayStr ?>">
<input type="hidden" name="rental_start_date" id="f_rent_start">
<input type="hidden" name="rental_end_date" id="f_rent_end">
<input type="hidden" name="gift_months" id="f_gift">
<input type="hidden" name="real_end_date" id="f_real_end">
</form>
<!-- ════════════════════════════
JavaScript
════════════════════════════ -->
<script>
// ── PHP 資料注入 ──────────────────────
const PHP = {
isSubscription: <?= $isSubscription ?>,
planOptions: <?= json_encode($plan_options) ?>,
today: '<?= $todayStr ?>',
remark: <?= json_encode($remark ?: '無備註') ?>,
enname: <?= json_encode($enname) ?>,
enphone: <?= json_encode($enphone) ?>,
endday: <?= json_encode($endday) ?>
};
// enname 存入 sessionStorage 作為備援(避免 PHP Session 遺失)
if(PHP.enname) {
sessionStorage.setItem('ys_enname', PHP.enname);
}
if(PHP.enphone) {
sessionStorage.setItem('ys_enphone', PHP.enphone);
}
// ── 步驟控制 ─────────────────────────
let currentStep = 0;
const totalSteps = 4;
let otpVerified = false; // 宣告在這裡,供 goStep 使用
let _pauseSave = false; // 宣告在最前面,restoreFromSS 才能正確使用
function goStep(n) {
// 從 step0 離開時:只有信箱「有填且格式正確但未驗證」才阻擋
if (currentStep === 0 && n !== 0) {
const emailVal = document.getElementById('applicant_email').value.trim();
const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal);
if (emailVal && emailOk && !otpVerified) {
alert('請完成電子信箱驗證後才能繼續');
document.getElementById('applicant_email').focus();
return;
}
}
document.querySelectorAll('.step-page').forEach((p,i) => {
p.classList.toggle('active', i === n);
});
document.querySelectorAll('.step-tab').forEach((t,i) => {
t.classList.toggle('active', i === n);
t.classList.toggle('done', i < n);
});
currentStep = n;
document.getElementById('prev-btn').disabled = n === 0;
const nb = document.getElementById('next-btn');
if (n === totalSteps - 1) {
nb.innerHTML = '確認送出 <i class="ti ti-send"></i>';
nb.onclick = () => submitForm();
} else {
nb.innerHTML = '下一步 <i class="ti ti-arrow-right"></i>';
nb.onclick = () => navigate(1);
}
}
function navigate(dir) {
const next = currentStep + dir;
if (next < 0 || next >= totalSteps) return;
goStep(next);
}
// ── 備註 ─────────────────────────────
document.getElementById('remark-btn').onclick = () => {
document.getElementById('remark-modal').classList.add('open');
};
document.getElementById('remark-modal').addEventListener('click', function(e) {
if (e.target === this) this.classList.remove('open');
});
// ── 行動電話格式(4-3-3,自動補上 "-")──────
document.getElementById('applicant_phone').addEventListener('input', function() {
let raw = this.value.replace(/\D/g,'').slice(0,10);
let formatted = raw;
if (raw.length > 7) {
formatted = raw.slice(0,4) + '-' + raw.slice(4,7) + '-' + raw.slice(7);
} else if (raw.length > 4) {
formatted = raw.slice(0,4) + '-' + raw.slice(4);
}
this.value = formatted;
const ok = /^09\d{2}-\d{3}-\d{3}$/.test(this.value);
const hint = document.getElementById('phone-hint');
if (!this.value) { hint.textContent=''; return; }
hint.textContent = ok ? '✔ 格式正確' : '✘ 格式錯誤(例:0912-345-678)';
hint.className = 'hint ' + (ok ? 'hint-ok' : 'hint-err');
});
// ── 住宅電話格式 ──────────────────────
document.getElementById('applicant_homenumber').addEventListener('input', function() {
let raw = this.value.replace(/\D/g,'');
this.value = raw.length > 2 ? raw.slice(0,2)+'-'+raw.slice(2) : raw;
const ok = /^0\d-\d{7,8}$/.test(this.value);
const hint = document.getElementById('home-hint');
if (!this.value) { hint.textContent=''; return; }
hint.textContent = ok ? '✔ 格式正確' : '✘ 格式錯誤(例:02-12345678)';
hint.className = 'hint ' + (ok ? 'hint-ok' : 'hint-err');
});
// ── 身分證驗證 ────────────────────────
function isValidTWID(v){ return /^[A-Z][12]\d{8}$/.test(v); }
function isValidTaxID(v){
if(!/^\d{8}$/.test(v)) return false;
const w=[1,2,1,2,1,2,4,1]; let s=0;
for(let i=0;i<8;i++){const p=+v[i]*w[i];s+=Math.floor(p/10)+(p%10);}
return s%10===0 || (v[6]==='7' && (s+1)%10===0);
}
function isValidARC(v){ return /^[A-Z]{2}\d{8}$/.test(v); }
document.getElementById('applicant_usernumber').addEventListener('input', function() {
const type = document.getElementById('id-type').value;
let val = this.value.toUpperCase().replace(type==='taxid' ? /\D/g : /[^A-Z0-9]/g, '');
this.value = val;
const hint = document.getElementById('twid-hint');
if (!val) { hint.textContent=''; return; }
let ok = false, msg = '';
if (type==='twid'){ ok=isValidTWID(val); msg='身分證'; }
else if(type==='taxid'){ ok=isValidTaxID(val); msg='統一編號'; }
else { ok=isValidARC(val); msg='居留證'; }
hint.textContent = ok ? `✔ ${msg}格式正確` : `✘ 格式錯誤`;
hint.className = 'hint ' + (ok ? 'hint-ok' : 'hint-err');
});
document.getElementById('id-type').addEventListener('change', function() {
const input = document.getElementById('applicant_usernumber');
input.value = '';
document.getElementById('twid-hint').textContent = '';
input.maxLength = this.value==='taxid' ? 8 : 10;
});
// ── 電子信箱 OTP ──────────────────────
// otpVerified 已在頂部宣告
document.getElementById('applicant_email').addEventListener('input', function(){
const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.value);
const hint = document.getElementById('email-hint');
if(!this.value){hint.textContent='';return;}
hint.textContent = ok ? '✔ 格式正確' : '✘ 格式錯誤';
hint.className = 'hint '+(ok?'hint-ok':'hint-err');
otpVerified = false;
document.getElementById('otp-box').style.display='none';
});
document.getElementById('send-otp-btn').addEventListener('click', () => sendOTP(false));
function sendOTP(isResend) {
const email = document.getElementById('applicant_email').value.trim();
if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)){
alert('請先輸入正確的電子信箱格式'); return;
}
const btn = document.getElementById('send-otp-btn');
btn.textContent = '寄送中…';
btn.disabled = true;
const fd = new FormData();
fd.append('action', 'send_otp');
fd.append('email', email);
// 使用目前頁面 URL(保留所有 query string)
fetch(window.location.href, { method:'POST', body:fd })
.then(r => {
if(!r.ok) throw new Error('HTTP ' + r.status);
return r.text();
})
.then(text => {
let data;
try { data = JSON.parse(text); }
catch(e) {
console.error('回應非 JSON:', text);
throw new Error('伺服器回應格式錯誤');
}
if(data.success){
document.getElementById('otp-target-email').textContent = email;
document.getElementById('otp-box').style.display = 'block';
document.getElementById('otp-input').value = '';
document.getElementById('email-hint').textContent = isResend ? '✔ 已重新發送驗證碼' : '✔ 驗證碼已寄出';
document.getElementById('email-hint').className = 'hint hint-ok';
btn.textContent = '60秒後可重發';
let sec = 59;
const timer = setInterval(()=>{
btn.textContent = sec + '秒後可重發';
if(sec-- <= 0){
clearInterval(timer);
btn.textContent = '重新發送';
btn.disabled = false;
}
}, 1000);
} else {
alert(data.message);
btn.textContent = isResend ? '重新發送' : '發送驗證';
btn.disabled = false;
}
})
.catch(err =>{
alert('發送失敗:' + err.message + '\n請確認網路或聯繫客服');
btn.textContent = isResend ? '重新發送' : '發送驗證';
btn.disabled = false;
});
}
function verifyOTP() {
const entered = document.getElementById('otp-input').value.trim();
const email = document.getElementById('applicant_email').value.trim();
if(!entered){ alert('請輸入驗證碼'); return; }
const fd = new FormData();
fd.append('action', 'verify_otp');
fd.append('otp', entered);
fd.append('email', email);
fetch(window.location.href, { method:'POST', body:fd })
.then(r => {
if(!r.ok) throw new Error('HTTP ' + r.status);
return r.text();
})
.then(text => {
let data;
try { data = JSON.parse(text); }
catch(e) { throw new Error('伺服器回應格式錯誤'); }
if(data.success){
otpVerified = true;
document.getElementById('otp-box').style.display='none';
document.getElementById('email-hint').textContent='✔ 信箱已驗證';
document.getElementById('email-hint').className='hint hint-ok';
// 驗證成功後整個按鈕直接隱藏
document.getElementById('send-otp-btn').style.display = 'none';
document.getElementById('applicant_email').readOnly = true;
SS.set('otp_verified', '1');
SS.set('applicant_email', document.getElementById('applicant_email').value);
} else {
document.getElementById('otp-input').style.borderColor='var(--red)';
setTimeout(()=>document.getElementById('otp-input').style.borderColor='',1500);
alert(data.message);
}
})
.catch(err=>alert('驗證失敗:' + err.message));
}
function cancelOTP() {
otpVerified = false;
document.getElementById('otp-box').style.display='none';
document.getElementById('otp-input').value='';
document.getElementById('email-hint').textContent='';
document.getElementById('applicant_email').readOnly = false;
document.getElementById('applicant_email').value = '';
document.getElementById('applicant_email').focus();
const btn = document.getElementById('send-otp-btn');
btn.textContent='發送驗證';
btn.disabled=false;
btn.style.cssText='font-size:12px;padding:5px 10px;border:0.5px solid var(--green);border-radius:var(--radius-md);background:none;cursor:pointer;color:var(--green);white-space:nowrap;font-family:inherit';
}
// ══════════════════════════════════════════
// 室內資訊箱設備(新增列模式)
// ══════════════════════════════════════════
const INFO_DEVICES = ['無','光貓','光轉B','100M有線分享器','1000M有線分享器',
'100M無線分享器','1000M無線分享器','MESH','100M集線器','1000M集線器','UY','Panel'];
let infoIdx = 0;
function getInfoSelected() {
const vals = [];
document.querySelectorAll('.info-group select.dev-select').forEach(s => {
if(s.value) vals.push(s.value);
});
return vals;
}
function rebuildInfoOptions() {
const taken = getInfoSelected();
document.querySelectorAll('.info-group').forEach(g => {
const sel = g.querySelector('select.dev-select');
const cur = sel.value;
const hasWu = taken.includes('無');
sel.innerHTML = '<option value="">— 選擇設備 —</option>' +
INFO_DEVICES.map(d => {
if(d === '無' && hasWu && cur !== '無') return '';
if(d !== '' && d !== cur && d !== '無' && taken.includes(d)) return '';
return `<option value="${d}" ${d===cur?'selected':''}>${d}</option>`;
}).join('');
const qtySel = g.querySelector('.info-qty-sel');
if(qtySel) qtySel.style.display = (cur && cur !== '無') ? 'inline-block' : 'none';
});
const hasWu = taken.includes('無');
document.getElementById('add-info-btn').style.display = hasWu ? 'none' : 'flex';
}
function addInfoGroup() {
const container = document.getElementById('info-container');
const idx = infoIdx++;
const div = document.createElement('div');
div.className = 'use-group info-group';
div.dataset.idx = idx;
div.innerHTML = `
<div class="use-group-header" style="align-items:center;gap:6px">
<select class="use-select dev-select" onchange="onInfoChange(this)" style="flex:1">
<option value="">— 選擇設備 —</option>
${INFO_DEVICES.map(d=>`<option value="${d}">${d}</option>`).join('')}
</select>
<select class="info-qty-sel" style="display:none;font-size:14px;padding:5px 6px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:#fff;font-family:inherit;width:58px;flex-shrink:0">
${Array.from({length:5},(_,i)=>`<option>${i+1}</option>`).join('')}
</select>
<button type="button" class="rm-btn" onclick="removeInfoGroup(this)">
<i class="ti ti-x"></i>
</button>
</div>
`;
container.appendChild(div);
rebuildInfoOptions();
}
function onInfoChange(sel) {
const g = sel.closest('.info-group');
const qtySel = g.querySelector('.info-qty-sel');
const show = sel.value && sel.value !== '無';
qtySel.style.display = show ? 'inline-block' : 'none';
rebuildInfoOptions();
}
function removeInfoGroup(btn) {
btn.closest('.info-group').remove();
rebuildInfoOptions();
}
// ══════════════════════════════════════════
// 室內網路設備(新增列模式)
// ══════════════════════════════════════════
const ROUTER_DEVICES = ['單機','100M有線分享器','1000M有線分享器',
'100M無線分享器','1000M無線分享器','MESH','電視盒','100M集線器','1000M集線器'];
let routerIdx = 0;
function getRouterSelected() {
const vals = [];
document.querySelectorAll('.router-group select.dev-select').forEach(s => {
if(s.value) vals.push(s.value);
});
return vals;
}
function rebuildRouterOptions() {
const taken = getRouterSelected();
const hasSingle = taken.includes('單機');
document.querySelectorAll('.router-group').forEach(g => {
const sel = g.querySelector('select.dev-select');
const cur = sel.value;
sel.innerHTML = '<option value="">— 選擇設備 —</option>' +
ROUTER_DEVICES.map(d => {
if(d === '單機' && hasSingle && cur !== '單機') return '';
if(d !== '' && d !== cur && d !== '單機' && taken.includes(d)) return '';
return `<option value="${d}" ${d===cur?'selected':''}>${d}</option>`;
}).join('');
// 數量(內嵌在 header 同列)
const qtySel = g.querySelector('select.router-qty-sel');
if(qtySel) qtySel.style.display = (cur && cur !== '單機') ? 'inline-block' : 'none';
// 電視盒品牌
const tvWrap = g.querySelector('.tv-brand-wrap');
if(tvWrap) tvWrap.style.display = (cur === '電視盒') ? 'inline-flex' : 'none';
});
document.getElementById('add-router-btn').style.display = hasSingle ? 'none' : 'flex';
}
function addRouterGroup() {
const container = document.getElementById('router-container');
const idx = routerIdx++;
const div = document.createElement('div');
div.className = 'use-group router-group';
div.dataset.idx = idx;
div.innerHTML = `
<div class="use-group-header" style="align-items:center;gap:6px">
<select class="use-select dev-select" onchange="onRouterChange(this)" style="flex:1">
<option value="">— 選擇設備 —</option>
${ROUTER_DEVICES.map(d=>`<option value="${d}">${d}</option>`).join('')}
</select>
<select class="router-qty-sel" style="display:none;width:58px;font-size:14px;padding:5px 4px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:#fff;font-family:inherit;flex-shrink:0">
${Array.from({length:5},(_,i)=>`<option>${i+1}</option>`).join('')}
</select>
<div class="tv-brand-wrap" style="display:none;align-items:center;gap:6px;flex-shrink:0">
<select class="tv-brand-sel" style="font-size:13px;padding:5px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:#fff;font-family:inherit"
onchange="toggleTvOther(this)">
<option value="cm">公司</option>
<option value="or">其他品牌</option>
</select>
<input type="text" class="tv-other-inp" placeholder="輸入廠牌"
style="display:none;font-size:13px;padding:5px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);width:80px">
</div>
<button type="button" class="rm-btn" onclick="removeRouterGroup(this)">
<i class="ti ti-x"></i>
</button>
</div>
`;
container.appendChild(div);
rebuildRouterOptions();
}
function onRouterChange(sel) {
rebuildRouterOptions();
}
function removeRouterGroup(btn) {
btn.closest('.router-group').remove();
rebuildRouterOptions();
}
function toggleTvOther(sel) {
const inp = sel.closest('.tv-brand-wrap').querySelector('.tv-other-inp');
inp.style.display = sel.value === 'or' ? 'inline-block' : 'none';
}
// ══════════════════════════════════════════
// 使用設備(先選用途 → 再選設備 → 填料號/押金/據點)
// ══════════════════════════════════════════
const usePurposeDevices = {
'gift' : ['1000M無線分享器', '100M無線分享器'],
'borrow': ['光貓', '光轉B'],
'node' : ['1000M集線器', '100M集線器', '光轉A']
};
let useIdx = 0;
function addUseGroup() {
const container = document.getElementById('use-container');
const idx = useIdx++;
const div = document.createElement('div');
div.className = 'use-group';
div.dataset.idx = idx;
div.innerHTML = `
<div class="use-group-header" style="display:flex;align-items:center;gap:6px;padding:10px 14px;border-bottom:0.5px solid var(--gray-bd)">
<!-- 用途(先選) -->
<select id="use-purpose-${idx}" class="use-purpose-sel"
style="flex:1;font-size:14px;padding:8px 10px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:#fff;font-family:inherit;color:var(--text);min-width:0"
onchange="onUsePurposeChange(${idx})">
<option value="">— 選擇用途 —</option>
<option value="gift">贈品</option>
<option value="borrow">借用</option>
<option value="node">據點</option>
</select>
<!-- 設備(選完用途才顯示) -->
<select id="use-device-${idx}" class="use-device-sel"
style="display:none;flex:1.4;font-size:14px;padding:8px 10px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:#fff;font-family:inherit;color:var(--text);min-width:0"
onchange="onUseDeviceChange(${idx})">
<option value="">— 選擇設備 —</option>
</select>
<button type="button"
style="width:30px;height:30px;border:none;border-radius:50%;background:var(--gray-bg);display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;color:var(--text-sec)"
onclick="removeUseGroup(this)">
<i class="ti ti-x" style="font-size:13px"></i>
</button>
</div>
<!-- 動態欄位(選完設備才顯示,料號 + 押金/據點,寬度一致) -->
<div id="use-fields-${idx}" style="display:none;padding:10px 14px">
<div style="display:flex;gap:8px;flex-wrap:wrap">
<div style="flex:1;min-width:140px">
<div style="font-size:12px;color:var(--text-sec);margin-bottom:4px">料號</div>
<input type="text" id="use-sn-${idx}" placeholder="輸入料號"
style="width:100%;font-size:14px;padding:9px 12px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);font-family:monospace;background:#fff;height:40px"
oninput="saveAllToSS()">
</div>
<!-- 押金(借用時顯示,鎖死 1000) -->
<div id="use-deposit-wrap-${idx}" style="display:none;flex:1;min-width:140px">
<div style="font-size:12px;color:var(--text-sec);margin-bottom:4px">押金(元)</div>
<input type="text" id="use-dp-${idx}" value="" readonly
style="width:100%;font-size:14px;padding:9px 12px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);font-family:inherit;background:#f5f5f5;color:#666;height:40px;cursor:not-allowed">
</div>
<!-- 據點編號(據點時顯示) -->
<div id="use-nodepos-wrap-${idx}" style="display:none;flex:1;min-width:140px">
<div style="font-size:12px;color:var(--text-sec);margin-bottom:4px">據點編號</div>
<input type="text" id="use-node-pos-${idx}" placeholder="輸入據點編號"
style="width:100%;font-size:14px;padding:9px 12px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);font-family:inherit;background:#fff;height:40px"
oninput="saveAllToSS()">
</div>
</div>
</div>
`;
container.appendChild(div);
updateAddUseBtn();
}
function onUsePurposeChange(idx) {
const purpose = document.getElementById('use-purpose-'+idx).value;
const deviceSel = document.getElementById('use-device-'+idx);
const fieldsW = document.getElementById('use-fields-'+idx);
// 重置設備與欄位
deviceSel.value = '';
fieldsW.style.display = 'none';
// 切換用途時,若不是借用,立即清空押金值,避免殘留舊值被誤算
if (purpose !== 'borrow') {
const dp = document.getElementById('use-dp-'+idx);
if (dp) dp.value = '';
}
if (purpose && usePurposeDevices[purpose]) {
deviceSel.innerHTML = '<option value="">— 選擇設備 —</option>' +
usePurposeDevices[purpose].map(d => `<option value="${d}">${d}</option>`).join('');
deviceSel.style.display = 'inline-block';
} else {
deviceSel.style.display = 'none';
deviceSel.innerHTML = '<option value="">— 選擇設備 —</option>';
}
calcDeposit();
if(!_pauseSave) saveAllToSS();
}
function onUseDeviceChange(idx) {
const purpose = document.getElementById('use-purpose-'+idx).value;
const device = document.getElementById('use-device-'+idx).value;
const fieldsW = document.getElementById('use-fields-'+idx);
const dpW = document.getElementById('use-deposit-wrap-'+idx);
const npW = document.getElementById('use-nodepos-wrap-'+idx);
if (device) {
fieldsW.style.display = 'block';
dpW.style.display = (purpose === 'borrow') ? 'block' : 'none';
npW.style.display = (purpose === 'node') ? 'block' : 'none';
if (purpose === 'borrow') {
const dp = document.getElementById('use-dp-'+idx);
if (dp) dp.value = '1000';
} else {
// 非借用:確保押金欄位為空,不參與計算
const dp = document.getElementById('use-dp-'+idx);
if (dp) dp.value = '';
}
} else {
fieldsW.style.display = 'none';
}
calcDeposit();
if(!_pauseSave) saveAllToSS();
}
function removeUseGroup(btn) {
btn.closest('.use-group').remove();
calcDeposit();
updateAddUseBtn();
if(!_pauseSave) saveAllToSS();
}
function updateAddUseBtn(){
const count = document.querySelectorAll('#use-container > .use-group').length;
document.getElementById('add-use-btn').style.display = count >= 5 ? 'none' : 'flex';
}
// 統一的押金加總函式:只計算「用途=借用」的項目,
// 避免贈品/據點因隱藏的 use-dp 輸入框殘留數值而被誤算進押金。
function getTotalDeposit() {
let total = 0;
document.querySelectorAll('#use-container > .use-group').forEach(g => {
const idx = g.dataset.idx;
const purpose = document.getElementById('use-purpose-'+idx)?.value || '';
if (purpose === 'borrow') {
const dp = document.getElementById('use-dp-'+idx);
total += parseInt(dp?.value) || 0;
}
});
return total;
}
function calcDeposit() {
const total = getTotalDeposit();
const fmt = '$' + total.toLocaleString();
document.getElementById('deposit-display').textContent = fmt;
const show = total > 0;
document.getElementById('deposit-summary').style.display = show ? 'block' : 'none';
// 底部押金
const barWrap = document.getElementById('bar-deposit-wrap');
if(barWrap){
barWrap.style.display = show ? 'block' : 'none';
document.getElementById('bar-deposit').textContent = fmt;
}
calcTotal();
}
// ══════════════════════════════════════════
// 贈送月份(已選選項不重複;只有最後一列可改原因;
// 住戶介紹 → 介紹用戶資訊欄位;他網轉入 → 他網名稱欄位)
// ══════════════════════════════════════════
const giftTypes = ['首裝優惠','他網轉入','總幹事介紹','業務贈送','主管贈送','LINE推廣','住戶介紹','團購優惠'];
let giftIdx = 0;
function getGiftSelected() {
const vals = [];
document.querySelectorAll('.gift-group select.gift-reason').forEach(s => {
if(s.value) vals.push(s.value);
});
return vals;
}
function rebuildGiftOptions() {
const groups = document.querySelectorAll('.gift-group');
const taken = getGiftSelected();
groups.forEach((g, i) => {
const isLast = (i === groups.length - 1);
const sel = g.querySelector('select.gift-reason');
const cur = sel.value;
sel.disabled = !isLast;
sel.innerHTML = giftTypes.map(t => {
if(t !== cur && taken.includes(t)) return '';
return `<option value="${t}" ${t===cur?'selected':''}>${t}</option>`;
}).join('');
});
}
function _buildGiftGroupHTML(idx, reason, months, intro, network) {
const introVal = (intro || '').replace(/"/g,'"');
const networkVal = (network || '').replace(/"/g,'"');
return `
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:6px;width:100%">
<select class="gift-select gift-reason" style="flex:1;min-width:110px"
onchange="onGiftReasonChange(${idx})">
${giftTypes.map(t => `<option value="${t}" ${t===reason?'selected':''}>${t}</option>`).join('')}
</select>
<input type="text" class="gift-network" id="gift-network-${idx}"
value="${networkVal}" placeholder="他網名稱"
style="display:${reason==='他網轉入'?'inline-block':'none'};width:130px;font-size:14px;padding:6px 8px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:#fff;font-family:inherit"
oninput="saveAllToSS()">
<select class="gift-qty" style="width:60px"
onchange="calcDates();saveAllToSS()">
${Array.from({length:12},(_,i)=>`<option value="${i+1}" ${(i+1)===parseInt(months)?'selected':''}>${i+1}月</option>`).join('')}
</select>
<button type="button" class="rm-btn" onclick="removeGift(this)">
<i class="ti ti-x"></i>
</button>
</div>
<input type="text" class="gift-intro" id="gift-intro-${idx}"
value="${introVal}" placeholder="介紹用戶資訊(姓名/用戶編號等)"
style="display:${reason==='住戶介紹'?'block':'none'};width:100%;margin-top:6px;font-size:14px;padding:7px 10px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:#fff;font-family:inherit"
oninput="saveAllToSS()">
`;
}
function addGiftGroup() {
const container = document.getElementById('gift-container');
// 移除所有靜態空白提示
container.querySelectorAll('.gift-empty').forEach(el => el.remove());
[...container.childNodes].forEach(n => { if(n.nodeType === 3) n.remove(); });
const taken = getGiftSelected();
const avail = giftTypes.filter(t => !taken.includes(t));
if(avail.length === 0){ alert('所有贈送類型已新增完畢'); return; }
const idx = giftIdx++;
const defaultReason = avail[0];
const div = document.createElement('div');
div.className = 'gift-group';
div.dataset.idx = idx;
div.style.cssText = 'display:flex;flex-direction:column;padding:9px 14px;border-bottom:0.5px solid var(--gray-bd)';
div.innerHTML = _buildGiftGroupHTML(idx, defaultReason, 1, '', '');
container.appendChild(div);
rebuildGiftOptions();
calcDates();
saveAllToSS();
}
// 還原專用:直接插入指定原因+月數+附加欄位,不受過濾邏輯影響
function restoreGiftGroup(reason, months, intro, network) {
const container = document.getElementById('gift-container');
container.querySelectorAll('.gift-empty').forEach(el => el.remove());
[...container.childNodes].forEach(n => { if(n.nodeType === 3) n.remove(); });
const idx = giftIdx++;
const div = document.createElement('div');
div.className = 'gift-group';
div.dataset.idx = idx;
div.style.cssText = 'display:flex;flex-direction:column;padding:9px 14px;border-bottom:0.5px solid var(--gray-bd)';
div.innerHTML = _buildGiftGroupHTML(idx, reason, months, intro, network);
container.appendChild(div);
// 插入完所有 gift 後再統一 rebuild(由呼叫端決定時機)
}
function onGiftReasonChange(idx) {
const group = document.querySelector(`.gift-group[data-idx="${idx}"]`);
if (!group) return;
const reason = group.querySelector('.gift-reason')?.value || '';
const introInp = document.getElementById('gift-intro-'+idx);
const networkInp = document.getElementById('gift-network-'+idx);
if (introInp) introInp.style.display = (reason === '住戶介紹') ? 'block' : 'none';
if (networkInp) networkInp.style.display = (reason === '他網轉入') ? 'inline-block' : 'none';
rebuildGiftOptions();
calcDates();
if(!_pauseSave) saveAllToSS();
}
function removeGift(btn){
btn.closest('.gift-group').remove();
if(document.querySelectorAll('.gift-group').length === 0){
// 重新放回空白提示
const container = document.getElementById('gift-container');
container.innerHTML = '<div class="gift-empty" style="padding:12px 14px;font-size:13px;color:var(--text-tert)">目前無贈送月份</div>';
}
rebuildGiftOptions();
calcDates();
saveAllToSS();
}
function calcDatesFromDOM(){ calcDates(); }
let selectedPlanPrice = 0;
let selectedPlanType = '';
function selectPlan(card) {
document.querySelectorAll('.plan-card').forEach(c=>c.classList.remove('selected'));
card.classList.add('selected');
selectedPlanPrice = parseInt(card.dataset.price) || 0;
selectedPlanType = card.dataset.key || '';
calcDates();
calcTotal();
// 顯示取消選擇按鈕(定期定額無此按鈕,DOM 不存在則略過)
const clearWrap = document.getElementById('plan-clear-wrap');
if(clearWrap) clearWrap.style.display = 'block';
// 立即存入 sessionStorage,避免重整後消失
if(!_pauseSave) saveAllToSS();
}
function clearPlanSelection() {
document.querySelectorAll('.plan-card').forEach(c=>c.classList.remove('selected'));
selectedPlanPrice = 0;
selectedPlanType = '';
const clearWrap = document.getElementById('plan-clear-wrap');
if(clearWrap) clearWrap.style.display = 'none';
calcDates();
calcTotal();
if(!_pauseSave) saveAllToSS();
}
// ── 加收項目 ─────────────────────────
let extraIdx = 0;
const extraTypes = ['配線費用','裝機費用','周邊設備'];
function addExtraRow() {
const container = document.getElementById('extra-container');
const idx = extraIdx++;
const div = document.createElement('div');
div.className = 'extra-row';
div.dataset.idx = idx;
div.style.cssText = 'display:flex;align-items:center;gap:6px;padding:9px 14px;border-bottom:0.5px solid var(--gray-bd)';
div.innerHTML = `
<select class="extra-select"
style="flex:1;font-size:14px;padding:7px 10px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:#fff;color:var(--text);font-family:inherit;min-width:0;height:38px"
onchange="onExtraTypeChange(this,${idx})">
${extraTypes.map(t=>`<option>${t}</option>`).join('')}
</select>
<input class="extra-sn-inline" type="text" id="extra-sn-inp-${idx}"
placeholder="料號" inputmode="numeric" maxlength="10"
style="display:none;width:100px;font-size:14px;padding:7px 8px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:#fff;color:var(--text);font-family:inherit;flex-shrink:0;height:38px"
oninput="this.value=this.value.replace(/\\D/g,'').slice(0,10);saveAllToSS()">
<input class="extra-amount" type="text" placeholder="金額"
inputmode="numeric" maxlength="4"
style="width:72px;font-size:14px;padding:7px 8px;text-align:right;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:#fff;color:var(--text);font-family:inherit;flex-shrink:0;height:38px"
oninput="this.value=this.value.replace(/\\D/g,'').slice(0,4);calcTotal();saveAllToSS()">
<button type="button" class="rm-btn"
onclick="this.closest('.extra-row').remove();calcTotal();saveAllToSS()">
<i class="ti ti-x"></i>
</button>
`;
container.appendChild(div);
}
function onExtraTypeChange(sel, idx) {
const snInp = document.getElementById('extra-sn-inp-'+idx);
if(snInp) snInp.style.display = sel.value === '周邊設備' ? 'inline-block' : 'none';
// 不直接呼叫 saveAllToSS,改由 change 事件 bubble 觸發,
// 這樣在 _pauseSave=true 期間不會提前覆蓋資料
if(!_pauseSave) saveAllToSS();
}
// ── 日期計算 ─────────────────────────
function fmtDate(d) {
if(!d || isNaN(d.getTime())) return '—';
return `${d.getFullYear()}年${String(d.getMonth()+1).padStart(2,'0')}月${String(d.getDate()).padStart(2,'0')}日`;
}
function addMonths(dateStr, months) {
const d = new Date(dateStr);
const day = d.getDate();
d.setDate(1);
d.setMonth(d.getMonth() + months);
const lastDay = new Date(d.getFullYear(), d.getMonth()+1, 0).getDate();
d.setDate(Math.min(day, lastDay));
return d;
}
function calcDates() {
const today = new Date(PHP.today);
const rentStart = new Date(today);
rentStart.setDate(rentStart.getDate() + 1);
document.getElementById('rent-start').textContent = fmtDate(rentStart);
const monthsMap = { season:3, halfyear:6, year:12, twoyear:24 };
const months = monthsMap[selectedPlanType] || 0;
let rentEnd = null;
if (PHP.endday) {
// ── 有 FileMaker 帶入到期日:以 endday 為基準 ──
if (months > 0) {
// 有選擇方案:到期日 = endday + 方案月份
rentEnd = addMonths(PHP.endday, months);
} else {
// 未選擇方案:到期日直接顯示 endday
rentEnd = new Date(PHP.endday + 'T00:00:00');
}
document.getElementById('rent-end').textContent = fmtDate(rentEnd);
} else if (months > 0) {
// ── 無 endday:照舊以今天為基準 + 方案月份 ──
rentEnd = addMonths(PHP.today, months);
document.getElementById('rent-end').textContent = fmtDate(rentEnd);
} else {
document.getElementById('rent-end').textContent = '—';
}
// 含贈送後到期:只有真正有贈送群組時才顯示
const giftGroups = document.querySelectorAll('#gift-container .gift-group');
const realEndCard = document.getElementById('real-end-card');
if (giftGroups.length === 0 || !rentEnd) {
if(realEndCard) realEndCard.style.display = 'none';
return;
}
let gifted = 0;
giftGroups.forEach(g => { gifted += parseInt(g.querySelector('.gift-qty')?.value) || 0; });
if (gifted > 0) {
const realEnd = addMonths(rentEnd.toISOString().split('T')[0], gifted);
document.getElementById('real-end').textContent = fmtDate(realEnd);
if(realEndCard) realEndCard.style.display = 'block';
} else {
if(realEndCard) realEndCard.style.display = 'none';
}
}
function calcTotal() {
let total = selectedPlanPrice;
document.querySelectorAll('.extra-amount').forEach(inp => {
total += parseInt(inp.value) || 0;
});
// 定期定額:費用欄顯示文字,底部也改顯示文字
const totalEl = document.getElementById('total-display');
const barEl = document.getElementById('bar-total');
if(PHP.isSubscription && total === 0){
if(totalEl) totalEl.textContent = '信用卡定期定額繳費';
if(barEl) barEl.textContent = '信用卡扣款';
} else {
if(totalEl) totalEl.textContent = '$' + total.toLocaleString();
if(barEl) barEl.textContent = '$' + total.toLocaleString();
}
// 底部押金顯示(只計算用途=借用的項目)
const deposit = getTotalDeposit();
const wrap = document.getElementById('bar-deposit-wrap');
const amt = document.getElementById('bar-deposit');
if(wrap && amt){
wrap.style.display = deposit > 0 ? 'block' : 'none';
amt.textContent = '$' + deposit.toLocaleString();
}
SS.set('total', total);
}
// ══════════════════════════════════════════
// sessionStorage 持久化
// ══════════════════════════════════════════
const SS = {
set(k, v) { try { sessionStorage.setItem('ys_'+k, typeof v === 'object' ? JSON.stringify(v) : String(v)); } catch(e){} },
// null = key 不存在;空字串 '' = 存在但為空
get(k) { try { return sessionStorage.getItem('ys_'+k); } catch(e){ return null; } },
getJ(k, def=null) { try { const v=sessionStorage.getItem('ys_'+k); return v!==null ? JSON.parse(v) : def; } catch(e){ return def; } },
clear() { try { Object.keys(sessionStorage).filter(k=>k.startsWith('ys_')).forEach(k=>sessionStorage.removeItem(k)); } catch(e){} }
};
function saveAllToSS() {
if(_pauseSave) return; // restore 期間不存,避免覆蓋正確資料
// 申請人欄位
['applicant_name','applicant_usernumber','applicant_phone','applicant_homenumber','applicant_email'].forEach(id => {
const el = document.getElementById(id);
if(el) SS.set(id, el.value);
});
SS.set('id_type', document.getElementById('id-type').value);
SS.set('otp_verified', otpVerified ? '1' : '0');
// 設備
const infoList = [];
document.querySelectorAll('.info-group').forEach(g => {
const sel = g.querySelector('select.dev-select');
if(!sel || !sel.value) return;
const qty = g.querySelector('select.info-qty-sel')?.value || '1';
infoList.push({ device: sel.value, qty });
});
SS.set('info_list', infoList);
const routerList = [];
document.querySelectorAll('.router-group').forEach(g => {
const sel = g.querySelector('select.dev-select');
if(!sel || !sel.value) return;
const qty = g.querySelector('select.router-qty-sel')?.value || '1';
const tvBrand = g.querySelector('select.tv-brand-sel')?.value || '';
const tvOther = g.querySelector('input.tv-other-inp')?.value || '';
routerList.push({ device: sel.value, qty, tvBrand, tvOther });
});
SS.set('router_list', routerList);
// 使用設備
const uses = [];
document.querySelectorAll('#use-container > .use-group').forEach(g => {
const idx = g.dataset.idx;
const purpose = document.getElementById('use-purpose-'+idx)?.value || '';
const device = document.getElementById('use-device-'+idx)?.value || '';
if (!purpose || !device) return;
const sn = document.getElementById('use-sn-'+idx)?.value || '';
const dp = purpose === 'borrow' ? (document.getElementById('use-dp-'+idx)?.value || '1000') : '';
const nodePos = purpose === 'node' ? (document.getElementById('use-node-pos-'+idx)?.value || '') : '';
uses.push({ device, purpose, sn, dp, nodePos });
});
SS.set('use_list', uses);
// 費率
SS.set('plan_type', selectedPlanType);
SS.set('plan_price', selectedPlanPrice);
// 加收
const extras = [];
document.querySelectorAll('.extra-row').forEach(row => {
const type = row.querySelector('.extra-select')?.value || '';
const amt = parseInt(row.querySelector('.extra-amount')?.value) || 0;
const idx = row.dataset.idx;
const sn = document.getElementById('extra-sn-inp-'+idx)?.value || '';
extras.push({ type, amt, sn });
});
SS.set('extras', extras);
// 贈送月份
const gifts = [];
document.querySelectorAll('.gift-group').forEach(g => {
const idx = g.dataset.idx;
const reason = g.querySelector('.gift-reason').value;
const months = g.querySelector('.gift-qty').value;
const intro = document.getElementById('gift-intro-'+idx)?.value || '';
const network = document.getElementById('gift-network-'+idx)?.value || '';
gifts.push({ reason, months, intro, network });
});
SS.set('gifts', gifts);
}
function restoreFromSS() {
_pauseSave = true;
// 申請人 — 只在 SS 中有值時才填入(不覆蓋空字串到有值的欄位)
['applicant_name','applicant_usernumber','applicant_phone','applicant_homenumber'].forEach(id => {
const v = SS.get(id);
if(v !== null){ // null = 從未存過,不動;'' = 存過空字串,填入
const el = document.getElementById(id);
if(el) el.value = v;
}
});
// 身分證類型
const idType = SS.get('id_type');
if(idType !== null){
const el = document.getElementById('id-type');
if(el) el.value = idType;
}
// Email
const emailV = SS.get('applicant_email');
if(emailV !== null && emailV !== ''){
const el = document.getElementById('applicant_email');
if(el){
el.value = emailV;
if(SS.get('otp_verified') === '1'){
otpVerified = true;
el.readOnly = true;
const hint = document.getElementById('email-hint');
if(hint){ hint.textContent='✔ 信箱已驗證'; hint.className='hint hint-ok'; }
const btn = document.getElementById('send-otp-btn');
if(btn) btn.style.display='none';
}
}
}
// 設備 - 資訊箱
const infoList = SS.getJ('info_list', []);
infoList.forEach(item => {
addInfoGroup();
const allG = document.querySelectorAll('#info-container .info-group');
const g = allG[allG.length - 1]; if(!g) return;
const sel = g.querySelector('select.dev-select');
if(sel){ sel.value = item.device; onInfoChange(sel); }
const qtySel = g.querySelector('select.info-qty-sel');
if(qtySel) qtySel.value = item.qty;
});
// 設備 - 網路
const routerList = SS.getJ('router_list', []);
routerList.forEach(item => {
addRouterGroup();
const allG = document.querySelectorAll('#router-container .router-group');
const g = allG[allG.length - 1]; if(!g) return;
const sel = g.querySelector('select.dev-select');
if(sel){ sel.value = item.device; onRouterChange(sel); }
const qtySel = g.querySelector('select.router-qty-sel');
if(qtySel) qtySel.value = item.qty;
if(item.tvBrand){ const tb=g.querySelector('select.tv-brand-sel'); if(tb) tb.value=item.tvBrand; }
if(item.tvOther){ const to=g.querySelector('input.tv-other-inp'); if(to){ to.value=item.tvOther; to.style.display='inline-block'; } }
});
// 使用設備
const useList = SS.getJ('use_list', []);
useList.forEach(item => {
addUseGroup();
const allUse = document.querySelectorAll('#use-container > .use-group');
const g = allUse[allUse.length - 1]; if(!g) return;
const idx = g.dataset.idx;
const purposeSel = document.getElementById('use-purpose-'+idx);
if(purposeSel && item.purpose){
purposeSel.value = item.purpose;
onUsePurposeChange(idx);
}
const deviceSel = document.getElementById('use-device-'+idx);
if(deviceSel && item.device){
deviceSel.value = item.device;
onUseDeviceChange(idx);
}
const sn = document.getElementById('use-sn-'+idx);
if(sn) sn.value = item.sn || '';
if(item.purpose === 'borrow'){
const dp = document.getElementById('use-dp-'+idx);
if(dp) dp.value = item.dp || '1000';
}
if(item.purpose === 'node'){
const np = document.getElementById('use-node-pos-'+idx);
if(np) np.value = item.nodePos || '';
}
});
calcDeposit();
// 費率 — restore 期間 selectPlan 內的 saveAllToSS 被 _pauseSave 擋住,不會誤存
const planType = SS.get('plan_type');
if(planType){
const card = document.querySelector(`.plan-card[data-key="${planType}"]`);
if(card){
document.querySelectorAll('.plan-card').forEach(c=>c.classList.remove('selected'));
card.classList.add('selected');
selectedPlanPrice = parseInt(card.dataset.price) || 0;
selectedPlanType = planType;
const clearWrap = document.getElementById('plan-clear-wrap');
if(clearWrap) clearWrap.style.display = 'block';
}
}
// 加收
const extras = SS.getJ('extras', []);
extras.forEach(item => {
addExtraRow();
const rows = document.querySelectorAll('#extra-container .extra-row');
const row = rows[rows.length-1]; if(!row) return;
const idx = row.dataset.idx;
const sel = row.querySelector('.extra-select');
if(sel){ sel.value = item.type; onExtraTypeChange(sel, idx); }
// 用 String() 轉換避免 0 被 || '' 轉成空字串
const inp = row.querySelector('.extra-amount');
if(inp) inp.value = (item.amt != null && item.amt !== '') ? String(item.amt) : '';
if(item.sn != null && item.sn !== ''){
const snInp = document.getElementById('extra-sn-inp-'+idx);
if(snInp){ snInp.value = String(item.sn); snInp.style.display = 'inline-block'; }
}
});
calcTotal();
// 贈送月份 —— 使用還原專用函式,所有項目插入後再統一 rebuild
const gifts = SS.getJ('gifts', []);
gifts.forEach(item => {
restoreGiftGroup(item.reason, item.months, item.intro, item.network);
});
if(gifts.length > 0) rebuildGiftOptions();
calcDates();
_pauseSave = false;
// 若為定期定額方案但 selectedPlanType 還是空(SS 沒有存),補選一次
if(PHP.isSubscription && !selectedPlanType){
const subCard = document.getElementById('plan-sub');
if(subCard){
subCard.classList.add('selected');
selectedPlanType = 'subscription';
selectedPlanPrice = 0;
}
}
// restore 完成後立即存一次,確保 SS 與 DOM 同步
saveAllToSS();
}
// 自動存檔(input/change 事件),restore 期間暫停
document.addEventListener('input', () => { if(!_pauseSave) saveAllToSS(); });
document.addEventListener('change', () => { if(!_pauseSave) saveAllToSS(); });
// ── 送出驗證與打包 ───────────────────
function collectFormData() {
// 申請人
document.getElementById('f_name').value = document.getElementById('applicant_name').value;
document.getElementById('f_id').value = document.getElementById('applicant_usernumber').value;
document.getElementById('f_idtype').value = document.getElementById('id-type').value;
document.getElementById('f_phone').value = document.getElementById('applicant_phone').value;
document.getElementById('f_home').value = document.getElementById('applicant_homenumber').value;
document.getElementById('f_email').value = document.getElementById('applicant_email').value;
// enname:PHP 靜態輸出已有值,若為空則從 sessionStorage 補
const ennameInput = document.querySelector('input[name="enname"]');
if(ennameInput && !ennameInput.value) {
const ssEnname = sessionStorage.getItem('ys_enname') || PHP.enname || '';
ennameInput.value = ssEnname;
}
// 設備 - 資訊箱
const infoList = [];
document.querySelectorAll('.info-group').forEach(g => {
const sel = g.querySelector('select.dev-select');
if(!sel || !sel.value) return;
const qty = g.querySelector('select.info-qty-sel')?.value || '1';
infoList.push({ device: sel.value, qty });
});
document.getElementById('f_info').value = JSON.stringify(infoList);
document.getElementById('f_info_qty').value = ''; // 已整合進 JSON
// 設備 - 網路
const routerList = [];
document.querySelectorAll('.router-group').forEach(g => {
const sel = g.querySelector('select.dev-select');
if(!sel || !sel.value) return;
const qty = g.querySelector('select.router-qty-sel')?.value || '1';
const tvBrand = g.querySelector('select.tv-brand-sel')?.value || '';
const tvOther = g.querySelector('input.tv-other-inp')?.value || '';
routerList.push({ device: sel.value, qty, tvBrand, tvOther });
});
document.getElementById('f_router').value = JSON.stringify(routerList);
document.getElementById('f_router_qty').value = '';
// 使用設備
const uses = [];
document.querySelectorAll('#use-container > .use-group').forEach(g => {
const idx = g.dataset.idx;
const purpose = document.getElementById('use-purpose-'+idx)?.value || '';
const device = document.getElementById('use-device-'+idx)?.value || '';
if(!purpose || !device) return;
const sn = document.getElementById('use-sn-'+idx)?.value || '';
const dp = purpose === 'borrow' ? (document.getElementById('use-dp-'+idx)?.value || '1000') : '';
const nodePos = purpose === 'node' ? (document.getElementById('use-node-pos-'+idx)?.value || '') : '';
uses.push({ device, purpose, sn, dp, nodePos });
});
document.getElementById('f_use').value = JSON.stringify(uses);
// 費率
document.getElementById('f_fee_type').value = selectedPlanType;
document.getElementById('f_fee_amount').value = selectedPlanPrice;
// 加收(包含周邊設備的料號)
const extras = [];
document.querySelectorAll('#extra-container .extra-row').forEach(row => {
const type = row.querySelector('.extra-select')?.value || '';
const amt = parseInt(row.querySelector('.extra-amount')?.value) || 0;
const idx = row.dataset.idx;
const sn = document.getElementById('extra-sn-inp-'+idx)?.value || '';
if(type) extras.push({ type, amt, sn });
});
document.getElementById('f_extra').value = JSON.stringify(extras);
// 金額
let total = selectedPlanPrice;
document.querySelectorAll('.extra-amount').forEach(i=>total+=parseInt(i.value)||0);
document.getElementById('f_total').value = total;
// 押金(只計算用途=借用的項目)
document.getElementById('f_deposit').value = getTotalDeposit();
// 日期
const today = new Date(PHP.today);
const rStart = new Date(today); rStart.setDate(rStart.getDate()+1);
document.getElementById('f_rent_start').value = rStart.toISOString().split('T')[0];
const monthsMap = {season:3,halfyear:6,year:12,twoyear:24};
const m = monthsMap[selectedPlanType]||0;
let rEnd = null;
if (PHP.endday) {
// 有 FileMaker 帶入到期日:以 endday 為基準
rEnd = m ? addMonths(PHP.endday, m) : new Date(PHP.endday + 'T00:00:00');
} else if (m > 0) {
rEnd = addMonths(PHP.today, m);
}
document.getElementById('f_rent_end').value = rEnd ? rEnd.toISOString().split('T')[0] : '';
// 贈送
const gifts = [];
document.querySelectorAll('.gift-group').forEach(g=>{
const idx = g.dataset.idx;
gifts.push({
reason : g.querySelector('.gift-reason').value,
months : g.querySelector('.gift-qty').value,
intro : document.getElementById('gift-intro-'+idx)?.value || '',
network: document.getElementById('gift-network-'+idx)?.value || ''
});
});
document.getElementById('f_gift').value = JSON.stringify(gifts);
let gifted=0; gifts.forEach(g=>gifted+=parseInt(g.months));
let realEnd = rEnd ? addMonths(rEnd.toISOString().split('T')[0], gifted) : null;
document.getElementById('f_real_end').value = realEnd ? realEnd.toISOString().split('T')[0] : '';
}
function submitForm() {
const missing = [];
// 身分證
const idVal = document.getElementById('applicant_usernumber').value.trim();
const idType = document.getElementById('id-type').value;
if(!idVal){
missing.push('請輸入證件號碼');
} else {
if(idType==='twid' && !isValidTWID(idVal)) missing.push('身分證字號格式錯誤');
if(idType==='taxid'&& !isValidTaxID(idVal)) missing.push('統一編號格式錯誤');
if(idType==='arc' && !isValidARC(idVal)) missing.push('居留證格式錯誤');
}
// 信箱
const emailVal = document.getElementById('applicant_email').value.trim();
if(!emailVal){ missing.push('請輸入電子信箱'); }
else if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal)){ missing.push('電子信箱格式錯誤'); }
else if(!otpVerified){ missing.push('請完成電子信箱驗證'); }
// 設備
const infoCount = document.querySelectorAll('.info-group select.dev-select')
.length > 0 && [...document.querySelectorAll('.info-group select.dev-select')].some(s=>s.value);
const routerCount = document.querySelectorAll('.router-group select.dev-select')
.length > 0 && [...document.querySelectorAll('.router-group select.dev-select')].some(s=>s.value);
if(!infoCount) missing.push('請選擇至少一項室內資訊箱設備');
if(!routerCount) missing.push('請選擇至少一項室內網路設備');
// 費率
if(!selectedPlanType && !PHP.isSubscription && !PHP.endday) missing.push('請選擇費率方案');
if(missing.length){
// 跳到有問題的步驟
if(missing.some(m=>m.includes('信箱')||m.includes('身分')||m.includes('證件'))) goStep(0);
else if(missing.some(m=>m.includes('設備'))) goStep(1);
else if(missing.some(m=>m.includes('費率'))) goStep(2);
alert(missing.join('\n'));
return;
}
collectFormData();
document.getElementById('submit-form').submit();
}
// 初始化
<?php if ($isSubscription): ?>
// 定期定額:先強制選取一次
(function(){
const subCard = document.getElementById('plan-sub');
if(subCard) selectPlan(subCard);
})();
<?php endif; ?>
calcDates();
restoreFromSS();
<?php if ($isSubscription): ?>
// restoreFromSS 後強制確認,並修正費用顯示
(function(){
const subCard = document.getElementById('plan-sub');
if(subCard) selectPlan(subCard);
const totalEl = document.getElementById('total-display');
const barEl = document.getElementById('bar-total');
if(totalEl && totalEl.textContent.startsWith('$0')) totalEl.textContent = '信用卡定期定額繳費';
if(barEl && barEl.textContent.startsWith('$0')) barEl.textContent = '信用卡扣款';
})();
<?php endif; ?>
// 若從 preview 返回,OTP 已驗過,直接標記
<?php if (!empty($_GET['from_preview'])): ?>
(function(){
// ── DEBUG:列出所有 ys_ 開頭的 sessionStorage key ──
const ssDebug = {};
Object.keys(sessionStorage).filter(k=>k.startsWith('ys_')).forEach(k=>{ ssDebug[k]=sessionStorage.getItem(k); });
console.log('[from_preview] sessionStorage dump:', JSON.stringify(ssDebug, null, 2));
// ════════════════════════════════════════════════
// 根本解法:直接從 PHP Session 注入所有資料到 DOM
// 不依賴 sessionStorage,確保資料一定在
// ════════════════════════════════════════════════
const previewData = <?= json_encode([
'applicant_name' => $_SESSION['preview']['applicant_name'] ?? '',
'applicant_phone' => $_SESSION['preview']['applicant_phone'] ?? '',
'applicant_homenumber' => $_SESSION['preview']['applicant_homenumber'] ?? '',
'applicant_usernumber' => $_SESSION['preview']['applicant_usernumber'] ?? '',
'applicant_email' => $_SESSION['preview']['applicant_email'] ?? '',
'id_type' => $_SESSION['preview']['id_type'] ?? 'twid',
'plan_type' => $_SESSION['preview']['fee_type'] ?? '',
'plan_price' => (int)($_SESSION['preview']['fee_amount'] ?? 0),
'otp_verified' => !empty($_SESSION['otp_verified']),
// FileMaker 靜態欄位(debug 用)
'_debug_member' => $_SESSION['form_member'] ?? 'MISSING',
'_debug_address' => $_SESSION['form_memberaddress'] ?? 'MISSING',
'_debug_speed' => $_SESSION['form_speed'] ?? 'MISSING',
'_debug_pppoeid' => $_SESSION['form_pppoeid'] ?? 'MISSING',
]) ?>;
// 1. 申請人欄位直接填入 DOM
const fieldMap = {
'applicant_name': 'applicant_name',
'applicant_phone': 'applicant_phone',
'applicant_homenumber': 'applicant_homenumber',
'applicant_usernumber': 'applicant_usernumber',
'applicant_email': 'applicant_email',
};
Object.entries(fieldMap).forEach(([key, id]) => {
const el = document.getElementById(id);
if(el && previewData[key]) el.value = previewData[key];
});
// 2. 身分證類型
if(previewData.id_type){
const el = document.getElementById('id-type');
if(el) el.value = previewData.id_type;
}
// 3. Email OTP 狀態
if(previewData.otp_verified || <?= !empty($_SESSION['otp_verified']) ? 'true' : 'false' ?>){
otpVerified = true;
const emailEl = document.getElementById('applicant_email');
if(emailEl) emailEl.readOnly = true;
const hint = document.getElementById('email-hint');
if(hint){ hint.textContent='✔ 信箱已驗證'; hint.className='hint hint-ok'; }
const btn = document.getElementById('send-otp-btn');
if(btn) btn.style.display='none';
SS.set('otp_verified','1');
}
// 4. 費率方案 — 直接從 PHP Session 的 fee_type 選中對應 card
if(previewData.plan_type){
const card = document.querySelector('.plan-card[data-key="'+previewData.plan_type+'"]');
if(card){
document.querySelectorAll('.plan-card').forEach(c=>c.classList.remove('selected'));
card.classList.add('selected');
selectedPlanType = previewData.plan_type;
selectedPlanPrice = previewData.plan_price || (parseInt(card.dataset.price)||0);
const clearWrap1 = document.getElementById('plan-clear-wrap');
if(clearWrap1) clearWrap1.style.display = 'block';
console.log('[from_preview] 已還原費率:', selectedPlanType, selectedPlanPrice);
} else {
console.warn('[from_preview] 找不到費率 card:', previewData.plan_type);
}
}
// 5. 同步到 sessionStorage(確保下次重整也能還原)
SS.set('applicant_name', previewData.applicant_name);
SS.set('applicant_phone', previewData.applicant_phone);
SS.set('applicant_homenumber', previewData.applicant_homenumber);
SS.set('applicant_usernumber', previewData.applicant_usernumber);
SS.set('applicant_email', previewData.applicant_email);
SS.set('id_type', previewData.id_type);
SS.set('plan_type', previewData.plan_type);
SS.set('plan_price', previewData.plan_price);
// 5b. 設備與加收 — 從 PHP Session 補回 sessionStorage
// (這些是 JS 存的,若 sessionStorage 已有就不覆蓋)
<?php
$prevInfoDev = $_SESSION['preview']['info_devices'] ?? '[]';
$prevRouterDev = $_SESSION['preview']['router_devices'] ?? '[]';
$prevUseDev = $_SESSION['preview']['use_devices'] ?? '[]';
$prevExtras = $_SESSION['preview']['extra_fees'] ?? '[]';
?>
(function(){
const devSessions = {
'info_list': <?= json_encode(json_decode($prevInfoDev, true) ?: []) ?>,
'router_list': <?= json_encode(json_decode($prevRouterDev, true) ?: []) ?>,
'use_list': <?= json_encode(json_decode($prevUseDev, true) ?: []) ?>,
'extras': <?= json_encode(json_decode($prevExtras, true) ?: []) ?>,
};
// 強制覆蓋 SS
Object.entries(devSessions).forEach(([k, v]) => SS.set(k, v));
// ── 資訊箱設備還原 ──
document.querySelectorAll('#info-container .info-group').forEach(g=>g.remove());
devSessions['info_list'].forEach(item => {
addInfoGroup();
const allG = document.querySelectorAll('#info-container .info-group');
const g = allG[allG.length-1]; if(!g) return;
const sel = g.querySelector('select.dev-select');
if(sel){ sel.value = item.device; onInfoChange(sel); }
const qtySel = g.querySelector('select.info-qty-sel');
if(qtySel) qtySel.value = item.qty || '1';
});
// ── 網路設備還原 ──
document.querySelectorAll('#router-container .router-group').forEach(g=>g.remove());
devSessions['router_list'].forEach(item => {
addRouterGroup();
const allG = document.querySelectorAll('#router-container .router-group');
const g = allG[allG.length-1]; if(!g) return;
const sel = g.querySelector('select.dev-select');
if(sel){ sel.value = item.device; onRouterChange(sel); }
const qtySel = g.querySelector('select.router-qty-sel');
if(qtySel) qtySel.value = item.qty || '1';
if(item.tvBrand){ const tb=g.querySelector('select.tv-brand-sel'); if(tb) tb.value=item.tvBrand; }
if(item.tvOther){ const to=g.querySelector('input.tv-other-inp'); if(to){ to.value=item.tvOther; to.style.display='inline-block'; } }
});
// ── 使用設備還原 ──
document.querySelectorAll('#use-container > .use-group').forEach(g=>g.remove());
updateAddUseBtn();
devSessions['use_list'].forEach(item => {
addUseGroup();
const allUse = document.querySelectorAll('#use-container > .use-group');
const g = allUse[allUse.length-1]; if(!g) return;
const idx = g.dataset.idx;
const purposeSel = document.getElementById('use-purpose-'+idx);
if(purposeSel && item.purpose){
purposeSel.value = item.purpose;
onUsePurposeChange(idx);
}
const deviceSel = document.getElementById('use-device-'+idx);
if(deviceSel && item.device){
deviceSel.value = item.device;
onUseDeviceChange(idx);
}
const sn = document.getElementById('use-sn-'+idx);
if(sn) sn.value = item.sn || '';
if(item.purpose === 'borrow'){
const dp = document.getElementById('use-dp-'+idx);
if(dp) dp.value = item.dp || '1000';
}
if(item.purpose === 'node'){
const np = document.getElementById('use-node-pos-'+idx);
if(np) np.value = item.nodePos || '';
}
});
calcDeposit();
// ── 加收項目還原 ──
document.querySelectorAll('#extra-container .extra-row').forEach(r=>r.remove());
devSessions['extras'].forEach(item => {
addExtraRow();
const rows = document.querySelectorAll('#extra-container .extra-row');
const row = rows[rows.length-1]; if(!row) return;
const idx = row.dataset.idx;
const sel = row.querySelector('.extra-select');
if(sel){ sel.value = item.type; onExtraTypeChange(sel, idx); }
const inp = row.querySelector('.extra-amount');
if(inp) inp.value = (item.amt != null) ? String(item.amt) : '';
if(item.sn){ const snInp=document.getElementById('extra-sn-inp-'+idx); if(snInp){ snInp.value=String(item.sn); snInp.style.display='inline-block'; } }
});
calcTotal();
})();
// 6. 贈送月份 — 從 PHP Session 注入
<?php
$sessionGifts = [];
if (!empty($_SESSION['preview']['gift_months'])) {
$sessionGifts = json_decode($_SESSION['preview']['gift_months'], true) ?: [];
}
?>
const fromPreviewGifts = <?= json_encode($sessionGifts) ?>;
if(fromPreviewGifts && fromPreviewGifts.length > 0){
document.querySelectorAll('#gift-container .gift-group').forEach(g => g.remove());
const gc = document.getElementById('gift-container');
if(!gc.querySelector('.gift-group')){
gc.innerHTML = '<div class="gift-empty" style="padding:12px 14px;font-size:13px;color:var(--color-text-tert,#999)">目前無贈送月份</div>';
}
fromPreviewGifts.forEach(function(item){
restoreGiftGroup(item.reason, item.months, item.intro || '', item.network || '');
});
rebuildGiftOptions();
SS.set('gifts', fromPreviewGifts);
}
// 7. 費率再次確認(from_preview 時 restoreFromSS 可能已還原,這裡再補一次確保正確)
if(previewData.plan_type){
const card = document.querySelector('.plan-card[data-key="'+previewData.plan_type+'"]');
if(card && selectedPlanType !== previewData.plan_type){
document.querySelectorAll('.plan-card').forEach(c=>c.classList.remove('selected'));
card.classList.add('selected');
selectedPlanType = previewData.plan_type;
selectedPlanPrice = previewData.plan_price || (parseInt(card.dataset.price)||0);
}
const clearWrap2 = document.getElementById('plan-clear-wrap');
if(clearWrap2 && selectedPlanType) clearWrap2.style.display = 'block';
}
// 8. 最後重新計算,確保日期、金額同步,然後存入 SS
calcDates();
calcTotal();
// 手動存入 plan_type/plan_price,避免 saveAllToSS 因時序問題存入空值
SS.set('plan_type', selectedPlanType);
SS.set('plan_price', selectedPlanPrice);
saveAllToSS();
console.log('[from_preview] 還原完成,plan_type=', selectedPlanType);
})();
<?php endif; ?>
</script>
</body>
</html>