# Implementation Plan — EPIC-002 / web-kingston

**Repo:** web-kingston
**Ngày:** 2026-05-15
**Skill:** `/implement-feature EPIC-002 web`

---

## Tổng quan

Thêm auto-reload 30 giây + nút "Làm mới" (cooldown 10s) vào Worker Panel.

**Chỉ 2 files thay đổi:**
1. Tạo mới `usePanelAutoRefresh.ts`
2. Sửa `ListTicketPanelLayout.tsx`

Không thay đổi API, DI container, domain, hay infrastructure.

---

## Step 1 — Tạo hook `usePanelAutoRefresh`

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

**Tạo mới file với nội dung:**

```typescript
import { useEffect, useRef, useState } from "react";

const COOLDOWN_KEY = "panel_refresh_cooldown_until";
const AUTO_RELOAD_MS = 30_000;
const COOLDOWN_MS = 10_000;

export interface UsePanelAutoRefreshReturn {
  cooldownRemaining: number;
  handleManualRefresh: () => void;
  lastReloadedAt: Date;
}

export function usePanelAutoRefresh(): UsePanelAutoRefreshReturn {
  const lastReloadedAt = useRef(new Date()).current;

  const getInitialCooldown = (): number => {
    try {
      const stored = localStorage.getItem(COOLDOWN_KEY);
      if (!stored) return 0;
      const remaining = Math.ceil((Number(stored) - Date.now()) / 1000);
      return remaining > 0 ? remaining : 0;
    } catch {
      return 0;
    }
  };

  const [cooldownRemaining, setCooldownRemaining] = useState<number>(
    getInitialCooldown,
  );

  // Auto-reload sau 30 giây
  useEffect(() => {
    const timer = setTimeout(() => window.location.reload(), AUTO_RELOAD_MS);
    return () => clearTimeout(timer);
  }, []);

  // Đếm ngược cooldown mỗi giây
  useEffect(() => {
    if (cooldownRemaining <= 0) return;
    const interval = setInterval(() => {
      setCooldownRemaining((prev) => {
        if (prev <= 1) {
          clearInterval(interval);
          try {
            localStorage.removeItem(COOLDOWN_KEY);
          } catch {}
          return 0;
        }
        return prev - 1;
      });
    }, 1000);
    return () => clearInterval(interval);
  }, [cooldownRemaining > 0]);

  const handleManualRefresh = () => {
    if (cooldownRemaining > 0) return;
    try {
      localStorage.setItem(
        COOLDOWN_KEY,
        String(Date.now() + COOLDOWN_MS),
      );
    } catch {}
    window.location.reload();
  };

  return { cooldownRemaining, handleManualRefresh, lastReloadedAt };
}
```

**Kiểm tra sau khi tạo:**
- File tồn tại tại đúng path
- Không có lỗi TypeScript (`bun type-check` từ `packages/main-app`)

---

## Step 2 — Sửa `ListTicketPanelLayout.tsx`

**File:** `packages/main-app/src/clean-architecture/presentation/components/pages/panel-worker/ListTicketPanel/ListTicketPanelLayout.tsx`

### 2a. Thêm import

Thêm vào block import hiện có:

```typescript
import { usePanelAutoRefresh } from "clean-architecture/presentation/hooks/usePanelAutoRefresh";
```

Và thêm `useRef` vào import từ React nếu chưa có (hook đã dùng `useRef` nội bộ nên không cần ở đây).

### 2b. Gọi hook trong component

Sau dòng `const { updateItem, getItem } = useLocalStorage();`, thêm:

```typescript
const { cooldownRemaining, handleManualRefresh, lastReloadedAt } =
  usePanelAutoRefresh();
```

### 2c. Thêm helper tính "Cập nhật: X trước"

Thêm `useState` để force re-render mỗi giây cho "Cập nhật: Xs trước":

```typescript
const [, setTick] = useState(0);
useEffect(() => {
  const id = setInterval(() => setTick((n) => n + 1), 1000);
  return () => clearInterval(id);
}, []);

const secondsSinceReload = Math.floor(
  (Date.now() - lastReloadedAt.getTime()) / 1000,
);
const lastReloadLabel =
  secondsSinceReload < 5
    ? "vừa xong"
    : `${secondsSinceReload}s trước`;
```

### 2d. Thêm nút "Làm mới" vào header

Trong `<div className="sm:ml-auto flex gap-2">` (block chứa các Button hiện có), thêm nút "Làm mới" **trước** nút Logout:

```tsx
<Button
  color="secondary"
  variant="outlined"
  disabled={cooldownRemaining > 0}
  onClick={handleManualRefresh}
>
  {cooldownRemaining > 0
    ? `Làm mới (${cooldownRemaining}s)`
    : "Làm mới"}
</Button>
```

Và thêm label "Cập nhật" trước block button (trong cùng flex row):

```tsx
<span className="text-sm text-muted-foreground hidden sm:inline self-center">
  Cập nhật: {lastReloadLabel}
</span>
```

**Kết quả header sau khi sửa:**
```
[Avatar] [Tên công nhân / Kỹ năng]    Cập nhật: 12s trước  [Làm mới]  [Đăng xuất]
```

---

## Step 3 — Kiểm tra

```bash
# Từ packages/main-app
bun type-check          # Không có lỗi TypeScript
bun lint                # Không có ESLint warning
```

**Manual test checklist:**
- [ ] Mở panel, đợi 30 giây → trang tự reload (có thể thấy flash)
- [ ] Nhấn "Làm mới" → trang reload ngay, nút disable + đếm ngược 10s
- [ ] Trong lúc cooldown, thử nhấn nút → không có gì xảy ra (disabled)
- [ ] Sau 10s → nút enabled trở lại với label "Làm mới"
- [ ] Label "Cập nhật: Xs trước" tăng mỗi giây, reset về "vừa xong" sau reload

---

## Định nghĩa Done

- [ ] `usePanelAutoRefresh.ts` được tạo
- [ ] `ListTicketPanelLayout.tsx` có nút "Làm mới" và label "Cập nhật"
- [ ] `bun type-check` pass
- [ ] `bun lint` pass
- [ ] Manual test checklist hoàn thành
