# ABIMS — ฐานข้อมูล (ตาราง tbl)

เอกสารนี้สำหรับ **โหลดไปอ่าน / ทำความเข้าใจโครงสร้างฐานข้อมูล ABIMS** — อธิบายทุกตาราง `tbl_*` ครบทุกคอลัมน์ ภาษาไทยเป็นหลัก  
โครงสร้างจริงอ้างอิง [`abims.sql`](../../abims.sql) (หลังล้างข้อมูลเหลือ Admin หลัก 1 บัญชี) และ [`database/migrations/`](../../database/migrations/)  
หน้าเว็บสรุปสั้น: [`docs/app/database.html`](../app/database.html)

**ชื่อฐานข้อมูล:** `abims` (การเชื่อมอ่านจาก `.env` / `config/database.php` — ไม่เก็บรหัสผ่านในเอกสารนี้)

### แนะนำวิธีอ่าน (สำหรับทีม / นักศึกษา)

1. อ่าน **ภาพรวม** และ **คำศัพท์ PK · FK · AI** ก่อน — จะได้เข้าใจคอลัมน์ “คีย์” ในตารางด้านล่าง  
2. ดู **รายชื่อ 11 ตาราง** และ **แผนผังความสัมพันธ์** — จำได้ว่า “ยืมของ” กับ “จองห้อง” แยกกัน  
3. Import [`abims.sql`](../../abims.sql) ใน phpMyAdmin แล้วเปิดดูตารางคู่กับเอกสารนี้  
4. ลงรายละเอียด **ตารางทีละตัว** ตอนทำงานจริงหรือออกแบบ query

---

## ภาพรวม — ฐานข้อมูลทำงานยังไง

MySQL ชื่อ `abims` มีตารางหลัก **11 ตัว** (ขึ้นต้น `tbl_`) แบ่งเป็น **สองเรื่องใหญ่**:

| ศูนย์กลาง | โฟลยืมอุปกรณ์ | โฟลจองห้อง |
|-----------|----------------|------------|
| `tbl_users` — ทุกคำขอและแจ้งเตือนผูก `user_id` | หมวด → ของ → หัวคำขอ → รายการชิ้น | ห้อง → การจอง / บล็อกเวลา + ชุดเวลาทำการ |
| `tbl_categories` → `tbl_equipments` | `tbl_bookings` → `tbl_booking_details` | `tbl_rooms` → `tbl_room_bookings`, `tbl_calendar_blocks` |
| | | `tbl_time_presets` → `tbl_time_preset_days` |

ห้ามใช้ `tbl_bookings` กับปฏิทินห้อง — เป็นคนละชุดตารางในระบบ

### Tree — โครงสร้างฐานข้อมูล (สองสาย)

```text
abims (MySQL)
├── ศูนย์กลาง
│   └── tbl_users  (+ OTP รีเซ็ตรหัสชั่วคราว reset_otp_*)
├── สายยืมอุปกรณ์
│   ├── tbl_categories
│   ├── tbl_equipments
│   ├── tbl_bookings          ← หัวคำขอ (ไม่ใช่จองห้อง)
│   └── tbl_booking_details
├── สายจองห้อง
│   ├── tbl_rooms
│   ├── tbl_room_bookings
│   ├── tbl_calendar_blocks
│   ├── tbl_time_presets
│   └── tbl_time_preset_days
└── ระบบเสริม
    └── tbl_notifications
```

```mermaid
flowchart TB
  users[tbl_users]
  users --> bookings[tbl_bookings]
  users --> roombk[tbl_room_bookings]
  users --> notif[tbl_notifications]
  cat[tbl_categories] --> equip[tbl_equipments]
  equip --> details[tbl_booking_details]
  bookings --> details
  rooms[tbl_rooms] --> roombk
  rooms --> blocks[tbl_calendar_blocks]
  presets[tbl_time_presets] --> days[tbl_time_preset_days]
```

---

## โครงสร้างตารางทั้ง 11 ตัว

ตารางด้านล่างคือ “แผนที่” ของฐานข้อมูลเรา — ชื่อตารางตรงกับใน phpMyAdmin หลัง import `abims.sql`

