Skip to main content

Campaign Planner

ภาพรวม​

Campaign Planner คือปฏิทินรายเดือนที่นำแคมเปญทั้งหมดมาจัดเรียงตามวัน สำหรับผู้ดูแลระบบและทีมการตลาดที่ต้องวางแผนว่าจะส่งแคมเปญวันไหน วันไหนส่งถี่เกินไป และมีแคมเปญร่างค้างอยู่หรือไม่

จุดสำคัญที่ต้องเข้าใจก่อนใช้งาน — เมนูนี้เป็นฟีเจอร์ อ่านอย่างเดียว และ ไม่มีโมดูลฝั่งเซิร์ฟเวอร์ของตัวเองเลย ทุกอย่างที่เห็นบนปฏิทินคือข้อมูลชุดเดียวกับเมนู Campaign ที่ดึงผ่าน GET /api/campaign ตัวเดิม เพียงแต่หน้าเว็บนำมาจัดวางเป็นปฏิทินแทนตาราง (ค้นทั้งโค้ดฝั่ง backend ไม่พบคำว่า "planner" อยู่ในไฟล์ใดเลย — ไม่มี controller, ไม่มี service, ไม่มี endpoint เฉพาะ)

บทบาทที่ใช้งาน — ผู้ใช้ที่มีสิทธิ์ดูข้อมูลแคมเปญ (ใช้สิทธิ์ตัวเดียวกับเมนู Campaign ทั้งฝั่งหน้าเว็บและฝั่งเซิร์ฟเวอร์ — ดูหัวข้อ สิทธิ์การเข้าถึง)

เส้นทางผู้ใช้งาน​

1. เปิดปฏิทินและอ่านการ์ดสรุป​

เปิดเมนู Campaign Planner ระบบแสดงปฏิทินของเดือนปัจจุบันพร้อมการ์ดสรุปด้านบน — การ์ดใบซ้ายแสดงชื่อเดือน/ปีกับยอด แคมเปญทั้งหมด ของเดือนนั้น และการ์ดใบขวาแยกยอดตาม 4 สถานะ (ส่งแล้ว / ตั้งเวลา / ยกเลิกแล้ว / ฉบับร่าง)

ระหว่างที่ยังโหลดข้อมูลไม่ครบ ตัวเลข "แคมเปญทั้งหมด" จะแสดงเป็นขีด — และพื้นที่ปฏิทินทั้งผืนจะถูกแทนด้วยวงหมุนรอโหลด ยังไม่แสดงตารางวันใด ๆ

2. อ่านปฏิทินรายวัน​

  1. แต่ละช่องวันแสดงรายการแคมเปญได้สูงสุด 3 รายการ พร้อมจุดสีตามสถานะ ถ้าวันนั้นมีมากกว่านั้นจะมีปุ่ม "+N เพิ่มเติม" ให้กดเปิดแผงรายการทั้งหมดของวันนั้น (แผงนี้เลื่อนดูได้ สูงไม่เกิน 420px และมีหัวข้อบอกวันที่กับจำนวนรวม)
  2. ลำดับภายในวันเดียวกันถูกจัดให้เสมอ — ตั้งเวลา ขึ้นก่อน แล้วต่อด้วย ส่งแล้ว → ฉบับร่าง → ยกเลิก ถ้าสถานะเท่ากันจึงเรียงตามเวลาที่ตั้งไว้
  3. มีแถบคำอธิบายสีอยู่เหนือปฏิทินกำกับว่าจุดสีใดหมายถึงสถานะใด

3. ดูข้อมูลย่อและเข้าดูรายละเอียด​

  1. นำเมาส์ไปวางค้างบนรายการ (หน่วง 0.3 วินาที) เพื่อเปิดกล่องข้อมูลย่อ — ป้ายสถานะ ชื่อแคมเปญ กลุ่มผู้รับ (ถ้าไม่มีกลุ่มจะขึ้นว่า "เพื่อนทั้งหมดใน OA") จำนวนผู้รับ (แสดงเฉพาะเมื่อมากกว่า 0) และบรรทัดวันเวลาที่เปลี่ยนป้ายกำกับตามสถานะ — ส่งแล้ว / ตั้งเวลา / ยกเลิกแล้ว / แก้ไขล่าสุด (สำหรับฉบับร่าง)
  2. คลิกที่รายการเพื่อเข้าดูรายละเอียด — ระบบพาออกจาก Planner ไปที่ฟอร์มของเมนู Campaign ในโหมด ดูอย่างเดียว (?view=true) ไม่ว่าแคมเปญนั้นจะเป็นสถานะใด แม้เป็นฉบับร่างก็เปิดในโหมดอ่านเช่นกัน ต้องกลับไปที่หน้ารายการของเมนู Campaign ถ้าอยากแก้ไข

