/**************************************************** * Aplikasi Form Guru Bertugas + Automasi Google Sheets * Disalin/ditiru sebagai replika fungsi umum. * Oleh: (isi nama anda jika perlu) ****************************************************/ const SHEET = { TEACHERS: 'SenaraiGuru', DUTIES: 'Tugas', HOLIDAYS: 'Cuti', SCHEDULE: 'Jadual', RESPONSES: 'Form Responses 1' }; const Q = { // Tajuk soalan pada Google Form (juga jadi header di Responses) NAME: 'Nama Guru', DATE: 'Tarikh Bertugas', SESSION: 'Sesi', DUTY: 'Tugas', NOTE: 'Catatan' }; const MENU = 'Guru Bertugas'; const PROP = PropertiesService.getScriptProperties(); const KEYS = { FORM_ID: 'FORM_ID', LAST_ROTATION_OFFSET: 'LAST_ROTATION_OFFSET' // untuk penjanaan giliran }; /** Menu utama */ function onOpen() { const ui = SpreadsheetApp.getUi(); ui.createMenu(MENU) .addItem('1) Bina / Kemas Kini Google Form', 'createOrUpdateForm') .addItem('2) Jana Jadual Mingguan (Isnin–Jumaat)', 'generateWeeklyRosterPrompt') .addItem('3) Hantar Emel Notifikasi Mingguan', 'sendWeeklyEmailsPrompt') .addToUi(); ensureBaseSheets(); } /** Pastikan helaian asas wujud & setup header Jadual */ function ensureBaseSheets() { const ss = SpreadsheetApp.getActive(); // Cipta helaian jika tiada [SHEET.TEACHERS, SHEET.DUTIES, SHEET.HOLIDAYS, SHEET.SCHEDULE].forEach(name => { if (!ss.getSheetByName(name)) ss.insertSheet(name); }); // Header Jadual const jad = ss.getSheetByName(SHEET.SCHEDULE); const headers = ['Tarikh', 'Hari', 'Sesi', 'Nama', 'Tugas', 'Catatan', 'Status']; const firstRow = jad.getRange(1,1,1,headers.length).getValues()[0]; const needHeader = headers.some((h,i)=> firstRow[i] !== h); if (needHeader) { jad.clear(); jad.getRange(1,1,1,headers.length).setValues([headers]); jad.setFrozenRows(1); } } /** Baca senarai nilai dari satu kolum (unik, tak kosong) */ function getList(sheetName, colIndex=1) { const sh = SpreadsheetApp.getActive().getSheetByName(sheetName); if (!sh) return []; const last = sh.getLastRow(); if (last < 2) return []; const vals = sh.getRange(2, colIndex, last-2+1, 1).getValues().flat() .map(v => (v||'').toString().trim()) .filter(v => v); return Array.from(new Set(vals)); } /** CIPTA atau KEMAS KINI Google Form & link ke spreadsheet ini */ function createOrUpdateForm() { ensureBaseSheets(); const ss = SpreadsheetApp.getActive(); const teachers = getList(SHEET.TEACHERS, 1); const duties = getList(SHEET.DUTIES, 1); if (teachers.length === 0) throw new Error('SenaraiGuru kosong. Isi Nama (kolum A) dan Emel (kolum B).'); if (duties.length === 0) throw new Error('Helaian Tugas kosong. Isi senarai tugasan.'); let form; const formId = PROP.getProperty(KEYS.FORM_ID); if (formId) { try { form = FormApp.openById(formId); // Bersih & bina semula item untuk sinkron form.getItems().forEach(it => form.deleteItem(it)); } catch (e) { // Jika gagal buka, cipta baharu form = FormApp.create('Borang Guru Bertugas'); PROP.setProperty(KEYS.FORM_ID, form.getId()); } } else { form = FormApp.create('Borang Guru Bertugas'); PROP.setProperty(KEYS.FORM_ID, form.getId()); } // Item: Nama Guru (dropdown) form.addListItem() .setTitle(Q.NAME) .setChoiceValues(teachers) .setRequired(true); // Item: Tarikh Bertugas form.addDateItem() .setTitle(Q.DATE) .setRequired(true); // Item: Sesi form.addMultipleChoiceItem() .setTitle(Q.SESSION) .setChoiceValues(['Pagi','Petang']) .setRequired(true); // Item: Tugas form.addListItem() .setTitle(Q.DUTY) .setChoiceValues(duties) .setRequired(true); // Item: Catatan form.addParagraphTextItem() .setTitle(Q.NOTE) .setRequired(false); // Linkkan respon ke Spreadsheet ini form.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()); SpreadsheetApp.getUi().alert('Borang siap! Tajuk: "'+ form.getTitle() + '". Respon ke "'+ SHEET.RESPONSES +'".'); } /** Trigger spreadsheet "On form submit" -> Process */ function onFormSubmit(e) { // Tip: Set up installable trigger: Triggers > Add Trigger > onFormSubmit > From spreadsheet > On form submit try { processSubmission_(); } catch (err) { console.error(err); } } /** Proses respon terbaru dan tulis ke Jadual */ function processSubmission_() { ensureBaseSheets(); const ss = SpreadsheetApp.getActive(); const resp = ss.getSheetByName(SHEET.RESPONSES); if (!resp) return; const lastRow = resp.getLastRow(); if (lastRow < 2) return; const header = resp.getRange(1,1,1, resp.getLastColumn()).getValues()[0]; const row = resp.getRange(lastRow,1,1, resp.getLastColumn()).getValues()[0]; // Dapatkan nilai ikut tajuk soalan const getByTitle = (title) => { const idx = header.indexOf(title); return idx === -1 ? '' : row[idx]; }; const nama = getByTitle(Q.NAME); const tarikh = new Date(getByTitle(Q.DATE)); const sesi = getByTitle(Q.SESSION); const tugas = getByTitle(Q.DUTY); const catatan = getByTitle(Q.NOTE); // Skip jika tarikh cuti if (isHoliday_(tarikh)) { appendScheduleRow_({tarikh, hari: getDayName_(tarikh), sesi, nama, tugas, catatan, status: 'Cuti - tidak dijadualkan'}); return; } appendScheduleRow_({tarikh, hari: getDayName_(tarikh), sesi, nama, tugas, catatan, status: 'Dijadualkan'}); } /** Tambah satu rekod ke helaian Jadual */ function appendScheduleRow_({tarikh, hari, sesi, nama, tugas, catatan, status}) { const sh = SpreadsheetApp.getActive().getSheetByName(SHEET.SCHEDULE); sh.appendRow([tarikh, hari, sesi, nama, tugas, catatan, status]); // Format tarikh & auto-size const lastRow = sh.getLastRow(); sh.getRange(lastRow,1).setNumberFormat('yyyy-mm-dd'); sh.autoResizeColumns(1, sh.getLastColumn()); } /** Semak tarikh cuti */ function isHoliday_(dateObj) { const sh = SpreadsheetApp.getActive().getSheetByName(SHEET.HOLIDAYS); if (!sh) return false; const last = sh.getLastRow(); if (last < 2) return false; const vals = sh.getRange(2,1,last-1,1).getValues().flat().filter(v => v); const dStr = toYMD_(dateObj); return vals.some(v => toYMD_(new Date(v)) === dStr); } function toYMD_(d) { const y = d.getFullYear(); const m = ('0'+(d.getMonth()+1)).slice(-2); const da = ('0'+d.getDate()).slice(-2); return `${y}-${m}-${da}`; } function getDayName_(d) { const days = ['Ahad','Isnin','Selasa','Rabu','Khamis','Jumaat','Sabtu']; return days[d.getDay()]; } /** Prompt pilih tarikh mula minggu (Isnin) */ function generateWeeklyRosterPrompt() { const ui = SpreadsheetApp.getUi(); const resp = ui.prompt('Tarikh mula minggu','Masukkan tarikh Isnin (yyyy-mm-dd):', ui.ButtonSet.OK_CANCEL); if (resp.getSelectedButton() !== ui.Button.OK) return; const d = new Date(resp.getResponseText()); generateWeeklyRoster_(d); ui.alert('Jadual mingguan dijana.'); } /** Jana jadual Isnin–Jumaat ikut senarai guru & tugasan; skip cuti */ function generateWeeklyRoster_(mondayDate) { ensureBaseSheets(); const teachers = getList(SHEET.TEACHERS, 1); // nama const duties = getList(SHEET.DUTIES, 1); if (teachers.length === 0 || duties.length === 0) throw new Error('SenaraiGuru/Tugas kosong.'); // "Giliran" ringkas: offset bertambah setiap kali jana let offset = parseInt(PROP.getProperty(KEYS.LAST_ROTATION_OFFSET) || '0', 10); const jad = SpreadsheetApp.getActive().getSheetByName(SHEET.SCHEDULE); for (let i=0; i<5; i++) { // Isnin–Jumaat const d = new Date(mondayDate); d.setDate(d.getDate() + i); if (isHoliday_(d)) { appendScheduleRow_({tarikh:d, hari:getDayName_(d), sesi:'-', nama:'-', tugas:'-', catatan:'Cuti', status:'Cuti - tidak dijadualkan'}); continue; } // Tugaskan seorang guru untuk setiap tugas (atau satu tugas sahaja — ubah ikut keperluan) duties.forEach((tugas, di) => { const guru = teachers[(offset + i + di) % teachers.length]; appendScheduleRow_({tarikh:d, hari:getDayName_(d), sesi:'Pagi', nama:guru, tugas, catatan:'', status:'Dijadualkan (Auto)'}); }); } PROP.setProperty(KEYS.LAST_ROTATION_OFFSET, (offset + 1).toString()); } /** Prompt emel minggu bermula pada tarikh */ function sendWeeklyEmailsPrompt() { const ui = SpreadsheetApp.getUi(); const resp = ui.prompt('Tarikh mula minggu','Masukkan tarikh Isnin (yyyy-mm-dd):', ui.ButtonSet.OK_CANCEL); if (resp.getSelectedButton() !== ui.Button.OK) return; const monday = new Date(resp.getResponseText()); const count = sendWeeklyEmails_(monday); ui.alert(`Emel dihantar kepada ${count} penerima.`); } /** Hantar emel notifikasi untuk jadual minggu tersebut */ function sendWeeklyEmails_(mondayDate) { const mapEmail = buildEmailMap_(); // {Nama: Emel} const sh = SpreadsheetApp.getActive().getSheetByName(SHEET.SCHEDULE); const last = sh.getLastRow(); if (last < 2) return 0; const rows = sh.getRange(2,1,last-1, sh.getLastColumn()).getValues(); const start = new Date(mondayDate); const end = new Date(mondayDate); end.setDate(end.getDate()+6); let sent = 0; rows.forEach(r => { const [tarikh, , sesi, nama, tugas, catatan, status] = r; if (!tarikh) return; const d = new Date(tarikh); if (d < start || d > end) return; if (!nama || nama === '-' || String(status).match(/cuti/i)) return; const email = mapEmail[nama]; if (!email) return; const subj = `Notifikasi Guru Bertugas: ${toYMD_(d)} (${sesi})`; const body = `Assalamualaikum / Salam sejahtera ${nama}, Anda telah dijadualkan sebagai GURU BERTUGAS. Butiran: - Tarikh: ${toYMD_(d)} (${getDayName_(d)}) - Sesi: ${sesi} - Tugasan: ${tugas} - Catatan: ${catatan || '-'} Pautan WhatsApp (pra-isi, jika perlu): ${makeWhatsAppLink_(nama, d, sesi, tugas)} Terima kasih.`; MailApp.sendEmail(email, subj, body); sent++; }); return sent; } function buildEmailMap_() { const sh = SpreadsheetApp.getActive().getSheetByName(SHEET.TEACHERS); const last = sh.getLastRow(); const map = {}; if (last >= 2) { const vals = sh.getRange(2,1,last-1,2).getValues(); // Nama, Emel vals.forEach(([nama, emel]) => { nama = (nama||'').toString().trim(); emel = (emel||'').toString().trim(); if (nama && emel) map[nama] = emel; }); } return map; } /** Pautan WhatsApp pra-isi mesej (optional) */ function makeWhatsAppLink_(nama, d, sesi, tugas) { const msg = `Assalamualaikum ${nama}. Anda bertugas pada ${toYMD_(d)} (${getDayName_(d)}), sesi ${sesi}, tugasan: ${tugas}.`; return 'https://wa.me/?text=' + encodeURIComponent(msg); } /** (Pilihan) Trigger mingguan automatik – set manual dalam Triggers: * Time-driven → Weekly → pilih hari & masa → jalankan fungsi ini */ function weeklyAuto_() { // Contoh: ambil Isnin minggu semasa const today = new Date(); const monday = new Date(today); const day = monday.getDay(); // 0 Ahad, 1 Isnin const diffToMonday = (day === 0 ? -6 : 1 - day); monday.setDate(monday.getDate() + diffToMonday); generateWeeklyRoster_(monday); sendWeeklyEmails_(monday); }