diff --git a/docs/setup/uat-system-flow.html b/docs/setup/uat-system-flow.html new file mode 100644 index 0000000..83d91c7 --- /dev/null +++ b/docs/setup/uat-system-flow.html @@ -0,0 +1,1176 @@ + + + + + + ALLA OS UAT Setup & System Flow + + + +
+
+
+
ALLA OS UAT Operation Guide
+

UAT Setup & System Flow

+

เอกสารนี้สรุปขั้นตอนเตรียมระบบ UAT จากชุด CSV ใน setup/ALLA-ONVALLA โดยเล่าแบบ user action ทีละ step เพื่อให้ tester รู้ว่าต้องทำอะไร ระบบทำอะไร และควรเห็นผลลัพธ์อะไรหลังจบแต่ละช่วง

+
+ +
+ +
+
+
+
Flow Timeline
+

ภาพรวมการเตรียมระบบ

+
+

ให้ไล่จากซ้ายไปขวา หาก step ใดขึ้น FAIL ให้แก้ข้อมูลหรือ environment ก่อนค่อยไปต่อ ห้าม commit CSV ถ้า preview ยังมี blocking error

+
+
+
Reset DBล้างฐานข้อมูล UAT
+
Migrateสร้าง schema ล่าสุด
+
Preview CSVตรวจไฟล์และ references
+
Commit CSVนำข้อมูลเข้าแบบ upsert
+
Complete Setupปิด bootstrap run
+
Readinessตรวจข้อมูลที่ต้องพร้อม
+
Login UATเริ่มทดสอบจาก role จริง
+
+
+ +
+
+
+
Step By Step
+

User action ในแต่ละขั้นตอน

+
+

ทุก step ด้านล่างออกแบบให้ tester หรือ admin ทำตามได้โดยไม่ต้องอ่านโค้ด

+
+
+
+

1 Reset DB

+
+
User Action

ยืนยันว่าจะล้างข้อมูล UAT แล้วรัน reset database ด้วย explicit confirmation

+
System Action

ลบ schema และตารางเดิม แล้วสร้าง schema ว่างสำหรับเริ่มรอบใหม่

+
Expected Result

ฐานข้อมูลไม่มีข้อมูล business เดิมค้างอยู่

+
Troubleshooting

ถ้า reset ถูก block ให้ตรวจ host, database name และ flag reset safety

+
+
+ +
+

2 Run Migrations

+
+
User Action

รัน migration เพื่อสร้างตารางตาม schema ล่าสุด

+
System Action

Drizzle applies migrations และสร้าง migration history

+
Expected Result

ตารางหลัก เช่น users, organizations, memberships, CRM, approval พร้อมใช้งาน

+
Troubleshooting

ถ้า migrate fail ให้แก้ schema/migration ก่อน import CSV

+
+
+ +
+

3 Preview CSV

+
+
User Action

เลือก source เป็น configured directory: setup/ALLA-ONVALLA

+
System Action

อ่าน 11 required CSV, validate header, duplicate, enum, dependency และ references

+
Expected Result

Preview ต้องมี 0 errors ก่อน commit

+
Troubleshooting

ถ้ามี unknown option ให้เติม master option หรือแก้ code ให้ตรงไฟล์อ้างอิง

+
+
+ +
+

4 Commit CSV

+
+
User Action

ใช้ preview hash ล่าสุดเพื่อ commit ข้อมูล CSV

+
System Action

นำข้อมูลเข้าแบบ transaction/upsert ตามลำดับ dependency

+
Expected Result

ข้อมูล users, orgs, memberships, roles, approvals, sequences ถูกสร้างครบ

+
Troubleshooting

ถ้า hash mismatch ให้ preview ใหม่ แล้ว commit ด้วย hash ใหม่

+
+
+ +
+

5 Complete Setup

+
+
User Action

กด complete หรือเรียก setup completion หลัง required steps ผ่านแล้ว

+
System Action

บันทึก setup run, seed manifest และล็อกสถานะ bootstrap เป็น completed

+
Expected Result

Setup run ล่าสุดเป็น completed และระบบออกจาก bootstrap mode

+
Troubleshooting

ถ้า complete ถูก block ให้ดู missing step หรือ readiness check ที่ยังไม่ผ่าน

+
+
+ +
+

6 Readiness Check

+
+
User Action

ตรวจจำนวนข้อมูลและจุด readiness ก่อนเริ่ม UAT

+
System Action

เช็ค active organization, role assignment, approval setup และ document sequences

+
Expected Result

ALLA และ ONVALLA มี users, Lead status, THB currency, quotation sequence พร้อม

+
Troubleshooting

ถ้า Lead สร้างไม่ได้ ให้ตรวจ crm_lead_status:new_job และ crm_lead sequence

+
+
+
+
+ +
+
+
+
Dataset Snapshot
+

ข้อมูลที่ควรถูก import

+
+

ตัวเลขเหล่านี้มาจาก CSV source ปัจจุบัน ไม่รวม row ที่ระบบสร้างเองระหว่าง runtime

+
+
+
25users.csv
+
2organizations.csv
+
25memberships.csv
+
27crm-role-assignments.csv
+
122master-options.csv
+
12document-sequences.csv
+
6approval-workflows.csv
+
24approval steps + matrix rows
+
+
+ +
+
+
+
Import Order
+

CSV dependency map

+
+

ระบบต้องอ่านตามลำดับนี้ เพราะไฟล์ท้าย ๆ อ้างอิงข้อมูลจากไฟล์ก่อนหน้า

+
+ + + + + + + + + + + + + + + + + + + + + +
OrderCSV FileTester Checkpoint
01users.csvมี user UAT ครบทั้ง ALLA และ ONVALLA
02organizations.csvมีองค์กร ALLA และ ONVALLA
03memberships.csvuser ทุกคนมี organization access
05crm-role-assignments.csvrole/scopes พร้อมสำหรับ marketing, sales, manager, admin
06master-options.csvoptions สำคัญ เช่น currency, lead status, customer status พร้อม
07branches.csvbranch head_office พร้อมใช้งานในแต่ละ org
08product-types.csvproduct type เช่น crane, dockdoor, solarcell พร้อม
14document-sequences.csvเลขเอกสารสำหรับ customer, lead, opportunity, quotation พร้อม
15approval-workflows.csvworkflow quotation 3 ระดับต่อ org
16approval-steps.csvstep approval เรียงตาม role
17approval-matrix.csvrouting approval ตาม product และ amount พร้อม
+
+ +
+
+
+
UAT Readiness
+