| กลุ่ม | ตาราง | เก็บอะไร (สรุป) |
|--------|--------|------------------|
| **ผู้ใช้** | `tbl_users` | บัญชี Admin / Manager / Employee + ฟิลด์ OTP ลืมรหัสชั่วคราว (`reset_otp_*`) |
| **ยืมอุปกรณ์ — ข้อมูลหลัก** | `tbl_categories` | หมวดหมู่อุปกรณ์ |
| | `tbl_equipments` | รายการอุปกรณ์ + จำนวนคงเหลือ |
| | `tbl_bookings` | หัวคำขอยืม (หนึ่งแถว = หนึ่งคำขอ) |
| | `tbl_booking_details` | รายการยืมแต่ละชิ้นในคำขอ |
| **จองห้อง — ข้อมูลหลัก** | `tbl_rooms` | ห้องประชุม / ห้องใช้งาน |
| | `tbl_room_bookings` | การจองช่วงเวลา |
| | `tbl_calendar_blocks` | บล็อกเวลา / วันหยุด (ต่อห้องหรือทั้งระบบ) |
| | `tbl_time_presets` | ชุดกฎเวลาเปิด–ปิด (เช่น วันทำการ) |
| | `tbl_time_preset_days` | วันในสัปดาห์ + เวลา ต่อหนึ่ง preset |
| **ระบบเสริม** | `tbl_notifications` | แจ้งเตือนในแอป |

นอกจากนี้ใน dump อาจมี `_migrations` สำหรับนักพัฒนา — ไม่นับเป็น 11 ตารางธุรกิจด้านบน

**การเปลี่ยน schema (ล่าสุด):** ลบ `secure_text` / `secure_pointer` และตาราง `tbl_password_reset_otps` — ย้าย OTP รีเซ็ตรหัสมาเก็บที่ `tbl_users.reset_otp_hash`, `reset_otp_expires_at`, `reset_otp_attempts` (migration `2026_10_07_000002_slim_users_merge_reset_otp.sql`) · รหัสผ่านหลักใช้ bcrypt ที่ `password` เท่านั้น

---

## คำศัพท์ที่ต้องรู้ (PK · FK · AI)

| คำ | ความหมายใน ABIMS |
|----|------------------|
| **PK (Primary Key)** | รหัส**ประจำแถว** — ในตารางเดียวกัน**ซ้ำกันไม่ได้** (เช่น `user_id`, `id`) · หนึ่งแถว = หนึ่งเรื่อง |
| **FK (Foreign Key)** | คอลัมน์**อ้างแถวแม่**ในตารางอื่น (เช่น `tbl_bookings.user_id` → `tbl_users`) · ลบแม่แล้วลูกถูกลบหรือปรับตามกฎ |
| **AI (AUTO_INCREMENT)** | เลข `id` ที่ระบบรันให้เอง 1, 2, 3… เมื่อเพิ่มแถวโดยไม่ใส่ `id` · **แต่ละแถวได้เลขไม่ซ้ำในตารางนั้น** · `tbl_users` ไม่ใช้ AI แต่ใช้ `user_id` ที่ระบบสร้าง |

**รหัสบทบาท `tbl_users.role`:** `1` = Admin · `2` = Manager · `3` = Employee

---

## รหัสสถานะ (status) สรุป

ฐานข้อมูลเก็บ **ตัวเลข** เสมอ — หน้าเว็บแปลงเป็นข้อความตามภาษาที่เลือก

| ตาราง / คอลัมน์ | ค่า |
|-----------------|-----|
| `tbl_users.status` | `1` ใช้งาน · `0` ปิดบัญชี |
| `tbl_rooms.status` | `1` พร้อมใช้ · `0` ปิดปรับปรุง |
| `tbl_rooms.requires_booking_approval` | `0` จองทันที · `1` ต้องอนุมัติ (คนละเรื่องกับ `status`) |
| `tbl_equipments.status` | `1` พร้อม · `0` ปิด · `2` ซ่อม · `3` จำกัด · `4` เลิกใช้ |
| `tbl_bookings.status` (ยืมอุปกรณ์) | `1` รออนุมัติ · `2` อนุมัติ/กำลังยืม · `3` ปฏิเสธ · `4` คืนแล้ว · `5` ผู้ขอยกเลิก |
| `tbl_room_bookings.status` (จองห้อง) | `1` รออนุมัติ · `2` อนุมัติ · `3` ปฏิเสธ · `4` ยกเลิก |
| `tbl_calendar_blocks.block_type` | `1` วันหยุดทั้งบริษัท (`room_id` = NULL ได้) · `2` ปิดเฉพาะห้อง |
| `tbl_time_preset_days.day_of_week` | `0` อาทิตย์ … `6` เสาร์ |