4. เปลี่ยนเดือน​

เปลี่ยนเดือนด้วยตัวควบคุมบนหัวปฏิทิน ระบบดึงข้อมูลชุดใหม่ให้อัตโนมัติ (เฉพาะการเปลี่ยนแบบ "เดือน" เท่านั้นที่ทำให้ดึงข้อมูลใหม่ — การสลับมุมมองเป็นรายปีไม่ได้เรียก API)

รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง​

ยืนยันจาก src/components/campaign-planner/ ของ cms-web-2026 ร่วมกับ internal/modules/campaign/{controller,service}.go และ internal/core/pagination/pagination.go ของ line-management-cms-api-go:

  • หน้านี้ยิง API 4 ครั้งขนานกัน ไม่ใช่ครั้งเดียว — เพราะ GET /api/campaign กรองได้ทีละสถานะเท่านั้น หน้าเว็บจึงเรียกแยกกัน 4 คำขอ (ส่งแล้ว / ตั้งเวลา / ฉบับร่าง / ยกเลิก) แล้วนำผลมาต่อกันเป็นชุดเดียวฝั่ง browser ทุกคำขอส่ง limit: 500 และ ไม่มีการแบ่งหน้า — ตัวเลขนี้เป็นเพดานจริง เพราะฝั่ง backend ตรวจ limit แค่ Min(1) กับ IsInt ไม่มีขีดบนไว้เลย จึงยอมรับ 500 ตามที่ขอ แต่ถ้าเดือนใดมีแคมเปญสถานะเดียวเกิน 500 รายการ ส่วนที่เกินจะหายไปจากปฏิทินโดยไม่มีการเตือน
  • ช่วงวันที่ที่ดึงกว้างกว่าเดือนที่แสดงอยู่ 7 วันทั้งสองข้าง — คำขอทุกตัวส่ง startDate = ต้นเดือน − 7 วัน และ endDate = ปลายเดือน + 7 วัน เพื่อให้ช่องวันของเดือนก่อน/เดือนถัดไปที่โผล่มาในตารางปฏิทินมีข้อมูลด้วย แต่ การ์ดสรุปด้านบนคำนวณใหม่ฝั่งหน้าเว็บโดยตัดให้เหลือเฉพาะวันในเดือนนั้นจริง ๆ จึงเป็นเรื่องปกติที่จะเห็นรายการอยู่ในช่องวันท้าย/ต้นตารางแต่ไม่ถูกนับในการ์ด
  • คอลัมน์วันที่ที่ backend ใช้กรองเปลี่ยนไปตามสถานะที่กรอง — ในฟังก์ชัน setFilter ถ้าสถานะเป็น sent/scheduled จะกรองด้วย campaign.start_date แต่ถ้าเป็น cancel/draft จะกรองด้วย campaign.updated_date แทน (และถ้าไม่ระบุสถานะมาเลย เงื่อนไขวันที่จะถูกข้ามไปทั้งหมด) — Planner จัดวางรายการลงช่องวันด้วยกฎเดียวกันเป๊ะ (ส่งแล้ว/ตั้งเวลา ใช้ startDate; ฉบับร่าง/ยกเลิก ใช้ updatedDate แล้ว fallback เป็น startDate) ทั้งสองฝั่งจึงตรงกัน แต่ความหมายของช่องวันไม่เท่ากันทุกสถานะ ดูรายละเอียดในหัวข้อจุดที่ควรระวัง
  • วันที่และตัวเลขบนหน้านี้ถูกล็อกเป็นรูปแบบภาษาอังกฤษไว้ในโค้ด — ทั้งชื่อเดือนบนการ์ดสรุป วันเวลาในกล่องข้อมูลย่อ และหัวข้อของแผง "+N" สร้างจาก new Intl.DateTimeFormat('en-US', …) ที่ประกาศไว้ตายตัว ส่วนจำนวนผู้รับใช้ new Intl.NumberFormat('en-US') — สลับภาษาหน้าเว็บเป็นไทยแล้ว ป้ายกำกับจะเปลี่ยนเป็นไทยแต่ตัวเดือนและวันเวลาจะยังเป็นอังกฤษ (เช่น "August 2026", "Wednesday, August 19, 2026")
  • ไม่มีการใช้ cache ของ backend — GET /api/campaign มีโหมด cache ผ่าน Redis ให้ใช้เมื่อส่ง query parameter cache มาด้วย แต่ Planner ไม่ได้ส่งพารามิเตอร์นี้เลย ทุกครั้งที่เปลี่ยนเดือนจึงเป็นการอ่านฐานข้อมูลสด (แคชอยู่ที่ฝั่ง React Query ของ browser ตามคีย์ช่วงวันที่แทน)
  • ปฏิทินรอครบทั้ง 4 คำขอก่อนจึงวาด — ตัวแปร isLoading เป็นการรวม (OR) สถานะโหลดของทุกคำขอ ดังนั้นถ้าคำขอใดคำขอหนึ่งช้า ทั้งปฏิทินจะยังเป็นวงหมุนอยู่ ไม่ได้ทยอยแสดงสถานะที่โหลดเสร็จก่อน
  • ลิงก์ปลายทางเป็นโหมดอ่านเสมอ — ทั้งรายการในช่องวันและรายการในแผง "+N" ชี้ไปที่ /campaign-management/form?id={id}&view=true แบบเดียวกันทั้งหมด ไม่มีเส้นทางไหนใน Planner ที่พาไปโหมดแก้ไข