Checklist ก่อนเริ่มทดสอบ

+
+

ใช้รายการนี้เป็น gate สั้น ๆ ก่อนส่งให้ tester เริ่มใช้งานจริง

+
+
+
PASSOrganizations

ALLA Company Limited และ ONVALLA Demo Company พร้อม

+
PASSUsers & Memberships

25 users มี active organization และ membership

+
PASSCRM Authorization

27 role assignments ครอบคลุม UAT roles

+
PASSLead Creation

new_job status และ crm_lead sequence พร้อม

+
PASSQuotation Approval

workflow, steps, matrix พร้อมสำหรับ quotation routing

+
NOTEImport Warning

WARNING แบบ AUDIT_GAP คือข้อจำกัด import engine ไม่ใช่ข้อมูลผิด

+
+
+ +
+
+
+
Login Reference
+

บัญชีที่ใช้เริ่ม UAT

+
+

เอกสารนี้ไม่แสดง password ให้เปิดดูจากช่องทางจัดการ credential หรือไฟล์ CSV เฉพาะผู้มีสิทธิ์เท่านั้น

+
+ + + + + + + + + + + + + + + + +
RoleALLAONVALLAใช้ทดสอบอะไร
System Adminsuper_admin@allaos.local-setup, admin access, configuration
Marketingmk.uat@allaos.localmk.onvalla@allaos.localสร้าง Lead และติดตาม handoff
Sales Managermanager.uat@allaos.localmanager.onvalla@allaos.localดูทีม, assign, approve ขั้นต้น
Salessales_crane.uat@allaos.localsales_crane.onvalla@allaos.localOpportunity และ Quotation
Department / Top Managergm.uat@allaos.local / ceo.uat@allaos.localgm.onvalla@allaos.local / ceo.onvalla@allaos.localapproval ระดับสูงและ dashboard monitoring
+
หาก login แล้วไม่เห็นเมนู ให้ตรวจ membership, CRM role assignment, branch scope และ product type scope ของ user นั้นก่อน
+
+ +
+
+
+
After Setup
+

หลัง setup แล้ว tester ทำอะไรต่อ

+
+

นี่คือ business flow สั้น ๆ เพื่อยืนยันว่าระบบ UAT พร้อมใช้งานจริง

+
+
+
1. Loginเข้าใช้งานด้วย user ตาม role ที่ต้องทดสอบ
+
2. Dashboardตรวจว่าเมนูและข้อมูลพื้นฐานโหลดได้
+
3. Create LeadMarketing สร้าง Lead ด้วย status new_job
+
4. Assign Leadส่งต่อให้ Sales และสร้าง sales-owned work item
+
5. Create QuotationSales สร้าง quotation จาก customer/opportunity
+
6. Submit Approvalตรวจ matrix routing และ approval chain ตามยอดเงิน
+
+
+ +
+
+
+
Daily Workspace
+

My Day ใช้งานอย่างไร

+
+

My Day คือหน้ารวม “งานที่ควรทำก่อน” ของผู้ใช้ในวันนั้น ไม่ใช่ที่เก็บข้อมูลหลักของ CRM แต่เป็น workspace ที่ดึงงานจาก Activities, Calendar, Approvals, Hot Projects และ Timeline มาเรียงให้ตัดสินใจง่าย

+
+
+
+

1 เปิด My Day

+
+
User Action

Login ด้วย user UAT แล้วเข้าเมนู CRM > My Day

+
System Action

โหลด workspace ตามสิทธิ์ของ user และแสดงโหมด Personal mode หรือ Manager mode

+
Expected Result

เห็นสรุป Activities, Meetings, Visits, Overdue, Hot Projects และ Approvals

+
Troubleshooting

ถ้าไม่เห็นเมนู ให้ตรวจ membership, CRM role assignment และ permission crm.activity.read

+
+
+ +
+

2 เลือกช่วงงาน

+
+
User Action

กดตัวกรอง today, tomorrow หรือ this week เพื่อดูคิวงานตามช่วงเวลา

+
System Action

กรองรายการตามวันครบกำหนด เวลาใน calendar และ scope ของ user หรือทีม

+
Expected Result

Priority Queue และ Today's Schedule เปลี่ยนตาม range ที่เลือก

+
Troubleshooting

ถ้ารายการว่าง ให้เช็คว่า activity มี assignee, due date/start date และอยู่ใน organization เดียวกับ user

+
+
+ +
+

3 ทำงานจาก Priority Queue

+
+
User Action

เริ่มจากรายการ P1/P2 ก่อน แล้วคลิกงานเพื่อเปิด record ต้นทาง เช่น Activity, Opportunity, Quotation หรือ Approval

+
System Action

รวม overdue activities, งานครบกำหนด, approval pending และ hot project แล้วจัดลำดับความสำคัญ

+
Expected Result

ผู้ใช้รู้ว่าต้อง follow-up งานไหนก่อน โดยไม่ต้องเปิดหลายหน้าเพื่อหาเอง

+
Troubleshooting

ถ้าคลิกแล้วไปหน้าที่ไม่เห็นข้อมูล ให้ตรวจสิทธิ์ของ source record และ branch/product scope

+
+
+ +
+

4 เพิ่มหรือติดตาม Activity

+
+
User Action

กด Add Activity เมื่อมีงาน follow-up ใหม่ หรือเปิด Activity เดิมเพื่ออัปเดตสถานะ/กำหนดเวลา

+
System Action

บันทึก Activity เป็นแหล่งข้อมูลหลัก แล้วส่งผลให้ My Day, Calendar และ Timeline แสดงงานล่าสุด

+
Expected Result

งานที่มี due date หรือ schedule จะกลับมาแสดงใน My Day ตามช่วงเวลาที่ถูกต้อง

+
Troubleshooting

ถ้าเพิ่ม Activity แล้วไม่ขึ้น ให้กด Refresh และตรวจว่า status ยังไม่ completed/cancelled

