5. โครงสร้างข้อมูล#
ฐานข้อมูล: PostgreSQL 17 (เปิดส่วนขยาย pgvector ไว้สำหรับคลังความรู้ใน Phase 3)
ทุกตารางใช้ uuid เป็น primary key และมี created_at / updated_at เสมอ
5.1 แผนภาพความสัมพันธ์#
erDiagram
PERSON ||--o{ COST_RATE : "มีประวัติ"
PERSON ||--o{ TIME_ENTRY : "ลงเวลา"
PERSON ||--o{ SHIFT : "เข้ากะ"
PERSON ||--o{ OT_REQUEST : "ขอ"
PERSON ||--o{ LEAVE_REQUEST : "ขอ"
PERSON ||--o{ PERSON_ROLE : "มีบทบาท"
CLIENT ||--o{ CONTRACT : "มีสัญญา"
CLIENT ||--o{ PROJECT : "เป็นเจ้าของ"
CONTRACT ||--o{ PROJECT : "ครอบคลุม"
PROJECT ||--o{ WORK_ITEM : "มีงานย่อย"
PROJECT ||--o{ TIME_ENTRY : "ถูกลงเวลา"
PROJECT ||--o{ INCIDENT : "มีเหตุขัดข้อง"
PROJECT ||--o{ KB_DOC : "มีความรู้"
WORK_ITEM ||--o{ TIME_ENTRY : "ถูกลงเวลา"
OT_REQUEST ||--o{ TIME_ENTRY : "รองรับ"
PERIOD ||--o{ TIME_ENTRY : "ครอบคลุม"
PERSON {
uuid id PK
string email UK
string full_name
string team
string employment_type
date hired_on
bool is_active
}
COST_RATE {
uuid id PK
uuid person_id FK
date effective_from
int monthly_cost
int standard_hours_per_month
}
CLIENT {
uuid id PK
string name
string alias
}
CONTRACT {
uuid id PK
uuid client_id FK
string type
int amount
date start_date
date end_date
}
PROJECT {
uuid id PK
uuid client_id FK
uuid contract_id FK
string code UK
string name
uuid ba_owner_id FK
string status
}
WORK_ITEM {
uuid id PK
uuid project_id FK
string title
string status
uuid assignee_id FK
decimal estimate_hours
date due_date
string external_ref
}
TIME_ENTRY {
uuid id PK
uuid person_id FK
uuid project_id FK
uuid work_item_id FK
date work_date
decimal hours
string kind
string note
string source
text raw_input
uuid ot_request_id FK
uuid period_id FK
}
SHIFT {
uuid id PK
uuid person_id FK
date shift_date
string shift_type
time start_time
time end_time
}
OT_REQUEST {
uuid id PK
uuid person_id FK
uuid project_id FK
date ot_date
decimal hours
string reason
string status
uuid approver_id FK
decimal multiplier
}
LEAVE_REQUEST {
uuid id PK
uuid person_id FK
string leave_type
date start_date
date end_date
string status
uuid approver_id FK
}
INCIDENT {
uuid id PK
uuid project_id FK
uuid reporter_id FK
timestamp occurred_at
string severity
string status
}
PERIOD {
uuid id PK
int year
int month
string status
decimal overhead_rate
}
KB_DOC {
uuid id PK
uuid project_id FK
uuid author_id FK
string title
text body
vector embedding
}
5.2 ตารางหลัก#
person — พนักงาน#
| คอลัมน์ | ชนิด | หมายเหตุ |
|---|---|---|
email |
text, unique | ต้องลงท้าย @buildupclick.com |
team |
enum | executive · ba · programmer · designer |
employment_type |
enum | full_time · part_time · contractor |
is_active |
bool | พนักงานลาออกให้ตั้ง false ห้ามลบ เพราะต้นทุนย้อนหลังต้องยังคำนวณได้ |
บทบาทเก็บแยกที่ person_role (หนึ่งคนมีหลายบทบาทได้ เช่น programmer + lead)
cost_rate — ต้นทุนต่อคน ⭐#
| คอลัมน์ | ชนิด | หมายเหตุ |
|---|---|---|
effective_from |
date | วันที่ rate นี้เริ่มมีผล |
monthly_cost |
int | เงินเดือน + ประกันสังคม + สวัสดิการ + โบนัสเฉลี่ย (บาท) |
standard_hours_per_month |
int | ปกติ 168 |
กติกาที่ห้ามละเมิด
ตอนคำนวณต้นทุนต้องใช้ rate ที่มีผล ณ time_entry.work_date
SELECT monthly_cost / standard_hours_per_month
FROM cost_rate
WHERE person_id = $1 AND effective_from <= $2
ORDER BY effective_from DESC LIMIT 1
executive และ admin
project — โครงการ#
| คอลัมน์ | หมายเหตุ |
|---|---|
code |
รหัสสั้นที่คนจำได้และพิมพ์ให้ Claude ได้ เช่น LSM-MEMBER, LSM-SUPPORT, TUM-WEB, INTERNAL |
ba_owner_id |
BA ที่รับผิดชอบคิวงานของโครงการนี้ |
status |
active · on_hold · closed |
ต้องมีโครงการพิเศษเสมอ: INTERNAL (ประชุมภายใน · งานบริษัท) และ LEAVE (ไม่เข้าต้นทุนโครงการ)
time_entry — บันทึกเวลา ⭐⭐#
ตารางที่สำคัญที่สุดในระบบ ออกแบบให้ดีตั้งแต่แรก เพราะแก้ทีหลังแพงที่สุด
| คอลัมน์ | ชนิด | หมายเหตุ |
|---|---|---|
work_date |
date | วันที่ทำงานจริง ไม่ใช่วันที่กรอก |
hours |
numeric(4,2) | ทวีคูณของ 0.5 |
kind |
enum | shift งานในกะ · ot ล่วงเวลา · support แก้เหตุขัดข้อง · internal งานภายใน · meeting ประชุม |
source |
enum | mcp (ผ่าน Claude) · web · import · admin |
raw_input |
text | ข้อความดิบที่ผู้ใช้พิมพ์ตอนลงผ่าน Claude — ไว้ตรวจย้อนหลังเมื่อสงสัยว่าข้อมูลเพี้ยน |
ot_request_id |
fk | บังคับต้องมีเมื่อ kind = 'ot' |
period_id |
fk | ผูกกับงวด ถ้างวดปิดแล้วจะแก้ไม่ได้ |
ข้อจำกัดที่บังคับในฐานข้อมูล
CHECK (hours > 0 AND hours <= 16)
CHECK (hours = ROUND(hours * 2) / 2) -- ทวีคูณ 0.5
CHECK (kind <> 'ot' OR ot_request_id IS NOT NULL) -- OT ต้องมีใบอนุมัติ
ทำไมไม่มี UNIQUE กันลงซ้ำ
การแบ่งลงเวลาเป็นหลายช่วงในโครงการเดียวกันวันเดียวกันเป็นเรื่องปกติ (เช่น เช้า 3 ชม. บ่ายอีก 2 ชม. คนละงานย่อย) ถ้าใส่ UNIQUE จะบล็อกพฤติกรรมที่ถูกต้อง ระบบจึงใช้วิธี เตือนตอนขั้นตรวจสอบ แทน เมื่อพบรายการที่ซ้ำทุกฟิลด์ — กันการกดส่งซ้ำโดยไม่บล็อกการใช้งานจริง
Index ที่ต้องมี (รายงานเกือบทั้งหมดวิ่งผ่านสามตัวนี้)
CREATE INDEX ON time_entry (project_id, work_date);
CREATE INDEX ON time_entry (person_id, work_date);
CREATE INDEX ON time_entry (period_id);
shift — ตารางกะ#
| คอลัมน์ | หมายเหตุ |
|---|---|
shift_type |
morning · afternoon · night · standby |
start_time / end_time |
กะดึกข้ามวันได้ — ให้เก็บ end_time < start_time แล้วตีความว่าข้ามวัน |
ระบบต้องตรวจได้ว่า ทุกช่วงเวลาใน 24 ชม. มีคนคุมอย่างน้อย 1 คน และเตือนเมื่อมีช่องว่าง
period — งวดบัญชี#
| คอลัมน์ | หมายเหตุ |
|---|---|
status |
open · locked |
overhead_rate |
เก็บ ค่าที่ใช้จริงในงวดนั้น เพื่อให้รายงานย้อนหลังไม่เปลี่ยนเมื่อปรับ overhead ปีถัดไป |
audit_log — บันทึกการแก้ไข#
ทุกการแก้ไขข้อมูลที่มีผลต่อต้นทุน (time_entry, cost_rate, ot_request, period)
ต้องบันทึก: ใคร · เมื่อไหร่ · ตารางอะไร · ค่าเดิม → ค่าใหม่ · เหตุผล (บังคับเมื่อแก้หลังปิดงวด)
5.3 มุมมองสำเร็จรูป (Views)#
สร้าง view เหล่านี้ไว้ให้ dashboard และ Claude เรียกใช้ จะได้ไม่ต้องเขียน logic ซ้ำหลายที่
| View | ให้อะไร |
|---|---|
v_time_entry_cost |
time_entry แต่ละแถว + cost_rate ณ วันนั้น + ตัวคูณ + ต้นทุนเป็นบาท |
v_project_cost_monthly |
ต้นทุนรวมต่อโครงการต่อเดือน (รวม overhead แล้ว) |
v_client_pnl_monthly |
รายได้ − ต้นทุน + effective rate ต่อลูกค้าต่อเดือน |
v_person_utilization |
ชั่วโมงงานลูกค้า ÷ ชั่วโมงที่ทำงานได้ (หักวันลา) ต่อคนต่อเดือน |
v_timesheet_completeness |
ใครยังลงเวลาไม่ครบ วันไหนบ้าง — ใช้ตามงานก่อนปิดงวด |
5.4 ข้อมูลตั้งต้นที่ต้องเตรียม#
| ข้อมูล | จำนวนโดยประมาณ | ใครเตรียม |
|---|---|---|
| รายชื่อพนักงาน + ทีม + บทบาท | ทั้งบริษัท | HR |
| cost_rate ของทุกคน | ทั้งบริษัท | บัญชี + ผู้บริหาร |
| ลูกค้า + สัญญา | 2 ราย | ผู้บริหาร |
| โครงการทั้งหมด (โดยเฉพาะโครงการย่อยของ LSM99) | ? | BA |
| ตารางกะเดือนแรก | 1 เดือน | หัวหน้างาน |
| overhead_rate + ตัวคูณ OT | — | ผู้บริหาร + HR |