一句話:嚮導 / 領隊專屬的受限 login portal —— 只看自己帶的梯次,讀出團 dossier(基礎資訊 / 司機 / 住宿 / 訂餐 / 成員名單)、寫成本與發票填報(經手),取代「發 Google 表單給嚮導填」的舊作法。建構於既有
departure_costs/expense_proofs/departure_*營運表之上,不另起平行資料模型。畫面(UI = wireframe):
嚮導 portal · 我帶的梯次·單一梯次 dossier + 填報
荒野現行讓帶隊嚮導 / 領隊用 Google 表單 / 試算表回報出團資訊與代墊成本(見 GS「0501玉山」出團前準備工作表:現場領隊本人逐筆填車資 / 餐費 / 接駁+發票繳回狀態)。問題:
本檔讓嚮導 / 領隊有受限 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)。
不新增成本 / dossier 表;只加「guide 實體 ↔ user 帳號」一條連結,其餘全 reuse。
guides 欄位(目錄主檔)guides 是嚮導目錄主檔(後台建檔,見 §4.3)。實際欄位(權威 code:packages/core/src/schema/business.ts):
| 欄位 | 型別 | 說明 |
|---|---|---|
id / slug | guide_<ulid> / text unique | slug 由姓名 slugify 自動產生(唯一,撞名加 -2…)。 |
name | text NOT NULL | 姓名。 |
status | text NOT NULL default active | active / inactive(停用 = archive,不硬刪)。 |
specialties | text[] | 專長路線(如 ['百岳','中級山','溯溪'])。 |
years_experience | integer nullable | 資歷(年)。 |
contact_phone / contact_email | text nullable | 聯絡電話 / Email(明文低敏,非旅客 PII)。 |
avatar_url / bio | text nullable | 頭像 URL / 個人簡介(Markdown)。 |
user_id | FK → user.id(同庫)nullable unique,onDelete: set null | 開通 portal 後連到其 user 帳號(user.role='guide')。NULL = 無 login 的純名錄嚮導。 |
roster_role / max_days_per_month | text default both / integer nullable | 排班職能 / 單月上限 —— 屬帶團排班,見 guide-roster §2.1。 |
engagement_type | text(CHECK full_time/partner/external) | 僱用 / 合作關係(正式 / 協作 / 外聘),與 roster_role 帶團職能為不同軸。後台表單 Select + 名錄 FilterSelect。 |
payee_account_name / payee_bank_code / payee_account_number | text nullable×3 | 出款收款銀行三欄,見 control-finance §5.6。 |
guides.user_id 唯一(guides_user_id_unique;NULL 可多筆=多個未開通嚮導);user.role='guide' 的帳號至多對一個 guide 實體。roster_role(帶團職能 leader/assistant/both)是不同軸——roster_role 是排班時可帶團 / 協作的職能,僱用型態是這位嚮導與公司的關係。已落地:guides.engagement_type(值域 full_time/partner/external 對應 正式/協作/外聘,DB CHECK 約束)+ 後台目錄建/編表單 <Select> 暴露 + 名錄列表 <FilterSelect> 篩選。嚮導「我帶的梯次」= departure_guide_assignments 中 guide_id = 該 user 對應 guide 且指派為 confirmed 的 departure_id 集合(含 role='leader' 與 'assistant')。這是嚮導所有讀寫的範圍閘,比照 sales owner-id 只看自己訂單(member-ownership)。
| 用途 | 表 / 來源 | 嚮導權限 |
|---|---|---|
| 基礎資訊 / 梯次 | departures + trips(行程 / 日期 / 人數 / 處理狀態) | 讀 |
| 司機 / 派車 | departure_transport(departure-prep §2.4) | 讀 |
| 訂餐 / 飲食 | departure_catering + travelers 飲食備註 | 讀 |
| 住宿 / 分房 | departure_prep 住宿軸 / 配套 lodging + 分房(room-assignment:房間 / 房友 / 性別) | 讀 |
| 證件 | departure_documents(departure-prep §2.2:入山 / 入園 / 床位 / 營位證) | 讀(下載隨身) |
| 成員名單 / 分組 | travelers(PII reveal 稽核)+ 同行夥伴分組(room-assignment) | 讀(遮罩) |
| 成本 / 發票填報 | departure_costs + expense_proofs(internal-external-ledger §2) | 寫(經手) |
每個嚮導端 repo 函式都以「該 user 的 guide.id → departure_guide_assignments confirmed 梯次集合」為範圍,伺服器端強制,不信任 client 傳入的 departure_id。存取非自己梯次 → fail-closed(404 / 403)。比照 owner-id 守衛寫在 query 層,不靠 UI 隱藏。
嚮導送出一筆成本 = 該梯次 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_costs(category='領隊費'),單一金額、無發票(自然人勞務 → 掛支出證明單 / 扣繳,非開發票),不出現「憑證額」欄。internal_rate_source='published'),外帳台銀牌告;若該團走預換外幣池(fx_account)由後台覆核時調整來源。嚮導端不顯示淨利 / WAC,只填外幣金額與幣別。覆核 → 代入帳表 → 退款給嚮導(後台,嚮導端不顯示):
review_status='pending',不計入淨利 / 可申報 / 轉請款,直到會計覆核。approve(ledger.approve,強制 approver ≠ handler)→ review_status='approved'(代入帳表);若掛 pending 證明單一併核准。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。
成員名單預設遮罩敏感欄。身分證 / 護照走 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 全攤開」,這是收斂。
dossier 各區從既有 repo 讀組合,不複製:基礎資訊(departures/trips)、車(departure_transport)、食(departure_catering + 飲食彙總)、宿(住宿軸)、成員名單(travelers + room-assignment 分組)。嚮導端不顯示金流真相(應收 / 實收 / 淨利)。
獨立最小殼(非後台 sidebar),頂列 + 窄欄置中、大觸控目標(嚮導多在山下用手機):
/guide 我帶的梯次:scoped 梯次卡列表(行程 / 日期 / 人數 / 領隊 vs 協作 / 待補成本筆數 / 階段)。/guide/[departureId] 單一梯次 dossier:以 tab 切分(手機友善,少長捲):
guide + 新 permission)新增角色 guide,不在 adminRoles(不得碰使用者管理 / better-auth admin 端點)。permission(packages/core/src/permissions-ac.ts 註冊):
| action | 用途 | guide(自己梯次) | op | admin | accountant |
|---|---|---|---|---|---|
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,也只限自己帶的梯次)。
/admin/guides)嚮導目錄 CRUD 是後台管理 surface(與嚮導本人的受限 portal 分權),權限 guide: ['read', 'manage'](packages/core/src/permissions-ac.ts):
| action | 用途 | admin | sales | op | accountant |
|---|---|---|---|---|---|
guide.read | 進 /admin/guides 列表 / 檢視卡片 | ✅ | ✅ | — | — |
guide.manage | 新增 / 編輯 / 停用(archive)嚮導 + 開通登入邀請 | ✅ | ✅ | — | — |
page.tsx,requirePermission('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)。inviteGuideAccountAction(guide.manage)產一次性暫時密碼手動轉交;已連結顯示「已開通」badge。連結建立即 §2.1 的 guides.user_id。packages/core/src/guides/admin-repo.ts(listGuides / createGuide / updateGuide / archiveGuide / inviteGuideAccount)。目錄 CRUD 用
guide.*(後台管理);嚮導本人 portal 用guide_portal.read(§4.2)—— 兩套權限、兩個 surface,別混。帶團排班(指派 / 換領隊)是第三套guide_roster.*,見 guide-roster §4.2。
/admin/guides 卡片「開通登入」邀請,inviteGuideAccount 建 user.role='guide' + 連結 guides.user_id(一對一,競態守衛 user_id IS NULL),印一次性暫時密碼手動轉交(比照 staff 邀請)。嚮導屬 staff-tier,2FA 一律強制(/2fa-setup 接受所有 staff-tier,含 guide)。departure_cost.create);是否限協作只填成本、不看全名單,待定。departure_costs / expense_proofs 關聯;保存策略與代收轉付收據分流,待 travelinvoice 對齊。| 版本 | 日期 | 變更 | Commit |
|---|---|---|---|
| 1 | 2026-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/*)。 | — |
| 2 | 2026-06-24 | §3.2 嚮導申報加覆核 gate(覆寫初版「覆核在後台」的隱性「送出即生效」):嚮導送出 review_status='pending' 不計入帳,會計 ledger.approve(approver≠handler)後 approved(代入帳表),approved 嚮導代墊一鍵轉請款 payable(payee=guide) 進請款管理退款。後台 / 團控直接登記者 approved。對齊 internal-external-ledger §2.1.3。未實作。 | — |
| 3 | 2026-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。未實作。 | |
| 4 | 2026-06-28 | 全鏈實作落地。 guides.user_id(nullable unique FK,set null)+ guide 角色 / guide_portal.read grant。安全核心 packages/core/src/guide-portal/{scope,portal-repo}.ts:resolveGuideForUser / 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)+ revealGuideTravelerPiiAction(traveler.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 閘)。 | — |
| 5 | 2026-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(RevealSchema 僅 idNumber/passportNo),電話 / 緊急聯絡為明文低敏、UI 遮罩不解密。跨梯次排班另立 guide-roster.md。 | — |
| 6 | 2026-07-02 | §2.1 code 落地:新增 guides.engagement_type(full_time/partner/external,DB CHECK)+ 後台目錄表單 <Select> + 名錄 <FilterSelect> 篩選,欄位表補一列。解除 D2 僱用型態 code follow-up。 | — |