技術架構

1-1 Monorepo 結構

/
├── packages/
│   ├── backend/
│   │   ├── src/
│   │   │   ├── routes/
│   │   │   │   └── admin.routes.ts
│   │   │   ├── controllers/
│   │   │   │   └── admin.controller.ts
│   │   │   ├── services/
│   │   │   │   ├── notification.service.ts
│   │   │   │   ├── google-docs.service.ts
│   │   │   │   ├── bob.service.ts
│   │   │   │   ├── cs.service.ts
│   │   │   │   └── token.service.ts
│   │   │   ├── middleware/
│   │   │   │   ├── auth.ts
│   │   │   │   └── rbac.ts
│   │   │   ├── lib/
│   │   │   │   ├── s3.ts
│   │   │   │   └── cron.ts  // prisma.ts 已移除,改用 Drizzle
│   │   │   └── index.ts
│   │   ├── src/db/  // Drizzle ORM(取代 prisma/)
│   │   └── package.json
│   └── dashboard/
│       ├── src/
│       │   ├── views/
│       │   │   ├── web/
│       │   │   │   ├── Dashboard.vue
│       │   │   │   ├── Contracts.vue
│       │   │   │   ├── ContractList.vue
│       │   │   │   ├── ContractDetail.vue
│       │   │   │   ├── ContractNew.vue
│       │   │   │   ├── ExpiringContracts.vue
│       │   │   │   ├── CsWorkspace.vue
│       │   │   │   ├── Settings.vue
│       │   │   │   └── AdminPanel.vue
│       │   │   ├── mobile/
│       │   │   │   ├── MobileDashboard.vue
│       │   │   │   └── MobileContracts.vue
│       │   │   └── token/
│       │   │       └── AdminTokenPage.vue
│       │   ├── components/
│       │   │   └── ContractForm.vue
│       │   ├── stores/
│       │   │   ├── auth.ts
│       │   │   ├── contracts.ts
│       │   │   └── notifications.ts
│       │   ├── router/
│       │   │   └── index.ts
│       │   └── composables/
│       └── package.json
├── package.json
└── railway.toml

1-2 技術棧

技術
後端Node.js + Express + TypeScript
ORMDrizzle + PostgreSQL
前端Vue 3 + Composition API + TypeScript + Vite
UI 元件NativUI
狀態管理Pinia
樣式Tailwind CSS
表單驗證Zod + VeeValidate
認證JWT(httpOnly Cookie)
檔案儲存AWS S3(發票、錄音檔)
文件產生Google Apps Script Web App
Email 服務Mailgun
Line 通知Line Messaging API
排程node-cron
部署Railway(兩個 Service)

1-3 Railway 部署設定

# railway.toml
[[services]]
name = "backend"
source = "packages/backend"
startCommand = "node dist/index.js"

[[services]]
name = "dashboard"
source = "packages/dashboard"
startCommand = "npx serve dist"

角色系統

enum Role {
  ADMIN             // 系統管理員,最高權限
  MANAGER           // 主管
  INSIDE_SALES      // 內勤業務
  CUSTOMER_SUCCESS  // 客戶成功(CS)
  FIELD_SALES       // 外勤業務,手機版
  OFFICE_SUPPORT    // 行政支援,可登入 + 一次性 token
}

路由守衛邏輯

// router/index.ts
router.beforeEach((to) => {
  const auth = useAuthStore()
  if (!auth.isLoggedIn) return '/login'
  if (auth.role === 'FIELD_SALES' && to.path.startsWith('/web')) return '/mobile/dashboard'
  if (auth.role !== 'FIELD_SALES' && to.path.startsWith('/mobile')) return '/web/dashboard'
  if (auth.role !== 'ADMIN' && to.path.startsWith('/web/admin')) return '/web/dashboard'
})

資料模型(Drizzle ORM Schema)

新增 Model

model Team {
  id         String   @id @default(cuid())
  name       String
  created_at DateTime @default(now())
  users      User[]
}

model OneTimeToken {
  id          String    @id @default(cuid())
  token       String    @unique @default(cuid())
  contract_id String
  action      AdminAction
  expires_at  DateTime
  used_at     DateTime?
  created_at  DateTime  @default(now())
  contract    Contract  @relation(fields: [contract_id], references: [id])
}

enum AdminAction {
  SEAL_CLIENT    // 客戶回簽時間登記
  SEAL_ADMIN     // 管理部用印時間登記
  REGISTER_KT    // Kintone 登記(觸發結案)
}

model NotificationLog {
  id          String   @id @default(cuid())
  type        NotificationType
  channel     NotificationChannel
  recipient   String
  contract_id String?
  sent_at     DateTime @default(now())
  status      String   // SENT | FAILED
  error       String?
}

enum NotificationType {
  ADMIN_ACTION      // 行政審核通知
  CS_OPEN           // CS 開通通知
  CONTRACT_RETURN   // 合約被退回
  STUCK_ALERT       // 卡關提醒
  EXPIRY_WARNING    // 到期預警
}

enum NotificationChannel {
  EMAIL
  LINE
}

model CsChecklist {
  id                    String    @id @default(cuid())
  contract_id           String    @unique
  payment_confirmed     Boolean   @default(false)
  line_permission       Boolean   @default(false)
  member_registered     Boolean   @default(false)
  line_api_missing      Boolean   @default(false)
  member_data_confirmed Boolean   @default(false)
  bob_created_at        DateTime?
  onboarding_type       OnboardingType?
  teaching_scheduled_at DateTime?
  doc_provided_at       DateTime?
  created_at            DateTime  @default(now())
  updated_at            DateTime  @updatedAt
  contract              Contract  @relation(fields: [contract_id], references: [id])
}

enum OnboardingType {
  PACKAGE   // 安心上線包
  TEACHING  // 購買教學
  DOCUMENT  // 僅提供文件
}

Contract 擴充欄位

model Contract {
  // ... 現有欄位保留 ...
  sent_date            DateTime?  // 合約寄送日期(行政登記)
  client_seal_date     DateTime?  // 客戶回簽日期
  admin_seal_date      DateTime?  // 管理部用印日期
  kt_date              DateTime?  // Kintone 登記日期
  kt_link              String?    // Kintone 連結
  gdrive_link          String?    // Google Drive 歸檔連結
  google_docs_link     String?    // Google Docs 合約連結(系統產生)

  // 合約文件額外欄位(Google Docs 模板填充用)
  software_months      Integer?   // 軟體服務授權期(月份)
  plan_label           String?    // 方案名稱(季繳/半年/一年期/二年期/三年期)
  plan_amount          Integer?   // 方案金額
  software_fee         Integer?   // 軟體費用
  addon_fee            Integer?   // 加購費用
  setup_fee            Integer?   // 導入設定費
  items_purchased      String?    // 本次購買項目(逗號分隔)
  main_module_fee      Integer?   // 模組總購費用(生活歐巴)
  addon_module_fee     Integer?   // 加購總共費用(生活歐巴)
  addon_items          String?    // 加購項目(美業歐巴)
  secure_package_fee   Integer?   // 安心上線包費用(美業歐巴)
  auth_start_date      DateTime?  // 軟體授權起始日(生活歐巴)
  shipping_address     String?    // 合約紙本寄送地址
  secondary_sales_id   String?    // 副業務
  payment_link         String?    // 付款連結
  contract_file_link   String?    // 合約電子檔連結
}

前端路由結構

// 公開路由(無需登入) /login
/token/:token
// Web(ADMIN / MANAGER / INSIDE_SALES / CUSTOMER_SUCCESS / OFFICE_SUPPORT) /web/dashboard
/web/contracts
/web/contracts/list
/web/contracts/new
/web/contracts/:id
/web/contracts/:id/cs
/web/expiring
/web/settings
/web/admin ← 後台管理(ADMIN 限定)
// Mobile(FIELD_SALES) /mobile/dashboard
/mobile/contracts
/mobile/contracts/:id
路由守衛:非 ADMIN 造訪 /web/admin 將導回 /web/dashboard

API 規格

5-1 認證

POST  /api/auth/login           # Email/密碼登入
POST  /api/auth/logout          # 登出(Token 黑名單)
GET   /api/auth/me              # 取得當前使用者
PATCH /api/users/:id/password   # 密碼重設(ADMIN)

5-2 合約

GET   /api/contracts                      # 列出合約(看板格式)
GET   /api/contracts/list                 # 列出合約(表格格式)
GET   /api/contracts/expiring?days=90     # 到期合約清單
POST  /api/contracts/generate             # 產生合約(含 Google Docs)
PATCH /api/contracts/:id                  # 更新合約
POST  /api/contracts/:id/cancel           # 取消合約
POST  /api/contracts/:id/refund           # 退款合約
GET   /api/contracts/:id                  # 取得單一合約

5-3 一次性 Token(行政快速操作)

POST  /api/admin-tokens/generate
  body:  { contract_id: string, action: AdminAction }
  auth:  requireRole(MANAGER, INSIDE_SALES, OFFICE_SUPPORT)
  回傳:  { token, expires_at, url }

POST  /api/admin-tokens/verify
  body:  { token: string }
  auth:  無需登入
  回傳:  { valid: boolean, action, contract }

POST  /api/admin-tokens/consume
  body:  { token: string, date: string, extra?: object }
  auth:  無需登入
  行為:  驗證 token → 更新 Contract 對應欄位 → 標記 used_at → 觸發通知

5-4 CS 作業

GET   /api/contracts/:id/cs-checklist
POST  /api/contracts/:id/cs-checklist
  body:  { payment_confirmed, line_permission, member_registered, line_api_missing }
  auth:  requireRole(CUSTOMER_SUCCESS)

POST  /api/contracts/:id/cs/bob-sync
  auth:  requireRole(CUSTOMER_SUCCESS)
  行為:  呼叫 Bob API 建立組織,記錄 bob_created_at

PATCH /api/contracts/:id/cs/onboarding
  body:  { onboarding_type, teaching_scheduled_at?, doc_provided_at? }
  auth:  requireRole(CUSTOMER_SUCCESS)

PATCH /api/contracts/:id/cs/member-data
  body:  { member_data_confirmed: boolean }
  auth:  requireRole(CUSTOMER_SUCCESS)

5-5 通知

手動觸發通知已由 Admin 端點取代(見 5-8)。
GET   /api/notifications/logs   # 通知記錄(MANAGER / ADMIN)
觸發時機類型對象內容
合約進入行政處理階段ADMIN_ACTIONOFFICE_SUPPORTEmail/Line 含一次性 token link
客戶付款確認後CS_OPENCUSTOMER_SUCCESS通知開始 CS 作業
合約被退回CONTRACT_RETURN業務退回原因
某階段 N 天未推進STUCK_ALERT負責單位卡關提醒
合約到期前 90/60/30 天EXPIRY_WARNING業務、主管續約提醒

5-6 業績目標

GET   /api/quotas?year=&month=
POST  /api/quotas
  body:  { user_id, year, month, quota_amount }
  auth:  requireRole(MANAGER, ADMIN)
PATCH /api/quotas/:id
  auth:  requireRole(MANAGER, ADMIN)

5-7 收款期數

GET   /api/contracts/:id/periods
POST  /api/contracts/:id/periods
PATCH /api/contracts/:id/periods/:periodId
POST  /api/contracts/:id/periods/:periodId/invoice   # 發票上傳(S3)

5-8 Admin 後台

POST  /api/admin/trigger/stuck   # 手動觸發卡關合約通知(ADMIN)
POST  /api/admin/trigger/expiry  # 手動觸發到期警告通知(ADMIN)
GET   /api/admin/cron-logs       # 查詢通知執行紀錄最近200筆(ADMIN)

Google Docs 整合

實作方式:Google Apps Script Web App
後端不使用 Service Account,改以 Google Apps Script Web App 作為文件產生服務。

流程

1. 後端組裝 record 物件(key 對應 docx 模板中的 {{placeholder}}
2. POST 到 Google Apps Script Web App URL
3. Apps Script 複製模板 → 替換 placeholder → 回傳文件 URL

環境變數

GOOGLE_APPS_SCRIPT_URL_BEAUTY=    # 美業歐巴 Apps Script URL
GOOGLE_APPS_SCRIPT_URL_LIFESTYLE= # 生活歐巴 Apps Script URL

Placeholder Keys

Placeholder 格式:{{key}}

分類Keys
共用contract_id, sales_name, contract_user_name, partya_reprsentative_name, partya_contact_phone, partya_enterprise_address, partya_enterprise_uniform_no
合約期間contract_start_year/month/day, contract_end_year/month/day
費用total_price, software_price, addon_price, import_setup_price
付款方案quarter_plan/price, helf_year_plan/price, one_year_plan/price, two_year_plan/price, three_year_plan/price
美業專用startup_plan, mini_shop_plan, expand_business_plan, flagship_plan, addon_plan_1/2, secure_online_plan/price, software_access_month
生活專用main_system_module/price, addon_module/price, auth_start_year/month/day, auth_end_year/month/day

行政一次性 Token 頁面規格

路由:/token/:token(無需登入)

流程

1. 頁面載入 → 呼叫 POST /api/admin-tokens/verify
2. Token 有效 → 顯示合約摘要 + 操作介面
3. Token 無效/過期 → 顯示錯誤頁,提示聯繫業務重新發送
4. 行政選擇日期 → 點擊確認 → 呼叫 POST /api/admin-tokens/consume
5. 成功 → 顯示完成頁,token 失效,觸發下一步通知

Action 對應

Action標題操作更新欄位
SEAL_CLIENT登記客戶回簽日期DatePickerclient_seal_date
SEAL_ADMIN登記管理部用印日期DatePickeradmin_seal_date
REGISTER_KT登記 Kintone 寫入日期DatePicker + KT連結輸入kt_datekt_link,觸發結案(WON)

通知服務實作規格

Email(Mailgun)

import formData from 'form-data';
import Mailgun from 'mailgun.js';

const mailgun = new Mailgun(formData);
const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY });
const mg = client.domains.domain(process.env.MAILGUN_DOMAIN);

// 行政審核通知範本
await mg.messages.create(process.env.MAILGUN_DOMAIN, {
  from: `B2B Sales System <noreply@${process.env.MAILGUN_DOMAIN}>`,
  to: admin_email,
  subject: `[合約審核] ${contract_number} — ${action_label} 待處理`,
  html: `...`
});

環境變數

MAILGUN_API_KEY=    # Mailgun API Key
MAILGUN_DOMAIN=     # 您的 Mailgun Domain(例:sandboxXXX.mailgun.org)

Line Messaging API

// 訊息格式
`[合約審核] ${contract_number}
店家:${store_name}
待處理:${action_label}
請點擊連結操作(48小時內有效):
${token_url}`

排程(node-cron)

// 每日 09:00 執行
cron.schedule('0 9 * * *', async () => {
  await checkStuckContracts()    // 掃描卡關合約
  await checkExpiringContracts() // 掃描到期合約
})

// 卡關定義:各階段超過 N 天未推進(N 由系統設定決定)
// 到期預警:合約結束日前 90 / 60 / 30 天各發一次

前端關鍵元件規格

9-1 AdminTokenPage.vue(/token/:token)

狀態機:
  LOADING → 驗證 token
  VALID   → 顯示操作介面
  SUCCESS → 顯示完成畫面
  ERROR   → 顯示錯誤(過期/已使用/無效)

9-2 Contracts.vue(看板)

- 每欄顯示合約卡片(w-64 flex-shrink-0)
- 點擊卡片展開 Drawer 顯示摘要
- Drawer 底部依狀態顯示對應操作 Modal(不再是 inline 按鈕)
- 支援左右橫向滑動(h-screen flex-col layout,overflow-x-auto)
- 各欄可獨立縱向捲動(overflow-y-auto)

9-3 ContractForm.vue(新增合約表單)

分區:
① 業務資訊:主業務(必填)、副業務(選填)
② 店家資訊:統編、店家名稱、聯絡人、電話、信箱、合約紙本寄送地址(必填)
③ 合約基本資訊:產品(美業歐巴/生活歐巴 下拉)、合約類型
④ 合約期間:起始日、結束日、軟體授權月數(必填)
⑤ 購買方案(依 product 動態顯示):
   - 美業歐巴:Radio 起步/微型/擴大/旗艦 + 加購項目輸入 + 安心上線包費用
   - 生活歐巴:Checkbox 主系統/加購模組 + 軟體授權起始日 + 加購模組項目
⑥ 費用明細:付款方案 Radio + 各費用欄位(生活歐巴額外顯示主系統/加購模組費用)
備註

9-4 ContractDetail.vue

- 合約詳情頁,恆顯示所有區塊(不再 v-if 隱藏空值,空值顯示 "-")
- 區塊:合約進度(視覺化步驟條)、業務資訊、店家資訊、合約基本資訊、
         購買方案、費用明細、相關連結、備註
- 相關連結整合所有外部連結(Google Docs、Kintone、合約掃描檔、付款連結)
- 移除舊有 ContractAdminActions 區塊(行政動作已在 Contracts.vue 看板 Modal 處理)
- 產生文件按鈕在相關連結卡片內

9-5 AdminPanel.vue(/web/admin)

ADMIN 限定頁面,包含:
① 通知測試:
   - 手動執行卡關合約通知(POST /api/admin/trigger/stuck)
   - 手動執行到期警告通知(POST /api/admin/trigger/expiry)
   - 顯示執行結果(送出封數 + 明細)
② 通知執行紀錄:
   - 顯示最近 200 筆通知 log
   - 欄位:時間、類型、頻道、收件人、合約連結、狀態、錯誤

9-6 CsWorkspace.vue(/web/contracts/:id/cs)

區塊一:四項確認 Checklist
  - 付款確認 Checkbox
  - Line@ 權限 Checkbox
  - 會員註冊 Checkbox
  - Line API 缺乏標記 Checkbox(橘色警示)

區塊二:會員資料確認
  - 使用者在小幫手 Line@ 註冊後才可確認
  - 確認後可填入會員相關備註

區塊三:店家開帳號資料(業務填寫)
  - 顯示業務填寫的服務項目
  - 若尚未填寫,顯示警示「請業務先填寫服務項目」
  - CS 無法進行 Bob 建立組織,直到業務完成填寫

區塊四:Bob 建立組織
  - 按鈕:呼叫 /cs/bob-sync
  - 顯示 bob_created_at 時間戳

區塊五:安心上線包 / 教學 / 文件
  - Radio:PACKAGE / TEACHING / DOCUMENT
  - 選 TEACHING → 顯示教學時間 DatePicker
  - 選 DOCUMENT → 顯示文件已提供按鈕

9-7 ExpiringContracts.vue

篩選:30天內 / 60天內 / 90天內
欄位:店家名稱、合約結束日、主業務、剩餘天數(依剩餘天數高亮)
快捷:點擊跳轉合約詳情

9-8 QuotaProgress.vue(Dashboard 內嵌)

顯示:本月目標金額 / 已達成金額 / 達成率進度條
MANAGER 可 Inline 編輯目標金額

開發優先順序

P0 — MVP 核心流程 P0

項目狀態說明
合約建立介面✓ 完成取代 Google Sheet,含完整店家資訊欄位
Google Docs 整合(Apps Script)✓ 完成產生合約文件,連結回存系統
行政一次性 Token 機制✓ 完成AdminTokenPage + token API
通知服務 Email(Mailgun)✓ 完成行政 token link 發送

P1 — 使用者功能完整 P1

項目狀態說明
CS 作業介面✓ 完成四項確認 + Bob API + 教學分流
Mobile 合約查詢✓ 完成外勤業務手機端唯讀
業績目標(SalesQuota API + UI)✓ 完成主管設定目標,儀表板顯示達成率
到期合約清單✓ 完成取代 Google Sheet 分頁
管理員後台(通知測試 + Cron Log)✓ 完成AdminPanel + Admin API

P2 — 強化 P2

項目說明
排程卡關偵測需通知服務先完成
Google Drive PDF 歸檔Kintone 寫入後自動匯出
密碼重設ADMIN 操作
經銷商管理前端完整 UI補齊現有後端

P3 — Phase 2(名單與商機) P3

項目說明
名單 CRUD開放現有隱藏功能
商機追蹤追蹤紀錄、潛力分級
錄音檔上傳S3 擴充
Excel 批次匯入名單批次建立
官網填單 API 串接需官網配合
筋斗雲整合視訪談結果決定

P4 — Phase 3(外部系統自動化)

項目說明
Kintone API 自動同步取代人工登打
Bob API 完整整合需 Bob API 文件
金流串接視付款期數訪談結果決定