橫幅按鈕

2025年2月20日 星期四

提早下班系列 - 線上收費三聯單

🎯 學期開始的重頭戲就是註冊收費三聯單啦!
經歷過古早年代需要幫忙學生蓋三聯單名字,
最後收費還要老師代收的年代!
不過現在學校都是委託銀行處理收費問題,繳費方式越來越多元。
但收費三聯單還是需要級任老師的幫忙,

之前的社團費用都是由社團老師代收,
但現在很多費用都需先入學校公庫才能使用,
對級任在製作三聯單上又是一個大工程!

接觸了才知道,
原來選修其他本土語的課本費都不相同,而且還需補其中差價。
選 原、越、泰 語的學生 則 不用收 本土語 課本費用,
程度不一的學生,授課老師還會降低難度,使用不同年級的課本。
光是這個部分,我想負責的老師就又要傷腦筋了。

這次利用 Google 試算表的函式,老師只要勾一勾三聯單的金額就設定好了!

接下來就要正式上線了,
發現原來的設計級任老師會看到其他學生的基本資料,
心裡感謝似乎有些不妥,建議改成不需勾選。
資料全部由承辦老師後台編輯,最後由老師校對簽名確認!

只要老師用學校帳號登入 Chrome 瀏覽器 連結網址,系統就會顯示自己該班的資料。


目前應該已經測試得差不多了,老師可以試用看看,
如果合用也可以下載回去修改成學校的版本。


 👉模擬測試:三聯單後台資料維護👈👉模擬測試:級任校對👈

身份別減免項目減免金額應繳金額繳交單位備註
低收入戶
平安保險全額減免0 元教學組需有公所證明
家長會費全額減免0 元
教科書費全額減免0 元教學組
身心障礙人士子女教科書費全額減免0 元教學組
中低收入戶
家庭突遭變故
平安保險全額減免0 元註冊組
家長會費全額減免0 元
教科書費補助600元扣除600元註冊組
清寒家庭家長會費全額減免0 元
需有村、里長證明
領有重度殘障手冊
(本人或父母)
平安保險全額減免0 元教學組
家長會費全額減免0 元
原住民平安保險全額減免0 元註冊組

1.費用工作表 - 由各處室承辦人負責輸入正確金額


2.設定收費項目 - 由出納組設定(學年度 - 上下學期)


3.學生資料
  • 校務系統 - 教務處 - 註冊組 - 學生資料管理 - 報表列印 - 名冊輸出 下載名冊以利後續使用

4.身分別
  • 校務系統 - 教務處 - 註冊組 - 學生資料理 - 身份管理 - 學生身分別清冊
  • 校務系統 - 教務處 - 註冊組 - 學生資料理 - 身份管理 - 學生身分別清冊
    • ✅ 本人身心障礙(身心障礙生)
    • ✅ 家長身心障礙
    • ✅ 家庭突發因素(幸福餐劵補助)
    • ✅ 原住民
    • ✅ 低收入戶
    • ✅ 中低收入戶
  • 最後由承辦人確認是否為重度殘障手冊 - 可減免平安保險費


4-1 身份別 - 整理資料(下載後記得依序排一下欄位順序)
  • 班級
  • 座號
  • 姓名
  • 身分別

5. 本土語 - 由 教學組長 共編提供
  • 客1 - 一年級閩南語、客語課本差價
  • 客2 - 二年級閩南語、客語課本差價
  • 客3 - 三年級閩南語、客語課本差價
  • 客4 - 四年級閩南語、客語課本差價
  • 客5 - 五年級閩南語、客語課本差價
  • 客6 - 六年級閩南語、客語課本差價
  • 原 - 扣除閩南語課本價錢
  • 越 - 扣除閩南語課本價錢
  • 泰 - 扣除閩南語課本價錢

6. 特殊生 - 由 特教組長 共編提供

7. 社團名冊 - 由 訓育組長 共編提供

8. 校外教學名冊 - 由 生教組長 共編提供

9. 校對各學年學生減免金額資料


10. 相關欄位公式

