Lab Week 11 — หน้าจอจัดการพนักงาน (Java Swing) · BONUS SUKI
เปิดเรื่อง หน้า 1 · อ่าน ~5 นาที

Lab Week 11 — หน้าจอจัดการพนักงานระบบ POS

ต่อจาก Lab Week 10 (โปรเจกต์ MoogataLab เดิม) · Java Swing + Login + สิทธิ์ผู้จัดการ · ธีมร้านหมูกะทะ BONUS SUKI

📖 เรื่องราวของ Lab นี้
จำตอนจบ Lab Week 10 ได้ไหม? เรามีระบบที่ "ทำงานได้" แต่ยัง "ใช้ไม่ได้จริง" — ล็อกอิน somchai / 1234 สำเร็จ แต่ต้องไปพิมพ์ในโปรแกรม main บนคอนโซลเหมือนนักพัฒนา ไม่ใช่พนักงานหน้าร้าน ทั้งที่เพิ่มพนักงานใหม่ก็ต้องรู้ชื่อคลาส ชื่อเมธอด แล้วแก้โค้ดเอง — แบบนั้นพนักงานร้านหมูกะทะใช้ไม่ได้แน่

Lab นี้คือบทถัดไปของเรื่อง: เราจะเอาชั้นที่ขาดหายไปคือ หน้าจอ (UI) มาต่อบนเลเยอร์ที่สร้างไว้ ให้พนักงานเปิดโปรแกรม เห็นหน้าต่างล็อกอิน พิมพ์ชื่อ-รหัสผ่าน แล้วจัดการข้อมูลพนักงานด้วยการคลิกและพิมพ์บนฟอร์มเหมือนโปรแกรมจริง

สิ่งที่คุณจะสร้างได้ (What you'll build)

จบ codelab นี้คุณจะมี 2 โปรเจกต์ ที่ใช้หลังบ้าน (ฐานข้อมูล/DAO/Service) เหมือนกันทุกบรรทัด ต่างกันแค่หน้าบ้าน:

โปรเจกต์ 1 — MoogataLab (ส่วนที่ 1)โปรเจกต์ 2 — MoogataLabDialog (ส่วนที่ 2)
โครงหน้าจัดการหน้าต่างเดียวจบ: ฟอร์ม input อยู่ด้านบน ตารางอยู่ด้านล่าง เพิ่ม/แก้บนฟอร์มเดียวกัน สลับโหมดด้วยการคลิกแถว/กดล้างฟอร์ม หน้าหลักมีแต่ตารางกับปุ่ม — เวลาเพิ่ม/แก้จะเด้งฟอร์มเป็น dialog ขึ้นมา บันทึกหรือยกเลิกแล้วค่อยกลับมา
เรียนรู้เด่นการคุมสถานะสองโหมดบนฟอร์มเดียว (editingId) + ListSelectionListenerการแยกฟอร์มเป็น modal dialog + เส้นทางยกเลิก

ทั้งสองโปรเจกต์มีเส้นทางเดียวกัน: เปิดโปรแกรม → ล็อกอิน → เฉพาะผู้จัดการ (role = 1) เข้าหน้าจัดการพนักงานได้ → เพิ่ม/แก้/ลบ/รีเฟรช

เส้นทางทั้งหมด 17 หน้า

หน้าเรื่องสิ่งที่จะได้ตอนจบหน้า
2ทฤษฎีที่ต้องเข้าใจก่อนลงมือเข้าใจ event-driven, EDT, modal, model-view
3ชุด Component Swing ที่ใช้รู้จักหน้าที่/จุดพลาดของ control ทุกตัวที่จะลาก
4–11ส่วนที่ 1 (Step 1–8): ต่อยอดใน MoogataLab เดิม สร้างแบบฟอร์มรวมแอปครบวงจร ผ่านทดสอบ 8 สถานการณ์
12–15ส่วนที่ 2 (Step 9–12): คัดลอกโปรเจกต์ รื้อเป็นแบบ dialogแอปแบบ dialog ผ่านทดสอบ 10 สถานการณ์
16–17Troubleshooting · เช็คลิสต์ส่งงาน · สรุปเรื่องราวงานพร้อมส่ง
ก่อนเริ่ม ต้องมี:
  • โปรเจกต์ MoogataLab ของตัวเองจาก Lab Week 10 — รัน TestEmployeeService ได้ Welcome user : somchai (แคชเชียร์)
  • JDK 17+ · Apache NetBeans 22+ · Letos (ชื่อเดิม SQLiteStudio) — เครื่องมือชุดเดิม
  • Lab นี้ ไม่สร้างโปรเจกต์ใหม่ ไม่สร้างตารางใหม่ — ส่วนที่ 1 ต่อยอดในโปรเจกต์เดิม ส่วนที่ 2 ค่อยคัดลอกไปทำเวอร์ชัน dialog
วิธีใช้ codelab นี้: กดปุ่ม "ถัดไป →" ด้านล่าง (หรือลูกศร ←/→ บนคีย์บอร์ด) เพื่อเดินทีละหน้า แต่ละ Step จบด้วย "✓ เช็ค" ให้รันพิสูจน์ก่อนไปต่อ และมีข้อความ git commit ให้เก็บงานทุกช่วง · ปุ่ม ☰ สารบัญด้านบนใช้กระโดดข้ามหน้าได้
เปิดเรื่อง หน้า 2 · ทฤษฎี 7 เรื่องที่จะใช้จริงใน lab นี้

ทฤษฎีที่ต้องเข้าใจก่อนลงมือ

อ่านรอบแรกแบบคร่าว ๆ ก็ได้ — เมื่อถึง Step ไหนที่ใช้เรื่องนั้น จะมีลิงก์ย้อนกลับมาที่นี่ทุกครั้ง ทฤษฎีจะเกาะเมื่อเห็นของจริง

1) Swing คืออะไร — ทำไมไม่ต้องติดตั้งอะไรเพิ่ม

Swing คือชุดไลบรารีสร้างหน้าจอ (GUI toolkit) ที่ มาพร้อม JDK ตั้งแต่เกิด อยู่ใน package javax.swing.* ทุกปุ่ม ทุกช่องกรอก ทุกตารางใน lab นี้วาดด้วย Java เอง (lightweight) จึงหน้าตาเหมือนกันทุก OS และ pom.xml ไม่ต้องเพิ่ม dependency แม้แต่บรรทัดเดียว — ต่างจาก sqlite-jdbc ที่ต้องประกาศเอง
🔗 ใช้จริงที่: ทุกไฟล์ใน package ui (Step 3 เป็นต้นไป)

2) Container กับ Component — ของซ้อนกันเป็นชั้น

ทุกสิ่งบนหน้าจอคือ Component (ปุ่ม ช่องกรอก ตาราง) และ component บางชนิดทำหน้าที่เป็น Container (ภาชนะวางของ) ซ้อนกันเป็นชั้น ๆ:
JFrame (หน้าต่าง) → contentPane (พื้นผิว) → JLabel / JTextField / JButton / JScrollPane → JTable
หน้าต่าง 1 บาน = 1 JFrame กำหนดชื่อหัวจอ (title), พฤติกรรมปุ่ม X (defaultCloseOperation), ขนาด — ส่วนหน้าออกแบบของ NetBeans (Free Design) ที่เราใช้ลากวาง ใช้ layout ชื่อ GroupLayout ที่ NetBeans สร้างโค้ดให้เอง พฤติกรรมเหมือน "วางพิกัดตรง" คือตำแหน่ง/ขนาดติดหนึบตามที่ลาก ผู้ใช้ย่อหน้าต่างแล้วของไม่ไหลตาม (ระบบจริงจะใช้ Layout Manager อื่น เช่น BorderLayout/GridBagLayout — หัวข้อของคอร์สถัดไป)
⚠ ห้ามไปเมนู Set Layout เอง: อย่าเลือก Set Layout → Absolute Layout ที่หน้าตาคล้ายกัน — ถ้าเลือก NetBeans จะเพิ่มไลบรารี AbsoluteLayout เข้า pom.xml โดยไม่จำเป็น (ขัดกับที่ lab บอกว่า pom ไม่ต้องเพิ่ม dependency ใด ๆ) ให้ใช้ Free Design ที่มาพร้อมฟอร์มตั้งแต่ New → JFrame Form เท่านั้น

3) Event-driven — โปรแกรม GUI "รอเหตุการณ์" ไม่ได้ไล่โค้ดบนลงล่าง

โปรแกรมคอนโซลที่เคยเขียน รันบน จบล่าง แล้วจบ แต่โปรแกรม GUI เปิดหน้าต่างแล้วหยุดรอ — จะไม่มีอะไรเกิดขึ้นจนกว่าผู้ใช้จะทำอะไรบางอย่าง (กดปุ่ม, คลิกแถว, พิมพ์) เหตุการณ์เหล่านั้นเรียกว่า event และโค้ดที่เราอยากให้รันเมื่อ event เกิด เรา "ผูก" ไว้กับตัว event ผ่าน Listener:
ผู้ใช้กดปุ่ม → Swing สร้าง ActionEvent → ส่งให้ ActionListener ที่ผูกไว้กับ btnLogin → รัน btnLoginActionPerformed(...) → กลับไปรอ event ถัดไป
ใน NetBeans การ ดับเบิลคลิกปุ่มใน designer จะสร้างเมธอด handler + ผูก listener ให้พร้อมกันทีเดียว (ถ้าพิมพ์เมธอดเองลอย ๆ โดยไม่มีการผูก — ปุ่มจะกดแล้วเงียบ นี่คือ Troubleshooting ที่เจอบ่อยที่สุด)
🔗 ใช้จริงที่: btnLogin (Step 4), btnSave/btnNew/btnDelete/btnRefresh/btnAdd/btnEdit/btnOK/btnCancel, และ ListSelectionListener ของตาราง (Step 7)

4) EDT (Event Dispatch Thread) — หน้าจอมีเธรดประจำการของตัวเอง

การวาดหน้าจอทุกพิกเซลและการส่ง event ทุกอันเกิดบนเธรดเดียวชื่อ EDT — เพราะฉะนั้นเปิดหน้าต่างต้องสั่งบน EDT ผ่าน invokeLater — ในโค้ดของ lab เราใช้ java.awt.EventQueue.invokeLater(...) (วิธีเดียวกัน) ซึ่งจะเห็นใน main() ของ LoginFrame — main เดียวที่เหลือตอนจบ lab และกติกาสำคัญคือ อย่าทำงานหนักบน EDT เพราะเธรดตัวเดียวติดงานหนักเมื่อไหร่ หน้าจอค้างทั้งบานทันที
ความจริงเรื่อง lab นี้: เราเรียกฐานข้อมูลจาก event handler ตรง ๆ (จึง "ทำงานบน EDT") — ยอมรับได้เพราะ SQLite ไฟล์เดียวเร็วมาก แต่ระบบจริงต้องย้ายงาน db ไปเธรดหลังบ้าน (เช่น SwingWorker) ซึ่งเป็นหัวข้อต่อยอด — จดไว้ในแบบฝึกหัดได้

5) Modal vs Non-modal — กล่องที่ "ล็อก" หน้าจอ

Modal dialog = เปิดแล้วหน้าต่างหลักถูกล็อก กดอะไรไม่ได้จนกว่าจะปิดกล่อง (ผู้ใช้ถูกบังคับให้ตอบจบเรื่องนี้ก่อน) · Non-modal = เปิดค้างไว้ได้ สลับไปกดหน้าอื่นได้ ใน lab นี้: EmployeeDialog สร้างด้วย super(owner, title, true) — ตัว true คือ modal และ JOptionPane.showConfirmDialog (ยืนยันลบ) ก็เป็น modal โดยธรรมชาติ
🔗 ใช้จริงที่: Step 10 (EmployeeDialog) และปุ่มลบทุกหน้าจอ — ข้อทดสอบ "กดยกเลิกแล้วตารางไม่เปลี่ยน" ก็พิสูจน์เรื่องนี้

6) JTable + DefaultTableModel — หน้าตา กับ ข้อมูล แยกกัน (MVC ย่อ)

JTable เป็นแค่ "หน้าตา" ส่วนข้อมูลจริง (หัวคอลัมน์ + แถว) อยู่ใน model ชื่อ DefaultTableModel — หลักการเดียวกับที่เราแยก model/dao มาทั้ง lab 10: แก้ข้อมูลที่ model แล้วหน้าตาวาดใหม่เอง
โจทย์ของหน้าจัดการคือ "โชว์ข้อมูลที่ถูกต้องเสมอ" — เราเลือกวิธี ล้างแล้วเติมใหม่ทั้งชุด: ทุกครั้งที่ข้อมูลเปลี่ยน ให้สร้าง model ใหม่จาก service.getAll() แล้ว setModel() ทิ้งตัวเก่า ง่าย ไม่มีทาง sync คลาด (แลกกับอ่าน db ซ้ำ — ซึ่งถูกมากสำหรับข้อมูลระดับนี้)
อีกข้อบังคับ: JTable ต้องวางใน JScrollPane ไม่งั้นหัวคอลัมน์หาย
🔗 ใช้จริงที่: showEmployees() (Step 6) — เมธอดเดียวเรียกซ้ำทุกครั้งที่ข้อมูลเปลี่ยน

7) JOptionPane — กล่องข้อความสำเร็จรูป