+
+
+
+
จำง่าย ๆ: My Day คือ “หน้าเริ่มงานประจำวัน” ใช้ดูว่าตอนนี้ควรทำอะไรต่อ ไม่ใช่หน้าสำหรับวางแผนตารางเวลาทั้งเดือน
+
+ +
+
+
+
Calendar Workspace
+

Calendar ใช้งานอย่างไร

+
+

Calendar คือมุมมองแบบเวลา ใช้ดู meeting, visit, task, reminder และ milestone จาก CRM โดยดึงข้อมูลจาก calendar projection ที่ผูกกับ Activity หรือ record ต้นทาง

+
+
+
+

1 เปิด Calendar

+
+
User Action

เข้าเมนู CRM > Calendar เพื่อดูตารางงานของตัวเองหรือทีมตามสิทธิ์

+
System Action

โหลด event ตาม date range, organization, lens, user/team scope และ permission visibility

+
Expected Result

เห็น event พร้อมสรุป Due Today, Overdue, Meetings, Visits และ Hot Projects

+
Troubleshooting

ถ้า calendar ว่าง ให้เช็คช่วงวันที่, owner/assignee, activity schedule และสิทธิ์การมองเห็นของ user

+
+
+ +
+

2 ค้นหาและเลือก event

+
+
User Action

ใช้ช่อง search เพื่อค้นหาชื่อ event, รายละเอียด, user หรือ source type แล้วคลิก event เพื่อดูรายละเอียด

+
System Action

กรองรายการบนหน้าจอ และแสดงข้อมูลเวลา สถานที่ ผู้รับผิดชอบ และ record ที่เกี่ยวข้อง

+
Expected Result

tester เข้าใจว่างานนี้เกี่ยวกับ customer, lead, opportunity, quotation หรือ approval ใด

+
Troubleshooting

ถ้า search ไม่เจอ ให้ลองล้าง keyword และตรวจว่างานอยู่ในช่วงวันที่ที่ calendar โหลดอยู่

+
+
+ +
+

3 ปรับเวลา event ที่แก้ได้

+
+
User Action

ลากหรือปรับเวลา event เฉพาะรายการที่เป็น editable activity และ user มีสิทธิ์แก้ไข

+
System Action

แปลงการลากเป็นคำสั่ง reschedule Activity แล้ว invalidate Calendar, My Day และ Activity cache

+
Expected Result

เวลาใหม่แสดงใน Calendar และ My Day หลังบันทึกสำเร็จ

+
Troubleshooting

ถ้าแก้ไม่ได้ อาจเป็น milestone/read-only event หรือ user ไม่มีสิทธิ์ update activity

+
+
+ +
+

4 สร้างงานใหม่จาก Calendar

+
+
User Action

เมื่อต้องเพิ่มนัดหมายใหม่ ให้ไปสร้างที่ Activity เพราะ Activity คือ source of truth ของงาน

+
System Action

หลัง Activity ถูกสร้าง ระบบสร้างหรืออัปเดต calendar projection เพื่อให้ event ปรากฏใน Calendar

+
Expected Result

event ใหม่แสดงใน Calendar ตามเวลา และแสดงใน My Day ถ้าอยู่ในช่วงที่เลือก

+
Troubleshooting

ถ้าสร้างแล้วไม่ขึ้น ให้ตรวจ start/end time, timezone, assignee และ refresh calendar

+
+
+
+
จำง่าย ๆ: Calendar คือ “แผนที่เวลา” ใช้ดูว่างานเกิดขึ้นเมื่อไร ส่วนการสร้าง/แก้รายละเอียดงานควรทำผ่าน Activity หรือ record ต้นทาง
+
+ +
+
+
+
How They Work Together
+

My Day, Calendar และ Activity ต่างกันอย่างไร

+
+

สามส่วนนี้ทำงานร่วมกัน แต่ตอบคำถามคนละแบบ: วันนี้ควรทำอะไร, งานอยู่เวลาไหน, และข้อมูลต้นทางอยู่ที่ใด

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ส่วนของระบบใช้ตอบคำถามUser Action หลักสิ่งที่ต้องจำ
My Dayวันนี้ฉันควรทำอะไรก่อนดู Priority Queue, Schedule, Overdue, Pending Approval แล้วเปิด record เพื่อทำงานต่อเหมาะกับเริ่มวันและ follow-up งานเร่งด่วน
Calendarงานนี้เกิดขึ้นวันและเวลาไหนดูตาราง, search event, เปิดรายละเอียด, reschedule event ที่แก้ได้เหมาะกับวางแผนเวลาและดูภาระงานรายวัน/สัปดาห์/เดือน
Activityรายละเอียดงานและ follow-up คืออะไรสร้างงาน, ใส่ assignee, ตั้งเวลา, อัปเดตสถานะ, บันทึกผลการติดตามเป็น source of truth ของงานที่ actionable
Lead / Opportunity / Quotationงานนี้เกี่ยวกับดีลหรือเอกสารใดเปิด record ต้นทางเพื่อดู customer, stage, amount, approval และเอกสารเป็น business context ไม่ใช่คิวงานประจำวันโดยตรง
+
+ +
+
+
+
UAT Scenarios
+

วิธีทดสอบ My Day และ Calendar แบบเข้าใจง่าย

+
+

ใช้ scenario ด้านล่างเพื่อยืนยันว่า workspace ทั้งสองหน้าเชื่อมกับ CRM flow จริง และ tester เห็นผลลัพธ์ที่คาดหวัง

+
+
+
1. Marketing Follow-upสร้าง Lead แล้วเพิ่ม Activity follow-up วันนี้ให้ sales user
+
2. Check My DayLogin เป็น sales user แล้วดูว่า Activity ปรากฏใน Priority Queue หรือ Schedule
+
3. Check Calendarเปิด Calendar ช่วงวันนี้ แล้วตรวจว่า event แสดงตามเวลาและ assignee
+
4. Rescheduleเลื่อนเวลา Activity ที่แก้ได้ แล้วตรวจว่า Calendar และ My Day อัปเดต
+
5. Approval WorkSubmit Quotation approval แล้วตรวจ pending approval ใน My Day ของผู้อนุมัติ
+
6. Manager ViewLogin manager แล้วตรวจทีมตาม scope ว่าเห็นงานที่ควรเห็นเท่านั้น
+
+
ถ้า My Day หรือ Calendar ไม่แสดงงาน ให้ไล่เช็คตามลำดับนี้: Activity มี assignee หรือไม่, วันที่อยู่ใน range หรือไม่, status ยัง active หรือไม่, user อยู่ organization เดียวกันหรือไม่, และ role/scope อนุญาตให้เห็น record นั้นหรือไม่
+
+ +
+
+
+
Activity Type Guide
+

