This commit is contained in:
louiscklaw
2025-01-31 19:29:24 +08:00
parent 843c590c8b
commit abff74fd77
81 changed files with 7754 additions and 0 deletions

4
catmk2/task1/.clasp.json Normal file
View File

@@ -0,0 +1,4 @@
{
"scriptId": "1E3I99AvUTQ7ydwCVsuD5-OpbCJyW4LhVbo78xDQJuWWFLNGOLRzRkJ1b",
"rootDir": "/workspace/carousell-comission-playlist/catmk2/task1"
}

View File

@@ -0,0 +1,2 @@
tests
**/node_modules/**

10
catmk2/task1/Code.js Normal file
View File

@@ -0,0 +1,10 @@
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu("Automator Menu " + VER)
// .addItem("send 繳費通知 (未通知 -> 已通知)", "processPaymentNotice")
.addItem("debug-Say Hello", "sayHelloworld")
// .addItem("debug-helloWorldEmail", "helloWorldEmail")
// .addItem("debug-helloWorldWriteCell", "helloWorldWriteCell")
// .addItem("debug-processPaymentNotice", "processPaymentNotice")
.addToUi();
}

View File

@@ -0,0 +1,6 @@
{
"timeZone": "Asia/Hong_Kong",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}

30
catmk2/task1/config.js Normal file
View File

@@ -0,0 +1,30 @@
const VER = "1.0.3";
const SHEET_ID = "1yHIRyYetq6SC3BZtGNVDzCfIJvuwcROnmdqyk9Jmyss";
// 學員進度
const SHEET_STUDENT_PROGRESS = "學員進度";
const COL_STUDENT_PROGRESS_CHINESE_NAME = "A"; //中文姓名
const COL_STUDENT_PROGRESS_ENGLISH_NAME = "B"; // 英文姓名
const COL_STUDENT_PROGRESS_COURSE_OFFERED = "C"; // 報讀課程
const COL_STUDENT_PROGRESS_COURSE_CODE = "D"; // 課程班號
const COL_STUDENT_PROGRESS_GENDER = "E"; // 性別
const COL_STUDENT_PROGRESS_PHONE_NUMBER = "F"; // 手提電話
const COL_STUDENT_PROGRESS_EMAIL_ADDRESS = "G"; // 電郵地址
const COL_STUDENT_PROGRESS_ATTENDANCE_PROGRESS = "H"; // 上堂進度
const COL_STUDENT_PROGRESS_PAYMENT_PROGRESS = "I"; // 繳費進度
const COL_STUDENT_PROGRESS_PAYMENT_LINK = "K"; // 付款連結
const COL_STUDENT_PROGRESS_NOTIFICATION_DATE = "J"; // 通知日期 (yyyy-mm-dd)
// const COL_STUDENT_PROGRESS_PAYMENT_PROOF_PROVIDED = "M"; // 已提供付款證明
const COL_STUDENT_PROGRESS_CERTIFICATE_REISSUE = "L"; // 證書補發
const COL_STUDENT_PROGRESS_CERTIFICATE_FEE_RECEIVED = "M"; // 己收補證書費用
const COL_STUDENT_PROGRESS_REMARKS = "O"; // 備註
const COL_STUDENT_PROGRESS_RESULT = "Y"; // RESULT
const CONST_NOT_NOTIFIED = "未通知";
const CONST_NOTIFIED_ALREADY = "已通知";
const EMAIL_QUOTA_USED_UP="對不起email 配額已用完"
const ROW_START = 2;
console.log("config initialized");

3301
catmk2/task1/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
catmk2/task1/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "task1",
"version": "1.0.0",
"description": "",
"main": "Code.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"cP": "clasp push",
"gitUpdate": "git add . && git commit -m\"update,\"",
"watch:cP": "nodemon -L --delay 1 --exec \"npm run cP\"",
"watch:format": "npx nodemon -L --exec \"npx prettier --write . \""
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"browser-sync": "^2.29.3",
"nodemon": "^3.0.1"
},
"dependencies": {
"clasp": "^1.0.0"
}
}

1
catmk2/task1/slides_src/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.html

BIN
catmk2/task1/slides_src/assets/email-screenshot.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

After

Width:  |  Height:  |  Size: 963 B

BIN
catmk2/task1/slides_src/assets/resend.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
catmk2/task1/slides_src/assets/run_script.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
catmk2/task1/slides_src/assets/send_email_done.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
catmk2/task1/slides_src/assets/wanted_row.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -ex
npx marp -w index.md
npx marp --pdf --allow-local-files ./index.md

View File

@@ -0,0 +1,58 @@
---
marp: true
title: Helloworld
description: Hosting Marp slide deck on the web
theme: uncover
paginate: true
_paginate: false
header: '**google apps script send email**'
footer: '![height:20px](./assets/github-mark.svg) https://louiscklaw.github.io'
---
<!-- headingDivider: 2 -->
# google apps script
send email
## 目的: 通知報名者
1. 已獲取錄
1. 付款
## 做法:
- 頁面 '學員進度'
- 欄 '通知日期' 中,該列 "變成" 觸發日期
- 欄 '繳費進度' 自己轉為 "已通知"
- 自動 SEND EMAIL 去 同列的 '電郵地址'
## requirement/需要資料:
1. 係學員進度呢個頁面度
1. 首先 "繳費進度" 要係未通知
1. 跟住 "電郵地址" 要係一個有效嘅地址
1. 加埋 "付款連結" 要係一條 link ()
![height:250px](./assets/wanted_row.png)
## how to run/點行呢?
1. click "Automator Menu"
1. click "send 繳費通知 (未通知 -> 己通知)"
![height:250px](./assets/run_script.png)
## done/行完?
**send 左 email 的話:**
1. 繳費進度 由 "未通知" -> "已通知"
1. 通知日期 會填上 "日期"
![width:900px](./assets/send_email_done.png)
## email/電郵?
![height:500px](./assets/email-screenshot.png)
## resend/s重發 email?
1. 將繳費進度由 "己通知" 轉番去 "未通知",
1. 然後再行個 script 一次 (slide 5)
![height:300px](./assets/resend.png)
## QnA/問問題?
😃

BIN
catmk2/task1/slides_src/main.png (Stored with Git LFS) Normal file

Binary file not shown.

2887
catmk2/task1/slides_src/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"name": "catmk2-presentation",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"gitUpdate": "git add . && git commit -m \"update catmk2-presentation,\" && git push",
"build": "rm -rf *.html **/*.html && yarn marp -I . -o .",
"watch": "npx nodemon -e \"*.md\" --exec yarn build"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@marp-team/marp-cli": "^2.0.4"
}
}