กล่องแจ้งเตือน/ถามตอบที่ Java เตรียมให้ ไม่ต้องออกแบบเอง มี 3 แบบที่ lab นี้ใช้:
เมธอดใช้เมื่อไหร่รูปแบบ
showMessageDialog(..., ERROR_MESSAGE)ทำไม่สำเร็จ (login ผิด, ชื่อซ้ำ)ไอคอนแดง ✕ + ปุ่ม OK
showMessageDialog(..., WARNING_MESSAGE)ยังไม่ได้เลือก / ไม่มีสิทธิ์ / ข้อมูลไม่ครบไอคอนเหลือง ⚠ + ปุ่ม OK
showConfirmDialog(..., YES_NO_OPTION)ถามก่อนทำสิ่งที่กู้ไม่ได้ (ลบ)คืนค่า JOptionPane.YES_OPTION / NO_OPTION ให้เราเช็ค
เปิดเรื่อง หน้า 3 · คู่มือฉบับพก — ทุกตัวที่จะลากใน NetBeans

ชุด Component Swing ที่ใช้ใน Lab นี้

แต่ละการ์ด: หน้าที่ · ใช้ที่หน้าจอไหน (ชื่อตัวแปรที่จะตั้ง) · จุดพลาดประจำตัว — เปิดหน้านี้คู่ไว้ตอนลาก control ได้เลย

ภาพรวมตำแหน่งของแต่ละ component บนหน้าจอจริง

LoginFrame
จากหน้าล็อกอิน (Step 3): JLabel (ข้อความนิ่ง: ① lblHeader ② lblLogin ④ lblPassword) · JTextField (③ txtLogin ช่องข้อความปกติ) · JPasswordField (⑤ txtPassword — พิมพ์แล้วเห็นจุดดำ) · JButton (⑥ btnLogin ⑦ btnExit) — ทั้งหมดวางอยู่บน JFrame
EmployeeDialog components
จากฟอร์ม dialog (Step 10): เพิ่ม JComboBox (⑧ cmbRole — dropdown เลือก 1 จากหลายตัวเลือก) และตัว dialog เองคือ JDialog (หน้าต่างลอยแบบ modal) — ปุ่ม ⑨⑩ คือ JButton ปกติ
JTable and JScrollPane
จากหน้าจัดการพนักงาน (Step 5/11): JTable (tblEmployee ตารางข้อมูล) · JScrollPane (jScrollPane1 — ภาชนะครอบตาราง ให้มี scroll bar และหัวคอลัมน์ติดอยู่) · JLabel ตัวใหญ่บนสุด (lblWelcome) · แถว JButton ด้านล่าง

การ์ดคู่มือราย component

JFrame — หน้าต่างหลัก

javax.swing.JFrame
หน้าที่: หน้าต่างระดับบนสุด 1 บาน มี title bar ปุ่ม X — หน้าจอ "หลัก" ของโปรแกรมใช้ตัวนี้
ใช้ที่: LoginFrame · EmployeeFrame (สร้างผ่าน New → JFrame Form)
คำสั่งที่ใช้: setTitle · setDefaultCloseOperation(EXIT_ON_CLOSE / DISPOSE_ON_CLOSE) · setVisible(true) · dispose() ปิดตัวเอง
จุดพลาด: EXIT_ON_CLOSE = ปิดทั้งโปรแกรม / DISPOSE_ON_CLOSE = ปิดเฉพาะหน้านี้ — เลือกผิดตัวแล้วโปรแกรมตายหรือค้างเฉย ๆ

JDialog — กล่องลอย (มักเป็น modal)

javax.swing.JDialog
หน้าที่: หน้าต่างรองที่ "ผูก" กับหน้าต่างหลัก ใช้เป็นฟอร์มย่อยเปิดเมื่อต้องการ
ใช้ที่: EmployeeDialog (New → JDialog Form) — constructor ต้องส่ง super(owner, title, true) = modal
คำสั่งที่ใช้: setVisible(true) (โปรแกรมรอตรงนี้จนปิด) · dispose() ปิดตัวเอง · isSaved() (เมธอดที่เราเพิ่มเองเพื่อตอบกลับหน้าหลัก)
จุดพลาด: ลืม true ตัวที่สาม → ผู้ใช้คลิกหน้าหลักพลาดได้ระหว่างกรอก

JLabel — ข้อความนิ่ง

javax.swing.JLabel
หน้าที่: โชว์ข้อความที่ผู้ใช้แก้ไม่ได้ — หัวเรื่อง, ป้ายหน้าช่องกรอก, สถานะ
ใช้ที่: lblHeader · lblLogin/lblPassword/... · lblWelcome (ตั้งข้อความด้วยโค้ดตอนเปิดจอ)
คำสั่งที่ใช้: setText(...) · ตั้ง font ได้ (lblHeader ใช้ Leelawadee UI Bold 18)
จุดพลาด: ตั้ง font ที่ไม่มีอักษรไทย → โชว์กล่อง □□□ (แก้ด้วย Leelawadee UI / Tahoma)

JTextField — ช่องข้อความ 1 บรรทัด

javax.swing.JTextField
หน้าที่: ให้ผู้ใช้พิมพ์ข้อความปกติ
ใช้ที่: txtLogin (ชื่อเข้าใช้) · txtPosition (ตำแหน่ง)
คำสั่งที่ใช้: getText().trim() อ่านค่ามาใช้ · setText("") ล้างช่อง
จุดพลาด: อ่านค่าแล้วไม่ trim() → เว้นวรรคท้ายช่องกลายเป็นชื่อคนละคน

JPasswordField — ช่องรหัสผ่าน

javax.swing.JPasswordField
หน้าที่: เหมือน JTextField แต่พิมพ์แล้วโชว์จุดดำ ไม่โชว์ตัวอักษร
ใช้ที่: txtPassword ทุกหน้าจอ
คำสั่งที่ใช้: new String(txtPassword.getPassword()) — ต้องแปลง เพราะ getPassword() คืน char[] ไม่ใช่ String
จุดพลาด: ใช้ getText() กับ JPasswordField (วิธีเก่า เลิกใช้แล้ว) หรือลืมแปลง char[] → คอมไพล์ไม่ผ่าน

JButton — ปุ่ม

javax.swing.JButton
หน้าที่: ตัวจุด event หลักของทั้ง lab — ผู้ใช้กด แล้วโค้ดเรารัน
ใช้ที่: btnLogin/btnExit (จอล็อกอิน) · btnSave/btnNew/btnDelete/btnRefresh (แบบรวม) · btnAdd/btnEdit/btnDelete/btnRefresh (แบบ dialog) · btnOK/btnCancel (dialog ฟอร์ม)
คำสั่งที่ใช้: ดับเบิลคลิกใน designer เพื่อสร้าง xxxActionPerformed + ผูก listener อัตโนมัติ
จุดพลาด: สร้างปุ่มใหม่แล้ววางโค้ดโดยไม่ดับเบิลคลิกก่อน → ไม่มีการผูก event → กดแล้วเงียบ

JComboBox — dropdown เลือก 1 จากหลายตัวเลือก

javax.swing.JComboBox<String>
หน้าที่: จำกัดทางเลือกให้ผู้ใช้เลือกจากลิสต์ที่กำหนดเท่านั้น (บทบาทมีได้แค่ 2 แบบ จึงไม่ควรให้พิมพ์เอง)
ใช้ที่: cmbRole — model 2 บรรทัด: พนักงาน (index 0), ผู้จัดการ (index 1)
คำสั่งที่ใช้: getSelectedIndex() อ่านว่าเลือกบรรทัดไหน · setSelectedIndex(i) ตั้งค่า (ตอนโหมดแก้ไข)
จุดพลาด: เก็บข้อความไทยลง db — เราแปลง index ↔ เลข role (1/2) ด้วยเมธอด getSelectedRole/setSelectedRole เสมอ

JTable — ตารางข้อมูล (ส่วนหน้าตา)

javax.swing.JTable
หน้าที่: โชว์ข้อมูลเป็นแถว/คอลัมน์ + ให้ผู้ใช้คลิกเลือกแถว
ใช้ที่: tblEmployee — ตั้งเลือกได้ทีละ 1 แถว: setSelectionMode(SINGLE_SELECTION)
คำสั่งที่ใช้: setModel(model) เติมข้อมูล · getSelectedRow() · getValueAt(row, col) · clearSelection()
ผูก event คลิกแถว: JTable ไม่มี addListSelectionListener ของตัวเอง (มีเฉพาะ JList) จึงไม่มีเมนู Events → ListSelection ใน designer — ต้องพิมพ์ผูกเองใน constructor: getSelectionModel().addListSelectionListener(...) (Step 7)
จุดพลาด: อ่านค่าจากตารางไปแก้ไขต่อ — ควรอ่านซ้ำจากฐานข้อมูลด้วย id ทุกครั้ง (Step 7)

JScrollPane — ภาชนะให้เลื่อนดูได้

javax.swing.JScrollPane
หน้าที่: ครอบ component ที่ใหญ่กว่าพื้นที่วาง แล้วให้ scroll bar อัตโนมัติ
ใช้ที่: jScrollPane1 ครอบ tblEmployee — ลาก Table ใน NetBeans แล้วได้มาให้เอง (คงชื่อเดิมไว้ได้)
คำสั่งที่ใช้: setViewportView(tblEmployee) — NetBeans ใส่ให้แล้ว
จุดพลาด: วาง JTable ตรง ๆ ไม่มี JScrollPane → หัวคอลัมน์หายตอนรัน

DefaultTableModel — ข้อมูลของตาราง (ส่วนหัวคอลัมน์+แถว)

javax.swing.table.DefaultTableModel
หน้าที่: "model" ของ JTable — เก็บหัวคอลัมน์และแถวข้อมูล ตารางวาดจากตรงนี้
ใช้ที่: ใน showEmployees() — สร้างใหม่พร้อมหัวคอลัมน์ → addRow(...) ทีละพนักงาน → tblEmployee.setModel(model)
คำสั่งที่ใช้: new DefaultTableModel(data, headers) · addRow(Object[]) · setModel(...)
จุดพลาด: พยายาม "แก้แถวบนตารางตรง ๆ" — ควรแก้ที่ model หรือที่เราเลือก: ล้าง-เติมใหม่จากฐานข้อมูลทั้งชุด

JOptionPane — กล่องแจ้งเตือน/ถามตอบสำเร็จรูป

javax.swing.JOptionPane
หน้าที่: กล่อง dialog พร้อมใช้ — ไม่ต้องออกแบบหน้าต่างใหม่สำหรับข้อความง่าย ๆ
ใช้ที่: แจ้ง error (แดง), เตือน (เหลือง), ถามยืนยันลบ (Yes/No) — ทุกหน้าจอ ทุกปุ่มบันทึก/ลบ
คำสั่งที่ใช้: showMessageDialog(parent, msg, title, TYPE) · showConfirmDialog(parent, msg, title, YES_NO_OPTION, QUESTION_MESSAGE) คืน int ให้เช็ค
จุดพลาด: ส่ง this (parent) ผิด → กล่องเด้งไปคนละจอ หรือไม่มี parent ให้ล็อก

Listener (ไม่ใช่ component — คือ "สายไฟ")

ActionListener · ListSelectionListener
หน้าที่: ตัวกลางที่ผูก "event" เข้ากับ "โค้ดของเรา" — ตามทฤษฎีข้อ 3 ของหน้าที่แล้ว
ใช้ที่: ActionListener กับทุกปุ่ม (ดับเบิลคลิกใน designer ได้) · ListSelectionListener กับตาราง — ต้องพิมพ์ผูกเองใน constructor ผ่าน getSelectionModel().addListSelectionListener(...) (JTable ไม่มีเมนูนี้ใน designer ต่างจาก JList)
คำสั่งที่ใช้: ปุ่ม = สร้างผ่าน designer (ดับเบิลคลิก) · ตาราง = พิมพ์ handler เองแล้วผูกใน constructor (ดูการ์ด JTable)
จุดพลาด: listener ของตารางยิงทุกครั้งที่ selection เปลี่ยน รวมถึงตอน clearSelection() — ต้องมี guard if (row == -1) return;
แผนที่ component → Step ที่ใช้: JFrame/JLabel/JTextField/JPasswordField/JButton = Step 3–4 (จอ login) · JLabel ใหญ่/JTable/JScrollPane = Step 5–6 · ListSelectionListener/JOptionPane ยืนยัน = Step 7 · JDialog/JComboBox = Step 10 · ปุ่มชุด btnAdd/btnEdit = Step 11
ส่วนที่ 1 — ฟอร์ม input รวม Step 1 จาก 8 · หน้า 4/17

Step 1 — เปิดโปรเจกต์ MoogataLab เดิม แล้วพิสูจน์ว่าของเดิมยังรันผ่าน

ก่อนต่อเรื่องใหม่ ต้องมั่นใจว่าตอนจบเก่ายังอยู่จริง — lab นี้จะยืนบนโครง lab 10 ทั้งหมด ถ้าฐานเอียงตั้งแต่ต้น ทุกอย่างที่ต่อทับจะพังตามโดยหาต้นตอยาก
  1. เปิด NetBeans → File → Open Project → เลือกโฟลเดอร์ MoogataLab ของตัวเอง (จาก lab 10)
  2. รันไฟล์ทดสอบเดิม 2 ไฟล์ (คลิกไฟล์ → Shift+F6):
    • TestEmployeeService.java → ต้องได้ Welcome user : somchai (แคชเชียร์) และ Error, wrong username or password — พิสูจน์ว่าเลเยอร์ service + dao + ฐานข้อมูลคุยกันได้ครบ
    • TestEmployeeDao.java → ต้องเห็นพนักงาน 3 คน (somjai, somchai, malee) — พิสูจน์ว่า seed data ใน moogata.db ยังอยู่ครบ
