File: /var/www/test/Installationlist/repair.php
<?php
// ============================================================
// repair.php ─ 電子派工維修單
// ============================================================
ob_start();
session_start();
// ── 第一次帶參數進來,存 Session 後導向乾淨 URL ─────────────
if (isset($_GET['user_id'])) {
$fields = ['user_id','member','user_name','memberaddress','email','homenumber',
'phone','pppoeid','pppoepw','ipaddress','basenumber','speed',
'homene','inforne','networkcm','question','ENname','APname',
'ENphone'];
foreach ($fields as $f) {
$_SESSION['rp_'.$f] = trim($_GET[$f] ?? '');
}
header('Location: repair.php');
exit;
}
// ── 從 Session 讀取 ─────────────────────────────────────────
$G = [];
$fields = ['user_id','member','user_name','memberaddress','email','homenumber',
'phone','pppoeid','pppoepw','ipaddress','basenumber','speed',
'homene','inforne','networkcm','question','ENname','APname',
'ENphone'];
foreach ($fields as $f) $G[$f] = $_SESSION['rp_'.$f] ?? '';
if (empty($G['user_id'])) {
die('<p style="color:red;font-size:18px;padding:20px">請透過正確連結開啟此頁面</p>');
}
$todayStr = (new DateTime())->format('Y-m-d');
$todayFmt = (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, maximum-scale=1.0, user-scalable=no">
<title>亞訊寬頻 電子派工維修單</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.19.0/dist/tabler-icons.min.css">
<style>
*,*::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:#F2F2F7;--gray-bd:rgba(0,0,0,.11);--gray-bd2:rgba(0,0,0,.22);
--text:#111;--text-sec:#555;--text-tert:#999;
--radius-sm:8px;--radius-md:10px;--radius-lg:14px;
}
body{font-family:"Microsoft JhengHei","PingFang TC",sans-serif;font-size:15px;background:var(--gray-bg);color:var(--text);padding-bottom:100px}
/* topbar */
.topbar{position:sticky;top:0;z-index:200;background:#fff;border-bottom:0.5px solid var(--gray-bd);padding:10px 16px;display:flex;align-items:center;gap:10px}
.topbar-logo{font-size:17px;font-weight:700;color:var(--green-deep);letter-spacing:.5px}
.topbar-title{flex:1;text-align:center;font-size:15px;font-weight:500}
.topbar-date{font-size:11px;color:var(--text-sec);text-align:right;line-height:1.5}
/* content */
.content{padding:12px 14px;display:flex;flex-direction:column;gap:10px}
/* card */
.card{background:#fff;border-radius:var(--radius-lg);border:0.5px solid var(--gray-bd);overflow:hidden}
.card-hdr{display:flex;align-items:center;gap:8px;padding:9px 14px;border-bottom:0.5px solid var(--gray-bd);background:var(--gray-bg)}
.card-hdr i{font-size:15px;color:var(--green)}
.card-hdr-txt{font-size:13px;font-weight:600;flex:1}
.card-hdr-badge{font-size:11px;padding:2px 8px;border-radius:20px;background:var(--green-lt);color:var(--green-deep)}
/* info rows */
.irow{display:flex;align-items:stretch;border-bottom:0.5px solid var(--gray-bd)}
.irow:last-child{border-bottom:none}
.ilbl{width:78px;flex-shrink:0;font-size:11.5px;color:var(--text-sec);padding:8px 6px 8px 13px;background:var(--gray-bg);display:flex;align-items:center;justify-content:center;text-align:center;line-height:1.4}
.ival{flex:1;padding:8px 13px;font-size:13.5px;word-break:break-all;display:flex;align-items:center}
.ival.mono{font-family:monospace;font-size:12.5px;color:var(--text-sec)}
.ival.green{color:var(--green-dk);font-weight:500}
.ival.pre{white-space:pre-wrap;line-height:1.7;align-items:flex-start;padding-top:10px;padding-bottom:10px}
.ival.empty{color:var(--text-tert);font-style:italic}
/* chip tags */
.chips{display:flex;flex-wrap:wrap;gap:4px;padding:8px 13px}
.chip{display:inline-flex;align-items:center;gap:3px;font-size:12px;background:var(--green-lt);color:var(--green-deep);padding:3px 10px;border-radius:20px}
.chip.gray{background:var(--gray-bg);color:var(--text-sec)}
/* 工程師填寫區 */
.eng-textarea{
width:100%;min-height:120px;font-size:14px;line-height:1.8;
padding:11px 13px;border:none;outline:none;resize:vertical;
font-family:inherit;color:var(--text);background:#fff;
border-top:0.5px solid var(--gray-bd)
}
.eng-textarea:focus{background:#FFFDF9}
.char-count{font-size:11px;color:var(--text-tert);text-align:right;padding:4px 13px 8px}
/* 使用設備:多層選單卡片 */
.device-card{border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);margin:8px 13px;overflow:hidden;background:#fafafa}
.device-row{display:flex;align-items:center;gap:7px;padding:8px 10px;border-bottom:0.5px solid var(--gray-bd)}
.device-row:last-child{border-bottom:none}
.device-row.sub{background:#fff}
.dev-sel{flex:1 1 0;min-width:0;font-size:13px;padding:7px 6px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-sm);background:#fff;color:var(--text);font-family:inherit}
.sn-wrap{display:flex;align-items:center;gap:5px;flex:1 1 0;min-width:0}
.sn-inp{flex:1;min-width:0;font-size:13px;padding:7px 8px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-sm);background:#fff;color:var(--text);font-family:monospace}
.basenum-inp{flex:1 1 0;min-width:0;font-size:13px;padding:7px 8px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-sm);background:#fff;color:var(--text);font-family:monospace}
.rm-btn{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)}
.add-row-btn{display:flex;align-items:center;gap:6px;padding:9px 13px;font-size:13px;color:var(--green-dk);cursor:pointer;border-top:0.5px solid var(--gray-bd);background:none;border-left:none;border-right:none;border-bottom:none;width:100%;font-family:inherit}
/* 收費 */
.fee-toggle{display:flex;gap:0;padding:10px 13px}
.fee-btn{flex:1;padding:9px;text-align:center;font-size:14px;border:0.5px solid var(--gray-bd2);cursor:pointer;font-family:inherit;transition:all .15s;background:#fff;color:var(--text-sec)}
.fee-btn:first-child{border-radius:var(--radius-sm) 0 0 var(--radius-sm)}
.fee-btn:last-child{border-radius:0 var(--radius-sm) var(--radius-sm) 0;border-left:none}
.fee-btn.sel-free{background:var(--green-lt);color:var(--green-dk);font-weight:500;border-color:var(--green)}
.fee-btn.sel-paid{background:#FFF3F3;color:var(--red);font-weight:500;border-color:var(--red)}
.fee-item-row{display:flex;align-items:center;gap:7px;padding:8px 13px;border-bottom:0.5px solid var(--gray-bd)}
.fee-item-row:last-child{border-bottom:none}
.fee-type-sel{flex:1;font-size:13px;padding:7px 8px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-sm);background:#fff;color:var(--text);font-family:inherit}
.fee-amt-inp{width:90px;font-size:14px;padding:7px 8px;text-align:right;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-sm);background:#fff;color:var(--text);font-family:inherit;flex-shrink:0}
.fee-total-row{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;border-top:0.5px solid var(--gray-bd)}
.fee-total-lbl{font-size:13px;color:var(--text-sec)}
.fee-total-amt{font-size:18px;font-weight:600;color:var(--red)}
.deposit-row{display:flex;justify-content:space-between;align-items:center;padding:8px 14px;border-top:0.5px solid var(--gray-bd)}
.deposit-lbl{font-size:13px;color:var(--text-sec)}
.deposit-amt{font-size:15px;font-weight:500;color:var(--amber-txt)}
/* 確認勾選 */
.chk-item{display:flex;align-items:center;gap:10px;padding:11px 13px;border-bottom:0.5px solid var(--gray-bd);cursor:pointer;user-select:none}
.chk-item:last-child{border-bottom:none}
.chk-box{width:24px;height:24px;border-radius:6px;border:1.5px solid var(--gray-bd2);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s;color:transparent}
.chk-box.on{background:var(--green);border-color:var(--green);color:#fff}
.chk-txt{font-size:14px}
.chk-sub{font-size:11px;color:var(--text-sec);margin-top:2px}
/* 簽名 */
.sig-trigger{border:1.5px dashed var(--gray-bd2);border-radius:var(--radius-md);background:var(--gray-bg);height:76px;display:flex;align-items:center;justify-content:center;cursor:pointer;gap:8px;color:var(--text-sec);font-size:13px;margin:11px 13px}
#sig-preview-wrap,#esig-preview-wrap{margin:11px 13px;display:none}
.sig-preview-img{width:100%;border:0.5px solid var(--gray-bd);border-radius:var(--radius-md);background:#fff;display:block}
.sig-rebtn{width:100%;margin-top:6px;padding:7px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-md);background:none;cursor:pointer;font-size:12px;color:var(--text-sec);font-family:inherit}
/* 簽名 Modal */
.sig-modal{display:none;position:fixed;inset:0;z-index:9999;background:#fff;flex-direction:column}
.sig-modal-bar{display:flex;align-items:center;justify-content:space-between;padding:11px 16px;border-bottom:0.5px solid var(--gray-bd);flex-shrink:0}
.sig-modal-title{font-size:16px;font-weight:500}
.sig-clr-btn{padding:7px 14px;border:0.5px solid var(--gray-bd2);border-radius:var(--radius-sm);background:none;font-size:13px;cursor:pointer;color:var(--text-sec);font-family:inherit}
.sig-ok-btn{padding:7px 18px;background:var(--green);color:#fff;border:none;border-radius:var(--radius-sm);font-size:14px;font-weight:500;cursor:pointer;font-family:inherit}
.sig-modal-area{flex:1;position:relative;overflow:hidden;background:#fafafa}
.sig-canvas-el{position:absolute;inset:0;touch-action:none;cursor:crosshair;display:block}
.sig-hint{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;pointer-events:none;color:var(--text-tert)}
.sig-modal-tip{padding:7px 16px;text-align:center;font-size:11px;color:var(--text-tert);border-top:0.5px solid var(--gray-bd);flex-shrink:0}
/* 底部 */
.bottom-bar{position:sticky;bottom:0;background:#fff;border-top:0.5px solid var(--gray-bd);padding:11px 14px}
.submit-btn{width:100%;padding:14px;background:var(--green);color:#fff;border:none;border-radius:var(--radius-md);font-size:15px;font-weight:500;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:6px}
.submit-btn:disabled{opacity:.35;cursor:not-allowed}
/* 遮罩 */
#submitting-overlay{display:none;position:fixed;inset:0;background:rgba(255,255,255,.96);z-index:9998;flex-direction:column;align-items:center;justify-content:center;gap:16px}
</style>
</head>
<body>
<div class="topbar">
<div class="topbar-logo">YSNET</div>
<div class="topbar-title">電子派工維修單</div>
<div class="topbar-date"><?= $todayFmt ?></div>
</div>
<div class="content">
<!-- ── 用戶基本資料 ── -->
<div class="card">
<div class="card-hdr"><i class="ti ti-user"></i><span class="card-hdr-txt">用戶基本資料</span></div>
<div class="irow"><div class="ilbl">用戶編號</div><div class="ival mono"><?= htmlspecialchars($G['user_id']) ?></div></div>
<div class="irow"><div class="ilbl">姓名</div><div class="ival"><?= htmlspecialchars($G['user_name']) ?></div></div>
<div class="irow"><div class="ilbl">居住社區</div><div class="ival"><?= htmlspecialchars($G['member']) ?></div></div>
<div class="irow"><div class="ilbl">裝機地址</div><div class="ival"><?= htmlspecialchars($G['memberaddress']) ?: '<span class="ival empty">—</span>' ?></div></div>
<div class="irow"><div class="ilbl">住家電話</div><div class="ival"><?= htmlspecialchars($G['homenumber']) ?: '<span class="ival empty">—</span>' ?></div></div>
<div class="irow"><div class="ilbl">行動電話</div><div class="ival"><?= htmlspecialchars($G['phone']) ?></div></div>
<div class="irow"><div class="ilbl">電子郵件</div><div class="ival" style="font-size:12.5px"><?= htmlspecialchars($G['email']) ?></div></div>
</div>
<!-- ── 網路資訊 ── -->
<?php $isStaticIP = !empty($G['ipaddress']); ?>
<div class="card">
<div class="card-hdr">
<i class="ti ti-wifi"></i>
<span class="card-hdr-txt">網路資訊</span>
<span style="margin-left:auto;font-size:11px;padding:2px 8px;border-radius:20px;
background:<?= $isStaticIP ? '#EEF2FF' : 'var(--green-lt)' ?>;
color:<?= $isStaticIP ? '#3730A3' : 'var(--green-deep)' ?>">
<?= $isStaticIP ? '靜態 IP 上網' : 'PPPoE 撥接上網' ?>
</span>
</div>
<?php if ($isStaticIP): ?>
<!-- 靜態 IP:顯示 IP 位址,不顯示 PPPoE -->
<div class="irow">
<div class="ilbl">IP 位址</div>
<div class="ival mono" style="font-size:15px;font-weight:500;color:#3730A3"><?= htmlspecialchars($G['ipaddress']) ?></div>
</div>
<?php else: ?>
<!-- PPPoE:顯示帳號密碼,不顯示 IP -->
<div class="irow">
<div class="ilbl">PPPoE 帳號</div>
<div class="ival mono"><?= htmlspecialchars($G['pppoeid']) ?></div>
</div>
<div class="irow">
<div class="ilbl">PPPoE 密碼</div>
<div class="ival mono"><?= htmlspecialchars($G['pppoepw']) ?></div>
</div>
<?php endif; ?>
<div class="irow"><div class="ilbl">據點編號</div><div class="ival mono"><?= htmlspecialchars($G['basenumber']) ?></div></div>
<div class="irow"><div class="ilbl">使用方案</div><div class="ival green"><?= htmlspecialchars($G['speed']) ?></div></div>
</div>
<!-- ── 設備資訊 ── -->
<div class="card">
<div class="card-hdr"><i class="ti ti-server"></i><span class="card-hdr-txt">設備資訊</span></div>
<div class="irow">
<div class="ilbl">家中設備</div>
<div class="ival"><?= $G['homene'] ? htmlspecialchars($G['homene']) : '<span style="color:var(--text-tert)">—</span>' ?></div>
</div>
<div class="irow">
<div class="ilbl">資訊箱設備</div>
<div class="ival"><?= $G['inforne'] ? htmlspecialchars($G['inforne']) : '<span style="color:var(--text-tert)">—</span>' ?></div>
</div>
<div class="irow">
<div class="ilbl">施工方式</div>
<div class="ival"><?= $G['networkcm'] ? htmlspecialchars($G['networkcm']) : '<span style="color:var(--text-tert)">—</span>' ?></div>
</div>
</div>
<!-- ── 問題狀況描述 ── -->
<div class="card">
<div class="card-hdr"><i class="ti ti-alert-circle"></i><span class="card-hdr-txt">問題狀況描述</span></div>
<div class="ival pre" style="padding:11px 13px;min-height:60px;font-size:14px"><?php
echo $G['question'] ? htmlspecialchars($G['question']) : '<span style="color:var(--text-tert);font-style:italic">(無描述)</span>';
?></div>
</div>
<!-- ── 工程師維修概況紀錄 ── -->
<div class="card">
<div class="card-hdr">
<i class="ti ti-pencil"></i>
<span class="card-hdr-txt">維修概況紀錄</span>
<span style="color:var(--red);font-size:12px;margin-left:4px">*必填</span>
</div>
<textarea id="repair-content" class="eng-textarea"
placeholder="請填寫本次維修的處理內容、更換零件、調整項目、測試結果…"
maxlength="1000" oninput="onRepairInput()"></textarea>
<div class="char-count"><span id="char-num">0</span> / 1000</div>
</div>
<!-- ── 使用設備 ── -->
<div class="card">
<div class="card-hdr"><i class="ti ti-package"></i><span class="card-hdr-txt">使用設備</span></div>
<div id="device-container"></div>
<button type="button" class="add-row-btn" onclick="addDeviceEntry()">
<i class="ti ti-plus" style="font-size:14px"></i> 新增設備紀錄
</button>
</div>
<!-- ── 收費紀錄 ── -->
<div class="card">
<div class="card-hdr"><i class="ti ti-receipt"></i><span class="card-hdr-txt">收費紀錄</span></div>
<div class="fee-toggle">
<button type="button" class="fee-btn" id="btn-free" onclick="setFeeMode('free')">本次無收費</button>
<button type="button" class="fee-btn" id="btn-paid" onclick="setFeeMode('paid')">本次收費</button>
</div>
<div id="fee-section" style="display:none">
<div id="fee-container"></div>
<button type="button" class="add-row-btn" onclick="addFeeRow()">
<i class="ti ti-plus" style="font-size:14px"></i> 新增收費項目
</button>
<div class="fee-total-row">
<span class="fee-total-lbl">本次收費總計</span>
<span class="fee-total-amt" id="fee-total">$0</span>
</div>
<div class="deposit-row" id="deposit-row" style="display:none">
<span class="deposit-lbl">押金</span>
<span class="deposit-amt" id="deposit-total">$0</span>
</div>
</div>
</div>
<!-- ── 確認事項 ── -->
<div class="card">
<div class="card-hdr"><i class="ti ti-list-check"></i><span class="card-hdr-txt">確認事項</span></div>
<div class="chk-item" onclick="toggleChk(this)">
<div class="chk-box"><i class="ti ti-check" style="font-size:14px"></i></div>
<div><div class="chk-txt">維修概況紀錄已填寫完整</div><div class="chk-sub">本次處理項目無誤</div></div>
</div>
<div class="chk-item" onclick="toggleChk(this)">
<div class="chk-box"><i class="ti ti-check" style="font-size:14px"></i></div>
<div><div class="chk-txt">已向客戶說明維修結果及費用</div><div class="chk-sub">客戶已了解本次服務內容</div></div>
</div>
<div class="chk-item" onclick="toggleChk(this)">
<div class="chk-box"><i class="ti ti-check" style="font-size:14px"></i></div>
<div><div class="chk-txt">網路連線已確認正常</div><div class="chk-sub">現場測試完成</div></div>
</div>
</div>
<!-- ── 客戶簽名 ── -->
<div class="card">
<div class="card-hdr"><i class="ti ti-signature"></i><span class="card-hdr-txt">客戶簽名</span>
<span id="csig-badge" style="margin-left:auto;font-size:11px;padding:2px 8px;border-radius:20px;background:var(--amber);color:var(--amber-txt)">未簽名</span>
</div>
<div id="sig-trigger" onclick="openSigModal('c')" class="sig-trigger">
<i class="ti ti-pencil" style="font-size:20px;color:var(--green)"></i>
點此開啟客戶簽名板
</div>
<div id="sig-preview-wrap">
<img id="sig-preview-img" class="sig-preview-img">
<button class="sig-rebtn" onclick="openSigModal('c')"><i class="ti ti-pencil" style="font-size:12px"></i> 重新簽名</button>
</div>
</div>
<!-- ── 工程師確認 ── -->
<div class="card">
<div class="card-hdr"><i class="ti ti-user-check"></i><span class="card-hdr-txt">工程師確認</span></div>
<div class="chk-item" id="eng-chk-item" onclick="toggleEngChk()">
<div class="chk-box" id="eng-chk-box"><i class="ti ti-check" style="font-size:14px"></i></div>
<div>
<div class="chk-txt">本表單資料確認無誤,維修工作已完成</div>
</div>
</div>
</div>
</div><!-- /content -->
<!-- 送出遮罩 -->
<div id="submitting-overlay" style="display:none;position:fixed;inset:0;background:rgba(255,255,255,.96);z-index:9998;flex-direction:column;align-items:center;justify-content:center;gap:16px">
<div style="width:52px;height:52px;border-radius:50%;background:var(--green-lt);display:flex;align-items:center;justify-content:center">
<i class="ti ti-loader" style="font-size:26px;color:var(--green)"></i>
</div>
<div style="font-size:16px;font-weight:500;color:var(--green-deep)">正在送出維修單…</div>
<div style="font-size:13px;color:var(--text-sec)">正在產生 PDF 並寄送 Email</div>
</div>
<!-- Email 確認 Modal -->
<div id="email-modal" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.45);align-items:center;justify-content:center;padding:20px">
<div style="background:#fff;border-radius:var(--radius-lg);width:100%;max-width:380px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,.18)">
<div style="padding:14px 16px;border-bottom:0.5px solid var(--gray-bd);display:flex;align-items:center;gap:8px">
<i class="ti ti-mail" style="font-size:18px;color:var(--green)"></i>
<span style="font-size:15px;font-weight:500">確認寄送 Email</span>
</div>
<div style="padding:14px 16px">
<div style="font-size:13px;color:var(--text-sec);margin-bottom:10px">PDF 維修單將寄送至以下 Email,如需更改請直接修改:</div>
<input id="email-confirm-inp" type="email" inputmode="email"
style="width:100%;font-size:14px;padding:10px 12px;border:1.5px solid var(--gray-bd2);border-radius:var(--radius-md);font-family:inherit;outline:none;color:var(--text)"
placeholder="輸入 Email,留空則不寄送"
onfocus="this.style.borderColor='var(--green)'"
onblur="this.style.borderColor='var(--gray-bd2)'">
<div id="email-hint-txt" style="font-size:11px;color:var(--text-tert);margin-top:5px">留空則不寄送 Email</div>
</div>
<div style="display:flex;gap:0;border-top:0.5px solid var(--gray-bd)">
<button onclick="closeEmailModal()" style="flex:1;padding:13px;background:none;border:none;border-right:0.5px solid var(--gray-bd);font-size:14px;cursor:pointer;color:var(--text-sec);font-family:inherit">取消</button>
<button onclick="confirmEmailAndSubmit()" style="flex:1;padding:13px;background:var(--green);border:none;font-size:14px;font-weight:500;cursor:pointer;color:#fff;font-family:inherit">確認送出</button>
</div>
</div>
</div>
<!-- 客戶簽名 Modal -->
<div id="csig-modal" class="sig-modal">
<div class="sig-modal-bar">
<span class="sig-modal-title">客戶簽名</span>
<div style="display:flex;gap:10px">
<button class="sig-clr-btn" onclick="clearSig('c')"><i class="ti ti-eraser" style="font-size:12px"></i> 清除</button>
<button class="sig-ok-btn" onclick="confirmSig('c')"><i class="ti ti-check" style="font-size:12px"></i> 確認</button>
</div>
</div>
<div class="sig-modal-area"><canvas id="csig-canvas" class="sig-canvas-el"></canvas><div id="csig-hint" class="sig-hint"><i class="ti ti-pencil" style="font-size:36px"></i><span>請在此區域簽名</span></div></div>
<div class="sig-modal-tip">建議橫向握持手機簽名</div>
</div>
<!-- 底部 -->
<div class="bottom-bar">
<button class="submit-btn" id="submit-btn" disabled onclick="doSubmit()">
<i class="ti ti-send" style="font-size:16px"></i>
<span id="submit-label">請完成所有必填項目</span>
</button>
</div>
<script>
// ── PHP 資料注入 ──────────────────────
const G = <?= json_encode($G) ?>;
const TODAY = <?= json_encode($todayStr) ?>;
// ── 維修紀錄字數 ─────────────────────
function onRepairInput() {
document.getElementById('char-num').textContent = document.getElementById('repair-content').value.length;
const repairOk = document.getElementById('repair-content').value.trim().length > 0;
if (!repairOk) {
// 維修概況紀錄被清空時,連動取消已勾選的確認事項與工程師確認,避免邏輯矛盾
document.querySelectorAll('.chk-item:not(#eng-chk-item) .chk-box').forEach(b=>b.classList.remove('on'));
if (engConfirmed) {
engConfirmed = false;
document.getElementById('eng-chk-box').classList.remove('on');
}
}
updateState();
}
// ── 確認勾選 ─────────────────────────
function toggleChk(row) {
const repairOk = document.getElementById('repair-content').value.trim().length > 0;
if (!repairOk) {
alert('請先填寫維修概況紀錄');
return;
}
row.querySelector('.chk-box').classList.toggle('on');
updateState();
}
// ── 使用設備:多層選單 ────────────────
// 新增/回收 → 住戶端/據點 → 對應設備清單
const deviceUserOptions = ['光貓','光轉B','1000M無線分享器','100M無線分享器'];
const deviceBaseOptionsNew = ['1000M集線器','光轉A','OLT','光分集器','主機'];
const deviceBaseOptionsRecycle = ['1000M集線器','100M集線器','光轉A','OLT','光分集器','主機'];
let devIdx = 0;
function addDeviceEntry() {
const c = document.getElementById('device-container');
const idx = devIdx++;
const div = document.createElement('div');
div.className = 'device-card'; div.dataset.idx = idx;
div.innerHTML = `
<div class="device-row">
<select class="dev-sel dev-action" onchange="onDeviceActionChange(${idx})">
<option value="">請選擇</option>
<option value="新增">新增</option>
<option value="回收">回收</option>
</select>
<select class="dev-sel dev-loc" style="display:none" onchange="onDeviceLocChange(${idx})">
<option value="">請選擇地點</option>
<option value="住戶端">住戶端</option>
<option value="據點">據點</option>
</select>
<div class="sn-wrap dev-basenum-wrap" style="display:none">
<input class="basenum-inp dev-basenum" type="text" placeholder="據點編號" maxlength="20"
inputmode="text" oninput="updateState()">
</div>
<button type="button" class="rm-btn" onclick="this.closest('.device-card').remove();updateState()">
<i class="ti ti-x" style="font-size:13px"></i>
</button>
</div>
<div id="dev-detail-${idx}"></div>
`;
c.appendChild(div);
}
function onDeviceActionChange(idx) {
const card = document.querySelector(`.device-card[data-idx="${idx}"]`);
const action = card.querySelector('.dev-action').value;
const locSel = card.querySelector('.dev-loc');
const basenumWrap = card.querySelector('.dev-basenum-wrap');
const detail = document.getElementById(`dev-detail-${idx}`);
if (!action) {
locSel.style.display = 'none';
locSel.value = '';
basenumWrap.style.display = 'none';
detail.innerHTML = '';
updateState();
return;
}
locSel.style.display = '';
// 若已選過據點,依新的新增/回收狀態重新產生設備清單
if (locSel.value === '據點') onDeviceLocChange(idx);
updateState();
}
function onDeviceLocChange(idx) {
const card = document.querySelector(`.device-card[data-idx="${idx}"]`);
const action = card.querySelector('.dev-action').value;
const loc = card.querySelector('.dev-loc').value;
const detail = document.getElementById(`dev-detail-${idx}`);
const basenumWrap = card.querySelector('.dev-basenum-wrap');
detail.innerHTML = '';
basenumWrap.style.display = (loc === '據點') ? 'flex' : 'none';
if (!loc) { updateState(); return; }
if (loc === '住戶端') {
detail.innerHTML = `
<div class="device-row sub">
<select class="dev-sel dev-type">
${deviceUserOptions.map(o=>`<option>${o}</option>`).join('')}
</select>
<div class="sn-wrap">
<input class="sn-inp dev-sn" type="text" placeholder="設備料號" maxlength="20"
inputmode="text" oninput="updateState()">
</div>
</div>`;
} else if (loc === '據點') {
const baseOptions = (action === '回收') ? deviceBaseOptionsRecycle : deviceBaseOptionsNew;
detail.innerHTML = `
<div class="device-row sub">
<select class="dev-sel dev-type">
${baseOptions.map(o=>`<option>${o}</option>`).join('')}
</select>
<div class="sn-wrap">
<input class="sn-inp dev-sn" type="text" placeholder="設備料號" maxlength="20"
inputmode="text" oninput="updateState()">
</div>
</div>`;
}
updateState();
}
// ── 收費 ─────────────────────────────
let feeMode = null; // 'free' | 'paid'
let feeIdx = 0;
const feeTypes = ['維修費','周邊設備','裝機費','移機費','押金','傳輸費'];
function setFeeMode(mode) {
feeMode = mode;
document.getElementById('btn-free').className = 'fee-btn' + (mode==='free'?' sel-free':'');
document.getElementById('btn-paid').className = 'fee-btn' + (mode==='paid'?' sel-paid':'');
const sec = document.getElementById('fee-section');
sec.style.display = mode==='paid' ? 'block' : 'none';
if(mode==='paid' && document.querySelectorAll('.fee-item-row').length===0) addFeeRow();
updateState();
}
function addFeeRow() {
const c = document.getElementById('fee-container');
const idx = feeIdx++;
const div = document.createElement('div');
div.className = 'fee-item-row'; div.dataset.idx = idx;
div.innerHTML = `
<select class="fee-type-sel" onchange="calcFees()">
${feeTypes.map(t=>`<option>${t}</option>`).join('')}
</select>
<input class="fee-amt-inp" type="text" placeholder="金額" inputmode="numeric" maxlength="6"
oninput="this.value=this.value.replace(/\\D/g,'');calcFees()">
<button type="button" class="rm-btn" onclick="this.closest('.fee-item-row').remove();calcFees()">
<i class="ti ti-x" style="font-size:13px"></i>
</button>`;
c.appendChild(div);
}
function calcFees() {
let total = 0, deposit = 0;
document.querySelectorAll('.fee-item-row').forEach(row => {
const type = row.querySelector('.fee-type-sel').value;
const amt = parseInt(row.querySelector('.fee-amt-inp').value) || 0;
if(type === '押金') deposit += amt;
else total += amt;
});
document.getElementById('fee-total').textContent = '$' + total.toLocaleString();
const dr = document.getElementById('deposit-row');
dr.style.display = deposit > 0 ? 'flex' : 'none';
document.getElementById('deposit-total').textContent = '$' + deposit.toLocaleString();
updateState();
}
// ── 簽名(僅客戶)───────────────────
let sigData = { c: null };
let sigStrokes = { c: false };
let drawing = false, currentSigKey = null;
function sigEl(k, sel) { return document.getElementById(k+'sig-'+sel); }
function initCanvas(k) {
const modal = document.getElementById(k+'sig-modal');
const area = modal.querySelector('.sig-modal-area');
const cv = sigEl(k,'canvas');
cv.width = area.offsetWidth || window.innerWidth;
cv.height = area.offsetHeight || (window.innerHeight - 110);
const ctx = cv.getContext('2d');
ctx.strokeStyle='#111'; ctx.lineWidth=2.5; ctx.lineCap='round'; ctx.lineJoin='round';
}
function openSigModal(k) {
const modal = document.getElementById(k+'sig-modal');
modal.style.display='flex';
document.body.style.overflow='hidden';
currentSigKey = k;
setTimeout(()=>{
initCanvas(k);
sigStrokes[k]=false;
const cv = sigEl(k,'canvas');
cv.getContext('2d').clearRect(0,0,cv.width,cv.height);
sigEl(k,'hint').style.display='flex';
},60);
}
function getPos(e, cv) {
const r=cv.getBoundingClientRect(), s=e.touches?e.touches[0]:e;
return { x:(s.clientX-r.left)*(cv.width/r.width), y:(s.clientY-r.top)*(cv.height/r.height) };
}
document.addEventListener('mousedown', startDraw);
document.addEventListener('touchstart', startDraw, {passive:false});
function startDraw(e) {
const k = currentSigKey; if(!k) return;
const modal = document.getElementById(k+'sig-modal');
if(modal.style.display!=='flex') return;
const cv = sigEl(k,'canvas');
if(!cv.contains(e.target) && e.target!==cv) return;
e.preventDefault(); drawing=true;
const p=getPos(e,cv), ctx=cv.getContext('2d');
ctx.beginPath(); ctx.moveTo(p.x,p.y);
sigEl(k,'hint').style.display='none';
document.addEventListener('mousemove', onDraw);
document.addEventListener('mouseup', stopDraw);
document.addEventListener('touchmove', onDraw, {passive:false});
document.addEventListener('touchend', stopDraw);
}
function onDraw(e) {
if(!drawing||!currentSigKey) return; e.preventDefault();
const cv=sigEl(currentSigKey,'canvas'), ctx=cv.getContext('2d');
const p=getPos(e,cv); ctx.lineTo(p.x,p.y); ctx.stroke();
sigStrokes[currentSigKey]=true;
}
function stopDraw() {
drawing=false;
document.removeEventListener('mousemove',onDraw); document.removeEventListener('mouseup',stopDraw);
document.removeEventListener('touchmove',onDraw); document.removeEventListener('touchend',stopDraw);
}
function clearSig(k) {
const cv=sigEl(k,'canvas'); cv.getContext('2d').clearRect(0,0,cv.width,cv.height);
sigStrokes[k]=false; sigEl(k,'hint').style.display='flex';
}
function confirmSig(k) {
if(!sigStrokes[k]){ alert('請先在方框內簽名'); return; }
sigData[k] = sigEl(k,'canvas').toDataURL('image/png');
document.getElementById(k+'sig-modal').style.display='none';
document.body.style.overflow=''; currentSigKey=null;
document.getElementById('sig-preview-img').src = sigData.c;
document.getElementById('sig-preview-wrap').style.display='block';
document.getElementById('sig-trigger').style.display='none';
const b=document.getElementById('csig-badge');
b.textContent='已簽名'; b.style.background='var(--green-lt)'; b.style.color='var(--green-deep)';
updateState();
}
window.addEventListener('orientationchange',()=>{
if(currentSigKey){ setTimeout(()=>initCanvas(currentSigKey),300); }
});
// ── 工程師確認勾選 ───────────────────
let engConfirmed = false;
function toggleEngChk() {
// 取消勾選不受限制(讓工程師可以反悔重新確認)
if (engConfirmed) {
engConfirmed = false;
document.getElementById('eng-chk-box').classList.toggle('on', engConfirmed);
updateState();
return;
}
const repairOk = document.getElementById('repair-content').value.trim().length > 0;
const allChked = [...document.querySelectorAll('.chk-item:not(#eng-chk-item) .chk-box')].every(b=>b.classList.contains('on'));
const feeOk = feeMode !== null;
const sigOk = !!sigData.c;
if (!repairOk) { alert('請先填寫維修概況紀錄'); return; }
if (!allChked) { alert('請先完成上方確認事項勾選'); return; }
if (!feeOk) { alert('請先選擇收費狀態'); return; }
if (!sigOk) { alert('請先完成客戶簽名'); return; }
engConfirmed = true;
document.getElementById('eng-chk-box').classList.toggle('on', engConfirmed);
updateState();
}
// ── 狀態更新 ─────────────────────────
function updateState() {
const repairOk = document.getElementById('repair-content').value.trim().length > 0;
const allChked = [...document.querySelectorAll('.chk-item:not(#eng-chk-item) .chk-box')].every(b=>b.classList.contains('on'));
const feeOk = feeMode !== null;
const sigOk = !!sigData.c;
const allOk = repairOk && allChked && feeOk && sigOk && engConfirmed;
const btn = document.getElementById('submit-btn');
const lbl = document.getElementById('submit-label');
btn.disabled = !allOk;
if(allOk) lbl.textContent = '送出維修單';
else if(!repairOk) lbl.textContent = '請填寫維修概況紀錄';
else if(!allChked) lbl.textContent = '請完成確認勾選';
else if(!feeOk) lbl.textContent = '請選擇收費狀態';
else if(!sigOk) lbl.textContent = '請完成客戶簽名';
else lbl.textContent = '請工程師勾選確認表單';
}
// ── Email 確認 Modal ─────────────────
function doSubmit() {
if(!sigData.c){ alert('請完成客戶簽名'); return; }
if(!engConfirmed){ alert('請工程師勾選確認表單'); return; }
const repairContent = document.getElementById('repair-content').value.trim();
if(!repairContent){ alert('請填寫維修概況紀錄'); return; }
// 開啟 Email 確認 Modal,預填原始 email
const origEmail = G.email || '';
const inp = document.getElementById('email-confirm-inp');
inp.value = origEmail;
const hint = document.getElementById('email-hint-txt');
hint.textContent = origEmail ? '原始信箱:' + origEmail + '(可修改或清空)' : '未提供信箱,可手動填寫或留空不寄送';
document.getElementById('email-modal').style.display = 'flex';
}
function closeEmailModal() {
document.getElementById('email-modal').style.display = 'none';
}
function confirmEmailAndSubmit() {
const finalEmail = document.getElementById('email-confirm-inp').value.trim();
document.getElementById('email-modal').style.display = 'none';
doActualSubmit(finalEmail);
}
function doActualSubmit(finalEmail) {
document.getElementById('submitting-overlay').style.display='flex';
document.querySelector('.bottom-bar').style.display='none';
const repairContent = document.getElementById('repair-content').value.trim();
const devices=[];
document.querySelectorAll('.device-card').forEach(card=>{
const action = card.querySelector('.dev-action')?.value || '';
const loc = card.querySelector('.dev-loc')?.value || '';
const type = card.querySelector('.dev-type')?.value || '';
const sn = card.querySelector('.dev-sn')?.value.trim() || '';
const basenum= card.querySelector('.dev-basenum')?.value.trim() || '';
if(action && loc && type){
devices.push({action, loc, type, sn, basenum});
}
});
const fees=[]; let feeTotal=0;
if(feeMode==='paid'){
document.querySelectorAll('.fee-item-row').forEach(row=>{
const type=row.querySelector('.fee-type-sel').value;
const amt=parseInt(row.querySelector('.fee-amt-inp').value)||0;
fees.push({type,amt});
if(type !== '押金') feeTotal+=amt;
});
}
const fd = new FormData();
fd.append('user_id', G.user_id);
fd.append('enname', G.ENname);
fd.append('apname', G.APname);
fd.append('repair_content', repairContent);
fd.append('repair_date', TODAY);
fd.append('fee_mode', feeMode);
fd.append('fees_json', JSON.stringify(fees));
fd.append('fee_total', feeTotal);
fd.append('devices_json', JSON.stringify(devices));
fd.append('form_data', JSON.stringify(G));
fd.append('final_email', finalEmail); // 最終 email(可能是修改後的或空)
const arr=sigData.c.split(','),mime=arr[0].match(/:(.*?);/)[1],bstr=atob(arr[1]);
const ia=new Uint8Array(bstr.length); for(let i=0;i<bstr.length;i++) ia[i]=bstr.charCodeAt(i);
fd.append('csig', new Blob([ia],{type:mime}), 'csig.png');
fetch('repair_submit.php',{method:'POST',body:fd})
.then(r=>{ if(!r.ok) throw new Error('伺服器錯誤 '+r.status); return r.json(); })
.then(data=>{
if(data.success){ window.location.replace('repair_done.php'); }
else {
document.getElementById('submitting-overlay').style.display='none';
document.querySelector('.bottom-bar').style.display='';
alert('送出失敗:'+(data.message||'請聯繫客服'));
}
})
.catch(err=>{
document.getElementById('submitting-overlay').style.display='none';
document.querySelector('.bottom-bar').style.display='';
alert('網路錯誤:'+err.message);
});
}
</script>
</body>
</html>