function doGet() {
return HtmlService.createHtmlOutputFromFile('index').setTitle('存貨查詢');
}
function searchIPByKeyword(keyword) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('賣場總覽');
const data = sheet.getDataRange().getValues();
const matches = new Set();
for (let i = 1; i < data.length; i++) {
const ip = data[i][0]; // IP 在第1欄(根據你圖中「作品」欄)
if (ip && ip.includes(keyword)) {
matches.add(ip);
}
}
return [...matches].sort();
}
function getKeywordsByIP(ip) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('賣場總覽');
const data = sheet.getDataRange().getValues();
const set = new Set();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === ip) { // IP 在第1欄
const kw = data[i][1]; // Keyword 在第2欄(根據你圖中「販售關鍵字」欄)
if (kw) set.add(kw);
const rowKeywords = data[i].slice(1);
rowKeywords.forEach(k => {
if (k) set.add(k); // 過濾空值
});
break; // 找到就跳出
}
}
return [...set];
}
function searchOrdersByIPKeyword(ip, keyword) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('存貨表');
const data = sheet.getDataRange().getValues();
const orderMap = new Map();
// 新增:用來統計訂單編號對應了多少個不同的賣場名稱
const fullOrderMartMap = new Map();
// 第一輪:統計全表,偵測複合賣場
for (let i = 1; i < data.length; i++) {
const row = data[i];
const martName = row[1] + "_" + row[2]; // IP + Keyword 組成賣場名
const orderNo = row[5];
if (!fullOrderMartMap.has(orderNo)) {
fullOrderMartMap.set(orderNo, new Set());
}
fullOrderMartMap.get(orderNo).add(martName);
}
// 第二輪:執行原本的搜尋邏輯
for (let i = 1; i < data.length; i++) {
const row = data[i];
if (row[1] === ip && row[2] === keyword) {
const orderNo = row[5];
const name = row[3];
const note = row[4] || "";
const qty = row[7] || 0;
const tracking = row[10];
if (!orderMap.has(orderNo)) {
// 判斷該訂單號在全表中是否有超過 1 個賣場名稱
const isMultiple = fullOrderMartMap.get(orderNo).size > 1;
orderMap.set(orderNo, { tracking, items: [], isMultiple: isMultiple });
}
orderMap.get(orderNo).items.push(`▪️${name}/${note}*${qty}`);
}
}
const results = [];
for (const [orderNo, { tracking, items, isMultiple }] of orderMap.entries()) {
results.push({
orderNo: orderNo,
tracking: tracking,
items: items,
martName: "", // 保持您的需求:此處不顯示賣場標題
isMultiple: isMultiple // 傳回標記
});
}
return results;
}
function submitToSheet(dataArray) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('回台總資料');
const startRow = sheet.getLastRow() + 1;
// 1. 準備資料陣列,一次性寫入 Google Sheet (效率最高)
const values = dataArray.map(entry => [
entry.UF_num,
entry.ip,
entry.keyword,
entry.tracking,
entry.orderNo,
entry.items,
entry.note,
new Date(),
entry.isComplete,
entry.isMultipleReport // 欄 J: 複合賣場YN (新增欄位)
]);
// 一次性寫入所有列與 9 個欄位
sheet.getRange(startRow, 1, values.length, 10).setValues(values);
Logger.log("✅ Google Sheet 寫入成功");
}
/**
* 根據 IP 與 Keyword 從「產品重量表」抓取產品清單
*/
function getProductsByInfo(ip, keyword) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("產品重量表");
if (!sheet) return [];
const data = sheet.getDataRange().getValues();
const headers = data[0];
const idxIp = headers.indexOf("作品");
const idxKw = headers.indexOf("產品關鍵字");
const idxName = headers.indexOf("產品名稱");
const idxWeight = headers.indexOf("原始重量");
const products = [];
for (let i = 1; i < data.length; i++) {
if (data[i][idxIp] == ip && data[i][idxKw] == keyword) {
products.push({
name: data[i][idxName],
weight: data[i][idxWeight],
row: i + 1 // 記錄在 Google Sheet 的列號
});
}
}
return products;
}
/**
* 更新單個產品重量回 Google Sheet (稍後可手動同步回 OneDrive)
*/
// 💡 請確保函式名稱是 updateBatchWeightsToSheet
function updateBatchWeightsToSheet(updates) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("產品重量表");
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const idxWeight = headers.indexOf("原始重量") + 1;
if (idxWeight === 0) return "❌ 找不到 '原始重量' 欄位";
// updates 是前端傳來的一串清單 [ {row:2, weight:1.5}, {row:5, weight:2.3} ]
updates.forEach(function(item) {
// 這裡的 item.row 才是真正的行號 (int)
var rowNum = parseInt(item.row);
var weightVal = item.weight; // 這裡可以是 double
if (!isNaN(rowNum) && rowNum > 0) {
// ✅ 這裡才會正確執行
sheet.getRange(rowNum, idxWeight).setValue(weightVal);
}
});
return "✅ 重量儲存成功!";
} catch (e) {
// 這裡就是您看到錯誤訊息的地方
return "更新失敗: " + e.toString();
}
}
function searchOrdersByTracking(trackingNum) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("存貨表");
var data = sheet.getDataRange().getValues();
if (data.length < 2) return [];
const headers = data[0];
const idxOrderNum = headers.indexOf("訂單編號");
const idxTracking = headers.indexOf("跟蹤號碼");
const idxName = headers.indexOf("產品名稱");
const idxNote = headers.indexOf("產品備註");
const idxQty = headers.indexOf("個數");
const idxIP = headers.indexOf("IP");
const idxKey = headers.indexOf("產品關鍵字");
// --- 新增:用來統計訂單編號對應了多少個不同的賣場名稱 ---
const fullOrderMartMap = new Map();
for (let i = 1; i < data.length; i++) {
const row = data[i];
const orderNo = row[idxOrderNum];
const martName = row[idxIP] + "_" + row[idxKey];
if (!fullOrderMartMap.has(orderNo)) {
fullOrderMartMap.set(orderNo, new Set());
}
fullOrderMartMap.get(orderNo).add(martName);
}
const orderMap = new Map();
for (var i = 1; i < data.length; i++) {
var row = data[i];
var rowTracking = String(row[idxTracking]);
if (rowTracking.toLowerCase().indexOf(trackingNum.toLowerCase()) > -1) {
const martName = row[idxIP] + "_" + row[idxKey];
const orderNo = row[idxOrderNum];
const tracking = row[idxTracking];
const name = row[idxName];
const note = row[idxNote] || "";
const qty = row[idxQty] || 0;
if (!orderMap.has(orderNo)) {
// 偵測是否為複合賣場
const isMultiple = fullOrderMartMap.get(orderNo).size > 1;
orderMap.set(orderNo, {
martName: martName,
tracking: tracking,
items: [],
isMultiple: isMultiple // 存入偵測結果
});
}
orderMap.get(orderNo).items.push(`▪️${name}/${note}*${qty}`);
}
}
const results = [];
for (const [orderNo, dataObj] of orderMap.entries()) {
results.push({
martName: dataObj.martName,
orderNo: orderNo,
tracking: dataObj.tracking,
items: dataObj.items,
isMultiple: dataObj.isMultiple // 傳回前端
});
}
return results;
}
function getRelatedMarts(orderNo, currentIP, currentKeyword) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("存貨表");
const data = sheet.getDataRange().getValues();
const headers = data[0];
const idxOrderNum = headers.indexOf("訂單編號");
const idxIP = headers.indexOf("IP");
const idxKey = headers.indexOf("產品關鍵字");
const idxTracking = headers.indexOf("跟蹤號碼");
const idxName = headers.indexOf("產品名稱");
const idxNote = headers.indexOf("產品備註");
const idxQty = headers.indexOf("個數");
const results = [];
const currentMart = currentIP + "_" + currentKeyword;
for (let i = 1; i < data.length; i++) {
const row = data[i];
const rowOrderNo = String(row[idxOrderNum]);
const rowMart = row[idxIP] + "_" + row[idxKey];
// 抓取相同訂單編號,但「不是」目前正在處理的這個賣場
if (rowOrderNo === orderNo && rowMart !== currentMart) {
results.push({
martName: rowMart,
orderNo: rowOrderNo,
tracking: row[idxTracking],
items: [`▪️${row[idxName]}/${row[idxNote] || ""}*${row[idxQty] || 0}`]
});
}
}
// 合併相同賣場的產品項
const merged = [];
results.forEach(item => {
const existing = merged.find(m => m.martName === item.martName);
if (existing) {
existing.items.push(item.items[0]);
} else {
merged.push(item);
}
});
return merged;
}
/**
* 將偵測資料寫入「賣貨便list」,並檢查重複
*/
function recordToMHBList(rIP, rKey) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("賣貨便list");
if (!sheet) {
throw new Error("找不到『賣貨便list』工作表");
}
const fullKey = rIP + "_" + rKey; // 賣場總名:IP_關鍵字
// --- 檢查重複邏輯 ---
const lastRow = sheet.getLastRow();
if (lastRow > 1) {
// 取得 C 欄 (第 3 欄) 所有已存在的賣場總名
const existingNames = sheet.getRange(2, 3, lastRow - 1, 1).getValues().flat();
// 如果已存在,則直接回傳,不重複寫入
if (existingNames.includes(fullKey)) {
return "賣場已存在於偵測清單,跳過寫入。";
}
}
// --- 準備寫入資料 ---
const now = new Date();
const dateString = Utilities.formatDate(now, "GMT+8", "yyyy/MM/dd");
const rowData = [
rIP, // A欄: IP
rKey, // B欄: 關鍵字
fullKey, // C欄: 賣場總名
dateString, // D欄: 上傳日期
"N", // E欄: 是否全到
"待產出" // F欄: 狀態
];
sheet.appendRow(rowData);
return "成功記錄至偵測清單";
} catch (e) {
return "寫入偵測清單失敗: " + e.toString();
}
}
function runMaihuobianCheck() {
try { syncInventoryFinalSuccess(); } catch(e) {}
const ss = SpreadsheetApp.getActiveSpreadsheet();
const listSheet = ss.getSheetByName("賣貨便list");
const invSheet = ss.getSheetByName("存貨表");
// 取得所有資料
const listRange = listSheet.getDataRange();
const listData = listRange.getValues();
const invData = invSheet.getDataRange().getValues();
let updatedCount = 0;
// 1. 建立「存貨表」的索引 Map (大幅減少比對次數)
// 將 invData 整理成 { "IP_Keyword": [項目1, 項目2...], ... }
const invMap = {};
for (let j = 1; j < invData.length; j++) {
const key = invData[j][1] + "_" + invData[j][2]; // IP + Keyword
if (!invMap[key]) invMap[key] = [];
invMap[key].push(invData[j]);
}
// 2. 遍歷 listData,並將結果存入一個陣列,不要在迴圈內 setValue
for (let i = 1; i < listData.length; i++) {
const ip = listData[i][0];
const keyword = listData[i][1];
const status = listData[i][5];
const partialYN = listData[i][6];
if (status === "待產出" || partialYN === "Y") {
const targetItems = invMap[ip + "_" + keyword];
if (targetItems) {
let totalRows = 0, trueRows = 0, missingItems = [];
targetItems.forEach(row => {
totalRows++;
const isReceived = row[9];
if (isReceived === true || String(isReceived).toUpperCase() === "TRUE") {
trueRows++;
} else {
missingItems.push(`${row[3]}/${row[4]}*${row[7]}`);
}
});
if (totalRows > 0) {
let isFull = (trueRows === totalRows) ? "Y" : "N";
let ratio = (trueRows / totalRows);
let detail = (isFull === "Y") ? "" : missingItems.join("\\n");
// 直接更新該行對應的 listSheet 欄位 (這裡仍用單筆更新,若要更快可整理成大矩陣一次 setValues)
listSheet.getRange(i + 1, 5).setValue(isFull);
listSheet.getRange(i + 1, 8).setValue(detail);
listSheet.getRange(i + 1, 9).setValue(ratio);
updatedCount++;
}
}
}
}
return `核對完成!已更新 ${updatedCount} 個賣場資料。`;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* --- 基礎樣式與變數 --- */
body {
font-family: Arial, sans-serif;
margin: 0;
background-color: #efebe0;
padding: 15px;
max-width: 800px;
margin: auto;
color: #434343;
}
/* --- 導覽列優化 --- */
.nav-container {
display: flex;
justify-content: center;
margin: 10px 0 25px 0;
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
}
.nav-btn {
flex: 1;
max-width: 200px;
padding: 15px 0;
border: 2px solid #ab4223;
background: #fdfaf5;
color: #ab4223;
cursor: pointer;
font-weight: bold;
font-size: 18px;
transition: 0.3s;
margin: 0; /* 覆蓋原有 margin auto */
}
.nav-btn.active { background: #ab4223; color: white; }
.nav-btn:first-child { border-radius: 10px 0 0 10px; }
.nav-btn:last-child { border-radius: 0 10px 10px 0; }
/* --- 輸入表單樣式 --- */
label {
display: block;
text-align: center;
margin-top: 16px;
font-weight: bold;
font-size: 18px;
}
input[type="text"], input[type="number"], select {
display: block;
width: 100%;
max-width: 500px;
padding: 12px;
font-size: 16px;
margin: 10px auto;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 8px;
}
/* --- 存貨卡片樣式 --- */
.order-card {
background: #fff;
border: 2px solid #ab4223;
border-radius: 10px;
padding: 16px;
margin: 16px auto;
max-width: 600px;
cursor: pointer;
transition: 0.2s;
}
.order-card.selected { border-color: #4c6aa0; background-color: #f0f8ff; }
/* --- 重量卡片樣式 (響應式優化) --- */
.weight-card {
background: white;
border: 1px solid #ddd;
border-radius: 10px;
padding: 15px;
margin: 10px auto;
max-width: 600px;
display: flex;
flex-direction: column; /* 手機版預設垂直排版 */
gap: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
/* --- 存貨卡片與狀態按鈕 (修正重點) --- */
.order-card {
background: #fff; border: 2px solid #ab4223; border-radius: 10px;
padding: 16px; margin: 16px auto; max-width: 600px; cursor: pointer;
}
.order-card.selected { border-color: #4c6aa0; background-color: #f0f8ff; }
.order-header { font-weight: bold; color: #ab4223; margin-bottom: 5px; }
.order-items { font-size: 14px; background: #f9f9f9; padding: 8px; border-radius: 4px; margin: 8px 0; }
.status-container { display: flex; gap: 8px; align-items: center; margin-top: 10px; padding-top: 10px; border-top: 1px dashed #ddd; }
.status-container input[type="radio"] { display: none; } /* 隱藏原始圓圈 */
.btn-toggle {
flex: 1; padding: 8px; text-align: center; border: 1px solid #ddd;
border-radius: 6px; cursor: pointer; font-size: 14px; background: #fff;
}
/* Yes 選中變綠色,No 選中變灰色 */
input[id^="y_"]:checked + .btn-toggle { background: #4caf50; color: white; border-color: #4caf50; }
input[id^="n_"]:checked + .btn-toggle { background: #757575; color: white; border-color: #757575; }
/* --- 重量卡片 --- */
.weight-card {
background: white; border: 1px solid #ddd; border-radius: 10px; padding: 15px;
margin: 10px auto; display: flex; flex-direction: column; gap: 10px;
}
@media (min-width: 600px) { .weight-card { flex-direction: row; justify-content: space-between; align-items: center; } }
.save-btn { background: #4caf50; color: white; border: none; padding: 10px 15px; border-radius: 6px; cursor: pointer; }
/* --- 其他 --- */
.suggestions-list { list-style: none; padding: 0; max-width: 500px; margin: -8px auto 10px; background: white; border: 1px solid #ddd; }
.suggestions-list li { padding: 12px; cursor: pointer; border-bottom: 1px solid #eee; }
.main-button { display: block; width: 100%; max-width: 500px; padding: 15px; font-size: 18px; background-color: #ab4223; color: white; border: none; border-radius: 8px; margin: 20px auto; cursor: pointer; }
.modal { display: none; position: fixed; z-index: 999; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
.modal-content { background: white; margin: 20% auto; padding: 30px; border-radius: 10px; width: 80%; max-width: 350px; text-align: center; }
/* 產品勾選清單容器 */
.item-checklist {
background-color: #f9f9f9;
border-radius: 5px;
padding: 10px;
margin: 10px 0;
border: 1px solid #eee;
}
/* 每一行產品的樣式 */
.checklist-item {
display: flex;
align-items: flex-start; /* 對齊頂部,避免多行時跑掉 */
padding: 4px 2px;
cursor: pointer;
border-bottom: 1px dashed #ddd;
transition: background 0.2s;
}
.checklist-item:last-child {
border-bottom: none;
}
.checklist-item:hover {
background-color: #f0f0f0;
}
/* 隱藏原生 checkbox,或者稍微加大 */
.checklist-item input[type="checkbox"] {
margin-right: 10px;
margin-top: 4px;
width: 18px;
height: 18px;
cursor: pointer;
}
/* 勾選後的文字效果:變灰 + 刪除線 */
.checklist-item input:checked + span {
color: #a0a0a0;
text-decoration: line-through;
}
.item-text {
font-size: 14px;
line-height: 1.4;
color: #333;
}
/* 賣貨便結果卡片樣式 */
.mhb-card {
transition: transform 0.2s, box-shadow 0.2s;
}
.mhb-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.1) !important;
}
/* 狀態文字加粗 */
.status-badge {
font-size: 15px;
font-weight: 800;
margin: 5px 0;
}
/* 下方按鈕平分區域 */
.card-action-btn {
font-weight: bold;
font-size: 14px;
transition: opacity 0.2s;
}
.card-action-btn:active {
opacity: 0.7;
}
@media (min-width: 600px) {
.weight-card { flex-direction: row; justify-content: space-between; align-items: center; }
}
.product-info { flex: 1; }
.original-weight { display: block; color: #888; font-size: 14px; margin-top: 4px; }
.weight-action { display: flex; align-items: center; gap: 8px; }
.weight-input { width: 100px !important; margin: 0 !important; }
.save-btn {
background: #4caf50;
color: white;
border: none;
padding: 10px 15px;
border-radius: 6px;
cursor: pointer;
min-width: 70px;
}
/* --- 自動補完選單 --- */
.suggestions-list {
list-style: none;
padding: 0;
max-width: 500px;
margin: -8px auto 10px;
background: white;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 8px 8px;
}
.suggestions-list li { padding: 12px; cursor: pointer; border-bottom: 1px solid #f9f9f9; }
.suggestions-list li:hover { background: #d0eaff; }
/* --- 原有通用按鈕 --- */
.main-button {
display: block;
width: 100%;
max-width: 500px;
padding: 15px;
font-size: 18px;
background-color: #ab4223;
color: white;
border: none;
cursor: pointer;
margin: 20px auto;
border-radius: 8px;
}
/* --- Modal 樣式 --- */
.modal { display: none; position: fixed; z-index: 999; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
.modal-content { background: white; margin: 20% auto; padding: 30px; border-radius: 10px; width: 80%; max-width: 350px; text-align: center; }
/* 手機版大字體適配 */
@media (max-width: 600px) {
body { font-size: 18px; }
.nav-btn { font-size: 16px; padding: 12px 0; }
label { font-size: 20px; }
input, select, .main-button { font-size: 20px; }
}
</style>
</head>
<body>
<div id="mainMenu" style="text-align: center; padding: 50px 20px;">
<h2 style="color: #a0522d; margin-bottom: 30px;">📦 賣場作業系統</h2>
<button onclick="switchView('inventory')" style="width: 200px; padding: 15px; margin: 10px; font-size: 18px; cursor: pointer; background: #a0522d; color: white; border: none; border-radius: 8px;">存貨核對</button>
<br>
<button onclick="showMHBPage()" style="width: 200px; padding: 15px; margin: 10px; font-size: 18px; cursor: pointer; background: #6b8e23; color: white; border: none; border-radius: 8px;">確認賣貨便</button>
</div>
<div id="inventoryUI" style="display: none;">
<button onclick="switchView('menu')" style="margin: 10px; padding: 8px 15px; background: #999; color: white; border: none; border-radius: 4px; cursor: pointer;">⬅ 返回主選單</button>
<div class="nav-container">
<button id="btn-track" class="nav-btn active" onclick="switchPage('track')">存貨追查</button>
<button id="btn-weight" class="nav-btn" onclick="switchPage('weight')">輸入重量</button>
</div>
<div id="page-track" class="page-area">
<label style="display: block; text-align: center;">請輸入回台追蹤碼</label>
<div style="display: flex; justify-content: center; width: 90%; max-width: 600px; margin: 10px auto 20px auto;">
<input type="text" id="UFInput" placeholder="請輸入回台追蹤碼"
style="flex: 1; margin: 0; padding: 10px; border: 2px solid #333; border-radius: 8px 0 0 8px; height: 42px; box-sizing: border-box; outline: none;">
<button type="button" onclick="clearUFInputOnly()"
style="height: 42px; padding: 0 20px; background-color: #666; color: white; border: 2px solid #333; border-left: none; border-radius: 0 8px 8px 0; cursor: pointer; white-space: nowrap; font-size: 14px; font-weight: bold;">
重置
</button>
</div>
<div class="nav-container" style="margin: 15px 0;">
<button id="mode-mart" class="nav-btn active" style="font-size: 14px;" onclick="toggleSearchMode('mart')">賣場名稱查詢</button>
<button id="mode-tracking" class="nav-btn" style="font-size: 14px;" onclick="toggleSearchMode('tracking')">追蹤號碼查詢</button>
</div>
<div id="search-group-mart">
<label>輸入 IP 名稱</label>
<input type="text" id="ipInput" oninput="suggestIPs('ipInput', 'ipSuggestions', 'keywordSelect')" placeholder="例如:我的英雄學院">
<ul id="ipSuggestions" class="suggestions-list"></ul>
<label>選擇 Keyword</label>
<input type="text" id="keywordInput" oninput="suggestKeywords()" placeholder="輸入關鍵字搜尋 Keyword"> <ul id="keywordSuggestions" class="suggestions-list"></ul>
</div>
<div id="search-group-tracking" style="display: none;">
<label>請輸入追蹤號碼</label>
<input type="text" id="trackingSearchInput" placeholder="請輸入包裹追蹤單號">
</div>
<label>備註</label>
<input type="text" id="noteInput" placeholder="包裹備注">
<button class="main-button" onclick="search()">查詢</button>
<button id="submitBtn" class="main-button" onclick="submitSelected()" style="display:none; background-color: #4c6aa0;">確認送出</button>
<div id="cardContainer"></div>
</div>
<div id="page-weight" class="page-area" style="display:none;">
<label>輸入 IP 名稱 (重量分頁)</label>
<input type="text" id="ipInputWeight" oninput="suggestIPs('ipInputWeight', 'ipSuggestionsWeight', 'keywordSelectWeight')" placeholder="搜尋作品名稱...">
<ul id="ipSuggestionsWeight" class="suggestions-list"></ul>
<label>選擇 Keyword</label>
<input type="text" id="keywordInputWeight" oninput="suggestKeywordsWeight()" placeholder="搜尋 Keyword...">
<ul id="keywordSuggestionsWeight" class="suggestions-list"></ul>
<div style="text-align: center; width: 100%; margin-top: 15px;">
<button type="button" onclick="clearWeightSearch()"
style="margin-top: 10px; display: inline-block; width: auto; padding: 8px 20px; background-color: #666; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">
🧹 清除搜尋條件 (IP & Keyword)
</button>
</div>
<div id="weightProductContainer" style="margin-top: 20px; padding-bottom: 80px;"></div>
<button id="saveAllWeightBtn" class="main-button save-all-btn" style="display:none;" onclick="saveBatchWeights()">儲存全部重量</button>
</div>
</div>
<div id="maihuobianUI" style="display: none; text-align: center; padding: 20px;">
<button onclick="switchView('menu')" style="margin-bottom: 20px; padding: 5px 15px; cursor: pointer;">⬅ 返回主選單</button>
<h3 style="color: #a0522d;">🔍 賣場狀態總覽</h3>
<div style="margin-bottom: 20px;">
<button id="mhbRefreshBtn" onclick="executeMHBRefresh()" style="padding: 10px 20px; background-color: #6b8e23; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold;">
🔄 刷新並取得最新資料
</button>
<p style="font-size: 12px; color: #888; margin-top: 5px;">(點擊後將同步存貨表並重新計算比率)</p>
</div>
<hr style="border: 0.5px solid #eee; margin: 15px 0;">
<div id="mhbCardContainer" style="display: flex; flex-direction: column; align-items: center; gap: 15px; padding-bottom: 50px;">
</div>
</div>
<div id="popupModal" class="modal">
<div class="modal-content">
<p id="popupText"></p>
<button class="main-button" onclick="closeModal()">關閉</button>
</div>
</div>
<div id="promptModal" class="modal" style="display:none;">
<div class="modal-content">
<h3 id="promptTitle" style="margin-top:0; color:#ab4223;">補充資訊</h3>
<p id="promptMsg"></p>
<input type="text" id="promptInput" placeholder="在此輸入號碼..."
style="width: 100%; padding: 10px; margin: 15px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;">
<div style="display: flex; gap: 10px;">
<button class="main-button" id="promptConfirmBtn" style="background-color: #4c6aa0;">確認</button>
<button class="main-button" id="promptCancelBtn" style="background-color: #999;">取消</button>
</div>
</div>
</div>
<div id="multipleModal" class="modal" style="display:none; position:fixed; z-index:1001; left:0; top:0; width:100%; height:100%; background:rgba(0,0,0,0.5);">
<div style="background:white; width:90%; max-width:500px; margin:10% auto; padding:20px; border-radius:12px; position:relative;">
<h3 id="multipleModalTitle" style="color:#ab4223; text-align:center;">此訂單號碼可能為複合賣場</h3>
<p style="text-align:center;">是否連同其他賣場一併申報?</p>
<div id="multipleModalContent" style="max-height:400px; overflow-y:auto; margin-bottom:20px;">
</div>
<div style="display:flex; gap:10px;">
<button onclick="confirmMultiple(true)" style="flex:1; padding:12px; background:#4CAF50; color:white; border:none; border-radius:8px; cursor:pointer; font-weight:bold;">Yes</button>
<button onclick="confirmMultiple(false)" style="flex:1; padding:12px; background:#eee; color:#333; border:none; border-radius:8px; cursor:pointer; font-weight:bold;">No</button>
</div>
</div>
</div>
<div id="confirmModal" class="modal" style="display:none;">
<div class="modal-content" style="text-align: center;">
<h3 style="color: #a0522d;">⚠️ 確認執行</h3>
<p id="confirmMsg">確定要將此賣場狀態更改為「待通知」嗎?</p>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="main-button" id="executeConfirmBtn" style="background-color: #4CAF50; flex: 1;">確認</button>
<button class="main-button" onclick="closeConfirmModal()" style="background-color: #999; flex: 1;">取消</button>
</div>
</div>
</div>
<script>
let currentSearchMode = 'mart'; // 預設模式
let pendingPayload = []; // 原始選取的資料
let relatedPayload = []; // 彈窗中的其餘賣場資料
let pendingRowIndex = null; // 用來暫存準備要更新的行號
// --- 畫面切換邏輯 ---
function switchView(view) {
document.getElementById('mainMenu').style.display = 'none';
document.getElementById('inventoryUI').style.display = 'none';
document.getElementById('maihuobianUI').style.display = 'none';
if (view === 'inventory') document.getElementById('inventoryUI').style.display = 'block';
else if (view === 'maihuobian') document.getElementById('maihuobianUI').style.display = 'block';
else document.getElementById('mainMenu').style.display = 'block';
}
function showMHBPage() {
switchView('maihuobian');
const container = document.getElementById('mhbCardContainer');
container.innerHTML = "⌛ 正在讀取現有賣場清單...";
// 這裡我們呼叫後端,但不執行 runMaihuobianCheck 的「同步」部分
// 只單純 getMaihuobianList 撈出目前試算表上的結果
google.script.run
.withSuccessHandler(function(dataList) {
renderMHBCards(dataList);
})
.getMaihuobianList(false); // 傳入參數 false 表示「不執行同步」
}
// 頁面內「刷新」按鈕呼叫這個
function executeMHBRefresh() {
const btn = document.getElementById('mhbRefreshBtn');
const container = document.getElementById('mhbCardContainer');
btn.disabled = true;
btn.innerText = "🔄 同步中,請稍候...";
container.innerHTML = "🔄 正在同步存貨資料並重新計算比率...<br>這可能需要較長時間,請勿關閉視窗。";
google.script.run
.withSuccessHandler(function(dataList) {
btn.disabled = false;
btn.innerText = "🔄 刷新並取得最新資料";
renderMHBCards(dataList);
// 使用自定義 Modal 提示完成
showPopup("✅ 存貨資料已完成同步與核對!");
})
.getMaihuobianList(true); // 傳入參數 true 表示「要執行同步」
}
// 輔助顯示彈窗
function showPopup(msg) {
document.getElementById('popupText').innerHTML = msg;
document.getElementById('popupModal').style.display = 'block';
}
// 渲染卡片
function renderMHBCards(dataList) {
const container = document.getElementById('mhbCardContainer');
container.innerHTML = dataList.map((item, index) => {
const isFull = item.isFull === "Y";
const statusText = isFull ? "全到貨" : "部分到貨";
const statusColor = isFull ? "#2e7d32" : "#f9a825"; // 綠色 vs 黃色
const ratioDisplay = (item.ratio * 100).toFixed(0) + "%";
return `
<div class="mhb-card" style="width: 90%; max-width: 400px; border: 1px solid #ddd; border-radius: 12px; background: white; box-shadow: 0 4px 6px rgba(0,0,0,0.05); padding: 15px; text-align: left; position: relative;">
<div style="font-weight: bold; font-size: 16px; color: #333; margin-bottom: 8px;">• ${item.martFullName}</div>
<div style="font-weight: bold; color: ${statusColor}; margin-bottom: 5px;">${statusText}</div>
<div style="color: #666; font-size: 14px;">存貨全到%:${ratioDisplay}</div>
<div style="display: flex; gap: 10px; margin-top: 15px; border-top: 1px solid #f0f0f0; padding-top: 10px;">
<button onclick="showMHBDetail('${item.detail.replace(/\\n/g, '<br>')}')" style="flex: 1; padding: 8px; background: #eee; border: none; border-radius: 6px; cursor: pointer;">查看細節</button>
<button onclick="updateMHBStatus(${item.rowIndex})" style="flex: 1; padding: 8px; background: #a0522d; color: white; border: none; border-radius: 6px; cursor: pointer;">產出賣貨便</button>
</div>
</div>
`;
}).join('');
}
// 顯示細節彈窗
function showMHBDetail(detail) {
const msg = detail ? detail : "所有品項皆已到貨!";
// 複用您原本的 popupModal
document.getElementById('popupText').innerHTML = msg;
document.getElementById('popupModal').style.display = 'block';
}
// 更新狀態為 "待通知"
// 第一步:按下卡片上的「產出賣貨便」按鈕
function updateMHBStatus(rowIndex) {
pendingRowIndex = rowIndex; // 存下行號
// 顯示詢問彈窗
document.getElementById('confirmModal').style.display = 'block';
}
// 第二步:按下 Modal 裡的「確認」按鈕
document.getElementById('executeConfirmBtn').onclick = function() {
if (pendingRowIndex !== null) {
// 暫存一下目前的行號,避免被清掉
const rowIndexToUpdate = pendingRowIndex;
// 顯示執行中的提示 (可選)
showPopup("🔄 正在更新狀態,請稍候...");
document.getElementById('confirmModal').style.display = 'none';
google.script.run.withSuccessHandler(function(res) {
// 更新成功後顯示結果
showPopup("✅ 已成功將狀態更新為「待通知」!<br>該賣場將移出待處理清單。");
// 重新整理列表,該賣場就會消失
showMHBPage();
pendingRowIndex = null; // 清空暫存
}).updateMHBStatusToNotify(rowIndexToUpdate);
}else {
// 如果發生意外沒有 index,就單純關閉
document.getElementById('confirmModal').style.display = 'none';
}
};
// 輔助函式:關閉詢問窗
function closeConfirmModal() {
document.getElementById('confirmModal').style.display = 'none';
pendingRowIndex = null;
}
// 頁面切換
function switchPage(pageType) {
document.getElementById('page-track').style.display = (pageType === 'track') ? 'block' : 'none';
document.getElementById('page-weight').style.display = (pageType === 'weight') ? 'block' : 'none';
document.getElementById('btn-track').classList.toggle('active', pageType === 'track');
document.getElementById('btn-weight').classList.toggle('active', pageType === 'weight');
}
// 自動補完
function suggestIPs(inputId, suggestionId, selectId) {
const input = document.getElementById(inputId).value.trim();
const ul = document.getElementById(suggestionId);
if (!input) { ul.innerHTML = ''; return; }
google.script.run.withSuccessHandler(function(matches) {
ul.innerHTML = matches.map(ip => `<li onclick="selectIP('${ip}', '${inputId}', '${suggestionId}', '${selectId}')">${ip}</li>`).join('');
}).searchIPByKeyword(input);
}
function selectIP(ip, inputId, suggestionId, selectId) {
document.getElementById(inputId).value = ip;
document.getElementById(suggestionId).innerHTML = '';
loadKeywords(ip, selectId);
}
function loadKeywords(ip, selectId) {
google.script.run.withSuccessHandler(function(keywords) {
const select = document.getElementById(selectId);
select.innerHTML = '<option value="">請選擇 Keyword</option>' + keywords.map(k => `<option value="${k}">${k}</option>`).join('');
}).getKeywordsByIP(ip);
}
// --- 重量分頁邏輯 ---
function loadWeightProducts() {
const ip = document.getElementById('ipInputWeight').value.trim();
const keyword = document.getElementById('keywordInputWeight').value.trim();
const container = document.getElementById('weightProductContainer');
const saveBtn = document.getElementById('saveAllWeightBtn');
if (!ip || !keyword) {
saveBtn.style.display = 'none';
return;
}
container.innerHTML = '<p style="text-align:center;">讀取資料中...</p>';
google.script.run.withSuccessHandler(function(products) {
if (!products.length) {
container.innerHTML = '<p style="text-align:center;">此類別下無產品資料</p>';
saveBtn.style.display = 'none';
return;
}
saveBtn.style.display = 'block';
container.innerHTML = products.map(p => `
<div class="weight-card">
<div class="product-info">
<strong>${p.name}</strong>
<span class="original-weight">目前: ${p.weight || 0} kg</span>
</div>
<input type="number" step="0.01" class="weight-input batch-weight-input"
data-row="${p.row}"
value="${p.weight || ''}"
placeholder="輸入重量">
</div>`).join('');
}).getProductsByInfo(ip, keyword);
}
// 批次儲存重量
function saveBatchWeights() {
const inputs = document.querySelectorAll('.batch-weight-input');
const updates = [];
inputs.forEach(input => {
const val = input.value.trim();
if (val !== "") {
updates.push({
row: input.getAttribute('data-row'),
weight: val
});
}
});
if (updates.length === 0) {
alert('請至少輸入一個產品的重量');
return;
}
const btn = document.getElementById('saveAllWeightBtn');
btn.disabled = true;
btn.textContent = "儲存中...";
google.script.run.withSuccessHandler(function(res) {
showModal(res);
btn.disabled = false;
btn.textContent = "儲存全部重量";
loadWeightProducts(); // 刷新顯示
}).updateBatchWeightsToSheet(updates); // 注意:後端需要對應此函式
}
function toggleSearchMode(mode) {
currentSearchMode = mode;
// 切換顯示/隱藏
document.getElementById('search-group-mart').style.display = (mode === 'mart') ? 'block' : 'none';
document.getElementById('search-group-tracking').style.display = (mode === 'tracking') ? 'block' : 'none';
// 切換按鈕樣式
document.getElementById('mode-mart').classList.toggle('active', mode === 'mart');
document.getElementById('mode-tracking').classList.toggle('active', mode === 'tracking');
}
// --- 存貨追查邏輯 ---
function search() {
const container = document.getElementById('cardContainer');
container.innerHTML = '<p style="text-align:center;">🔍 搜尋中...</p>';
if (currentSearchMode === 'mart') {
// 模式一:賣場名稱搜尋
const ip = document.getElementById('ipInput').value.trim();
const keyword = document.getElementById('keywordInput').value;
if (!ip || !keyword) { alert('請輸入 IP 和 Keyword'); return; }
google.script.run.withSuccessHandler(renderOrderCards).searchOrdersByIPKeyword(ip, keyword);
} else {
// 模式二:追蹤號碼搜尋
const tracking = document.getElementById('trackingSearchInput').value.trim();
if (!tracking) { alert('請輸入追蹤號碼'); return; }
google.script.run.withSuccessHandler(renderOrderCards).searchOrdersByTracking(tracking);
}
}
// 統一渲染卡片的函式 (讓兩種搜尋共用)
function renderOrderCards(orders) {
const container = document.getElementById('cardContainer');
if (!orders || !orders.length) {
container.innerHTML = '<p style="text-align:center;">查無資料</p>';
return;
}
container.innerHTML = orders.map(order => {
// --- 核心修改:判斷複合賣場警示 ---
const multipleAlert = order.isMultiple ?
`<div class="multiple-badge" style="margin-top: 12px; color: #ab4223; font-weight: bold; font-size: 13px; text-align: center; border: 1.5px dashed #ab4223; padding: 6px; border-radius: 4px; background-color: #fff5f2;">
⚠️ 注意!此為複合賣場
</div>` : '';
return `
<div class="order-card" onclick="selectCard(this)">
<div class="mart-name-header" style="color: #a0522d; font-weight: bold; margin-bottom: 5px;">
🏪 ${order.martName || '未知賣場'}
</div>
<div class="order-header">🌟訂單編號:${order.orderNo}</div>
<div class="order-tracking">🚛追蹤號碼:${order.tracking}</div>
<div class="order-items" style="margin-top: 10px; background: #fff; padding: 10px; border-radius: 4px; display: flex; flex-direction: column; align-items: flex-start;">
${order.items.map(item => `
<label class="item-checklist-label" onclick="event.stopPropagation();" style="display: flex; align-items: flex-start; width: 100%; padding: 2px 0; border-bottom: 1px dashed #eee; cursor: pointer; text-align: left;">
<input type="checkbox" style="margin-right: 10px; margin-top: 4px; flex-shrink: 0; cursor: pointer;">
<span style="font-size: 14px; color: #333; line-height: 1.5;">${item}</span>
</label>
`).join('')}
</div>
<div class="status-container" onclick="event.stopPropagation();">
<span style="font-size:14px">全到?</span>
<input type="radio" name="status_${order.orderNo}" value="Yes" id="y_${order.orderNo}" checked>
<label for="y_${order.orderNo}" class="btn-toggle">Yes</label>
<input type="radio" name="status_${order.orderNo}" value="No" id="n_${order.orderNo}" >
<label for="n_${order.orderNo}" class="btn-toggle">No</label>
</div>
${multipleAlert}
<input type="hidden" class="card-mart-name" value="${order.martName || ''}">
<input type="hidden" class="is-multiple-flag" value="${order.isMultiple}">
</div>`;
}).join('');
}
function selectCard(card) {
card.classList.toggle('selected');
document.getElementById('submitBtn').style.display = document.querySelectorAll('.order-card.selected').length ? 'block' : 'none';
}
// 新增一個輔助函式,讓 Modal 像 prompt 一樣回傳結果
function customPrompt(title, msg) {
return new Promise((resolve) => {
const modal = document.getElementById('promptModal');
const input = document.getElementById('promptInput');
const confirmBtn = document.getElementById('promptConfirmBtn');
const cancelBtn = document.getElementById('promptCancelBtn');
document.getElementById('promptTitle').textContent = title;
document.getElementById('promptMsg').textContent = msg;
input.value = ""; // 清空舊值
modal.style.display = 'block';
input.focus();
confirmBtn.onclick = () => {
modal.style.display = 'none';
resolve(input.value.trim());
};
cancelBtn.onclick = () => {
modal.style.display = 'none';
resolve(null); // 使用者取消
};
});
}
async function submitSelected() {
const ufNum = document.getElementById('UFInput').value.trim();
// 使用現有的 showModal 替代 alert
if (!ufNum) { showModal("請輸入回台追蹤碼"); return; }
const selectedCards = document.querySelectorAll('.order-card.selected');
if (selectedCards.length === 0) { showModal("請至少選擇一張卡片"); return; }
let payload = [];
let hasMultiple = false;
let targetOrderNo = "";
let targetIP = document.getElementById('ipInput').value.trim();
let targetKey = document.getElementById('keywordInput').value.trim();
let currentNote = document.getElementById('noteInput').value.trim();
// --- 關鍵修正:使用 for...of 迴圈才能配合 await 等待 Modal 輸入 ---
for (const card of selectedCards) {
const orderNo = card.querySelector('.order-header').innerText.replace('🌟訂單編號:', '');
let tracking = card.querySelector('.order-tracking').innerText.replace('🚛追蹤號碼:', '');
const items = card.querySelector('.order-items').innerText;
const status = card.querySelector(`input[name^="status_"]:checked`).value;
const isMultipleStr = card.querySelector('.is-multiple-flag').value;
const isMultiple = (isMultipleStr === "true");
// --- 偵測追蹤號碼是否為 0 ---
if (tracking === "0" || tracking === "" || tracking === "undefined") {
// 呼叫您現有的 promptModal (透過 customPrompt 函式)
const userInput = await customPrompt("補填追蹤號碼", `訂單 ${orderNo} 的號碼為 0,請手動輸入:`);
if (userInput !== null && userInput !== "") {
tracking = userInput; // 成功獲取輸入值
} else if (userInput === null) {
return; // 使用者按「取消」,中斷整個提交程序
} else {
// 使用者沒填寫就按確認,使用 showModal 提示
showModal("追蹤號碼不能為空,請重新提交並填寫號碼。");
return;
}
}
const martName = card.querySelector('.card-mart-name').value;
let cardIP = "";
let cardKey = "";
if (martName.includes('_')) {
const firstUnderscoreIndex = martName.indexOf('_');
cardIP = martName.substring(0, firstUnderscoreIndex);
cardKey = martName.substring(firstUnderscoreIndex + 1);
}
if (!targetIP) targetIP = cardIP;
if (!targetKey) targetKey = cardKey;
if (isMultiple) {
hasMultiple = true;
targetOrderNo = orderNo;
}
payload.push({
UF_num: ufNum,
ip: cardIP || targetIP,
keyword: cardKey || targetKey,
tracking: tracking,
orderNo: orderNo,
items: items,
note: currentNote,
isComplete: status,
isMultipleReport: "N"
});
}
pendingPayload = payload;
if (hasMultiple) {
openMultipleModal(targetOrderNo, targetIP, targetKey);
} else {
finalExecuteSubmit(pendingPayload, targetIP, targetKey);
}
}
// 開啟彈窗並抓取資料
function openMultipleModal(orderNo, ip, key) {
const content = document.getElementById('multipleModalContent');
const ufNum = document.getElementById('UFInput').value.trim();
const currentNote = document.getElementById('noteInput').value.trim();
content.innerHTML = '<p style="text-align:center;">正在讀取其餘賣場資料...</p>';
document.getElementById('multipleModal').style.display = 'block';
google.script.run.withSuccessHandler(results => {
if (!results || results.length === 0) {
content.innerHTML = '<p style="text-align:center;">無其他賣場資訊</p>';
return;
}
// 渲染 Modal 畫面:改為與主頁面一致的卡片式
content.innerHTML = results.map((m, index) => `
<div class="modal-order-card selected" onclick="this.classList.toggle('selected')">
<div style="color:#ab4223; font-weight:bold; margin-bottom:5px;">🏪 賣場:${m.martName}</div>
<div style="font-size:13px;">🌟 訂單:${m.orderNo}</div>
<div style="font-size:12px; color:#666; margin:8px 0; padding-left:10px; border-left:2px solid #eee;">
${m.items.join('<br>')}
</div>
<div class="modal-status-row" onclick="event.stopPropagation();">
<span style="font-size:13px; color:#555;">全到?</span>
<div class="toggle-group">
<input type="radio" name="rel_status_${index}" value="Yes" id="ry_${index}" checked>
<label for="ry_${index}">Yes</label>
<input type="radio" name="rel_status_${index}" value="No" id="rn_${index}">
<label for="rn_${index}">No</label>
</div>
</div>
<input type="hidden" class="rel-data" value='${JSON.stringify(m)}'>
</div>
`).join('');
// 注入樣式
injectModalStyles();
}).getRelatedMarts(orderNo, ip, key);
}
// 切換選取狀態的函式
function selectModalCard(card) {
card.classList.toggle('selected');
}
// Modal 按鈕點擊處理
function confirmMultiple(isConfirmed) {
document.getElementById('multipleModal').style.display = 'none';
// 取得當初查詢時的 IP 和 Key (從 pendingPayload 第一筆抓最準)
const mainIP = pendingPayload.length > 0 ? pendingPayload[0].ip : "";
const mainKey = pendingPayload.length > 0 ? pendingPayload[0].keyword : "";
let finalData = [];
const isMultipleReport = isConfirmed ? "Y" : "N";
const updatedPending = pendingPayload.map(item => ({ ...item, isMultipleReport: isMultipleReport }));
finalData = updatedPending;
if (isConfirmed) {
const selectedCards = document.querySelectorAll('.modal-order-card.selected');
selectedCards.forEach(card => {
const rawData = JSON.parse(card.querySelector('.rel-data').value);
const status = card.querySelector(`input[type="radio"]:checked`).value;
// 取得第一個底線的位置
const firstUnderscoreIndex = rawData.martName.indexOf('_');
let rIP, rKey;
if (firstUnderscoreIndex !== -1) {
// 取得第一個底線左邊的所有文字
rIP = rawData.martName.substring(0, firstUnderscoreIndex);
// 取得第一個底線右邊的所有文字 (不包含底線本身)
rKey = rawData.martName.substring(firstUnderscoreIndex + 1);
} else {
// 防呆:如果名稱中完全沒有底線
rIP = rawData.martName;
rKey = "";
}
finalData.push({
UF_num: updatedPending[0].UF_num,
ip: rIP,
keyword: rKey,
tracking: rawData.tracking,
orderNo: rawData.orderNo,
items: rawData.items.join('\\n'),
note: updatedPending[0].note,
isComplete: status,
isMultipleReport: "Y"
});
});
}
// 執行送出,並傳入主店資訊
finalExecuteSubmit(finalData, mainIP, mainKey);
}
// 最終執行 Google Script 送出
// 修改後的最終送出函式
function finalExecuteSubmit(payload, lastIP, lastKeyword) {
const btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.textContent = "傳送中...";
google.script.run.withSuccessHandler(() => {
showModal(`已成功送出 ${payload.length} 筆資料`);
// --- 新功能:成功送出存貨後,觸發賣貨便偵測紀錄 ---
if (lastIP && lastKeyword) {
google.script.run
.withSuccessHandler(function(msg) {
console.log("偵測清單狀態:", msg);
})
.recordToMHBList(lastIP, lastKeyword);
}
// --- 關鍵修正 3:成功送出後,將 IP & Keyword 填入重量分頁 ---
if (lastIP && lastKeyword) {
document.getElementById('ipInputWeight').value = lastIP;
document.getElementById('keywordInputWeight').value = lastKeyword;
// 自動觸發載入該賣場的產品重量列表
loadWeightProducts();
}
// 重置 UI
btn.disabled = false;
btn.textContent = "確認送出";
btn.style.display = 'none';
document.getElementById('ipInput').value = '';
document.getElementById('keywordInput').value = '';
document.getElementById('trackingSearchInput').value = '';
document.getElementById('noteInput').value = '';
document.getElementById('cardContainer').innerHTML = '';
pendingPayload = [];
}).submitToSheet(payload);
}
function showModal(msg) { document.getElementById('popupText').textContent = msg; document.getElementById('popupModal').style.display = 'block'; }
function closeModal() { document.getElementById('popupModal').style.display = 'none'; }
function suggestKeywords() {
const ip = document.getElementById('ipInput').value.trim();
const input = document.getElementById('keywordInput').value.trim();
const ul = document.getElementById('keywordSuggestions');
if (!ip || !input) {
ul.innerHTML = '';
return;
}
google.script.run.withSuccessHandler(function(keywords) {
const filtered = keywords.filter(k => k.includes(input));
ul.innerHTML = filtered
.map(k => `<li onclick="selectKeyword('${k}')">${k}</li>`)
.join('');
}).getKeywordsByIP(ip);
}
function selectKeyword(keyword) {
document.getElementById('keywordInput').value = keyword;
document.getElementById('keywordSuggestions').innerHTML = '';
}
function suggestKeywordsWeight() {
const ip = document.getElementById('ipInputWeight').value.trim();
const input = document.getElementById('keywordInputWeight').value.trim();
const ul = document.getElementById('keywordSuggestionsWeight');
if (!ip || !input) {
ul.innerHTML = '';
return;
}
google.script.run.withSuccessHandler(function(keywords) {
const filtered = keywords.filter(k => k.includes(input));
ul.innerHTML = filtered
.map(k => `<li onclick="selectKeywordWeight('${k}')">${k}</li>`)
.join('');
}).getKeywordsByIP(ip);
}
function selectKeywordWeight(keyword) {
document.getElementById('keywordInputWeight').value = keyword;
document.getElementById('keywordSuggestionsWeight').innerHTML = '';
loadWeightProducts(); // 自動載入重量資料
}
function clearWeightSearch() {
// 1. 同時清空兩個主要的輸入框
document.getElementById('ipInputWeight').value = '';
document.getElementById('keywordInputWeight').value = '';
// 2. 清空下拉提示選單 (避免殘留)
document.getElementById('ipSuggestionsWeight').innerHTML = '';
document.getElementById('keywordSuggestionsWeight').innerHTML = '';
// 3. 清空下方已經載入的產品列表
document.getElementById('weightProductContainer').innerHTML = '';
// 4. 隱藏「儲存全部重量」按鈕
document.getElementById('saveAllWeightBtn').style.display = 'none';
console.log("已清除重量分頁所有搜尋資訊");
}
// A. 專屬清除回台追蹤碼 (手動按鈕觸發)
function clearUFInputOnly() {
document.getElementById('UFInput').value = '';
document.getElementById('UFInput').focus();
}
function injectModalStyles() {
if (document.getElementById('modal-custom-style')) return;
const style = document.createElement('style');
style.id = 'modal-custom-style';
style.innerHTML = `
/* 彈窗卡片基礎樣式 */
.modal-order-card {
border: 2px solid #eee;
padding: 15px;
border-radius: 10px;
margin-bottom: 12px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
}
/* 選取後的樣式 */
.modal-order-card.selected {
border-color: #4c6aa0;
background-color: #f0f4f9;
}
/* 狀態行排版 */
.modal-status-row {
margin-top: 10px;
display: flex;
align-items: center;
gap: 12px;
}
/* 按鈕組:解決白框問題的核心 */
.toggle-group {
display: flex;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden; /* 確保內部 Label 填滿 */
background: #fff;
}
.toggle-group input[type="radio"] {
display: none; /* 隱藏原本的圓點 */
}
.toggle-group label {
padding: 6px 15px;
font-size: 13px;
cursor: pointer;
background: #f9f9f9;
margin: 0; /* 移除可能的預設邊距 */
border: none;
display: block;
line-height: 1.2;
color: #333;
}
/* 選取後的顏色切換 */
.toggle-group input[value="Yes"]:checked + label {
background: #4c6aa0;
color: white;
}
.toggle-group input[value="No"]:checked + label {
background: #666;
color: white;
}
/* 取消選取的卡片變淡 */
.modal-order-card:not(.selected) {
opacity: 0.6;
border-style: dashed;
}
`;
document.head.appendChild(style);
}
</script>
</body>
</html>
自助下單系統
const SPREADSHEET_ID = '1YCAzPW-_1nRePyPUvmks6s64G_l7Jq5ZkMOr1Mk_Q8w'; // 請替換成您的 Google Sheet ID
const MEMBER_SHEET_NAME = '會員資料';
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('簡易下單系統')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
// 註冊功能
function registerUser(userData) {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(MEMBER_SHEET_NAME);
const data = sheet.getDataRange().getValues();
// 檢查 ID 是否重複
for (let i = 1; i < data.length; i++) {
if (data[i][0] === userData.id) return { success: false, msg: '此 ID 已被註冊' };
}
// 寫入資料:ID, 密碼, 信箱, FB姓名, 手機後三碼
sheet.appendRow([
userData.id,
userData.password,
userData.email,
userData.fbName,
userData.phoneTail
]);
return { success: true, id: userData.id };
}
// 登入功能
function loginUser(id, password) {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(MEMBER_SHEET_NAME);
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === id && data[i][1].toString() === password) {
return { success: true, id: id };
}
}
return { success: false, msg: '帳號或密碼錯誤' };
}
/**
* 處理新會員註冊
* @param {Object} userData 包含 id, password, email, fbName, phoneTail 的物件
*/
function registerUser(userData) {
try {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(MEMBER_SHEET_NAME);
// 1. 取得現有資料檢查 ID 是否重複
const data = sheet.getDataRange().getValues();
const existingIds = data.map(row => row[0].toString().toLowerCase()); // 取得第一欄(ID)的所有資料
if (existingIds.includes(userData.id.toLowerCase())) {
return { success: false, msg: '此 ID 已被使用,請更換一個。' };
}
// 2. 使用 LockService 防止多人同時寫入造成衝突
const lock = LockService.getScriptLock();
lock.waitLock(10000); // 最多等待 10 秒
// 3. 按照您的表格順序寫入:ID, 密碼, 信箱, FB姓名, 手機後三碼
sheet.appendRow([
userData.id,
userData.password,
userData.email,
userData.fbName,
userData.phoneTail
]);
// 釋放鎖定
lock.releaseLock();
return { success: true, id: userData.id };
} catch (e) {
return { success: false, msg: '註冊失敗,錯誤原因:' + e.toString() };
}
}
/**
* 會員登入驗證
*/
function loginUser(id, password) {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(MEMBER_SHEET_NAME);
const data = sheet.getDataRange().getValues();
// 從第二列開始比對 (索引 1)
for (let i = 1; i < data.length; i++) {
// 假設 A 欄是 ID, B 欄是密碼
if (data[i][0].toString() === id && data[i][1].toString() === password) {
return { success: true, id: id };
}
}
return { success: false, msg: '帳號或密碼錯誤' };
}
function getShopData() {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
// 抓取產品表 (圖二)
const productSheet = ss.getSheetByName('產品表');
const productData = productSheet.getDataRange().getValues();
// 抓取賣場表 (圖三)
const marketSheet = ss.getSheetByName('賣場表');
const marketData = marketSheet.getDataRange().getValues();
// 簡單邏輯:抓取第一筆賣場資訊作為範例
// 您可以根據需求修改為抓取所有賣場
return {
imageUrl: productData[1][14], // 假設 O 欄 (索引 14) 是商品連結
note: marketData[1][11] // 假設 L 欄 (索引 11) 是賣場備註
};
}
function getActiveShops() {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const marketSheet = ss.getSheetByName('賣場表');
const productSheet = ss.getSheetByName('產品表');
const marketData = marketSheet.getDataRange().getValues();
const productData = productSheet.getDataRange().getValues();
const now = new Date();
// 1. 建立產品圖片查找表:改為一個產品名稱對應一個圖片陣列
// 假設產品表 B 欄是賣場名稱/產品名稱,O 欄是圖片
let imageMap = {};
for (let i = 1; i < productData.length; i++) {
const productName = productData[i][13];
const imgUrl = productData[i][14]; // O 欄索引 14
if (productName && imgUrl) {
if (!imageMap[productName]) {
imageMap[productName] = [];
}
// 確保圖片網址不重複
if (imageMap[productName].indexOf(imgUrl) === -1) {
imageMap[productName].push(imgUrl);
}
}
}
// 2. 過濾進行中的賣場
const activeShops = marketData.slice(1).filter(row => {
if (!row[9]) return false;
const deadLine = new Date(row[9]);
return row[0] && deadLine > now;
}).map(row => {
const pName = row[3];
return {
name: row[3],
note: row[11],
images: imageMap[pName] || ['<https://via.placeholder.com/500?text=No+Image>'], // 這裡變成陣列
deadLine: row[9]
};
});
return activeShops;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="<https://fonts.googleapis.com>">
<link rel="preconnect" href="<https://fonts.gstatic.com>" crossorigin>
<link href="<https://fonts.googleapis.com/css2?family=Lobster&display=swap>" rel="stylesheet">
<style>
/* --- 沿用存貨追查系統樣式 --- */
body {
font-family: Arial, sans-serif;
margin: 0;
background-color: #efebe0;
padding: 15px;
max-width: 800px;
margin: auto;
color: #434343;
}
/* --- 彈窗樣式 (Modal) --- */
.modal {
display: flex; /* 預設顯示 */
position: fixed;
z-index: 999;
left: 0; top: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.6);
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 10px;
width: 85%;
max-width: 400px;
text-align: center;
border: 2px solid #ab4223;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
h2, h3 { color: #ab4223; margin-top: 0; }
/* --- 輸入表單樣式 --- */
input[type="text"], input[type="password"], input[type="email"] {
display: block;
width: 100%;
padding: 12px;
font-size: 16px;
margin: 10px auto;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 8px;
}
/* --- 按鈕樣式 --- */
.main-button {
display: block;
width: 100%;
padding: 15px;
font-size: 18px;
background-color: #ab4223;
color: white;
border: none;
border-radius: 8px;
margin: 15px auto 5px auto;
cursor: pointer;
font-weight: bold;
}
.main-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.switch-link {
color: #ab4223;
text-decoration: underline;
font-size: 14px;
cursor: pointer;
display: block;
margin-top: 10px;
}
.pwd-hint {
font-size: 12px;
color: #888;
text-align: left;
display: block;
padding: 0 5px;
}
.code-display {
font-size: 24px;
font-weight: bold;
color: #ab4223;
background: #fdfaf5;
padding: 15px;
margin: 15px 0;
border: 2px dashed #ab4223;
cursor: pointer;
}
.overview-title {
font-family: 'Lobster', cursive; /* 使用您匯入的 Lobster 字體 */
text-align: center;
color: #ab4223;
font-size: 52px; /* 字體大一點效果更好 */
margin: 25px 0;
font-weight: normal; /* Lobster 本身就是粗體,通常設為 normal 即可 */
text-shadow: 2px 2px 4px rgba(0,0,0,0.05); /* 輕微陰影增加層次感 */
}
/* 讓 Alert Modal 的 z-index 更高,確保蓋在所有視窗最上面 */
#alertModal { z-index: 2000; }
#alertTitle {
border-bottom: 1px dashed #ab4223;
padding-bottom: 10px;
margin-bottom: 15px;
}
#alertMsg {
font-size: 16px;
line-height: 1.5;
margin-bottom: 20px;
}
.hidden { display: none !important; }
/* 賣場容器 */
.shop-container {
padding-bottom: 80px; /* 為底部導覽留空間 */
}
/* 理想版面圖卡樣式 */
.overview-card {
background: white;
border: 2px solid #000;
border-radius: 20px;
margin: 20px 0;
overflow: hidden;
position: relative;
padding-bottom: 10px;
}
.product-image {
width: 100%;
display: block;
}
.shop-note-area {
background: #fdfaf5;
padding: 10px;
font-size: 14px;
font-weight: bold;
border-top: 1px solid #eee;
text-align: left;
}
/* 底部導覽列 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 60px;
background: #ab4223;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 100;
}
.nav-item {
color: white;
text-decoration: none;
font-weight: bold;
padding: 0 20px;
display: flex;
align-items: center;
}
.center-logo {
width: 70px;
height: 70px;
background: white;
border-radius: 50%;
border: 3px solid #ab4223;
position: absolute;
left: 50%;
top: -20px;
transform: translateX(-50%);
display: flex;
justify-content: center;
align-items: center;
}
.center-logo img {
width: 80%;
}
.carousel-img {
width: 100%;
height: auto;
display: block;
border-bottom: 2px solid #000;
transition: opacity 0.5s ease-in-out; /* 切換時有淡入淡出感 */
}
.carousel-container img {
position: relative;
top: 0;
left: 0;
}
/* 手機版字體加大 */
@media (max-width: 600px) {
input { font-size: 18px !important; }
.main-button { font-size: 20px; }
}
</style>
</head>
<body>
<div id="alertModal" class="modal hidden">
<div class="modal-content">
<h3 id="alertTitle">提示</h3>
<p id="alertMsg"></p>
<button class="main-button" onclick="closeAlert()">確定</button>
</div>
</div>
<div id="authModal" class="modal">
<div class="modal-content">
<h2 id="authTitle">會員登入</h2>
<div id="loginArea">
<input type="text" id="loginId" placeholder="使用者 ID">
<input type="password" id="loginPwd" placeholder="密碼">
<button class="main-button" onclick="doLogin()">登入</button>
</div>
<div id="regArea" class="hidden">
<input type="text" id="regId" placeholder="設定使用者 ID">
<input type="password" id="regPwd" placeholder="設定密碼">
<span class="pwd-hint">※ 需 8 碼以上,包含英文與數字</span>
<input type="email" id="regEmail" placeholder="電子信箱">
<input type="text" id="regFb" placeholder="FB 姓名">
<input type="text" id="regPhone" placeholder="手機後 3 碼" maxlength="3">
<button class="main-button" onclick="doRegister()">註冊並領取代碼</button>
</div>
<span class="switch-link" id="switchBtn" onclick="toggleAuth()">沒有帳號?立即註冊</span>
</div>
</div>
<div id="copyModal" class="modal hidden">
<div class="modal-content">
<h3>註冊成功!</h3>
<p>點擊代碼複製,作為對帳憑據:</p>
<div id="codeBox" class="code-display" onclick="handleCopy()"></div>
<p id="copyFeedback" style="color: #4caf50; font-size: 14px; display: none;">✓ 已複製到剪貼簿</p>
<button id="nextBtn" class="main-button" disabled onclick="finalConfirm()">確認已保存,進入賣場</button>
</div>
</div>
<div id="mainContent">
<h2 style="text-align: center;">🛒 簡易下單賣場</h2>
<p id="welcomeMsg" style="text-align: center;">請先完成驗證...</p>
</div>
<script>
let isLoginMode = true;
let tempId = "";
function toggleAuth() {
isLoginMode = !isLoginMode;
document.getElementById('authTitle').innerText = isLoginMode ? "會員登入" : "新會員註冊";
document.getElementById('loginArea').classList.toggle('hidden');
document.getElementById('regArea').classList.toggle('hidden');
document.getElementById('switchBtn').innerText = isLoginMode ? "沒有帳號?立即註冊" : "已有帳號?返回登入";
}
// 驗證密碼:8碼以上且包含英數
function isPasswordValid(pwd) {
const regex = /^(?=.*[A-Za-z])(?=.*\\d).{8,}$/;
return regex.test(pwd);
}
function handleCopy() {
navigator.clipboard.writeText(tempId).then(() => {
document.getElementById('copyFeedback').style.display = 'block';
document.getElementById('nextBtn').disabled = false;
});
}
function finalConfirm() {
// 這裡不需要額外的 confirm 視窗,因為 copyModal 本身就是一個強制流程
// 只要他能點到這個按鈕,代表他已經完成了複製
localStorage.setItem('user_id', tempId);
enterShop(tempId);
// 顯示一小段進入成功的提示
showAlert("歡迎", "註冊成功!即將為您開啟賣場。");
}
function enterShop(userId) {
// 1. 隱藏登入視窗
document.getElementById('authModal').classList.add('hidden');
document.getElementById('copyModal').classList.add('hidden');
const mainContent = document.getElementById('mainContent');
// 設定標題 (請確保 .overview-title 的 CSS 已正確設定為 Ultra 字體)
mainContent.innerHTML = `<h1 class="overview-title">Overview</h1>`;
// 2. 呼叫後端函式 getActiveShops (後端需回傳 images 陣列)
google.script.run.withSuccessHandler(shops => {
if (!shops || shops.length === 0) {
mainContent.innerHTML += `<p style="text-align:center; margin-top:50px;">目前沒有進行中的賣場喔!</p>`;
return;
}
let allShopsHtml = `<div class="shop-container" style="padding-bottom: 100px;">`;
// 3. 迴圈產生賣場圖卡
shops.forEach((shop, index) => {
// 將圖片陣列轉為字串存入 data-images 屬性,供後續輪播 JavaScript 使用
const imagesJson = JSON.stringify(shop.images);
allShopsHtml += `
<div class="overview-card" style="background: white; border: 3px solid #000; border-radius: 25px; overflow: hidden; margin-bottom: 30px; box-shadow: 0 6px 0px rgba(0,0,0,0.05);">
<div class="carousel-container" id="carousel-${index}" data-images='${imagesJson}' style="position: relative; background: #f0f0f0;">
<img src="${shop.images[0]}" id="img-${index}" style="width: 100%; height: auto; display: block; transition: opacity 0.5s ease-in-out;">
</div>
<div style="padding: 15px; background: #fff; border-top: 1px solid #eee;">
<div style="color: #ab4223; font-family: 'Ultra', serif; font-size: 20px; margin-bottom: 5px;">${shop.name}</div>
<div style="font-size: 14px; color: #666; font-weight: normal; white-space: pre-wrap;">${shop.note}</div>
<div style="font-size: 12px; color: #999; margin-top: 10px;">⏳ 結單日:${new Date(shop.deadLine).toLocaleDateString()}</div>
</div>
</div>
`;
});
allShopsHtml += `</div>`;
// 4. 加入底部導覽列
const navHtml = `
<div class="bottom-nav">
<div class="nav-item">◀ 個人中心</div>
<div class="center-logo" onclick="window.scrollTo({top: 0, behavior: 'smooth'})">
<img src="<https://i.imgur.com/Nq4TZl8.png>" style="width: 75%;">
</div>
<div class="nav-item">購物車 ▶</div>
</div>
`;
mainContent.insertAdjacentHTML('beforeend', allShopsHtml + navHtml);
// 5. 重要:渲染完 HTML 後,啟動輪播計時器
startCarousels(shops.length);
}).getActiveShops();
}
// 6. 新增輪播邏輯函式
function startCarousels(totalShops) {
for (let i = 0; i < totalShops; i++) {
const container = document.getElementById(`carousel-${i}`);
if (!container) continue;
const images = JSON.parse(container.getAttribute('data-images'));
// 如果該賣場有多張 Unique 圖片,才啟動計時器
if (images && images.length > 1) {
let currentIndex = 0;
setInterval(() => {
currentIndex = (currentIndex + 1) % images.length;
const imgElement = document.getElementById(`img-${i}`);
// 淡入淡出效果
imgElement.style.opacity = 0;
setTimeout(() => {
imgElement.src = images[currentIndex];
imgElement.style.opacity = 1;
}, 500);
}, 3500); // 設定 3.5 秒切換一次
}
}
}
// --- 自訂 Alert 函式 ---
function showAlert(title, msg) {
document.getElementById('alertTitle').innerText = title;
document.getElementById('alertMsg').innerText = msg;
document.getElementById('alertModal').classList.remove('hidden');
}
function closeAlert() {
document.getElementById('alertModal').classList.add('hidden');
}
// --- 修改後的註冊邏輯 ---
function doRegister() {
const id = document.getElementById('regId').value.trim();
const pwd = document.getElementById('regPwd').value;
const email = document.getElementById('regEmail').value.trim();
const fb = document.getElementById('regFb').value.trim();
const phone = document.getElementById('regPhone').value.trim();
if(!id || !pwd || !email || !fb || !phone) {
showAlert("缺少資訊", "所有欄位皆為必填項目喔!");
return;
}
if(!isPasswordValid(pwd)) {
showAlert("密碼格式錯誤", "密碼需至少 8 碼,且必須包含英文與數字。");
return;
}
const userData = { id, password: pwd, email, fbName: fb, phoneTail: phone };
// 顯示處理狀態
const regBtn = document.querySelector('#regArea .main-button');
regBtn.disabled = true;
regBtn.innerText = "資料處理中...";
google.script.run
.withSuccessHandler(res => {
regBtn.disabled = false;
regBtn.innerText = "註冊並領取代碼";
if(res.success) {
tempId = res.id;
document.getElementById('codeBox').innerText = res.id;
document.getElementById('authModal').classList.add('hidden');
document.getElementById('copyModal').classList.remove('hidden');
} else {
showAlert("註冊失敗", res.msg);
}
})
.withFailureHandler(err => {
regBtn.disabled = false;
regBtn.innerText = "註冊並領取代碼";
showAlert("系統錯誤", "連線失敗,請稍後再試。");
})
.registerUser(userData);
}
// --- 修改後的登入邏輯 ---
function doLogin() {
const id = document.getElementById('loginId').value;
const pwd = document.getElementById('loginPwd').value;
google.script.run.withSuccessHandler(res => {
if(res.success) {
localStorage.setItem('user_id', res.id);
enterShop(res.id);
} else {
showAlert("登入失敗", res.msg);
}
}).loginUser(id, pwd);
}
window.onload = () => {
const savedUser = localStorage.getItem('user_id');
if(savedUser) enterShop(savedUser);
}
</script>
</body>
</html>