一句話:嚮導 / 領隊專屬的受限 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 對齊。