Activity Type แต่ละแบบใช้เมื่อไร

+
+

Activity Type คือชนิดของงานที่ต้องทำ มีผลต่อการกรองงาน, badge, สถิติใน My Day, การแสดงใน Calendar และการตีความว่าผู้ใช้ต้อง action แบบไหน

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Activity Typeใช้เมื่อไรตัวอย่าง User Actionผลต่อระบบ
meetingมีนัดประชุมกับลูกค้า ทีมขาย หรือทีมภายในนัด review requirement, demo solution, kickoffนับเป็น Meeting และแสดงเป็น event ใน Calendar เมื่อมี scheduled date
visitออกไปพบลูกค้าหรือไปพื้นที่ลูกค้าMK/Sales ไปเยี่ยมลูกค้า A วันที่กำหนดนับเป็น Visit ใน My Day/Calendar และช่วยตามประวัติการเข้าพบ
phone_callโทรติดตามหรือโทรสอบถามข้อมูลโทรถามความคืบหน้า, โทรยืนยันวันนัดใช้แยกงานโทรออกจาก meeting/visit เพื่อดู workload ได้ชัด
emailส่งอีเมลเป็น action หลักส่ง catalog, ส่งเอกสาร, ส่งสรุปหลังประชุมช่วยให้ Timeline เห็นว่ามีการสื่อสารผ่านอีเมล
site_surveyต้องเข้าสำรวจหน้างานเพื่อเก็บข้อมูลเทคนิคนัดสำรวจพื้นที่ติดตั้ง crane, dock door, solarเหมาะกับงานก่อนเสนอราคา และช่วยทีมเห็น milestone สำรวจหน้างาน
presentationนำเสนอสินค้า โซลูชัน หรือ proposalDemo solution, present quotation, present project conceptทำให้แยกกิจกรรมเชิงขาย/นำเสนอออกจาก meeting ทั่วไป
reminderตั้งเตือนให้กลับมาทำบางอย่างในอนาคตเตือน follow-up ลูกค้าอีก 2 เดือน, เตือนขอข้อมูลเพิ่มขึ้นใน My Day/Calendar ตาม due/scheduled date แต่ไม่ใช่งานอนุมัติ
internal_taskงานภายในที่ไม่ใช่การติดต่อกับลูกค้าโดยตรงเตรียมเอกสาร, ตรวจข้อมูล, ประสานทีม serviceมักใช้กับ Primary Entity = internal หรือ record ที่เกี่ยวข้อง
follow_upติดตามต่อจาก activity/lead/opportunity/quotation เดิมติดตาม lead หลังลูกค้าขอคิดก่อน, ติดตาม quotation หลังส่งราคาเป็น type หลักสำหรับงานต่อเนื่อง และมักโผล่ใน Priority Queue
delivery_coordinationประสานงานส่งมอบ ติดตั้ง หรือส่งเอกสารหลังปิดงานนัดวันส่งของ, ประสาน schedule ติดตั้ง, แจ้งทีม operationช่วยแยกงาน post-sale/operation ออกจากงานขายปกติ
approval_actionงานที่เกี่ยวกับการอนุมัติหรือรอ action จากผู้อนุมัติตรวจ quotation ก่อน approve, request revision, follow approvalช่วยเชื่อมงานผู้อนุมัติกับ My Day แต่ approval workflow ยังเป็น source หลัก
email_follow_upalias เก่าสำหรับ follow-up ผ่านอีเมลติดตามลูกค้าด้วยอีเมลยังรองรับเพื่อ compatibility แต่ควรใช้ email หรือ follow_up ให้ชัดกว่า
reminder_actionalias เก่าสำหรับ action เตือนความจำงานเตือนที่ถูกสร้างจาก flow เก่ายังรองรับเพื่อ compatibility แต่ type หลักที่ควรเลือกใหม่คือ reminder
otherใช้เมื่อไม่มี type ใดตรงจริง ๆงานเฉพาะกิจที่ยังไม่เข้าหมวดควรใช้ให้น้อย เพราะทำให้ reporting และการกรองงานไม่คม
+
Reminder vs Reminder Action: reminder คือ Activity Type หลักสำหรับตั้งเตือนใน CRM ส่วน reminder_action เป็น alias เก่าที่ระบบยังรับได้เพื่อไม่ให้ข้อมูลเก่าพัง งานใหม่ควรเลือก reminder เป็นหลัก
+
+ +
+
+
+
Prospecting Case
+

ถ้ายังไม่รู้จักลูกค้าและยังไม่มี Customer ในระบบ บันทึกอย่างไร

+
+

กรณีเป็นการวิ่งหาลูกค้าใหม่ ยังไม่รู้บริษัทชัด ยังไม่มี contact และยังไม่มี lead ให้บันทึกเป็น Activity แบบ internal/prospecting ก่อน เพื่อเก็บงานของทีมโดยไม่สร้าง customer หรือ lead ปลอม

+
+
+
+

1 ก่อนออกไปหาลูกค้า

+
+
Primary Entity

เลือก internal

+
Primary Record Id

เว้นว่าง เพราะยังไม่มี customer/lead/opportunity จริงในระบบ

+
Activity Type

เลือก visit ถ้าออกพื้นที่, phone_call ถ้าโทรหา, หรือ internal_task ถ้าเป็นงานเตรียมรายชื่อ

+
Subject

เขียนให้ค้นเจอ เช่น Prospecting visit - Industrial estate A

+
+
+ +
+

2 หลังไปแล้วได้เพียงสัญญาณอนาคต

+
+
User Action

Complete activity เดิม แล้วใส่ Outcome Summary ว่าเจอข้อมูลอะไร แต่ยังไม่มี lead confirmed

+
System Action

เก็บเป็นประวัติการทำงานของ MK/Sales โดยยังไม่ผูกกับ customer record

+
Expected Result

ผู้จัดการเห็นว่ามี prospecting activity เกิดขึ้น แต่ pipeline/lead ยังไม่บวมจากข้อมูลที่ยังไม่ยืนยัน