**ค้างคืน (overdue)** ไม่ใช่ค่า status แยก — คือ `tbl_bookings.status = 2` ที่เลย `return_date` แล้วยังไม่คืน

```text
status ที่พบบ่อย
├── tbl_bookings (ยืม)
│   ├── 1 รอ → 2 อนุมัติ → 4 คืนแล้ว
│   ├── 3 ปฏิเสธ
│   └── 5 ยกเลิก
└── tbl_room_bookings (จอง)
    ├── 1 รอ → 2 อนุมัติ
    ├── 3 ปฏิเสธ
    └── 4 ยกเลิก
```

---

## Admin คนแรกในฐานข้อมูล

ไฟล์ `abims.sql` ถูกล้างให้เหลือ**บัญชีผู้ดูแลระบบหลักเพียงหนึ่งคน** — import แล้วล็อกอินได้ทันที แต่ยังไม่มีห้อง อุปกรณ์ หรือประวัติคำขอ

| รายการ | ค่า |
|--------|-----|
| ล็อกอิน | `username` = **admin** |
| รหัสในระบบ (PK) | `user_id` = `1z38ml5XLUUH` |
| บทบาท | `role` = 1 (Admin) |
| หลัง import | ตั้งห้อง · หมวด/อุปกรณ์ · ชุดเวลาทำการ จากเมนู Admin แล้วค่อยเพิ่ม Manager/Employee |

บัญชีนี้ในระบบจริงเป็น **Admin คนแรก** (Admin ที่ `created_at` เก่าสุดใน `role = 1`) — **เปลี่ยน role / ปิดบัญชีไม่ได้** เพื่อให้มีผู้ดูแล default หลังติดตั้ง (รายละเอียดใน [`00-roles-complete.md`](00-roles-complete.md))

---

## ความสัมพันธ์ — อ่านจากแม่ไปลูก

**หลักการง่ายๆ**

- **แม่** = ตารางที่ถูกอ้าง (มี PK ที่ลูกชี้มา)  
- **ลูก** = ตารางที่มีคอลัมน์ FK  
- ลบแม่แล้วลูกจะ **ถูกลบตาม (CASCADE)** หรือ **เคลียร์ค่าเป็น NULL (SET NULL)** ตามที่ตั้งใน `abims.sql`

### แผนผัง (อ่านจากบนลงล่าง)

```text
tbl_users  (ศูนย์กลาง — ใครทำอะไร)
  ├── tbl_bookings.user_id          ผู้ยืม
  ├── tbl_bookings.approved_by      ผู้ดำเนินการคำขอยืม (อาจ NULL)
  ├── tbl_room_bookings.user_id     ผู้จองห้อง
  ├── tbl_room_bookings.approved_by ผู้อนุมัติจองห้อง (อาจ NULL)
  ├── tbl_notifications.user_id     ผู้รับแจ้งเตือน (ไม่มี FK บังคับใน schema)
  ├── reset_otp_*                   OTP ลืมรหัส (ชั่วคราวบนแถวผู้ใช้ — ไม่มีตารางแยก)
  └── calendar_blocks.created_by    อ้าง Admin (ตรรกะในแอป)

tbl_categories
  └── tbl_equipments.category_id
        └── tbl_booking_details.equipment_id

tbl_bookings
  └── tbl_booking_details.booking_id

tbl_rooms
  ├── tbl_room_bookings.room_id
  └── tbl_calendar_blocks.room_id   (NULL = บล็อกทั้งระบบ / วันหยุดรวม)

tbl_time_presets
  └── tbl_time_preset_days.preset_id

tbl_notifications.booking_id — อาจเป็นคำขอยืมหรือจองห้อง ตาม logic แอป (ไม่มี FK)
```

### FK ที่ตั้งในฐานข้อมูล (อ้างอิง `abims.sql`)

