工程規格與改動清單
以下對應現有 internx-me/frontend 的檔案。這是建議的最小改動路徑,不是要求重寫。
1 · 票券資料模型(核心)
目前票券只有名稱與價格,缺少「販售時間」與「數量」。建議把 feeItems 從 { name, price } 擴充為完整票券物件。
現況 · lib/form-schema/activity-form-schema.ts:328
feeItems: Array<{ name: string; price: number }> — 無時間、無數量,故早鳥過期仍可選、無法限量。
// 建議的票券型別(新增 data/ticket.ts,或擴充 activity.ts) interface Ticket { id: string; name: string; // 票種名稱 price: number; // 0 = 免費 quantity: number | null; // + 數量上限,null = 不限 sold: number; // + 已售(後端維護,前端唯讀) saleStart: Timestamp; // + 販售開始 saleEnd: Timestamp; // + 販售結束 description?: string; order: number; // 排序用 }
狀態由系統計算,不另存欄位
報名端與編輯端共用同一個函式,避免主辦方手動上下架。
function ticketStatus(t: Ticket, now = new Date()) { if (t.quantity != null && t.sold >= t.quantity) return 'soldout'; // 已售完 if (now < t.saleStart) return 'soon'; // 尚未開賣 if (now > t.saleEnd) return 'ended'; // 販售已截止 return 'live'; // 販售中(唯一可購買) }
報名送出時後端需再驗一次 ticketStatus === 'live' 且 sold < quantity,避免前端被繞過或併發超賣。
2 · 表單驗證調整
activity-form-schema.ts 的 feeItems.validation 需加上時間與數量檢查:
| 規則 | 訊息 |
|---|---|
saleStart < saleEnd | 販售開始需早於結束時間 |
quantity == null || quantity >= 1 | 數量上限需 ≥ 1,或留空表示不限 |
price >= 0 | (現有)價格不可為負 |
| 付費活動至少 1 張票 | 請至少新增一個票種 |
原本獨立的 feeAmount(單一費用)與 參加名額上限(schema:371)可由票券模型取代;保留與否視既有報名資料相容性決定。
3 · 報名表單排序:用拖曳(@dnd-kit)
現況 · components/Activities/FormBuilder/FormBuilder.tsx:88
已有 handleReorderFields(fromIndex, toIndex),但拖曳是自製的、不穩、手機難用,所以才難實作。
改法:保留既有的 handleReorderFields,改用成熟套件 @dnd-kit 來驅動拖曳。內建指標 / 觸控 / 鍵盤支援,工程端不用自己處理拖曳事件,抓把手就能順暢拖曳。
npm i @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
// FormPreview:用 SortableContext 包欄位,handleReorderFields 不用改 <DndContext onDragEnd={({ active, over }) => { if (over && active.id !== over.id) { const from = fields.findIndex(f => f.id === active.id); const to = fields.findIndex(f => f.id === over.id); handleReorderFields(from, to); // 既有函式直接重用 } }}> <SortableContext items={fields.map(f => f.id)}> {fields.map(f => <SortableFieldRow key={f.id} field={f} />)} </SortableContext> </DndContext> // SortableFieldRow 用 useSortable({id}) 取得把手 listeners,套到 grip 圖示上。
系統必填欄位(姓名 / Email)標記 locked:不給拖曳把手、固定最上方,並用 dnd-kit 的 modifiers 限制不可被拖到其上方。完整步驟見 INTEGRATION.md。
4 · 改動檔案清單
| 檔案 | 動作 | 說明 |
|---|---|---|
data/ticket.ts | 新增 | Ticket 型別 + ticketStatus(),編輯/報名端共用 |
data/activity.ts | 擴充 | 活動關聯 tickets[](取代或相容 feeItems) |
lib/form-schema/activity-form-schema.ts | 改 | feeItems 改為票券物件、新增驗證(:328、:371) |
components/Activities/AddActivity.tsx | 沿用 | 維持步驟式結構;票券步驟換成新的編輯器卡片 |
components/Activities/FormBuilder/FormPreview.tsx | 改 | 每列加上 ↑ ↓ 按鈕(呼叫既有 reorder) |
components/Payments/Payments.tsx | 沿用 | 金流邏輯不變,僅金額來源改自所選票券 |
data/registration.ts | 擴充 | 報名記錄需存 ticketId 與張數 |
5 · 驗收標準
本交接檔為獨立 repo,與 internx.me 程式碼分離;可作為 PR 描述與設計依據附在工程任務上。