S3 — Activity Report & Course Registration (PRC) — ER Diagram (Target Design)¶
TARGET DESIGN
ความสัมพันธ์ตาม schema.sql (FK ทุกเส้นถูกประกาศจริงใน DDL — solid line ทุกเส้น) · เป็น greenfield design modeled จาก demo ar_and_crs/fe/src/data.jsx — ยังไม่ลงฐานข้อมูลจริง · ดูพจนานุกรมข้อมูลที่ ./data_dict.md และภาพรวม design ที่ ../index.md
ทุกตาราง business มี audit columns CreatedBy/UpdatedBy (int FK → AppUser) — ละไว้ในไดอะแกรมเพื่อความชัด วาดเฉพาะ FK เชิงความหมาย (ownership / actor) เช่น Registration.ReviewedBy, KnowledgeReport.SubmittedBy, Trainer.AppUserId · audit/soft-delete (IsDeleted) ละไว้ทั้งหมด · รายการ FK ครบถ้วน (รวม CreatedBy/UpdatedBy) อยู่ที่ตารางสรุปท้ายเอกสาร
ไดอะแกรมแยกเป็นหลายบล็อกตาม domain เพื่อให้อ่านง่าย — ตารางที่เป็นสะพานข้าม domain (เช่น Course, Batch, Member, Registration) จะปรากฏมากกว่าหนึ่งบล็อก
Auth & Identity (thin SSO mirror)¶
erDiagram
OrgUnit ||--o{ OrgUnit : "ParentOrgUnitId"
OrgUnit ||--o{ AppUser : "OrgUnitId"
Trainer ||--o{ AppUser : "TrainerId"
Member ||--o{ AppUser : "MemberId"
AppUser ||--o{ RoleAssignment : "AppUserId"
Role ||--o{ RoleAssignment : "RoleId"
AppUser ||--o{ RoleAssignment : "GrantedBy"
AppUser เป็น mirror จาก SSO กลาง (ไม่มีคอลัมน์ password) · สิทธิ์จริงมาจาก JWT — Role/RoleAssignment เป็น catalog/mirror ภายใน · AppUser อ้างกลับไป Trainer/Member (nullable) เมื่อ user เป็นวิทยากร/ศิษย์เก่า — เกิด cycle เชิงความหมายกับ Trainer.AppUserId ซึ่งจัดการได้เพราะทุกเส้นเป็น nullable
Course / Project / Trainer¶
erDiagram
Province ||--o{ Course : "ProvinceId"
Trainer ||--o{ Course : "TrainerId"
Project ||--o{ ProjectCourse : "ProjectId"
Course ||--o{ ProjectCourse : "CourseId"
Course ||--o{ CoursePrerequisite : "CourseId"
Course ||--o{ CoursePrerequisite : "PrerequisiteCourseId"
Project ||--o{ BudgetCalculation : "ProjectId"
OrgUnit ||--o{ Trainer : "OrgUnitId"
AppUser ||--o{ Trainer : "AppUserId"
Trainer ||--o{ TrainerSpecialty : "TrainerId"
Trainer ||--o{ TrainerEditRequest : "TrainerId"
AppUser ||--o{ TrainerEditRequest : "ReviewedBy"
ProjectCourse คือ junction N:M ระหว่าง Project กับ Course · CoursePrerequisite คือ self-reference N:M บน Course (หลักสูตร→หลักสูตรที่ต้องผ่านก่อน, REQ-008) · BudgetCalculation เป็น snapshot ค่าใช้จ่ายต่อหัวของ Project · TrainerEditRequest คือ workflow ร่าง → ADMIN อนุมัติ (ReviewedBy = ผู้พิจารณา)
Member / Enrollment / Registration¶
erDiagram
MemberType ||--o{ Member : "MemberTypeId"
OrgUnit ||--o{ Member : "OrgUnitId"
Province ||--o{ Member : "ProvinceId"
Member ||--o{ Enrollment : "MemberId"
Course ||--o{ Enrollment : "CourseId"
Batch ||--o{ Enrollment : "BatchId"
Course ||--o{ Registration : "CourseId"
MemberType ||--o{ Registration : "MemberTypeId"
Member ||--o{ Registration : "MemberId"
AppUser ||--o{ Registration : "ReviewedBy"
Enrollment คือ junction Member × Course (+Batch รุ่น) = ศิษย์เก่า/สมาชิกในหลักสูตร · Registration คือคำขอลงทะเบียน + คิวอนุมัติ — ReviewedBy = OFFICER_KSS ผู้พิจารณา, MemberId ผูกภายหลังอนุมัติ
Batch / Certificate¶
erDiagram
Course ||--o{ Batch : "CourseId"
Course ||--o{ Certificate : "CourseId"
Member ||--o{ Certificate : "MemberId"
Batch ||--o{ Certificate : "BatchId"
Certificate ||--o{ CertificatePrintLog : "CertificateId"
AppUser ||--o{ CertificatePrintLog : "PrintedBy"
Batch = รุ่นของหลักสูตร · Certificate (e-cert) ออกให้ Member ต่อ Course/Batch · CertificatePrintLog เป็น append-only log บันทึกการพิมพ์ทุกครั้ง (ORIGINAL/COPY) — PrintedBy = ผู้พิมพ์
Equipment¶
erDiagram
AppUser ||--o{ Equipment : "ResponsibleUserId"
Equipment ||--o{ EquipmentOwnershipHistory : "EquipmentId"
AppUser ||--o{ EquipmentOwnershipHistory : "FromUserId"
AppUser ||--o{ EquipmentOwnershipHistory : "ToUserId"
AppUser ||--o{ EquipmentOwnershipHistory : "ChangedBy"
Equipment = ครุภัณฑ์ (ผู้รับผิดชอบ ResponsibleUserId map ไป AppUser ได้, มี free-text fallback ResponsibleName) · EquipmentOwnershipHistory append-only บันทึกการเปลี่ยนผู้รับผิดชอบ (FromUserId → ToUserId, ChangedBy = ผู้บันทึก)
Knowledge / Notification / Audit¶
erDiagram
KnowledgeType ||--o{ KnowledgeReport : "KnowledgeTypeId"
OrgUnit ||--o{ KnowledgeReport : "OrgUnitId"
Province ||--o{ KnowledgeReport : "ProvinceId"
AppUser ||--o{ KnowledgeReport : "SubmittedBy"
AppUser ||--o{ KnowledgeReport : "FlaggedBy"
Registration ||--o{ EmailNotification : "RegistrationId"
EmailNotification ||--o{ EmailAttempt : "EmailNotificationId"
AppUser ||--o{ AuditLog : "ActorUserId"
KnowledgeReport คือรายงานการเผยแพร่ความรู้ — SubmittedBy = OFFICER_OTHER (data-scoping sees-own), FlaggedBy = ADMIN ผู้ flag · EmailNotification 1 ต่อ Registration (แจ้งผลลงทะเบียน), EmailAttempt append-only ทุกครั้งที่พยายามส่ง (รองรับ retry/backoff) · AuditLog append-only, denormalized — ActorUserId nullable (NULL = system)
Attachment (polymorphic — ไม่มี FK ไปยังเจ้าของ)¶
PRC.Attachment เป็นตารางแนบไฟล์แบบ polymorphic ตัวเดียว ใช้ร่วมหลาย entity ผ่านคู่คอลัมน์ OwnerEntityType + OwnerEntityId (OwnerEntityType ∈ COURSE/PROJECT/TRAINER/REGISTRATION/KNOWLEDGE_REPORT/MEMBER) · ไม่มี FK ระดับ DB ไปยังตารางเจ้าของ (เป็นความสัมพันธ์เชิงตรรกะ resolve ที่ชั้นแอป) จึงไม่วาดเส้นในไดอะแกรม · FK เดียวที่มีจริงคือ Attachment.UploadedBy → AppUser (ผู้อัปโหลด)
Foreign Keys (สรุป)¶
ตารางครบทุก FK ที่ประกาศใน schema.sql Section 2 (รวม audit CreatedBy/UpdatedBy) — schema PRC ทุกตาราง
| Child Table | Column | Parent Table | Parent Column |
|---|---|---|---|
| OrgUnit | ParentOrgUnitId | OrgUnit | OrgUnitId |
| AppUser | OrgUnitId | OrgUnit | OrgUnitId |
| AppUser | TrainerId | Trainer | TrainerId |
| AppUser | MemberId | Member | MemberId |
| AppUser | CreatedBy | AppUser | AppUserId |
| AppUser | UpdatedBy | AppUser | AppUserId |
| RoleAssignment | AppUserId | AppUser | AppUserId |
| RoleAssignment | RoleId | Role | RoleId |
| RoleAssignment | GrantedBy | AppUser | AppUserId |
| Course | ProvinceId | Province | ProvinceId |
| Course | TrainerId | Trainer | TrainerId |
| Course | CreatedBy | AppUser | AppUserId |
| Course | UpdatedBy | AppUser | AppUserId |
| ProjectCourse | ProjectId | Project | ProjectId |
| ProjectCourse | CourseId | Course | CourseId |
| ProjectCourse | CreatedBy | AppUser | AppUserId |
| CoursePrerequisite | CourseId | Course | CourseId |
| CoursePrerequisite | PrerequisiteCourseId | Course | CourseId |
| CoursePrerequisite | CreatedBy | AppUser | AppUserId |
| Project | OwnerUserId | AppUser | AppUserId |
| Project | CreatedBy | AppUser | AppUserId |
| Project | UpdatedBy | AppUser | AppUserId |
| BudgetCalculation | ProjectId | Project | ProjectId |
| BudgetCalculation | ConfirmedBy | AppUser | AppUserId |
| BudgetCalculation | CreatedBy | AppUser | AppUserId |
| BudgetCalculation | UpdatedBy | AppUser | AppUserId |
| Trainer | OrgUnitId | OrgUnit | OrgUnitId |
| Trainer | AppUserId | AppUser | AppUserId |
| Trainer | CreatedBy | AppUser | AppUserId |
| Trainer | UpdatedBy | AppUser | AppUserId |
| TrainerSpecialty | TrainerId | Trainer | TrainerId |
| TrainerEditRequest | TrainerId | Trainer | TrainerId |
| TrainerEditRequest | RequestedBy | AppUser | AppUserId |
| TrainerEditRequest | ReviewedBy | AppUser | AppUserId |
| TrainerEditRequest | CreatedBy | AppUser | AppUserId |
| TrainerEditRequest | UpdatedBy | AppUser | AppUserId |
| Member | MemberTypeId | MemberType | MemberTypeId |
| Member | OrgUnitId | OrgUnit | OrgUnitId |
| Member | ProvinceId | Province | ProvinceId |
| Member | CreatedBy | AppUser | AppUserId |
| Member | UpdatedBy | AppUser | AppUserId |
| Enrollment | MemberId | Member | MemberId |
| Enrollment | CourseId | Course | CourseId |
| Enrollment | BatchId | Batch | BatchId |
| Enrollment | CreatedBy | AppUser | AppUserId |
| Enrollment | UpdatedBy | AppUser | AppUserId |
| Registration | CourseId | Course | CourseId |
| Registration | MemberTypeId | MemberType | MemberTypeId |
| Registration | MemberId | Member | MemberId |
| Registration | ReviewedBy | AppUser | AppUserId |
| Registration | CreatedBy | AppUser | AppUserId |
| Registration | UpdatedBy | AppUser | AppUserId |
| Batch | CourseId | Course | CourseId |
| Batch | CreatedBy | AppUser | AppUserId |
| Batch | UpdatedBy | AppUser | AppUserId |
| Certificate | CourseId | Course | CourseId |
| Certificate | MemberId | Member | MemberId |
| Certificate | BatchId | Batch | BatchId |
| Certificate | CreatedBy | AppUser | AppUserId |
| Certificate | UpdatedBy | AppUser | AppUserId |
| CertificatePrintLog | CertificateId | Certificate | CertificateId |
| CertificatePrintLog | PrintedBy | AppUser | AppUserId |
| Equipment | ResponsibleUserId | AppUser | AppUserId |
| Equipment | CreatedBy | AppUser | AppUserId |
| Equipment | UpdatedBy | AppUser | AppUserId |
| EquipmentOwnershipHistory | EquipmentId | Equipment | EquipmentId |
| EquipmentOwnershipHistory | FromUserId | AppUser | AppUserId |
| EquipmentOwnershipHistory | ToUserId | AppUser | AppUserId |
| EquipmentOwnershipHistory | ChangedBy | AppUser | AppUserId |
| KnowledgeReport | KnowledgeTypeId | KnowledgeType | KnowledgeTypeId |
| KnowledgeReport | OrgUnitId | OrgUnit | OrgUnitId |
| KnowledgeReport | ProvinceId | Province | ProvinceId |
| KnowledgeReport | SubmittedBy | AppUser | AppUserId |
| KnowledgeReport | FlaggedBy | AppUser | AppUserId |
| KnowledgeReport | CreatedBy | AppUser | AppUserId |
| KnowledgeReport | UpdatedBy | AppUser | AppUserId |
| EmailNotification | RegistrationId | Registration | RegistrationId |
| EmailNotification | CreatedBy | AppUser | AppUserId |
| EmailNotification | UpdatedBy | AppUser | AppUserId |
| EmailAttempt | EmailNotificationId | EmailNotification | EmailNotificationId |
| AuditLog | ActorUserId | AppUser | AppUserId |
| Attachment | UploadedBy | AppUser | AppUserId |