| ตารางลูก | คอลัมน์ FK | ชี้ไปแม่ | เมื่อลบแม่ |
|-----------|------------|----------|------------|
| `tbl_bookings` | `user_id` | `tbl_users.user_id` | ลบคำขอตาม (CASCADE) |
| `tbl_bookings` | `approved_by` | `tbl_users.user_id` | ตั้งเป็น NULL |
| `tbl_booking_details` | `booking_id` | `tbl_bookings.id` | ลบรายการตาม |
| `tbl_booking_details` | `equipment_id` | `tbl_equipments.id` | ลบรายการตาม |
| `tbl_equipments` | `category_id` | `tbl_categories.id` | ลบอุปกรณ์ตาม |
| `tbl_room_bookings` | `room_id` | `tbl_rooms.id` | ลบการจองตาม |
| `tbl_room_bookings` | `user_id` | `tbl_users.user_id` | ลบการจองตาม |
| `tbl_room_bookings` | `approved_by` | `tbl_users.user_id` | ตั้งเป็น NULL |
| `tbl_calendar_blocks` | `room_id` | `tbl_rooms.id` | ลบบล็อกตาม |
| `tbl_time_preset_days` | `preset_id` | `tbl_time_presets.id` | ลบวันใน preset ตาม |

---

## ไฟล์ `abims.sql` คืออะไร

ไฟล์ dump ในโปรเจกต์ใช้ **ติดตั้งฐานใหม่** หรือให้ทุกคนได้ schema เดียวกัน

| หัวข้อ | รายละเอียด |
|--------|------------|
| **โครงสร้าง** | `CREATE TABLE`, index, FK ครบ — ตรงกับเอกสารตารางด้านล่าง |
| **ข้อมูลหลังล้าง** | เหลือ **Admin หลัก 1 แถว** (`username` = `admin`, `user_id` = `1z38ml5XLUUH`) · ตารางคำขอ / แจ้งเตือน / ห้อง / อุปกรณ์ **ว่าง** · อาจมี `_migrations` สำหรับ dev |
| **หลัง import** | ล็อกอิน admin → ตั้งห้อง · หมวด/อุปกรณ์ · ชุดเวลาทำการ จากเมนู Admin → ค่อยเพิ่ม Manager/Employee |

**ขั้นตอนแนะนำ:** สร้างฐาน `abims` ใน phpMyAdmin → Import `abims.sql` → เปิดดูตารางทีละตัวคู่กับหัวข้อ “ตารางทีละตัว” ด้านล่าง

---

## ตารางทีละตัว — ทุกคอลัมน์

ด้านล่างเป็นรายการฟิลด์ครบตาม schema ปัจจุบัน

- คอลัมน์ **คีย์**: **PK** = คีย์หลัก (ซ้ำไม่ได้) · **FK** = อ้างตารางอื่น · **AI** = AUTO_INCREMENT (เลข `id` ไม่ซ้ำในตารางนั้น)  
- **UNIQUE** = ค่าในคอลัมน์นั้นซ้ำกันไม่ได้ (เช่น `username`)  
- ชนิดข้อมูลและค่าเริ่มต้นตรงกับใน `abims.sql`

---

## `tbl_users` — บัญชีผู้ใช้

เก็บ Admin / Manager / Employee ทุกคน

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `user_id` | varchar(12) | **PK** | — | รหัสผู้ใช้ไม่ซ้ำ (สร้างตอนลงทะเบียน) |
| `username` | varchar(50) | UNIQUE | — | ชื่อล็อกอิน |
| `full_name` | varchar(150) | | NULL | ชื่อ-นามสกุลแสดงในระบบ |
| `gender` | varchar(22) | | — | เพศ (ค่าที่ฟอร์มกำหนด) |
| `prefix` | varchar(22) | | — | คำนำหน้า เช่น นาย / นางสาว |
| `email` | varchar(150) | | NULL | อีเมล |
| `phone` | varchar(20) | | NULL | เบอร์โทร |
| `birth_date` | date | | NULL | วันเกิด |
| `profile_image` | varchar(255) | | NULL | path หรือ URL รูปโปรไฟล์ |
| `password` | varchar(255) | | — | รหัสผ่าน bcrypt (`password_hash`) — ไม่เก็บ plain text |
| `reset_otp_hash` | varchar(64) | | NULL | แฮช SHA-256 ของ OTP ลืมรหัส (NULL = ไม่มีการขอรีเซ็ต) |
| `reset_otp_expires_at` | datetime | | NULL | หมดอายุ OTP |
| `reset_otp_attempts` | int(11) | | 0 | จำนวนครั้งใส่ OTP ผิด (สูงสุด 5 แล้วล้างในแอป) |
| `status` | tinyint(1) | | 1 | `1` ใช้งาน · `0` ปิดบัญชี |
| `created_at` | datetime | | current | วันที่สร้างบัญชี |
| `updated_at` | datetime | | current | อัปเดตล่าสุด (อัตโนมัติเมื่อแก้) |
| `role` | tinyint(1) | | 2 | `1` Admin · `2` Manager · `3` Employee |

