# 供應商管理 - 相關採購訂單查詢功能規格

**系統名稱：** 小東 POS 2.0  
**模組：** 採購管理（Purchasing）  
**功能：** 相關採購訂單查詢（Supplier Related Purchase Orders Query）  
**路由：** `/purchasing/suppliers/new` → Tab：相關採購訂單  
**技術棧：** Angular + Java (Spring Boot)  
**文件版本：** v1.0  
**最後更新：** 2026-04-25

---

## 1. 功能概述

「相關採購訂單」為供應商新增／編輯頁面的第二個 Tab，用於查詢**指定供應商**所對應的所有採購單記錄。

頁面分為兩個區塊：

1. **搜尋篩選區** — 提供採購人員、採購日期、採購編號、採購內容等條件過濾
2. **採購明細列表** — 顯示查詢結果，支援排序、關鍵字快速搜尋，結果範圍限定於當前供應商

---

## 2. 頁面佈局

```
┌──────────────────────────────────────────────────────────────┐
│  ≡  供應商管理                                   👤 andy ▾   │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  供應商新增   [回上一頁（藍底白字）]                          │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  [基本資料]  [相關採購訂單（底線藍色，active）]      │    │
│  │ ────────────────────────────────────────────────    │    │
│  │                                                     │    │
│  │  採購人員 [請選擇  👤]  採購日期 [年/月/日 📅]      │    │
│  │  採購編號 [________]    採購內容 [__________]       │    │
│  │                                                     │    │
│  │                    [  查詢（橘色）  ]               │    │
│  │                                                     │    │
│  │  採購明細  共 {N} 筆        [關鍵字查詢...  ] [Go!] │    │
│  │  ──────────────────────────────────────────────     │    │
│  │  採購編號↑↓ 採購內容 採購日期↑↓ 採購人員↑↓         │    │
│  │  供應商/工廠↑↓ 預計交貨日期 實際交貨日期 狀態       │    │
│  │  ──────────────────────────────────────────────     │    │
│  │  PO240929...  這是一筆...  2008/11/28  Tokyo        │    │
│  │  33           2008/11/28   2008/11/28  [已結案]     │    │
│  └─────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────┘
```

---

## 3. Tab 分頁規格

| Tab 名稱 | 說明 |
|----------|------|
| 基本資料 | 供應商基本欄位（另立規格） |
| 相關採購訂單 | 本規格範疇，active 時底部藍色底線 |

- Active Tab 樣式：文字藍色（`#1976D2`）、底部藍色底線
- Inactive Tab 樣式：文字灰色（`#616161`）、無底線
- Tab 切換**不重新載入頁面**，保留各 Tab 的搜尋條件與捲軸位置

---

## 4. 搜尋篩選區

### 4.1 區塊標題

無獨立區塊標題，篩選欄位與查詢按鈕直接呈現於 Tab 內容頂部。

### 4.2 篩選欄位定義

表單採 **4 欄 Grid 佈局**，分 2 列排列。

| 列 | 欄位標籤 | 欄位 ID | 元件類型 | 必填 | 查詢方式 | 備註 |
|----|----------|---------|----------|------|----------|------|
| Row 1 | 採購人員 | `purchaseUser` | select dropdown + 👤 icon | ❌ | 完全比對（員工代碼） | 從員工資料表 API 動態載入，見 §4.3 |
| Row 1 | 採購日期 | `purchaseDate` | date picker | ❌ | 完全比對（日期） | 格式：`yyyy/MM/dd`，placeholder：`年/月/日` |
| Row 1 | 採購編號 | `purchaseCode` | text input | ❌ | LIKE `%value%` | 最多 30 字元 |
| Row 1 | 採購內容 | `purchaseContent` | text input | ❌ | LIKE `%value%` | 最多 200 字元 |

### 4.3 採購人員選項（`purchaseUser`）— 員工 API

