Project

General

Profile

Feature #26939 » Employee-Status-Icon-Spec.md

Parujee Pangsriuthai, 05/08/2026 11:38 AM

 
1
# สรุปข้อกำหนด: ระบบแสดง Icon Status บนการ์ดพนักงาน
2

    
3
## ภาพรวม
4

    
5
พัฒนาระบบแสดงไอคอนและป้ายสถานะ (Status Icon & Label) บนการ์ดพนักงานในหน้าเลือกช่างซ่อมบำรุง (AssignMaintenanceSupportDrawer) แทนที่ placeholder "ICON" และ "STATUS" ที่มีอยู่ใน `EmployeeItem` component
6

    
7
## ที่มาของข้อมูลสถานะ
8

    
9
ข้อมูลสถานะมาจาก API (`/call-terminal/employee`) ซึ่ง query จาก DB โดยตรง:
10

    
11
- **is_available** (boolean): `true` = ไม่มีงาน `during work` ที่ active อยู่ (ว่างรับงานได้)
12
- **is_working** (boolean): `true` = มีงาน `during work` ที่ active และไม่ถูก suspend (กำลังทำงานอยู่)
13
- **Off Shift**: คำนวณจากกะหมุนเวียน — ดู `ms_shift_pattern_week_detail` ว่าวันนี้พนักงาน (ตาม work_group_id) ทำกะอะไร แล้วเทียบเวลาปัจจุบันกับ `start_ts` / `end_ts` ของกะนั้น
14

    
15
> ⚠️ ปัจจุบัน API ยังไม่มี field `is_off_shift` — ต้องเพิ่ม logic คำนวณจาก `ms_shift_pattern` + `ms_shift_pattern_week_detail` + `ms_shift` (ผ่าน `work_group_id` ของพนักงาน)
16

    
17
### SQL Query ปัจจุบัน (employee.repository.ts)
18

    
19
```sql
20
-- is_available: ไม่มีงาน during work
21
NOT EXISTS (
22
  SELECT 1 FROM mp_request_work_worker mp
23
  JOIN request_work rw ON rw.id = mp.request_work_id
24
  WHERE mp.employee_id = me.id
25
    AND mp.worker_type = 'employee'
26
    AND mp.is_active = TRUE
27
    AND rw.current_status = 'during work'
28
) AS is_available
29

    
30
-- is_working: มีงาน during work ที่ไม่ถูก suspend
31
EXISTS (
32
  SELECT 1 FROM mp_request_work_worker mp
33
  JOIN request_work rw ON rw.id = mp.request_work_id
34
  WHERE mp.employee_id = me.id
35
    AND mp.worker_type = 'employee'
36
    AND mp.is_active = TRUE
37
    AND rw.current_status = 'during work'
38
    AND rw.is_suspension = FALSE
39
) AS is_working
40

    
41
-- shift_start_ts / shift_end_ts: ดึงจาก ms_shift ผ่าน work_shift_id (กะตายตัว)
42
CAST(ms.start_ts AS TEXT) AS shift_start_ts,
43
CAST(ms.end_ts AS TEXT) AS shift_end_ts
44
-- JOIN: LEFT JOIN ms_shift ms ON ms.id = me.work_shift_id
45
```
46

    
47
### ปัญหาของ SQL ปัจจุบัน
48

    
49
- `shift_start_ts` / `shift_end_ts` ดึงจาก `ms_shift` ผ่าน `work_shift_id` ตรงๆ
50
- ใช้ได้แค่กะตายตัว (เช่น กลุ่ม C ที่ทำ 08:00-17:00 ตลอด)
51
- **ไม่รองรับกะหมุน** — เพราะกลุ่ม A อาจทำกะเช้า (06:00-14:30) สัปดาห์นี้ แต่ทำกะบ่าย (14:30-23:00) สัปดาห์หน้า
52

    
53
### Frontend Logic ปัจจุบัน (useInteralAssignMaintenanceDrawer.ts)
54

    
55
- Function `isInShiftRange()` เทียบเวลาปัจจุบันกับ `shift_start_ts`/`shift_end_ts` ที่ได้จาก API
56
- รองรับกะข้ามวัน (overnight shift) อยู่แล้ว
57
- **ไม่ต้องแก้ Frontend** ถ้า Backend ส่ง `shift_start_ts`/`shift_end_ts` ที่ถูกต้องมา
58

    
59
### SQL ที่ต้องแก้ (รองรับกะหมุนเวียน)
60

    
61
เปลี่ยนจาก JOIN `ms_shift` ผ่าน `work_shift_id` เป็นคำนวณจาก pattern:
62

    
63
```sql
64
SELECT
65
  me.id,
66
  mct.value AS department_incharge_value,
67
  me.firstname,
68
  me.lastname,
69
  NOT EXISTS (
70
    SELECT 1
71
    FROM mp_request_work_worker mp
72
    JOIN request_work rw ON rw.id = mp.request_work_id
73
    WHERE mp.employee_id = me.id
74
      AND mp.worker_type = 'employee'
75
      AND mp.is_active = TRUE
76
      AND rw.current_status = 'during work'
77
  ) AS is_available,
78
  EXISTS (
79
    SELECT 1
80
    FROM mp_request_work_worker mp
81
    JOIN request_work rw ON rw.id = mp.request_work_id
82
    WHERE mp.employee_id = me.id
83
      AND mp.worker_type = 'employee'
84
      AND mp.is_active = TRUE
85
      AND rw.current_status = 'during work'
86
  ) AS is_working,
87
  -- ใหม่: ดึง shift_start_ts / shift_end_ts จากกะหมุนเวียน
88
  CAST(rotation_shift.start_ts AS TEXT) AS shift_start_ts,
89
  CAST(rotation_shift.end_ts AS TEXT) AS shift_end_ts
90
FROM
91
  ms_employee me
92
  LEFT JOIN ms_code_table mct ON mct.id = me.department_incharge
93
    AND mct.code = 'department_incharge'
94
  -- ใหม่: JOIN กะหมุนเวียนแทน ms_shift ตรงๆ
95
  LEFT JOIN ms_work_group wg ON wg.id = me.work_group_id
96
  LEFT JOIN LATERAL (
97
    SELECT s.start_ts, s.end_ts
98
    FROM ms_shift_pattern sp
99
    JOIN ms_shift_pattern_week_detail d ON d.pattern_id = sp.id
100
      AND d.work_group_id = wg.id
101
      AND d.is_active = TRUE
102
      AND d.is_deleted = FALSE
103
    JOIN ms_shift s ON s.id = d.shift_id
104
    WHERE sp.is_active = TRUE
105
      AND sp.is_deleted = FALSE
106
      AND d.week_no = (
107
        FLOOR(
108
          ((CURRENT_DATE - sp.anchor_date) % sp.cycle_days)::int / 7
109
        ) + 1
110
      )
111
    LIMIT 1
112
  ) rotation_shift ON TRUE
113
WHERE
114
  me.is_active = TRUE
115
  AND me.is_deleted = FALSE
116
ORDER BY
117
  me.firstname;
118
```
119

    
120
**หลักการ:**
121
1. JOIN `ms_work_group` ผ่าน `work_group_id` ของพนักงาน
122
2. ใช้ LATERAL subquery คำนวณ week_no จาก `anchor_date` + `cycle_days`
123
3. ดึง `start_ts`/`end_ts` ของกะที่กลุ่มนั้นทำในสัปดาห์นั้น
124
4. Frontend ใช้ `isInShiftRange()` เหมือนเดิม — ไม่ต้องแก้
125

    
126
### ตรรกะ Off Shift (ต้องแก้ใหม่ — รองรับกะหมุนเวียน)
127

    
128
**เดิม (กะตายตัว):**
129
```
130
Off Shift = เวลาปัจจุบัน ไม่อยู่ในช่วง ms_shift.start_ts ~ ms_shift.end_ts
131
            ของกะที่ผูกกับพนักงาน (ms_employee.work_shift_id → ms_shift.id)
132
```
133

    
134
**ใหม่ (รองรับกะหมุนเวียน):**
135
```
136
1. ดู work_group_id ของพนักงาน → ได้กลุ่ม (เช่น A)
137
2. ดึง ms_shift_pattern ที่ active → ได้ anchor_date, cycle_days
138
3. คำนวณ week_no = floor(((วันนี้ - anchor_date) % cycle_days) / 7) + 1
139
4. ดู ms_shift_pattern_week_detail WHERE week_no = ค่าที่คำนวณ AND work_group_id = กลุ่มพนักงาน
140
   → ได้ shift_id (เช่น MORNING)
141
5. ดู ms_shift → ได้ start_ts / end_ts (เช่น 06:00-14:30)
142
6. เทียบเวลาปัจจุบันกับ start_ts ~ end_ts
143
7. ถ้าไม่อยู่ในช่วง → is_off_shift = true
144
```
145

    
146
**SQL ใหม่ (concept):**
147
```sql
148
-- คำนวณ is_off_shift จากกะหมุนเวียน
149
WITH current_pattern AS (
150
    SELECT id, anchor_date, cycle_days
151
    FROM ms_shift_pattern
152
    WHERE is_active = true AND is_deleted = false
153
    LIMIT 1
154
),
155
current_week AS (
156
    SELECT 
157
        floor(((CURRENT_DATE - cp.anchor_date) % cp.cycle_days) / 7) + 1 AS week_no,
158
        cp.id AS pattern_id
159
    FROM current_pattern cp
160
)
161
SELECT 
162
    me.id AS employee_id,
163
    CASE 
164
        WHEN CURRENT_TIME NOT BETWEEN s.start_ts AND s.end_ts THEN true
165
        ELSE false
166
    END AS is_off_shift
167
FROM ms_employee me
168
JOIN ms_work_group wg ON wg.id = me.work_group_id
169
JOIN current_week cw ON true
170
JOIN ms_shift_pattern_week_detail d 
171
    ON d.pattern_id = cw.pattern_id
172
    AND d.week_no = cw.week_no
173
    AND d.work_group_id = wg.id
174
    AND d.is_active = true
175
    AND d.is_deleted = false
176
JOIN ms_shift s ON s.id = d.shift_id
177
WHERE me.id = :employee_id;
178
```
179

    
180
> ⚠️ กรณีกะดึก (NIGHT: 23:00-06:00) ที่ข้ามวัน ต้องจัดการ logic เพิ่ม:
181
> `CURRENT_TIME >= start_ts OR CURRENT_TIME <= end_ts` (แทนที่จะใช้ BETWEEN)
182

    
183
## เลย์เอาต์การ์ดพนักงาน
184

    
185
### โครงสร้างทั่วไป
186

    
187
```
188
┌─────────────────────────────────┐
189
│  ชื่อ นามสกุล (bold)        ● ← จุดสี (Status Dot)
190
│                                 │
191
│  ⊙ Available                    │  ← ไอคอน + ข้อความสถานะ
192
└─────────────────────────────────┘
193
```
194

    
195
- **แถวบน**: ชื่อพนักงาน (ซ้าย) + วงกลมสีทึบ 16x16px (ขวา)
196
- **แถวล่าง**: ไอคอน 16x16px + ข้อความสถานะ (ชิดซ้าย, gap-2)
197

    
198
### ตัวอย่างการ์ดแต่ละสถานะ
199

    
200
#### ✅ Available (ว่าง) — สีเขียว
201

    
202
```
203
┌─────────────────────────────────┐
204
│                                 │
205
│  Anupong Chaiya             🟢  │  ← bg-green-500
206
│                                 │
207
│  ✓ Available                    │  ← CheckCircle + text-green-600
208
│                                 │
209
└─────────────────────────────────┘
210
  border: border-gray-300 (ปกติ)
211
  bg: bg-white
212
```
213

    
214
#### 🔧 Working (กำลังทำงาน) — สีน้ำเงิน
215

    
216
```
217
┌─────────────────────────────────┐
218
│                                 │
219
│  Chaiyong Siripong          🔵  │  ← bg-blue-500
220
│                                 │
221
│  🕐 Working                     │  ← Clock + text-blue-600
222
│                                 │
223
└─────────────────────────────────┘
224
  border: border-gray-300 (ปกติ)
225
  bg: bg-white
226
```
227

    
228
#### 🚫 Off Shift (ไม่อยู่กะ) — สีเทา
229

    
230
```
231
┌─────────────────────────────────┐
232
│                                 │
233
│  Somchai Three              ⚪  │  ← bg-gray-400
234
│                                 │
235
│  ⊘ Off Shift                    │  ← MinusCircle + text-gray-400
236
│                                 │
237
└─────────────────────────────────┘
238
  border: border-gray-300 (ปกติ)
239
  bg: bg-white
240
```
241

    
242
#### 🔲 เมื่อถูกเลือก (Selected) — ทุกสถานะเหมือนกัน
243

    
244
```
245
┌═════════════════════════════════┐
246
│                                 │  ← border-blue-600 (เส้นขอบน้ำเงิน)
247
│  Sakchai Rattana            🟢  │     bg-blue-50 (พื้นหลังฟ้าอ่อน)
248
│                                 │
249
│  ✓ Available                    │
250
│                                 │
251
└═════════════════════════════════┘
252
  border: border-blue-600 (เลือกแล้ว)
253
  bg: bg-blue-50
254
```
255

    
256
### ตัวอย่างหน้าจอรวม (Grid 4 คอลัมน์)
257

    
258
```
259
⚡ Electrician  12
260
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
261
│ Anupong Chaiya 🟢│  │ Ganya Ratchada 🟢│  │ Sakchai Rattan 🟢│  │ Chaiyong Siri  🔵│
262
│ ✓ Available      │  │ ✓ Available      │  │ ✓ Available      │  │ 🕐 Working       │
263
└──────────────────┘  └──────────────────┘  └──────────────────┘  └──────────────────┘
264

    
265
🔧 Mechanic  5
266
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
267
│ Somchai Three  ⚪│  │ User One       🟢│  │ Prasit Kaew    🔵│  │ Wichai Suk     ⚪│
268
│ ⊘ Off Shift      │  │ ✓ Available      │  │ 🕐 Working       │  │ ⊘ Off Shift      │
269
└──────────────────┘  └──────────────────┘  └──────────────────┘  └──────────────────┘
270
```
271

    
272
### ตัวอย่างปุ่มกรองสถานะ
273

    
274
```
275
┌───────────────────────────────────────────────────────────────────────────────────┐
276
│  🔍 Search maintenance staff...     [  All  ] [ Available ] [ Working ] [Off Shift]│
277
│                                      (red)     (green)       (blue)     (gray)    │
278
└───────────────────────────────────────────────────────────────────────────────────┘
279
```
280

    
281
- **All** (แดง): แสดงทุกคน
282
- **Available** (เขียว): แสดงเฉพาะ `is_available = true` AND `is_off_shift = false`
283
- **Working** (น้ำเงิน): แสดงเฉพาะ `is_working = true` AND `is_off_shift = false`
284
- **Off Shift** (เทา): แสดงเฉพาะ `is_off_shift = true`
285

    
286
## สถานะทั้ง 3 แบบ (Priority: Off Shift > Working > Available)
287

    
288
| สถานะ | เงื่อนไข | สี Dot | ไอคอน | ข้อความ | สีข้อความ/ไอคอน |
289
|---|---|---|---|---|---|
290
| Off Shift | ไม่อยู่ในเวลากะ (`is_off_shift = true`) | ⚪ bg-gray-400 | MinusCircle | "Off Shift" | text-gray-400 |
291
| Working | ถูก assign เครื่อง + งานเป็น `during work` (`is_working = true`) | 🔵 bg-blue-500 | Clock | "Working" | text-blue-600 |
292
| Available | อยู่ในกะ + ไม่ได้ทำงาน (`is_available = true` AND `is_off_shift = false`) | 🟢 bg-green-500 | CheckCircle | "Available" | text-green-600 |
293

    
294
> Priority: ถ้า Off Shift → แสดง Off Shift เสมอ (แม้จะมีงาน assign อยู่)
295

    
296
## ไฟล์ที่เกี่ยวข้อง
297

    
298
### Frontend (Web)
299

    
300
| ไฟล์ | สิ่งที่ต้องทำ |
301
|---|---|
302
| `EmployeeItem.tsx` | แทนที่ placeholder ด้วย status dot, icon, label จริง |
303
| `assign-maintenance.model.ts` | เพิ่ม `is_available`, `is_working`, `is_off_shift` ใน EmployeeRecordData interface |
304
| `useInteralAssignMaintenanceDrawer.ts` | เพิ่ม filter logic สำหรับ "offShift" โดยใช้ `is_off_shift` |
305

    
306
### Backend (API)
307

    
308
| ไฟล์ | สิ่งที่ต้องทำ |
309
|---|---|
310
| `employee.repository.ts` | แก้ SQL คำนวณ `is_off_shift` — เปลี่ยนจากดู `ms_shift` ตรงๆ เป็นคำนวณผ่าน `ms_shift_pattern` + `ms_shift_pattern_week_detail` + `ms_shift` (รองรับกะหมุนเวียน) |
311
| `employee.model.ts` | เพิ่ม `is_off_shift: boolean` ใน `EmployeeForGroupByWorkType` interface |
312

    
313
## รายการข้อกำหนด (8 ข้อ)
314

    
315
### 1. เพิ่ม/แก้ is_off_shift ใน API (Backend) — รองรับกะหมุนเวียน
316
- แก้ SQL ใน `employee.repository.ts` เพื่อคำนวณ `is_off_shift` จากกะหมุนเวียน
317
- Logic ใหม่:
318
  1. ดึง `work_group_id` ของพนักงาน
