Web #86 » 供應商管理 - 相關採購訂單查詢功能規格-spec.md
供應商管理 - 相關採購訂單查詢功能規格
系統名稱: 小東 POS 2.0
模組: 採購管理(Purchasing)
功能: 相關採購訂單查詢(Supplier Related Purchase Orders Query)
路由: /purchasing/suppliers/new → Tab:相關採購訂單
技術棧: Angular + Java (Spring Boot)
文件版本: v1.0
最後更新: 2026-04-25
1. 功能概述
「相關採購訂單」為供應商新增/編輯頁面的第二個 Tab,用於查詢指定供應商所對應的所有採購單記錄。
頁面分為兩個區塊:
- 搜尋篩選區 — 提供採購人員、採購日期、採購編號、採購內容等條件過濾
- 採購明細列表 — 顯示查詢結果,支援排序、關鍵字快速搜尋,結果範圍限定於當前供應商
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:
{
"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)顯示截圖中為數字代號(如33、47),實際顯示內容依資料庫欄位而定 - 狀態欄位部分列為空白(截圖中第一列無 Badge),後端回傳
null時前端顯示空白,不顯示任何 Badge - 採購內容欄截斷規則:超過 30 字元顯示
...,Hover 顯示完整內容(titleattribute 或 Angular Tooltip) - 此 Tab 僅提供唯讀查詢,不包含新增、編輯、刪除採購單的功能
- 員工清單(
GET /api/hr/employees/active)可與基本資料 Tab 共用快取,避免重複呼叫