從員工資料表 API 動態載入：`GET /api/hr/employees/active`

```
請選擇（預設，值為空）
{員工姓名} → {員工代碼}  （依 API 回傳動態渲染）
```

- 下拉選單顯示**員工姓名**，實際送出查詢值為**員工代碼（ID）**
- 僅載入**在職（active）**員工
- 右側 👤 icon：點擊等同展開下拉選單（或未來擴充為人員搜尋 Popup）

### 4.4 採購日期元件

| 屬性 | 說明 |
|------|------|
| 元件類型 | HTML5 `<input type="date">` 或 Angular Material Datepicker |
| placeholder | `年/月/日` |
| 日期格式 | `yyyy/MM/dd`（顯示）/ `yyyy-MM-dd`（API 傳送） |
| 清除方式 | 點擊 X 圖示或直接清空 |

### 4.5 查詢按鈕

| 按鈕文字 | 顏色 | 位置 | 行為 |
|----------|------|------|------|
| 查詢 | 橘色（`#FF9800`） | 第 2 列置中 | 以目前篩選條件 + 當前供應商 ID 呼叫查詢 API，重設分頁至第 1 頁後重新載入列表 |

### 4.6 互動細節

- 任意文字欄位按 `Enter` 鍵，等同點擊「查詢」按鈕
- 日期與下拉選單不自動觸發查詢
- Tab 切換至「相關採購訂單」時，自動以**空條件**執行初始查詢（帶入 `supplierId`），載入該供應商所有採購單

---

## 5. 採購明細列表

### 5.1 區塊標題與工具列

| 位置 | 內容 |
|------|------|
| 左側 | `採購明細`（藍色粗體）+ `共 {total} 筆`（灰色小字） |
| 右側 | 關鍵字快速搜尋輸入框（placeholder：`關鍵字查詢...`）+ `Go!` 按鈕（深灰色） |

**關鍵字快速搜尋行為：**
- 以關鍵字對「採購編號」、「採購內容」同時進行 LIKE 搜尋
- 關鍵字搜尋與上方篩選條件**合併**套用（AND 條件）
- 按 `Enter` 或點擊 `Go!` 觸發搜尋，**不即時搜尋**

### 5.2 表格欄位定義

| 欄位標題 | 資料欄位 | 型別 | 可排序 | 說明 |
|----------|----------|------|--------|------|
| 採購編號 | `purchaseCode` | string | ✅（預設升冪） | 如 `PO240929091520` |
| 採購內容 | `purchaseContent` | string | ❌ | 長文字截斷顯示（超過 30 字元顯示 `...`），Hover 顯示 Tooltip 完整內容 |
| 採購日期 | `purchaseDate` | date | ✅ | 格式：`yyyy/MM/dd` |
| 採購人員 | `purchaseUserName` | string | ✅ | 顯示人員姓名 |
| 供應商/工廠 | `factoryName` | string | ✅ | 供應商底下的工廠名稱或代號 |
| 預計交貨日期 | `expectedDeliveryDate` | date | ❌ | 格式：`yyyy/MM/dd` |
| 實際交貨日期 | `actualDeliveryDate` | date | ❌ | 格式：`yyyy/MM/dd`；未交貨時顯示 `—` |
| 狀態 | `orderStatus` | enum | ❌ | Badge 元件，樣式見 §5.3 |

### 5.3 狀態 Badge 樣式

| 狀態值 | 顯示文字 | 背景色 | 文字色 |
|--------|----------|--------|--------|
| `CLOSED` | 已結案 | `#4CAF50`（綠） | `#FFFFFF` |
| `IN_PROGRESS` | 進行中 | `#1976D2`（藍） | `#FFFFFF` |
| `PENDING` | 待處理 | `#FF9800`（橘） | `#FFFFFF` |
| `CANCELLED` | 已取消 | `#9E9E9E`（灰） | `#FFFFFF` |