**เชื่อมกับ:** ถูกอ้างจาก `tbl_bookings`, `tbl_room_bookings`, `tbl_notifications` · OTP รีเซ็ตผูกทาง `email` ใน [`reset_handler.php`](../../app/reset_handler.php) (ไม่มี FK แยก)

---

## `tbl_categories` — หมวดหมู่อุปกรณ์

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `id` | int(11) | **PK**, **AI** | — | รหัสหมวด |
| `category_name` | varchar(100) | | — | ชื่อหมวด (ไทย/อังกฤษ) |
| `icon` | varchar(50) | | bookmark | ชื่อไอคอน Material ใน UI |
| `created_at` | timestamp | | current | วันที่สร้าง |

**เชื่อมกับ:** `tbl_equipments.category_id` → ลบหมวดแล้วลบอุปกรณ์ในหมวด (CASCADE)

---

## `tbl_equipments` — รายการอุปกรณ์

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `id` | int(11) | **PK**, **AI** | — | รหัสอุปกรณ์ |
| `category_id` | int(11) | **FK** → `tbl_categories.id` | — | อยู่หมวดใด |
| `equipment_name` | varchar(255) | | — | ชื่ออุปกรณ์ |
| `image_path` | varchar(255) | | NULL | รูปสินค้า |
| `total_quantity` | int(11) | | 1 | จำนวนทั้งหมดในคลัง |
| `available_quantity` | int(11) | | 1 | จำนวนที่ยืมได้ตอนนี้ |
| `comment` | text | | NULL | หมายเหตุ |
| `status` | tinyint(1) | | 1 | `1` พร้อม · `0` ปิด · `2` ซ่อม · `3` จำกัด · `4` เลิกใช้ |
| `max_borrow_duration` | int(11) | | 1 | ยืมได้สูงสุดกี่วัน (ต่อรายการ) |
| `max_per_request` | int(11) | | 1 | ยืมต่อคำขอได้สูงสุดกี่ชิ้น |
| `created_at` | timestamp | | current | วันที่เพิ่มรายการ |

**เชื่อมกับ:** `tbl_booking_details.equipment_id`

---

## `tbl_bookings` — หัวคำขอยืมอุปกรณ์

หนึ่งแถว = หนึ่งคำขอยืม (อาจมีหลายชิ้นใน `tbl_booking_details`)

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `id` | int(11) | **PK**, **AI** | — | รหัสคำขอยืม |
| `user_id` | varchar(12) | **FK** → `tbl_users.user_id` | — | ผู้ยืม |
| `status` | tinyint(1) | | 1 | `1` รอ · `2` อนุมัติ/กำลังยืม · `3` ปฏิเสธ · `4` คืนแล้ว · `5` ยกเลิก |
| `approved_by` | varchar(12) | **FK** → `tbl_users.user_id` | NULL | ผู้ดำเนินการล่าสุด (อนุมัติ/ปฏิเสธ/รับคืน) |
| `admin_mgr_notify_unread` | tinyint(1) | | 0 | `1` = Admin ยังไม่เคลียร์แจ้งเตือนกิจกรรม Manager |
| `reject_reason` | text | | NULL | เหตุผลปฏิเสธ |
| `borrow_date` | timestamp | | current | วันที่ส่งคำขอ/เริ่มยืม |
| `return_date` | timestamp | | NULL | กำหนดคืน — ใช้เช็คค้างคืน |
| `return_evidence` | text | | NULL | รูปหลักฐานตอนคืน 1–5 รูป เก็บเป็น JSON array ของ path (ข้อมูลเก่าอาจเป็น path เดียว) |

**เชื่อมกับ:** `tbl_booking_details.booking_id` · ลบคำขอแล้วลบรายละเอียด (CASCADE)

---

## `tbl_booking_details` — รายการอุปกรณ์ในคำขอยืม

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `id` | int(11) | **PK**, **AI** | — | รหัสบรรทัดรายการ |
| `booking_id` | int(11) | **FK** → `tbl_bookings.id` | — | อยู่ในคำขอใด |
| `equipment_id` | int(11) | **FK** → `tbl_equipments.id` | — | อุปกรณ์ชิ้นใด |
| `quantity` | int(11) | | 1 | จำนวนที่ยืมในบรรทัดนี้ |

