# Tech Design — EPIC-002: Panel Tự Động Nhận Nhiệm Vụ Mới

**Epic:** EPIC-002
**Phiên bản:** 1.0
**Ngày:** 2026-05-15
**Author:** Nghĩa (TL)
**Repos ảnh hưởng:** `web-kingston` only

---

## Summary

Thêm tính năng tự động reload trang và nút reload thủ công vào `ListTicketPanelLayout`. Sau 30 giây, trang tự gọi `window.location.reload()`. Nút "Làm mới" cho phép công nhân reload ngay lập tức, sau đó bị disable 10 giây (cooldown được lưu vào `localStorage` để survive qua page reload). Thay đổi hoàn toàn nằm trong `web-kingston` — không chạm `api-kingston` hay `app-kingston`.

---

## Architecture

Codebase theo Clean Architecture. Tính năng này **chỉ nằm ở Presentation layer** — không cần thêm use case, repository, hay domain entity mới.

```
Presentation layer
  └── ListTicketPanelLayout.tsx         ← thêm refresh button + "Cập nhật: Xs trước"
        └── usePanelAutoRefresh (hook)  ← NEW: timer + cooldown logic
              └── localStorage          ← persist cooldown timestamp across reloads
```

### Key design choices

**`window.location.reload()` thay vì `queryClient.refetchQueries()`**

PRD yêu cầu "trang tự reload" — full page reload. Đây là lựa chọn đơn giản nhất, không cần thêm state management phức tạp. Toàn bộ React Query cache được xóa sạch sau reload → dữ liệu luôn fresh.

**`localStorage` cho cooldown timestamp**

Page reload xóa React component state. Nếu dùng `useState` để track cooldown, sau khi nhấn nút và page reload, cooldown biến mất. Dùng `localStorage` để persist `panel_refresh_cooldown_until` (epoch ms) qua reload — component đọc lại khi mount và tính countdown còn lại.

**`setTimeout` thay vì `setInterval` cho auto-reload**

Vì `window.location.reload()` reset toàn bộ JS context, `setInterval` không có ý nghĩa — timer tự động bị hủy. Dùng `setTimeout(reload, 30_000)` trong `useEffect` là đủ. Cleanup trong `useEffect` return để tránh memory leak khi component unmount (VD: user đăng xuất trước khi hết 30s).

---

## API / Interface Contract

Không có thay đổi API. Tính năng dùng lại toàn bộ endpoints hiện tại — page reload tự gọi lại các React Query hooks đang có.

---

## Data Model

Không có schema mới. Một entry trong `localStorage`:

| Key | Value | Ý nghĩa |
|-----|-------|---------|
| `panel_refresh_cooldown_until` | epoch ms (number as string) | Thời điểm cooldown kết thúc. Đọc khi mount để tính countdown còn lại. Xóa khi countdown hết. |

---

## Component Design

### Hook mới: `usePanelAutoRefresh`

**File:** `src/clean-architecture/presentation/hooks/usePanelAutoRefresh.ts`

```typescript
export interface UsePanelAutoRefreshReturn {
  cooldownRemaining: number;   // giây còn lại, 0 = enabled
  handleManualRefresh: () => void;
  lastReloadedAt: Date;        // thời điểm trang được load (= new Date() khi mount)
}
```

**Logic:**

1. **Auto-reload:** `useEffect` đặt `setTimeout(() => window.location.reload(), 30_000)`. Return cleanup để clear timeout khi unmount.

2. **Cooldown:** Khi mount, đọc `localStorage.getItem('panel_refresh_cooldown_until')`. Nếu còn trong tương lai → tính `remaining = Math.ceil((stored - Date.now()) / 1000)`. `useState(remaining)` và `useEffect` với `setInterval(tick, 1000)` đếm ngược.

3. **Manual refresh:** `handleManualRefresh` — kiểm tra `cooldownRemaining > 0` → bail out. Nếu không: lưu `Date.now() + 10_000` vào `localStorage`, gọi `window.location.reload()`.

4. **lastReloadedAt:** `useState(new Date())` — set một lần khi mount. Dùng để tính "Cập nhật: Xs trước".

### Thay đổi: `ListTicketPanelLayout.tsx`

Thêm vào header hiện có (sau avatar/tên công nhân, trước nút Logout):

```
[Cập nhật: Xs trước]  [Làm mới] hoặc [Làm mới (8s)] khi disabled
```

- Import `usePanelAutoRefresh` và gọi hook
- Hiển thị `lastReloadedAt` dạng relative time ("vừa xong" / "Xs trước")
- Render nút `<Button>` với `disabled={cooldownRemaining > 0}` và label động

---

## Dependency Wiring / Registration

Không có DI registration mới. `usePanelAutoRefresh` là một React hook thuần — không cần Inversify container.

---

## Kingston Wiring Checklist

**web-kingston:**
- `di/types.ts` — không thay đổi
- `di/panelContainer.ts` — không thay đổi
- Không có token mới

---

## Non-Functional Design

**Performance:**
- `setInterval` 1 giây cho countdown: overhead không đáng kể (~0.01ms/tick)
- `localStorage` read/write: synchronous nhưng < 1ms, chỉ xảy ra khi mount/click

**Reliability:**
- `useEffect` cleanup đảm bảo `setTimeout` bị cancel khi component unmount (VD: user logout)
- Nếu `localStorage` bị lỗi (private browsing): bắt exception, cooldown gracefully về 0

**Security:**
- Không có input user, không có server interaction mới → không có surface attack mới

---

## Rollout & Reversibility

**Direct rollout** — deploy `web-kingston`. Không cần thay đổi `api-kingston`.

**Rollback:** Deploy lại `web-kingston` phiên bản trước. Không có migration data cần revert. `localStorage` entry `panel_refresh_cooldown_until` sẽ tự expire hoặc bị ignore bởi version cũ.

---

## File / Module Impact

| File | Trạng thái | Lý do |
|------|-----------|-------|
| `src/clean-architecture/presentation/hooks/usePanelAutoRefresh.ts` | **NEW** | Hook encapsulate toàn bộ auto-reload + cooldown logic |
| `src/clean-architecture/presentation/components/pages/panel-worker/ListTicketPanel/ListTicketPanelLayout.tsx` | **MODIFIED** | Thêm hook call + refresh button + "Cập nhật: Xs trước" vào header |

Chỉ 2 files. Không thay đổi domain, application, infrastructure, DI.

---

## Risks & Technical Debt

| Rủi ro | Mitigation |
|--------|------------|
| Auto-reload ngắt giữa chừng khi công nhân đang nhập TicketLog | Acceptable — PRD AC05 đã confirm không có exception cho interaction đang dở |
| `localStorage` không available (private mode) | Try-catch, fallback: cooldown không persist, button luôn enabled sau reload |
| 30s reload gây nhấp nháy/flash trên màn hình xưởng | Acceptable với approach này; nếu sau này cần mượt hơn → refactor sang `refetchInterval` |