✓ เช็ค: ของเดิมผ่านครบ = พร้อมเริ่ม lab (ถ้าไม่ผ่าน กลับไปแก้ lab 10 ก่อน ดู Troubleshooting ข้อ 1)
ส่วนที่ 1 — ฟอร์ม input รวม Step 2 จาก 8 · หน้า 5/17

Step 2 — ปรับของเก่า: เปิดบริการ CRUD ใน EmployeeService

ขั้นนี้เป็นการ "แก้ของเก่า" จุดเดียวของทั้งส่วนที่ 1 — ตอนจบ lab 10 EmployeeService เปิดบริการแค่เมธอดเดียวคือ login() เพราะยังไม่มีใครต้องการอย่างอื่น แต่ต่อจากนี้ หน้าจอ จะมาเป็นลูกค้าตัวใหม่ของ service และหน้าจอต้องการทั้งอ่าน เพิ่ม แก้ ลบ — เราจึงต้องขยาย "หน้าเคาน์เตอร์" ให้เปิดบริการครบก่อน แล้วหน้าจอที่เขียนในขั้นถัดไปจะไม่ต้องรู้จัก DAO เลยสักนิด

เปิดไฟล์ service/EmployeeService.java — เพิ่ม import ด้านบนไฟล์:

import java.util.List;

แล้วเพิ่มเมธอดต่อท้าย login() (ก่อนปิดคลาส):

// ===== เพิ่มใน Lab 11 — เปิดบริการ CRUD ที่ EmployeeDao รองรับอยู่แล้ว ให้หน้าจอใช้ =====

/** พนักงานทุกคน — ใช้โดย EmployeeFrame เพื่อเติมตาราง */
public List<Employee> getAll() {
    return employeeDao.getAll();
}

/** พนักงาน 1 คนจาก id — ใช้โดย EmployeeFrame ตอนคลิกเลือกแถวในตาราง */
public Employee get(int id) {
    return employeeDao.get(id);
}

/** เพิ่มพนักงานใหม่ — สำเร็จได้ object กลับมาพร้อม id, ล้มเหลว (ชื่อซ้ำ) ได้ null */
public Employee save(Employee employee) {
    return employeeDao.save(employee);
}

/** แก้ไขพนักงาน — สำเร็จได้ object กลับมา, ไม่พบแถว/ล้มเหลวได้ null */
public Employee update(Employee employee) {
    return employeeDao.update(employee);
}

/** ลบพนักงานตาม id — คืนจำนวนแถวที่ถูกลบ (0 = ไม่พบ, -1 = ผิดพลาด) */
public int delete(int id) {
    Employee target = new Employee();
    target.setId(id);
    return employeeDao.delete(target);
}
📖 สังเกต: ทั้ง 5 เมธอดเป็นแค่ "ส่งต่อ" ไปที่ DAO — เพราะ EmployeeDao มี CRUD ครบอยู่แล้วตั้งแต่ lab 10 หน้าที่ของ service คือเป็น "หน้าเคาน์เตอร์" ให้ UI ไม่ต้องรู้จัก DAO เลย และวันหน้าถ้ามีกฎธุรกิจใหม่ (เช่น ห้ามลบผู้จัดการคนสุดท้าย) จะเขียนกฎไว้ที่เคาน์เตอร์นี้จุดเดียว ทุกหน้าจอได้กฎใหม่ทันที

รัน TestEmployeeService ซ้ำ — ผลต้องเหมือนเดิมทุกบรรทัด (นี่คือแบบฝึกหัดสไตล์นักพัฒนา: แก้โค้ดแล้วต้องรู้ว่าของเดิมไม่พัง — เราจะทำแบบนี้ทุกครั้งที่แตะของเก่า)

✓ เช็ค: ทดสอบเดิมผ่านเหมือนเดิม
git add src/main/java/com/example/moogata/service/EmployeeService.java
git commit -m "lab11(ช่วง 1): เปิดบริการ CRUD ใน EmployeeService สำหรับหน้าจอ"
ส่วนที่ 1 — ฟอร์ม input รวม Step 3 จาก 8 · หน้า 6/17

Step 3 — สร้าง package ui + ออกแบบหน้าจอ LoginFrame

ถึงเวลาหน้าจอแรกของโปรแกรม — ตามเรื่องราวของร้าน พนักงานเดินมาเปิดเครื่อง POS ตอนเช้า สิ่งแรกที่เจอคือหน้าล็อกอิน ใครล็อกอินผ่านและเป็นผู้จัดการเท่านั้นถึงจะไปหน้าจัดการพนักงานได้ (เราจะสร้าง LoginFrame ให้จบก่อน แล้วค่อยสร้างหน้าจัดการใน Step 5 — แต่ละชิ้นจบในตัวตามแบบ lab 10)
  1. คลิกขวา package com.example.moogata → New → Java Package ชื่อ ui — package ใหม่เพื่อแยก "หน้าจอ" ออกจากโค้ดหลังบ้าน ตามกฎเลเยอร์ที่แผนภาพ lab 10 วางไว้
  2. คลิกขวา package ui → New → JFrame Form (หมวด Swing GUI Forms) ชื่อ LoginFrame — NetBeans จะเปิดหน้าออกแบบ (Design) ให้ลากวาง control ได้เลย และสลับดูโค้ดได้ที่ปุ่ม Source
  3. ลาก control จาก Palette มาวางตามภาพ แล้ว เปลี่ยนชื่อตัวแปรให้ตรงเป๊ะ (คลิกขวา control → Change Variable Name…) — เลขวงกลมในภาพ = เลขแถวในตารางด้านล่าง ชื่อที่แท็กไว้คือชื่อที่โค้ดใน Step 4 จะอ้างถึง ถ้าตั้งไม่ตรงโค้ดจะคอมไพล์ไม่ผ่าน
LoginFrame
ภาพที่ต้องได้เมื่อรันจริง (ป้ายกำกับ = ชื่อตัวแปรที่ต้องตั้ง): ①lblHeader หัวเรื่องกลางจอ · ②lblLogin คู่กับ ③txtLogin ช่องกรอกชื่อ · ④lblPassword คู่กับ ⑤txtPassword (ใช้ Password Field ไม่ใช่ Text Field — ให้พิมพ์แล้วเห็นจุดดำ) · ⑥btnLogin และ ⑦btnExit อยู่แถวล่าง
#ControltextVariable Nameเพิ่มเติม
1Labelระบบ POS BONUS SUKIlblHeaderfont: Leelawadee UI, Bold, 18 (กันไทยเป็นกล่อง □□□)
2Labelชื่อเข้าใช้:lblLogin
3Text Field—txtLogin
4Labelรหัสผ่าน:lblPassword
5Password Field—txtPassword
6Buttonเข้าสู่ระบบbtnLogin
7ButtonออกbtnExit

ตั้งค่าฟอร์ม (คลิกพื้นหลัง → Properties): title เข้าสู่ระบบ — BONUS SUKI POS · defaultCloseOperation EXIT_ON_CLOSE (ปิดหน้านี้ = จบโปรแกรม เพราะเป็นหน้าแรก) · resizable ออก · ขนาด: ลากขอบฟอร์มใน designer ให้ได้ราว ~380 × 310 (NetBeans จัดการขนาดให้เอง — พิมพ์ตัวเลขใน Properties ไม่มีผล)

และเพิ่มบรรทัดเดียวใน constructor (หลัง initComponents();) เพื่อให้หน้าต่างเปิดกลางจอ ไม่ติดมุมซ้ายบน:

    setLocationRelativeTo(null); // null = กึ่งกลางหน้าจอ
📖 ทำไมชื่อตัวแปรต้องมีหัว lbl/txt/btn? เพราะหน้าจอใหญ่ขึ้นเรื่อย ๆ มองโค้ดแล้วต้องรู้ทันทีว่าอะไรคือปุ่ม อะไรคือช่องกรอก — นิสัยนี้จะช่วยตอน EmployeeFrame ที่มี control 14 ตัว · หน้าที่ของแต่ละ component ดูเต็ม ๆ ได้ที่ หน้า 3 (ชุด Component Swing)
ส่วนที่ 1 — ฟอร์ม input รวม Step 4 จาก 8 · หน้า 7/17

Step 4 — โค้ด event ของ LoginFrame + คำสั่ง start แล้วทดสอบล็อกอิน

ตอนนี้หน้าจอวางเสร็จแต่กดปุ่มแล้วเงียบ — โปรแกรม GUI ไม่ได้รันบน-ลงล่างเหมือน main แต่ "รอเหตุการณ์" แล้วรันโค้ดที่เราผูกไว้กับเหตุการณ์นั้น (ทฤษฎีข้อ 3 หน้า 2) เราจะผูก 2 ปุ่ม และตั้งใจให้ล็อกอินสำเร็จชั่วคราวเป็นแค่ "กล่องทักทาย" ก่อน เพราะหน้าจัดการยังไม่เกิด — ทำชิ้นเล็กให้รันได้จริงก่อน แล้วค่อยต่อชิ้นถัดไป

ดับเบิลคลิกปุ่ม btnLogin ใน designer (NetBeans จะสร้างเมธอด event ให้เอง — อย่าพิมพ์เมธอดเองลอย ๆ เพราะต้องมีการผูก event ที่โค้ดส่วน NetBeans ดูแล) แล้ววางโค้ด:

⚠ วางเฉพาะ "เนื้อใน" ระหว่างปีกกา — ใช้กับทุก handler ของทั้ง lab: เมื่อดับเบิลคลิก NetBeans สร้าง private void btnLoginActionPerformed(...) { ... } พร้อมปีกกาเปิด-ปิดให้แล้ว (ส่วนหัวล็อกแก้ไม่ได้ ข้างในจะมีบรรทัด // TODO add your handling code here: รออยู่) — โค้ดใน lab แสดง "เมธอดเต็ม" เพื่อให้เห็นภาพรวมเท่านั้น ตอนวางจริงให้คัดลอก เฉพาะบรรทัดเนื้อความ มาแทนบรรทัด TODO ถ้าวางทั้งก้อนรวมหัวเมธอด = เมธอดซ้อนเมธอด → compile ไม่ผ่านทันที
private void btnLoginActionPerformed(java.awt.event.ActionEvent evt) {
    // getPassword() คืน char[] (ไม่ใช่ String) — เพื่อความปลอดภัย ต้องแปลงก่อนใช้
    String login = txtLogin.getText().trim();
    String password = new String(txtPassword.getPassword());

    EmployeeService employeeService = new EmployeeService();
    Employee employee = employeeService.login(login, password);

    if (employee == null) {
        JOptionPane.showMessageDialog(this,
                "ชื่อเข้าใช้หรือรหัสผ่านไม่ถูกต้อง",
                "เข้าสู่ระบบไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
        return;
    }

    // (Step 6 จะเปลี่ยนข้อความชั่วคราวนี้เป็นการเปิดหน้าจัดการพนักงาน)
    JOptionPane.showMessageDialog(this,
            "ยินดีต้อนรับ " + employee.getName() + " (" + employee.getPosition() + ")",
            "เข้าสู่ระบบสำเร็จ", JOptionPane.INFORMATION_MESSAGE);
}
📖 อ่านโค้ดเป็นเรื่อง: อ่านค่าจากช่องกรอก 2 ช่อง → ถาม service ว่า "คู่นี้ใช่พนักงานคนนี้จริงไหม" → ไม่จริงเปิดกล่องแดง → จริงเปิดกล่องทักทาย — สังเกตว่าในหน้าจอ ไม่มี SQL ไม่มี Connection ไม่มี try-catch เลย ทั้งหมดอยู่ในเลเยอร์ข้างล่างที่ทำไปแล้ว นี่คือผลตอบแทนของเลเยอร์ที่จัดวางดี

ดับเบิลคลิก btnExit:

private void btnExitActionPerformed(java.awt.event.ActionEvent evt) {
    System.exit(0);
}

main() — NetBeans สร้างมาให้แล้วใน template ของ JFrame Form (อยู่ท้ายคลาส มีโค้ดตั้งค่า Nimbus ยาว ๆ ข้างใน) — ห้ามเพิ่ม main ซ้ำเด็ดขาด (จะ compile ไม่ผ่านเพราะมี main สองตัว) ให้แทนที่เนื้อในด้วยแบบสั้น ๆ ตามด้านล่าง:

/** คำสั่ง start โปรแกรม — รัน LoginFrame บนเธรดของ Swing (EDT) */
public static void main(String args[]) {
    java.awt.EventQueue.invokeLater(() -> new LoginFrame().setVisible(true));
}
import com.example.moogata.model.Employee;
import com.example.moogata.service.EmployeeService;
import javax.swing.JOptionPane;

รัน LoginFrame (Shift+F6) แล้วลอง 2 กรณี: พิมพ์ somchai / 0000 กดเข้าสู่ระบบ — ต้องได้กล่องแดง:

Login ไม่สำเร็จ
สิ่งที่ต้องเห็น: กล่องแดง "ชื่อเข้าใช้หรือรหัสผ่านไม่ถูกต้อง" — สังเกตว่าข้อความ ไม่ได้บอกว่า "ชื่อไม่มี" หรือ "รหัสผิด" (กรณีชื่อไม่มีจริงก็ได้ข้อความเดียวกัน) ตั้งใจให้เป็นเช่นนั้น เพื่อไม่ให้คนร้ายใช้กล่อง error เดาว่าชื่อไหนมีอยู่ในระบบ

จากนั้นลอง somchai / 1234 — ต้องได้กล่อง "ยินดีต้อนรับ สมชาย มานะงาน (แคชเชียร์)" = login วิ่งลงไปถึงฐานข้อมูลจริงแล้วกลับมาถูก

✓ เช็ค: รหัสผิด = กล่องแดง · รหัสถูก = กล่องต้อนรับ · ปุ่มออกปิดโปรแกรมได้
git add src/main/java/com/example/moogata/ui/LoginFrame.java
git commit -m "lab11(ช่วง 2): หน้าจอ LoginFrame — ล็อกอินด้วย Swing"
ส่วนที่ 1 — ฟอร์ม input รวม Step 5 จาก 8 · หน้า 8/17

Step 5 — ออกแบบหน้าจอ EmployeeFrame — ฟอร์มบน + ตารางล่าง

หน้าจอที่สองคือ "ห้องทำงาน" ของผู้จัดการ — แบบที่เราเลือกสร้างก่อนคือ หน้าต่างเดียวจบ: ครึ่งบนเป็นฟอร์มสำหรับพิมพ์ข้อมูลพนักงาน 1 คน (เพิ่มหรือแก้ก็ใช้ฟอร์มนี้) ปุ่ม 4 ตัวอยู่คั่นกลาง และครึ่งล่างเป็นตารางโชว์พนักงานทุกคนที่อ่านมาจากฐานข้อมูล — จัดวางให้ "ข้อมูลเก่าอยู่ล่าง งานที่กำลังทำอยู่บน" ตาจะไล่ได้จากบนลงล่างตามลำดับการทำงาน

New → JFrame Form ชื่อ EmployeeFrame ใน package ui แล้วลาก control ตามภาพ:

EmployeeFrame แบบฟอร์มรวม
โครงหน้าจอที่ต้องวางให้ได้: ①lblWelcome บอกว่าใครล็อกอินอยู่ (ตั้งค่าในโค้ด) · ฟอร์มครึ่งบน: ②lblLogin+③txtLogin กับ ④lblPosition+⑤txtPosition แถวแรก, ⑥lblPassword+⑦txtPassword กับ ⑧lblRole+⑨cmbRole แถวสอง · แถวปุ่ม: ⑩btnSave ⑪btnNew ⑫btnDelete ⑬btnRefresh · ⑭tblEmployee ตารางครึ่งล่าง (ตอนนี้ตารางยังไม่มีข้อมูล — จะเติมด้วยโค้ดใน Step 6)
#ControltextVariable Nameเพิ่มเติม
1Label(ตั้งในโค้ด)lblWelcomefont: Bold, 14
2Labelชื่อล็อกอิน:lblLoginแถวบนของฟอร์ม
3Text Field—txtLogin
4Labelตำแหน่ง:lblPosition
5Text Field—txtPosition
6Labelรหัสผ่าน:lblPasswordแถวล่างของฟอร์ม
7Password Field—txtPassword
8Labelบทบาท:lblRole
9Combo Box—cmbRoleProperties → model: พนักงาน (บรรทัด 1), ผู้จัดการ (บรรทัด 2)
10ButtonบันทึกbtnSaveแถวปุ่มใต้ฟอร์ม
11Buttonล้างฟอร์มbtnNew
12ButtonลบbtnDelete
13ButtonรีเฟรชbtnRefresh
14Table—tblEmployeeลาก Table แล้ว NetBeans ครอบด้วย jScrollPane1 ให้เอง (คงชื่อเดิม)

ตั้งค่าฟอร์ม: title จัดการพนักงานระบบ POS — BONUS SUKI · defaultCloseOperation DISPOSE_ON_CLOSE (ต่างจากหน้าล็อกอิน — ปิดหน้านี้แค่หน้านี้ปิด โปรแกรมไม่ตาย และหน้าแรกก็ปิดไปแล้วตอนเข้ามา) · resizable ออก · ขนาด: ลากขอบฟอร์มใน designer ให้ได้ราว ~656 × 560

⚠ JTable ต้องอยู่ใน JScrollPane เสมอ — ไม่งั้นหัวคอลัมน์จะหายตอนรัน (หัวคอลัมน์ตอนนี้ยังเป็น Title 1, Title 2… ไม่ต้องแก้ใน designer — เราสั่งด้วยโค้ดใน Step 6)
ส่วนที่ 1 — ฟอร์ม input รวม Step 6 จาก 8 · หน้า 9/17

Step 6 — โค้ดส่วนอ่านข้อมูล: ตารางโชว์พนักงานจริง + เชื่อม LoginFrame → EmployeeFrame

ตอนนี้มีหน้าจอ 2 ใบที่ยังไม่คุยกัน — ขั้นนี้จะทำ 2 เรื่อง: (1) ให้ EmployeeFrame อ่านพนักงานจากฐานข้อมูลมาโชว์ในตารางได้ และ (2) เชื่อมปลายทางของ LoginFrame จาก "กล่องทักทายชั่วคราว" เป็น "เปิดหน้าจัดการจริง" — ทำสองเรื่องนี้เสร็จ โปรแกรมจะมีเส้นทางครบตั้งแต่เปิดเครื่องจนถึงเห็นข้อมูลจริง (ยังไม่รวมเพิ่ม/แก้/ลบ)

สลับไปแท็บ Source ของ EmployeeFrame แล้วแก้ 3 จุด:

(1) field + constructor (แก้ constructor ที่ NetBeans สร้างให้ — เพิ่มพารามิเตอร์ loginUser เพื่อให้หน้านี้รู้ว่า "ใคร" เข้ามาใช้ จะได้โชว์ชื่อและกันลบบัญชีตัวเองใน Step 7):

⚠ อย่าลืมลบ main() ของ template: NetBeans สร้าง main() มาให้ท้ายคลาส ซึ่งเรียก new EmployeeFrame() แบบไม่มีพารามิเตอร์ — พอเราเปลี่ยน constructor ให้ต้องมีพารามิเตอร์ จะ compile ไม่ผ่านทันที ให้ลบ main() ทิ้ง (หน้านี้เปิดจาก LoginFrame ไม่ได้รันตรง)
/** พนักงานที่ล็อกอินเข้ามา (เก็บไว้โชว์ที่ lblWelcome และกันลบบัญชีตัวเอง) */
private final Employee loginUser;
private final EmployeeService employeeService = new EmployeeService();

/** id ของแถวที่กำลังแก้ไข (-1 = ไม่ได้แก้ไข คือโหมดเพิ่มใหม่) */
private int editingId = -1;

public EmployeeFrame(Employee loginUser) {
    this.loginUser = loginUser;
    initComponents();
    tblEmployee.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
    tblEmployee.getTableHeader().setReorderingAllowed(false); // ล็อกลำดับคอลัมน์ — กันลากสลับแล้วโค้ดอ่านค่าผิดคอลัมน์
    // (บรรทัดผูก listener "คลิกแถว → เติมฟอร์ม" จะเพิ่มใน Step 7)
    lblWelcome.setText("ผู้ใช้งาน: " + loginUser.getName() + " (" + loginUser.getPosition() + ")");
    showEmployees();
}

(2) เมธอดเติมตาราง — หัวใจของครึ่งล่าง สังเกตว่าเราสร้าง model ใหม่ทุกครั้ง (หัวคอลัมน์ + แถวจากฐานข้อมูลสด ๆ) ไม่ใช่แก้ทีละแถว — วิธี "ล้างแล้วเติมใหม่ทั้งชุด" ง่ายและไม่มีทางข้อมูลค้าง (ทฤษฎีข้อ 6 หน้า 2) พร้อมตั้งให้ช่องในตาราง แก้ไขไม่ได้: ตารางทำหน้าที่โชว์เท่านั้น ถ้าให้พิมพ์ทับบนจอ ค่าจะเปลี่ยนแต่ไม่ลงฐานข้อมูล (แล้วผู้ใช้จะเข้าใจผิดว่าบันทึกแล้ว):

/** อ่านข้อมูลพนักงานทุกคนจาก service แล้วเติมลงตาราง — เรียกทุกครั้งที่ข้อมูลเปลี่ยน */
private void showEmployees() {
    // ตั้งหัวคอลัมน์แล้วเติมแถวจากฐานข้อมูลใหม่ทั้งหมด
    // isCellEditable = false: ตารางไว้ "ดู" เท่านั้น — แก้ข้อมูลต้องผ่านฟอร์มด้านบนแล้วบันทึก จึงลงฐานข้อมูลจริง
    DefaultTableModel model = new DefaultTableModel(
            new Object[][]{},
            new String[]{
                "รหัส", "ชื่อล็อกอิน", "ตำแหน่ง", "รหัสผ่าน", "บทบาท"
            }) {
        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }
    };

    for (Employee emp : employeeService.getAll()) {
        model.addRow(new Object[]{
            emp.getId(),
            emp.getName(),
            emp.getPosition(),
            emp.getPassword(),
            roleName(emp.getRole())
        });
    }
    tblEmployee.setModel(model);
}

(3) เมธอดช่วยแปลงบทบาท — ตารางโชว์ข้อความไทย ("ผู้จัดการ"/"พนักงาน") ไม่ใช่เลข 1/2 — ใส่ไว้เลยเพื่อให้จบ Step 6 แล้ว compile ผ่าน:

/** แปลงเลขบทบาทเป็นข้อความสำหรับคอลัมน์ "บทบาท" ในตาราง */
private String roleName(int role) {
    return role == 1 ? "ผู้จัดการ" : "พนักงาน";
}

(4) ปุ่มรีเฟรช + import: ดับเบิลคลิก btnRefresh → วางโค้ด แล้วเพิ่ม import

private void btnRefreshActionPerformed(java.awt.event.ActionEvent evt) {
    showEmployees();
}
import com.example.moogata.model.Employee;
import com.example.moogata.service.EmployeeService;
import javax.swing.JOptionPane;
import javax.swing.table.DefaultTableModel;

จากนั้นกลับไปที่ LoginFrame — แทนข้อความชั่วคราวท้าย btnLoginActionPerformed ด้วย "ประตูที่สอง" คือกฎเรื่องสิทธิ์ + การเปิดหน้าจัดการ:

// เดิม (Step 4):  JOptionPane.showMessageDialog(this, "ยินดีต้อนรับ ...");
// เปลี่ยนเป็น:
// กฎธุรกิจ: หน้าจัดการพนักงานเปิดเฉพาะผู้จัดการ (role = 1) เท่านั้น
if (employee.getRole() != 1) {
    JOptionPane.showMessageDialog(this,
            employee.getName() + " ไม่ใช่ผู้จัดการ จึงเข้าหน้าจัดการพนักงานไม่ได้",
            "ไม่มีสิทธิ์เข้าใช้", JOptionPane.WARNING_MESSAGE);
    return;
}

EmployeeFrame employeeFrame = new EmployeeFrame(employee);
employeeFrame.setLocationRelativeTo(this);
employeeFrame.setVisible(true);
this.dispose();
📖 อ่านเป็นเรื่อง: กล่องเหลืองคือ "ประตูที่สอง" — ผ่านรหัสมาแล้วแต่บทบาทยังต้องถูก (พนักงานอย่าง somchai จะโดนกันไว้ตรงนี้ ตรงกับสถานการณ์ทดสอบข้อ 2 ของ Step 8) · ส่วนท้ายคือการสร้างหน้าใหม่โดย "ส่งกระเป๋า" (object employee ที่ล็อกอินผ่าน) ไปให้ · จัดกลางจอ · เปิดโชว์ · แล้ว dispose() ปิดหน้าล็อกอินทิ้ง — ต่างจาก System.exit(0) ของปุ่มออกที่ปิดทั้งโปรแกรม

รัน LoginFrame → login somjai / 1234 → ต้องเห็นหน้าจัดการพนักงาน มีตาราง 3 แถว ทดลองกดรีเฟรช: เปิด Letos ไปแก้ข้อมูล (เช่นเปลี่ยนตำแหน่ง malee) แล้วกดรีเฟรชในโปรแกรม — ตารางต้องเปลี่ยนตาม = พิสูจน์ว่าตารางอ่านจากฐานข้อมูลจริงทุกครั้ง ไม่ใช่จำค่าเก่า

✓ เช็ค: login somjai เห็นตาราง 3 แถว + รีเฟรชทำงาน — ฟอร์มด้านบนยังไม่ทำอะไร (Step 7 ค่อยทำ)
git add src/main/java/com/example/moogata/ui/
git commit -m "lab11(ช่วง 3): หน้าจอ EmployeeFrame — ฟอร์มบน ตารางล่าง"
ส่วนที่ 1 — ฟอร์ม input รวม Step 7 จาก 8 · หน้า 10/17

Step 7 — คลิกแถว → ค่าขึ้นฟอร์ม + ปุ่มบันทึก/ล้างฟอร์ม/ลบ (CRUD ครบบนฟอร์มเดียว)

ขั้นนี้คือหัวใจของ "แบบรวม" — ฟอร์มด้านบนจะรับหน้าที่สองโหมดในกล่องเดียว: ปกติเป็น โหมดเพิ่ม (ช่องว่าง พิมพ์ใหม่ทั้งหมด) แต่พอผู้ใช้ คลิกแถวในตาราง ค่าเดิมจะไหลขึ้นมาที่ฟอร์ม กลายเป็น โหมดแก้ไข ทันที จนกว่าจะกด "ล้างฟอร์ม" กลับสู่โหมดเพิ่ม — ตัวแยกสองโหมดคือตัวแปร editingId ที่ประกาศไว้แล้วใน Step 6

(1) คลิกเลือกแถวแล้วค่าขึ้นฟอร์ม — handler ของตารางต่างจากปุ่ม: ไม่มีเมนู Events → ListSelection ให้ใช้ (JTable ไม่มี addListSelectionListener ของตัวเอง — มีเฉพาะ JList) ทำ 2 ขั้น:

ขั้นแรก — ผูก listener ใน constructor (เพิ่มบรรทัดนี้ต่อจาก setSelectionMode ที่เขียนไว้ใน Step 6):

    // ผูก listener "คลิกแถว → เติมฟอร์ม"
    tblEmployee.getSelectionModel().addListSelectionListener(this::tblEmployeeValueChanged);

ขั้นต่อมา — เขียนเมธอด handler วางใน Source:

/** คลิกเลือกแถวในตาราง → อ่านข้อมูลจากฐานข้อมูลแล้วเติมลงฟอร์มด้านบน (เข้าสู่โหมดแก้ไข) */
private void tblEmployeeValueChanged(javax.swing.event.ListSelectionEvent evt) {
    if (evt.getValueIsAdjusting()) {
        return; // event นี้ยิง 2 รอบต่อการคลิก 1 ครั้ง — รอบแรก (selection ยังปรับไม่จบ) ข้ามไปก่อน
    }
    int row = tblEmployee.getSelectedRow();
    if (row == -1) {
        return; // ช่วงล้างตาราง/ล้างฟอร์ม — ไม่มีแถวถูกเลือก ไม่ต้องทำอะไร
    }
    int id = (Integer) tblEmployee.getValueAt(row, 0);
    Employee emp = employeeService.get(id);
    if (emp == null) {
        return;
    }
    editingId = emp.getId();
    txtLogin.setText(emp.getName());
    txtPosition.setText(emp.getPosition());
    txtPassword.setText(emp.getPassword());
    setSelectedRole(emp.getRole());
}
📖 สามจุดที่ต้องเข้าใจ: (1) เราอ่านข้อมูลซ้ำจากฐานข้อมูลด้วย employeeService.get(id) ไม่ใช่อ่านจากที่โชว์บนตาราง — ให้ฟอร์มได้ค่าจาก "แหล่งจริง" เสมอ กรณีคนอื่นแก้ไปก่อน ฟอร์มจะได้ค่าล่าสุด (2) listener นี้ถูกเรียก ทุกครั้งที่ selection เปลี่ยน รวมถึงตอนโปรแกรมเราเองล้าง selection จึงต้องมี if (row == -1) return; ดักไว้บนสุด ไม่งั้นโปรแกรมคว่ำตอน clearSelection() (3) ListSelectionEvent ยิง 2 รอบต่อการคลิก 1 ครั้ง (รอบแรก getValueIsAdjusting() = true คือกำลังปรับ, รอบสอง = จบแล้ว) — ดักด้วย getValueIsAdjusting() ไว้บรรทัดแรก ไม่งั้นเราจะอ่านฐานข้อมูลซ้ำเปล่า ๆ รอบละ 2 ครั้ง

(2) ปุ่มบันทึก (ดับเบิลคลิก btnSave) — ปุ่มเดียวสองพฤติกรรม แยกด้วยสถานะ editingId:

private void btnSaveActionPerformed(java.awt.event.ActionEvent evt) {
    String name = txtLogin.getText().trim();
    String position = txtPosition.getText().trim();
    String password = new String(txtPassword.getPassword());

    if (name.isEmpty() || position.isEmpty() || password.isEmpty()) {
        JOptionPane.showMessageDialog(this,
                "กรอกข้อมูลให้ครบทุกช่องก่อนบันทึก",
                "ข้อมูลไม่ครบ", JOptionPane.WARNING_MESSAGE);
        return;
    }

    Employee emp = new Employee(name, position, password, getSelectedRole());

    if (editingId > 0) {
        // มีแถวถูกเลือกอยู่ = โหมดแก้ไข (ใช้ id ของแถวที่คลิก)
        emp.setId(editingId);
        if (employeeService.update(emp) == null) {
            JOptionPane.showMessageDialog(this,
                    "แก้ไขไม่สำเร็จ (ชื่อล็อกอินซ้ำกับคนอื่น หรือไม่พบแถวเดิม)",
                    "แก้ไขไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
            return;
        }
    } else {
        // ฟอร์มว่างจากการล้าง = โหมดเพิ่มใหม่
        if (employeeService.save(emp) == null) {
            JOptionPane.showMessageDialog(this,
                    "บันทึกไม่สำเร็จ (ชื่อล็อกอินซ้ำกับคนอื่น)",
                    "บันทึกไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
            return;
        }
    }
    clearForm();
    showEmployees();
}

(3) ปุ่มล้างฟอร์ม (ดับเบิลคลิก btnNew) — หน้าที่เดียวคือพาฟอร์มกลับสู่โหมดเพิ่ม:

private void btnNewActionPerformed(java.awt.event.ActionEvent evt) {
    clearForm();
}

(4) ปุ่มลบ (ดับเบิลคลิก btnDelete) — ลบเป็นการกระทำที่กู้ไม่ได้ จึงมีกันตัว 2 ชั้น: ห้ามลบบัญชีที่ล็อกอินอยู่ และถามยืนยันก่อนทุกครั้ง:

private void btnDeleteActionPerformed(java.awt.event.ActionEvent evt) {
    int row = tblEmployee.getSelectedRow();
    if (row == -1) {
        JOptionPane.showMessageDialog(this,
                "เลือกพนักงานในตารางก่อนกดลบ",
                "ยังไม่ได้เลือก", JOptionPane.WARNING_MESSAGE);
        return;
    }
    int id = (Integer) tblEmployee.getValueAt(row, 0);
    String selectedName = (String) tblEmployee.getValueAt(row, 1);

    // กฎธุรกิจ: ห้ามลบบัญชีที่กำลังล็อกอินอยู่ (กันผู้จัดการลบตัวเองจนเข้าระบบไม่ได้)
    if (selectedName.equals(loginUser.getName())) {
        JOptionPane.showMessageDialog(this,
                "ลบบัญชีที่กำลังใช้งานอยู่ไม่ได้",
                "ลบไม่ได้", JOptionPane.WARNING_MESSAGE);
        return;
    }

    int choice = JOptionPane.showConfirmDialog(this,
            "ต้องการลบพนักงาน " + selectedName + " ใช่หรือไม่?",
            "ยืนยันการลบ", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
    if (choice != JOptionPane.YES_OPTION) {
        return;
    }

    int deleted = employeeService.delete(id);
    if (deleted > 0) {
        clearForm();
        showEmployees();
    } else {
        JOptionPane.showMessageDialog(this,
                "ลบไม่สำเร็จ (อาจถูกลบไปก่อนแล้ว)",
                "ลบไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
    }
}

(5) เมธอดช่วย — เพิ่มท้ายคลาส (roleName() ใส่ไว้แล้วใน Step 6):

/** ล้างฟอร์มกลับสู่โหมดเพิ่มใหม่ (editingId = -1) */
private void clearForm() {
    editingId = -1;
    tblEmployee.clearSelection(); // จะเรียก valueChanged กลับมาด้วยแถว -1 (มี guard รออยู่)
    txtLogin.setText("");
    txtPosition.setText("");
    txtPassword.setText("");
    cmbRole.setSelectedIndex(0);
}

/** แปลงรายการที่เลือกใน cmbRole เป็นเลข role ที่เก็บในฐานข้อมูล */
private int getSelectedRole() {
    return cmbRole.getSelectedIndex() == 1 ? 1 : 2;
}

/** ตั้ง cmbRole ให้ตรงกับเลข role ที่เก็บในฐานข้อมูล */
private void setSelectedRole(int role) {
    cmbRole.setSelectedIndex(role == 1 ? 1 : 0);
}

ทดสอบชิ้นนี้ทันที: รัน → login somjai → คลิกแถว somchai → ฟอร์มต้องเติมค่าเดิมทันที:

คลิกแถวแล้วค่าขึ้นฟอร์ม
สิ่งที่ต้องเห็น: ฟอร์มด้านบนถูกเติมด้วยข้อมูลของ somchai (ชื่อล็อกอิน, ตำแหน่ง "แคชเชียร์", รหัสผ่านเป็นจุดดำ 4 ตัว, บทบาทเปลี่ยนเป็น "พนักงาน") — ขณะนี้โปรแกรมอยู่ใน "โหมดแก้ไขแถว 2" ถ้ากดบันทึกตอนนี้จะเป็นการ update ไม่ใช่เพิ่มแถวใหม่ · กด "ล้างฟอร์ม" แล้วทุกช่องต้องว่างกลับเป็นโหมดเพิ่ม
ส่วนที่ 1 — ฟอร์ม input รวม Step 8 จาก 8 · หน้า 11/17

Step 8 — ทดสอบส่วนที่ 1 ครบ 8 สถานการณ์ (ทำตามสคริปต์ทีละข้อ)

ทดสอบจบงานไม่ใช่ "กดเล่นดูว่า work ไหม" แต่คือ เดินสคริปต์ที่ออกแบบไว้ให้ครบทุกกฎที่เราเขียน — แต่ละสถานการณ์ด้านล่างถูกเรียงให้เล่นต่อกันได้ (ข้อ 4 สร้าง sompong ไว้ให้ข้อ 8 ใช้ลบ) และแต่ละข้อพิสูจน์ "กฎ" คนละกฎ: การยืนยันตัวตน · สิทธิ์ · การเขียนข้อมูล · constraint ของฐานข้อมูล · การอัปเดตหน้าจอ · กันมือส่องปืน

เตรียมก่อนทดสอบ (2 นาที)

  1. เปิด Letos → ตาราง employee → ต้องเหลือเดิม 3 แถว (somjai, somchai, malee) — ถ้ามีคนอื่นค้างจากตอนลองโค้ด ลบทิ้งให้เหลือ 3 คนนี้ก่อน (ข้อมูลสะอาด = ผลทดสอบเชื่อถือได้)
  2. รัน LoginFrame จาก NetBeans แล้ววางหน้าต่าง Letos ไว้ข้าง ๆ — เราจะยืนยัน "ของจริงใน db" คู่กับ "บนจอ" ไปด้วย

สคริปต์ทดสอบ — ทำตามลำดับ 1 → 8

1) รหัสผ่านผิด
ทำ: พิมพ์ somchai / 0000 → เข้าสู่ระบบ → อ่านข้อความ → กด OK
ต้องเห็น: กล่องแดง "ชื่อเข้าใช้หรือรหัสผ่านไม่ถูกต้อง" แล้วอยู่หน้า login ต่อ (ไม่เด้งออก)
ทำไมข้อนี้สำคัญ: พิสูจน์ว่า login เทียบรหัสกับฐานข้อมูลจริง ไม่ใช่รับทุกอย่างที่พิมพ์มา — และข้อความ error ไม่เผยว่า "ชื่อมีจริงแต่รหัสผิด" (กันการเดาชื่อ)
2) พนักงานล็อกอิน (รหัสถูก แต่ไม่ใช่ผู้จัดการ)
ทำ: พิมพ์ somchai / 1234 → เข้าสู่ระบบ
ต้องเห็น: กล่องเหลือง "สมชาย มานะงาน ไม่ใช่ผู้จัดการ จึงเข้าหน้าจัดการพนักงานไม่ได้"
ทำไม: พิสูจน์กฎสิทธิ์ — รหัสถูกเพียงอย่างเดียวไม่พอ role ต้องถูกด้วย
พนักงานถูกปฏิเสธ
ภาพที่ต้องได้ของข้อ 2: กล่องเหลือง (WARNING) ต่างจากข้อ 1 ที่เป็นแดง (ERROR) — ระดับของกล่องก็เป็นการสื่อสาร: แดง = ผิดพลาด, เหลือง = ผ่านการยืนยันตัวตนแล้วแต่ไม่มีสิทธิ์
3) ผู้จัดการล็อกอิน
ทำ: พิมพ์ somjai / 1234 → เข้าสู่ระบบ
ต้องเห็น: หน้าจัดการพนักงานเปิด หัวจอบอก "ผู้ใช้งาน: สมใจ ใจดี (ผู้จัดการ)" ตารางมี 3 แถว และหน้า login ปิดไปเอง
ทำไม: พิสูจน์เส้นทางครบวง: ผ่านประตู → ส่ง object พนักงานต่อ → ปิดหน้าเก่า (dispose) → หน้าใหม่อ่านข้อมูลจริงมาโชว์
4) เพิ่มพนักงานใหม่
ทำ: พิมพ์ฟอร์มด้านบน: sompong / พนักงานเสิร์ฟ / 1234 / บทบาท "พนักงาน" → บันทึก
ต้องเห็น: ตารางกลายเป็น 4 แถวทันที และฟอร์มถูกล้างกลับเป็นช่องว่าง · เปิด Letos กด Refresh → เห็น sompong เข้า db จริง
ทำไม: พิสูจน์ว่าบันทึกแล้ว (ก) อ่านใหม่จากฐานข้อมูล (ตารางอัปเดตเอง ไม่ได้ไปแก้ตารางตรง ๆ) (ข) ฟอร์มกลับโหมดเพิ่มพร้อมรับคนใหม่
5) เพิ่มชื่อที่มีอยู่แล้ว (ให้ฐานข้อมูลปฏิเสธ)
ทำ: พิมพ์ somchai / อะไรก็ได้ / 1234 → บันทึก
ต้องเห็น: กล่องแดง "บันทึกไม่สำเร็จ (ชื่อล็อกอินซ้ำกับคนอื่น)" ตารางยัง 4 แถว
ทำไม: พิสูจน์ constraint UNIQUE ที่ติดไว้ตั้งแต่ lab 10 ทำงานจริง และเส้นทาง "save คืน null → หน้าจอโชว์ error" ที่เราออกแบบไว้ถูกใช้จริง
6) แก้ไขพนักงาน
ทำ: คลิกแถว somchai (ค่าเดิมขึ้นฟอร์ม) → เปลี่ยนตำแหน่งเป็น "หัวหน้ากะ" → บันทึก
ต้องเห็น: ตารางแถว somchai เปลี่ยนเป็น "หัวหน้ากะ" ทันที (ยัง 4 แถว — ไม่เกิดแถวใหม่)
ทำไม: พิสูจน์กลไกสองโหมด — คลิกแถวแล้วบันทึก = update ไม่ใช่ save ถ้าข้อนี้กลายเป็น "เพิ่มแถวใหม่" แปลว่า editingId มีปัญหา (ดู Troubleshooting ข้อ 5)
7) ลบบัญชีตัวเอง (ต้องถูกกัน)
ทำ: คลิกแถว somjai (คนที่ล็อกอินอยู่) → ลบ
ต้องเห็น: กล่องเหลือง "ลบบัญชีที่กำลังใช้งานอยู่ไม่ได้" — ไม่มีกล่องยืนยันตามมา
ทำไม: พิสูจน์กฎธุรกิจที่เราเขียนไว้ — ถ้าปล่อยลบได้ ผู้จัดการจะลบตัวเองแล้วล็อกอินไม่ได้อีก = หน้าจัดการตายทั้งร้าน
8) ลบพนักงานคนอื่น
ทำ: คลิกแถว sompong → ลบ → อ่านกล่องยืนยัน → กด Yes
ต้องเห็น: กล่องคำถาม "ต้องการลบพนักงาน sompong ใช่หรือไม่?" → ตารางกลับเป็น 3 แถว ฟอร์มถูกล้าง
ทำไม: พิสูจน์ลบจริง + ปิดจบข้อมูลทดสอบให้ db กลับสภาพเดิม 3 คน
ยืนยันการลบ
ภาพที่ต้องได้ของข้อ 8: กล่องคำถาม Yes/No ขึ้นกลางหน้าจัดการ — ผู้ใช้ยังเห็นตารางด้านหลังได้ (ไม่ใช่หน้าจอใหม่ทั้งใบ) กด No ก็ไม่มีอะไรเปลี่ยน
หลังเพิ่ม sompong
ภาพสถานะ 4 แถว (ถ่ายไว้เป็นหลักฐานข้อ 4 ได้): ตารางมี sompong แถวที่ 4 ก่อนที่ข้อ 8 จะลบทิ้ง — สังเกตคอลัมน์บทบาทโชว์ข้อความไทย ("พนักงาน") ไม่ใช่เลข 2 เพราะเราแปลงใน roleName() ก่อนเติมตาราง

