diff --git a/AutoReturn.gs b/AutoReturn.gs
new file mode 100644
index 0000000..8452473
--- /dev/null
+++ b/AutoReturn.gs
@@ -0,0 +1,161 @@
+/**
+ * 「帙雲」02 - 自動發還課業
+ *
+ * 功用:將老師批改完畢、放在「03_老師回饋區」的檔案,自動歸類至「04_已發還課業」。
+ * 方法:提取檔案名稱中的【班別】、【姓名】及課業【關鍵詞】,並配對至對應的學生文件夾。
+ *
+ * 觸發器:distributeHomework,每 15 分鐘觸發一次。
+ * 執行 createReturnTrigger() 可自動建立觸發器。
+ *
+ * 注意:ROOT_FOLDER_ID、getConfig() 及 getOrCreateFolder() 定義於 Shared.gs。
+ */
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 主函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 將「03_老師回饋區」中的批改課業,自動歸類至「04_已發還課業」對應的學生文件夾。
+ *
+ * 「04_已發還課業」的文件夾結構:
+ * 04_已發還課業/
+ * 【1C】/
+ * 【陳大文】/
+ * 寫作(長文)/
+ * 《藏在泥土的【寶物】》/
+ * 閱讀/
+ * 寫作(實用文)/
+ */
+function distributeHomework() {
+ const config = getConfig();
+ const uploadFolderId = config.TEACHER_RETURN_FOLDER_ID; // 03_老師回饋區
+ const returnFolderId = config.RETURNED_FOLDER_ID; // 04_已發還課業
+
+ const uploadFolder = DriveApp.getFolderById(uploadFolderId);
+ const returnFolder = DriveApp.getFolderById(returnFolderId);
+
+ // 步驟 1:獲取班別資料夾並建立映射(鍵:1C, 4A, ...)
+ const classFolderIter = returnFolder.getFolders();
+ const classMap = {};
+ while (classFolderIter.hasNext()) {
+ const classFolder = classFolderIter.next();
+ const className = classFolder.getName(); // 例如 "【1C】"
+ const classKey = className.replace(/【|】/g, ''); // 提取 "1C"
+ classMap[classKey] = classFolder;
+ }
+
+ // 步驟 2:獲取學生資料夾並建立映射(鍵:classKey_studentKey)
+ const studentMap = {};
+ for (const classKey in classMap) {
+ const classFolder = classMap[classKey];
+ const studentFolderIter = classFolder.getFolders();
+ while (studentFolderIter.hasNext()) {
+ const studentFolder = studentFolderIter.next();
+ const studentName = studentFolder.getName(); // 例如 "【陳大文】"
+ const studentKey = studentName.replace(/【|】/g, ''); // 提取 "陳大文"
+ if (!studentMap[classKey]) studentMap[classKey] = {};
+ studentMap[classKey][studentKey] = studentFolder;
+ }
+ }
+
+ // 步驟 3:在每位學生的「寫作(長文)」子文件夾下,提取課業關鍵詞
+ const assignmentMap = {};
+ for (const classKey in studentMap) {
+ for (const studentKey in studentMap[classKey]) {
+ const studentFolder = studentMap[classKey][studentKey];
+ const writingFolderIter = studentFolder.getFoldersByName('寫作(長文)');
+ if (writingFolderIter.hasNext()) {
+ const writingFolder = writingFolderIter.next();
+ const assignmentFolderIter = writingFolder.getFolders();
+ while (assignmentFolderIter.hasNext()) {
+ const assignmentFolder = assignmentFolderIter.next();
+ const match = assignmentFolder.getName().match(/【(.*?)】/);
+ if (match) {
+ const keyword = match[1];
+ assignmentMap[classKey + '_' + studentKey + '_' + keyword] = assignmentFolder;
+ }
+ }
+ }
+ }
+ }
+
+ // 步驟 4:處理上傳資料夾中的所有檔案
+ const files = uploadFolder.getFiles();
+ while (files.hasNext()) {
+ const file = files.next();
+ const fileName = file.getName();
+
+ // 提取班別
+ let fileClassKey = null;
+ for (const ck in classMap) {
+ if (fileName.indexOf(ck) !== -1) {
+ fileClassKey = ck;
+ break;
+ }
+ }
+
+ // 提取姓名
+ let fileStudentKey = null;
+ if (fileClassKey && studentMap[fileClassKey]) {
+ for (const sk in studentMap[fileClassKey]) {
+ if (fileName.indexOf(sk) !== -1) {
+ fileStudentKey = sk;
+ break;
+ }
+ }
+ }
+
+ // 提取課業關鍵詞
+ let assignmentKeyword = null;
+ if (fileClassKey && fileStudentKey) {
+ for (const key in assignmentMap) {
+ const parts = key.split('_');
+ if (parts[0] === fileClassKey && parts[1] === fileStudentKey) {
+ const keyword = parts[2];
+ if (fileName.indexOf(keyword) !== -1) {
+ assignmentKeyword = keyword;
+ break;
+ }
+ }
+ }
+ }
+
+ // 步驟 5:根據匹配情況移動檔案
+ if (fileClassKey && fileStudentKey && assignmentKeyword) {
+ // 完整匹配:移動到課業資料夾
+ const targetFolder = assignmentMap[fileClassKey + '_' + fileStudentKey + '_' + assignmentKeyword];
+ file.moveTo(targetFolder);
+ } else if (fileClassKey && fileStudentKey) {
+ // 缺少課業名稱:移動到學生資料夾
+ file.moveTo(studentMap[fileClassKey][fileStudentKey]);
+ } else if (fileClassKey) {
+ // 只有班別:移動到班別資料夾
+ file.moveTo(classMap[fileClassKey]);
+ }
+ // 完全無法匹配,留在原地
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 輔助函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 觸發器設置
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 建立每 15 分鐘觸發一次 distributeHomework 的時間觸發器。
+ */
+function createReturnTrigger() {
+ ScriptApp.getProjectTriggers().forEach(function(trigger) {
+ if (trigger.getHandlerFunction() === 'distributeHomework') {
+ ScriptApp.deleteTrigger(trigger);
+ }
+ });
+ ScriptApp.newTrigger('distributeHomework')
+ .timeBased()
+ .everyMinutes(15)
+ .create();
+ Logger.log('✅ 已建立觸發器:distributeHomework,每 15 分鐘觸發一次。');
+}
diff --git a/AutoShare.gs b/AutoShare.gs
new file mode 100644
index 0000000..41b23d3
--- /dev/null
+++ b/AutoShare.gs
@@ -0,0 +1,116 @@
+/**
+ * 「帙雲」03 - 自動共用、收集位址
+ *
+ * 功用:將「04_已發還課業」中每位學生的專屬文件夾共用給學生,
+ * 並將文件夾位址記錄至「自動共用、收集位址」試算表的 C 欄,
+ * 方便批量分發給學生。
+ *
+ * 方法:在試算表中手動輸入學號(A 欄)及學生姓名(B 欄),
+ * 然後手動執行 shareAllClasses() 或針對特定班別執行 shareFoldersForClass()。
+ *
+ * 觸發器:不設觸發器,手動執行。
+ *
+ * 試算表格式(每個分頁對應一個班別):
+ * A1=學號, B1=姓名, C1=文件夾位址
+ * A2 起:實際學號, B2 起:實際姓名, C2 起(自動填入):文件夾 URL
+ *
+ * 注意:ROOT_FOLDER_ID、SCHOOL_EMAIL_DOMAIN、getConfig() 及 getOrCreateFolder()
+ * 定義於 Shared.gs,此處直接使用。
+ */
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 主函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 針對試算表中所有班別,共用學生專屬文件夾並收集位址。
+ * 試算表中每個分頁(Sheet)對應一個班別,分頁名稱即班別名稱(如 1C)。
+ */
+function shareAllClasses() {
+ const config = getConfig();
+ const spreadsheet = SpreadsheetApp.openById(config.SHARE_SHEET_ID);
+ const returnedFolder = DriveApp.getFolderById(config.RETURNED_FOLDER_ID);
+
+ // 取得「04_已發還課業」下所有班別文件夾
+ const classFolderIter = returnedFolder.getFolders();
+ while (classFolderIter.hasNext()) {
+ const classFolder = classFolderIter.next();
+ const classKey = classFolder.getName().replace(/【|】/g, ''); // 去除【】
+ Logger.log('處理班別:' + classKey);
+ shareFoldersForClass(classKey, classFolder, spreadsheet);
+ }
+}
+
+/**
+ * 針對特定班別,共用學生專屬文件夾並收集位址。
+ *
+ * @param {string} className 班別名稱,如 "1C"
+ * @param {GoogleAppsScript.Drive.Folder} [classFolderOverride] 可選,直接傳入班別文件夾
+ * @param {GoogleAppsScript.Spreadsheet.Spreadsheet} [spreadsheetOverride] 可選,直接傳入試算表
+ */
+function shareFoldersForClass(className, classFolderOverride, spreadsheetOverride) {
+ const config = getConfig();
+ const spreadsheet = spreadsheetOverride || SpreadsheetApp.openById(config.SHARE_SHEET_ID);
+ const returnedFolder = DriveApp.getFolderById(config.RETURNED_FOLDER_ID);
+
+ // 找到對應的班別文件夾(名稱格式為「【1C】」)
+ let classFolder = classFolderOverride;
+ if (!classFolder) {
+ const iter = returnedFolder.getFoldersByName('【' + className + '】');
+ if (!iter.hasNext()) {
+ throw new Error('找不到班別文件夾:【' + className + '】');
+ }
+ classFolder = iter.next();
+ }
+
+ // 取得或建立試算表分頁
+ let sheet = spreadsheet.getSheetByName(className);
+ if (!sheet) {
+ sheet = spreadsheet.insertSheet(className);
+ sheet.getRange('A1:C1').setValues([['學號', '姓名', '文件夾位址']]);
+ sheet.getRange('A1:C1').setFontWeight('bold');
+ }
+
+ // 獲取學號及姓名(A2:B 往下)
+ const lastRow = sheet.getLastRow();
+ if (lastRow < 2) {
+ Logger.log('班別 ' + className + ' 的試算表中尚未輸入學生資料,跳過。');
+ return;
+ }
+ const values = sheet.getRange('A2:B' + lastRow).getValues();
+ const students = values.filter(function(row) { return row[0] && row[1]; });
+
+ // 建立姓名→文件夾映射
+ const folderMap = {};
+ const studentFolderIter = classFolder.getFolders();
+ while (studentFolderIter.hasNext()) {
+ const folder = studentFolderIter.next();
+ const match = folder.getName().match(/【(.*?)】/);
+ if (match) folderMap[match[1]] = folder;
+ }
+
+ // 共用並填入 URL
+ students.forEach(function(student, index) {
+ const studentId = student[0];
+ const studentName = student[1];
+ const email = studentId + '@' + SCHOOL_EMAIL_DOMAIN;
+ const folder = folderMap[studentName];
+
+ if (folder) {
+ try {
+ folder.addEditor(email);
+ sheet.getRange(index + 2, 3).setValue(folder.getUrl());
+ Logger.log('已共用文件夾給 ' + studentName + ' (' + email + ')');
+ } catch (e) {
+ Logger.log('共用失敗:' + studentName + ' - ' + e.message);
+ }
+ } else {
+ Logger.log('找不到學生文件夾:' + studentName);
+ }
+ });
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 輔助函數
+// ─────────────────────────────────────────────────────────────────────────────
+
diff --git a/CollectHomework.gs b/CollectHomework.gs
new file mode 100644
index 0000000..b08f0e8
--- /dev/null
+++ b/CollectHomework.gs
@@ -0,0 +1,153 @@
+/**
+ * 「帙雲」01 - 收集功課
+ *
+ * 功用:將「01_學生上傳區」的檔案移至「02_待批改課業」,並自動歸類。
+ * 方法:提取檔案名稱中的班別(如 1C、4A)及子文件夾以「【】」括起的關鍵詞,並作配對。
+ *
+ * 觸發器:sortStudentAssignments,每 1 分鐘觸發一次。
+ * 執行 createCollectTrigger() 可自動建立觸發器。
+ *
+ * 注意:ROOT_FOLDER_ID、getConfig() 及 getOrCreateFolder() 定義於 Shared.gs。
+ */
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 主函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 將「01_學生上傳區」中的檔案,依班別及【關鍵詞】歸類至「02_待批改課業」。
+ * 支援的檔案格式:PDF、JPEG、PNG、GIF、BMP、WEBP。
+ */
+function sortStudentAssignments() {
+ const config = getConfig();
+ const sourceFolderId = config.UPLOAD_FOLDER_ID; // 01_學生上傳區
+ const targetFolderId = config.PENDING_FOLDER_ID; // 02_待批改課業
+
+ const supportedMimeTypes = [
+ MimeType.PDF,
+ MimeType.JPEG,
+ MimeType.PNG,
+ MimeType.GIF,
+ MimeType.BMP,
+ MimeType.WEBP
+ ];
+
+ // 取得目標文件夾下所有班別的文件夾結構(支援多層子文件夾)
+ const classFolders = getClassFoldersRecursive(targetFolderId);
+
+ const sourceFolder = DriveApp.getFolderById(sourceFolderId);
+ const allFiles = sourceFolder.getFiles();
+
+ while (allFiles.hasNext()) {
+ const file = allFiles.next();
+ const fileName = file.getName();
+ const fileMimeType = file.getMimeType();
+
+ // 過濾不支援的檔案格式
+ if (!supportedMimeTypes.includes(fileMimeType)) {
+ Logger.log('跳過不支援的檔案格式: ' + fileName);
+ continue;
+ }
+
+ // 提取班別資訊(配對如 1C、4A 等格式)
+ const classMatch = fileName.match(/(\d+[A-Z])/);
+ if (!classMatch) {
+ Logger.log('跳過無班別資訊的檔案: ' + fileName);
+ continue;
+ }
+ const className = classMatch[0];
+ const classInfo = classFolders[className];
+ if (!classInfo) {
+ Logger.log('未找到班別文件夾: ' + className);
+ continue;
+ }
+
+ let targetSubfolderId = null;
+ // 查找配對【關鍵詞】的子文件夾
+ for (const keyword in classInfo.keywordFolders) {
+ if (Object.prototype.hasOwnProperty.call(classInfo.keywordFolders, keyword) &&
+ fileName.includes(keyword)) {
+ targetSubfolderId = classInfo.keywordFolders[keyword];
+ break;
+ }
+ }
+
+ // 若沒有配對的關鍵詞,使用班別根文件夾
+ if (!targetSubfolderId) {
+ targetSubfolderId = classInfo.rootFolderId;
+ }
+
+ try {
+ const targetFolder = DriveApp.getFolderById(targetSubfolderId);
+ file.moveTo(targetFolder);
+ Logger.log('成功移動檔案: ' + fileName + ' → ' + targetFolder.getName());
+ } catch (e) {
+ Logger.log('移動檔案失敗: ' + fileName + ' - ' + e.message);
+ }
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 輔助函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 遞迴取得 parentFolderId 下所有班別文件夾及其【關鍵詞】子文件夾。
+ * @param {string} parentFolderId
+ * @returns {Object} 班別名稱 → { rootFolderId, keywordFolders }
+ */
+function getClassFoldersRecursive(parentFolderId) {
+ const parentFolder = DriveApp.getFolderById(parentFolderId);
+ const classFolderIter = parentFolder.getFolders();
+ const result = {};
+
+ while (classFolderIter.hasNext()) {
+ const classFolder = classFolderIter.next();
+ const className = classFolder.getName();
+ const keywordFolders = {};
+ collectKeywordsRecursive(classFolder, keywordFolders);
+
+ result[className] = {
+ rootFolderId: classFolder.getId(),
+ keywordFolders: keywordFolders
+ };
+ }
+
+ return result;
+}
+
+/**
+ * 遞迴收集 folder 下所有【關鍵詞】格式子文件夾的關鍵詞與 ID。
+ */
+function collectKeywordsRecursive(folder, keywordFolders) {
+ const subfolders = folder.getFolders();
+ while (subfolders.hasNext()) {
+ const subfolder = subfolders.next();
+ const keywordMatch = subfolder.getName().match(/【(.*?)】/);
+ if (keywordMatch) {
+ keywordFolders[keywordMatch[1]] = subfolder.getId();
+ }
+ collectKeywordsRecursive(subfolder, keywordFolders);
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 觸發器設置
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 建立每 1 分鐘觸發一次 sortStudentAssignments 的時間觸發器。
+ * 執行前會先刪除舊有的同名觸發器,避免重複。
+ */
+function createCollectTrigger() {
+ ScriptApp.getProjectTriggers().forEach(function(trigger) {
+ if (trigger.getHandlerFunction() === 'sortStudentAssignments') {
+ ScriptApp.deleteTrigger(trigger);
+ }
+ });
+ ScriptApp.newTrigger('sortStudentAssignments')
+ .timeBased()
+ .everyMinutes(1)
+ .create();
+ Logger.log('✅ 已建立觸發器:sortStudentAssignments,每 1 分鐘觸發一次。');
+}
diff --git a/Index.html b/Index.html
new file mode 100644
index 0000000..fa5923f
--- /dev/null
+++ b/Index.html
@@ -0,0 +1,180 @@
+
+
+
+
+
+ 帙雲 - 控制面板
+
+
+
+
+
+
+
+
diff --git a/OverdueAssignments.gs b/OverdueAssignments.gs
new file mode 100644
index 0000000..d079961
--- /dev/null
+++ b/OverdueAssignments.gs
@@ -0,0 +1,156 @@
+/**
+ * 「帙雲」06 - OverdueAssignments
+ *
+ * 功用:整理逾期未交課業名單,供 Microsoft Power Automate 自動追收功課使用。
+ * 方法:在「繳交紀錄及課業佈置」找出逾期未交的學生,
+ * 對照「自動共用、收集位址」試算表取得學生的 Teams 電郵,
+ * 將結果寫入「OverdueAssignments」試算表的「Overdue Assignments」分頁。
+ *
+ * 觸發器:generateOverdueAssignments,每 1 分鐘觸發一次。
+ * 執行 createTrigger() 可自動建立觸發器。
+ *
+ * ⚠️ 此腳本應綁定至「OverdueAssignments」試算表(Container-bound),
+ * 或以獨立腳本(Standalone)方式運行(已支援兩種方式)。
+ */
+
+// ─── 唯一需要手動設定的值 ────────────────────────────────────────────────────
+const ROOT_FOLDER_ID = 'YOUR_ROOT_FOLDER_ID_HERE'; // ← 只需填寫這個
+// 學生 Microsoft Teams 電郵域名(格式:學號@域名)
+const STUDENT_EMAIL_DOMAIN = 'ms.ccckyc.edu.hk'; // ← 按學校實際域名修改
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 從 Script Properties 讀取設定,若尚未設定則自動從根文件夾探索並儲存。
+ */
+function getConfig() {
+ const props = PropertiesService.getScriptProperties();
+ let config = props.getProperties();
+
+ if (!config.SUBMISSION_SHEET_ID || !config.SHARE_SHEET_ID || !config.OVERDUE_SHEET_ID) {
+ const root = DriveApp.getFolderById(ROOT_FOLDER_ID);
+
+ const subIter = root.getFilesByName('繳交紀錄及課業佈置');
+ if (!subIter.hasNext()) {
+ throw new Error('找不到「繳交紀錄及課業佈置」試算表,請先執行 setup/Setup.gs 中的 setup()。');
+ }
+ config.SUBMISSION_SHEET_ID = subIter.next().getId();
+
+ const shareIter = root.getFilesByName('自動共用、收集位址');
+ if (!shareIter.hasNext()) {
+ throw new Error('找不到「自動共用、收集位址」試算表,請先執行 setup/Setup.gs 中的 setup()。');
+ }
+ config.SHARE_SHEET_ID = shareIter.next().getId();
+
+ const overdueIter = root.getFilesByName('OverdueAssignments');
+ if (!overdueIter.hasNext()) {
+ throw new Error('找不到「OverdueAssignments」試算表,請先執行 setup/Setup.gs 中的 setup()。');
+ }
+ config.OVERDUE_SHEET_ID = overdueIter.next().getId();
+
+ props.setProperties({
+ SUBMISSION_SHEET_ID: config.SUBMISSION_SHEET_ID,
+ SHARE_SHEET_ID: config.SHARE_SHEET_ID,
+ OVERDUE_SHEET_ID: config.OVERDUE_SHEET_ID
+ });
+ Logger.log('✅ 已自動探索並儲存資源 ID。');
+ }
+
+ return config;
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 主函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 生成逾期未交課業名單並寫入「OverdueAssignments」試算表。
+ * 條件:狀態為「未繳交」且已超過截止日期。
+ */
+function generateOverdueAssignments() {
+ const config = getConfig();
+
+ // 從「自動共用、收集位址」取得學號→電郵映射
+ const studentSpreadsheet = SpreadsheetApp.openById(config.SHARE_SHEET_ID);
+ const studentSheet = studentSpreadsheet.getSheetByName('Sheet1')
+ || studentSpreadsheet.getSheets()[0];
+ const studentData = studentSheet.getRange('A2:B' + studentSheet.getLastRow()).getValues();
+
+ const studentMap = {};
+ studentData.forEach(function(row) {
+ const studentNumber = row[0];
+ const studentName = row[1];
+ if (studentNumber && studentName) {
+ studentMap[studentName] = studentNumber + '@' + STUDENT_EMAIL_DOMAIN;
+ }
+ });
+
+ // 開啟「OverdueAssignments」試算表,清空並重寫
+ const overdueSpreadsheet = SpreadsheetApp.openById(config.OVERDUE_SHEET_ID);
+ let overdueSheet = overdueSpreadsheet.getSheetByName('Overdue Assignments');
+ if (!overdueSheet) {
+ overdueSheet = overdueSpreadsheet.insertSheet('Overdue Assignments');
+ }
+ overdueSheet.clear();
+ overdueSheet.appendRow(['班別', '學生姓名', '學生電郵', '課業名稱', '截止日期']);
+
+ const currentDate = new Date();
+
+ // 讀取「繳交紀錄及課業佈置」各班別的繳交狀態
+ const submissionSpreadsheet = SpreadsheetApp.openById(config.SUBMISSION_SHEET_ID);
+ const sheets = submissionSpreadsheet.getSheets();
+
+ sheets.forEach(function(sheet) {
+ const className = sheet.getRange('A1').getValue().toString().trim();
+ if (!className) return;
+
+ const lastColumn = sheet.getLastColumn();
+ if (lastColumn < 2) return;
+
+ const assignmentNames = sheet.getRange(1, 2, 1, lastColumn - 1).getValues()[0];
+ const deadlines = sheet.getRange(2, 2, 1, lastColumn - 1).getValues()[0];
+ const studentNames = sheet.getRange('A4:A' + sheet.getLastRow()).getValues()
+ .flat().filter(String);
+
+ if (studentNames.length === 0) return;
+
+ const statuses = sheet.getRange(4, 2, studentNames.length, lastColumn - 1).getValues();
+
+ studentNames.forEach(function(student, rowIndex) {
+ assignmentNames.forEach(function(assignment, colIndex) {
+ if (!assignment) return;
+ const status = statuses[rowIndex][colIndex];
+ const deadlineStr = deadlines[colIndex];
+ const deadline = new Date(deadlineStr);
+
+ if (status === '未繳交' && currentDate > deadline) {
+ const studentEmail = studentMap[student];
+ if (studentEmail) {
+ overdueSheet.appendRow([className, student, studentEmail, assignment, deadlineStr]);
+ }
+ }
+ });
+ });
+ });
+
+ Logger.log('✅ 已更新逾期課業名單。');
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 觸發器設置
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 建立每 1 分鐘觸發一次 generateOverdueAssignments 的時間觸發器。
+ */
+function createTrigger() {
+ ScriptApp.getProjectTriggers().forEach(function(trigger) {
+ if (trigger.getHandlerFunction() === 'generateOverdueAssignments') {
+ ScriptApp.deleteTrigger(trigger);
+ }
+ });
+ ScriptApp.newTrigger('generateOverdueAssignments')
+ .timeBased()
+ .everyMinutes(1)
+ .create();
+ Logger.log('✅ 已建立觸發器:generateOverdueAssignments,每 1 分鐘觸發一次。');
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..51c0775
--- /dev/null
+++ b/README.md
@@ -0,0 +1,251 @@
+# 帙雲 (ZhiYun) — Homework Management System
+
+> A Hong Kong school homework management platform built on **Google Apps Script** + **Google Drive** + **Microsoft Power Automate**.
+
+---
+
+## 📖 Introduction
+
+**帙雲** (ZhiYun) is a teacher-facing homework management system for Hong Kong secondary schools. It automates the entire homework lifecycle — from collection and sorting, to tracking submissions and chasing overdue work — so teachers can focus on teaching rather than administration.
+
+### ✨ Features
+
+1. **一鍵下載全班課業** — One-click access to download all homework submitted by the whole class.
+2. **一鍵發還批改課業** — One-click return of marked homework to students' personal Google Drive folders.
+3. **實時繳交狀態追蹤** — Real-time colour-coded tracking of each student's submission status (已繳交 ✅ / 未繳交 🔴 / 遲交 🟡).
+4. **自動歸類文件夾** — Auto-categorise files by class → category → assignment, and by student → category → assignment.
+5. **自動追收逾期功課** — Auto-chase overdue homework via Microsoft Teams (morning, noon, and evening reminders) using Power Automate.
+
+---
+
+## 🏗️ Architecture
+
+```
+Google Drive (Root Folder)
+├── 01_學生上傳區 ← Students upload homework here
+├── 02_待批改課業 ← Auto-sorted by class/category/assignment
+│ └── 1C/
+│ ├── 閱讀/
+│ ├── 寫作(長文)/
+│ │ └── 藏在泥土的【寶物】/
+│ └── 寫作(實用文)/
+├── 03_老師回饋區 ← Teacher places marked work here
+└── 04_已發還課業 ← Auto-sorted by class/student/category/assignment
+ └── 【1C】/
+ └── 【陳大文】/
+ ├── 閱讀/
+ ├── 寫作(長文)/
+ └── 寫作(實用文)/
+
+Google Sheets (inside root folder)
+├── 自動共用、收集位址 ← Student ID → personal folder URL
+├── 繳交紀錄及課業佈置 ← Submission records per class (multi-sheet)
+└── OverdueAssignments ← Overdue list for Power Automate
+
+Google Apps Script Project (single project, all files at root)
+├── Setup.gs ← Run ONCE to create all folders & sheets
+├── CollectHomework.gs ← Sorts uploaded files (1-min trigger)
+├── AutoReturn.gs ← Returns marked files (15-min trigger)
+├── AutoShare.gs ← Shares student folders & collects URLs (manual)
+├── SubmissionRecord.gs ← Creates folders & tracks submission (5-min trigger)
+├── WebInterface.gs ← Web App backend
+├── Index.html ← Control panel
+├── record.html ← Submission records viewer
+└── homework.html ← Homework assignment form
+(OverdueAssignments.gs is deployed in a second project bound to the OverdueAssignments sheet)
+```
+
+> ⚠️ **Google Apps Script does not support subfolders.** All `.gs` and `.html` files must be at the root level of the Apps Script project.
+
+---
+
+## ✅ Prerequisites
+
+| Requirement | Details |
+|---|---|
+| Google Account | With Google Drive and Google Sheets access |
+| Google Apps Script | [script.google.com](https://script.google.com) |
+| Microsoft Account | With Power Automate (Flow) access |
+| School domains | Google email: e.g. `ccckyc.edu.hk` / Teams email: e.g. `ms.ccckyc.edu.hk` |
+
+---
+
+## 🚀 Quick Start
+
+### Step 1 — Create the Root Folder
+
+1. Open [Google Drive](https://drive.google.com).
+2. Create a new folder (e.g. `帙雲`).
+3. Open the folder and copy its **ID** from the URL:
+ ```
+ https://drive.google.com/drive/folders/<>
+ ```
+
+### Step 2 — Run the Setup Script
+
+1. Go to [script.google.com](https://script.google.com) and create a new **standalone** project named `帙雲`.
+2. In the project, add all files from this repository (one by one using the **+** button). Google Apps Script does not support subfolders — all files must be at the project root.
+3. In `Shared.gs`, replace `YOUR_ROOT_FOLDER_ID_HERE` with your root folder ID:
+ ```javascript
+ const ROOT_FOLDER_ID = 'your_actual_folder_id_here';
+ ```
+4. Also in `Shared.gs`, update `SCHOOL_EMAIL_DOMAIN` to your school's Google email domain.
+5. Click **Run** → `setup`.
+6. Check **View → Logs**. All created folder IDs, spreadsheet IDs, and URLs will be printed — **keep this for reference**.
+
+> ✅ After this step, four folders and three spreadsheets will have been automatically created inside your root folder.
+
+### Step 3 — Add All Scripts to the Apps Script Project
+
+The **main** `帙雲` project contains all files except `OverdueAssignments.gs`. All shared constants (`ROOT_FOLDER_ID`, `SCHOOL_EMAIL_DOMAIN`, etc.) and shared utilities (`getConfig()`, `getOrCreateFolder()`) are defined **once** in `Shared.gs`.
+
+**Server-side scripts (`.gs`):**
+
+| File | Description | Trigger function |
+|---|---|---|
+| `Shared.gs` | Shared constants & utilities | — |
+| `Setup.gs` | One-time setup | Run `setup()` once manually |
+| `CollectHomework.gs` | Collect & sort student homework | `createCollectTrigger()` → every 1 min |
+| `AutoReturn.gs` | Auto-return marked homework | `createReturnTrigger()` → every 15 min |
+| `AutoShare.gs` | Share student folders & collect URLs | No trigger (run `shareAllClasses()` manually) |
+| `SubmissionRecord.gs` | Submission tracking & folder creation | `createSubmissionTrigger()` → every 5 min |
+| `WebInterface.gs` | Web App backend | Web App deployment |
+
+**HTML templates (`.html`):**
+
+| File | Description |
+|---|---|
+| `Index.html` | Control panel |
+| `record.html` | Submission records viewer |
+| `homework.html` | Homework assignment form |
+| `setup.html` | Class & student management panel |
+
+**For the OverdueAssignments script:**
+1. Create a **second, separate** Apps Script project named `帙雲_OverdueAssignments`.
+2. Add `OverdueAssignments.gs` (rename to `Code` in the editor).
+3. Set `ROOT_FOLDER_ID` and `STUDENT_EMAIL_DOMAIN` at the top of the file.
+4. Run `createTrigger()` to set up the 1-min trigger.
+
+**For the main project:**
+1. All configuration is in `Shared.gs` — set `ROOT_FOLDER_ID` and `SCHOOL_EMAIL_DOMAIN` there.
+2. Run `createCollectTrigger()`, `createReturnTrigger()`, and `createSubmissionTrigger()` once each.
+3. Run `shareAllClasses()` manually from `AutoShare.gs` when needed.
+
+### Step 4 — Deploy the Web Interface
+
+1. In the `帙雲` Apps Script project, click **Deploy** → **New deployment**.
+2. Select **Web app**.
+3. Set **Execute as**: Me (your account).
+4. Set **Who has access**: Anyone within your organization (or Anyone if needed).
+5. Click **Deploy** and copy the **Web app URL**.
+
+---
+
+## 📊 Spreadsheet Setup
+
+### 繳交紀錄及課業佈置
+
+Class tabs and student lists are now managed via the **班別及學生管理** web panel (`?page=setup`). You no longer need to edit the spreadsheet directly for this.
+
+Each **sheet tab** = one class, managed automatically by the web panel.
+
+| Cell | Content |
+|---|---|
+| A1 | Class name (e.g. `1C`) — set by web panel |
+| A2 | Leave blank initially — auto-filled with `created` after folders are built |
+| B1, C1 … | Homework name — set via `homework.html` web panel |
+| B2, C2 … | Deadline — set via `homework.html` web panel |
+| B3, C3 … | Folder ID — **auto-filled**, do not edit |
+| A4 onwards | Student names — set by web panel |
+| B4 onwards | Submission status — **auto-updated**: 已繳交 / 未繳交 / 遲交 |
+
+**Homework name format explained:**
+- `「寫作(長文)」` — category in `「」` brackets (used to sort files into correct category subfolder)
+- `【寶物】` — keyword in `【】` brackets (used to match uploaded filenames to the correct assignment folder)
+
+### 自動共用、收集位址
+
+Student IDs and names are now managed via the **班別及學生管理** web panel (`?page=setup` → 學生帳號管理 tab). You no longer need to edit the spreadsheet directly.
+
+| Column | Content |
+|---|---|
+| A | Student ID (學號) |
+| B | Student name (姓名) |
+| C | Personal folder URL — auto-filled by `AutoShare.gs` |
+
+After entering student IDs and names via the web panel, run `shareAllClasses()` in `AutoShare.gs` to share folders and populate column C.
+
+---
+
+## 🌐 Web Interface Pages
+
+| URL parameter | Page | Purpose |
+|---|---|---|
+| (none) | `Index.html` | Control panel with links to all folders and tools |
+| `?page=record` | `record.html` | View colour-coded submission records by class |
+| `?page=homework` | `homework.html` | Assign new homework (updates the spreadsheet) |
+| `?page=setup` | `setup.html` | Add classes, manage student lists and student accounts |
+
+---
+
+## ⚙️ Microsoft Power Automate Setup
+
+Set up a **Scheduled cloud flow** to send Teams messages to overdue students:
+
+1. **Recurrence** — Set your schedule (e.g. daily at 08:00, 12:00, 20:00).
+2. **Get worksheet** — Connect to the `OverdueAssignments` Google Sheets file.
+ > ⚠️ The spreadsheet **file** is named `OverdueAssignments` (no space); the **sheet tab** inside it is named `Overdue Assignments` (with a space).
+3. **List rows present in a table** — Select the `Overdue Assignments` sheet tab. Read all rows.
+4. **Apply to each** — Loop over each row.
+5. **Post message in a chat or channel** — Send a Teams message to the student's email address from the `學生電郵` column, mentioning the homework name and deadline.
+
+> **Tip:** In the Power Automate editor, type `/` to insert dynamic content from the spreadsheet columns.
+
+---
+
+## 📁 File / Folder Naming Conventions
+
+| Format | Example | Meaning |
+|---|---|---|
+| Class code in filename | `1C 陳大文 寶物.pdf` | Used by scripts to identify class |
+| `【keyword】` in filename | `1C 陳大文 【寶物】.pdf` | Used for assignment folder matching |
+| `【ClassName】` folder | `【1C】` | Class folder inside 04_已發還課業 |
+| `【StudentName】` folder | `【陳大文】` | Student folder inside class folder |
+| `「Category」` in homework name | `「寫作(長文)」` | Category prefix in spreadsheet cell |
+
+---
+
+## 🔧 Troubleshooting
+
+| Problem | Solution |
+|---|---|
+| `ROOT_FOLDER_ID` error on `setup()` | Ensure you replaced `YOUR_ROOT_FOLDER_ID_HERE` with your actual folder ID |
+| Files not moving to `02_待批改課業` | Check the filename contains a valid class code (e.g. `1C`, `4A`) |
+| Submission status not updating | Ensure `SubmissionRecord.gs` trigger is running; check folder IDs in row 3 of the sheet |
+| Student folder not shared | Ensure student names in the spreadsheet exactly match the `【name】` folder names |
+| Overdue list empty | Check that `OverdueAssignments.gs` trigger is running and `STUDENT_EMAIL_DOMAIN` is correct |
+| Web App not loading | Re-deploy the Web App after code changes; check execution permissions |
+
+---
+
+## 🗂️ Repository Structure
+
+> ⚠️ Google Apps Script **does not support subfolders**. All files must be at the root of the Apps Script project. This repository mirrors that flat structure exactly.
+
+```
+/
+├── Shared.gs ← Shared constants & utilities (ROOT_FOLDER_ID, getConfig, etc.)
+├── Setup.gs ← One-time setup (creates Drive folders + Sheets)
+├── CollectHomework.gs ← Collect & sort student homework (1-min trigger)
+├── AutoReturn.gs ← Auto-return marked homework (15-min trigger)
+├── AutoShare.gs ← Share student folders & collect URLs (manual)
+├── SubmissionRecord.gs ← Submission tracking & folder creation (5-min trigger)
+├── WebInterface.gs ← Web App backend (doGet, data helpers, setup panel API)
+├── OverdueAssignments.gs ← Generate overdue list for Power Automate (1-min trigger, separate project)
+├── Index.html ← Control panel (Web App page)
+├── record.html ← Submission records viewer (Web App page)
+├── homework.html ← Homework assignment form (Web App page)
+├── setup.html ← Class & student management panel (Web App page)
+├── Draft.md ← Original design draft (for reference)
+└── README.md ← This file
+```
diff --git a/Setup.gs b/Setup.gs
new file mode 100644
index 0000000..fee96a2
--- /dev/null
+++ b/Setup.gs
@@ -0,0 +1,154 @@
+/**
+ * 「帙雲」一次性設置腳本
+ *
+ * 使用方法:
+ * 1. 在 Google Drive 建立一個根文件夾(例如「帙雲」),複製其 ID。
+ * 2. 將 Shared.gs 中的 ROOT_FOLDER_ID 替換為你的根文件夾 ID。
+ * 3. 在 Apps Script 編輯器中執行 setup()。
+ * 4. 查看執行紀錄(View → Logs),複製所有 ID 以供其他腳本使用。
+ *
+ * 注意:ROOT_FOLDER_ID、FOLDER_NAMES、SHEET_NAMES、PROP_KEYS 及 getOrCreateFolder()
+ * 定義於 Shared.gs,此處直接使用。
+ */
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 主入口
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 一次性設置函數。
+ * 在根文件夾下建立四個文件夾及三個試算表,並將所有 ID 儲存至 Script Properties。
+ */
+function setup() {
+ if (ROOT_FOLDER_ID === 'YOUR_ROOT_FOLDER_ID_HERE') {
+ throw new Error(
+ '❌ 請先將 Shared.gs 中的 ROOT_FOLDER_ID 替換為你的 Google Drive 根文件夾 ID,然後再執行 setup()。'
+ );
+ }
+
+ const root = DriveApp.getFolderById(ROOT_FOLDER_ID);
+ Logger.log('✅ 根文件夾:' + root.getName() + ' (' + ROOT_FOLDER_ID + ')');
+
+ // 建立文件夾
+ const uploadFolder = getOrCreateFolder(root, FOLDER_NAMES.UPLOAD);
+ const pendingFolder = getOrCreateFolder(root, FOLDER_NAMES.PENDING);
+ const teacherReturnFolder = getOrCreateFolder(root, FOLDER_NAMES.TEACHER_RETURN);
+ const returnedFolder = getOrCreateFolder(root, FOLDER_NAMES.RETURNED);
+
+ // 建立試算表
+ const shareSS = getOrCreateSpreadsheet(root, SHEET_NAMES.SHARE);
+ const submissionSS = getOrCreateSpreadsheet(root, SHEET_NAMES.SUBMISSION);
+ const overdueSS = getOrCreateSpreadsheet(root, SHEET_NAMES.OVERDUE);
+
+ // 初始化試算表標頭
+ setupShareSheet(shareSS);
+ setupOverdueSheet(overdueSS);
+
+ // 儲存所有 ID 至 Script Properties
+ const props = PropertiesService.getScriptProperties();
+ props.setProperties({
+ [PROP_KEYS.ROOT_FOLDER_ID]: ROOT_FOLDER_ID,
+ [PROP_KEYS.UPLOAD_FOLDER_ID]: uploadFolder.getId(),
+ [PROP_KEYS.PENDING_FOLDER_ID]: pendingFolder.getId(),
+ [PROP_KEYS.TEACHER_RETURN_FOLDER_ID]: teacherReturnFolder.getId(),
+ [PROP_KEYS.RETURNED_FOLDER_ID]: returnedFolder.getId(),
+ [PROP_KEYS.SHARE_SHEET_ID]: shareSS.getId(),
+ [PROP_KEYS.SUBMISSION_SHEET_ID]: submissionSS.getId(),
+ [PROP_KEYS.OVERDUE_SHEET_ID]: overdueSS.getId()
+ });
+
+ Logger.log('✅ 所有 ID 已儲存至 Script Properties。');
+ printSummary();
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 輔助函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 在 parent 文件夾下取得或建立名為 name 的試算表(冪等)。
+ */
+function getOrCreateSpreadsheet(parent, name) {
+ const iter = parent.getFilesByName(name);
+ if (iter.hasNext()) {
+ const file = iter.next();
+ Logger.log('📊 已存在試算表:' + name + ' (' + file.getId() + ')');
+ return SpreadsheetApp.openById(file.getId());
+ }
+ const ss = SpreadsheetApp.create(name);
+ // 將新建的試算表移至根文件夾
+ const ssFile = DriveApp.getFileById(ss.getId());
+ parent.addFile(ssFile);
+ DriveApp.getRootFolder().removeFile(ssFile);
+ Logger.log('📊 已建立試算表:' + name + ' (' + ss.getId() + ')');
+ return ss;
+}
+
+/**
+ * 初始化「自動共用、收集位址」試算表的標頭列。
+ * A1=學號, B1=姓名, C1=文件夾位址
+ */
+function setupShareSheet(ss) {
+ const sheet = ss.getSheets()[0];
+ sheet.setName('Sheet1');
+ if (!sheet.getRange('A1').getValue()) {
+ sheet.getRange('A1:C1').setValues([['學號', '姓名', '文件夾位址']]);
+ sheet.getRange('A1:C1').setFontWeight('bold');
+ Logger.log('📝 已設置「自動共用、收集位址」標頭。');
+ }
+}
+
+/**
+ * 初始化「OverdueAssignments」試算表,建立「Overdue Assignments」頁面並設定標頭。
+ * 欄位:班別, 學生姓名, 學生電郵, 課業名稱, 截止日期
+ */
+function setupOverdueSheet(ss) {
+ let sheet = ss.getSheetByName('Overdue Assignments');
+ if (!sheet) {
+ sheet = ss.insertSheet('Overdue Assignments');
+ const defaultSheet = ss.getSheetByName('Sheet1');
+ if (defaultSheet && ss.getSheets().length > 1) {
+ ss.deleteSheet(defaultSheet);
+ }
+ }
+ if (!sheet.getRange('A1').getValue()) {
+ sheet.getRange('A1:E1').setValues([['班別', '學生姓名', '學生電郵', '課業名稱', '截止日期']]);
+ sheet.getRange('A1:E1').setFontWeight('bold');
+ Logger.log('📝 已設置「OverdueAssignments」標頭。');
+ }
+}
+
+/**
+ * 印出所有已建立資源的 ID 及 URL。
+ */
+function printSummary() {
+ const props = PropertiesService.getScriptProperties().getProperties();
+ Logger.log('\n══════════════════════════════════════════════');
+ Logger.log(' 「帙雲」設置摘要');
+ Logger.log('══════════════════════════════════════════════');
+ Logger.log('📌 請將以下 ROOT_FOLDER_ID 複製到各腳本中:');
+ Logger.log(' ROOT_FOLDER_ID = ' + ROOT_FOLDER_ID);
+ Logger.log('');
+ Logger.log('📁 文件夾 ID:');
+ Logger.log(' 01_學生上傳區 UPLOAD_FOLDER_ID = ' + props[PROP_KEYS.UPLOAD_FOLDER_ID]);
+ Logger.log(' 02_待批改課業 PENDING_FOLDER_ID = ' + props[PROP_KEYS.PENDING_FOLDER_ID]);
+ Logger.log(' 03_老師回饋區 TEACHER_RETURN_FOLDER_ID = ' + props[PROP_KEYS.TEACHER_RETURN_FOLDER_ID]);
+ Logger.log(' 04_已發還課業 RETURNED_FOLDER_ID = ' + props[PROP_KEYS.RETURNED_FOLDER_ID]);
+ Logger.log('');
+ Logger.log('📊 試算表 ID:');
+ Logger.log(' 自動共用、收集位址 SHARE_SHEET_ID = ' + props[PROP_KEYS.SHARE_SHEET_ID]);
+ Logger.log(' 繳交紀錄及課業佈置 SUBMISSION_SHEET_ID = ' + props[PROP_KEYS.SUBMISSION_SHEET_ID]);
+ Logger.log(' OverdueAssignments OVERDUE_SHEET_ID = ' + props[PROP_KEYS.OVERDUE_SHEET_ID]);
+ Logger.log('');
+ Logger.log('🔗 文件夾連結:');
+ Logger.log(' 學生上傳區:https://drive.google.com/drive/folders/' + props[PROP_KEYS.UPLOAD_FOLDER_ID]);
+ Logger.log(' 待批改課業:https://drive.google.com/drive/folders/' + props[PROP_KEYS.PENDING_FOLDER_ID]);
+ Logger.log(' 老師回饋區:https://drive.google.com/drive/folders/' + props[PROP_KEYS.TEACHER_RETURN_FOLDER_ID]);
+ Logger.log(' 已發還課業:https://drive.google.com/drive/folders/' + props[PROP_KEYS.RETURNED_FOLDER_ID]);
+ Logger.log('');
+ Logger.log('🔗 試算表連結:');
+ Logger.log(' 自動共用:https://docs.google.com/spreadsheets/d/' + props[PROP_KEYS.SHARE_SHEET_ID]);
+ Logger.log(' 繳交紀錄:https://docs.google.com/spreadsheets/d/' + props[PROP_KEYS.SUBMISSION_SHEET_ID]);
+ Logger.log(' 逾期課業:https://docs.google.com/spreadsheets/d/' + props[PROP_KEYS.OVERDUE_SHEET_ID]);
+ Logger.log('══════════════════════════════════════════════');
+}
diff --git a/Shared.gs b/Shared.gs
new file mode 100644
index 0000000..975c8cd
--- /dev/null
+++ b/Shared.gs
@@ -0,0 +1,106 @@
+/**
+ * 「帙雲」共用常數與工具函數
+ *
+ * 在 Google Apps Script 中,同一專案的所有 .gs 檔案共用同一全域範疇。
+ * 將共用常數與函數集中於此,避免重複宣告而導致的錯誤。
+ *
+ * ⚠️ 部署前,請將下方 ROOT_FOLDER_ID 替換為你的 Google Drive 根文件夾 ID。
+ *
+ * 注意:OverdueAssignments.gs 部署於獨立的 Apps Script 專案,
+ * 因此不共用此檔案,並自行宣告所需常數。
+ */
+
+// ─── 需要手動設定的值 ─────────────────────────────────────────────────────────
+
+/** Google Drive 根文件夾 ID(所有腳本共用) */
+const ROOT_FOLDER_ID = 'YOUR_ROOT_FOLDER_ID_HERE';
+
+/** 學生 Google 帳號電郵域名(AutoShare.gs 使用) */
+const SCHOOL_EMAIL_DOMAIN = 'ccckyc.edu.hk';
+
+// ─────────────────────────────────────────────────────────────────────────────
+
+/** Drive 文件夾名稱常數 */
+const FOLDER_NAMES = {
+ UPLOAD: '01_學生上傳區',
+ PENDING: '02_待批改課業',
+ TEACHER_RETURN: '03_老師回饋區',
+ RETURNED: '04_已發還課業'
+};
+
+/** 試算表名稱常數 */
+const SHEET_NAMES = {
+ SHARE: '自動共用、收集位址',
+ SUBMISSION: '繳交紀錄及課業佈置',
+ OVERDUE: 'OverdueAssignments'
+};
+
+/** Script Properties 鍵名 */
+const PROP_KEYS = {
+ ROOT_FOLDER_ID: 'ROOT_FOLDER_ID',
+ UPLOAD_FOLDER_ID: 'UPLOAD_FOLDER_ID',
+ PENDING_FOLDER_ID: 'PENDING_FOLDER_ID',
+ TEACHER_RETURN_FOLDER_ID: 'TEACHER_RETURN_FOLDER_ID',
+ RETURNED_FOLDER_ID: 'RETURNED_FOLDER_ID',
+ SHARE_SHEET_ID: 'SHARE_SHEET_ID',
+ SUBMISSION_SHEET_ID: 'SUBMISSION_SHEET_ID',
+ OVERDUE_SHEET_ID: 'OVERDUE_SHEET_ID'
+};
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 共用函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 從 Script Properties 讀取設定。
+ * 若必要的 ID 尚未儲存,則自動從根文件夾探索並儲存至 Script Properties。
+ * @returns {Object} 包含所有資源 ID 的設定物件
+ */
+function getConfig() {
+ const props = PropertiesService.getScriptProperties();
+ let config = props.getProperties();
+
+ const needsDiscovery = !config.UPLOAD_FOLDER_ID ||
+ !config.PENDING_FOLDER_ID ||
+ !config.TEACHER_RETURN_FOLDER_ID ||
+ !config.RETURNED_FOLDER_ID ||
+ !config.SUBMISSION_SHEET_ID;
+
+ if (needsDiscovery) {
+ const root = DriveApp.getFolderById(ROOT_FOLDER_ID);
+
+ config.UPLOAD_FOLDER_ID = getOrCreateFolder(root, FOLDER_NAMES.UPLOAD).getId();
+ config.PENDING_FOLDER_ID = getOrCreateFolder(root, FOLDER_NAMES.PENDING).getId();
+ config.TEACHER_RETURN_FOLDER_ID = getOrCreateFolder(root, FOLDER_NAMES.TEACHER_RETURN).getId();
+ config.RETURNED_FOLDER_ID = getOrCreateFolder(root, FOLDER_NAMES.RETURNED).getId();
+
+ const shareIter = root.getFilesByName(SHEET_NAMES.SHARE);
+ config.SHARE_SHEET_ID = shareIter.hasNext() ? shareIter.next().getId() : '';
+
+ const subIter = root.getFilesByName(SHEET_NAMES.SUBMISSION);
+ if (subIter.hasNext()) {
+ config.SUBMISSION_SHEET_ID = subIter.next().getId();
+ } else {
+ throw new Error(
+ '找不到「' + SHEET_NAMES.SUBMISSION + '」試算表,請先執行 setup()。'
+ );
+ }
+
+ props.setProperties(config);
+ Logger.log('✅ 已自動探索並儲存資源 ID。');
+ }
+
+ return config;
+}
+
+/**
+ * 在 parent 文件夾下取得或建立名為 name 的子文件夾(冪等)。
+ * @param {GoogleAppsScript.Drive.Folder} parent
+ * @param {string} name
+ * @returns {GoogleAppsScript.Drive.Folder}
+ */
+function getOrCreateFolder(parent, name) {
+ const iter = parent.getFoldersByName(name);
+ if (iter.hasNext()) return iter.next();
+ return parent.createFolder(name);
+}
diff --git a/SubmissionRecord.gs b/SubmissionRecord.gs
new file mode 100644
index 0000000..6af16ae
--- /dev/null
+++ b/SubmissionRecord.gs
@@ -0,0 +1,227 @@
+/**
+ * 「帙雲」04 - 繳交紀錄及課業佈置
+ *
+ * 功用:
+ * 1. 自動生成「02_待批改課業」及「04_已發還課業」的分層文件夾結構。
+ * 2. 實時追蹤學生繳交課業的狀態(已繳交 / 未繳交 / 遲交)。
+ *
+ * 試算表格式(每個分頁對應一個班別):
+ * A1 = 班別名稱(如 1C)
+ * A2 = "created"(文件夾建立後自動填入,請勿更改)
+ * B1, C1... = 課業名稱,格式:「寫作(長文)」藏在泥土的【寶物】
+ * B2, C2... = 截止日期,格式:2025-04-29 23:59
+ * B3, C3... = 課業文件夾 ID(自動填入)
+ * A4 以下 = 學生姓名
+ * B4 以下 = 繳交狀態(自動更新:已繳交 / 未繳交 / 遲交)
+ *
+ * 觸發器:createFoldersAndUpdateSheet,每 5 分鐘觸發一次。
+ * 執行 createSubmissionTrigger() 可自動建立觸發器。
+ *
+ * 注意:ROOT_FOLDER_ID、getConfig() 及 getOrCreateFolder() 定義於 Shared.gs。
+ */
+
+// 全局文件夾緩存(減少 API 呼叫次數)。
+// 注意:在 Apps Script 中,全局變數的生命週期與單次函數執行相同,
+// 每次觸發器執行都會重建此物件,不存在跨執行週期的過時資料問題。
+const folderCache = {};
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 主函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 主函數:建立分層文件夾(如未建立)並更新繳交狀態。
+ * 每 5 分鐘由觸發器自動執行。
+ */
+function createFoldersAndUpdateSheet() {
+ const config = getConfig();
+ const spreadsheet = SpreadsheetApp.openById(config.SUBMISSION_SHEET_ID);
+ const sheets = spreadsheet.getSheets();
+
+ const pendingFolderId = config.PENDING_FOLDER_ID; // 02_待批改課業
+ const returnedFolderId = config.RETURNED_FOLDER_ID; // 04_已發還課業
+
+ sheets.forEach(function(sheet) {
+ const className = sheet.getRange('A1').getValue().toString().trim();
+ if (!className) return; // 跳過沒有班別名稱的分頁
+
+ // 檢查是否已建立文件夾
+ const folderCreated = sheet.getRange('A2').getValue();
+ if (folderCreated !== 'created') {
+ // 建立「02_待批改課業」的班別及課業文件夾
+ const classFolder = createFolderIfNotExists(pendingFolderId, className);
+ const categoryFolders = {
+ '閱讀': createFolderIfNotExists(classFolder.getId(), '閱讀'),
+ '寫作(長文)': createFolderIfNotExists(classFolder.getId(), '寫作(長文)'),
+ '寫作(實用文)': createFolderIfNotExists(classFolder.getId(), '寫作(實用文)')
+ };
+
+ const lastColumn = sheet.getLastColumn();
+ if (lastColumn >= 2) {
+ const homeworkValues = sheet.getRange(1, 2, 2, lastColumn - 1).getValues();
+ const homeworkNames = homeworkValues[0]; // B1, C1, ...
+ homeworkNames.forEach(function(name, index) {
+ if (!name) return;
+ const categoryMatch = name.match(/「(.*?)」/);
+ if (!categoryMatch) return;
+ const category = categoryMatch[1];
+ const homeworkTitle = name.replace(/「.*?」/, '').trim();
+ const categoryFolder = categoryFolders[category];
+ if (categoryFolder) {
+ const homeworkFolder = createFolderIfNotExists(categoryFolder.getId(), homeworkTitle);
+ sheet.getRange(3, 2 + index).setValue(homeworkFolder.getId());
+ }
+ });
+ }
+
+ // 建立「04_已發還課業」的班別→學生→課業類別文件夾
+ const returnedClassFolder = createFolderIfNotExists(returnedFolderId, '【' + className + '】');
+ const studentNames = sheet.getRange('A4:A' + sheet.getLastRow()).getValues()
+ .flat().filter(String);
+ studentNames.forEach(function(student) {
+ const studentFolder = createFolderIfNotExists(returnedClassFolder.getId(), '【' + student + '】');
+ ['閱讀', '寫作(長文)', '寫作(實用文)'].forEach(function(category) {
+ createFolderIfNotExists(studentFolder.getId(), category);
+ });
+ });
+
+ // 標記文件夾已建立
+ sheet.getRange('A2').setValue('created');
+ }
+
+ // 更新繳交狀態
+ const lastColumn = sheet.getLastColumn();
+ if (lastColumn >= 2) {
+ const homeworkValues = sheet.getRange(1, 2, 2, lastColumn - 1).getValues();
+ const homeworkNames = homeworkValues[0];
+ const deadlines = homeworkValues[1];
+ const studentNames = sheet.getRange('A4:A' + sheet.getLastRow()).getValues()
+ .flat().filter(String);
+ updateSubmissionStatus(sheet, studentNames, homeworkNames, deadlines);
+ }
+ });
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 輔助函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 在 parentId 文件夾下取得或建立名為 folderName 的子文件夾(冪等,含緩存)。
+ * @param {string} parentId
+ * @param {string} folderName
+ * @returns {GoogleAppsScript.Drive.Folder}
+ */
+function createFolderIfNotExists(parentId, folderName) {
+ const key = parentId + '_' + folderName;
+ if (folderCache[key]) {
+ return DriveApp.getFolderById(folderCache[key]);
+ }
+
+ const parentFolder = DriveApp.getFolderById(parentId);
+ const iter = parentFolder.getFoldersByName(folderName);
+ if (iter.hasNext()) {
+ const folder = iter.next();
+ folderCache[key] = folder.getId();
+ return folder;
+ }
+
+ const newFolder = parentFolder.createFolder(folderName);
+ folderCache[key] = newFolder.getId();
+ return newFolder;
+}
+
+/**
+ * 批次更新試算表中的繳交狀態。
+ * 比對「02_待批改課業」中對應課業文件夾的檔案與學生姓名:
+ * - 已繳交(綠色):檔案名稱包含學生姓名且在截止日期前
+ * - 遲交(黃色): 檔案名稱包含學生姓名但在截止日期後
+ * - 未繳交(紅色):未找到對應檔案
+ */
+function updateSubmissionStatus(sheet, studentNames, homeworkNames, deadlines) {
+ const homeworkFolderIds = sheet.getRange(3, 2, 1, homeworkNames.length).getValues()[0];
+
+ // 一次性搜索所有相關文件並緩存
+ const fileMap = {};
+ homeworkFolderIds.forEach(function(folderId) {
+ if (!folderId) return;
+ const files = DriveApp.getFolderById(folderId).getFiles();
+ fileMap[folderId] = [];
+ while (files.hasNext()) {
+ fileMap[folderId].push(files.next());
+ }
+ });
+
+ const numRows = studentNames.length;
+ const numCols = homeworkNames.length;
+ const values = [];
+ const backgrounds = [];
+
+ studentNames.forEach(function(student) {
+ const rowValues = [];
+ const rowBackgrounds = [];
+ homeworkFolderIds.forEach(function(folderId, colIndex) {
+ if (!folderId || !homeworkNames[colIndex]) {
+ rowValues.push('');
+ rowBackgrounds.push('white');
+ return;
+ }
+
+ const files = fileMap[folderId] || [];
+ let submitted = false;
+ let late = false;
+
+ for (let i = 0; i < files.length; i++) {
+ if (files[i].getName().includes(student)) {
+ submitted = true;
+ const uploadTime = files[i].getDateCreated();
+ const deadline = new Date(deadlines[colIndex]);
+ if (uploadTime > deadline) late = true;
+ break;
+ }
+ }
+
+ if (submitted) {
+ if (late) {
+ rowValues.push('遲交');
+ rowBackgrounds.push('yellow');
+ } else {
+ rowValues.push('已繳交');
+ rowBackgrounds.push('green');
+ }
+ } else {
+ rowValues.push('未繳交');
+ rowBackgrounds.push('red');
+ }
+ });
+ values.push(rowValues);
+ backgrounds.push(rowBackgrounds);
+ });
+
+ // 一次性寫入值和背景色
+ if (numRows > 0 && numCols > 0) {
+ const range = sheet.getRange(4, 2, numRows, numCols);
+ range.setValues(values);
+ range.setBackgrounds(backgrounds);
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 觸發器設置
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 建立每 5 分鐘觸發一次 createFoldersAndUpdateSheet 的時間觸發器。
+ */
+function createSubmissionTrigger() {
+ ScriptApp.getProjectTriggers().forEach(function(trigger) {
+ if (trigger.getHandlerFunction() === 'createFoldersAndUpdateSheet') {
+ ScriptApp.deleteTrigger(trigger);
+ }
+ });
+ ScriptApp.newTrigger('createFoldersAndUpdateSheet')
+ .timeBased()
+ .everyMinutes(5)
+ .create();
+ Logger.log('✅ 已建立觸發器:createFoldersAndUpdateSheet,每 5 分鐘觸發一次。');
+}
diff --git a/WebInterface.gs b/WebInterface.gs
new file mode 100644
index 0000000..3956647
--- /dev/null
+++ b/WebInterface.gs
@@ -0,0 +1,298 @@
+/**
+ * 「帙雲」05 - 繳交紀錄及課業佈置介面(Web App)
+ *
+ * 功用:建立網頁介面,連結至各文件夾,供老師查閱繳交紀錄及佈置課業。
+ * 部署:以 Web App 方式部署(Deploy → New deployment → Web app)。
+ *
+ * 頁面:
+ * / → Index.html 控制面板
+ * ?page=record → record.html 繳交紀錄
+ * ?page=homework → homework.html 佈置課業
+ * ?page=setup → setup.html 班別及學生管理
+ *
+ * 注意:ROOT_FOLDER_ID、getConfig() 及 getOrCreateFolder() 定義於 Shared.gs。
+ */
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Web App 入口
+// ─────────────────────────────────────────────────────────────────────────────
+
+function doGet(e) {
+ const page = e.parameter.page;
+
+ if (page === 'record') {
+ const template = HtmlService.createTemplateFromFile('record');
+ template.classData = getClassData();
+ return template.evaluate().setTitle('作業繳交紀錄查閱');
+ }
+
+ if (page === 'homework') {
+ const template = HtmlService.createTemplateFromFile('homework');
+ return template.evaluate().setTitle('布置課業');
+ }
+
+ if (page === 'setup') {
+ const template = HtmlService.createTemplateFromFile('setup');
+ return template.evaluate().setTitle('班別及學生管理');
+ }
+
+ // 預設:控制面板
+ const template = HtmlService.createTemplateFromFile('Index');
+ template.classData = getClassData();
+ template.folderUrls = getFolderUrls();
+ return template.evaluate().setTitle('帙雲 - 控制面板');
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 供 HTML 頁面呼叫的函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 獲取所有文件夾及試算表的 URL,供 Index.html 動態注入連結。
+ * @returns {Object}
+ */
+function getFolderUrls() {
+ const config = getConfig();
+ return {
+ UPLOAD_URL: 'https://drive.google.com/drive/folders/' + config.UPLOAD_FOLDER_ID,
+ PENDING_URL: 'https://drive.google.com/drive/folders/' + config.PENDING_FOLDER_ID,
+ TEACHER_RETURN_URL: 'https://drive.google.com/drive/folders/' + config.TEACHER_RETURN_FOLDER_ID,
+ RETURNED_URL: 'https://drive.google.com/drive/folders/' + config.RETURNED_FOLDER_ID,
+ SHARE_SHEET_URL: config.SHARE_SHEET_ID
+ ? 'https://docs.google.com/spreadsheets/d/' + config.SHARE_SHEET_ID
+ : '#',
+ SUBMISSION_SHEET_URL: 'https://docs.google.com/spreadsheets/d/' + config.SUBMISSION_SHEET_ID
+ };
+}
+
+/**
+ * 獲取試算表資料(用於 homework.html 的班別選單及現有課業列表)。
+ * @returns {{classes: string[], homeworks: Object}}
+ */
+function getSpreadsheetData() {
+ const config = getConfig();
+ const spreadsheet = SpreadsheetApp.openById(config.SUBMISSION_SHEET_ID);
+ const sheets = spreadsheet.getSheets();
+
+ const data = { classes: [], homeworks: {} };
+
+ sheets.forEach(function(sheet) {
+ const className = sheet.getRange('A1').getValue().toString().trim();
+ if (!className) return;
+
+ data.classes.push(className);
+
+ const lastColumn = sheet.getLastColumn();
+ let homeworkNames = [];
+ let deadlines = [];
+
+ if (lastColumn >= 2) {
+ const values = sheet.getRange(1, 2, 2, lastColumn - 1).getValues();
+ homeworkNames = values[0].filter(String);
+ deadlines = values[1].map(function(d) { return d.toString(); });
+ }
+
+ data.homeworks[className] = { names: homeworkNames, deadlines: deadlines };
+ });
+
+ return data;
+}
+
+/**
+ * 將新課業寫入試算表(由 homework.html 呼叫)。
+ * @param {string} className 班別
+ * @param {string} homeworkName 課業名稱(含類別及關鍵詞)
+ * @param {string} deadline 截止日期,格式:2025-04-29 23:59
+ */
+function updateSpreadsheet(className, homeworkName, deadline) {
+ const config = getConfig();
+ const spreadsheet = SpreadsheetApp.openById(config.SUBMISSION_SHEET_ID);
+
+ const sheet = spreadsheet.getSheets().find(function(s) {
+ return s.getRange('A1').getValue().toString().trim() === className;
+ });
+ if (!sheet) throw new Error('找不到指定的班別:' + className);
+
+ // 找到下一個可用欄位(欄 A 為班別名稱,課業從欄 B 起)
+ const nextColumn = Math.max(2, sheet.getLastColumn() + 1);
+ sheet.getRange(1, nextColumn).setValue(homeworkName);
+ sheet.getRange(2, nextColumn).setValue(deadline);
+}
+
+/**
+ * 獲取所有班別的繳交紀錄資料(用於 record.html)。
+ * @returns {Array}
+ */
+function getClassData() {
+ const config = getConfig();
+ const spreadsheet = SpreadsheetApp.openById(config.SUBMISSION_SHEET_ID);
+ const sheets = spreadsheet.getSheets();
+ const classData = [];
+
+ sheets.forEach(function(sheet) {
+ const className = sheet.getRange('A1').getValue().toString().trim();
+ if (!className) return;
+
+ const lastColumn = sheet.getLastColumn();
+ const homeworkNames = lastColumn >= 2
+ ? sheet.getRange(1, 2, 1, lastColumn - 1).getValues()[0]
+ : [];
+ const deadlines = lastColumn >= 2
+ ? sheet.getRange(2, 2, 1, lastColumn - 1).getValues()[0].map(function(date) {
+ if (date instanceof Date) {
+ return Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm');
+ }
+ return date.toString();
+ })
+ : [];
+ const studentNames = sheet.getRange('A4:A' + sheet.getLastRow()).getValues()
+ .flat().filter(String);
+
+ const homeworkData = homeworkNames.map(function(name, index) {
+ return {
+ name: name,
+ deadline: deadlines[index],
+ folderId: sheet.getRange(3, 2 + index).getValue()
+ };
+ });
+
+ const students = studentNames.map(function(student, rowIndex) {
+ const submissions = homeworkData.map(function(hw, colIndex) {
+ const cell = sheet.getRange(4 + rowIndex, 2 + colIndex);
+ return { homework: hw.name, color: cell.getBackground() };
+ });
+ return { name: student, submissions: submissions };
+ });
+
+ classData.push({
+ className: className,
+ homework: homeworkData,
+ students: students
+ });
+ });
+
+ return classData;
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 設置面板後端函數
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * 獲取設置面板所需的所有資料。
+ * @returns {{ submissionClasses: Array, shareClasses: Array }}
+ */
+function getSetupData() {
+ const config = getConfig();
+ const submissionSS = SpreadsheetApp.openById(config.SUBMISSION_SHEET_ID);
+
+ const submissionClasses = submissionSS.getSheets().map(function(sheet) {
+ const className = sheet.getRange('A1').getValue().toString().trim();
+ if (!className) return null;
+ const lastRow = sheet.getLastRow();
+ const students = lastRow >= 4
+ ? sheet.getRange('A4:A' + lastRow).getValues().flat().filter(String)
+ : [];
+ return { name: className, students: students };
+ }).filter(Boolean);
+
+ const shareClasses = [];
+ if (config.SHARE_SHEET_ID) {
+ const shareSS = SpreadsheetApp.openById(config.SHARE_SHEET_ID);
+ shareSS.getSheets().forEach(function(sheet) {
+ const sheetName = sheet.getName();
+ const lastRow = sheet.getLastRow();
+ const students = [];
+ if (lastRow >= 2) {
+ sheet.getRange('A2:C' + lastRow).getValues().forEach(function(row) {
+ if (row[0] || row[1]) {
+ students.push({ id: row[0].toString(), name: row[1].toString(), url: row[2].toString() });
+ }
+ });
+ }
+ shareClasses.push({ name: sheetName, students: students });
+ });
+ }
+
+ return { submissionClasses: submissionClasses, shareClasses: shareClasses };
+}
+
+/**
+ * 新增班別至繳交紀錄試算表。
+ * @param {string} className 班別名稱(如 1C)
+ */
+function addClass(className) {
+ className = className.toString().trim();
+ if (!className) throw new Error('班別名稱不可為空');
+
+ const config = getConfig();
+ const ss = SpreadsheetApp.openById(config.SUBMISSION_SHEET_ID);
+
+ const existing = ss.getSheets().some(function(s) {
+ return s.getRange('A1').getValue().toString().trim() === className;
+ });
+ if (existing) throw new Error('班別「' + className + '」已存在');
+
+ const sheet = ss.insertSheet(className);
+ sheet.getRange('A1').setValue(className);
+}
+
+/**
+ * 設定某班別的學生名單(覆蓋 A4 以下的現有名單)。
+ * @param {string} className 班別名稱
+ * @param {string} studentsText 學生姓名(每行一個)
+ */
+function setClassStudents(className, studentsText) {
+ const config = getConfig();
+ const ss = SpreadsheetApp.openById(config.SUBMISSION_SHEET_ID);
+
+ const sheet = ss.getSheets().find(function(s) {
+ return s.getRange('A1').getValue().toString().trim() === className;
+ });
+ if (!sheet) throw new Error('找不到班別:' + className);
+
+ const names = studentsText.toString().split('\n')
+ .map(function(n) { return n.trim(); })
+ .filter(Boolean);
+
+ // 清除現有學生名單(A4 以下)
+ const lastRow = sheet.getLastRow();
+ if (lastRow >= 4) {
+ sheet.getRange('A4:A' + lastRow).clearContent();
+ }
+
+ if (names.length > 0) {
+ sheet.getRange(4, 1, names.length, 1).setValues(names.map(function(n) { return [n]; }));
+ }
+}
+
+/**
+ * 設定共用試算表中某班別的學生帳號資料(覆蓋現有資料)。
+ * @param {string} className 班別名稱
+ * @param {string} studentsJson JSON 字串,格式:[{id, name}, ...]
+ */
+function setShareStudents(className, studentsJson) {
+ const config = getConfig();
+ if (!config.SHARE_SHEET_ID) throw new Error('找不到「自動共用、收集位址」試算表,請先執行 setup()。');
+
+ const ss = SpreadsheetApp.openById(config.SHARE_SHEET_ID);
+ let sheet = ss.getSheetByName(className);
+ if (!sheet) {
+ sheet = ss.insertSheet(className);
+ sheet.getRange('A1:C1').setValues([['學號', '姓名', '文件夾位址']]);
+ sheet.getRange('A1:C1').setFontWeight('bold');
+ }
+
+ const students = JSON.parse(studentsJson);
+
+ // 清除現有資料(第 2 行以下)
+ const lastRow = sheet.getLastRow();
+ if (lastRow >= 2) {
+ sheet.getRange(2, 1, lastRow - 1, 3).clearContent();
+ }
+
+ if (students.length > 0) {
+ const rows = students.map(function(s) { return [s.id || '', s.name || '', '']; });
+ sheet.getRange(2, 1, rows.length, 3).setValues(rows);
+ }
+}
diff --git a/homework.html b/homework.html
new file mode 100644
index 0000000..d025e91
--- /dev/null
+++ b/homework.html
@@ -0,0 +1,349 @@
+
+
+
+
+
+ 布置課業
+
+
+
+
+
+
+
+
+
+
+
確認課業資料
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/record.html b/record.html
new file mode 100644
index 0000000..affdb48
--- /dev/null
+++ b/record.html
@@ -0,0 +1,221 @@
+
+
+
+
+
+ 作業繳交紀錄查閱
+
+
+
+
+
+
+
+ for (const classInfo of classData) { ?>
+
+ } ?>
+
+
+ for (const classInfo of classData) { ?>
+
+
= classInfo.className ?>
+
+
+
+
+ | 學生姓名 |
+ for (const hw of classInfo.homework) { ?>
+
+
+ = hw.name ?>
+ = hw.deadline ?>
+
+ |
+ } ?>
+
+
+
+ for (const student of classInfo.students) { ?>
+
+ | = student.name ?> |
+ for (const submission of student.submissions) { ?>
+ |
+ } ?>
+
+ } ?>
+
+
+
+
+ } ?>
+
+
+
+
+
diff --git a/setup.html b/setup.html
new file mode 100644
index 0000000..23023e9
--- /dev/null
+++ b/setup.html
@@ -0,0 +1,444 @@
+
+
+
+
+
+ 班別及學生管理
+
+
+
+
+ 載入中…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
新增班別
+
+
+
+
+
+
+
+
+
+
+
學生名單
+
選擇一個班別後,可新增或更新學生姓名。
+
+
+ 載入中…
+
+
+
+
+
每行輸入一個學生姓名。儲存後將覆蓋該班別的現有名單。
+
+
+
+
+
+
+
+
+
+
+
學生帳號(共用文件夾用)
+
+ 輸入每位學生的學號及姓名,用於自動共用文件夾及發送 Teams 通知。
+ 儲存後,可在控制面板執行「自動共用專屬文件夾」。
+
+
+
+ 載入中…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+