| รหัส | คณะที่ลงพื้นที่ | วันจัดเดิม | ✏️ สถานะนัดหมาย (รัตนา) | ✏️ วันจัดจริง (ถ้าเลื่อนแก้วันนี้) | ✏️ เวลา / ห้องประชุม / หมายเหตุ | 🔔 แจ้งเตือนผลกระทบ | 🔄 อาวาติฟ/นูรีซัน ร่างคิว & PR (D-5) |
🔄 รัตนา/อาวาติฟ ตรวจห้อง & สไลด์ (D-3) |
🔄 ทุกทีม Freeze & เตือน (D-1) |
🔄 ซุรียาณี สรุป 1 หน้า (D+1) |
🔄 นูรีซัน/มาดีฮะห์ ข่าว & ประเมิน (D+2) |
บันทึก |
|---|
| รหัสคณะ | คณะเป้าหมาย | วันจัดจริง | 📂 โฟลเดอร์ประจำคณะ | ✏️ [รัตนา] นัดหมาย/ห้อง |
✏️ [อาวาติฟ] สไลด์/คิวงาน |
✏️ [นูรีซัน] แจ้งเตือน/ข่าว |
✏️ [ซุรียาณี] สรุป 1 หน้า |
✏️ [มาดีฮะห์] QR/ประเมิน |
✏️ [กันตกาล] Freeze/รับรอง |
✏️ หมายเหตุ/ชื่อไฟล์ที่ส่งแล้ว | บันทึก |
|---|
| รหัส | ผู้รับผิดชอบ | งานที่ต้องทำ และสิ่งที่ต้องส่ง (ภาษาง่ายๆ) | กำหนดส่ง | 📂 โฟลเดอร์เก็บหลักฐาน | ✏️ [ช่อง 1] สถานะ | ✏️ [ช่อง 2] บันทึกผล / ชื่อไฟล์ที่วางแล้ว | ✏️ [ช่อง 3] ปัญหา / การปรับแผน | บันทึก |
|---|
| ลำดับ | ✏️ คณะที่ถาม | ✏️ คำถาม / ปัญหา / เรื่องที่คณะฝากให้ช่วยตามต่อ | ✏️ ผู้รับผิดชอบหาคำตอบ | ✏️ กำหนดส่งคำตอบ | ✏️ สถานะ | ✏️ สรุปคำตอบที่ส่งแล้ว & ชื่อไฟล์หลักฐาน | บันทึก |
|---|
1. ฐานข้อมูล (Google Sheet)
ใช้ไฟล์ สมุดบันทึกงาน_สวพ.สัญจร_2570_ฉบับใช้ง่าย เดิมเป็นฐานข้อมูลกลาง แก้ไขจากใน Sheet หรือแก้ผ่านหน้าเว็บ ข้อมูลจะอัปเดตตรงกันเสมอ
2. หลังบ้าน (Google Apps Script)
ทำหน้าที่เป็น API (doGet ดึงข้อมูลสาธารณะ / doPost ตรวจสอบรหัส PIN: GA123555 ก่อนอนุญาตให้บันทึกข้อมูลลง Sheet)
3. โค้ดหน้าบ้าน (GitHub Repo)
เก็บไฟล์ index.html ชุดนี้ไว้บน GitHub เพื่อควบคุมเวอร์ชันและแก้ไขข้อความ/ดีไซน์ได้สะดวก
4. ระบบเผยแพร่ (Cloudflare Pages)
เชื่อมกับ GitHub เพื่อเปิดเว็บให้ทีมงานและผู้บริหารเข้าใช้งานผ่าน URL ความเร็วสูง รองรับทั้งมือถือและคอมพิวเตอร์ฟรี
ขั้นตอนการติดตั้งใช้งานจริง (ทำครั้งเดียว ไม่เกิน 5 นาที)
- ติดตั้งโค้ดหลังบ้าน (GAS): เปิดไฟล์ สมุดบันทึกงาน_สวพ.สัญจร_2570_ฉบับใช้ง่าย ➜ ไปที่เมนู ส่วนขยาย (Extensions) > Apps Script ➜ ลบโค้ดเดิมแล้ววางโค้ด
Code.gsด้านล่างลงไป - Deploy เป็น Web App: ในหน้า Apps Script กดปุ่ม การทำให้ใช้งานได้ (Deploy) > การทำให้ใช้งานได้รายการใหม่ (New deployment) ➜ เลือกประเภท เว็บแอป (Web app) ➜ ตั้งค่า ดำเนินการในฐานะ (Execute as): ฉัน (Me) และ ผู้ที่มีสิทธิ์เข้าถึง (Who has access): ทุกคน (Anyone) ➜ กด Deploy แล้วคัดลอก Web App URL (ที่ลงท้ายด้วย
/exec) - เชื่อม Web App URL เข้ากับหน้าเว็บ: กดปุ่ม "⚙️ ตั้งค่าลิงก์ GAS" บนแถบด้านบนของเว็บนี้ แล้ววาง Web App URL (หรือใส่ไว้ในตัวแปร
DEFAULT_GAS_URLในไฟล์index.htmlก่อนอัปโหลดขึ้น GitHub + Cloudflare Pages)
Code.gs) — ผูกกับ Sheet ID และรหัส PIN: GA123555 เรียบร้อยแล้ว
/**
* ============================================================================
* ระบบ Backend API (Google Apps Script) สำหรับกิจกรรม สวพ.สัญจร ปีงบประมาณ พ.ศ. 2570
* สถาบันวิจัยและพัฒนาชายแดนภาคใต้ (สวพ.ชต. / SRDI) มหาวิทยาลัยราชภัฏยะลา
* เชื่อมต่อกับ Google Sheet: สมุดบันทึกงาน_สวพ.สัญจร_2570_ฉบับใช้ง่าย
* ============================================================================
*/
const SPREADSHEET_ID = '1MiLun3SHJtD5RJmuD5bsmKeLtLKQWLq9woORMsD6Ynw';
const EDIT_PIN = 'GA123555';
const SHEET_TASKS = '1_บันทึกงานประจำทีม (ใช้ง่าย)';
const SHEET_SCHEDULE = '2_ตารางนัดหมายยืดหยุ่น_6คณะ';
const SHEET_ISSUES = '3_สมุดจดคำถามจากคณะ';
/**
* GET API: ดึงข้อมูลทั้งหมดจาก Google Sheet ไปแสดงผลบน Frontend (ทุกคนดูได้ ไม่ต้องใช้ PIN)
*/
function doGet(e) {
try {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const data = {
status: 'success',
updatedAt: new Date().toISOString(),
spreadsheetUrl: ss.getUrl(),
tasks: readTasksSheet_(ss.getSheetByName(SHEET_TASKS)),
schedule: readScheduleSheet_(ss.getSheetByName(SHEET_SCHEDULE)),
issues: readIssuesSheet_(ss.getSheetByName(SHEET_ISSUES))
};
return ContentService.createTextOutput(JSON.stringify(data))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({
status: 'error',
message: err.toString()
})).setMimeType(ContentService.MimeType.JSON);
}
}
/**
* POST API: บันทึก/แก้ไขข้อมูลจาก Frontend ลง Google Sheet (ต้องมี PIN: GA123555 เท่านั้น)
*/
function doPost(e) {
const lock = LockService.getScriptLock();
lock.waitLock(10000);
try {
const payload = JSON.parse(e.postData.contents || '{}');
// ตรวจสอบรหัส PIN ที่ฝั่ง Server ทุกครั้ง
if (String(payload.pin || '').trim() !== EDIT_PIN) {
return jsonResponse_({
status: 'error',
code: 'INVALID_PIN',
message: 'รหัส PIN ไม่ถูกต้อง ไม่อนุญาตให้บันทึกข้อมูล (ต้องใช้รหัส PIN สำหรับทีมงานเท่านั้น)'
});
}
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const action = payload.action;
// 1) อัปเดตงานประจำทีม (แท็บที่ 1: แถวที่ 8-27, คอลัมน์ F, G, H)
if (action === 'updateTask') {
const sh = ss.getSheetByName(SHEET_TASKS);
const values = sh.getRange(8, 1, 20, 8).getValues();
let foundRow = -1;
for (let i = 0; i < values.length; i++) {
if (String(values[i][0]).trim() === String(payload.taskId).trim()) {
foundRow = i + 8;
break;
}
}
if (foundRow === -1) throw new Error('ไม่พบรหัสงาน: ' + payload.taskId);
sh.getRange(foundRow, 6).setValue(payload.status || 'ยังไม่เริ่ม');
sh.getRange(foundRow, 7).setValue(payload.note || '');
sh.getRange(foundRow, 8).setValue(payload.problem || '');
}
// 2) อัปเดตวันนัดหมายยืดหยุ่นของรัตนา & สถานะรายคณะ (แท็บที่ 2)
else if (action === 'updateSchedule') {
const sh = ss.getSheetByName(SHEET_SCHEDULE);
// ตารางบน: แถว 6-11 (คอลัมน์ D=สถานะนัด, E=วันจัดจริง, F=เวลา/ห้อง/หมายเหตุ)
const topVals = sh.getRange(6, 1, 6, 6).getValues();
let topRow = -1;
for (let i = 0; i < topVals.length; i++) {
if (String(topVals[i][0]).trim() === String(payload.facultyCode).trim()) {
topRow = i + 6;
break;
}
}
if (topRow === -1) throw new Error('ไม่พบรหัสคณะ: ' + payload.facultyCode);
if (payload.coordStatus !== undefined) sh.getRange(topRow, 4).setValue(payload.coordStatus);
if (payload.actualDate !== undefined) sh.getRange(topRow, 5).setValue(payload.actualDate);
if (payload.roomNote !== undefined) sh.getRange(topRow, 6).setValue(payload.roomNote);
// ตารางล่าง: แถว 16-21 (คอลัมน์ E ถึง K = สถานะรายทีมของคณะนั้น)
const botRow = topRow + 10;
if (payload.teamStatuses) {
const ts = payload.teamStatuses;
if (ts.rattana !== undefined) sh.getRange(botRow, 5).setValue(ts.rattana);
if (ts.awatif !== undefined) sh.getRange(botRow, 6).setValue(ts.awatif);
if (ts.nurisan !== undefined) sh.getRange(botRow, 7).setValue(ts.nurisan);
if (ts.suriyanee !== undefined) sh.getRange(botRow, 8).setValue(ts.suriyanee);
if (ts.madeehah !== undefined) sh.getRange(botRow, 9).setValue(ts.madeehah);
if (ts.kantakan !== undefined) sh.getRange(botRow, 10).setValue(ts.kantakan);
if (ts.filesNote !== undefined) sh.getRange(botRow, 11).setValue(ts.filesNote);
}
}
// 3) เพิ่มหรืออัปเดตคำถามจากคณะ (แท็บที่ 3: Issue & Commitment Log)
else if (action === 'saveIssue') {
const sh = ss.getSheetByName(SHEET_ISSUES);
if (payload.rowIndex && Number(payload.rowIndex) >= 5) {
const r = Number(payload.rowIndex);
sh.getRange(r, 2, 1, 6).setValues([[
payload.faculty || '',
payload.question || '',
payload.owner || '',
payload.dueDate || '',
payload.status || 'กำลังดำเนินการ',
payload.answer || ''
]]);
} else {
// หาแถวว่างแรกตั้งแต่แถวที่ 6 หรือเพิ่มแถวต่อท้าย
const allVals = sh.getRange(5, 1, 30, 7).getValues();
let targetRow = -1;
let nextSeq = 1;
for (let i = 1; i < allVals.length; i++) {
const qText = String(allVals[i][2] || '').trim();
if (!qText) {
targetRow = i + 5;
nextSeq = allVals[i][0] || i;
break;
}
}
if (targetRow === -1) {
targetRow = sh.getLastRow() + 1;
nextSeq = targetRow - 4;
}
sh.getRange(targetRow, 1, 1, 7).setValues([[
String(nextSeq),
payload.faculty || '',
payload.question || '',
payload.owner || '',
payload.dueDate || '',
payload.status || 'กำลังดำเนินการ',
payload.answer || ''
]]);
}
} else {
throw new Error('ไม่รู้จักคำสั่ง action: ' + action);
}
SpreadsheetApp.flush();
return jsonResponse_({
status: 'success',
message: 'บันทึกข้อมูลลง Google Sheet เรียบร้อยแล้ว',
updatedAt: new Date().toISOString()
});
} catch (err) {
return jsonResponse_({
status: 'error',
message: err.toString()
});
} finally {
lock.releaseLock();
}
}
function readTasksSheet_(sh) {
if (!sh) return [];
const vals = sh.getRange(8, 1, 20, 8).getDisplayValues();
const formulas = sh.getRange(8, 5, 20, 1).getFormulas();
return vals.filter(r => r[0]).map((r, idx) => {
const f = formulas[idx][0] || '';
const urlMatch = f.match(/HYPERLINK\("([^"]+)"/i);
return {
id: r[0],
owner: r[1],
title: r[2],
dueText: r[3],
folderLabel: r[4],
folderUrl: urlMatch ? urlMatch[1] : 'https://drive.google.com/drive/folders/1dwr0i_9ub6-qGyMkw_0caRrzjdT8aVkL',
status: r[5] || 'ยังไม่เริ่ม',
note: r[6] || '',
problem: r[7] || ''
};
});
}
function readScheduleSheet_(sh) {
if (!sh) return [];
const top = sh.getRange(6, 1, 6, 12).getDisplayValues();
const bot = sh.getRange(16, 1, 6, 12).getDisplayValues();
const botFormulas = sh.getRange(16, 4, 6, 1).getFormulas();
return top.map((r, i) => {
const b = bot[i] || [];
const bf = (botFormulas[i] && botFormulas[i][0]) || '';
const urlMatch = bf.match(/HYPERLINK\("([^"]+)"/i);
return {
facultyCode: r[0],
facultyName: r[1],
planDate: r[2],
coordStatus: r[3],
actualDate: r[4],
roomNote: r[5],
impactAlert: r[6],
dMinus5: r[7],
dMinus3: r[8],
dMinus1: r[9],
dPlus1: r[10],
dPlus2: r[11],
folderUrl: urlMatch ? urlMatch[1] : 'https://drive.google.com/drive/folders/1jlZftdw28M_nmU8aEK7dHQuY49uw-5T1',
teamStatuses: {
rattana: b[4] || 'ยังไม่เริ่ม',
awatif: b[5] || 'ยังไม่เริ่ม',
nurisan: b[6] || 'ยังไม่เริ่ม',
suriyanee: b[7] || 'ยังไม่เริ่ม',
madeehah: b[8] || 'ยังไม่เริ่ม',
kantakan: b[9] || 'ยังไม่เริ่ม',
filesNote: b[10] || ''
}
};
});
}
function readIssuesSheet_(sh) {
if (!sh) return [];
const lastRow = Math.max(sh.getLastRow(), 5);
const vals = sh.getRange(5, 1, lastRow - 4, 7).getDisplayValues();
const result = [];
vals.forEach((r, idx) => {
if (String(r[1] || '').trim() || String(r[2] || '').trim()) {
result.push({
rowIndex: idx + 5,
seq: r[0],
faculty: r[1],
question: r[2],
owner: r[3],
dueDate: r[4],
status: r[5] || 'ยังไม่เริ่ม',
answer: r[6] || ''
});
}
});
return result;
}
function jsonResponse_(obj) {
return ContentService.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}