319
  2. ดึง `ms_shift_pattern` ที่ active → ได้ `anchor_date`, `cycle_days`
320
  3. คำนวณ `week_no` = floor(((วันนี้ - anchor_date) % cycle_days) / 7) + 1
321
  4. JOIN `ms_shift_pattern_week_detail` ด้วย week_no + work_group_id → ได้ shift_id
322
  5. JOIN `ms_shift` → ได้ start_ts / end_ts
323
  6. เทียบเวลาปัจจุบันกับ start_ts ~ end_ts → ถ้าไม่อยู่ในช่วง = Off Shift
324
- กรณีกะดึก (ข้ามวัน): ใช้ `CURRENT_TIME >= start_ts OR CURRENT_TIME <= end_ts`
325
- เพิ่ม `is_off_shift: boolean` ใน `EmployeeForGroupByWorkType` interface (API model)
326

    
327
### 2. ขยาย EmployeeRecordData Model (Frontend)
328
- เพิ่มฟิลด์ `is_available: boolean`, `is_working: boolean`, `is_off_shift: boolean` ใน interface
329
- ให้ EmployeeItem ใช้งานได้โดยไม่มี type error
330

    
331
### 3. ตรรกะคำนวณสถานะ (Status Derivation Logic)
332
- `is_off_shift = true` → "offShift" (priority สูงสุด)
333
- `is_working = true` → "working"
334
- `is_available = true` AND `is_off_shift = false` → "available"
335
- Working = ช่างที่ถูก assign เครื่องไว้ + สถานะงาน `during work`
336
- Off Shift = ไม่อยู่ในเวลาที่ตั้งไว้ใน `ms_shift`
337

    
338
### 4. วงกลมสีสถานะ (Status Dot)
339
- แสดงจุดกลมทึบ 16x16px ที่มุมขวาบนของการ์ด
340
- สี: เขียว (available), น้ำเงิน (working), เทา (offShift)
341

    
342
### 5. ไอคอนสถานะ (Status Icon)
343
- แสดงใต้ชื่อพนักงาน ฝั่งซ้าย ขนาด 16x16px
344
- ไอคอน: CheckCircle (available), Clock (working), MinusCircle (offShift)
345
- ใช้ lucide-react
346

    
347
### 6. ข้อความสถานะ (Status Label)
348
- แสดงคู่กับไอคอน: "Available", "Working", "Off Shift"
349
- สีตรงกับสถานะ
350

    
351
### 7. เลือกได้ทุกสถานะ — ไม่ล็อก
352
- พนักงานทุกสถานะ (available, working, offShift) กดเลือกได้หมด
353
- เมื่อเลือกแล้วแสดง border-blue-600, bg-blue-50 เหมือนกันทุกสถานะ
354

    
355
### 8. ตัวกรอง Off Shift
356
- เพิ่ม filter logic: เมื่อเลือก "Off Shift" → แสดงเฉพาะพนักงานที่ `is_off_shift = true`
357
- ปุ่มกรองที่มีอยู่แล้ว: All, Available, Working, Off Shift
(1-1/3)