Binary file not shown.

View File

@@ -0,0 +1,84 @@
const email_title = (course_title) => `${course_title} 取錄 及 付款通知`;
const email_content = (
chinese_name,
course_name,
course_code,
payment_link,
chinese_today_string
) => {
try {
var output = {
state: "init",
debug: { chinese_name, course_name, course_code, chinese_today_string },
error: {},
};
return `
<div>
${chinese_name ? chinese_name + " 您好," : "您好,"}<br />
<br />
多謝閣下報名參加&nbsp;<br />
<br />
${course_name ? "課程:&nbsp;" + course_name : ""}<br />
${course_code ? "班號:&nbsp;" + course_code : ""}<br />
<br />
很高興通知您,課程即將開辦,請閣下於盡快完成付款手續,以確認報名。<br />
<br />
<a href="${payment_link}" target="_blank">按此到 付款連結</a><br />
<br />
另煩請保留&nbsp;<underline>付款截圖</underline>&nbsp;或&nbsp;<underline>證明</underline>,貼上附件後回覆本電郵。<br />
<br />
<br />
如付款過程中遇到任何困難,煩請聯絡 9134 7967 或 <a href="">回覆</a>本電郵即可。<br />
於課程開始&nbsp;7&nbsp;天前仍未付款者,本司有權將課程名額轉讓至候補名單<br />
<br />
最後,隨信附上二維碼付款指引教學以供參考。謝謝 😊 <br />
<br />
<br />
<br />
樂歷課程報名處<br />
${chinese_today_string || ""}<br />
<br />
<br />
<br />
</div>
`.trim();
} catch (error) {
output = { ...output, error };
console.log("notification_email.js errors");
console.log(error);
}
};
const email_content1 = (chinese_name, course_name, course_code) => {
return `
<中文姓名> 你好,
多謝閣下報名參加
${course_name}
${course_code}
很高興通知您,課程即將開辦,請閣下於盡快完成付款收續,以確認報名。
繳費方式 :
請按下方連結下載轉數快/ FPS 二維碼並使用各大銀行的手機現財App 内掃描二維碼會直接付款。
<付款連結>
請保留付款截圖或證明後,貼上附件後回覆本電郵。
如付款有任何困難,請聯絡 9134 7967 或 回覆本電郵即可
如課程前7天前仍未付款者本司有權將課程名額轉讓至候補名單
最後,附上二維碼付款指引教學,以供參考。謝謝 😊
<附件 - 智豐收二維碼付款指引.pdf>
樂歷課程報名處
<今日日期>
`.trim();
};