篩選 特殊生:
=ARRAYFORMULA(IFERROR(VLOOKUP(TRIM(TEXT($O$2:$O$121, "0")) & "-" & TRIM(TEXT($P$2:$P$121, "0")) & "-" & TRIM(TEXT($Q$2:$Q$121, "0")), {TRIM(TEXT('特殊生'!A1:A, "0")) & "-" & TRIM(TEXT('特殊生'!B1:B, "0")) & "-" & TRIM(TEXT('特殊生'!D1:D, "0")), '特殊生'!G1:G}, 2, FALSE), ""))

篩選 低收入戶:
=ARRAYFORMULA(IFERROR(VLOOKUP(TRIM(TEXT($O$2:$O$121, "0")) & "-" & TRIM(TEXT($P$2:$P$121, "0")) & "-" & TRIM(TEXT($Q$2:$Q$121, "0")), {TRIM(TEXT('身份別'!A1:A, "0")) & "-" & TRIM(TEXT('身份別'!B1:B, "0")) & "-" & TRIM(TEXT('身份別'!D1:D, "0")), '身份別'!K1:K}, 2, FALSE), ""))

篩選 家長身心障礙
=ARRAYFORMULA(IFERROR(VLOOKUP(TRIM(TEXT($O$2:$O$121, "0")) & "-" & TRIM(TEXT($P$2:$P$121, "0")) & "-" & TRIM(TEXT($Q$2:$Q$121, "0")), {TRIM(TEXT('身份別'!A1:A, "0")) & "-" & TRIM(TEXT('身份別'!B1:B, "0")) & "-" & TRIM(TEXT('身份別'!D1:D, "0")), '身份別'!I1:I}, 2, FALSE), ""))

篩選 中低收入戶
=ARRAYFORMULA(IFERROR(VLOOKUP(TRIM(TEXT($O$2:$O$121, "0")) & "-" & TRIM(TEXT($P$2:$P$121, "0")) & "-" & TRIM(TEXT($Q$2:$Q$121, "0")), {TRIM(TEXT('身份別'!A1:A, "0")) & "-" & TRIM(TEXT('身份別'!B1:B, "0")) & "-" & TRIM(TEXT('身份別'!D1:D, "0")), '身份別'!J1:J}, 2, FALSE), ""))

篩選 家庭突發因素(幸福餐劵補助)
=ARRAYFORMULA(IFERROR(VLOOKUP(TRIM(TEXT($O$2:$O$121, "0")) & "-" & TRIM(TEXT($P$2:$P$121, "0")) & "-" & TRIM(TEXT($Q$2:$Q$121, "0")), {TRIM(TEXT('身份別'!A1:A, "0")) & "-" & TRIM(TEXT('身份別'!B1:B, "0")) & "-" & TRIM(TEXT('身份別'!D1:D, "0")), '身份別'!L1:L}, 2, FALSE), ""))

篩選 本人身心障礙(身心障礙生)
=ARRAYFORMULA(IFERROR(VLOOKUP(TRIM(TEXT($O$2:$O$121, "0")) & "-" & TRIM(TEXT($P$2:$P$121, "0")) & "-" & TRIM(TEXT($Q$2:$Q$121, "0")), {TRIM(TEXT('身份別'!A1:A, "0")) & "-" & TRIM(TEXT('身份別'!B1:B, "0")) & "-" & TRIM(TEXT('身份別'!D1:D, "0")), '身份別'!H1:H}, 2, FALSE), ""))

篩選 原住民身份
=ARRAYFORMULA(IFERROR(VLOOKUP(TRIM(TEXT($O$2:$O$121, "0")) & "-" & TRIM(TEXT($P$2:$P$121, "0")) & "-" & TRIM(TEXT($Q$2:$Q$121, "0")), {TRIM(TEXT('身份別'!A1:A, "0")) & "-" & TRIM(TEXT('身份別'!B1:B, "0")) & "-" & TRIM(TEXT('身份別'!D1:D, "0")), '身份別'!G1:G}, 2, FALSE), ""))