ปิดการทดสอบ

เช็ค Letos อีกครั้ง — ตารางต้องกลับมา 3 แถวเดิม (somjai/somchai/malee โดย somchai อาจเป็น "หัวหน้ากะ" จากข้อ 6 — จะแก้คืนใน Letos ก็ได้) = ฐานข้อมูลสะอาดเท่าก่อนทดสอบ และถ่ายภาพหน้าจออย่างน้อย 3 ภาพเก็บเป็นหลักฐาน (ข้อ 3, ข้อ 4/7, ข้อ 8)

ถ้าข้อนี้ไม่ผ่านปัญหาอยู่ที่ย้อนไปดู
1–2 (login ผิดพฤติกรรม)เลเยอร์ service / event ปุ่ม loginStep 2, 4
3 (เข้าหน้าจัดการไม่ได้)การส่ง object / เปิด frameStep 6 (จุดเชื่อม LoginFrame)
4–5 (เพิ่ม/ชนซ้ำ)btnSave + การรีเฟรชตารางStep 7 (2) + showEmployees
6 (แก้ไขกลายเป็นเพิ่ม)สถานะ editingId / listenerStep 7 (1), (2), (5)
7–8 (ลบผิดพฤติกรรม)กฎธุรกิจ / ยืนยัน / ลบStep 7 (4)
✓ เช็ค: ผ่านครบ 8 สถานการณ์ = จบส่วนที่ 1 (แบบฟอร์มรวม) — เก็บงานด้วย commit แล้วไปต่อส่วนที่ 2
git add src/main/java/com/example/moogata/ui/
git commit -m "lab11(ช่วง 4): CRUD พนักงานครบในหน้าต่างเดียว + สิทธิ์ผู้จัดการ"
ส่วนที่ 2 — คัดลอกไปทำแบบ dialog Step 9 จาก 12 · หน้า 12/17

