ออกแบบ Select All และจัดกลุ่มข้อมูลใน Web App ให้ใช้งานง่าย

แนวคิดออกแบบปุ่ม Select All และการจัดกลุ่มข้อมูลสำหรับ Web App บน Google Sheets เพื่อช่วยจัดการรายการจำนวนมากและลดการคลิกซ้ำ

สอนทำ Google Apps Script Web App จัดการข้อมูล (EP17)

สวัสดีครับทุกคน! กลับมาพบกับผม ตระกูล อ่อนรัตน์ (Trakun Onrat) ที่ trakun.com อีกครั้งนะครับ

ถ้าคุณเป็นคนหนึ่งที่ต้องทำงานกับข้อมูลเยอะๆ ไม่ว่าจะเป็นการจัดการสต็อกสินค้า การดูรายการสั่งซื้อ การอนุมัติเอกสาร หรือแม้แต่การจัดการคิวงานต่างๆ คุณคงคุ้นเคยกับความน่าเบื่อของการต้องมานั่ง “ติ๊กเลือกทีละช่อง” หรือการต้องกวาดสายตาหาข้อมูลที่ปะปนกันมั่วไปหมดใช่ไหมครับ?

ปัญหาเหล่านี้ดูเหมือนจะเป็นเรื่องเล็ก แต่ถ้าเราต้องทำมันทุกวัน วันละหลายๆ รอบ มันคือ “ตัวสูบเวลา” และต้นเหตุของความผิดพลาดชั้นดีเลยครับ วันนี้ผมเลยจะพาทุกคนมาสวมวิญญาณโปรแกรมเมอร์ (ฉบับคนธรรมดาก็ทำได้) อัปเกรดระบบเดิมๆ ให้กลายเป็น Web App อัจฉริยะ ที่มีฟังก์ชันตัวช่วยอย่างปุ่ม “Select All” (เลือกทั้งหมด) และ “การจัดกลุ่มข้อมูล (Categorization)” เพื่อให้ชีวิตการทำงานของเราง่ายและดู “โปร” ขึ้นแบบก้าวกระโดด!

🛑 ปัญหาคลาสสิก: เมื่อข้อมูลล้นมือ แต่ระบบไม่เป็นใจ

ลองนึกภาพตามนะครับ สมมติว่าวันนี้มีรายการสั่งซื้อ หรือรายการแจ้งซ่อมเข้ามา 50 รายการ

ในระบบเดิมๆ ที่ไม่ได้ออกแบบมาเพื่อรองรับผู้ใช้งาน (UX/UI ไม่ดี) คุณอาจจะต้องเลื่อนเมาส์คลิกกล่องสี่เหลี่ยม (Checkbox) ทีละอัน… 1, 2, 3… ไปจนถึง 50 เพื่อเปลี่ยนสถานะงาน

ยิ่งไปกว่านั้น ข้อมูลทั้ง 50 รายการดันกองรวมกันโดยไม่ได้แยกหมวดหมู่ว่า อันไหนงานด่วน อันไหนงานทั่วไป… แค่คิดก็ปวดหัวแล้วใช่ไหมครับ? นี่แหละครับคือจุดที่แยกระหว่าง “คนทำงานถึก” กับ “คนทำงานฉลาด”

ภาพเปรียบเทียบการติ๊กเลือกรายการทีละช่อง กับ Web App ที่มีปุ่มเลือกทั้งหมด (Select All)

⚖️ เปรียบเทียบให้เห็นภาพ: ทำไมเราถึงต้องมี “UI ที่ฉลาด”

เพื่อให้เห็นภาพชัดเจนว่า ทริคเล็กๆ อย่างปุ่ม Select All และการจัดหมวดหมู่ มันเปลี่ยนชีวิตเราได้แค่ไหน ลองดูตารางเปรียบเทียบนี้ครับ:

หัวข้อการใช้งาน❌ ระบบแบบเก่า (ไม่มีตัวช่วย)✅ ระบบอัจฉริยะ (มี Select All & จัดกลุ่ม)
การจัดการข้อมูลจำนวนมากต้องคลิกทีละรายการ เสียเวลาและน่าหงุดหงิดคลิกปุ่มเดียว “Select All” จัดการรวดเดียวจบ
การค้นหาและวิเคราะห์ตาลาย ข้อมูลปะปนกัน แยกความสำคัญยากข้อมูลถูกจัดกลุ่ม (หมวดหมู่) ชัดเจน ดูปุ๊บรู้ปั๊บ
โอกาสเกิดความผิดพลาดสูง (อาจเผลอคลิกข้าม หรือตกหล่นบางรายการ)ต่ำ (ระบบจัดการแบบเหมาเข่ง หรือตามกลุ่มที่เลือก)
ความรู้สึกของผู้ใช้งานเหนื่อยล้า รู้สึกเหมือนเป็นหุ่นยนต์ทำงานซ้ำๆรู้สึกควบคุมได้ ทำงานเสร็จไว มีเวลาไปทำอย่างอื่น

🛠️ โครงสร้างความฉลาด: เบื้องหลังการทำงาน (ฉบับเข้าใจง่าย)

เราสามารถสร้างระบบแบบนี้ได้ด้วยเครื่องมือฟรีที่อยู่ใน Google Ecosystem ครับ นั่นคือการใช้ Google Apps Script ร่วมกับ HTML/JavaScript สร้างหน้าเว็บ (Web App) ขึ้นมาครอบ Google Sheets อีกที

หัวใจสำคัญของการเขียนโค้ด (หน้า index.html) ในโปรเจกต์นี้ มีอยู่ 2 จุดครับ:

  1. ฟังก์ชัน Select All: เราจะเพิ่มคำสั่ง JavaScript สั้นๆ ที่บอกหน้าเว็บว่า “ถ้าฉันกดปุ่มนี้ ให้ไปติ๊กถูกที่ Checkbox ทุกอันที่แสดงอยู่บนหน้าจอนะ”
  2. การจัดกลุ่มข้อมูล (Group-based categorization): เราจะเขียนเงื่อนไขให้ระบบดึงข้อมูลจาก Google Sheets มาแล้วทำการคัดแยกตามคอลัมน์ที่เรากำหนด (เช่น แยกตาม “สถานะ”, “ประเภทสินค้า”, หรือ “แผนก”) ก่อนที่จะแสดงผลออกมาเป็นตารางแยกกัน
ภาพเปรียบเทียบตารางข้อมูลที่รกและหายาก กับหน้าจอ Web App ใหม่ที่จัดข้อมูลเป็นกลุ่มให้อ่านง่าย

ลองทำตาม: หน้าจัดการงานแบบแยกกลุ่ม พร้อมปุ่มเลือกทั้งหมด

ตัวอย่างนี้ต่อยอดจากชีต Requests ของฟอร์มแจ้งซ่อมในบทความ Google Sheets เป็น Web App หน้าเว็บจะดึงงานทั้งหมดมาแยกกลุ่มตามสถานะ แต่ละกลุ่มมีช่อง “เลือกทั้งหมด” แล้วเปลี่ยนสถานะรายการที่เลือกได้ในคลิกเดียว

หน้านี้แก้ข้อมูลได้ จึงต้องแยกเป็นโปรเจกต์ใหม่ ไม่รวมกับฟอร์มที่เปิดให้คนทั่วไปใช้ ไปที่ script.google.com กด New project แล้วคัดลอก ID ของชีตแจ้งซ่อม (ข้อความระหว่าง /d/ กับ /edit ในลิงก์ของชีต) มาใส่ในโค้ด

1. โค้ดฝั่งเซิร์ฟเวอร์ (Code.gs)

const SPREADSHEET_ID = 'วาง ID ของชีตแจ้งซ่อม';
const SHEET_NAME = 'Requests';
const STATUSES = ['รอดำเนินการ', 'กำลังซ่อม', 'เสร็จแล้ว'];