+
Troubleshooting

ถ้าต้องตามต่อ ให้สร้าง Activity ใหม่ชนิด reminder หรือ follow_up ในวันที่เหมาะสม

+
+
+ +
+

3 เมื่อเริ่มรู้จักลูกค้าจริง

+
+
Customer

สร้าง customer/contact เมื่อรู้ชื่อบริษัทหรือผู้ติดต่อมากพอ

+
Lead

สร้าง lead เมื่อมี requirement หรือโอกาสงานที่ยืนยันได้

+
Linking

Activity ใหม่หลังจากนั้นควรผูกกับ customer หรือ lead แทน internal

+
Expected Result

Timeline ของ customer/lead เริ่มสะสมประวัติจากจุดที่มี record จริง

+
+
+ +
+

4 ตัวอย่างจากเคส MK

+
+
Visit

internal + visit, scheduled date เป็นวันออกไปพื้นที่, subject ระบุพื้นที่หรือกลุ่มเป้าหมาย

+
Outcome

บันทึกว่า “ยังไม่ได้ lead แต่ได้ยินว่าจะมีงานในอีก 2 เดือน”

+
Next Activity

สร้าง reminder หรือ follow_up อีก 2 เดือน เพื่อกลับไปตรวจสอบโอกาสงาน

+
Do Not

อย่าสร้าง Customer/Lead ปลอมถ้ายังไม่มีข้อมูลยืนยัน เพราะจะทำให้ UAT pipeline และ report เพี้ยน

+
+
+
+
+ +
+
+
+
Create Activity Fields
+

Status แต่ละแบบมีผลอย่างไร

+
+

Status คือสถานะการทำงานของ Activity มีผลต่อคิวงานใน My Day, การนับ overdue, การแก้ไขงานต่อ และ Timeline ของ record ที่เกี่ยวข้อง

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Statusความหมายควรใช้เมื่อไรผลต่อระบบ
draftร่างงานไว้ก่อน ยังไม่ถือว่าเริ่มทำจริงยังไม่พร้อมนัดหมาย หรือยังรอข้อมูลก่อน assign/scheduleยังเป็นงานเปิด แต่ไม่ควรใช้เป็นงานปฏิบัติประจำวันนานเกินไป
scheduledวางแผนแล้ว มีวันทำงานหรือวันครบกำหนดมีนัด visit, meeting, call, follow-up หรือ reminder ที่ต้องทำในอนาคตแสดงใน Calendar/My Day ตาม Scheduled Date หรือ Due Date และอาจกลายเป็น overdue เมื่อเลยเวลา
in_progressกำลังทำงานนี้อยู่เริ่มติดต่อ เริ่มติดตาม หรือกำลังประสานงานแต่ยังไม่เสร็จยังเป็น active work และถ้าเลย Due Date จะถูกมองเป็น overdue
completedทำงานเสร็จแล้วโทรแล้ว, ไปพบแล้ว, ส่งอีเมลแล้ว, follow-up แล้ว และบันทึกผลลัพธ์แล้วปิดงาน ไม่ถูกดันเป็น overdue อีก และควรมี Outcome Summary / Next Action ถ้าต้องตามต่อ
cancelledยกเลิกงานนัดถูกยกเลิก ลูกค้าไม่ต้องการแล้ว หรือสร้างผิดปิดงาน ไม่ขึ้นเป็น overdue และควรใส่ Cancellation Reason
skippedข้ามงานนี้ไปงานไม่จำเป็นแล้ว แต่ไม่ใช่การยกเลิกแบบ error เช่น ลูกค้าตอบผ่านช่องทางอื่นแล้วปิดงาน ไม่ขึ้นเป็น overdue และควรใส่ Skip Reason เพื่ออธิบายเหตุผล
overdueเลยกำหนดแล้วโดยปกติไม่ต้องเลือกเอง ระบบคำนวณจากวันที่เป็น effective status: ถ้างานยังไม่ completed/cancelled/skipped และเลย Due Date หรือ Scheduled Date ระบบจะแสดงเป็น overdue
+
Transition สำคัญ: draft ไป scheduled หรือ cancelled; scheduled ไป in_progress, completed, cancelled, skipped; งานที่ completed, cancelled, skipped แล้วถือว่าปิดและไม่ควรย้อนกลับ
+
+ +
+
+
+
Schedule & Due Logic
+

Scheduled Date และ Due Date มีผลอย่างไร

+
+

สองช่องนี้คล้ายกันแต่ตอบคนละคำถาม: Scheduled Date คือ “จะทำเมื่อไร” ส่วน Due Date คือ “ต้องเสร็จไม่เกินเมื่อไร”

+
+
+
+

1 Scheduled Date

+
+
ความหมาย

วันที่วางแผนให้ activity เกิดขึ้น เช่น วันนัดประชุม วันไปพบลูกค้า วันโทร

+
ใช้เมื่อไร

ใช้กับงานที่มีเวลาทำชัดเจน เช่น meeting, visit, site_survey, phone_call

+
ผลต่อ Calendar

ทำให้งานแสดงเป็น event ใน Calendar และถ้าลาก event ใน Calendar ระบบจะ reschedule Activity

+
ผลต่อ My Day

งานจะเข้า Schedule/คิวประจำวันเมื่ออยู่ในช่วง today, tomorrow หรือ this week

+
+
+ +
+

2 Due Date

+
+
ความหมาย

วันครบกำหนดที่งานควรเสร็จ ไม่จำเป็นต้องเป็นวันเดียวกับวันที่นัดทำงาน

+
ใช้เมื่อไร

ใช้กับงานที่มี deadline เช่น ส่งเอกสารภายในวันศุกร์, follow-up ไม่เกินสิ้นเดือน

+
ผลต่อ Overdue

ระบบใช้ Due Date เป็นตัวเทียบก่อน ถ้าเลยกำหนดและงานยังไม่ปิด จะแสดงเป็น overdue

+
ผลต่อ Priority

งานที่ due ใกล้หรือ overdue มีโอกาสถูกดันให้เห็นชัดใน My Day / Priority Queue

+
+
+ +
+

3 ถ้าใส่ทั้งสองช่อง

+
+
ตัวอย่าง