篩選 家長會費
=ARRAYFORMULA(IFERROR(VLOOKUP(TRIM(TEXT($O$2:$O$121, "0")) & "-" & TRIM(TEXT($P$2:$P$121, "0")) & "-" & TRIM(TEXT($Q$2:$Q$121, "0")), {TRIM(TEXT('學生資料'!A1:A, "0")) & "-" & TRIM(TEXT('學生資料'!B1:B, "0")) & "-" & TRIM(TEXT('學生資料'!D1:D, "0")), '學生資料'!M1:M}, 2, FALSE), ""))

篩選 本土語
=ARRAYFORMULA(IFERROR(VLOOKUP(
O2:O121 & "-" & P2:P121 & "-" & Q2:Q121,
{
'本土語'!A1:A & "-" & '本土語'!B1:B & "-" & '本土語'!D1:D,
'本土語'!F1:F
},
2,
FALSE
), ""))

篩選 課後照顧
=ARRAYFORMULA(IFERROR(VLOOKUP(
    TRIM(TEXT($O$2:$O$121, "0")) & "-" & TRIM(TEXT($P$2:$P$121, "0")) & "-" & TRIM(TEXT($Q$2:$Q$121, "0")), 
    {
        TRIM(TEXT('課後照顧'!A1:A, "0")) & "-" & 
        TRIM(TEXT('課後照顧'!B1:B, "0")) & "-" & 
        TRIM(TEXT('課後照顧'!D1:D, "0")), 
        '課後照顧'!G1:G
    }, 
    2, FALSE
), ""))

篩選 是否繳交 平安保險 費用
=IF(B2="低收入戶",0,IF(D2="中低收入戶",0,IF(E2="家庭突發因素(幸福餐劵補助)",0,IF(G2="原住民",0,'費用'!$O$1))))

篩選 是否繳交 教科書 費用
=IFS(
    B2="低收入戶", 0,
    C2="家長身心障礙", 0,
    D2="中低收入戶", INDEX('費用'!$B$1:$B$6, O2) - '費用'!$O$3,
    E2="家庭突發因素(幸福餐劵補助)", INDEX('費用'!$B$1:$B$6, O2) - '費用'!$O$3,
    LEFT(I2, 1)="客", INDEX('費用'!$B$1:$B$6, O2) + INDEX('費用'!$F$1:$F$6, VALUE(RIGHT(I2, 1))),
    OR(I2="原", I2="越", I2="泰"), INDEX('費用'!$B$1:$B$6, O2) - INDEX('費用'!$H$1:$H$6, O2),
    TRUE, INDEX('費用'!$B$1:$B$6, O2)
)

篩選 是否繳交 家長會費
=IF(OR(B2="低收入戶", D2="中低收入戶", E2="家庭突發因素(幸福餐劵補助)"), 0, IF(H2="家長會費", '費用'!$O$2, 0))

篩選 社團費用
=ARRAYFORMULA(IFERROR(VLOOKUP(TRIM(TEXT(O2:O121, "0")) & "-" & TRIM(TEXT(P2:P121, "0")) & "-" & TRIM(TEXT(Q2:Q121, "0")), {TRIM(TEXT('社團'!A1:A, "0")) & "-" & TRIM(TEXT('社團'!B1:B, "0")) & "-" & TRIM(TEXT('社團'!D1:D, "0")), '社團'!G1:G}, 2, FALSE), ""))

篩選 課後照顧班費用
=ARRAYFORMULA(IF(J2:J121=TRUE, IF(LEN(O2:O121), VLOOKUP(O2:O121, {ROW('費用'!L1:L6), '費用'!L1:L6}, 2, FALSE), ""), ""))

篩選 校外教學 費用
=ARRAYFORMULA(IFERROR(VLOOKUP(TRIM(TEXT(O2:O121, "0")) & "-" & TRIM(TEXT(P2:P121, "0")) & "-" & TRIM(TEXT(Q2:Q121, "0")), {TRIM(TEXT('社團'!A1:A, "0")) & "-" & TRIM(TEXT('校外教學'!B1:B, "0")) & "-" & TRIM(TEXT('校外教學'!D1:D, "0")), '校外教學'!G1:G}, 2, FALSE), ""))

11. 建議列印紙本,請導師簽名確認