---

## `tbl_rooms` — ห้องประชุม

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `id` | int(11) | **PK**, **AI** | — | รหัสห้อง |
| `room_name` | varchar(150) | | — | ชื่อห้อง |
| `description` | text | | NULL | รายละเอียดห้อง |
| `capacity` | int(11) | | 0 | ความจุ (คน) |
| `allowed_roles` | varchar(50) | | 1,2,3 | บทบาทที่จองได้ เช่น `1,2,3` |
| `image_path` | varchar(255) | | NULL | รูปห้อง |
| `color_tag` | varchar(20) | | #3788d8 | สีบนปฏิทิน (hex) |
| `status` | tinyint(1) | | 1 | `1` เปิดใช้ · `0` ปิดปรับปรุง |
| `requires_booking_approval` | tinyint(1) | | 0 | `1` = จองแล้วรออนุมัติ · `0` = ใช้ได้ทันที (ตามกฎ role) |
| `created_at` | timestamp | | current | วันที่สร้าง |
| `access_role` | tinyint(1) | | 3 | ระดับการมองเห็น/สิทธิ์ใน UI (1 Admin … 3 ทั่วไป) |

**เชื่อมกับ:** `tbl_room_bookings`, `tbl_calendar_blocks`

---

## `tbl_room_bookings` — การจองห้อง

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `id` | int(11) | **PK**, **AI** | — | รหัสการจอง |
| `user_id` | varchar(12) | **FK** → `tbl_users.user_id` | — | ผู้จอง |
| `room_id` | int(11) | **FK** → `tbl_rooms.id` | — | ห้องที่จอง |
| `title` | varchar(255) | | — | หัวข้อ/วัตถุประสงค์ |
| `attendees` | int(11) | | 1 | จำนวนผู้เข้าร่วม |
| `color_tag` | varchar(7) | | NULL | สี event (ถ้ากำหนด) |
| `start_datetime` | datetime | | — | เริ่ม |
| `end_datetime` | datetime | | — | สิ้นสุด |
| `status` | tinyint(1) | | 1 | `1` รอ · `2` อนุมัติ · `3` ปฏิเสธ · `4` ยกเลิก |
| `approved_by` | varchar(12) | **FK** → `tbl_users.user_id` | NULL | ผู้อนุมัติ/ปฏิเสธล่าสุด |
| `reject_reason` | text | | NULL | เหตุผลปฏิเสธ |
| `created_at` | timestamp | | current | วันที่สร้างคำขอ |

---

## `tbl_calendar_blocks` — บล็อกเวลา / วันหยุด

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `id` | int(11) | **PK**, **AI** | — | รหัสบล็อก |
| `room_id` | int(11) | **FK** → `tbl_rooms.id` | NULL | ห้อง — NULL = ทั้งองค์กร (เช่น วันหยุด) |
| `title_reason` | varchar(255) | | — | เหตุผลที่แสดงในปฏิทิน |
| `block_type` | tinyint(1) | | 2 | `1` วันหยุดบริษัท · `2` ปิดเฉพาะห้อง |
| `color_tag` | varchar(7) | | NULL | สีแสดง |
| `start_datetime` | datetime | | — | เริ่มบล็อก |
| `end_datetime` | datetime | | — | สิ้นสุดบล็อก |
| `created_by` | varchar(12) | | — | `user_id` ของ Admin ที่สร้าง (อ้างอิงตรรกะ ไม่มี FK ใน dump) |
| `created_at` | timestamp | | current | วันที่สร้าง |

---

## `tbl_time_presets` — ชุดเวลาทำการ

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `id` | int(11) | **PK**, **AI** | — | รหัสชุด |
| `preset_name` | varchar(100) | | — | ชื่อชุด เช่น วันทำการปกติ |
| `is_active` | tinyint(1) | | 0 | `1` = ชุดที่ใช้ตรวจการจองห้องอยู่ |
| `created_at` | timestamp | | current | วันที่สร้าง |

**เชื่อมกับ:** `tbl_time_preset_days.preset_id`

---

