Web #85 » 供應商新增-spec.md
供應商管理 - 供應商基本資料新增功能規格
系統名稱: 小東 POS 2.0
模組: 採購管理(Purchasing)
功能: 供應商新增(Supplier Create)
路由: /purchasing/suppliers/new
技術棧: Angular + Java (Spring Boot)
文件版本: v1.0
最後更新: 2026-04-25
1. 功能概述
供應商新增頁面提供使用者建立新供應商基本資料的入口,包含兩個分頁:
- 基本資料 — 填寫供應商基本欄位(本規格範疇)
- 相關採購訂單 — 顯示該供應商的採購訂單(新增時為空,本規格不包含)
供應商編號由系統自動產生,使用者無法手動輸入。填寫完成後點擊「儲存」呼叫 Insert API 建立資料,成功後返回列表頁。
2. 頁面佈局
┌──────────────────────────────────────────────────────────────┐
│ ≡ 供應商管理 👤 andy ▾ │
├──────────────────────────────────────────────────────────────┤
│ │
│ 供應商新增 [回上一頁(藍底白字)] │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [基本資料(底線藍色,active)] [相關採購訂單] │ │
│ │ ──────────────────────────────────────────────── │ │
│ │ │ │
│ │ 供應商編號 * [系統自動產生,唯讀] │ │
│ │ 供應商名稱 * [________________] │ │
│ │ 聯絡人姓名 [________________] │ │
│ │ 統一編號 [________________] │ │
│ │ │ │
│ │ 公司地址 [________________] │ │
│ │ 服務類別 [請選擇 ▾] │ │
│ │ 負責業務 * [請選擇 👤] │ │
│ │ 付款條件 [請選擇 ▾] │ │
│ │ │ │
│ │ 聯絡電話 [________________] │ │
│ │ 備註說明 [________________] │ │
│ │ 風險評估 [請選擇 ▾] │ │
│ │ 供應商狀態 * [請選擇 ▾] │ │
│ │ │ │
│ │ [ 儲存(橘色) ] │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
3. 頁面標題區
3.1 標題
- 文字:
供應商新增 - 樣式:藍色粗體(
color: #1976D2; font-weight: bold)
3.2 回上一頁按鈕
| 屬性 | 值 |
|---|---|
| 文字 | 回上一頁 |
| 樣式 | 藍底白字,圓角(background: #1976D2; color: #FFF; border-radius: 20px) |
| 行為 | 導頁至 /purchasing/suppliers,不儲存任何資料 |
4. Tab 分頁規格
| Tab 名稱 | 路由片段 | 說明 |
|---|---|---|
| 基本資料 | (預設,active) | 本規格範疇 |
| 相關採購訂單 | — | 新增時 Tab 存在但內容為空,不可操作 |
- Active Tab 樣式:文字藍色、底部藍色底線
- Inactive Tab 樣式:文字灰色、無底線
5. 基本資料表單欄位定義
5.1 欄位清單
表單採 4 欄 Grid 佈局,每列放 4 個欄位(標籤 + 輸入元件)。
| 列 | 欄位標籤 | 欄位 ID | 元件類型 | 必填 | 說明 |
|---|---|---|---|---|---|
| Row 1 | 供應商編號 | supplierCode |
text input(唯讀) | ✅ | 系統自動產生,頁面載入時由 API 取得,使用者不可編輯 |
| Row 1 | 供應商名稱 | supplierName |
text input | ✅ | 最多 100 字元 |
| Row 1 | 聯絡人姓名 | contactName |
text input | ❌ | 最多 50 字元 |
| Row 1 | 統一編號 | taxId |
text input | ❌ | 8 碼數字,格式驗證 |
| Row 2 | 公司地址 | address |
text input | ❌ | 最多 200 字元 |
| Row 2 | 服務類別 | serviceCategory |
select dropdown | ❌ | 選項從 API 動態載入,見 §5.2 |
| Row 2 | 負責業務 | ownerUserId |
select dropdown + 👤 icon | ✅ | 從員工資料表 API 載入,見 §5.3 |
| Row 2 | 付款條件 | paymentTerm |
select dropdown | ❌ | 選項見 §5.4 |
| Row 3 | 聯絡電話 | phone |
text input | ❌ | 格式:數字與 -,最多 20 字元 |
| Row 3 | 備註說明 | remark |
text input | ❌ | 最多 500 字元 |
| Row 3 | 風險評估 | riskLevel |
select dropdown | ❌ | 選項見 §5.5 |
| Row 3 | 供應商狀態 | supplierStatus |
select dropdown | ✅ | 選項見 §5.6 |
*標記代表必填欄位,標籤後方顯示紅色星號(color: #F44336)
5.2 服務類別選項(serviceCategory)
從後端 API 動態載入:GET /api/reference/service-categories
請選擇(預設,值為空)
文具用品 → STATIONERY
生產製造 → MANUFACTURING
包裝 → PACKAGING
物流運輸 → LOGISTICS
其他 → OTHER
5.3 負責業務選項(ownerUserId)— 員工 API
從員工資料表 API 動態載入:GET /api/hr/employees/active
請選擇(預設,值為空)
{員工姓名} → {員工代碼} (依 API 回傳動態渲染)
- 下拉選單顯示員工姓名,實際儲存值為員工代碼(ID)
- 僅載入**在職(active)**員工
- 右側 👤 icon 點擊:保留 icon 樣式,行為同下拉選單展開(或未來擴充為人員選取 Popup)
5.4 付款條件選項(paymentTerm)
請選擇(預設,值為空)
30天付款 → DAYS_30
35天付款 → DAYS_35
60天付款 → DAYS_60
月結 → MONTHLY
貨到付款 → COD
5.5 風險評估選項(riskLevel)
請選擇(預設,值為空)
低 → LOW
中 → MEDIUM
高 → HIGH
5.6 供應商狀態選項(supplierStatus)— 必填
請選擇(預設,值為空)
合作中 → ACTIVE
暫停合作 → SUSPENDED
終止合作 → TERMINATED
6. 供應商編號自動產生規則
| 屬性 | 說明 |
|---|---|
| 產生時機 | 頁面載入時,前端呼叫 GET /api/purchasing/suppliers/next-code 取得 |
| 格式 |
PO + YYMMDD(建單日期) + 6位流水號,例:PO2604250001
|
| 欄位狀態 |
readonly,顯示灰底樣式(background: #F5F5F5)以示不可編輯 |
| 流水號規則 | 每日從 000001 重置,由後端維護 |
7. 儲存按鈕
| 屬性 | 值 |
|---|---|
| 文字 | 儲存 |
| 位置 | 表單底部置中 |
| 樣式 | 橘色(background: #FF9800; color: #FFF; border-radius: 20px; padding: 10px 40px) |
| 行為 | 觸發表單驗證 → 通過後呼叫 Insert API → 成功後導頁至列表頁並顯示成功訊息 |
| Loading 狀態 | 點擊後按鈕 disabled + 顯示 Spinner,防止重複送出 |
8. API 規格
8.1 取得下一個供應商編號
GET /api/purchasing/suppliers/next-code
Response 200:
{
"supplierCode": "PO2604250001"
}
8.2 取得在職員工列表(負責業務下拉選單)
GET /api/hr/employees/active
Response 200:
[
{
"employeeId": "EMP001",
"employeeName": "王小明",
"department": "採購部"
},
{
"employeeId": "EMP002",
"employeeName": "李大華",
"department": "業務部"
}
]
8.3 新增供應商(Insert API)
POST /api/purchasing/suppliers
Request Body:
{
"supplierCode": "PO2604250001",
"supplierName": "小東實業有限公司",
"contactName": "王小明",
"taxId": "12345678",
"address": "台北市中正區XX路1號",
"serviceCategory": "STATIONERY",
"ownerUserId": "EMP001",
"paymentTerm": "DAYS_30",
"phone": "02-2345-6789",
"remark": "長期合作供應商",
"riskLevel": "LOW",
"supplierStatus": "ACTIVE"
}
Request 欄位說明:
| 欄位名稱 | 型別 | 必填 | 說明 |
|---|---|---|---|
supplierCode |
string | ✅ | 由前端帶入自動產生的編號 |
supplierName |
string | ✅ | 最多 100 字元 |
contactName |
string | ❌ | 最多 50 字元 |
taxId |
string | ❌ | 8 碼數字 |
address |
string | ❌ | 最多 200 字元 |
serviceCategory |
string | ❌ | 服務類別代碼 |
ownerUserId |
string | ✅ | 員工代碼 |
paymentTerm |
string | ❌ | 付款條件代碼 |
phone |
string | ❌ | 聯絡電話 |
remark |
string | ❌ | 備註說明,最多 500 字元 |
riskLevel |
string | ❌ |
LOW / MEDIUM / HIGH
|
supplierStatus |
string | ✅ |
ACTIVE / SUSPENDED / TERMINATED
|
Response 201(建立成功):
{
"success": true,
"message": "供應商新增成功",
"supplierId": "uuid-string",
"supplierCode": "PO2604250001"
}
Response 400(驗證失敗):
{
"success": false,
"message": "欄位驗證失敗",
"errors": [
{ "field": "supplierName", "message": "供應商名稱為必填" },
{ "field": "taxId", "message": "統一編號格式不正確" }
]
}
Response 409(供應商編號重複):
{
"success": false,
"message": "供應商編號已存在,請重新整理頁面"
}
8.4 取得服務類別選項
GET /api/reference/service-categories
(同查詢規格 §5.3,頁面初始化時一併呼叫)
9. Angular 元件規格
9.1 元件結構
purchasing/
└── suppliers/
├── suppliers.module.ts
├── suppliers-routing.module.ts
├── supplier-create/
│ ├── supplier-create.component.ts
│ ├── supplier-create.component.html
│ └── supplier-create.component.scss
└── services/
├── supplier.service.ts
└── employee.service.ts
9.2 SupplierCreateComponent
// supplier-create.component.ts
@Component({
selector: 'app-supplier-create',
templateUrl: './supplier-create.component.html',
styleUrls: ['./supplier-create.component.scss']
})
export class SupplierCreateComponent implements OnInit {
createForm: FormGroup;
isSubmitting = false;
isLoadingCode = false;
// 下拉選單資料來源
serviceCategories: ReferenceOption[] = [];
employees: EmployeeOption[] = [];
paymentTerms: ReferenceOption[] = [];
riskLevels: ReferenceOption[] = [];
supplierStatuses: ReferenceOption[] = [];
ngOnInit(): void {
this.initForm();
this.loadNextCode(); // 取得自動編號
this.loadServiceCategories();
this.loadEmployees();
}
initForm(): void { ... } // 初始化 ReactiveForm
loadNextCode(): void { ... } // 呼叫 GET /api/purchasing/suppliers/next-code
loadServiceCategories(): void { ... }
loadEmployees(): void { ... }
onSave(): void { ... } // 驗證 → POST API → 導頁
onBack(): void { ... } // 導頁至 /purchasing/suppliers
}
9.3 表單初始化(ReactiveFormsModule)
initForm(): void {
this.createForm = this.fb.group({
supplierCode: [{ value: '', disabled: true }], // 唯讀
supplierName: ['', [Validators.required, Validators.maxLength(100)]],
contactName: ['', [Validators.maxLength(50)]],
taxId: ['', [Validators.pattern(/^\d{8}$/)]],
address: ['', [Validators.maxLength(200)]],
serviceCategory: [''],
ownerUserId: ['', Validators.required],
paymentTerm: [''],
phone: ['', [Validators.pattern(/^[\d\-]{0,20}$/)]],
remark: ['', [Validators.maxLength(500)]],
riskLevel: [''],
supplierStatus: ['', Validators.required],
});
}
9.4 儲存流程(onSave)
onSave(): void {
if (this.createForm.invalid) {
this.createForm.markAllAsTouched(); // 觸發所有欄位顯示錯誤訊息
return;
}
this.isSubmitting = true;
const payload: CreateSupplierRequest = {
...this.createForm.getRawValue(), // getRawValue 包含 disabled 的 supplierCode
};
this.supplierService.createSupplier(payload).subscribe({
next: (res) => {
this.toastService.success('供應商新增成功');
this.router.navigate(['/purchasing/suppliers']);
},
error: (err) => {
this.isSubmitting = false;
if (err.status === 409) {
this.toastService.error('供應商編號已存在,請重新整理頁面');
} else {
this.toastService.error('新增失敗,請稍後再試');
}
}
});
}
9.5 SupplierService 新增方法
// supplier.service.ts
@Injectable({ providedIn: 'root' })
export class SupplierService {
// 既有方法...
getSuppliers(params: SupplierQueryParams): Observable<SupplierListResponse> { ... }
deleteSupplier(supplierId: string): Observable<ApiResponse> { ... }
getServiceCategories(): Observable<ReferenceOption[]> { ... }
// 新增方法
getNextCode(): Observable<{ supplierCode: string }> {
return this.http.get<{ supplierCode: string }>(`${this.baseUrl}/next-code`);
}
createSupplier(payload: CreateSupplierRequest): Observable<CreateSupplierResponse> {
return this.http.post<CreateSupplierResponse>(this.baseUrl, payload);
}
}
9.6 EmployeeService
// employee.service.ts
@Injectable({ providedIn: 'root' })
export class EmployeeService {
private baseUrl = '/api/hr/employees';
getActiveEmployees(): Observable<EmployeeOption[]> {
return this.http.get<EmployeeOption[]>(`${this.baseUrl}/active`);
}
}
9.7 資料模型(TypeScript Interface)
// 新增 Request
export interface CreateSupplierRequest {
supplierCode: string;
supplierName: string;
contactName?: string;
taxId?: string;
address?: string;
serviceCategory?: string;
ownerUserId: string;
paymentTerm?: string;
phone?: string;
remark?: string;
riskLevel?: 'LOW' | 'MEDIUM' | 'HIGH';
supplierStatus: 'ACTIVE' | 'SUSPENDED' | 'TERMINATED';
}
// 新增 Response
export interface CreateSupplierResponse {
success: boolean;
message: string;
supplierId: string;
supplierCode: string;
}
// 員工選項
export interface EmployeeOption {
employeeId: string;
employeeName: string;
department: string;
}
// 通用下拉選項
export interface ReferenceOption {
code: string;
label: string;
}
10. 後端 Java 規格
10.1 Controller(新增端點)
@RestController
@RequestMapping("/api/purchasing/suppliers")
public class SupplierController {
// 既有端點...
@GetMapping
public ResponseEntity<PageResult<SupplierDTO>> getSuppliers(...) { ... }
@DeleteMapping("/{supplierId}")
public ResponseEntity<ApiResponse> deleteSupplier(...) { ... }
// 新增端點
@GetMapping("/next-code")
public ResponseEntity<NextCodeResponse> getNextCode() { ... }
@PostMapping
public ResponseEntity<CreateSupplierResponse> createSupplier(
@RequestBody @Valid CreateSupplierRequest request
) { ... }
}
10.2 Request DTO(含 Bean Validation)
public class CreateSupplierRequest {
@NotBlank(message = "供應商編號為必填")
@Size(max = 30)
private String supplierCode;
@NotBlank(message = "供應商名稱為必填")
@Size(max = 100)
private String supplierName;
@Size(max = 50)
private String contactName;
@Pattern(regexp = "^\\d{8}$", message = "統一編號須為 8 碼數字")
private String taxId;
@Size(max = 200)
private String address;
private String serviceCategory;
@NotBlank(message = "負責業務為必填")
private String ownerUserId;
private String paymentTerm;
@Pattern(regexp = "^[\\d\\-]{0,20}$", message = "聯絡電話格式不正確")
private String phone;
@Size(max = 500)
private String remark;
private String riskLevel; // LOW / MEDIUM / HIGH
@NotBlank(message = "供應商狀態為必填")
private String supplierStatus; // ACTIVE / SUSPENDED / TERMINATED
}
10.3 Response DTO
public class CreateSupplierResponse {
private boolean success;
private String message;
private String supplierId;
private String supplierCode;
}
public class NextCodeResponse {
private String supplierCode;
}
10.4 員工 API Controller
@RestController
@RequestMapping("/api/hr/employees")
public class EmployeeController {
@GetMapping("/active")
public ResponseEntity<List<EmployeeOptionDTO>> getActiveEmployees() { ... }
}
public class EmployeeOptionDTO {
private String employeeId;
private String employeeName;
private String department;
}
10.5 供應商編號產生邏輯(Service 層)
// SupplierService.java
public String generateNextCode() {
String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd"));
// 查詢當日最大流水號
int seq = supplierRepository.findMaxSeqByDate(dateStr) + 1;
return String.format("PO%s%06d", dateStr, seq);
}
11. 表單驗證規則
| 欄位 | 必填 | 格式規則 | 前端錯誤訊息 |
|---|---|---|---|
| 供應商編號 | ✅ | 系統產生,唯讀 | — |
| 供應商名稱 | ✅ | 最多 100 字元 | 供應商名稱為必填 |
| 負責業務 | ✅ | 需選取員工 | 負責業務為必填 |
| 供應商狀態 | ✅ | 需選取選項 | 供應商狀態為必填 |
| 統一編號 | ❌ | 若填寫,限 8 碼純數字 | 統一編號須為 8 碼數字 |
| 聯絡電話 | ❌ | 若填寫,限數字與 -,最多 20 碼 |
聯絡電話格式不正確 |
| 供應商名稱 | ❌ | 最多 100 字元 | 供應商名稱不可超過 100 字元 |
| 備註說明 | ❌ | 最多 500 字元 | 備註說明不可超過 500 字元 |
驗證時機:
- 欄位 blur(離開焦點)時顯示單一欄位錯誤
- 點擊「儲存」時觸發全表單驗證(
markAllAsTouched()) - 錯誤訊息顯示於欄位下方(紅色字體
#F44336)
12. 使用者互動與錯誤處理
12.1 頁面初始化流程
頁面載入
├─ 呼叫 GET /api/purchasing/suppliers/next-code → 填入供應商編號(唯讀)
├─ 呼叫 GET /api/reference/service-categories → 填充服務類別下拉選單
├─ 呼叫 GET /api/hr/employees/active → 填充負責業務下拉選單
└─ 其他下拉選項(付款條件、風險評估、狀態)使用前端靜態常數
12.2 Toast 通知
| 情境 | 提示類型 | 提示內容 |
|---|---|---|
| 新增成功 | Toast Success(綠) | 供應商新增成功 |
| 供應商編號重複 | Toast Error(紅) | 供應商編號已存在,請重新整理頁面 |
| 新增 API 失敗 | Toast Error(紅) | 新增失敗,請稍後再試 |
| 取得編號 API 失敗 | Toast Warning(橘) | 無法取得供應商編號,請重新整理頁面 |
| 取得員工清單失敗 | Toast Warning(橘) | 無法載入員工資料,請重新整理頁面 |
12.3 Loading 狀態
| 情境 | 行為 |
|---|---|
| 頁面初始化 API 載入中 | 各下拉選單顯示 載入中... 並 disabled |
| 儲存送出中 | 儲存按鈕顯示 Spinner + disabled,防止重複送出 |
12.4 離開頁面確認(可選)
若表單已有任何修改且尚未儲存,使用者點擊「回上一頁」或瀏覽器返回時,顯示確認 Dialog:
標題:確認離開
內容:目前有未儲存的資料,確定要離開嗎?
按鈕:[取消] [確認離開]
13. 其他備註
- 供應商編號格式:
PO+YYMMDD+ 6位流水號(每日重置),由後端GET /next-code產生 - 「相關採購訂單」Tab 在新增模式下不可點擊(disabled),待儲存成功後進入編輯模式方可查看
- 負責業務的 👤 icon 目前行為為展開下拉選單;若未來需要支援人員搜尋 Popup,可另立「人員選取元件」共用規格
- 付款條件、風險評估選項為靜態常數,無需 API 動態載入
- 後端需實作
@UniqueConstraint確保supplierCode唯一,前端碰到 409 時提示重新整理