> 截圖中狀態欄部分列無 Badge（空白），表示該欄位資料為空或狀態未設定，顯示空白即可

### 5.4 排序行為

- 欄位標題旁顯示 `↑↓` 圖示表示可排序
- 點擊一次：升冪（`↑`）；再點擊：降冪（`↓`）；第三次：取消排序（恢復預設）
- 排序透過 API 參數傳遞（`sortField`、`sortDirection`），由後端處理
- 預設排序：`purchaseCode ASC`

### 5.5 分頁規格

- 預設每頁顯示：**20 筆**（截圖顯示「共 10 筆」，不足一頁時不顯示分頁元件）
- 分頁元件於資料超過 1 頁時才顯示，位於列表下方
- 顯示格式：`共 {total} 筆　第 {current} / {totalPages} 頁`
- 每頁筆數可切換：`10 / 20 / 50`（下拉選單）

### 5.6 垂直捲軸

- 列表區塊高度固定（截圖可見右側捲軸），超出高度時內部捲動
- CSS：`overflow-y: auto; max-height: calc(100vh - 400px)`

---

## 6. API 規格

### 6.1 查詢指定供應商的採購訂單列表

```
GET /api/purchasing/suppliers/{supplierId}/purchase-orders
```

**Path Parameter：**

| 參數名稱 | 型別 | 必填 | 說明 |
|----------|------|------|------|
| `supplierId` | string | ✅ | 供應商 UUID，由頁面路由帶入 |

**Query Parameters：**

| 參數名稱 | 型別 | 必填 | 說明 |
|----------|------|------|------|
| `purchaseUser` | string | ❌ | 採購人員代碼（完全比對） |
| `purchaseDate` | string | ❌ | 採購日期，格式：`yyyy-MM-dd` |
| `purchaseCode` | string | ❌ | 採購編號（LIKE） |
| `purchaseContent` | string | ❌ | 採購內容（LIKE） |
| `keyword` | string | ❌ | 關鍵字（同時搜尋採購編號、採購內容） |
| `sortField` | string | ❌ | 排序欄位，預設 `purchaseCode` |
| `sortDirection` | string | ❌ | `ASC` / `DESC`，預設 `ASC` |
| `page` | integer | ❌ | 頁碼，從 1 開始，預設 `1` |
| `pageSize` | integer | ❌ | 每頁筆數，預設 `20` |

**Response 200：**

```json
{
  "total": 10,
  "page": 1,
  "pageSize": 20,
  "data": [
    {
      "purchaseOrderId": "uuid-string",
      "purchaseCode": "PO240929091520",
      "purchaseContent": "這是一筆採購內容",
      "purchaseDate": "2008/11/28",
      "purchaseUserId": "EMP001",
      "purchaseUserName": "Tokyo",
      "factoryId": "F033",
      "factoryName": "33",
      "expectedDeliveryDate": "2008/11/28",
      "actualDeliveryDate": "2008/11/28",
      "orderStatus": "CLOSED"
    }
  ]
}
```

**Response 404（供應商不存在）：**

```json
{
  "success": false,
  "message": "找不到指定的供應商"
}
```

### 6.2 取得在職員工列表（採購人員下拉選單）

```
GET /api/hr/employees/active
```

（同供應商新增規格，頁面初始化時呼叫）

**Response 200：**

```json
[
  {
    "employeeId": "EMP001",
    "employeeName": "王小明",
    "department": "採購部"
  }
]
```

---

## 7. Angular 元件規格

### 7.1 元件結構

