專案

一般

配置概況

Web #86 » 供應商管理 - 相關採購訂單查詢功能規格-spec.md

Andy Huang, 2026-04-25 23:02

 

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

系統名稱: 小東 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 參數傳遞(sortFieldsortDirection),由後端處理
  • 預設排序: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:

{
  "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(供應商不存在):

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

6.2 取得在職員工列表(採購人員下拉選單)

GET /api/hr/employees/active

(同供應商新增規格,頁面初始化時呼叫)

Response 200:

[
  {
    "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

// 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)

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

  switchTab(tab: 'basic' | 'purchaseOrders'): void {
    this.activeTab = tab;
  }
}
<!-- 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)

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

7.5 查詢流程(loadPurchaseOrders)

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

// 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)

// 採購訂單
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(新增端點)

@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

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 查詢邏輯

// 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 排序白名單

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

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

8.5 分頁回傳格式(共用)

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)顯示截圖中為數字代號(如 3347),實際顯示內容依資料庫欄位而定
  • 狀態欄位部分列為空白(截圖中第一列無 Badge),後端回傳 null 時前端顯示空白,不顯示任何 Badge
  • 採購內容欄截斷規則:超過 30 字元顯示 ...,Hover 顯示完整內容(title attribute 或 Angular Tooltip)
  • 此 Tab 僅提供唯讀查詢,不包含新增、編輯、刪除採購單的功能
  • 員工清單(GET /api/hr/employees/active)可與基本資料 Tab 共用快取,避免重複呼叫
(2-2/2)