Login.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>資料查詢系統</title>
<style>
  body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    background-color: #f0f2f5;
    margin: 0;
  }
  .container {
    background-color: #fff;
    padding: 2rem;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    box-sizing: border-box;
    margin: 2rem auto;
  }
  #login-container {
    max-width: 400px;
  }
  #data-container {
    display: none;
    width: 100%;
    max-width: none;
    margin: 0;
    border-radius: 0;
    box-shadow: none;
    padding: 1rem 1.5rem;
  }
  h2 { text-align: center; color: #333; margin-top:0; }
  label { font-weight: bold; margin-bottom: 0.5rem; display: block; }
  input[type="text"], input[type="password"] {
    width: 100%;
    padding: 0.75rem;
    margin-bottom: 1rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    box-sizing: border-box;
  }
  input[type="button"] {
    width: 100%;
    padding: 0.75rem;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 1rem;
    cursor: pointer;
    transition: background-color 0.2s;
  }
  input[type="button"]:hover { background-color: #0056b3; }
  #message { color: red; text-align: center; margin-top: 1rem; min-height: 1.2em; }
  #data-table-wrapper {
    overflow-x: auto;
    width: 100%;
  }
  table {
    border-collapse: collapse;
    width: 100%;
    margin-top: 0.4rem;
  }
  th, td {
    border: 1px solid #ddd;
    padding: 0.4rem 0.75rem;
    text-align: center;
    white-space: nowrap;
  }
  th {
    background-color: #f2f2f2;
    position: sticky;
    top: 0;
    z-index: 1;
  }

  /* --- 新增處:設定表格交錯顏色 --- */
  tbody tr:nth-child(even) {
    background-color: #f9f9f9; /* 為偶數行設定一個淡灰色背景 */
  }
</style>

  </head>
  <body>
    <div class="container" id="loading-container">
      <h2>資料查詢系統</h2>
      <p id="message">正在驗證您的身份並載入資料...</p>
    </div>

    <div class="container" id="data-container">
      <h2 id="welcome-message"></h2>
      <div id="data-table-wrapper"></div>
    </div>

    <script>
      // 當頁面載入完成後,立即執行
      window.addEventListener('load', function() {
        google.script.run
          .withSuccessHandler(onDataReceived)
          .withFailureHandler(onFailure)
          .getUserData(); // 直接呼叫新的後端函式
      });

      function onDataReceived(response) {
        document.getElementById('loading-container').style.display = 'none';

        if (response.success) {
          document.getElementById('data-container').style.display = 'block';
          document.getElementById('welcome-message').innerText = `歡迎,${response.user}。提醒:資料僅供老師參考,最後請以出組組長紙本為主!`;
         
          const wrapper = document.getElementById('data-table-wrapper');
          let html = '<table><thead><tr>';
          response.headers.forEach(header => html += `<th>${header}</th>`);
          html += '</tr></thead><tbody>';
          response.data.forEach(row => {
            html += '<tr>';
            row.forEach(cell => html += `<td>${cell}</td>`);
            html += '</tr>';
          });
          html += '</tbody></table>';
          wrapper.innerHTML = html;
        } else {
          // 如果驗證失敗或出錯,顯示錯誤訊息
          const messageDiv = document.getElementById('message');
          messageDiv.innerText = response.message;
          messageDiv.style.color = 'red';
          document.getElementById('loading-container').style.display = 'block';
        }
      }

      function onFailure(error) {
         const messageDiv = document.getElementById('message');
         messageDiv.innerText = "發生未預期的錯誤:" + error.message;
         messageDiv.style.color = 'red';
         document.getElementById('loading-container').style.display = 'block';
      }
    </script>
  </body>
</html>

程式碼.gs

const SPREADSHEET_ID = '試算表ID'; //填自己的試算表ID

function doGet() {
  return HtmlService.createHtmlOutputFromFile('Login')
    .setTitle('資料查詢系統');
}

function getUserData() {
  try {
    const userEmail = Session.getActiveUser().getEmail();
    if (!userEmail) {
      return { success: false, message: '無法識別您的 Google 帳號。請確認您已登入並授權。' };
    }
    const permissions = getUserPermissions(userEmail);
    if (!permissions) {
      return { success: false, message: `您的帳號 (${userEmail}) 未被授權存取本系統。` };
    }
    return fetchData(userEmail, permissions);
  } catch (e) {
    Logger.log('getUserData 錯誤: ' + e.toString());
    return { success: false, message: '發生未預期的錯誤。' };
  }
}

// --- 偵錯函式 (保留供未來使用) ---
function debugUserPermissions() {
  const testEmail = '您可以換成任何想測試的 Email'; // 您可以換成任何想測試的 Email

  Logger.log("--- 開始權限偵錯 ---");
  Logger.log("正在測試的 Email: " + testEmail);
  const permissions = getUserPermissions(testEmail);

  if (permissions === null) {
    Logger.log("偵錯結果: 找不到此 Email。");
    Logger.log("可能原因: 您在上面 testEmail 變數中填寫的 Email,與 Users 工作表 A 欄中的任何一筆 Email 都不相符。請仔細核對,前後不能有空格。");
  } else {
    Logger.log("偵錯結果: 成功找到 Email!");
    Logger.log("程式讀取到的 SheetName (B欄): '" + permissions.sheetName + "'");
    Logger.log("程式讀取到的 DataRange (C欄): '" + permissions.dataRange + "'");

    if (!permissions.sheetName || !permissions.dataRange) {
        Logger.log(">>> 問題判斷: SheetName 或 DataRange 其中一項為空值 (空白),這就是導致錯誤的原因! <<<");
    } else {
        Logger.log(">>> 資料看起來都存在,請仔細檢查引號中的內容是否有誤 (例如多餘的空格、名稱與工作表分頁不符)。");
    }
  }
  Logger.log("--- 偵錯結束 ---");
}


/**
 * 根據 Email 在 Users 工作表中查找權限設定。
 * (此函式無需修改)
 */
function getUserPermissions(email) {
  const USER_SHEET_NAME = 'Users';
  const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(USER_SHEET_NAME);
  const data = sheet.getDataRange().getValues();

  for (let i = 1; i < data.length; i++) {
    const storedEmail = data[i][0];
    if (storedEmail.toLowerCase() === email.toLowerCase()) {
      return {
        sheetName: data[i][1], // B欄: SheetName
        dataRange: data[i][2]  // C欄: DataRange
      };
    }
  }
  return null;
}

/**
 * [⭐️ 已升級] 根據權限設定,抓取並組合單一或多個不連續範圍的資料。
 * @param {string} userEmail - 當前使用者 Email (用於顯示歡迎訊息)。
 * @param {object} permissions - 包含 sheetName 和 dataRange 的物件。
 * @returns {object} 返回包含資料或錯誤訊息的物件。
 */
function fetchData(userEmail, permissions) {
  const { sheetName, dataRange } = permissions;
 
  try {
    if (!sheetName || !dataRange) {
      return { success: false, message: '您的帳號權限設定不正確(缺少工作表或範圍),請聯絡管理員。' };
    }

    const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(sheetName);
    if (!sheet) {
      return { success: false, message: `設定的資料表 "${sheetName}" 不存在。` };
    }

    // --- 核心升級:解析並處理單一或多個範圍 ---

    // 1. 將 dataRange 字串用分號分割成一個範圍陣列
    const rangeStrings = dataRange.split(';').map(r => r.trim()).filter(r => r);

    if (rangeStrings.length === 0) {
      return { success: false, message: 'DataRange 格式錯誤或為空。' };
    }

    // 2. 處理標題:
    // 如果是單一範圍 (如 K2:AB31),就智慧抓取第1列的對應標題。
    // 如果是多範圍 (如 A1:AC1;A20:AC51),就使用第一個範圍當作標題。
    let headers;
    const firstRange = sheet.getRange(rangeStrings[0]);
    if (rangeStrings.length === 1) {
      // 單一範圍模式
      const startCol = firstRange.getColumn();
      const numCols = firstRange.getNumColumns();
      headers = sheet.getRange(1, startCol, 1, numCols).getValues()[0];
    } else {
      // 多範圍模式,第一個範圍就是標題
      headers = firstRange.getValues()[0];
    }
   
    // 3. 獲取並合併所有資料範圍
    let allUserData = [];
    if (rangeStrings.length === 1) {
      // 單一範圍模式,第一個(也是唯一的)範圍就是資料
      allUserData = firstRange.getValues();
    } else {
      // 多範圍模式,從第二個範圍開始遍歷合併
      for (let i = 1; i < rangeStrings.length; i++) {
        const currentData = sheet.getRange(rangeStrings[i]).getValues();
        allUserData = allUserData.concat(currentData);
      }
    }
   
    return { success: true, user: userEmail.split('@')[0], headers: headers, data: allUserData };

  } catch(e) {
     Logger.log('fetchData 錯誤: ' + e.toString());
     if (e.message.includes("Range not found")) {
       return { success: false, message: `權限設定中的範圍 "${dataRange}" 包含無效的儲存格名稱,請聯絡管理員。`};
     }
     return { success: false, message: '讀取資料時發生錯誤。' };
  }
}

function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('🎯資料處理')
    .addItem('0️⃣🧺清空身份別資料', 'clearRange')
    .addItem('1️⃣⚙️分割身份別資料', 'splitOrCopyData')    
    .addSeparator()
    .addItem('2️⃣✅校對減免金額', 'calculateDiscounts')
    .addItem('3️⃣🧹清空AC欄位資料', 'clearABColumn')  
    .addSeparator()    
    .addItem('4️⃣🚀將資料複製到新工作表手動修改', 'copyFilteredRangeWithHeaderAndWidthManualName')
   
    .addToUi();
}