```
purchasing/
└── suppliers/
    ├── suppliers.module.ts
    ├── suppliers-routing.module.ts
    ├── supplier-create/
    │   ├── supplier-create.component.ts       ← Tab 切換控制
    │   ├── supplier-create.component.html
    │   └── supplier-create.component.scss
    ├── supplier-basic-info/                   ← 基本資料 Tab（另立規格）
    │   ├── supplier-basic-info.component.ts
    │   ├── supplier-basic-info.component.html
    │   └── supplier-basic-info.component.scss
    ├── supplier-purchase-orders/              ← 相關採購訂單 Tab（本規格）
    │   ├── supplier-purchase-orders.component.ts
    │   ├── supplier-purchase-orders.component.html
    │   └── supplier-purchase-orders.component.scss
    └── services/
        ├── supplier.service.ts
        ├── supplier-purchase-order.service.ts ← 本規格新增
        └── employee.service.ts
```

### 7.2 SupplierPurchaseOrdersComponent

```typescript
// supplier-purchase-orders.component.ts
@Component({
  selector: 'app-supplier-purchase-orders',
  templateUrl: './supplier-purchase-orders.component.html',
  styleUrls: ['./supplier-purchase-orders.component.scss']
})
export class SupplierPurchaseOrdersComponent implements OnInit {

  @Input() supplierId!: string;  // 由父元件（supplier-create）傳入

  searchForm: FormGroup;
  purchaseOrders: PurchaseOrder[] = [];
  total = 0;
  currentPage = 1;
  pageSize = 20;
  sortField = 'purchaseCode';
  sortDirection: 'ASC' | 'DESC' = 'ASC';
  keyword = '';
  isLoading = false;

  // 下拉選單資料
  employees: EmployeeOption[] = [];

  ngOnInit(): void {
    this.initForm();
    this.loadEmployees();
    this.loadPurchaseOrders();   // 初始載入：帶入 supplierId，其餘條件為空
  }

  initForm(): void { ... }
  loadEmployees(): void { ... }
  loadPurchaseOrders(): void { ... }   // 呼叫 Query API
  onSearch(): void { ... }             // 重設分頁至第 1 頁後重新查詢
  onReset(): void { ... }              // 清空篩選條件
  onSort(field: string): void { ... }
  onKeywordSearch(): void { ... }
  onPageChange(page: number): void { ... }
}
```

### 7.3 父元件 Tab 控制（SupplierCreateComponent）

```typescript
// supplier-create.component.ts
export class SupplierCreateComponent implements OnInit {
  activeTab: 'basic' | 'purchaseOrders' = 'basic';
  supplierId: string = '';           // 路由參數或新增後取得的 ID

  switchTab(tab: 'basic' | 'purchaseOrders'): void {
    this.activeTab = tab;
  }
}
```

```html
<!-- supplier-create.component.html -->
<div class="tabs">
  <button [class.active]="activeTab === 'basic'" (click)="switchTab('basic')">基本資料</button>
  <button [class.active]="activeTab === 'purchaseOrders'" (click)="switchTab('purchaseOrders')">相關採購訂單</button>
</div>

<app-supplier-basic-info *ngIf="activeTab === 'basic'"></app-supplier-basic-info>
<app-supplier-purchase-orders
  *ngIf="activeTab === 'purchaseOrders'"
  [supplierId]="supplierId">
</app-supplier-purchase-orders>
```

### 7.4 表單初始化（ReactiveFormsModule）

```typescript
initForm(): void {
  this.searchForm = this.fb.group({
    purchaseUser:    [''],
    purchaseDate:    [''],
    purchaseCode:    ['', Validators.maxLength(30)],
    purchaseContent: ['', Validators.maxLength(200)],
  });
}
```

### 7.5 查詢流程（loadPurchaseOrders）

```typescript
loadPurchaseOrders(): void {
  this.isLoading = true;

  const params: PurchaseOrderQueryParams = {
    ...this.searchForm.value,
    keyword: this.keyword,
    sortField: this.sortField,
    sortDirection: this.sortDirection,
    page: this.currentPage,
    pageSize: this.pageSize,
  };

  this.purchaseOrderService
    .getSupplierPurchaseOrders(this.supplierId, params)
    .subscribe({
      next: (res) => {
        this.purchaseOrders = res.data;
        this.total = res.total;
        this.isLoading = false;
      },
      error: () => {
        this.toastService.error('查詢失敗，請稍後再試');
        this.isLoading = false;
      }
    });
}

onSearch(): void {
  this.currentPage = 1;
  this.loadPurchaseOrders();
}
```