## `tbl_time_preset_days` — เวลาเปิด-ปิดรายวัน

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `id` | int(11) | **PK**, **AI** | — | รหัสบรรทัด |
| `preset_id` | int(11) | **FK** → `tbl_time_presets.id` | — | อยู่ในชุดใด |
| `day_of_week` | tinyint(1) | | — | `0` อาทิตย์ … `6` เสาร์ |
| `is_open` | tinyint(1) | | 1 | `1` เปิดทำการ · `0` ปิด |
| `open_time` | time | | 08:00:00 | เวลาเปิด (ถ้าเปิด) |
| `close_time` | time | | 18:00:00 | เวลาปิด (ถ้าเปิด) |

---

## `tbl_notifications` — แจ้งเตือนในระบบ

| ฟิลด์ | ชนิดข้อมูล | คีย์ | ค่าเริ่มต้น | ความหมาย |
|--------|------------|------|------------|----------|
| `id` | int(11) | **PK**, **AI** | — | รหัสแจ้งเตือน |
| `user_id` | varchar(12) | INDEX | — | ผู้รับ (ควรตรง `tbl_users`) |
| `booking_id` | int(11) | INDEX | — | อ้าง id คำขอ (ยืมหรือจองห้อง ตามประเภทข้อความ) |
| `type` | tinyint(1) | | — | ประเภทแจ้งเตือน (ตัวเลข — แปลในโค้ด) |
| `message` | text | | — | ข้อความหรือ payload แบบ `key|ส่วน|ส่วน` |
| `is_read` | tinyint(1) | | 0 | `1` อ่านแล้ว |
| `created_at` | timestamp | | current | เวลาแจ้ง |

---

## ER-Diagram — โครงสร้าง 11 ตาราง

![ER-Diagram ABIMS แนวนอน](../er-diagram-landscape.png)

แผนภาพต้นฉบับแนวตั้ง (`er-diagram.png`) — ในเอกสารเว็บหมุนเป็น**แนวนอน** (`er-diagram-landscape.png`) ให้อ่านซ้าย→ขวา · **สี่เหลี่ยม** = ตาราง · **วงรี** = คอลัมน์ · **เส้น + 1/m** = แม่–ลูก

### สัญลักษณ์

| สัญลักษณ์ | ความหมาย |
|-----------|----------|
| **PK** | รหัสไม่ซ้ำประจำแถว (`user_id`, `id`) |
| **FK** | ลูกชี้ PK ของแม่ |
| **1 : m** | แม่หนึ่งแถว ลูกหลายแถว |
| **m : m** | ใช้ตารางกลาง `tbl_booking_details` |

### 1) ศูนย์กลาง `tbl_users`

- **→ `tbl_notifications`** (`user_id`) แจ้งเตือน
- **→ `tbl_bookings`** (`user_id`, `approved_by`) ยืมอุปกรณ์
- **→ `tbl_room_bookings`** (`user_id`, `approved_by`) จองห้อง
- **→ `tbl_calendar_blocks`** (`created_by`) ผู้สร้างบล็อก
- OTP ลืมรหัส: `reset_otp_*` บนแถว user

### 2) สายยืมอุปกรณ์

`tbl_categories` → `tbl_equipments` → `tbl_booking_details` ← `tbl_bookings` (หัวคำขอ) · ผู้ยืมผูก `tbl_bookings.user_id` → users

### 3) สายจองห้อง

`tbl_rooms` → `tbl_room_bookings` และ `tbl_calendar_blocks` · **ไม่ใช้** `tbl_bookings`

### 4) เวลาทำการ

`tbl_time_presets` → `tbl_time_preset_days` (`preset_id`) — ใช้ตรวจจองห้อง

### แม่–ลูกหลัก (FK)

| แม่ (1) | ลูก (m) | FK |
|---------|---------|-----|
| `tbl_users` | `tbl_bookings`, `tbl_room_bookings`, `tbl_notifications` | `user_id`, `approved_by` |
| `tbl_categories` | `tbl_equipments` | `category_id` |
| `tbl_equipments` | `tbl_booking_details` | `equipment_id` |
| `tbl_bookings` | `tbl_booking_details` | `booking_id` |
| `tbl_rooms` | `tbl_room_bookings`, `tbl_calendar_blocks` | `room_id` |
| `tbl_time_presets` | `tbl_time_preset_days` | `preset_id` |

---

*อัปเดตเมื่อ schema ใน migration เปลี่ยน — หน้าเว็บสรุป: [`database.html`](../app/database.html)*
