ข้ามไปที่เนื้อหา

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
ห้าม UPDATE แถวเดิมเมื่อขึ้นเงินเดือน — ให้ INSERT แถวใหม่เสมอ ตารางนี้เข้าถึงได้เฉพาะ 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