### 7.6 SupplierPurchaseOrderService

```typescript
// supplier-purchase-order.service.ts
@Injectable({ providedIn: 'root' })
export class SupplierPurchaseOrderService {
  private baseUrl = '/api/purchasing/suppliers';

  getSupplierPurchaseOrders(
    supplierId: string,
    params: PurchaseOrderQueryParams
  ): Observable<PurchaseOrderListResponse> {
    return this.http.get<PurchaseOrderListResponse>(
      `${this.baseUrl}/${supplierId}/purchase-orders`,
      { params: { ...params } }
    );
  }
}
```

### 7.7 資料模型（TypeScript Interface）

```typescript
// 採購訂單
export interface PurchaseOrder {
  purchaseOrderId: string;
  purchaseCode: string;
  purchaseContent: string;
  purchaseDate: string;           // yyyy/MM/dd
  purchaseUserId: string;
  purchaseUserName: string;
  factoryId: string;
  factoryName: string;
  expectedDeliveryDate: string;   // yyyy/MM/dd
  actualDeliveryDate: string | null;
  orderStatus: 'CLOSED' | 'IN_PROGRESS' | 'PENDING' | 'CANCELLED' | null;
}

// 列表 Response
export interface PurchaseOrderListResponse {
  total: number;
  page: number;
  pageSize: number;
  data: PurchaseOrder[];
}

// 查詢參數
export interface PurchaseOrderQueryParams {
  purchaseUser?: string;
  purchaseDate?: string;          // yyyy-MM-dd
  purchaseCode?: string;
  purchaseContent?: string;
  keyword?: string;
  sortField?: string;
  sortDirection?: 'ASC' | 'DESC';
  page?: number;
  pageSize?: number;
}

// 員工選項（共用）
export interface EmployeeOption {
  employeeId: string;
  employeeName: string;
  department: string;
}
```

---

## 8. 後端 Java 規格

### 8.1 Controller（新增端點）

```java
@RestController
@RequestMapping("/api/purchasing/suppliers")
public class SupplierController {

    // 既有端點...

    // 新增：查詢指定供應商的採購訂單
    @GetMapping("/{supplierId}/purchase-orders")
    public ResponseEntity<PageResult<PurchaseOrderDTO>> getSupplierPurchaseOrders(
        @PathVariable String supplierId,
        @RequestParam(required = false) String purchaseUser,
        @RequestParam(required = false) String purchaseDate,
        @RequestParam(required = false) String purchaseCode,
        @RequestParam(required = false) String purchaseContent,
        @RequestParam(required = false) String keyword,
        @RequestParam(defaultValue = "purchaseCode") String sortField,
        @RequestParam(defaultValue = "ASC") String sortDirection,
        @RequestParam(defaultValue = "1") int page,
        @RequestParam(defaultValue = "20") int pageSize
    ) { ... }
}
```

### 8.2 DTO

```java
public class PurchaseOrderDTO {
    private String purchaseOrderId;
    private String purchaseCode;
    private String purchaseContent;
    private String purchaseDate;           // 格式：yyyy/MM/dd
    private String purchaseUserId;
    private String purchaseUserName;
    private String factoryId;
    private String factoryName;
    private String expectedDeliveryDate;   // 格式：yyyy/MM/dd
    private String actualDeliveryDate;     // 可為 null
    private String orderStatus;            // CLOSED / IN_PROGRESS / PENDING / CANCELLED
}
```

### 8.3 Service 查詢邏輯

