專案

一般

配置概況

Web #85 » 供應商新增-spec.md

Andy Huang, 2026-04-25 22:53

 

供應商管理 - 供應商基本資料新增功能規格

系統名稱: 小東 POS 2.0
模組: 採購管理(Purchasing)
功能: 供應商新增(Supplier Create)
路由: /purchasing/suppliers/new
技術棧: Angular + Java (Spring Boot)
文件版本: v1.0
最後更新: 2026-04-25


1. 功能概述

供應商新增頁面提供使用者建立新供應商基本資料的入口,包含兩個分頁:

  1. 基本資料 — 填寫供應商基本欄位(本規格範疇)
  2. 相關採購訂單 — 顯示該供應商的採購訂單(新增時為空,本規格不包含)

供應商編號由系統自動產生,使用者無法手動輸入。填寫完成後點擊「儲存」呼叫 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 時提示重新整理
(2-2/2)