นัด visit วันที่ 2026-07-17 แต่ต้องส่งสรุปไม่เกิน 2026-07-18

+
Scheduled Date

ใส่วันที่งานเกิดขึ้นจริง: 2026-07-17

+
Due Date

ใส่ deadline: 2026-07-18

+
Expected Result

Calendar ช่วยดูวันนัด ส่วน My Day/Overdue ช่วยเตือน deadline

+
+
+ +
+

4 ถ้าใส่แค่ช่องเดียว

+
+
มีแต่ Scheduled Date

เหมาะกับนัดหมายที่เกิดวันนั้นและไม่มี deadline แยก ระบบจะใช้ scheduled date ช่วยคำนวณ overdue ถ้างานไม่ปิด

+
มีแต่ Due Date

เหมาะกับ task/reminder ที่ต้องเสร็จภายในวันหนึ่ง แต่ไม่ได้มีเวลานัดชัดเจน

+
ไม่มีทั้งสอง

งานจะค้นเจอใน Activity แต่ My Day/Calendar จะช่วยเตือนได้จำกัด

+
Best Practice

ถ้าต้องการให้งานขึ้น Calendar ให้ใส่ Scheduled Date; ถ้าต้องการกันลืม deadline ให้ใส่ Due Date

+
+
+
+
หลักจำง่าย: Scheduled Date = วัน/เวลาที่จะทำงาน, Due Date = วันสุดท้ายที่งานต้องเสร็จ. ถ้า Due Date มีค่า ระบบจะใช้ Due Date เป็นหลักในการตัดสิน overdue; ถ้าไม่มี Due Date จึงใช้ Scheduled Date แทน
+
+ + +
+ + diff --git a/setup/ALLA-ONVALLA/approval-steps.csv b/setup/ALLA-ONVALLA/approval-steps.csv index dc633e7..80b6dd8 100644 --- a/setup/ALLA-ONVALLA/approval-steps.csv +++ b/setup/ALLA-ONVALLA/approval-steps.csv @@ -1,13 +1,13 @@ organization_code,workflow_code,step_number,role_code,role_name,approval_mode,is_required,sla_hours,calendar_mode,timeout_action -ALLA,quotation_standard,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate -ALLA,quotation_manager_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate -ALLA,quotation_manager_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,auto_escalate -ALLA,quotation_executive_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate -ALLA,quotation_executive_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,auto_escalate +ALLA,quotation_standard,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate +ALLA,quotation_manager_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate +ALLA,quotation_manager_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,escalate +ALLA,quotation_executive_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate +ALLA,quotation_executive_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,escalate ALLA,quotation_executive_review,3,top_manager,Top Manager,sequential,true,72,calendar_days,none -ONVALLA,quotation_standard,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate -ONVALLA,quotation_manager_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate -ONVALLA,quotation_manager_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,auto_escalate -ONVALLA,quotation_executive_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate -ONVALLA,quotation_executive_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,auto_escalate +ONVALLA,quotation_standard,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate +ONVALLA,quotation_manager_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate +ONVALLA,quotation_manager_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,escalate +ONVALLA,quotation_executive_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate +ONVALLA,quotation_executive_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,escalate ONVALLA,quotation_executive_review,3,top_manager,Top Manager,sequential,true,72,calendar_days,none diff --git a/setup/ALLA-ONVALLA/document-sequences.csv b/setup/ALLA-ONVALLA/document-sequences.csv index c72de7e..5d4c278 100644 --- a/setup/ALLA-ONVALLA/document-sequences.csv +++ b/setup/ALLA-ONVALLA/document-sequences.csv @@ -1,6 +1,13 @@ organization_code,branch_code,product_type_code,document_type,prefix,period,current_number,padding_length,format,reset_policy,is_active -ALLA,head_office,crane,customer,CUS,yearly,0,4,{prefix}-{yyyy}-{running},period,true -ALLA,head_office,crane,lead,LD,yearly,0,4,{prefix}-{yyyy}-{running},period,true -ALLA,head_office,crane,opportunity,OP,yearly,0,4,{prefix}-{yyyy}-{running},period,true -ALLA,head_office,crane,quotation,QT-AL-CR,monthly,0,4,{prefix}-{yy}{MM}-{running},period,true -ONVALLA,head_office,crane,quotation,QT-ON-CR,monthly,0,4,{prefix}-{yy}{MM}-{running},period,true +ALLA,head_office,,customer,CUS,2607,0,3,{prefix}{period}-{running},period,true +ALLA,head_office,,crm_lead,LD,2607,0,3,{prefix}{period}-{running},period,true +ALLA,head_office,,opportunity,OP,2607,0,3,{prefix}{period}-{running},period,true +ALLA,head_office,,crm_opportunity,EN,2607,0,3,{prefix}{period}-{running},period,true +ALLA,head_office,crane,quotation,QT-AL-CR,2607,0,4,{prefix}{period}-{running},period,true +ALLA,head_office,dockdoor,quotation,QT-AL-DK,2607,0,4,{prefix}{period}-{running},period,true +ONVALLA,head_office,,customer,CUS,2607,0,3,{prefix}{period}-{running},period,true +ONVALLA,head_office,,crm_lead,LD,2607,0,3,{prefix}{period}-{running},period,true +ONVALLA,head_office,,opportunity,OP,2607,0,3,{prefix}{period}-{running},period,true +ONVALLA,head_office,,crm_opportunity,EN,2607,0,3,{prefix}{period}-{running},period,true +ONVALLA,head_office,crane,quotation,QT-ON-CR,2607,0,4,{prefix}{period}-{running},period,true +ONVALLA,head_office,solarcell,quotation,QT-ON-SC,2607,0,4,{prefix}{period}-{running},period,true diff --git a/setup/ALLA-ONVALLA/master-options.csv b/setup/ALLA-ONVALLA/master-options.csv index b6b8a74..b405f42 100644 --- a/setup/ALLA-ONVALLA/master-options.csv +++ b/setup/ALLA-ONVALLA/master-options.csv @@ -1,4 +1,16 @@ organization_code,category,code,label,value,parent_category,parent_code,sort_order,is_active,metadata_json +ALLA,crm_currency,thb,Thai Baht,THB,,,1,true,{} +ONVALLA,crm_currency,thb,Thai Baht,THB,,,1,true,{} +ALLA,crm_lead_status,new_job,New Job,new_job,,,1,true,{} +ALLA,crm_lead_status,follow_up,Follow Up,follow_up,,,2,true,{} +ALLA,crm_lead_status,assigned,Assigned,assigned,,,3,true,{} +ALLA,crm_lead_status,closed_lost,Closed Lost,closed_lost,,,4,true,{} +ALLA,crm_lead_status,cancel,Cancel,cancel,,,5,true,{} +ONVALLA,crm_lead_status,new_job,New Job,new_job,,,1,true,{} +ONVALLA,crm_lead_status,follow_up,Follow Up,follow_up,,,2,true,{} +ONVALLA,crm_lead_status,assigned,Assigned,assigned,,,3,true,{} +ONVALLA,crm_lead_status,closed_lost,Closed Lost,closed_lost,,,4,true,{} +ONVALLA,crm_lead_status,cancel,Cancel,cancel,,,5,true,{} ALLA,crm_customer_group,construction,Construction,,,,1,true,{} ALLA,crm_customer_group,industrial,Industrial,,,,2,true,{} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 35a2701..3f8795a 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,25 +1,29 @@ -import KBar from '@/components/kbar'; -import AppSidebar from '@/components/layout/app-sidebar'; -import Header from '@/components/layout/header'; -import { InfoSidebar } from '@/components/layout/info-sidebar'; -import { InfobarProvider } from '@/components/ui/infobar'; -import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; -import type { Metadata } from 'next'; -import { cookies } from 'next/headers'; +import KBar from "@/components/kbar"; +import AppSidebar from "@/components/layout/app-sidebar"; +import Header from "@/components/layout/header"; +import { InfoSidebar } from "@/components/layout/info-sidebar"; +import { InfobarProvider } from "@/components/ui/infobar"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import type { Metadata } from "next"; +import { cookies } from "next/headers"; export const metadata: Metadata = { - title: 'Next Shadcn Dashboard Starter', - description: 'Basic dashboard with Next.js and Shadcn', + title: "ALLA OS", + description: "ALLA OS", robots: { index: false, - follow: false - } + follow: false, + }, }; -export default async function DashboardLayout({ children }: { children: React.ReactNode }) { +export default async function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { // Persisting the sidebar state in the cookie. const cookieStore = await cookies(); - const defaultOpen = cookieStore.get('sidebar_state')?.value === 'true'; + const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"; return ( @@ -28,7 +32,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
{children} - + diff --git a/src/features/crm/activities/server/service.ts b/src/features/crm/activities/server/service.ts index cc91569..2d94a2a 100644 --- a/src/features/crm/activities/server/service.ts +++ b/src/features/crm/activities/server/service.ts @@ -4,6 +4,7 @@ import { db } from '@/lib/db'; import { AuthError } from '@/lib/auth/session'; import { generateNextDocumentCode } from '@/features/foundation/document-sequence/service'; import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service'; +import { drainProjectionOutboxBestEffort } from '@/features/foundation/projections/server/best-effort-drain'; import { CRM_ACTIVITY_ENTITY_TYPES, CRM_ACTIVITY_PERMISSIONS, @@ -677,6 +678,7 @@ export async function createActivity( return createdRow; }); + await drainProjectionOutboxBestEffort(); const activity = await getActivityById(created.id, organizationId, context); return activity; @@ -810,6 +812,7 @@ correlationId return nextActivity; }); +await drainProjectionOutboxBestEffort(); const activity = await getActivityById(activityEventRecord.id, organizationId, context); return activity; @@ -846,6 +849,7 @@ actorUserId: userId return nextActivity; }); +await drainProjectionOutboxBestEffort(); const activity = await getActivityById(completedEventRecord.id, organizationId, context); return activity; } @@ -880,6 +884,7 @@ actorUserId: userId return nextActivity; }); +await drainProjectionOutboxBestEffort(); const activity = await getActivityById(cancelledEventRecord.id, organizationId, context); return activity; } @@ -917,7 +922,8 @@ actorUserId: userId return nextActivity; }); -const activity = await getActivityById(assignedEventRecord.id, organizationId, context); + await drainProjectionOutboxBestEffort(); + const activity = await getActivityById(assignedEventRecord.id, organizationId, context); return activity; } @@ -964,6 +970,7 @@ actorUserId: userId return nextActivity; }); +await drainProjectionOutboxBestEffort(); const activity = await getActivityById(rescheduledEventRecord.id, organizationId, context); return activity; } @@ -991,4 +998,6 @@ activity: current, actorUserId: userId }); }); + +await drainProjectionOutboxBestEffort(); } diff --git a/src/features/foundation/projections/consumers.ts b/src/features/foundation/projections/consumers.ts index 7563c0f..f80a3cb 100644 --- a/src/features/foundation/projections/consumers.ts +++ b/src/features/foundation/projections/consumers.ts @@ -1,4 +1,5 @@ import type { BusinessEvent } from '../business-events/types.ts'; +import { calendarProjectionConsumer as crmCalendarProjectionConsumer } from '../../crm/calendar/server/consumer.ts'; import { getProjectionDefinition } from './registry.ts'; import type { ProjectionConsumer, ProjectionHandleResult } from './types.ts'; @@ -21,7 +22,7 @@ function createNoopConsumer(projectionName: string): ProjectionConsumer { } export const timelineProjectionConsumer = createNoopConsumer('Timeline'); -export const calendarProjectionConsumer = createNoopConsumer('Calendar'); +export const calendarProjectionConsumer = crmCalendarProjectionConsumer; export const dashboardProjectionConsumer = createNoopConsumer('Dashboard'); export const notificationProjectionConsumer = createNoopConsumer('Notification'); diff --git a/src/features/foundation/projections/server/best-effort-drain.ts b/src/features/foundation/projections/server/best-effort-drain.ts new file mode 100644 index 0000000..65566c1 --- /dev/null +++ b/src/features/foundation/projections/server/best-effort-drain.ts @@ -0,0 +1,23 @@ +import { INITIAL_PROJECTION_CONSUMERS } from '../consumers.ts'; +import { ProjectionOutboxWorker } from '../worker.ts'; +import { DrizzleProjectionRuntimeStore } from './store.ts'; + +export async function drainProjectionOutboxBestEffort(): Promise { + try { + const worker = new ProjectionOutboxWorker({ + store: new DrizzleProjectionRuntimeStore(), + consumers: INITIAL_PROJECTION_CONSUMERS, + config: { + enabled: true, + batchSize: 25, + maxConcurrentEvents: 1, + maxConcurrentConsumers: 4, + workerId: `inline-projection-${crypto.randomUUID().slice(0, 8)}` + } + }); + + await worker.drainOnce(); + } catch (error) { + console.error('[projections] best-effort inline drain failed', error); + } +} diff --git a/src/features/foundation/projections/server/store.ts b/src/features/foundation/projections/server/store.ts index 380e112..4466268 100644 --- a/src/features/foundation/projections/server/store.ts +++ b/src/features/foundation/projections/server/store.ts @@ -1,4 +1,4 @@ -import { and, eq, sql } from 'drizzle-orm'; +import { and, asc, eq, inArray, lte, or } from 'drizzle-orm'; import { businessEventOutbox, projectionConsumerCheckpoints, @@ -51,37 +51,47 @@ export class DrizzleProjectionRuntimeStore implements ProjectionRuntimeStore { async claimAvailableEvents( input: ClaimBusinessEventDeliveriesInput ): Promise { - const result = await db.execute(sql` - WITH claimable AS ( - SELECT id - FROM business_event_outbox - WHERE - ( - delivery_status IN ('pending', 'retry_scheduled') - AND available_at <= ${input.now} + const claimable = await db + .select({ id: businessEventOutbox.id }) + .from(businessEventOutbox) + .where( + or( + and( + inArray(businessEventOutbox.deliveryStatus, ['pending', 'retry_scheduled']), + lte(businessEventOutbox.availableAt, input.now) + ), + and( + inArray(businessEventOutbox.deliveryStatus, ['claimed', 'processing']), + lte(businessEventOutbox.claimExpiresAt, input.now) ) - OR ( - delivery_status IN ('claimed', 'processing') - AND claim_expires_at IS NOT NULL - AND claim_expires_at <= ${input.now} - ) - ORDER BY available_at ASC, created_at ASC, id ASC - LIMIT ${input.batchSize} - FOR UPDATE SKIP LOCKED + ) ) - UPDATE business_event_outbox - SET - delivery_status = 'claimed', - claimed_by = ${input.workerId}, - claimed_at = ${input.now}, - claim_expires_at = ${input.leaseExpiresAt}, - updated_at = ${input.now} - FROM claimable - WHERE business_event_outbox.id = claimable.id - RETURNING business_event_outbox.* - `); + .orderBy( + asc(businessEventOutbox.availableAt), + asc(businessEventOutbox.createdAt), + asc(businessEventOutbox.id) + ) + .limit(input.batchSize); + + if (claimable.length === 0) return []; + + const rows = await db + .update(businessEventOutbox) + .set({ + deliveryStatus: 'claimed', + claimedBy: input.workerId, + claimedAt: input.now, + claimExpiresAt: input.leaseExpiresAt, + updatedAt: input.now + }) + .where( + inArray( + businessEventOutbox.id, + claimable.map((item) => item.id) + ) + ) + .returning(); - const rows = result as unknown as Array; return rows.map((row) => mapDelivery(row)); } @@ -256,27 +266,49 @@ export class DrizzleProjectionRuntimeStore implements ProjectionRuntimeStore { }); } - async recordHealthSnapshot(input: ProjectionHealthSnapshotInput): Promise { - await db.insert(projectionHealthSnapshots).values({ - id: crypto.randomUUID(), - organizationId: input.organizationId, - projectionName: input.projectionName, - consumerName: input.consumerName, - status: input.status, - lastProcessedEventId: input.lastProcessedEventId ?? null, - lastSuccessfulProcessingAt: input.lastSuccessfulProcessingAt - ? new Date(input.lastSuccessfulProcessingAt) - : null, - pendingCount: input.pendingCount ?? 0, - retryCount: input.retryCount ?? 0, - deadLetterCount: input.deadLetterCount ?? 0, - unsupportedVersionCount: input.unsupportedVersionCount ?? 0, - processingLagMs: input.processingLagMs ?? 0, - averageProcessingDurationMs: input.averageProcessingDurationMs ?? 0, - lastRebuildAt: input.lastRebuildAt ? new Date(input.lastRebuildAt) : null, - rebuildSource: input.rebuildSource ?? null - }); - } +async recordHealthSnapshot(input: ProjectionHealthSnapshotInput): Promise { +const values = { +id: crypto.randomUUID(), +organizationId: input.organizationId, +projectionName: input.projectionName, +consumerName: input.consumerName, +status: input.status, +lastProcessedEventId: input.lastProcessedEventId ?? null, +lastSuccessfulProcessingAt: input.lastSuccessfulProcessingAt +? new Date(input.lastSuccessfulProcessingAt) +: null, +pendingCount: input.pendingCount ?? 0, +retryCount: input.retryCount ?? 0, +deadLetterCount: input.deadLetterCount ?? 0, +unsupportedVersionCount: input.unsupportedVersionCount ?? 0, +processingLagMs: input.processingLagMs ?? 0, +averageProcessingDurationMs: input.averageProcessingDurationMs ?? 0, +lastRebuildAt: input.lastRebuildAt ? new Date(input.lastRebuildAt) : null, +rebuildSource: input.rebuildSource ?? null +}; + +await db +.insert(projectionHealthSnapshots) +.values(values) +.onConflictDoUpdate({ +target: [projectionHealthSnapshots.organizationId, projectionHealthSnapshots.consumerName], +set: { +projectionName: values.projectionName, +status: values.status, +lastProcessedEventId: values.lastProcessedEventId, +lastSuccessfulProcessingAt: values.lastSuccessfulProcessingAt, +pendingCount: values.pendingCount, +retryCount: values.retryCount, +deadLetterCount: values.deadLetterCount, +unsupportedVersionCount: values.unsupportedVersionCount, +processingLagMs: values.processingLagMs, +averageProcessingDurationMs: values.averageProcessingDurationMs, +lastRebuildAt: values.lastRebuildAt, +rebuildSource: values.rebuildSource, +updatedAt: new Date() +} +}); +} private async getDelivery(eventId: string): Promise { const [row] = await db