```java
// SupplierPurchaseOrderService.java
public PageResult<PurchaseOrderDTO> getSupplierPurchaseOrders(
    String supplierId,
    PurchaseOrderQueryRequest request
) {
    // 1. 確認 supplierId 存在，否則拋出 NotFoundException
    // 2. 以 supplierId 為主要過濾條件（WHERE supplier_id = ?）
    // 3. 附加可選條件：
    //    - purchaseUser  → AND purchase_user_id = ?
    //    - purchaseDate  → AND DATE(purchase_date) = ?
    //    - purchaseCode  → AND purchase_code LIKE ?
    //    - purchaseContent → AND purchase_content LIKE ?
    //    - keyword       → AND (purchase_code LIKE ? OR purchase_content LIKE ?)
    // 4. 套用排序（sortField、sortDirection 白名單驗證防 SQL Injection）
    // 5. 套用分頁（LIMIT / OFFSET）
    // 6. 回傳 PageResult<PurchaseOrderDTO>
}
```

### 8.4 排序白名單

```java
private static final Set<String> ALLOWED_SORT_FIELDS = Set.of(
    "purchaseCode",
    "purchaseDate",
    "purchaseUserName",
    "factoryName"
);

// 使用前驗證
if (!ALLOWED_SORT_FIELDS.contains(sortField)) {
    sortField = "purchaseCode";
}
```

### 8.5 分頁回傳格式（共用）

```java
public class PageResult<T> {
    private int total;
    private int page;
    private int pageSize;
    private List<T> data;
}
```

---

## 9. 使用者互動與錯誤處理

### 9.1 初始載入行為

```
Tab「相關採購訂單」被點擊
  ├─ 若員工清單尚未載入 → 呼叫 GET /api/hr/employees/active
  └─ 以空條件呼叫 GET /api/purchasing/suppliers/{supplierId}/purchase-orders
       → 載入該供應商所有採購單（第 1 頁，依 purchaseCode ASC）
```

### 9.2 Toast 通知

| 情境 | 提示類型 | 提示內容 |
|------|----------|----------|
| 查詢 API 失敗 | Toast Error（紅） | `查詢失敗，請稍後再試` |
| 供應商不存在（404） | Toast Error（紅） | `找不到指定的供應商` |
| 載入員工清單失敗 | Toast Warning（橘） | `無法載入人員資料，請重新整理頁面` |

### 9.3 Loading 狀態

| 情境 | 行為 |
|------|------|
| 查詢 API 執行中 | 列表區顯示 Loading Spinner，查詢按鈕 disabled |
| 員工清單載入中 | 採購人員下拉顯示 `載入中...` 並 disabled |

### 9.4 空資料狀態

當查詢結果為 0 筆時，列表區顯示：

```
（圖示）
查無符合條件的採購訂單
```

---

## 10. 驗證規則

| 欄位 | 規則 |
|------|------|
| 採購編號（篩選） | 若有輸入，最多 30 字元 |
| 採購內容（篩選） | 若有輸入，最多 200 字元 |
| 採購日期（篩選） | 日期格式驗證，不允許非法日期 |
| 所有篩選欄位 | 無必填限制，允許空條件查全部 |

---

## 11. 其他備註

- 查詢範圍**固定為當前供應商**，後端以 `supplierId` 為主要過濾條件，前端無法跨供應商查詢
- 「供應商/工廠」欄位（`factoryName`）顯示截圖中為數字代號（如 `33`、`47`），實際顯示內容依資料庫欄位而定
- 狀態欄位部分列為空白（截圖中第一列無 Badge），後端回傳 `null` 時前端顯示空白，不顯示任何 Badge
- 採購內容欄截斷規則：超過 30 字元顯示 `...`，Hover 顯示完整內容（`title` attribute 或 Angular Tooltip）
- 此 Tab 僅提供**唯讀查詢**，不包含新增、編輯、刪除採購單的功能
- 員工清單（`GET /api/hr/employees/active`）可與基本資料 Tab 共用快取，避免重複呼叫