function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('จัดการงานซ่อม')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

function getSheet_() {
  return SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
}

function getRequests() {
  const rows = getSheet_().getDataRange().getDisplayValues();
  return rows.slice(1).map(function (r, i) {
    return { row: i + 2, name: r[1], detail: r[2], status: r[3] || STATUSES[0] };
  });
}

function updateStatus(rowNumbers, newStatus) {
  if (STATUSES.indexOf(newStatus) === -1) throw new Error('สถานะไม่ถูกต้อง');
  if (!Array.isArray(rowNumbers) || rowNumbers.length === 0) throw new Error('ยังไม่ได้เลือกรายการ');
  const lock = LockService.getScriptLock();
  lock.waitLock(10000);
  try {
    const sheet = getSheet_();
    const lastRow = sheet.getLastRow();
    let count = 0;
    rowNumbers.forEach(function (value) {
      const row = Number(value);
      if (Number.isInteger(row) && row >= 2 && row <= lastRow) {
        sheet.getRange(row, 4).setValue(newStatus);
        count++;
      }
    });
    return 'เปลี่ยนสถานะแล้ว ' + count + ' รายการ';
  } finally {
    lock.releaseLock();
  }
}

ฝั่งเซิร์ฟเวอร์ไม่เชื่อค่าที่หน้าเว็บส่งมาทันที แต่ตรวจว่าสถานะอยู่ในรายการที่อนุญาต และเลขแถวอยู่ในช่วงข้อมูลจริง ก่อนเขียนลงชีตภายใต้ Lock เพื่อไม่ให้สองคนแก้พร้อมกันจนข้อมูลทับกัน