function splitOrCopyData() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var targetKeywords = ["原住民", "本人身心障礙(身心障礙生)", "家長身心障礙", "中低收入戶", "低收入戶", "家庭突發因素(幸福餐劵補助)"];

  for (var i = 1; i < data.length; i++) {
    var fColumnValue = data[i][5];
    if (typeof fColumnValue === 'string') {
      var splitValues = fColumnValue.includes(';') ? fColumnValue.split(';') : [fColumnValue]; // 如果沒有分號,將整個值放入陣列

      for (var j = 0; j < targetKeywords.length; j++) {
        var keyword = targetKeywords[j];
        if (splitValues.includes(keyword)) {
          sheet.getRange(i + 1, 7 + j).setValue(keyword);
        }
      }
    }
  }
}

function clearRange() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.getRange('G2:L').clearContent();
}

/**
 * @OnlyCurrentDoc
 *
 * 這個腳本用於根據學生資料和費用表來計算折扣金額。
 * 它會將結果寫入學生資料表中的指定欄位。
 */

function calculateDiscounts() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var feeSheet = ss.getSheetByName("費用");
  if (!feeSheet) {
    SpreadsheetApp.getUi().alert("找不到『費用』工作表!");
    return;
  }

  // --- 讀取 年級→教科書費用(費用!A:B) ---
  // 假設一年級~六年級都在前幾列,取到第20列以防萬一
  var gradeRows = feeSheet.getRange("A1:B20").getValues();
  var gradeFeeMap = {};
  gradeRows.forEach(function (r) {
    var g = (r[0] || "").toString().trim();
    var fee = Number(r[1]);
    if (g && !isNaN(fee)) gradeFeeMap[g] = fee;
  });

  // --- 新增:讀取 年級、客家費用、原住民/越南費用(費用!A:F:H) ---
  var gradeFeeRows = feeSheet.getRange("A1:H20").getValues();
  var gradeFeeFandHMap = {};
  gradeFeeRows.forEach(function (r) {
    var g = (r[0] || "").toString().trim();
    var fFee = Number(r[5]); // F欄的金額
    var hFee = Number(r[7]); // H欄的金額
    if (g) {
      gradeFeeFandHMap[g] = {
        'fFee': isNaN(fFee) ? 0 : fFee,
        'hFee': isNaN(hFee) ? 0 : hFee
      };
    }
  });


  // --- 讀取其他費用(費用!N:O) ---
  var feeRows = feeSheet.getRange("N1:O50").getValues(); // 包含標題也沒關係
  var fees = {};
  feeRows.forEach(function (r) {
    var name = (r[0] || "").toString().trim();
    var val = Number(r[1]);
    if (name && !isNaN(val)) fees[name] = val;
  });

  // 正規化年級:把 O 欄的「1 / 一年級 / 1年級」都轉成「一年級」來對照費用表
  function normalizeGrade(val) {
    var zh = ["零", "一", "二", "三", "四", "五", "六"];
    if (typeof val === "number") {
      var n = Math.floor(val);
      return (n >= 1 && n <= 6) ? zh[n] + "年級" : "";
    }
    var s = (val || "").toString().trim();
    // 含數字
    var m = s.match(/([1-6])/);
    if (m) return zh[Number(m[1])] + "年級";
    // 含中文數字
    for (var i = 1; i <= 6; i++) {
      if (s.indexOf(zh[i]) !== -1) return zh[i] + "年級";
    }
    // 已經是「一年級」這種
    if (/^[一二三四五六]年級$/.test(s)) return s;
    return "";
  }

  var lastRow = sheet.getLastRow();
  if (lastRow < 2) return;

  // --- A:G 身份別(7 欄,與每一列一一對應) ---
  var identities = sheet.getRange("A2:G" + lastRow).getValues();
  // --- O 欄 年級 ---
  var gradeCol = sheet.getRange("O2:O" + lastRow).getValues();
  // --- 新增:I 欄 類別 ---
  var categoryCol = sheet.getRange("I2:I" + lastRow).getValues();

  var results = [];

  for (var i = 0; i < gradeCol.length; i++) {
    var gradeKey = normalizeGrade(gradeCol[i][0]);
    if (!gradeKey) { results.push([""]); continue; }

    var textbookFee = Number(gradeFeeMap[gradeKey]) || 0;

    // A:G -> [特殊生, 低收入戶, 家庭突發因素, 本人身心障礙, 中低收入戶, 家長身心障礙, 原住民]
    var row = identities[i] || [];
    var isSpecialStudent = !!(row[0]);
    var isLowIncome = !!(row[1]);
    var isParentDisabled = !!(row[2]);
    var isMidLowIncome = !!(row[3]);
    var isSuddenFactor = !!(row[4]);
    var isDisabled = !!(row[5]);
    var isIndigenous = !!(row[6]);

    // 新增:讀取 I 欄值
    var category = (categoryCol[i][0] || "").toString().trim();

    var discounts = [];
    // 所有金額來自「費用」表,沒有就當 0
    var safe = function (k, d) { return (typeof fees[k] === "number" ? fees[k] : (d || 0)); };

    // 新增:根據 I 欄類別計算費用
    var newFeeInfo = gradeFeeFandHMap[gradeKey];
    if (newFeeInfo) {
      if (category === "客") {
        discounts.push({ type: "客語課本差額", amount: newFeeInfo.fFee, priority: 0 });
      } else if (category === "原" || category === "越") {
        discounts.push({ type: "原民族語課本/越語課本", amount: newFeeInfo.hFee, priority: 0 });
      }
    }


    if (isSpecialStudent) discounts.push({ type: "特殊生", amount: safe("特殊生補助", 0), priority: 1 });
    if (isLowIncome) discounts.push({ type: "低收入戶", amount: textbookFee + safe("平安保險", 0), priority: 2 });
    if (isSuddenFactor) discounts.push({ type: "家庭突發因素", amount: safe("註冊組代收代辦費", 0) + safe("平安保險", 0), priority: 3 });
    if (isDisabled) discounts.push({ type: "本人身心障礙", amount: safe("平安保險", 0) + safe("家長會費", 0), priority: 4 });
    if (isMidLowIncome) discounts.push({ type: "中低收入戶", amount: Math.min(textbookFee, 500) + safe("平安保險", 0) + safe("家長會費", 0), priority: 5 });
    if (isParentDisabled) discounts.push({ type: "家長身心障礙", amount: textbookFee, priority: 6 });
    if (isIndigenous) discounts.push({ type: "原住民", amount: safe("平安保險", 0), priority: 7 });

    if (discounts.length > 0) {
      var pick = discounts.reduce(function (a, b) { return (a.priority < b.priority) ? a : b; });
      results.push([pick.type + ": " + pick.amount + "元"]);
    } else {
      results.push([""]);
    }
  }

  // --- 寫入 AC 欄(用 A1 位置,避免欄位插入造成位移) ---
  sheet.getRange("AC2:AC" + (results.length + 1)).setValues(results);
}


function clearABColumn() {
  // 取得當前活動中的工作表
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
 
  // 定義要清除的範圍 AC2:AC
  const range = sheet.getRange("AC2:AC");
 
  // 清除範圍內的內容
  range.clearContent();
}

/**
 * 複製活頁簿中指定範圍和標題列到一個新工作表,
 * 只複製 N 欄有資料的列。同時保留原始欄位的寬度。
 * 新的工作表名稱由使用者手動輸入。
 */
function copyFilteredRangeWithHeaderAndWidthManualName() {
 
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var activeSheet = spreadsheet.getActiveSheet();
  var ui = SpreadsheetApp.getUi();
 
  // 取得目前工作表的最後一列
  var lastRow = activeSheet.getLastRow();
 
  if (lastRow < 1) {
    ui.alert('目前工作表沒有資料可以複製!');
    return;
  }

  // 1. 彈出對話框讓使用者輸入新工作表的名稱
  var response = ui.prompt('請為新的工作表命名:', ui.ButtonSet.OK_CANCEL);
 
  // 檢查使用者是否取消
  if (response.getSelectedButton() == ui.Button.CANCEL) {
    return;
  }
  var newSheetName = response.getResponseText();
 
  if (!newSheetName) {
    ui.alert('工作表名稱不能為空,腳本已結束。');
    return;
  }

  // 2. 取得原始欄位的寬度 (K到AC欄)
  // K欄是第11欄,AC欄是第29欄
  var sourceWidths = [];
  for (var i = 11; i <= 29; i++) {
    sourceWidths.push(activeSheet.getColumnWidth(i));
  }
 
  // 3. 取得標題列的範圍 (第一列,K到AC欄)
  // 19 是 K 到 AC 的總欄數 (29 - 11 + 1)
  var headerRange = activeSheet.getRange(1, 11, 1, 19);
  var headerValues = headerRange.getValues();
 
  // 4. 取得所有內容範圍 (從第二列到最後一列,K到AC欄)
  var bodyRange = activeSheet.getRange(2, 11, lastRow - 1, 19);
  var bodyValues = bodyRange.getValues();

  // 5. 篩選出 N 欄有資料的列
  // N 欄在 K 到 AC 這個範圍中的相對位置是第 4 欄 (26 - 22)
  var filteredBodyValues = bodyValues.filter(function(row) {
    // N 欄是第 4 個元素 (K=0, L=1, M=2, N=3)
    return row[3] && row[3].toString().trim() !== '';
  });
 
  if (filteredBodyValues.length === 0) {
    ui.alert('在 N 欄中沒有找到任何資料,腳本已結束。');
    return;
  }
 
  // 6. 將標題和篩選後的內容合併
  var allValues = headerValues.concat(filteredBodyValues);
 
  // 7. 創建一個新的工作表
  // 為了避免名稱重複,可以檢查是否已存在
  if (spreadsheet.getSheetByName(newSheetName)) {
    ui.alert('名稱為「' + newSheetName + '」的工作表已存在,請重新執行並輸入新的名稱。');
    return;
  }
  var newSheet = spreadsheet.insertSheet(newSheetName);
 
  // 8. 複製所有值到新工作表中
  newSheet.getRange(1, 1, allValues.length, allValues[0].length).setValues(allValues);
 
  // 9. 將取得的欄寬應用到新工作表上
  for (var i = 0; i < sourceWidths.length; i++) {
    newSheet.setColumnWidth(i + 1, sourceWidths[i]);
  }
 
  // 10. 提示使用者完成
  ui.alert('已成功複製 N 欄有資料的列到新的工作表:' + newSheetName + '。');
}




快來試試吧~





沒有留言:

張貼留言

歡迎大家一起留言討論!