View File

@@ -0,0 +1,12 @@
{
"name": "template",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,20 @@
function appendResult(row, comment_to_write) {
var output = { state: "init", debug: { comment_to_write }, error: "" };
try {
var sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(
SHEET_STUDENT_PROGRESS
);
var result_cell = getCell(sheet, row, COL_STUDENT_PROGRESS_RESULT);
var result_value = readCell(result_cell);
if (result_value.trim() == "") {
writeCell(result_cell, comment_to_write);
} else {
writeCell(result_cell, [result_value, comment_to_write].join("\n"));
}
} catch (error) {
output = { ...output, error };
console.log(output);
}
}

View File

@@ -0,0 +1,12 @@
function checkEmailQuotaAvailable() {
var output = { state: "init", debug: {}, error: {} };
try {
var emailQuotaRemaining = MailApp.getRemainingDailyQuota();
return emailQuotaRemaining > 0;
} catch (error) {
output = {...output , error}
console.log('checkEmailQuotaAvailable error')
console.log(output);
}
}

View File

@@ -0,0 +1,57 @@
function checkLastRow(sheet, current_row) {
// return true if considered last row, false if not
// check email column only
output = {state:'init', debug:{sheet, current_row}, error:{}}
try {
var current_row_cell = getCell(
sheet,
current_row,
COL_STUDENT_PROGRESS_EMAIL_ADDRESS
);
var current_row_cell_1 = getCell(
sheet,
current_row + 1,
COL_STUDENT_PROGRESS_EMAIL_ADDRESS
);
var current_row_cell_2 = getCell(
sheet,
current_row + 2,
COL_STUDENT_PROGRESS_EMAIL_ADDRESS
);
var current_row_cell_3 = getCell(
sheet,
current_row + 3,
COL_STUDENT_PROGRESS_EMAIL_ADDRESS
);
var current_row_cell_4 = getCell(
sheet,
current_row + 4,
COL_STUDENT_PROGRESS_EMAIL_ADDRESS
);
var current_row_cell_5 = getCell(
sheet,
current_row + 5,
COL_STUDENT_PROGRESS_EMAIL_ADDRESS
);
var check_is_empty = [
isCellEmpty(current_row_cell),
isCellEmpty(current_row_cell_1),
isCellEmpty(current_row_cell_2),
isCellEmpty(current_row_cell_3),
isCellEmpty(current_row_cell_4),
isCellEmpty(current_row_cell_5),
];
return check_is_empty.indexOf(false) < 0;
} catch (error) {
output ={...output, error}
console.log("checkLastRow: error");
console.log(output)
return false;
}
}

View File

@@ -0,0 +1,10 @@
function checkNotNotifiedForPayment(cell_value) {
var output = { state: "init", debug: {}, error: "" };
try {
return cell_value == CONST_NOT_NOTIFIED;
} catch (error) {
console.log('checkNotNotifiedForPayment error')
output = { ...output, error };
console.log(output);
}
}

View File

@@ -0,0 +1,27 @@
function checkPaymentLinkAvailable(current_row) {
var output = { state: "init", debug: { current_row }, error: {} };
try {
var sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(
SHEET_STUDENT_PROGRESS
);
var payment_link_cell = getCell(
sheet,
current_row,
COL_STUDENT_PROGRESS_PAYMENT_LINK
);
var payment_link = readCell(payment_link_cell);
console.log({ payment_link });
if (payment_link.search(/^https?:\/\/.+/) > -1) {
return true;
}
return false;
} catch (error) {
output = { ...output, error };
console.log('checkPaymentLinkAvailable error')
console.log(output);
return false;
}
}

View File

@@ -0,0 +1,10 @@
function getCell(sheet, row, column) {
output = {state:'init', debug:{}, error:{}}
try {
var cell = sheet.getRange(column + row);
return cell;
} catch (error) {
output = {...output, error}
console.log(output);
}
}

View File

@@ -0,0 +1,3 @@
function getChineseDayString() {
return Utilities.formatDate(new Date(), "GMT+8", "yyyy 年 MM 月 dd 日");
}

View File

@@ -0,0 +1,27 @@
function getLastRow(sheet) {
var output = { state: "init", debug: {sheet}, error: "" };
try {
var last_row = -1;
var row_scan = 99999;
for (let i = 1; i < row_scan; i++) {
if (checkLastRow(sheet, i)) {
// print last row number
last_row = i - 1;
output = { ...output, debug: { ...output.debug, last_row } };
console.log("last row is " + last_row.toString());
break;
} else {
// keep going
}
}
if (last_row == -1) {
throw new Error('cannot find the last row')
}
return last_row;
} catch (error) {
console.log('getLastRow error')
output = { ...output, error };
console.log(output);
}
}

View File

@@ -0,0 +1,14 @@
function getSheetStudentProgress() {
output = {state:'init', debug:{}, error:{}}
try {
var sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(
SHEET_STUDENT_PROGRESS
);
return sheet;
} catch (error) {
output = {...output, error}
console.log("getSheetStudentProgress error")
console.log(output);
}
}

View File

@@ -0,0 +1,12 @@
function isCellEmpty(cell) {
var output = { state: "init", debug: {}, error: {} };
try {
// console.log("isCellEmpty:" + cell.getValue());
var temp = cell.getValue().toString().trim();
return temp == "";
} catch (error) {
console.log("isCellEmpty error");
output = { ...output, error };
console.log(output);
}
}

View File

@@ -0,0 +1,51 @@
function processPaymentNotice() {
var output = { state: "init", debug: {}, error: "" };
try {
var sheet = getSheetStudentProgress();
var last_row = getLastRow(sheet);
output = { ...output, debug: { ...output.debug, last_row } };
for (var i = ROW_START; i < last_row + 1; i++) {
var email_cell = getCell(sheet, i, COL_STUDENT_PROGRESS_EMAIL_ADDRESS);
var payment_progress_cell = getCell(
sheet,
i,
COL_STUDENT_PROGRESS_PAYMENT_PROGRESS
);
var payment_progress = readCell(payment_progress_cell);
if (checkNotNotifiedForPayment(payment_progress)) {
resetResult(i);
appendResult(
i,
`not notified(${CONST_NOT_NOTIFIED}), proceed send payment notification email`
);
var quota_available = checkEmailQuotaAvailable();
var payment_link_available = checkPaymentLinkAvailable(i);
if (quota_available && payment_link_available) {
try {
sendPaymentNoticeEmail(i);
updateRowToNotificationSent(i);
} catch (error) {
Browser.msgBox("error during sending email");
}
} else {
if (quota_available < 1) {
Browser.msgBox(EMAIL_QUOTA_USED_UP);
}
if (!payment_link_available) {
appendResult(i, `payment link not exist, skipping`);
}
}
} else {
resetResult(i);
appendResult(i, `not "${CONST_NOT_NOTIFIED}" skipping`);
// var student_email = readCell(email_cell);
}
}
} catch (error) {
output = { ...output, error };
console.log(output);
}
}

View File

@@ -0,0 +1,10 @@
function readCell(cell) {
var output = { state: "init", debug: {}, error: {} };
try {
return cell.getValue() || "";
} catch (error) {
output = {...output, err: error}
console.log(error);
}
}

View File

@@ -0,0 +1,16 @@
function resetResult(row) {
var output = { state: "init", debug: {}, error: "" };
try {
var sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(
SHEET_STUDENT_PROGRESS
);
var result_cell = getCell(sheet, row, COL_STUDENT_PROGRESS_RESULT);
return writeCell(result_cell, null);
} catch (error) {
console.log("resetResult error");
output = { ...output, error };
console.log(output);
console.log(error);
}
}

View File

@@ -0,0 +1,3 @@
function sayHelloworld() {
console.log("say helloworld");
}

View File

@@ -0,0 +1,11 @@
function sendEmail(options) {
var output = { state: "init", debug: {}, error: {} };
try {
MailApp.sendEmail(options);
output = { ...output, state: "done" };
} catch (error) {
output = { ...output, error };
console.log("sendEmail error");
console.log(output);
}
}

View File

@@ -0,0 +1,121 @@
function sendPaymentNoticeEmail(row) {
var output = { state: "init", debug: {}, error: {} };
var sheet = getSheetStudentProgress();
// var filename = "payment_guide.pdf";
var fps_tutorial_pdf_file_id = "1iD4CL8X-Nr7vfmj3UI3OzrJaKl1Gbg8tGAi6T6EmEJ0";
var fps_tutorial_pdf_file = DriveApp.getFileById(fps_tutorial_pdf_file_id);
// NOTE: student email?
var student_email_address_cell = getCell(
sheet,
row,
COL_STUDENT_PROGRESS_EMAIL_ADDRESS
);
var student_email_address = readCell(student_email_address_cell);
var student_chinese_name_cell = getCell(
sheet,
row,
COL_STUDENT_PROGRESS_CHINESE_NAME
);
var student_chinese_name = readCell(student_chinese_name_cell);
// NOTE: courase_name
var course_offered_cell = getCell(
sheet,
row,
COL_STUDENT_PROGRESS_COURSE_OFFERED
);
var course_offered = readCell(course_offered_cell);
if (course_offered == "") {
output = {
...output,
debug: {
...output.debug,
remarks: {
course_offered,
comment: "found empty",
},
},
};
}
// NOTE: course_code
var course_code_cell = getCell(sheet, row, COL_STUDENT_PROGRESS_COURSE_CODE);
var course_code = readCell(course_code_cell);
if (course_code == "") {
output = {
...output,
debug: {
...output.debug,
remarks: {
course_code,
comment: "found empty",
},
},
};
}
// NOTE: payment_link
var payment_link_cell = getCell(
sheet,
row,
COL_STUDENT_PROGRESS_PAYMENT_LINK
);
var payment_link = readCell(payment_link_cell);
if (payment_link == "") {
output = {
...output,
debug: {
...output.debug,
remarks: {
payment_link,
comment: "found empty",
},
},
};
}
// NOTE: courase_name
// NOTE: courase_name
try {
var subject = email_title(course_offered);
// var body = email_content("中文名", "chinese 中文科", "CHI001");
var htmlBody = email_content(
student_chinese_name,
course_offered,
course_code,
payment_link,
getChineseDayString()
);
var recipient = student_email_address;
if (recipient=='') throw new Error('email address not valid');
var sender = "testhelloworld04@gmail.com";
// https://developers.google.com/apps-script/reference/mail/mail-app
var options = {
bcc: sender,
replyTo: sender,
to: recipient,
subject: subject,
// body: body,
htmlBody: htmlBody,
attachments: [fps_tutorial_pdf_file.getAs(MimeType.PDF)],
};
sendEmail(options);
updateNotificationDate(row)
appendResult(row, "send email done");
output = { ...output, state: "send email done" };
} catch (error) {
output = { ...output, error };
console.log("sendEmail error");
console.log(error);
throw error;
}
}

View File

@@ -0,0 +1,15 @@
function updateNotificationDate(row) {
output = {state:"init", debug:{}, error:{}}
try {
var student_progress_sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(
SHEET_STUDENT_PROGRESS
);
var notification_date_cell = getCell(student_progress_sheet, row, COL_STUDENT_PROGRESS_NOTIFICATION_DATE)
writeCell(notification_date_cell, getChineseDayString().replace(/ /g,''))
} catch (error) {
output = {...output, error}
console.log('updateNotificationDate error')
console.log(output)
}
}

View File

@@ -0,0 +1,17 @@
function updateRowToNotificationSent(row) {
var output = { state: "init", debug: { row }, error: {} };
try {
var sheet = getSheetStudentProgress();
var payment_progress_cell = getCell(
sheet,
row,
COL_STUDENT_PROGRESS_PAYMENT_PROGRESS
);
writeCell(payment_progress_cell, CONST_NOTIFIED_ALREADY);
return;
} catch (error) {
output = { ...output, error };
console.log(error);
}
}

View File

@@ -0,0 +1,11 @@
function writeCell(cell, content) {
var output = { state: "init", debug: {}, error: "" };
try {
cell.setValue(content);
} catch (error) {
output = {...output, error}
console.log("writeCell error");
console.log(output);
}
}