2. หน้าเว็บ (index.html)

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      body { font-family: sans-serif; max-width: 640px; margin: 24px auto; padding: 0 16px; }
      section { border: 1px solid #ddd; border-radius: 12px; padding: 8px 16px; margin-bottom: 16px; }
      label { display: block; padding: 6px 0; }
    </style>
  </head>
  <body>
    <h1>จัดการงานซ่อม</h1>
    <div id="groups">กำลังโหลด...</div>
    <p>
      เปลี่ยนรายการที่เลือกเป็น
      <select id="new-status">
        <option>รอดำเนินการ</option>
        <option>กำลังซ่อม</option>
        <option>เสร็จแล้ว</option>
      </select>
      <button id="apply">บันทึก</button>
    </p>
    <p id="message"></p>
    <script>
      var groupsBox = document.getElementById('groups');
      var message = document.getElementById('message');

      function render(items) {
        var groups = {};
        items.forEach(function (item) {
          (groups[item.status] = groups[item.status] || []).push(item);
        });
        groupsBox.innerHTML = '';
        Object.keys(groups).forEach(function (status) {
          var section = document.createElement('section');
          var header = document.createElement('label');
          header.innerHTML = '<input type="checkbox"> <strong></strong>';
          header.querySelector('strong').textContent = status + ' (' + groups[status].length + ' รายการ)';
          section.appendChild(header);
          groups[status].forEach(function (item) {
            var line = document.createElement('label');
            line.innerHTML = '<input type="checkbox" class="item"> <span></span>';
            line.querySelector('.item').value = item.row;
            line.querySelector('span').textContent = item.name + ': ' + item.detail;
            section.appendChild(line);
          });
          header.querySelector('input').addEventListener('change', function () {
            var checked = this.checked;
            section.querySelectorAll('.item').forEach(function (box) { box.checked = checked; });
          });
          groupsBox.appendChild(section);
        });
      }

      function load() {
        google.script.run
          .withSuccessHandler(render)
          .withFailureHandler(function (error) { groupsBox.textContent = error.message; })
          .getRequests();
      }

      document.getElementById('apply').addEventListener('click', function () {
        var rows = Array.prototype.map.call(document.querySelectorAll('.item:checked'), function (box) {
          return Number(box.value);
        });
        if (!rows.length) { message.textContent = 'ยังไม่ได้เลือกรายการ'; return; }
        var button = this;
        button.disabled = true;
        google.script.run
          .withSuccessHandler(function (text) { message.textContent = text; button.disabled = false; load(); })
          .withFailureHandler(function (error) { message.textContent = error.message; button.disabled = false; })
          .updateStatus(rows, document.getElementById('new-status').value);
      });

      load();
    </script>
  </body>
</html>

ช่อง “เลือกทั้งหมด” ของแต่ละกลุ่มติ๊กเฉพาะรายการในกลุ่มของตัวเอง จึงไม่เผลอเลือกงานข้ามสถานะ ข้อความจากผู้แจ้งใส่ลงหน้าเว็บด้วย textContent ไม่ใช่ innerHTML เพราะข้อมูลมาจากฟอร์มที่ใครก็กรอกได้ ถ้ามีคนแอบใส่โค้ด HTML มา โค้ดนั้นจะแสดงเป็นตัวหนังสือธรรมดา ไม่ทำงานบนเครื่องผู้ดูแล

3. Deploy ให้เฉพาะผู้ดูแล

Deploy เป็น Web App ตามขั้นตอนเดิม เลือก Execute as เป็น Me และเลือก Who has access เป็น Only myself หรือ Anyone within โดเมนของหน่วยงาน เมื่อทุกคนในโดเมนเป็นผู้ดูแลงานนี้ ห้ามเลือก Anyone เพราะหน้านี้แสดงชื่อผู้แจ้งและแก้สถานะงานได้

ตัวอย่างนี้อ้างอิงงานด้วยเลขแถวเพื่อให้โค้ดสั้น ระบบที่ใช้จริงควรเพิ่มคอลัมน์รหัสงานที่ไม่ซ้ำกัน แล้วอัปเดตตามรหัสแทน เพราะถ้ามีคนเรียงหรือลบแถวในชีตระหว่างที่หน้าเว็บเปิดค้างไว้ เลขแถวจะเปลี่ยนและอาจไปแก้ผิดรายการ

อ้างอิง: HTML Service: Communicate with Server Functions, Class Lock และ คู่มือ Web Apps — ตรวจข้อมูล 18 ตุลาคม 2026

🎯 บทสรุป

การออกแบบระบบที่ดี ไม่ใช่แค่การทำให้คอมพิวเตอร์ทำงานได้ แต่มันคือการ “แก้ปัญหาให้ผู้ใช้งาน” การเพิ่มฟังก์ชันเล็กๆ อย่างปุ่ม Select All หรือการจัดหมวดหมู่ข้อมูลให้ดูง่าย คือจุดเด่นที่ทำให้เราแตกต่างและดูเป็นมืออาชีพ (Pro) มากขึ้น

หวังว่าไอเดียใน EP17 นี้จะเป็นประโยชน์ และช่วยลดเวลา “งานถึก” ให้ทุกคนได้เอาไปต่อยอดสร้างสรรค์สิ่งใหม่ๆ กันนะครับ ถ้าใครเอาไปลองทำแล้วติดขัดตรงไหน หรืออยากให้ผมทำระบบอะไรเพิ่มเติม คอมเมนต์บอกกันด้านล่างได้เลย!

อย่าลืมกดติดตาม trakun.com เพื่ออัปเดตเทคนิคดีๆ ที่จะทำให้คุณก้าวนำหน้าเทคโนโลยีไปอีกขั้นครับ

ตระกูล อ่อนรัตน์

ตระกูล อ่อนรัตน์ Avatar

ตระกูล อ่อนรัตน์

ลงมือสร้างระบบช่วยงานจริง แล้วเขียนเล่าเป็นขั้นตอนให้ทำตามได้ ติดตรงไหน ถามไว้ในคอมเมนต์ด้านล่าง หรือส่งโจทย์มาที่หน้าติดต่อ

ส่งโจทย์ให้ผม

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *