線稿索引所有 spec最後更新 2026-07-02
specs/guide-portal.md

嚮導 / 領隊 Portal

一句話:嚮導 / 領隊專屬的受限 login portal —— 只看自己帶的梯次,讀出團 dossier(基礎資訊 / 司機 / 住宿 / 訂餐 / 成員名單)、寫成本與發票填報(經手),取代「發 Google 表單給嚮導填」的舊作法。建構於既有 departure_costs / expense_proofs / departure_* 營運表之上,不另起平行資料模型

畫面(UI = wireframe):嚮導 portal · 我帶的梯次 · 單一梯次 dossier + 填報

1. 目標 / 問題

荒野現行讓帶隊嚮導 / 領隊用 Google 表單 / 試算表回報出團資訊與代墊成本(見 GS「0501玉山」出團前準備工作表:現場領隊本人逐筆填車資 / 餐費 / 接駁+發票繳回狀態)。問題:

  • PII 全攤開:試算表把旅客身分證 / 電話 / 緊急聯絡原始值給有連結的人,零稽核、零範圍控制。
  • 資料雙寫脫節:嚮導填的成本要人工搬進 ERP,落差大。
  • 無範圍:嚮導看得到不該看的梯次 / 旅客。

本檔讓嚮導 / 領隊有受限 login,在 ERP 內:scoped 到自己帶的梯次、讀 dossier、現場拍照填成本 / 發票直接落 departure_costs(經手=嚮導,覆核在後台),PII 遮罩+reveal 稽核。

範圍guide 角色 + guide↔user 連結 + 梯次 scoping + 嚮導 portal UI(dossier 唯讀 + 成本 / 發票填報)。 不做:淨利 / 內外帳分析 / 證明單核簽 / 轉請款(後台 admin / accountant,見 internal-external-ledger);嚮導排班指派本身(後台,departure_guide_assignments);嚮導薪資 / 出款(payables(payee_type='guide'),見 control-finance §5)。

2. 資料模型

不新增成本 / dossier 表;只加「guide 實體 ↔ user 帳號」一條連結,其餘全 reuse。

2.1 guides 欄位(目錄主檔)

guides 是嚮導目錄主檔(後台建檔,見 §4.3)。實際欄位(權威 code:packages/core/src/schema/business.ts):

欄位型別說明
id / slugguide_<ulid> / text uniqueslug 由姓名 slugify 自動產生(唯一,撞名加 -2…)。
nametext NOT NULL姓名。
statustext NOT NULL default activeactive / inactive(停用 = archive,不硬刪)。
specialtiestext[]專長路線(如 ['百岳','中級山','溯溪'])。
years_experienceinteger nullable資歷(年)。
contact_phone / contact_emailtext nullable聯絡電話 / Email(明文低敏,非旅客 PII)。
avatar_url / biotext nullable頭像 URL / 個人簡介(Markdown)。
user_idFK → user.id(同庫)nullable unique,onDelete: set null開通 portal 後連到其 user 帳號(user.role='guide')。NULL = 無 login 的純名錄嚮導。
roster_role / max_days_per_monthtext default both / integer nullable排班職能 / 單月上限 —— 屬帶團排班,見 guide-roster §2.1
engagement_typetext(CHECK full_time/partner/external僱用 / 合作關係(正式 / 協作 / 外聘),與 roster_role 帶團職能為不同軸。後台表單 Select + 名錄 FilterSelect。
payee_account_name / payee_bank_code / payee_account_numbertext nullable×3出款收款銀行三欄,見 control-finance §5.6
  • 一對一guides.user_id 唯一(guides_user_id_unique;NULL 可多筆=多個未開通嚮導);user.role='guide' 的帳號至多對一個 guide 實體。
  • 連結建立 = 後台邀請嚮導開帳號(§4.3 / §5,比照 staff 邀請),不是 self-signup。
  • 嚮導僱用型態(正式 / 協作 / 外聘)決策:保留並落地(2026-07-01,三方稽核 D2)。此為僱用/合作關係維度,與 roster_role(帶團職能 leader/assistant/both)是不同軸——roster_role 是排班時可帶團 / 協作的職能,僱用型態是這位嚮導與公司的關係。已落地guides.engagement_type(值域 full_time/partner/external 對應 正式/協作/外聘,DB CHECK 約束)+ 後台目錄建/編表單 <Select> 暴露 + 名錄列表 <FilterSelect> 篩選。

2.2 梯次 scoping 來源(既有,不改)

嚮導「我帶的梯次」= departure_guide_assignmentsguide_id = 該 user 對應 guide 且指派為 confirmed 的 departure_id 集合(含 role='leader''assistant')。這是嚮導所有讀寫的範圍閘,比照 sales owner-id 只看自己訂單(member-ownership)。

2.3 reuse 的既有表(讀 / 寫)

用途表 / 來源嚮導權限
基礎資訊 / 梯次departures + trips(行程 / 日期 / 人數 / 處理狀態)
司機 / 派車departure_transportdeparture-prep §2.4
訂餐 / 飲食departure_catering + travelers 飲食備註
住宿 / 分房departure_prep 住宿軸 / 配套 lodging + 分房(room-assignment:房間 / 房友 / 性別)
證件departure_documentsdeparture-prep §2.2:入山 / 入園 / 床位 / 營位證)讀(下載隨身)
成員名單 / 分組travelers(PII reveal 稽核)+ 同行夥伴分組(room-assignment讀(遮罩)
成本 / 發票填報departure_costs + expense_proofsinternal-external-ledger §2寫(經手)

3. 邏輯 / 存取層

3.1 梯次 scoping(所有讀寫的閘)

每個嚮導端 repo 函式都以「該 user 的 guide.id → departure_guide_assignments confirmed 梯次集合」為範圍,伺服器端強制,不信任 client 傳入的 departure_id。存取非自己梯次 → fail-closed(404 / 403)。比照 owner-id 守衛寫在 query 層,不靠 UI 隱藏。

3.2 成本 / 發票填報(嚮導=經手)

嚮導送出一筆成本 = 該梯次 departure_costs 一筆,handler 語意為嚮導本人,並進覆核佇列review_status='pending'internal-external-ledger §2.1.3)。

每筆成本兩個金額(對齊 GS 領隊薪資申請單「申請總額 vs 發票總金額」成對欄)——這兩欄 departure_costs 早已具備(internal-external-ledger §2.1),嚮導端把它們拆成兩個明確輸入,不再只填單一「金額」:

嚮導端欄位寫入語意
代墊 / 實付金額amount_twd嚮導實際墊付、公司要退還給嚮導的錢(= 領隊薪資申請單「申請總額」)。必填。
憑證 / 發票金額invoice_amount_twd此筆取得的合法憑證面額、可報稅的金額(= 「發票總金額」)。僅「有發票」時填。
  • 有發票 / 收據is_declarable=true憑證影像上傳待物件儲存(GCS)上線(§5 開放議題)——第一版嚮導先填憑證金額即可送出,實體發票 / 收據自行留存、後台覆核時繳交(cost-form 現為 Alert 佔位,刻意不放假的拍照 input 以免影像被靜默丟棄)。憑證 / 發票金額預設帶入代墊額,可上修可下修(無 ≤ 代墊額 上限——實務常見「停車費代墊 300、卻拿到 2,000 發票」,internal-external-ledger §3.3)。
  • 無發票is_declarable=false、憑證 / 發票金額強制 0,同步開立一張 expense_proofs(經手=嚮導),送後台核准。嚮導仍可拿回代墊額(amount_twd),只是這筆不可申報。
  • 領隊費本身(嚮導勞務,非代墊支出)走同一張 departure_costscategory='領隊費'),單一金額、無發票(自然人勞務 → 掛支出證明單 / 扣繳,非開發票),不出現「憑證額」欄。
  • 外幣成本(海外領隊現場日幣 / 外幣憑證,internal-external-ledger §2.1.4):嚮導可選幣別、填外幣金額;內帳匯率第一版預設當日台銀牌告internal_rate_source='published'),外帳台銀牌告;若該團走預換外幣池(fx_account)由後台覆核時調整來源。嚮導端不顯示淨利 / WAC,只填外幣金額與幣別。
  • 送出後嚮導不可改(append-only 對嚮導端):更正洽線控 / 會計走後台沖正。

覆核 → 代入帳表 → 退款給嚮導(後台,嚮導端不顯示):

  1. 不再「送出即生效」:嚮導送出的成本 review_status='pending'不計入淨利 / 可申報 / 轉請款,直到會計覆核。
  2. 會計覆核 approveledger.approve,強制 approver ≠ handler)→ review_status='approved'(代入帳表);若掛 pending 證明單一併核准。
  3. 轉請款給嚮導:approved 的嚮導代墊成本可一鍵 convertCostsToPayable(payee_type='guide')control-finance §5),進請款管理走出款核簽,把代墊款退還嚮導。

覆核 / 核准 / 轉請款 / 淨利全在後台admin / accountant),嚮導端不顯示淨利、毛利、收入、覆核狀態。

這是「成本登記」的嚮導 surface;線控 / op 的 surface 在團控成本登記頁(internal-external-ledger §4 / control-finance §4),其登記直接 review_status='approved'(現場營運即生效)。兩個 surface 寫同一份 departure_costs

3.3 PII reveal(遮罩 + 稽核)

成員名單預設遮罩敏感欄。身分證 / 護照走 KMS reveal:嚮導點「顯示」才解密、且寫 traveler.decrypt audit(RevealSchema 僅接受 idNumber / passportNo 兩欄,order-scoped reveal、防 IDOR),沿用 travelers 既有 reveal 流程。電話 / 緊急聯絡為明文低敏欄,UI 遮罩但不解密(無 reveal action,不進 KMS)。嚮導只能 reveal 自己帶梯次的旅客(scoping 二次收斂:traveler→order→departure ∈ 自己 confirmed 梯次)。相較舊試算表「原始 PII 全攤開」,這是收斂。

3.4 dossier 組裝(唯讀派生)

dossier 各區從既有 repo 讀組合,不複製:基礎資訊(departures/trips)、車(departure_transport)、食(departure_catering + 飲食彙總)、宿(住宿軸)、成員名單(travelers + room-assignment 分組)。嚮導端不顯示金流真相(應收 / 實收 / 淨利)。

4. UI 與權限

4.1 版面(獨立 portal · 行動裝置優先)

獨立最小殼(非後台 sidebar),頂列 + 窄欄置中、大觸控目標(嚮導多在山下用手機):

  • /guide 我帶的梯次:scoped 梯次卡列表(行程 / 日期 / 人數 / 領隊 vs 協作 / 待補成本筆數 / 階段)。
  • /guide/[departureId] 單一梯次 dossier:以 tab 切分(手機友善,少長捲):
    • 出團資訊:基礎資訊 grid + 車 / 食 / 宿 status 卡(沿用出團前準備卡);宿含分房表(房間 / 房友 chip / 性別 / 同行夥伴分組,沿用分房表)。
    • 成員名單:旅客表(PII 遮罩 + reveal 稽核)。
    • 證件:入山 / 入園 / 床位 / 營位證清單,唯讀 + 下載隨身(申請在 departure-prep,嚮導不負責申請)。
    • 成本 / 發票:已送出列(顯示代墊額 / 憑證額兩欄)+ 新增 dialog(類別 → 項目 → 幣別 → 代墊 / 實付金額(外幣即時換算顯示)→ 有發票?→ 有則填憑證 / 發票金額(預設帶入代墊額、可改,提示「發票額可大於代墊額」;影像上傳待物件儲存上線,現為 Alert 佔位)/ 無則開支出證明單,憑證額 = 0),帶待覆核計數。兩金額語意見 §3.2。

4.2 權限(新角色 guide + 新 permission)

新增角色 guide不在 adminRoles(不得碰使用者管理 / better-auth admin 端點)。permission(packages/core/src/permissions-ac.ts 註冊):

action用途guide(自己梯次)opadminaccountant
guide_portal.read進嚮導 portal、讀自己帶梯次的 dossier
departure_cost.create登記成本 + 開支出證明單(經手)✅(scoped)
departure_cost.manage編輯 / 刪除 / 轉請款(跨梯次)
ledger.read淨利 / 內外帳分析 / 毛利
ledger.approve支出證明單核准(經手 ≠ 核准)
traveler reveal解密身分證 / 電話(travelers✅(scoped)

departure_cost.create 把「成本登記」從 2026-06-23 的 ledger.manage(admin/accountant 限定)拆出來:成本+憑證登記是 per-departure 現場作業(線控 / 嚮導),淨利分析才是敏感財務。詳見 internal-external-ledger §4.2

嚮導讀寫一律經 §3.1 的梯次 scoping 閘二次收斂(即使有 action,也只限自己帶的梯次)。

4.3 後台嚮導目錄(/admin/guides

嚮導目錄 CRUD 是後台管理 surface(與嚮導本人的受限 portal 分權),權限 guide: ['read', 'manage']packages/core/src/permissions-ac.ts):

action用途adminsalesopaccountant
guide.read/admin/guides 列表 / 檢視卡片
guide.manage新增 / 編輯 / 停用(archive)嚮導 + 開通登入邀請
  • 列表頁page.tsxrequirePermission('guide.read')):卡片格(頭像 / 姓名 / 狀態 / 資歷 / 專長 badge / 聯絡方式),狀態 + 關鍵字 filter bar,KPI(合作 / 在線 / 停用)。帶隊數 / 評分為 Phase 4 佔位(顯示破折號)。
  • 建 / 編表單guide-form.tsx):name / slug(不填自動產生)/ years_experience / status / contact_phone / contact_email / avatar_url / specialties(逗號分隔)/ bio。actions 經 guide.manage gate(createGuideAction / updateGuideAction / archiveGuideAction,各寫 guide.create / .update / .archive audit)。
  • 開通登入(§5):卡片對 active、未連結帳號的嚮導顯示「開通登入」,經 inviteGuideAccountActionguide.manage)產一次性暫時密碼手動轉交;已連結顯示「已開通」badge。連結建立即 §2.1 的 guides.user_id
  • repopackages/core/src/guides/admin-repo.tslistGuides / createGuide / updateGuide / archiveGuide / inviteGuideAccount)。

目錄 CRUD 用 guide.*(後台管理);嚮導本人 portal 用 guide_portal.read(§4.2)—— 兩套權限、兩個 surface,別混。帶團排班(指派 / 換領隊)是第三套 guide_roster.*,見 guide-roster §4.2

5. 開放議題

  • 嚮導開帳號流程(已定):後台 /admin/guides 卡片「開通登入」邀請,inviteGuideAccountuser.role='guide' + 連結 guides.user_id(一對一,競態守衛 user_id IS NULL),印一次性暫時密碼手動轉交(比照 staff 邀請)。嚮導屬 staff-tier,2FA 一律強制/2fa-setup 接受所有 staff-tier,含 guide)。
  • 協作 vs 正領隊權限差異:第一版兩者 dossier / 填報相同(都進 departure_cost.create);是否限協作只填成本、不看全名單,待定。
  • 離線 / 弱訊號:山下訊號差,成本 / 發票是否需離線暫存後補送(PWA / 本地暫存),第一版線上即時。
  • 發票照片儲存:走物件儲存(GCS)+與 departure_costs / expense_proofs 關聯;保存策略與代收轉付收據分流,待 travelinvoice 對齊。
  • 嚮導端通知:行前提醒 / 待補成本提醒是否複用 emails / 罐頭範本推播。

Changelog

版本日期變更Commit
12026-06-24初版:嚮導 / 領隊受限 login portal(取代 Google 表單)。guides.user_id 連 user + departure_guide_assignments 梯次 scoping;dossier 唯讀(reuse departure-prep / travelers / room-assignment)+ 成本 / 發票填報(經手=嚮導,寫既有 departure_costs / expense_proofs,覆核在後台);PII 遮罩 + reveal 稽核;新 guide 角色(不在 adminRoles)+ guide_portal.read / departure_cost.create(scoped)。未實作(wireframe 先行:apps/wireframes/app/guide/*)。
22026-06-24§3.2 嚮導申報加覆核 gate(覆寫初版「覆核在後台」的隱性「送出即生效」):嚮導送出 review_status='pending' 不計入帳,會計 ledger.approve(approver≠handler)後 approved(代入帳表),approved 嚮導代墊一鍵轉請款 payable(payee=guide) 進請款管理退款。後台 / 團控直接登記者 approved。對齊 internal-external-ledger §2.1.3未實作。
32026-06-28§3.2 / §4.1 代墊額 vs 憑證額兩金額明確化(對齊 GS 領隊薪資申請單「申請總額 / 發票總金額」成對欄):嚮導端把單一「金額」拆成 代墊 / 實付金額amount_twd,公司退還嚮導)與 憑證 / 發票金額invoice_amount_twd,可申報,預設帶入代墊額、可上修無上限——對應「停車費代墊 300 / 發票 2,000」)兩個輸入;無發票則憑證額 0 + 開支出證明單;領隊費單一金額無憑證欄。零 schema 變更(兩欄早具備,internal-external-ledger §2.1)。嚮導出款銀行資料見 control-finance §5.6未實作。
42026-06-28全鏈實作落地。 guides.user_id(nullable unique FK,set null)+ guide 角色 / guide_portal.read grant。安全核心 packages/core/src/guide-portal/{scope,portal-repo}.tsresolveGuideForUser / listGuideConfirmedDepartureIds / isDepartureInGuideScope(§3.1,server 端 user→guide→confirmed 派生、tentative 排除、停用 guide 撤權)、getGuideDossier(dossier 唯讀派生 + PII 遮罩 roster)、createGuideCost(強制 review_status='pending')、getTravelerEncForGuideReveal(§3.3 traveler→order→departure ∈ confirmed 二次收斂,anti-IDOR)。inviteGuideAccount(§5 後台邀請)。UI apps/web/src/app/guide/**(行動裝置優先,對齊 wireframe)+ revealGuideTravelerPiiActiontraveler.decrypt audit)。後台 /admin/guides 開通邀請 dialog(一次性密碼)。2FA:嚮導屬 staff-tier 一律強制(/2fa-setup 改 staff-tier 判定)。測試:guide-portal-scope(16)+ guide-portal-grants(6)+ e2e 41-guide-portal(越權 404 / RBAC 閘)。
52026-07-01三方對齊稽核 B8 收斂(Code-Is-Truth)。 §2.1 欄位表對齊 code 實際 guides 欄(name/slug/status/specialties/years_experience/contact_/avatar_url/bio/user_id/roster_role/max_days/payee_),取代 stale 的 id/slug/status/roster_role/max_days;補「嚮導 type 維度未落地、待決策 D2」。新增 §4.3「後台嚮導目錄(/admin/guides)」節(guide.read / guide.manage CRUD + 開通登入邀請 + admin-repo.ts),與 portal 的 guide_portal.read、排班的 guide_roster.* 三套權限分清。§3.2 / §4.1 「拍照上傳發票」下修為「憑證影像上傳待物件儲存(GCS)上線」(cost-form 現為 Alert 佔位)。§3.3 PII reveal 措辭修正:身分證 / 護照走 KMS reveal + audit(RevealSchemaidNumber/passportNo),電話 / 緊急聯絡為明文低敏、UI 遮罩不解密。跨梯次排班另立 guide-roster.md
62026-07-02§2.1 code 落地:新增 guides.engagement_typefull_time/partner/external,DB CHECK)+ 後台目錄表單 <Select> + 名錄 <FilterSelect> 篩選,欄位表補一列。解除 D2 僱用型態 code follow-up。