Step 9 — คัดลอกโปรเจกต์ MoogataLab → MoogataLabDialog

เรื่องราวของส่วนที่ 2: แบบฟอร์มรวมใช้ได้จริง แต่ลองนึกภาพวันที่ฟอร์มมีช่องเยอะขึ้นเรื่อย ๆ (สักวันพนักงานมีวันเกิด ที่อยู่ สาขา…) หน้าจอจะแน่นจนตารางถูกบีบเหลือนิดเดียว อีกสำนักความคิดจึงแยก "ฟอร์มกรอก" ออกไปเป็นกล่อง dialog เปิดเมื่อต้องการ ปิดเมื่อเสร็จ — หน้าหลักสงวนไว้ให้ตารางเต็มที่

เหตุผลของการคัดลอกแทนการแก้ของเดิม: (1) เก็บงานส่วนที่ 1 ไว้ส่ง/เทียบได้ (2) ถ้ารื้อพังกลางทาง ต้นทางยังอยู่ให้เริ่มใหม่ (3) จบแล้วมีสองโปรเจกต์เปิดเทียบกันได้ หลังบ้านทุกไฟล์ไม่ต้องแตะเลยแม้แต่บรรทัดเดียว
  1. ใน NetBeans: คลิกขวาโปรเจกต์ MoogataLab → Close (ปิดก่อนคัดลอก — กันไฟล์ถูกล็อกจากโปรแกรมที่กำลังรันอยู่)
  2. เปิด File Explorer เข้าไปที่โฟลเดอร์ NetBeansProjects → คลิกขวาโฟลเดอร์ MoogataLab → Copy → Paste → ตั้งชื่อ MoogataLabDialog (คัดลอกทั้งโฟลเดอร์ = ได้โค้ด + moogata.db ที่มีข้อมูลครบมาด้วย)
  3. เปิดโฟลเดอร์ MoogataLabDialog → แก้ไฟล์ pom.xml 2 จุด: ① เปลี่ยน <artifactId>moogata-lab</artifactId> เป็น <artifactId>moogata-lab-dialog</artifactId> ② เปลี่ยน <name>moogata-lab</name> เป็น <name>moogata-lab-dialog</name> — จุดที่ 2 สำคัญ: NetBeans แสดงชื่อโปรเจกต์จาก <name> ถ้าไม่แก้จะเห็นสองโปรเจกต์ชื่อเดียวกันจนเปิดผิด)
  4. ถ้าใช้ git เก็บงาน: โฟลเดอร์ที่คัดลอกมาจะมี .git ติดมาด้วย (ประวัติ commit ของส่วนที่ 1) — เปิดให้เห็นไฟล์ซ่อนก่อน: File Explorer → View → Show → Hidden items (Windows 11) / แท็บ View ติ๊ก Hidden items (Windows 10) แล้วลบโฟลเดอร์ .git ใน MoogataLabDialog ทิ้ง แล้ว git init ใหม่ เพื่อให้สองโปรเจกต์มีประวัติแยกกัน (ส่วนที่ 1 อยู่ repo เดิม ส่วนที่ 2 เริ่ม repo ใหม่)
  5. คลิกขวาโฟลเดอร์ MoogataLabDialog → ลบโฟลเดอร์ target/ ทิ้ง (ของเก่าที่คัดลอกมาด้วย) — หรือเปิดแล้วกด Clean and Build หนึ่งรอบ
  6. กลับมาที่ NetBeans → File → Open Project → เลือก MoogataLabDialog → Open
  7. ทดสอบทันที: รัน TestEmployeeService ต้องผ่านเหมือนเดิมทันที (พิสูจน์ว่าคัดลอกมาครบ — รัน LoginFrame ดูหน้าจอก็ได้ ต้องใช้งานได้ปกติ)
✓ เช็ค: MoogataLabDialog เปิดและรันทดสอบเดิมผ่าน — จากนี้แก้งานในโปรเจกต์นี้เท่านั้น (MoogataLab เดิมเก็บไว้เป็นแบบฟอร์มรวม)
git add .
git commit -m "lab11(dialog) ช่วง 5: คัดลอกโปรเจกต์จากแบบฟอร์มรวมมาทำแบบ dialog"
ส่วนที่ 2 — คัดลอกไปทำแบบ dialog Step 10 จาก 12 · หน้า 13/17

Step 10 — สร้าง EmployeeDialog — ฟอร์มพนักงาน 1 คนในกล่อง

EmployeeDialog คือ "ฟอร์มบน" ของแบบรวม ยกมาไว้ในกล่องของตัวเอง — control เหมือนเดิมทุกตัว (ชื่อล็อกอิน/ตำแหน่ง/รหัสผ่าน/บทบาท) แต่เพิ่มบทบาทพิเศษ: มันจะเปิดเป็น modal (เปิดแล้วล็อกหน้าต่างหลักจนปิด — ทฤษฎีข้อ 5 หน้า 2) และต้องตอบคำถามเสมอว่า "ผู้ใช้กดบันทึกจริง หรือกดยกเลิกไป" — เราจึงให้มันจำสถานะไว้ใน isSaved()

New → JDialog Form ชื่อ EmployeeDialog ใน package ui แล้วลาก control ตามภาพ:

EmployeeDialog
โครง dialog ที่ต้องวาง: ①lblLogin+②txtLogin · ③lblPosition+④txtPosition · ⑤lblPassword+⑥txtPassword · ⑦lblRole+⑧cmbRole (model: พนักงาน/ผู้จัดการ) · ⑨btnOK "บันทึก" และ ⑩btnCancel "ยกเลิก" — ภาพนี้คือโหมดเพิ่ม (ทุกช่องว่าง) ที่จะเปิดขึ้นมาเมื่อกดปุ่ม "เพิ่มพนักงาน" ใน Step 11

ตั้งค่า: resizable ออก · ขนาด: ลากขอบใน designer ราว ~480 × 330 (title ตั้งในโค้ดตอนเปิด)

⚠ ก่อนวางโค้ด — เก็บของเก่าที่ template สร้างมาให้ให้หมด: NetBeans สร้าง template มาให้ 2 ชิ้นที่ต้องจัดการ: (1) constructor เดิม EmployeeDialog(java.awt.Frame parent, boolean modal) — ลบทิ้ง แล้ววาง 2 constructor ใหม่ของเราแทน (2) เมธอด main() ที่ template สร้างมาให้ท้ายคลาส — ลบทิ้ง (dialog เปิดจาก EmployeeFrame ไม่ได้รันตรง) — ถ้าลบ constructor เดิมแต่ลืมลบ main() จะคอมไพล์ไม่ผ่านทันที เพราะ main() ยังเรียก constructor เดิม (new EmployeeDialog(new JFrame(), true)) อยู่ · และวางโค้ด event ใหม่ = เฉพาะเนื้อในระหว่างปีกกา (กล่องเตือนใน Step 4)

ฟอร์มนี้ใช้ร่วม 2 โหมด (เพิ่ม = ช่องว่าง / แก้ไข = เติมค่าเดิม) — สลับไป Source แล้วแก้ constructor ที่ NetBeans สร้างให้เป็น 2 แบบ + เพิ่ม field/เมธอดช่วย:

private final EmployeeService employeeService = new EmployeeService();
private boolean saved = false;
private int employeeId = -1;        // > 0 เมื่อเป็นโหมดแก้ไข

/** โหมดเพิ่มพนักงานใหม่ */
public EmployeeDialog(java.awt.Frame owner, String title) {
    super(owner, title, true);
    initComponents();
    setLocationRelativeTo(owner); // เปิดกลางหน้าต่างหลัก
}

/** โหมดแก้ไข — เติมค่าเดิมของพนักงานที่เลือกให้ทุกช่อง */
public EmployeeDialog(java.awt.Frame owner, String title, Employee editEmployee) {
    super(owner, title, true);
    initComponents();
    setLocationRelativeTo(owner); // เปิดกลางหน้าต่างหลัก
    this.employeeId = editEmployee.getId();
    txtLogin.setText(editEmployee.getName());
    txtPosition.setText(editEmployee.getPosition());
    txtPassword.setText(editEmployee.getPassword());
    setSelectedRole(editEmployee.getRole());
}

/** EmployeeFrame ใช้ตรวจหลัง dialog ปิด: true = บันทึกสำเร็จ ควรรีเฟรชตาราง */
public boolean isSaved() {
    return saved;
}

/** แปลงรายการที่เลือกใน cmbRole เป็นเลข role ที่เก็บในฐานข้อมูล */
private int getSelectedRole() {
    return cmbRole.getSelectedIndex() == 1 ? 1 : 2;
}

/** ตั้ง cmbRole ให้ตรงกับเลข role ที่เก็บในฐานข้อมูล */
private void setSelectedRole(int role) {
    cmbRole.setSelectedIndex(role == 1 ? 1 : 0);
}
📖 อ่าน constructor เป็นเรื่อง: ตัว true ตัวที่สามของ super(...) = modal — เปิดแล้วหน้าต่างหลักถูกล็อกจนกว่า dialog จะปิด · constructor overload 2 แบบ (เหมือนบทเรียน constructor ของ Employee ใน lab 10) ให้ผู้เรียกเลือก "โหมดเพิ่ม" หรือ "โหมดแก้ไข" โดยส่งพนักงานมาหรือไม่ · isSaved() คือช่องทางที่ dialog "ตอบกลับ" หน้าจอหลักหลังปิดตัว