เมนูที่เกี่ยวข้องกันในเส้นทางนี้​

  • Campaign — เป็นทั้งแหล่งข้อมูลเดียวของหน้านี้ (GET /api/campaign) และปลายทางของทุกการคลิก การสร้าง/แก้ไข/ยกเลิก/ลบแคมเปญทำได้ที่เมนูนั้นเท่านั้น
  • Dashboard — ปุ่ม View All บนการ์ด "Upcoming Activities" ของหน้า Dashboard พามาที่ Campaign Planner นี้
  • Segment audience — ชื่อกลุ่มผู้รับที่แสดงในกล่องข้อมูลย่อคือกลุ่มเป้าหมายที่เตรียมไว้จากเมนูนั้น (ถ้าแคมเปญเป็นแบบ Broadcast จะขึ้นว่า "เพื่อนทั้งหมดใน OA")

จุดที่ควรระวัง​

  • ช่องวันของแคมเปญ "ฉบับร่าง" และ "ยกเลิกแล้ว" ไม่ใช่วันที่ตั้งใจจะส่ง — ทั้งสองสถานะถูกจัดลงช่องวันตาม วันที่แก้ไขล่าสุด (ตามคอลัมน์ที่ backend ใช้กรอง) ดังนั้นแคมเปญที่ตั้งใจส่งสิ้นเดือนแต่เพิ่งไปแก้ร่างวันนี้ จะปรากฏที่ วันนี้ ไม่ใช่สิ้นเดือน — อ่านปฏิทินเป็น "แผนการส่ง" ได้เต็มที่เฉพาะสองสถานะแรก (ส่งแล้ว/ตั้งเวลา) เท่านั้น
  • เพดาน 500 รายการต่อสถานะต่อเดือน — เกินจากนั้นจะไม่แสดงและไม่มีข้อความเตือน ทั้งการ์ดสรุปและปฏิทินจะแสดงยอดที่ต่ำกว่าความจริง
  • แคมเปญที่เพิ่งกด "ส่งทันที" อาจไม่ปรากฏบนปฏิทินชั่วคราว — ระหว่างที่ backend ตั้งสถานะเป็น sending รอ worker ประมวลผล แคมเปญนั้นไม่เข้าเงื่อนไขสถานะใดใน 4 คำขอของ Planner เลย จึงหายไปจากปฏิทินเช่นเดียวกับที่หายไปจากทุกแท็บของเมนู Campaign
  • ไม่มีปุ่มรีเฟรช — วิธีดึงข้อมูลใหม่คือเปลี่ยนเดือนไปแล้วเปลี่ยนกลับ หรือโหลดหน้าใหม่
  • ชื่อเดือนและวันเวลาแสดงเป็นภาษาอังกฤษเสมอแม้ใช้หน้าเว็บภาษาไทย (เป็นข้อจำกัดที่ล็อกไว้ในโค้ด ไม่ใช่การตั้งค่า)

สิทธิ์การเข้าถึง (ตรวจสอบจากซอร์สโค้ดจริง)​

  • ฝั่งหน้าเว็บ — หน้านี้ถูกครอบด้วย PermissionGuard ตรวจ action VIEW ของ subject campaign-management (ไม่ใช่ subject ของตัวเอง) ในไฟล์ src/app/campaign-planner/page.tsx — ใครที่เห็นเมนู Campaign ก็เห็นเมนูนี้ด้วยโดยปริยาย และในทางกลับกัน ถ้าปิดสิทธิ์ดูแคมเปญก็จะเข้าหน้านี้ไม่ได้ทันที
  • ฝั่งเซิร์ฟเวอร์ — ไม่มีโมดูลหรือ ModuleGate ของ Campaign Planner เลย เพราะไม่มี endpoint ของตัวเอง ทุกคำขอที่หน้านี้ยิงออกไปวิ่งผ่าน ModuleGate("campaign") ของโมดูล Campaign ที่บังคับใช้จริงอยู่แล้ว — ถ้าผู้ดูแลระดับแพลตฟอร์มปิดโมดูลแคมเปญให้องค์กรใด หน้า Planner ของผู้ใช้ในองค์กรนั้นจะได้ 403 ทั้ง 4 คำขอและแสดงปฏิทินว่าง
  • policy annotation ที่ติดมากับเส้นทางนั้นคือ readAll ของโมดูล campaign แต่วิ่งผ่าน middleware ที่เป็น no-op ตามทั้งระบบ — การบังคับจริงจึงอยู่ที่ ModuleGate ระดับโมดูลของ Campaign เท่านั้น

ไฟล์อ้างอิงจากซอร์สโค้ด​

Frontend (cms-web-2026)

  • src/app/campaign-planner/page.tsx — จุดเข้าหน้า, PermissionGuard ที่ตรวจ subject campaign-management
  • src/components/campaign-planner/campaign-planner.container.tsx — 4 คำขอขนาน, ช่วงวันที่ ±7 วัน, การจัดกลุ่มลงวันและการคำนวณการ์ดสรุป
  • src/components/campaign-planner/campaign-calendar.tsx — ตารางปฏิทิน, เพดาน 3 รายการต่อวัน, ปุ่ม "+N", แถบคำอธิบายสี
  • src/components/campaign-planner/campaign-event-tooltip.tsx, campaign-day-list.tsx — กล่องข้อมูลย่อและแผงรายการรายวัน (จุดที่ล็อกรูปแบบวันที่เป็น en-US)
  • src/components/campaign-planner/status-badge.tsx — สีและป้ายของแต่ละสถานะ
  • src/services/campaign-management.service.ts — ฟังก์ชัน getList ที่หน้านี้เรียกใช้ร่วมกับเมนู Campaign

Backend (line-management-cms-api-go)

  • internal/modules/campaign/controller.go — เส้นทาง GET /campaign และ ModuleGate("campaign") ที่ครอบอยู่
  • internal/modules/campaign/service.go — ฟังก์ชัน setFilter (จุดที่คอลัมน์วันที่เปลี่ยนตามสถานะ) และโหมด cache ที่หน้านี้ไม่ได้ใช้
  • internal/core/pagination/pagination.go — จุดที่ยืนยันว่า limit ไม่มีขีดบน

ดูรายละเอียดเชิงเทคนิคเพิ่มเติม (เวอร์ชันก่อนหน้าของทีมเอกสาร) ที่ ปฏิทินวางแผนแคมเปญ