ดับเบิลคลิก btnOK:

private void btnOKActionPerformed(java.awt.event.ActionEvent evt) {
    String name = txtLogin.getText().trim();
    String position = txtPosition.getText().trim();
    String password = new String(txtPassword.getPassword());

    if (name.isEmpty() || position.isEmpty() || password.isEmpty()) {
        JOptionPane.showMessageDialog(this,
                "กรอกข้อมูลให้ครบทุกช่องก่อนบันทึก",
                "ข้อมูลไม่ครบ", JOptionPane.WARNING_MESSAGE);
        return;
    }

    Employee emp = new Employee(name, position, password, getSelectedRole());

    if (employeeId > 0) {
        // โหมดแก้ไข — ใส่ id เดิมกลับเข้าไปก่อน update
        emp.setId(employeeId);
        if (employeeService.update(emp) == null) {
            JOptionPane.showMessageDialog(this,
                    "แก้ไขไม่สำเร็จ (ชื่อล็อกอินซ้ำกับคนอื่น หรือไม่พบแถวเดิม)",
                    "แก้ไขไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
            return;
        }
    } else {
        // โหมดเพิ่ม — save จะคืน object พร้อม id ใหม่ หรือ null ถ้าล้มเหลว
        if (employeeService.save(emp) == null) {
            JOptionPane.showMessageDialog(this,
                    "บันทึกไม่สำเร็จ (ชื่อล็อกอินซ้ำกับคนอื่น)",
                    "บันทึกไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
            return;
        }
    }

    saved = true;
    dispose();
}

ดับเบิลคลิก btnCancel:

private void btnCancelActionPerformed(java.awt.event.ActionEvent evt) {
    dispose();
}

เพิ่ม import: com.example.moogata.model.Employee, com.example.moogata.service.EmployeeService, javax.swing.JOptionPane (ตอนนี้ dialog ยังไม่มีใครเรียก — โปรแกรมรันได้ปกติ เราจะเชื่อมใน Step 11)

git add src/main/java/com/example/moogata/ui/EmployeeDialog.java
git commit -m "lab11(dialog) ช่วง 6: EmployeeDialog — ฟอร์มเพิ่ม/แก้ไขพนักงาน 1 คน"
ส่วนที่ 2 — คัดลอกไปทำแบบ dialog Step 11 จาก 12 · หน้า 14/17

Step 11 — รื้อ EmployeeFrame: ลบฟอร์มด้านบน เหลือตาราง + ปุ่ม (เรียก dialog แทน)

ถึงเวลารื้อจริง — หลักการคือ: สิ่งที่เคยอยู่ใน "ฟอร์มบน" (ช่องกรอกทั้ง 4, คอมโบ, การเดาโหมดเองด้วย editingId) ย้ายไปอยู่ใน dialog ที่สร้างเมื่อกี้แล้ว หน้าจอหลักจึงเหลือหน้าที่เดียว: โชว์ตาราง + สั่งเปิด dialog + รีเฟรชตามคำตอบ — สังเกตว่าเราจะ "ลบโค้ด" มากกว่า "เขียนโค้ด" นั่นคือสัญญาณดี

เป้าหมายของหน้าจอหลังรื้อ (ตารางเต็มหน้าต่าง ปุ่มอยู่แถวล่าง — ไม่มีฟอร์ม):

EmployeeFrame แบบ dialog
โครงหลังรื้อ: ①lblWelcome · ②tblEmployee ตารางเต็มพื้นที่ (สูงขึ้นกว่าเดิมเพราะไม่มีฟอร์มบีบ) · ③btnAdd "เพิ่มพนักงาน" (ปุ่มใหม่ แทน btnSave/btnNew เดิม) · ④btnEdit · ⑤btnDelete · ⑥btnRefresh — สังเกตว่าไม่มีช่องกรอกใด ๆ เหลืออยู่แล้ว
⚠ ลบ handler เก่าอย่างถูกวิธี (กัน compile error): ต้องถอด "สายไฟ" ก่อนลบเมธอด ไม่งั้น compile ไม่ผ่าน — มี 2 แบบ:
· ปุ่ม btnSave / btnNew: การผูกอยู่ใน guarded block → คลิกขวาปุ่ม → Events → Action → actionPerformed (คลิกเอาติ๊กออก) — NetBeans จะลบบรรทัดผูกและเมธอดให้เอง
· listener ของตาราง: บรรทัด tblEmployee.getSelectionModel().addListSelectionListener(this::tblEmployeeValueChanged); ใน constructor เป็นของเรา (ไม่อยู่ใน guarded block) → ลบบรรทัดนี้และเมธอด tblEmployeeValueChanged เองใน Source
(เช่นเดียวกัน: ถ้าจะเปลี่ยนชื่อคลาส ให้ใช้ Refactor → Rename เท่านั้น ไฟล์ .form ของ designer จะได้ตามไปด้วย)
  1. ลบ control ฝั่งฟอร์มออกจาก designer: คลิกเลือก lblLogin, txtLogin, lblPosition, txtPosition, lblPassword, txtPassword, lblRole, cmbRole แล้วกด Delete (ลบใน designer เท่านั้น — อย่าไปลบโค้ดใน guarded block เอง)
  2. ลบปุ่มเก่า 2 ตัว: btnSave และ btnNew ทิ้ง (หน้าที่ของทั้งคู่ย้ายไปอยู่ที่ btnOK ของ dialog แล้ว)
  3. ลาก Button ใหม่ 2 ตัว มาไว้หัวแถวปุ่ม: text เพิ่มพนักงาน → ตั้งชื่อตัวแปร btnAdd · text แก้ไข → ตั้งชื่อตัวแปร btnEdit (ส่วนที่ 1 ไม่มีปุ่มแก้ไข — เพราะเคยแก้ผ่านฟอร์มบน) · จากนั้น ลากตารางขยายสูงขึ้นกินพื้นที่ว่างของฟอร์มเดิม และจัดปุ่มเรียงใต้ตาราง: เพิ่มพนักงาน · แก้ไข · ลบ · รีเฟรช (ดูตำแหน่งจากภาพ · ขนาดฟอร์ม: ลากขอบให้ได้ราว ~656 × 470)
  4. แก้โค้ดฝั่ง Source: ถอด event ของ btnSave/btnNew ผ่าน designer ก่อน (ดูกล่องเตือนด้านบน) และลบบรรทัดผูก listener ของตาราง + เมธอด tblEmployeeValueChanged เองใน Source จากนั้นลบโค้ดเก่าที่เกี่ยวกับฟอร์มออก (editingId, btnSaveActionPerformed, btnNewActionPerformed, clearForm, getSelectedRole, setSelectedRole) — ส่วน constructor, showEmployees(), btnRefreshActionPerformed, roleName() คงไว้ (ยังใช้อยู่) แล้วเพิ่ม handler ใหม่ 2 ตัว (btnAdd, btnEdit — ดับเบิลคลิกสร้างก่อน) และแทนที่เนื้อในของ btnDeleteActionPerformed ที่มีอยู่แล้วจากส่วนที่ 1 (ห้ามสร้างเมธอดซ้ำ) ตามด้านล่าง
private void btnAddActionPerformed(java.awt.event.ActionEvent evt) {
    EmployeeDialog dialog = new EmployeeDialog(this, "เพิ่มพนักงานใหม่");
    dialog.setVisible(true);            // modal — โปรแกรมรอตรงนี้จนกว่า dialog จะถูกปิด
    if (dialog.isSaved()) {
        showEmployees();                // ข้อมูลเปลี่ยน → อ่านใหม่จากฐานข้อมูล
    }
}

private void btnEditActionPerformed(java.awt.event.ActionEvent evt) {
    int row = tblEmployee.getSelectedRow();
    if (row == -1) {
        JOptionPane.showMessageDialog(this,
                "เลือกพนักงานในตารางก่อนกดแก้ไข",
                "ยังไม่ได้เลือก", JOptionPane.WARNING_MESSAGE);
        return;
    }
    int id = (Integer) tblEmployee.getValueAt(row, 0);
    Employee emp = employeeService.get(id);
    if (emp == null) {
        JOptionPane.showMessageDialog(this,
                "หาพนักงานรหัส " + id + " ไม่พบ (อาจถูกลบไปแล้ว)",
                "ไม่พบข้อมูล", JOptionPane.ERROR_MESSAGE);
        return;
    }
    EmployeeDialog dialog = new EmployeeDialog(this, "แก้ไขพนักงาน: " + emp.getName(), emp);
    dialog.setVisible(true);
    if (dialog.isSaved()) {
        showEmployees();
    }
}

private void btnDeleteActionPerformed(java.awt.event.ActionEvent evt) {
    int row = tblEmployee.getSelectedRow();
    if (row == -1) {
        JOptionPane.showMessageDialog(this,
                "เลือกพนักงานในตารางก่อนกดลบ",
                "ยังไม่ได้เลือก", JOptionPane.WARNING_MESSAGE);
        return;
    }
    int id = (Integer) tblEmployee.getValueAt(row, 0);
    String selectedName = (String) tblEmployee.getValueAt(row, 1);

    // กฎธุรกิจ: ห้ามลบบัญชีที่กำลังล็อกอินอยู่ (กันผู้จัดการลบตัวเองจนเข้าระบบไม่ได้)
    if (selectedName.equals(loginUser.getName())) {
        JOptionPane.showMessageDialog(this,
                "ลบบัญชีที่กำลังใช้งานอยู่ไม่ได้",
                "ลบไม่ได้", JOptionPane.WARNING_MESSAGE);
        return;
    }

    int choice = JOptionPane.showConfirmDialog(this,
            "ต้องการลบพนักงาน " + selectedName + " ใช่หรือไม่?",
            "ยืนยันการลบ", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
    if (choice != JOptionPane.YES_OPTION) {
        return;
    }

    int deleted = employeeService.delete(id);
    if (deleted > 0) {
        showEmployees();
    } else {
        JOptionPane.showMessageDialog(this,
                "ลบไม่สำเร็จ (อาจถูกลบไปก่อนแล้ว)",
                "ลบไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
    }
}
⚠ อย่าลืมผูก event ของปุ่มใหม่ทั้ง 2 ตัว: ดับเบิลคลิก btnAdd และ btnEdit ใน designer เพื่อสร้าง btnAddActionPerformed / btnEditActionPerformed ก่อนวางโค้ด — ปุ่มที่สร้างใหม่ยังไม่มี handler จนกว่าจะดับเบิลคลิก (ส่วน btnDelete/btnRefresh ใช้ตัวเดิมจากส่วนที่ 1 — แค่แก้เนื้อในของ btnDelete) · วางโค้ดใหม่ = เฉพาะเนื้อในระหว่างปีกกา (กล่องเตือนใน Step 4)
📖 เทียบสองแบบให้เห็นกับตา: แบบฟอร์มรวม: หน้าจอเดียวมีทั้งฟอร์มและตาราง ต้องคุมสถานะ editingId + listener + clearForm ระวังชนกันเอง · แบบ dialog: หน้าจอหลักเหลือ "ตาราง + 4 ปุ่ม" ไม่มีสถานะซ่อนอยู่เลย โหมดเพิ่ม/แก้ไขถูกตัดสินที่ constructor ของ dialog — โค้ดหน้าหลักสั้นลงแต่แลกมาด้วยการคลิกเพิ่ม 1–2 ครั้งต่อการบันทึกหนึ่งรายการ นี่คือ trade-off ที่คนออกแบบ UI ต้องเลือกจริง
git add src/main/java/com/example/moogata/ui/
git commit -m "lab11(dialog) ช่วง 7: รื้อ EmployeeFrame เป็นตาราง+ปุ่ม เรียก EmployeeDialog"
ส่วนที่ 2 — คัดลอกไปทำแบบ dialog Step 12 จาก 12 · หน้า 15/17

Step 12 — ทดสอบส่วนที่ 2 ครบ 10 สถานการณ์ (สังเกตพฤติกรรมที่ต่างจากแบบรวม)

การทดสอบแบบ dialog มีหัวใจที่แบบรวมไม่มี: พฤติกรรม modal และเส้นทางยกเลิก — เพราะตอนนี้ "การกดบันทึก" และ "การแก้ข้อมูล" แยกอยู่คนละหน้าต่าง เราจึงต้องพิสูจน์เพิ่มว่า dialog ล็อกหน้าหลักจริงไหม · กดยกเลิกแล้วตารางต้องไม่เปลี่ยน · กรอกผิดใน dialog แล้ว dialog ต้องไม่ปิดทิ้ง (เพราะถ้าปิด ข้อมูลที่พิมพ์ค้างไว้จะหายหมด)

เตรียม + สัมผัสพฤติกรรม modal ก่อน (2 นาที)

  1. เช็ค Letos: ตาราง employee ต้องเหลือ 3 แถวเดิม (เหมือนตอนเตรียมของ Step 8)
  2. รัน LoginFrame ของโปรเจกต์ MoogataLabDialog (อย่าสับสนกับโปรเจกต์เดิม!) → login somjai / 1234
  3. กด "เพิ่มพนักงาน" ให้ dialog เปิดขึ้น → ลองคลิกแถวในตารางด้านหลัง → ต้องคลิกไม่ได้ (มือถือกล่อง dialog อยู่) = พิสูจน์ว่า modal ทำงาน → กด "ยกเลิก" ปิดไปก่อน

สคริปต์ทดสอบ — 10 สถานการณ์

#ทำอย่างไรต้องเห็นพิสูจน์อะไร / ต่างจากแบบรวม
1login somchai / 0000กล่องแดง ค้างหน้า loginเหมือนเดิม — การยืนยันตัวตนไม่ขึ้นกับรูปแบบหน้าจอ
2login somchai / 1234กล่องเหลือง "ไม่ใช่ผู้จัดการ..."เหมือนเดิม — กฎสิทธิ์อยู่ที่ LoginFrame ทั้งสองโปรเจกต์
3login somjai / 1234หน้าจัดการ ตาราง 3 แถว ไม่มีฟอร์มด้านบนแล้วโครงใหม่ตามที่รื้อใน Step 11
4ไม่เลือกแถว กด "แก้ไข" / "ลบ" ทีละปุ่มกล่องเหลือง "เลือกพนักงานในตารางก่อน..."มีเพิ่มในแบบ dialog — เพราะไม่มีฟอร์มให้พิมพ์แล้ว การเลือกแถวจึงเป็นเงื่อนไขบังคับของทุกปุ่มยกเว้นเพิ่ม
5กด "เพิ่มพนักงาน" → พิมพ์ครึ่งหนึ่ง → กด ยกเลิกdialog ปิด ตารางไม่เปลี่ยน (ยัง 3 แถว) ไม่มีข้อมูลค้างใน dbเส้นทางยกเลิก — isSaved() คืน false จึงไม่รีเฟรช และไม่มีอะไรเข้า db เลย
6กด "เพิ่มพนักงาน" → sompong / พนักงานเสิร์ฟ / 1234 / พนักงาน → บันทึกdialog ปิดเอง ตารางกลายเป็น 4 แถวบันทึกสำเร็จ → isSaved() true → หน้าหลักรีเฟรชให้
7เพิ่มใหม่ด้วยชื่อ somchai (ซ้ำ) → บันทึกกล่องแดง "บันทึกไม่สำเร็จ..." และ dialog ยังเปิดค้าง ข้อมูลที่พิมพ์ยังอยู่พฤติกรรมสำคัญ: กรอกผิดไม่ปิดทิ้ง ผู้ใช้แก้แล้วบันทึกซ้ำได้ทันที (แก้ชื่อเป็น sompong2 ลองบันทึกต่อได้เลย)
8เลือกแถว somchai → "แก้ไข" → เปลี่ยนตำแหน่งเป็น "หัวหน้ากะ" → บันทึกdialog เปิดมาเติมค่าเดิมพร้อม (ภาพด้านล่าง) บันทึกแล้วตารางอัปเดตโหมดแก้ไขผ่าน constructor ตัวที่สอง — ไม่มี editingId ให้สับสนแล้ว
9เลือกแถว somjai → "ลบ"กล่องเหลือง "ลบบัญชีที่กำลังใช้งานอยู่ไม่ได้"กฎธุรกิจเดิมทำงานเหมือนเดิม
10เลือกแถว sompong → "ลบ" → Yesยืนยัน → ตารางกลับ 3 แถวปิดจบข้อมูลทดสอบ db กลับสภาพเดิม
Dialog แก้ไข
ภาพของข้อ 8 (โหมดแก้ไข): dialog เปิดมาพร้อมค่าเดิมของ somchai ครบทุกช่อง (ชื่อล็อกอิน, ตำแหน่ง "แคชเชียร์", รหัสผ่านจุดดำ, บทบาท "พนักงาน") และ title bar บอกกำลังแก้ใคร — สังเกตว่าโหมดแก้ไขในแบบ dialog "ชัดเจนกว่า" แบบรวม เพราะเป็นหน้าต่างแยก ไม่ต้องสังเกตว่าฟอร์มมีค่าขึ้นมาหรือยัง
ยืนยันการลบ
ภาพของข้อ 10: กล่องยืนยันขึ้นกลางหน้าจัดการ — ทำงานเหมือนแบบรวมทุกอย่าง เพราะเป็น JOptionPane ตัวเดิม ไม่เกี่ยวกับการรื้อ
หลังเพิ่ม sompong
ภาพสถานะ 4 แถวของข้อ 6 (ถ่ายเป็นหลักฐานได้): sompong อยู่แถวที่ 4 ก่อนถูกลบในข้อ 10 — เทียบกับภาพ 07-after-add ของแบบรวมได้ว่าผลลัพธ์ข้อมูลเหมือนกันเป๊ะ ต่างแค่ขั้นตอนการคลิก

ปิดการทดสอบ

เช็ค Letos ต้องเหลือ 3 แถวเดิม · ปิดโปรแกรม · ลองเปิดโปรเจกต์ MoogataLab เดิม รันเทียบ — ทั้งสองโปรเจกต์ต้องยังใช้งานได้ครบ = เรามีระบบ 2 รูปแบบบนหลังบ้านชุดเดียวกัน

ถ้าข้อนี้ไม่ผ่านปัญหาอยู่ที่ย้อนไปดู
1–3ไม่เกี่ยวกับการรื้อ — สำเนา/หน้า login/กฎสิทธิ์Step 9 (คัดลอก) · Step 4 (login) · Step 6 (กฎสิทธิ์) ของส่วนที่ 1
4 (ไม่เลือกแถวแล้วไม่เตือน)เงื่อนไข row == -1 ใน btnEdit/btnDeleteStep 11 (โค้ดชุดใหม่)
5 (ยกเลิกแล้วข้อมูลเข้า / ตารางเด้ง)isSaved() หรือเงื่อนไข if (dialog.isSaved())Step 10 (btnOK/btnCancel) · Step 11 (btnAdd)
6–8 (บันทึก/แก้ไข)btnOK สองโหมด / constructor overloadStep 10
7 (กรอกผิดแล้ว dialog ปิดทิ้ง)ลืม return; ในสาย error ของ btnOKStep 10
9–10 (ลบ)กฎธุรกิจ / ยืนยันStep 11 (btnDelete)
✓ เช็ค: ผ่านครบ 10 สถานการณ์ = จบ lab — ตอนนี้มีโปรเจกต์คู่กัน 2 ตัว: MoogataLab (ฟอร์มรวม) และ MoogataLabDialog (dialog) หลังบ้านเหมือนกัน 100%
ปิดท้าย หน้า 16/17

Troubleshooting — อาการที่เจอบ่อยและวิธีแก้

#อาการสาเหตุ / วิธีแก้
1no such table: employeeโปรแกรมเปิด db ผิดไฟล์ — moogata.db ต้องอยู่รากโปรเจกต์ข้าง ๆ pom.xml (สำเนา MoogataLabDialog ต้องมีไฟล์ db ของตัวเองด้วย — คัดลอกมาพร้อมโฟลเดอร์แล้ว)
2ภาษาไทยบนหน้าจอเป็นกล่อง □□□ฟอนต์ไม่รองรับไทย — Windows: ตั้ง Leelawadee UI หรือ Tahoma · macOS: Thonburi · Linux: Noto Sans Thai / Garuda (อย่างน้อย lblHeader ที่ตั้ง font เอง)
3คลิกแถวแล้วฟอร์มไม่เติม / ล้างฟอร์มแล้วโปรแกรมคว่ำ (เฉพาะแบบฟอร์มรวม)ลืมบรรทัดผูก listener ใน constructor (getSelectionModel().addListSelectionListener(this::tblEmployeeValueChanged); — JTable ไม่มีเมนู Events นี้ให้ ต้องพิมพ์ผูกเอง) หรือลืม guard if (row == -1) return; บนสุดของ handler
4บันทึก/ลบแล้วตารางไม่อัปเดตลืมเรียก showEmployees() หลังบันทึกสำเร็จ / หลัง dialog.isSaved() — ตารางไม่รู้เองว่าฐานข้อมูลเปลี่ยน
5แก้ไขแล้วกลายเป็น "เพิ่มแถวใหม่" (เฉพาะแบบฟอร์มรวม)ลืม editingId — คลิกแถวต้องเก็บ editingId = emp.getId() และตอนบันทึกต้องเช็ค editingId > 0 ก่อนเลือก update/save
6UNIQUE constraint failed: employee.employee_nameชื่อล็อกอินซ้ำ — constraint ทำงานไม่ใช่บั๊ก (save คืน null → หน้าจอโชว์กล่องแดง)
7dialog ปิดแล้วหน้าจอหลักค้าง / ไม่ล็อกหน้าจอหลักเรื่อง modal — constructor ต้องส่ง true ตัวที่สาม: super(owner, title, true)
8กดปุ่มแล้วเงียบevent ไม่ได้ผูก — ต้องดับเบิลคลิกปุ่มใน designer เพื่อสร้าง handler (ปุ่มใหม่ใน Step 11 คือ btnAdd และ btnEdit ทั้งคู่)
9cannot find symbol: class EmployeeDialog/EmployeeFrameสร้างคลาสคนละ package — ทุกหน้าจอต้องอยู่ com.example.moogata.ui และ import ให้ครบ
10เปิด MoogataLabDialog แล้ว NetBeans งงกับโปรเจกต์เดิม / เห็นชื่อเดียวกันสองตัวลืมแก้ pom.xml ของสำเนา — ต้องเปลี่ยนทั้ง <artifactId> และ <name> (Step 9 ข้อ 3)
11ข้อความไทยในตาราง/ปุ่มเพี้ยนเป็นตัวประหลาด (แต่ไม่ใช่กล่อง □)คนละปัญหากับข้อ 2 — นี่คือ encoding ของไฟล์: เช็ค pom.xml ต้องมี <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> (โปรเจกต์จาก Lab Week 10 มีให้แล้ว — เจอตอนสร้าง pom ใหม่เอง)
12database is locked ตอนเพิ่ม/แก้/ลบจากโปรแกรมเปิด moogata.db ค้างใน Letos แล้วมี transaction ยังไม่ commit — กลับไปกด Commit (Ctrl+Enter) ในแท็บ Data ของ Letos หรือปิด Letos แล้วรันโปรแกรมใหม่
13รันครั้งแรก (Shift+F6) แล้ว fail หรือค้างที่ดาวน์โหลดMaven ต้องดาวน์โหลด plugin (เช่น exec-maven-plugin) รอบแรก — ต้องมีอินเทอร์เน็ต ถ้าห้องแล็บติด proxy ให้ตั้งค่า Maven ก่อน หรือรันให้ cache ไว้ที่บ้านมา
14ลบ handler แล้ว cannot find symbol หรือ compile ไม่ผ่านการผูก event ยังค้างใน guarded block — ถอดผ่าน designer ก่อน (คลิกขวาปุ่ม → Events → เอาติ๊กออก) แล้วค่อยลบเมธอด ดู Step 11 · เปลี่ยนชื่อคลาสให้ใช้ Refactor → Rename เท่านั้น (ไฟล์ .form จะตามไปด้วย)
ปิดท้าย หน้า 17/17

เช็คลิสต์ส่งงาน + สรุปเรื่องราวทั้ง lab

เช็คลิสต์ส่งงาน

สรุปเรื่องราวทั้ง lab — เราได้อะไรมาบ้าง

เริ่ม lab นี้ด้วย: ระบบที่ทำงานได้แต่ต้องใช้ผ่านคอนโซล — นักพัฒนาใช้ได้ พนักงานร้านใช้ไม่ได้

สิ่งที่เกิดขึ้นตลอด 12 Step: เราแก้ของเก่าแค่ไฟล์เดียว (EmployeeService เปิดบริการ CRUD) แล้วสร้างหน้าจอใหม่ทั้งหมด 2 ชุดบนหลังบ้านชุดเดียวกัน — ชุดแรกให้ทุกอย่างอยู่หน้าต่างเดียว (ฟอร์มบน ตารางล่าง) ต้องคุม "โหมดเพิ่ม/แก้ไข" เองด้วยสถานะและ listener · ชุดที่สองคัดลอกงานเดิมมารื้อ ย้ายฟอร์มไปเป็น dialog ทำให้หน้าหลักเหลือตารางกับปุ่ม ไม่มีสถานะซ่อนอยู่เลย

บทเรียนที่ควรติดตัวออกจาก lab:

  • หน้าจอคุยกับ service เท่านั้น — จึงสลับรูปแบบหน้าจอได้โดยไม่แตะ SQL เลยสักบรรทัด
  • การเปลี่ยนข้อมูลบนจอเสมอ = "อ่านใหม่จากฐานข้อมูล" (showEmployees()) ไม่ใช่แก้ตารางตรง ๆ
  • ฟอร์มเดียวสองโหมด (แบบรวม) แลกด้วยสถานะที่ต้องระวัง · dialog แยกโหมดชัดเจน แลกด้วยคลิกเพิ่ม — ไม่มีแบบไหนชนะขาด เลือกตามงาน
  • ทดสอบเป็น "สคริปต์สถานการณ์" ไม่ใช่กดเล่น — และ db ต้องกลับสภาพเดิมหลังทดสอบจบ

เรื่องต่อไป: ระบบ POS เต็มของร้าน (pos/) ก็ใช้โครงเดียวกันนี้ — เพียงแต่เลเยอร์ UI เป็นหน้าจอเว็บ และมีตารางจริงหลายตาราง (โต๊ะ บิล โปรโมชัน สมาชิก) ที่ต่อกันด้วย FK ตามที่เคยเรียน — สิ่งที่ฝึกมาทั้ง 2 lab คือเสาหลักของมัน

เอกสารอ้างอิงเต็ม (README ของแต่ละแบบ)