From 9b28fbcfb6cb03d30269141de90afa253a21a1f9 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Sun, 24 May 2026 20:54:10 -0700 Subject: [PATCH] Initial commit: monorepo scaffold for ROLAC - Add .gitignore covering C#/.NET and Angular/Node - Add placeholder structure for API (C#) and APP (Angular) - Add project docs Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 93 +++ API/.gitkeep | 0 APP/.gitkeep | 0 docs/BOARD_PRESENTATION.md | 205 +++++ docs/DB_SCHEMA.md | 1097 ++++++++++++++++++++++++++ docs/INFRASTRUCTURE.md | 462 +++++++++++ docs/IRS_RECEIPT.md | 156 ++++ docs/NOTIFICATIONS.md | 157 ++++ docs/PLANNING.md | 1466 +++++++++++++++++++++++++++++++++++ docs/REQUIREMENTS_REVIEW.md | 115 +++ docs/UI_ARCHITECTURE.md | 464 +++++++++++ 11 files changed, 4215 insertions(+) create mode 100644 .gitignore create mode 100644 API/.gitkeep create mode 100644 APP/.gitkeep create mode 100644 docs/BOARD_PRESENTATION.md create mode 100644 docs/DB_SCHEMA.md create mode 100644 docs/INFRASTRUCTURE.md create mode 100644 docs/IRS_RECEIPT.md create mode 100644 docs/NOTIFICATIONS.md create mode 100644 docs/PLANNING.md create mode 100644 docs/REQUIREMENTS_REVIEW.md create mode 100644 docs/UI_ARCHITECTURE.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd6c0b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,93 @@ +# ============================================================ +# C# / .NET +# ============================================================ +bin/ +obj/ +*.user +*.suo +.vs/ +*.vspscc +*.vssscc +_ReSharper*/ +*.DotSettings.user +TestResults/ +*.ncrunch* +*.pidb +*.swp +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp_proj +*.vsix +publish/ +PublishProfiles/ + +# ASP.NET / EF +appsettings.Development.json +appsettings.Production.json +appsettings.Staging.json +Migrations/ + +# ============================================================ +# Angular / Node +# ============================================================ +node_modules/ +dist/ +.angular/ +.cache/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnp +.pnp.js +*.tsbuildinfo + +# ============================================================ +# Environment & Secrets +# ============================================================ +.env +.env.* +*.pfx +*.p12 +secrets.json +serviceAccountKey.json + +# ============================================================ +# OS +# ============================================================ +.DS_Store +Thumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# ============================================================ +# IDE +# ============================================================ +.idea/ +*.iml +.vscode/ +*.code-workspace + +# ============================================================ +# Claude Code (local settings only) +# ============================================================ +.claude/settings.local.json + +# ============================================================ +# Logs & Temp +# ============================================================ +logs/ +*.log +*.tmp +*.temp diff --git a/API/.gitkeep b/API/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/APP/.gitkeep b/APP/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/BOARD_PRESENTATION.md b/docs/BOARD_PRESENTATION.md new file mode 100644 index 0000000..868bfd9 --- /dev/null +++ b/docs/BOARD_PRESENTATION.md @@ -0,0 +1,205 @@ +# River Of Life Christian Church In Arcadia +# 教會管理系統 — 理事會報告 + +**報告日期:** 2026 年 5 月 24 日 +**系統名稱:** ROLAC Church Management System +**目標上線:** 2026 年 6 月(第一階段) + +--- + +## 一、為什麼需要這套系統? + +| 目前痛點 | 系統解決方案 | +|----------|-------------| +| 教友資料分散於多份 Excel | 集中管理,隨時查詢 | +| 奉獻記錄手工計算,容易出錯 | 系統自動加總,月底一鍵對帳 | +| 年底收據需人工製作 | 自動產生 IRS 合規 PDF,Email 寄出 | +| 支出報銷流程不透明 | 線上申請、財務審核、留存記錄 | +| 教會網站資訊無法即時更新 | 行政同工自行發佈消息,無需工程師 | + +--- + +## 二、系統架構概覽 + +系統分為三個部分,**全部使用同一套系統管理**: + +``` +┌─────────────────────────────────────────────┐ +│ rolac.org(教會公開網站) │ +│ 供訪客、慕道友瀏覽 │ +├─────────────────────────────────────────────┤ +│ 後台管理系統(行政、財務、事工負責人使用) │ +├─────────────────────────────────────────────┤ +│ 教友 App(手機 App,教友查看個人資訊) │ +└─────────────────────────────────────────────┘ +``` + +**支援語言:** 英文 + 繁體中文雙語切換 +**支援設備:** 電腦、平板、手機(iOS + Android App) + +--- + +## 四、開發階段規劃 + +--- + +### 🏗️ 準備階段(現在 → 6 月初,約 2 週) + +> 建立技術基礎,為所有功能打地基。教友不會直接感受到,但這是系統能正常運作的前提。 + +**主要工作:** +- 架設雲端伺服器 +- 建立系統骨架與帳號安全機制 +- 設定自動部署流程 + +--- + +### 🚀 第一階段(Phase 1)— 目標 2026 年 6 月上線 + +> **核心目標:** 網站上線 + 教友資料數位化 + 財務透明化 + +#### 📌 功能 1:教會官方網站(rolac.org) + +| 功能 | 說明 | +|------|------| +| 教會首頁 | 歡迎頁面、教會簡介、異象 | +| 主日資訊 | 聚會時間、地點、Google Maps | +| 消息公告 | 行政同工自行發佈,雙語顯示 | +| 講道影片 | 嵌入 YouTube 最新講道 | +| 聯絡表單 | 訪客留言,通知指定同工 | + +**誰受益:** 訪客、慕道友、全體教友 + +--- + +#### 📌 功能 2:教友管理 + +| 功能 | 說明 | +|------|------| +| 教友資料建檔 | 姓名(中英)、照片、聯絡資料、受洗日期 | +| 家庭單元 | 一個家庭多位成員,清楚呈現家庭關係 | +| 教友狀態 | 會員 / 訪客 / 前會員 分類管理 | +| 搜尋篩選 | 按姓名、狀態、事工快速搜尋 | +| 事工歸屬 | 記錄每位教友參與的事工部門 | + +**誰受益:** 牧師、行政秘書、各事工負責人 + +--- + +#### 📌 功能 3:財務管理(奉獻 + 支出 + 月結) + +**奉獻記錄** + +| 功能 | 說明 | +|------|------| +| 主日奉獻快速錄入 | 財務同工開袋後逐筆輸入,系統即時加總,最後核對實收金額 | +| 支付方式 | 現金、支票(含支票號碼)、Zelle、PayPal | +| 個人奉獻查詢 | 教友可在 App 查看自己的歷年奉獻記錄 | +| **年度奉獻收據** | 年底自動產生 IRS 合規 PDF,Email 寄給每位教友 | + +**支出記錄** + +| 功能 | 說明 | +|------|------| +| 廠商付款 | 直接記錄(如:愛宴外燴支票),含支票號碼 | +| 同工代墊報銷 | 同工提交申請 + 拍收據照片 → 財務審核 → 標記已還款 | +| 支出分類 | 依事工部門 + 類別(餐飲、設備、教材…)分類 | + +**月底對帳** + +| 功能 | 說明 | +|------|------| +| 收支月結報表 | 期初餘額 + 奉獻收入 − 各項支出 = 帳面結餘 | +| 銀行對帳 | 輸入銀行對帳單餘額,系統顯示差異,目標為零 | + +**誰受益:** 財務同工、牧師、理事會 + +--- + +### 📦 第二階段(Phase 2) + +| 功能模組 | 說明 | +|----------|------| +| **服事表排班** | 各事工排班、自動提醒(服事前 3 天通知)、缺席替換 | +| **主日出席統計** | 每週記錄大人 / 青少年 / 兒童人數,產生趨勢圖 | +| **小組管理** | 管理小組架構、組長指派、小組聚會出席記錄 | + +### 📦 第三階段(Phase 3) + +| 功能模組 | 說明 | +|----------|------| +| **事工預算** | 各事工年度預算設定,對比實際支出,超支自動警示 | + +### 📦 第四階段(Phase 4) + +| 功能模組 | 說明 | +|----------|------| +| **線上奉獻** | 網站直接刷卡奉獻(Stripe)、定期自動扣款 | + +### ⏸️ 暫緩功能(未來再評估) + +| 功能模組 | 說明 | +|----------|------| +| **敬拜歌曲庫** | 詩歌歌詞管理、敬拜歌單規劃(待 CCLI 版權確認後排入) | +| **事工報表** | 教友成長趨勢、出席率、奉獻摘要圖表 | +| **廣播通知** | 向全體或特定事工群發 Email / 推播 / 簡訊 | + +--- + +## 五、各事工部門對應 + +以下 10 個事工部門已納入系統,各部門負責人擁有專屬的資料存取權限: + +| 事工部門 | 可管理的資料 | +|----------|-------------| +| 行政 | 全教友資料、財務報表 | +| 講道 | 自身服事排班 | +| 司會 | 自身服事排班 | +| 敬拜 | 服事排班、敬拜歌曲庫 | +| PPT/影音 | 服事排班、影音設備支出 | +| 音控 | 服事排班 | +| 場地組 | 服事排班、場地支出 | +| 招待 | 服事排班 | +| 兒牧 | 自身教友資料、兒童出席 | +| 餐飲 | 服事排班、餐飲支出(愛宴) | + +--- + +## 六、用戶角色與權限 + +不同角色的同工看到的功能不同,**確保敏感資料只有授權人員可查看**: + +| 角色 | 可查看的資料範圍 | +|------|----------------| +| 牧師 | 全部,含財務摘要 | +| 理事 | 教友概覽、財務摘要(唯讀)| +| 同工會主席 | 教友資料、服事排班、小組管理、報表 | +| 事工領袖 | 僅限自身事工的教友與排班資料 | +| 區長 | 轄下小組的教友資料 | +| 小組長 | 自身小組成員資料 | +| 財務同工 | 全部奉獻與支出記錄 | +| 行政秘書 | 教友資料、排班管理 | +| 同工 | 自身事工資料、可申請費用報銷 | +| 一般教友 | 個人資料、個人奉獻記錄、公開服事表 | + +--- + +## 七、里程碑時間表 + +``` +Phase 0 → Phase 1 → Phase 2 → Phase 3 → Phase 4 + 準備 上線 服事/ 預算 線上 + 建設 網站+ 小組管理 奉獻 + 教友+ + 財務 +``` + +| 里程碑 | 階段 | +|--------|------| +| 伺服器建置完成 | Phase 0 | +| 教會網站上線(rolac.org)| Phase 1 | +| 教友管理系統上線 | Phase 1 | +| 財務記錄系統上線 | Phase 1 | +| 服事表 + 小組管理 | Phase 2 | +| 事工預算 | Phase 3 | +| 線上奉獻 | Phase 4 | \ No newline at end of file diff --git a/docs/DB_SCHEMA.md b/docs/DB_SCHEMA.md new file mode 100644 index 0000000..832799b --- /dev/null +++ b/docs/DB_SCHEMA.md @@ -0,0 +1,1097 @@ +# ROLAC — Database Schema Design + +**版本:** v1.0 (2026-05-24) +**資料庫:** PostgreSQL 15+ +**ORM:** Entity Framework Core 8 (Code-First Migrations) +**命名慣例:** PascalCase 表名 / 欄位名(EF Core 預設映射到 snake_case PostgreSQL 欄位) + +--- + +## 目錄 + +1. [設計原則](#1-設計原則) +2. [BaseEntity 模式](#2-baseentity-模式) +3. [Phase 1 — Authentication & Identity](#3-authentication--identity) +4. [Phase 1 — Member Management](#4-member-management) +5. [Phase 1 — Ministry(事工部門)](#5-ministry-事工部門) +6. [Phase 1 — CMS](#6-cms) +7. [Phase 1 — Giving & Donations(奉獻)](#7-giving--donations-奉獻) +8. [Phase 1 — Expense Tracking(支出)](#8-expense-tracking-支出) +9. [Phase 1 — Prayer Requests(代禱)](#9-prayer-requests-代禱) +10. [Phase 1 — Audit Log](#10-audit-log) +11. [Phase 1 — Notifications](#11-notifications) +12. [Phase 2 — Service Roster(服事表)](#12-service-roster-服事表) +13. [Phase 2 — Sunday Attendance(主日出席)](#13-sunday-attendance-主日出席) +14. [Phase 2 — Cell Groups(小組)](#14-cell-groups-小組) +15. [Phase 3 — Ministry Budget(事工預算)](#15-ministry-budget-事工預算) +16. [Seed Data](#16-seed-data) +17. [Indexes](#17-indexes) +18. [EF Core 設定摘要](#18-ef-core-設定摘要) + +--- + +## 1. 設計原則 + +| 原則 | 說明 | +|------|------| +| **Code-First** | 全部透過 EF Core Migration 建立,不手寫 DDL | +| **Soft Delete** | 重要實體用 `IsDeleted` 標記刪除,不實際刪除 DB 記錄 | +| **Audit Trail** | 每個可修改實體均記錄 `CreatedAt / CreatedBy / UpdatedAt / UpdatedBy` | +| **Bilingual Fields** | 需要雙語的欄位加 `_en` / `_zh` 後綴(如 `Name_en` / `Name_zh`)| +| **Money** | 所有金額欄位使用 `decimal(18,2)` | +| **Phone** | `varchar(30)`(支援國際格式)| +| **EIN** | 不存入資料庫,由環境變數 `CHURCH_EIN` 提供 | +| **Azure Blob** | 圖片/PDF 只存 Blob 路徑(`varchar(500)`),不存 Base64 | +| **jsonb** | AuditLog 的 Before/After 用 PostgreSQL `jsonb` 儲存 | +| **IDs** | 一般實體用 `int` (SERIAL);User 繼承 ASP.NET Identity 使用 `string (Guid)` | + +--- + +## 2. BaseEntity 模式 + +大部分實體繼承以下基礎類別(透過 EF Core TPH 或 owned type 實現): + +```csharp +// 帶 Audit 的實體(可建立/修改) +public abstract class AuditableEntity +{ + public DateTime CreatedAt { get; set; } + public string CreatedBy { get; set; } = null!; // FK → AppUser.Id + public DateTime UpdatedAt { get; set; } + public string UpdatedBy { get; set; } = null!; // FK → AppUser.Id +} + +// 帶軟刪除的實體 +public abstract class SoftDeleteEntity : AuditableEntity +{ + public bool IsDeleted { get; set; } = false; + public DateTime? DeletedAt { get; set; } + public string? DeletedBy { get; set; } // FK → AppUser.Id +} +``` + +--- + +## 3. Authentication & Identity + +### AppUser(繼承 IdentityUser) + +``` +Table: AspNetUsers (ASP.NET Identity 預設名稱) +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | varchar(450) PK | Guid,由 Identity 管理 | +| UserName | varchar(256) | 使用者名稱(通常等同 Email)| +| NormalizedUserName | varchar(256) | 大寫版本,用於查詢 | +| Email | varchar(256) | 電子郵件 | +| NormalizedEmail | varchar(256) | | +| EmailConfirmed | bool | | +| PasswordHash | varchar(MAX) | | +| SecurityStamp | varchar(MAX) | | +| ConcurrencyStamp | varchar(MAX) | | +| PhoneNumber | varchar(256)? | | +| PhoneNumberConfirmed | bool | | +| TwoFactorEnabled | bool | | +| LockoutEnd | datetimeoffset? | | +| LockoutEnabled | bool | | +| AccessFailedCount | int | | +| **MemberId** | int? | FK → Member.Id(可為 null)| +| **LanguagePreference** | varchar(10) | 'en' \| 'zh-TW',DEFAULT 'en' | +| **IsActive** | bool | DEFAULT true,停用帳號用 | +| **LastLoginAt** | timestamp? | 最後登入時間 | +| **CreatedAt** | timestamp | 帳號建立時間 | + +> Identity 角色表(AspNetRoles, AspNetUserRoles)由 ASP.NET Identity 自動管理。 + +### AppRole(繼承 IdentityRole) + +``` +Table: AspNetRoles +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | varchar(450) PK | Guid | +| Name | varchar(256) | 角色名稱(見下方列表)| +| NormalizedName | varchar(256) | | +| ConcurrencyStamp | varchar(MAX) | | +| **Description** | varchar(500)? | 角色說明 | + +**預設角色(Seed):** + +> ROLAC 為靈糧堂體制,無長老制。 + +| Name | 中文 | 說明 | +|------|------|------| +| super_admin | 系統管理員 | 所有權限 | +| pastor | 牧師 | 全覽教友與財務摘要 | +| board_member | 理事 | 教會治理委員,可查看財務摘要與教友概覽 | +| coworker_chair | 同工會主席 | 統籌各事工領袖,可管理服事與小組 | +| ministry_leader | 事工領袖 | 受 Ministry Scope 限制 | +| district_leader | 區長 | 管理轄下多個小組 | +| cell_leader | 小組長 | 僅限自身小組 | +| coworker | 同工 | 參與指定事工,可申請報銷 | +| finance | 財務同工 | 管理奉獻與支出 | +| secretary | 行政秘書 | 管理教友資料與排班 | +| worship_leader | 敬拜領袖 | 管理歌曲庫與歌單(Phase 暫緩)| +| member | 一般教友 | 查看個人資料與服事表 | +| visitor | 訪客 | 僅限公開頁面 | + +### UserMinistry(Ministry Scope — 用戶管理哪個事工) + +``` +Table: UserMinistries +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| UserId | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| MinistryId | int NOT NULL | FK → Ministries.Id | +| AssignedAt | timestamp NOT NULL | | +| AssignedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| **UNIQUE** | (UserId, MinistryId) | 一個用戶不重複指派同一事工 | + +### UserDevice(FCM 推播 Token) + +``` +Table: UserDevices +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| UserId | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| FcmToken | varchar(500) NOT NULL | Firebase Cloud Messaging token | +| Platform | varchar(20) NOT NULL | 'ios' \| 'android' \| 'web' | +| DeviceName | varchar(100)? | 裝置名稱(選填)| +| LastSeenAt | timestamp NOT NULL | | +| IsActive | bool NOT NULL DEFAULT true | | +| CreatedAt | timestamp NOT NULL | | + +--- + +## 4. Member Management + +### Member(教友資料) + +``` +Table: Members +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| FirstName_en | varchar(100) NOT NULL | 英文名 | +| LastName_en | varchar(100) NOT NULL | 英文姓 | +| FirstName_zh | varchar(100)? | 中文名 | +| LastName_zh | varchar(100)? | 中文姓 | +| Gender | varchar(10)? | 'M' \| 'F' \| 'Other' | +| DateOfBirth | date? | 生日 | +| BaptismDate | date? | 受洗日期 | +| BaptismChurch | varchar(200)? | 受洗教會 | +| Email | varchar(200)? | | +| PhoneCell | varchar(30)? | 手機 | +| PhoneHome | varchar(30)? | 家電 | +| Address | varchar(500)? | 地址 | +| City | varchar(100)? | | +| State | varchar(50)? | | +| ZipCode | varchar(20)? | | +| Country | varchar(100) NOT NULL DEFAULT 'USA' | | +| PhotoBlobPath | varchar(500)? | Azure Blob 路徑 | +| Status | varchar(20) NOT NULL DEFAULT 'Member' | 'Member' \| 'Visitor' \| 'Inactive' \| 'Former' | +| LanguagePreference | varchar(10) NOT NULL DEFAULT 'en' | 'en' \| 'zh-TW' | +| JoinDate | date? | 加入教會日期 | +| Notes | text? | 內部備注 | +| FamilyUnitId | int? | FK → FamilyUnits.Id | +| IsDeleted | bool NOT NULL DEFAULT false | 軟刪除 | +| DeletedAt | timestamp? | | +| DeletedBy | varchar(450)? | FK → AspNetUsers.Id | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +### FamilyUnit(家庭單元) + +``` +Table: FamilyUnits +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| FamilyName_en | varchar(200)? | 家庭英文名(如 "Chang Family")| +| FamilyName_zh | varchar(200)? | 家庭中文名(如「張家」)| +| Notes | text? | | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +> 一個 FamilyUnit 可有多個 Members(One-to-Many via `Member.FamilyUnitId`) + +### MemberMinistry(教友服事事工關聯 — M:N) + +``` +Table: MemberMinistries +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| MemberId | int NOT NULL | FK → Members.Id | +| MinistryId | int NOT NULL | FK → Ministries.Id | +| MinistryRole | varchar(50)? | 在事工中的角色(Leader/Member/Coordinator)| +| JoinedAt | date? | | +| Notes | varchar(200)? | | +| **UNIQUE** | (MemberId, MinistryId) | | + +### MemberTag(教友標籤) + +``` +Table: MemberTags +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| MemberId | int NOT NULL | FK → Members.Id | +| Tag | varchar(100) NOT NULL | e.g., 'NewBeliever', 'Volunteer', 'Youth' | +| **UNIQUE** | (MemberId, Tag) | | + +--- + +## 5. Ministry(事工部門) + +``` +Table: Ministries +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| Name_en | varchar(200) NOT NULL | | +| Name_zh | varchar(200)? | | +| Description_en | text? | | +| Description_zh | text? | | +| SortOrder | int NOT NULL DEFAULT 0 | 顯示排序 | +| IsActive | bool NOT NULL DEFAULT true | | + +--- + +## 6. CMS + +### CmsPage(靜態頁面:關於我們、異象等) + +``` +Table: CmsPages +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| Slug | varchar(100) UNIQUE NOT NULL | URL 識別碼,如 'about', 'vision' | +| Title_en | varchar(300) NOT NULL | | +| Title_zh | varchar(300)? | | +| Body_en | text? | Markdown 或 HTML | +| Body_zh | text? | | +| IsPublished | bool NOT NULL DEFAULT false | | +| PublishedAt | timestamp? | | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +### Announcement(消息公告) + +``` +Table: Announcements +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| Title_en | varchar(300) NOT NULL | | +| Title_zh | varchar(300)? | | +| Body_en | text NOT NULL | | +| Body_zh | text? | | +| PinnedUntil | date? | null = 不置頂 | +| ScheduledAt | timestamp? | null = 立即發佈 | +| IsPublished | bool NOT NULL DEFAULT false | | +| PublishedAt | timestamp? | | +| ImageBlobPath | varchar(500)? | 封面圖 Azure Blob 路徑 | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +### SermonVideo(講道影片 — YouTube 嵌入) + +``` +Table: SermonVideos +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| Title_en | varchar(300) NOT NULL | | +| Title_zh | varchar(300)? | | +| YouTubeVideoId | varchar(50) NOT NULL | YouTube 影片 ID(非完整 URL)| +| PreacherName | varchar(200)? | 講員姓名 | +| SermonDate | date NOT NULL | 講道日期 | +| Description_en | text? | | +| Description_zh | text? | | +| IsPublished | bool NOT NULL DEFAULT true | | +| SortOrder | int NOT NULL DEFAULT 0 | 顯示排序(新到舊)| +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +### ContactInquiry(聯絡表單) + +``` +Table: ContactInquiries +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| Name | varchar(200) NOT NULL | | +| Email | varchar(200) NOT NULL | | +| Phone | varchar(30)? | | +| Subject | varchar(300)? | | +| Message | text NOT NULL | | +| Language | varchar(10) NOT NULL DEFAULT 'en' | 表單填寫語言 | +| IsRead | bool NOT NULL DEFAULT false | | +| ReadAt | timestamp? | | +| ReadBy | varchar(450)? | FK → AspNetUsers.Id | +| AssignedTo | varchar(450)? | FK → AspNetUsers.Id | +| InternalNotes | text? | 後台同工備注 | +| CreatedAt | timestamp NOT NULL DEFAULT now() | | + +--- + +## 7. Giving & Donations(奉獻) + +### GivingCategory(奉獻類型) + +``` +Table: GivingCategories +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| Name_en | varchar(200) NOT NULL | e.g., 'Tithe', 'General Offering', 'Building Fund' | +| Name_zh | varchar(200)? | e.g., '什一奉獻', '一般奉獻', '建堂基金' | +| Description_en | varchar(500)? | | +| Description_zh | varchar(500)? | | +| IsActive | bool NOT NULL DEFAULT true | | +| SortOrder | int NOT NULL DEFAULT 0 | | + +### OfferingSession(主日奉獻袋批次作業) + +``` +Table: OfferingSessions +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| SessionDate | date NOT NULL | 主日日期 | +| Status | varchar(20) NOT NULL DEFAULT 'Draft' | 'Draft' \| 'Submitted' \| 'Reconciled' | +| CashTotal | decimal(18,2) NOT NULL DEFAULT 0 | 財務同工清點現金總額(手輸)| +| CheckTotal | decimal(18,2) NOT NULL DEFAULT 0 | 支票加總總額(手輸)| +| SystemTotal | decimal(18,2) NOT NULL DEFAULT 0 | 系統計算奉獻條目加總(觸發或計算)| +| Difference | decimal(18,2) NOT NULL DEFAULT 0 | = (CashTotal + CheckTotal) - SystemTotal | +| Notes | text? | | +| SubmittedAt | timestamp? | | +| SubmittedBy | varchar(450)? | FK → AspNetUsers.Id | +| ReconciledAt | timestamp? | | +| ReconciledBy | varchar(450)? | FK → AspNetUsers.Id | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| **UNIQUE** | (SessionDate) | 一個主日只有一個 Session | + +> **鍵盤優先 UI 設計:** 財務同工 Tab/Enter 跳欄逐筆輸入,每筆加入後右側即時更新加總;最後輸入 CashTotal / CheckTotal 人工清點金額,系統計算 Difference,目標為零後點「提交」。 + +### Giving(奉獻記錄) + +``` +Table: Givings +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| MemberId | int? | FK → Members.Id;null = 匿名 | +| GivingCategoryId | int NOT NULL | FK → GivingCategories.Id | +| OfferingSessionId | int? | FK → OfferingSessions.Id;null = 非批次單筆 | +| Amount | decimal(18,2) NOT NULL | 用於 IRS 收據的金額(PayPal 用 NetAmount)| +| GrossAmount | decimal(18,2)? | PayPal 手續費前總額 | +| FeeAmount | decimal(18,2)? | PayPal/Stripe 手續費 | +| PaymentMethod | varchar(20) NOT NULL | 'Cash' \| 'Check' \| 'Zelle' \| 'PayPal' \| 'Stripe' \| 'Other' | +| CheckNumber | varchar(50)? | 支票號碼 | +| ZelleReferenceCode | varchar(100)? | Zelle 參考碼(手動輸入)| +| PayPalTransactionId | varchar(100)? | PayPal 交易 ID | +| StripePaymentIntentId | varchar(200)? | Stripe PaymentIntent ID(Phase 4)| +| GivingDate | date NOT NULL | 奉獻日期 | +| IsAnonymous | bool NOT NULL DEFAULT false | | +| Notes | varchar(500)? | | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +> **IRS 規則:** 匿名奉獻(`IsAnonymous = true` 或 `MemberId = null`)不計入個人年度收據。 +> **PayPal:** `Amount` = `GrossAmount` - `FeeAmount`(Net Amount 才用於 IRS)。 + +### GivingReceipt(年度 IRS 奉獻收據) + +``` +Table: GivingReceipts +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| MemberId | int NOT NULL | FK → Members.Id | +| FiscalYear | int NOT NULL | 年份(如 2026)| +| TotalAmount | decimal(18,2) NOT NULL | 該年度奉獻總額 | +| GivingCount | int NOT NULL | 奉獻筆數 | +| PdfBlobPath | varchar(500)? | Azure Blob 路徑(receipts/{year}/{memberId}.pdf)| +| GeneratedAt | timestamp? | PDF 產生時間 | +| SentAt | timestamp? | Email 寄出時間 | +| SentToEmail | varchar(200)? | 寄送 Email 位址 | +| IsVoided | bool NOT NULL DEFAULT false | 作廢(保留 PDF,不再有效)| +| VoidReason | varchar(500)? | | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| **UNIQUE** | (MemberId, FiscalYear) | 每人每年只一份正式收據 | + +### GivingRecurringSchedule(定期奉獻排程 — Phase 4) + +``` +Table: GivingRecurringSchedules +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| MemberId | int NOT NULL | FK → Members.Id | +| GivingCategoryId | int NOT NULL | FK → GivingCategories.Id | +| Amount | decimal(18,2) NOT NULL | | +| Frequency | varchar(20) NOT NULL | 'Weekly' \| 'BiWeekly' \| 'Monthly' | +| StripeSubscriptionId | varchar(200)? | Stripe Subscription ID | +| StartDate | date NOT NULL | | +| EndDate | date? | null = 持續進行 | +| IsActive | bool NOT NULL DEFAULT true | | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +--- + +## 8. Expense Tracking(支出) + +### ExpenseCategoryGroup(支出大類) + +``` +Table: ExpenseCategoryGroups +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| Name_en | varchar(200) NOT NULL | | +| Name_zh | varchar(200)? | | +| SortOrder | int NOT NULL DEFAULT 0 | | +| IsActive | bool NOT NULL DEFAULT true | | + +**Seed 大類(10 個):** + +| Id | Name_en | Name_zh | +|----|---------|---------| +| 1 | Equipment | 設備 | +| 2 | Consumables | 消耗品 | +| 3 | Food & Beverage | 餐飲 | +| 4 | Training | 培訓 | +| 5 | Materials | 教材 | +| 6 | Facility | 場地 | +| 7 | Printing | 印刷 | +| 8 | Missions | 宣教 | +| 9 | Benevolence | 關懷救助 | +| 10 | Other | 其他 | + +### ExpenseSubCategory(支出子項目) + +``` +Table: ExpenseSubCategories +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| GroupId | int NOT NULL | FK → ExpenseCategoryGroups.Id | +| Name_en | varchar(200) NOT NULL | | +| Name_zh | varchar(200)? | | +| SortOrder | int NOT NULL DEFAULT 0 | | +| IsActive | bool NOT NULL DEFAULT true | | + +**Seed 子項目(部分範例):** + +| GroupId | Name_en | Name_zh | +|---------|---------|---------| +| 1 Equipment | Audio/Visual Equipment | 影音設備 | +| 1 Equipment | Computer & Peripherals | 電腦周邊 | +| 1 Equipment | Musical Instruments | 樂器 | +| 2 Consumables | Office Supplies | 辦公用品 | +| 2 Consumables | Cleaning Supplies | 清潔用品 | +| 3 Food & Beverage | Sunday Agape Meal | 愛宴餐費 | +| 3 Food & Beverage | Fellowship Snacks | 交誼茶點 | +| 4 Training | Conference Registration | 會議報名費 | +| 4 Training | Books & Resources | 書籍教材 | +| 5 Materials | Children's Curriculum | 兒童教材 | +| 5 Materials | Bibles & Hymnals | 聖經/詩本 | +| 6 Facility | Rent | 場地租金 | +| 6 Facility | Utilities | 水電費 | +| 7 Printing | Bulletins | 週報印刷 | +| 8 Missions | Missionary Support | 宣教士支持 | + +### Expense(支出記錄) + +``` +Table: Expenses +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| MinistryId | int NOT NULL | FK → Ministries.Id | +| CategoryGroupId | int NOT NULL | FK → ExpenseCategoryGroups.Id | +| SubCategoryId | int NOT NULL | FK → ExpenseSubCategories.Id | +| Type | varchar(30) NOT NULL | 'VendorPayment' \| 'StaffReimbursement' | +| Status | varchar(30) NOT NULL DEFAULT 'Draft' | 見下方說明 | +| Amount | decimal(18,2) NOT NULL | | +| Description | varchar(500) NOT NULL | 費用說明 | +| VendorName | varchar(200)? | 廠商名稱(VendorPayment 用)| +| CheckNumber | varchar(50)? | 付款支票號碼 | +| ExpenseDate | date NOT NULL | 費用日期 | +| ReceiptBlobPath | varchar(500)? | 收據照片 Azure Blob 路徑 | +| Notes | text? | | +| SubmittedBy | varchar(450)? | FK → AspNetUsers.Id(申請報銷的同工)| +| SubmittedAt | timestamp? | | +| ReviewedBy | varchar(450)? | FK → AspNetUsers.Id(財務審核人)| +| ReviewedAt | timestamp? | | +| ReviewNotes | varchar(500)? | 審核備注 | +| PaidAt | timestamp? | 標記已付款時間 | +| PaidBy | varchar(450)? | FK → AspNetUsers.Id | +| IsDeleted | bool NOT NULL DEFAULT false | | +| DeletedAt | timestamp? | | +| DeletedBy | varchar(450)? | FK → AspNetUsers.Id | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +**Status 工作流程:** + +``` +VendorPayment: + 財務直接建立 → Status = 'Paid'(無需審核) + +StaffReimbursement: + 同工提交 → 'Draft' + 提交審核 → 'PendingApproval' + 財務審核通過 → 'Approved' + 標記已還款 → 'Paid' + 財務拒絕 → 'Rejected' +``` + +### MonthlyStatement(月底對帳表) + +``` +Table: MonthlyStatements +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| Year | int NOT NULL | | +| Month | int NOT NULL | 1–12 | +| OpeningBalance | decimal(18,2) NOT NULL | 期初餘額(手輸)| +| TotalGiving | decimal(18,2) NOT NULL | 系統計算:本月奉獻合計 | +| TotalOtherIncome | decimal(18,2) NOT NULL DEFAULT 0 | 其他收入(手輸)| +| TotalExpenses | decimal(18,2) NOT NULL | 系統計算:本月已付支出合計 | +| CalculatedClosingBalance | decimal(18,2) NOT NULL | = OpeningBalance + TotalGiving + TotalOtherIncome − TotalExpenses | +| BankStatementBalance | decimal(18,2) NOT NULL | 銀行對帳單期末餘額(手輸)| +| Difference | decimal(18,2) NOT NULL | = CalculatedClosingBalance − BankStatementBalance(目標 = 0)| +| Notes | text? | | +| IsFinalized | bool NOT NULL DEFAULT false | 定稿後鎖定,不允許修改 | +| FinalizedAt | timestamp? | | +| FinalizedBy | varchar(450)? | FK → AspNetUsers.Id | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| **UNIQUE** | (Year, Month) | 每個月只有一份月結報表 | + +--- + +## 9. Prayer Requests(代禱) + +### PrayerRequest(代禱事項) + +``` +Table: PrayerRequests +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| RequestedByMemberId | int NOT NULL | FK → Members.Id(提交人)| +| Title | varchar(300) NOT NULL | | +| Content | text NOT NULL | | +| Visibility | varchar(20) NOT NULL | 'Private' \| 'CellGroup' \| 'AllLeaders' \| 'Public' | +| IsAnswered | bool NOT NULL DEFAULT false | 已應允 | +| AnsweredAt | timestamp? | | +| ExpiresAt | date? | 可選到期日(過期不顯示)| +| IsDeleted | bool NOT NULL DEFAULT false | | +| DeletedAt | timestamp? | | +| DeletedBy | varchar(450)? | FK → AspNetUsers.Id | +| CreatedAt | timestamp NOT NULL | | +| UpdatedAt | timestamp NOT NULL | | + +**Visibility 規則:** + +| Visibility | 可見對象 | +|------------|---------| +| Private | 僅本人 + pastor + super_admin | +| CellGroup | 本人所在小組成員 + cell_leader + district_leader + pastor | +| AllLeaders | ministry_leader / district_leader / coworker_chair / board_member / pastor 及以上 | +| Public | 全體教友(登入後可見)| + +### PrayerFollow(代禱跟進記錄) + +``` +Table: PrayerFollows +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| PrayerRequestId | int NOT NULL | FK → PrayerRequests.Id | +| FollowedByUserId | varchar(450) NOT NULL | FK → AspNetUsers.Id(牧者/組長)| +| Note | text? | 跟進備注 | +| CreatedAt | timestamp NOT NULL | | + +--- + +## 10. Audit Log + +### AuditLog(稽核記錄 — 不可修改/刪除) + +``` +Table: AuditLogs +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | bigint PK | 使用 bigint(記錄量大)| +| UserId | varchar(450)? | FK → AspNetUsers.Id;null = 系統自動操作 | +| Action | varchar(50) NOT NULL | 'Create' \| 'Update' \| 'Delete' \| 'Login' \| 'Logout' \| 'Export' \| 'ViewSensitive' | +| EntityType | varchar(100) NOT NULL | 實體名稱(如 'Member', 'Giving', 'Expense')| +| EntityId | varchar(100)? | 受影響實體的主鍵值 | +| OldValues | jsonb? | 修改前的 JSON 快照 | +| NewValues | jsonb? | 修改後的 JSON 快照 | +| IpAddress | varchar(45)? | IPv4 或 IPv6 | +| UserAgent | varchar(500)? | 瀏覽器/App 資訊 | +| CreatedAt | timestamp NOT NULL DEFAULT now() | | + +> **重要:** AuditLog 永遠只有 INSERT,永遠不 UPDATE 或 DELETE。 + +--- + +## 11. Notifications + +### NotificationLog(通知發送記錄) + +``` +Table: NotificationLogs +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| UserId | varchar(450)? | FK → AspNetUsers.Id | +| MemberId | int? | FK → Members.Id | +| Channel | varchar(20) NOT NULL | 'Email' \| 'Push' \| 'SMS' \| 'InApp' | +| TemplateKey | varchar(100) NOT NULL | 通知模板 Key(如 'giving.receipt.sent')| +| Subject | varchar(300)? | Email 主旨 | +| SentToAddress | varchar(200)? | Email / 手機號 / FCM Token | +| Status | varchar(20) NOT NULL | 'Sent' \| 'Failed' \| 'Queued' | +| ErrorMessage | varchar(500)? | 失敗原因 | +| RelatedEntityType | varchar(100)? | 關聯實體類型 | +| RelatedEntityId | varchar(100)? | 關聯實體 ID | +| CreatedAt | timestamp NOT NULL DEFAULT now() | | + +--- + +## 12. Service Roster(服事表 — Phase 2) + +### ServiceSlot(服事項目定義) + +``` +Table: ServiceSlots +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| MinistryId | int NOT NULL | FK → Ministries.Id | +| Name_en | varchar(200) NOT NULL | e.g., 'Worship Leader' | +| Name_zh | varchar(200)? | e.g., '敬拜帶領' | +| RequiredCount | int NOT NULL DEFAULT 1 | 每次需要幾人 | +| IsActive | bool NOT NULL DEFAULT true | | +| SortOrder | int NOT NULL DEFAULT 0 | | + +**Seed 服事項目(16 個主日服事槽,參見 PLANNING.md §3.4):** + +| MinistryId | Name_en | Name_zh | +|------------|---------|---------| +| 2 Preaching | Preacher | 講員 | +| 3 Emcee | Emcee | 司會 | +| 4 Worship | Worship Leader | 敬拜帶領 | +| 4 Worship | Keyboard / Piano | 鍵盤/鋼琴 | +| 4 Worship | Guitar | 吉他 | +| 4 Worship | Bass | 貝斯 | +| 4 Worship | Drums | 爵士鼓 | +| 4 Worship | Vocalist | 詩班 | +| 5 PPT/Media | PPT Operator | PPT 操作 | +| 5 PPT/Media | Livestream | 直播 | +| 6 Sound | Sound Engineer | 音控 | +| 7 Facility | Setup Lead | 場地佈置組長 | +| 7 Facility | Setup Team | 場地佈置組員 | +| 8 Hospitality | Greeter | 招待接待 | +| 9 Children | Children's Teacher | 兒童老師 | +| 10 Catering | Agape Meal Coord. | 愛宴負責人 | + +### ServiceAssignment(服事排班記錄) + +``` +Table: ServiceAssignments +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| ServiceSlotId | int NOT NULL | FK → ServiceSlots.Id | +| MemberId | int NOT NULL | FK → Members.Id | +| ServiceDate | date NOT NULL | 服事日期(通常為主日)| +| Status | varchar(20) NOT NULL DEFAULT 'Scheduled' | 'Scheduled' \| 'Confirmed' \| 'Absent' \| 'Replaced' | +| ReplacedByMemberId | int? | FK → Members.Id(替補人選)| +| Notes | varchar(200)? | | +| ReminderSentAt | timestamp? | 提醒通知發送時間 | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +--- + +## 13. Sunday Attendance(主日出席 — Phase 2) + +``` +Table: SundayAttendances +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| AttendanceDate | date NOT NULL UNIQUE | 主日日期 | +| AdultCount | int NOT NULL DEFAULT 0 | 大人 | +| TeenagerCount | int NOT NULL DEFAULT 0 | 青少年 | +| ChildrenCount | int NOT NULL DEFAULT 0 | 兒童 | +| TotalCount | int NOT NULL | 計算欄位(= Adult + Teenager + Children)| +| Notes | varchar(500)? | | +| RecordedByUserId | varchar(450) NOT NULL | FK → AspNetUsers.Id(行政秘書)| +| CreatedAt | timestamp NOT NULL | | +| UpdatedAt | timestamp NOT NULL | | + +--- + +## 14. Cell Groups(小組 — Phase 2) + +### CellGroup(小組) + +``` +Table: CellGroups +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| Name_en | varchar(200) NOT NULL | | +| Name_zh | varchar(200)? | | +| ParentGroupId | int? | FK → CellGroups.Id(巢狀小組)| +| LeaderMemberId | int? | FK → Members.Id(組長)| +| CoLeaderMemberId | int? | FK → Members.Id(副組長)| +| MeetingDay | varchar(20)? | 'Monday' \| 'Tuesday' \| ... | +| MeetingTime | time? | | +| MeetingLocation | varchar(200)? | | +| Description_en | text? | | +| Description_zh | text? | | +| IsActive | bool NOT NULL DEFAULT true | | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +### CellGroupMembership(小組成員 — M:N) + +``` +Table: CellGroupMemberships +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| CellGroupId | int NOT NULL | FK → CellGroups.Id | +| MemberId | int NOT NULL | FK → Members.Id | +| JoinedAt | date? | | +| LeftAt | date? | null = 仍在小組中 | +| IsActive | bool NOT NULL DEFAULT true | | +| **UNIQUE** | (CellGroupId, MemberId) WHERE IsActive = true | | + +### CellGroupMeeting(小組聚會記錄) + +``` +Table: CellGroupMeetings +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| CellGroupId | int NOT NULL | FK → CellGroups.Id | +| MeetingDate | date NOT NULL | | +| Topic | varchar(300)? | 聚會主題 | +| Notes | text? | | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | + +### CellGroupAttendance(小組出席 — 個人層級) + +``` +Table: CellGroupAttendances +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| MeetingId | int NOT NULL | FK → CellGroupMeetings.Id | +| MemberId | int NOT NULL | FK → Members.Id | +| IsPresent | bool NOT NULL DEFAULT true | 出席 / 缺席 | +| Notes | varchar(200)? | | +| **UNIQUE** | (MeetingId, MemberId) | | + +--- + +## 15. Ministry Budget(事工預算 — Phase 3) + +``` +Table: MinistryBudgets +``` + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | | +| MinistryId | int NOT NULL | FK → Ministries.Id | +| CategoryGroupId | int NOT NULL | FK → ExpenseCategoryGroups.Id | +| SubCategoryId | int? | FK → ExpenseSubCategories.Id;null = 整個大類的預算 | +| FiscalYear | int NOT NULL | 財政年度(如 2027)| +| BudgetAmount | decimal(18,2) NOT NULL | | +| Notes | varchar(500)? | | +| CreatedAt | timestamp NOT NULL | | +| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| UpdatedAt | timestamp NOT NULL | | +| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id | +| **UNIQUE** | (MinistryId, CategoryGroupId, SubCategoryId, FiscalYear) | | + +> **備注:** 此表 Phase 3 才建立,但 `ExpenseCategoryGroups`、`ExpenseSubCategories`、`Ministries` 表已在 Phase 1 建好,無需 Schema 改動。 + +--- + +## 16. Seed Data + +以下資料在 `DbInitializer`(或 `HasData` Migration)中植入: + +### Roles(角色) +``` +super_admin, pastor, board_member, coworker_chair, ministry_leader, district_leader, cell_leader, coworker, finance, secretary, worship_leader, member, visitor +``` + +### Ministries(10 個事工部門) +``` +1. Administration / 行政 +2. Preaching / 講道 +3. Emcee / 司會 +4. Worship / 敬拜 +5. PPT/Media / PPT/影音 +6. Sound / 音控 +7. Facility / 場地組 +8. Hospitality / 招待 +9. Children / 兒牧 +10. Catering / 餐飲 +``` + +### GivingCategories(奉獻類型) +``` +1. Tithe / 什一奉獻 +2. General Offering / 一般奉獻 +3. Special Offering / 特別奉獻 +4. Building Fund / 建堂基金 +5. Mission / 宣教奉獻 +``` + +### ExpenseCategoryGroups(10 個大類) +``` +見 §8 Seed 大類列表 +``` + +### CmsPages(靜態頁面 Slug) +``` +about, vision, service-times, contact +``` + +--- + +## 17. Indexes + +```sql +-- Members +CREATE INDEX idx_members_status ON "Members" ("Status") WHERE "IsDeleted" = false; +CREATE INDEX idx_members_family_unit ON "Members" ("FamilyUnitId"); +CREATE INDEX idx_members_email ON "Members" ("Email") WHERE "Email" IS NOT NULL; + +-- Giving +CREATE INDEX idx_givings_member_date ON "Givings" ("MemberId", "GivingDate"); +CREATE INDEX idx_givings_session ON "Givings" ("OfferingSessionId") WHERE "OfferingSessionId" IS NOT NULL; +CREATE INDEX idx_givings_date ON "Givings" ("GivingDate"); + +-- Expense +CREATE INDEX idx_expenses_ministry ON "Expenses" ("MinistryId"); +CREATE INDEX idx_expenses_status ON "Expenses" ("Status") WHERE "IsDeleted" = false; +CREATE INDEX idx_expenses_date ON "Expenses" ("ExpenseDate"); + +-- AuditLog +CREATE INDEX idx_auditlog_user ON "AuditLogs" ("UserId"); +CREATE INDEX idx_auditlog_entity ON "AuditLogs" ("EntityType", "EntityId"); +CREATE INDEX idx_auditlog_created ON "AuditLogs" ("CreatedAt"); + +-- PrayerRequest +CREATE INDEX idx_prayer_visibility ON "PrayerRequests" ("Visibility") WHERE "IsDeleted" = false; +CREATE INDEX idx_prayer_member ON "PrayerRequests" ("RequestedByMemberId"); + +-- Phase 2: ServiceAssignment +CREATE INDEX idx_service_assign_date ON "ServiceAssignments" ("ServiceDate"); +CREATE INDEX idx_service_assign_member ON "ServiceAssignments" ("MemberId"); + +-- Phase 2: SundayAttendance +-- UNIQUE constraint on AttendanceDate already creates an index +``` + +--- + +## 18. EF Core 設定摘要 + +### DbContext 範例結構 + +```csharp +public class RolacDbContext : IdentityDbContext +{ + // Phase 1 + public DbSet Members => Set(); + public DbSet FamilyUnits => Set(); + public DbSet MemberMinistries => Set(); + public DbSet MemberTags => Set(); + public DbSet Ministries => Set(); + public DbSet UserMinistries => Set(); + public DbSet UserDevices => Set(); + + public DbSet Announcements => Set(); + public DbSet SermonVideos => Set(); + public DbSet CmsPages => Set(); + public DbSet ContactInquiries => Set(); + + public DbSet GivingCategories => Set(); + public DbSet OfferingSessions => Set(); + public DbSet Givings => Set(); + public DbSet GivingReceipts => Set(); + public DbSet GivingRecurringSchedules => Set(); + + public DbSet ExpenseCategoryGroups => Set(); + public DbSet ExpenseSubCategories => Set(); + public DbSet Expenses => Set(); + public DbSet MonthlyStatements => Set(); + + public DbSet PrayerRequests => Set(); + public DbSet PrayerFollows => Set(); + + public DbSet AuditLogs => Set(); + public DbSet NotificationLogs => Set(); + + // Phase 2 (schema defined now, implemented later) + public DbSet ServiceSlots => Set(); + public DbSet ServiceAssignments => Set(); + public DbSet SundayAttendances => Set(); + public DbSet CellGroups => Set(); + public DbSet CellGroupMemberships => Set(); + public DbSet CellGroupMeetings => Set(); + public DbSet CellGroupAttendances => Set(); + + // Phase 3 + public DbSet MinistryBudgets => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // Apply all entity configurations from assembly + builder.ApplyConfigurationsFromAssembly(typeof(RolacDbContext).Assembly); + + // Global soft-delete filter + builder.Entity().HasQueryFilter(m => !m.IsDeleted); + builder.Entity().HasQueryFilter(e => !e.IsDeleted); + builder.Entity().HasQueryFilter(p => !p.IsDeleted); + + // Decimal precision + foreach (var property in builder.Model.GetEntityTypes() + .SelectMany(t => t.GetProperties()) + .Where(p => p.ClrType == typeof(decimal) || p.ClrType == typeof(decimal?))) + { + property.SetColumnType("decimal(18,2)"); + } + } +} +``` + +### 重要 EF Core 慣例 + +```csharp +// 軟刪除攔截 (SaveChangesInterceptor) +// 在 SaveChanges 前,如果 EntityState = Deleted 且實體繼承 SoftDeleteEntity, +// 改成 Modified 並設定 IsDeleted = true, DeletedAt = now(), DeletedBy = currentUser + +// Audit 自動填充 (SaveChangesInterceptor) +// 在 SaveChanges 前,自動填充 CreatedAt/CreatedBy(新增)和 UpdatedAt/UpdatedBy(修改) +// CurrentUser 透過 IHttpContextAccessor 取得 + +// AuditLog 攔截 (SaveChangesInterceptor) +// 記錄所有 Create/Update/Delete 動作到 AuditLogs 表 +// OldValues / NewValues 使用 JsonSerializer.Serialize(entry.OriginalValues.ToObject()) +``` + +--- + +*文件由 ROLAC 開發團隊維護。如需更新 Schema,必須同步更新此文件和 EF Core Migration。* diff --git a/docs/INFRASTRUCTURE.md b/docs/INFRASTRUCTURE.md new file mode 100644 index 0000000..bbf6662 --- /dev/null +++ b/docs/INFRASTRUCTURE.md @@ -0,0 +1,462 @@ +# ROLAC — Infrastructure Design + +**平台:** Microsoft Azure (Nonprofit $2,000 Credit) +**文件版本:** v0.1 (2026-05-24) + +--- + +## 目錄 + +1. [Azure 資源規劃](#1-azure-資源規劃) +2. [VM 容器架構](#2-vm-容器架構) +3. [Docker Compose 服務清單](#3-docker-compose-服務清單) +4. [網路與 DNS](#4-網路與-dns) +5. [Nginx 反向代理路由](#5-nginx-反向代理路由) +6. [CI/CD Pipeline (Jenkins + Gitea)](#6-cicd-pipeline-jenkins--gitea) +7. [Azure Storage Account 使用規劃](#7-azure-storage-account-使用規劃) +8. [備份策略](#8-備份策略) +9. [成本估算 (Monthly)](#9-成本估算-monthly) +10. [部署步驟清單](#10-部署步驟清單) + +--- + +## 1. Azure 資源規劃 + +| 資源 | 規格 | 用途 | 估計月費 | +|------|------|------|----------| +| **Azure VM** | Standard B2s (2 vCPU / 4 GB RAM) | 主機:所有 Docker 容器 | ~$30–35/月 | +| **Azure Storage Account** | LRS, Cool tier | Blob 儲存(照片/PDF/媒體) | ~$2–5/月 | +| **Azure Static Public IP** | Standard | 固定對外 IP | ~$4/月 | +| **Azure DNS Zone** *(可選)* | — | 管理 DNS 記錄 | ~$0.5/月 | +| **OS Disk** | Premium SSD 64 GB | VM 系統碟 | ~$10/月 | +| **資料磁碟** | Standard SSD 128 GB | Docker volumes(DB、Gitea、Jenkins) | ~$11/月 | + +> **總估算: ~$57–65/月**,年約 $700,在 $2,000 Credit 範圍內可運行約 **30 個月**。 +> 若流量低,可降級至 **B1ms (1 vCPU / 2 GB)** 節省至 ~$15/月。 + +--- + +## 2. VM 容器架構 + +``` +Azure VM (Ubuntu 22.04 LTS) +│ +├── /data/ ← 資料磁碟掛載點 +│ ├── postgres/ ← PostgreSQL data +│ ├── gitea/ ← Gitea repos & config +│ ├── jenkins/ ← Jenkins home +│ └── nginx/certs/ ← SSL 憑證 +│ +└── Docker Engine + │ + ├── [nginx] :80, :443 ← 反向代理 + SSL 終止 + ├── [angular-app] :4200 ← Angular SPA (nginx serve) + ├── [rolac-api] :5000 ← ASP.NET Core API + ├── [postgres] :5432 ← PostgreSQL 14+ + ├── [gitea] :3000, :22 ← 原始碼管理 + └── [jenkins] :8080 ← CI/CD +``` + +--- + +## 3. Docker Compose 服務清單 + +```yaml +# docker-compose.yml (簡化草圖) + +version: '3.9' + +services: + + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d + - ./nginx/certs:/etc/letsencrypt + depends_on: + - angular-app + - rolac-api + + angular-app: + image: rolac/frontend:latest # 由 Jenkins build + expose: + - "80" + restart: unless-stopped + + rolac-api: + image: rolac/api:latest # 由 Jenkins build + expose: + - "5000" + environment: + - ConnectionStrings__Default=Host=postgres;... + - Azure__StorageAccount__ConnectionString=... + - Jwt__Secret=${JWT_SECRET} + depends_on: + - postgres + restart: unless-stopped + + postgres: + image: postgres:16-alpine + volumes: + - /data/postgres:/var/lib/postgresql/data + environment: + - POSTGRES_DB=rolac + - POSTGRES_USER=${DB_USER} + - POSTGRES_PASSWORD=${DB_PASSWORD} + expose: + - "5432" + restart: unless-stopped + + gitea: + image: gitea/gitea:latest + ports: + - "2222:22" # Git SSH + expose: + - "3000" # Web UI (透過 nginx 代理) + volumes: + - /data/gitea:/data + restart: unless-stopped + + jenkins: + image: jenkins/jenkins:lts-jdk21 + expose: + - "8080" # Web UI (透過 nginx 代理) + volumes: + - /data/jenkins:/var/jenkins_home + - /var/run/docker.sock:/var/run/docker.sock # Jenkins 可操作 Docker + restart: unless-stopped +``` + +> **Secrets 管理:** 使用 `.env` 檔搭配 `docker-compose --env-file`,**不得** commit 到 Gitea。 + +--- + +## 4. 網路與 DNS + +``` +Internet + │ + ▼ +Azure Public IP (Static) + │ + ▼ +Azure NSG (Network Security Group) + ├── Port 80 (HTTP → 轉 443) + ├── Port 443 (HTTPS) + └── Port 2222 (Git SSH) + │ + ▼ +Ubuntu VM → Docker → Nginx +``` + +**DNS 記錄 — rolac.org** + +| 子域名 | 完整網址 | 類型 | 指向 | 用途 | +|--------|----------|------|------|------| +| `@` | rolac.org | A | VM Public IP | 教會公開網站(根域名) | +| `www` | www.rolac.org | CNAME | rolac.org | 同上(重定向) | +| `app` | app.rolac.org | A | VM Public IP | Angular 後台管理入口 | +| `api` | api.rolac.org | A | VM Public IP | ASP.NET Core REST API | +| `git` | git.rolac.org | A | VM Public IP | Gitea 原始碼管理 | +| `ci` | ci.rolac.org | A | VM Public IP | Jenkins CI/CD | + +--- + +## 5. Nginx 反向代理路由 + +```nginx +# /nginx/conf.d/rolac.conf (草圖) + +# HTTP → HTTPS redirect +server { + listen 80; + server_name *.rolac.org rolac.org; + return 301 https://$host$request_uri; +} + +# 教會公開網站 (Angular SSR or static) +server { + listen 443 ssl; + server_name rolac.org www.rolac.org; + location / { proxy_pass http://angular-app:80; } +} + +# 後台管理 App +server { + listen 443 ssl; + server_name app.rolac.org; + location / { proxy_pass http://angular-app:80; } +} + +# API +server { + listen 443 ssl; + server_name api.rolac.org; + location / { + proxy_pass http://rolac-api:5000; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } +} + +# Gitea +server { + listen 443 ssl; + server_name git.rolac.org; + location / { proxy_pass http://gitea:3000; } +} + +# Jenkins +server { + listen 443 ssl; + server_name ci.rolac.org; + location / { proxy_pass http://jenkins:8080; } +} +``` + +**SSL 憑證 (Let's Encrypt)** +```bash +certbot certonly --webroot -w /data/nginx/www \ + -d rolac.org \ + -d www.rolac.org \ + -d app.rolac.org \ + -d api.rolac.org \ + -d git.rolac.org \ + -d ci.rolac.org +``` + +--- + +## 6. CI/CD Pipeline (Jenkins + Gitea) + +### 流程圖 + +``` +開發者 push → Gitea + │ + │ Webhook (HTTP POST) + ▼ + Jenkins Job + │ + ┌───────────┼───────────┐ + ▼ ▼ ▼ + Build Test Lint + Docker dotnet ng lint + image test + │ + ▼ + Push image (Web) ← rolac.org & app.rolac.org 用 + docker compose up -d + │ + ▼ (main branch only) + Capacitor Build ← 產出 iOS .ipa / Android .apk + npx cap build ios + npx cap build android + │ + ▼ + Health check + 通知結果 (Email) +``` + +> **注意:** Capacitor build (iOS) 需要 macOS 環境,Jenkins 跑在 Linux VM 上只能產 Android APK;iOS `.ipa` 需在 Mac 上另外 build 或使用 Xcode Cloud。 + +### Jenkinsfile 草圖 + +```groovy +pipeline { + agent any + + environment { + API_IMAGE = "rolac/api:${BUILD_NUMBER}" + WEB_IMAGE = "rolac/frontend:${BUILD_NUMBER}" + } + + stages { + stage('Checkout') { + steps { checkout scm } + } + + stage('Test API') { + steps { + sh 'dotnet test ./backend/ROLAC.Tests' + } + } + + stage('Build Images') { + parallel { + stage('API') { + steps { + sh 'docker build -t ${API_IMAGE} -t rolac/api:latest ./backend' + } + } + stage('Frontend (Web)') { + steps { + sh 'docker build -t ${WEB_IMAGE} -t rolac/frontend:latest ./frontend' + } + } + } + } + + stage('Deploy Web') { + when { branch 'main' } + steps { + sh 'docker compose -f /opt/rolac/docker-compose.yml up -d --no-deps rolac-api angular-app' + } + } + + stage('Build Android APK') { + when { branch 'main' } + steps { + dir('frontend') { + sh 'npm ci' + sh 'npm run build -- --configuration production' + sh 'npx cap sync android' + sh 'cd android && ./gradlew assembleRelease' + } + archiveArtifacts artifacts: 'frontend/android/app/build/outputs/apk/release/*.apk' + } + } + } + + post { + failure { mail to: 'admin@rolac.org', subject: "Build FAILED: ${env.JOB_NAME} #${BUILD_NUMBER}" } + success { mail to: 'admin@rolac.org', subject: "Build OK: ${env.JOB_NAME} #${BUILD_NUMBER}" } + } +} +``` + +### Branch 策略 + +| Branch | 說明 | 部署 | +|--------|------|------| +| `main` | 正式版本 | 自動部署 Production | +| `develop` | 開發整合 | 自動部署 Staging (同 VM,不同 port) | +| `feature/*` | 功能開發 | 僅跑 Build + Test,不部署 | +| `hotfix/*` | 緊急修復 | 審核後 merge 至 main | + +--- + +## 7. Azure Storage Account 使用規劃 + +### Container (Blob) 結構 + +``` +rolac-storage (Storage Account) +├── members/ +│ ├── photos/ ← 教友大頭照 +│ └── documents/ ← 個人相關文件 +├── receipts/ +│ └── {year}/ ← 年度奉獻收據 PDF +├── cms/ +│ ├── images/ ← 網站上傳圖片 +│ └── attachments/ ← 公告附件 +└── reports/ + └── exports/ ← 匯出的報表 PDF/CSV +``` + +### 存取控制 + +| Container | 存取層級 | 說明 | +|-----------|----------|------| +| `members/` | **Private** | 僅 API 透過 SAS Token 存取 | +| `receipts/` | **Private** | API 產生限時下載連結給教友 | +| `cms/images/` | **Blob (Public Read)** | 網站圖片可公開存取 | +| `reports/` | **Private** | 僅管理員 API 存取 | + +### C# 整合 + +```csharp +// 使用 Azure.Storage.Blobs SDK +// 上傳教友照片範例 +var blobClient = new BlobContainerClient(connectionString, "members"); +await blobClient.UploadBlobAsync($"photos/{memberId}.jpg", fileStream); + +// 產生限時 SAS URL (收據下載) +var sasUri = blobClient.GenerateSasUri(BlobSasPermissions.Read, + DateTimeOffset.UtcNow.AddHours(1)); +``` + +--- + +## 8. 備份策略 + +### PostgreSQL 備份 + +```bash +# crontab (每日 02:00 UTC 備份) +0 2 * * * docker exec postgres pg_dump -U rolac_user rolac \ + | gzip > /data/backups/rolac_$(date +%Y%m%d).sql.gz + +# 保留 30 天 +find /data/backups -name "*.sql.gz" -mtime +30 -delete +``` + +### 備份上傳至 Azure Blob + +```bash +# 使用 azcopy 或 Azure CLI 將備份上傳 +azcopy copy "/data/backups/rolac_$(date +%Y%m%d).sql.gz" \ + "https://rolacstorage.blob.core.windows.net/backups/" +``` + +### 備份範圍 + +| 資料 | 方式 | 頻率 | 保留 | +|------|------|------|------| +| PostgreSQL DB | pg_dump → Azure Blob | 每日 | 30 天 | +| Gitea repos | volume backup → Azure Blob | 每日 | 14 天 | +| Jenkins config | volume backup | 每週 | 4 週 | +| Azure Blob 本身 | Azure Soft Delete | 自動 | 7 天 | + +--- + +## 9. 成本估算 (Monthly) + +| 項目 | 規格 | 月費 (USD) | +|------|------|-----------| +| Azure VM | Standard_B2s | $30 | +| OS Disk | Premium SSD 64 GB | $10 | +| 資料磁碟 | Standard SSD 128 GB | $11 | +| Static Public IP | Standard | $4 | +| Storage Account | LRS 50 GB | $2 | +| 出站流量 | ~10 GB/月 | $1 | +| **合計** | | **~$58/月** | + +> 💡 **$2,000 nonprofit credit → 約 34 個月 (接近 3 年)** +> 如規模成長,可升級 VM 規格但仍在 credit 範圍內。 + +--- + +## 10. 部署步驟清單 + +### Phase 0 — Azure 環境建置 + +- [ ] 申請 Microsoft Nonprofit Azure 帳號 +- [ ] 建立 Resource Group: `rg-rolac-prod` +- [ ] 建立 Azure VM (Ubuntu 22.04, Standard_B2s) +- [ ] 建立資料磁碟 (128 GB) 並掛載至 `/data` +- [ ] 設定 NSG (開放 80, 443, 2222) +- [ ] 建立 Static Public IP 並綁定 +- [ ] 建立 Storage Account (`rolacstorage`) +- [ ] 設定 DNS A 記錄指向 VM IP + +### Phase 0 — VM 環境初始化 + +- [ ] 安裝 Docker Engine + Docker Compose +- [ ] 安裝 Certbot (Let's Encrypt) +- [ ] 建立目錄結構 (`/data/postgres`, `/data/gitea`, `/data/jenkins`) +- [ ] 啟動 Gitea 容器,建立組織與 Repo +- [ ] 啟動 Jenkins 容器,安裝 Plugins (Git, Docker, Pipeline) +- [ ] 設定 Gitea Webhook → Jenkins +- [ ] 申請 SSL 憑證 (certbot) +- [ ] 啟動 Nginx 容器,配置反向代理 + +### Phase 1+ — 每次 Release + +- [ ] Push code to Gitea `main` branch +- [ ] Jenkins 自動觸發 Build → Test → Deploy +- [ ] 確認 Health Check endpoint 回應正常 +- [ ] 必要時執行 EF Core Migration: `dotnet ef database update` diff --git a/docs/IRS_RECEIPT.md b/docs/IRS_RECEIPT.md new file mode 100644 index 0000000..94be0ac --- /dev/null +++ b/docs/IRS_RECEIPT.md @@ -0,0 +1,156 @@ +# ROLAC — 年度奉獻收據 (IRS Annual Giving Statement) + +**文件版本:** v0.1 (2026-05-24) + +> **重要提示:** 本文件為技術設計參考。正式收據的法律用語及符合性,請在發送前諮詢持牌會計師或稅務顧問。 + +--- + +## 法律背景 + +- ROLAC 為 IRS **501(c)(3)** 免稅組織 +- **EIN:** `42-2682968`(儲存為環境變數 `CHURCH_EIN`,不 hardcode) +- 依 IRS 規定,單筆奉獻 **$250 或以上** 需提供書面確認 +- 年度收據通常在每年 **1 月 31 日前** 寄出,供教友報稅使用 +- **現金奉獻** 若無同時期的銀行記錄,IRS 要求有組織的書面記錄 + +--- + +## 收據必要欄位(IRS 要求) + +| 欄位 | 說明 | +|------|------| +| 組織全名 | River Of Life Christian Church In Arcadia | +| EIN | 42-2682968 | +| 教友姓名 | 依登錄姓名 | +| 奉獻年度 | 例:January 1 – December 31, 2025 | +| 奉獻明細 | 日期、金額、類型(每筆逐一列出) | +| 年度奉獻總計 | 所有非匿名奉獻的 Net Amount 加總 | +| 免稅聲明語 | 見下方標準語句 | +| 未提供商品或服務聲明 | 見下方標準語句 | +| 組織代表簽名 | 財務同工姓名、職稱、日期 | + +--- + +## 標準免稅聲明語(中英雙語) + +**英文版(收據上必須包含)** +``` +River Of Life Christian Church In Arcadia is a tax-exempt organization +under Section 501(c)(3) of the Internal Revenue Code. +EIN: 42-2682968 + +No goods or services were provided in exchange for this contribution. +This letter serves as your official receipt for income tax purposes. +``` + +**繁體中文版(附於英文後,供教友參考)** +``` +River Of Life Christian Church In Arcadia 為依據美國國稅局 +第 501(c)(3) 條款登記之免稅組織。 +稅務識別號碼 (EIN):42-2682968 + +貴教友所奉獻之款項未換取任何商品或服務。 +本信函作為您申報所得稅之正式收據。 +``` + +--- + +## PDF 收據版面設計 + +``` +┌─────────────────────────────────────────────┐ +│ [教會 Logo] │ +│ River Of Life Christian Church In Arcadia │ +│ [教會地址] | EIN: 42-2682968 │ +├─────────────────────────────────────────────┤ +│ Annual Giving Statement / 年度奉獻收據 │ +│ Tax Year: January 1 – December 31, {Year} │ +├─────────────────────────────────────────────┤ +│ Prepared for: {Member Full Name} │ +│ Date Issued: {Issue Date} │ +├─────────────────────────────────────────────┤ +│ Giving Detail / 奉獻明細 │ +│ ───────────────────────────────────── │ +│ Date Type Method Amount │ +│ 2025-01-05 Tithe Check $500.00 │ +│ 2025-01-05 Offering Cash $50.00 │ +│ 2025-02-02 Tithe Zelle $500.00 │ +│ ... │ +│ ───────────────────────────────────── │ +│ Total Contributions: $X,XXX.XX │ +│ (PayPal fees are excluded) │ +├─────────────────────────────────────────────┤ +│ [免稅聲明語(英文)] │ +│ [免稅聲明語(繁中)] │ +├─────────────────────────────────────────────┤ +│ Authorized by: {Finance Staff Name} │ +│ Title: Church Finance │ +│ Date: {Issue Date} │ +└─────────────────────────────────────────────┘ +``` + +--- + +## 金額計算規則 + +| 支付方式 | 收據金額 | 備注 | +|----------|----------|------| +| 現金 | `Amount` | 以信封記錄為準 | +| 支票 | `Amount` | 以支票金額為準 | +| Zelle | `Amount` | 以轉入金額為準 | +| PayPal | `NetAmount`(= Amount − FeeAmount) | PayPal 手續費不計入可扣稅金額 | +| 匿名奉獻 | **不列入** 個人收據 | 匿名奉獻無法與個人對應 | + +--- + +## 系統流程 + +``` +財務同工觸發 → 選擇年度 + │ + ▼ +系統查詢該年度所有 +非匿名 Giving 記錄 + │ + ▼ +依 MemberId 分組加總 + │ + ├── 產生個人 PDF(QuestPDF) + ├── 上傳至 Azure Blob + │ receipts/{year}/{memberId}.pdf + └── Email 寄送給教友 + (含 PDF 附件 + 語言偏好) + │ + ▼ +記錄至 ReceiptLog +(誰產生、何時、寄送狀態) +``` + +--- + +## 資料模型補充 + +``` +GivingReceipt +├── Id +├── MemberId +├── TaxYear (int, e.g. 2025) +├── TotalAmount (decimal) +├── PdfBlobPath (Azure Blob 路徑) +├── GeneratedAt +├── GeneratedByUserId +├── SentAt (Email 寄送時間,null = 未寄) +├── SentToEmail +└── IsVoided (bool, 作廢旗標) +``` + +--- + +## 重要注意事項 + +1. **不要 hardcode EIN** — 存於環境變數 `CHURCH_EIN`,設定頁面可修改 +2. **收據一旦發出即存檔** — 即使奉獻記錄事後修改,已發出的 PDF 不可覆蓋,只能補發新版 +3. **補發流程** — 財務同工可為特定教友重新產生,系統記錄「重新發出」事件至 Audit Log +4. **作廢** — 設 `IsVoided = true`,但 PDF 原檔仍保留於 Blob,不可刪除 +5. **稅務諮詢** — 建議每年收據發出前,讓持牌會計師審閱格式一次 diff --git a/docs/NOTIFICATIONS.md b/docs/NOTIFICATIONS.md new file mode 100644 index 0000000..8261730 --- /dev/null +++ b/docs/NOTIFICATIONS.md @@ -0,0 +1,157 @@ +# ROLAC — 通知系統設計 + +**文件版本:** v0.1 (2026-05-24) + +--- + +## 通知渠道總覽 + +| 渠道 | 技術 | 適用場景 | 狀態 | +|------|------|----------|------| +| **Email** | SendGrid / SMTP | 年度收據、週報、重要公告 | Phase 0 | +| **Push Notification** | Capacitor Push | 服事提醒、排班更新、活動通知 | Phase 1 | +| **SMS** | Twilio | 緊急通知、服事前一天提醒 | Phase 2 | +| **App 內通知** | Angular 元件 | 所有通知的 in-app 備份 | Phase 1 | + +--- + +## 各渠道設計 + +### Email (SendGrid) + +**觸發時機** + +| 事件 | 收件人 | 說明 | +|------|--------|------| +| 年度奉獻收據產生 | 個別教友 | 含 PDF 附件 | +| 歡迎新教友 | 新教友 | 帳號啟用信 | +| 密碼重設 | 帳號持有人 | OTP / Reset Link | +| 服事排班確認 | 當週服事同工 | 每週四自動發送 | +| 系統 Build 失敗 | admin@rolac.org | Jenkins CI 通知 | + +**Email 範本要求** +- 雙語(EN / zh-TW),依教友語言偏好發送 +- 教會 Logo header +- 手機版 Responsive HTML +- Unsubscribe 連結(行銷類) + +--- + +### Push Notification (Capacitor) + +**使用 `@capacitor/push-notifications`** +後端使用 **Firebase Cloud Messaging (FCM)** 作為推播服務(Android + iOS 共用)。 + +**推播事件** + +| 事件 | 時機 | 內容範例 | +|------|------|----------| +| 服事提醒 | 服事前 2 天 09:00 | "您下週日的敬拜服事提醒" | +| 排班更新 | 立即 | "您的服事排班已更新,請查閱" | +| 新消息公告 | 發佈後立即 | "[公告] 復活節特別聚會" | +| 生日祝福 | 當天 08:00 | 可選,需教友同意 | + +**裝置 Token 管理** + +``` +UserDevice +├── Id +├── UserId +├── DeviceToken (FCM token) +├── Platform (ios | android) +├── AppVersion +├── LastSeenAt +└── IsActive +``` + +- Token 登入時更新,登出時標記 `IsActive = false` +- FCM token 失效時(delivery failure)自動清理 + +--- + +### SMS (Twilio) + +**使用時機:輔助通知,非主要渠道** + +| 事件 | 觸發條件 | 說明 | +|------|----------|------| +| 服事前一天提醒 | 前一天 16:00 | 確保沒裝 App 的同工也收到 | +| 緊急取消通知 | 手動觸發 | 主日緊急狀況(天氣、設備) | +| 新教友歡迎 | 首次建立帳號 | 可選 | + +**教友設定** +- 預設 **關閉** SMS,教友自行在個人設定開啟 +- 須明確同意才接收(Opt-in 機制) +- 每則 SMS 結尾附 "Reply STOP to unsubscribe" + +**Twilio 設定** +```csharp +// appsettings.json(實際值存環境變數) +"Twilio": { + "AccountSid": "${TWILIO_ACCOUNT_SID}", + "AuthToken": "${TWILIO_AUTH_TOKEN}", + "FromNumber": "${TWILIO_FROM_NUMBER}" // e.g. +1-xxx-xxx-xxxx +} +``` + +**費用估算(50–100 人教會)** +- 假設 30 人開啟 SMS、每月約 8 次通知 +- 30 人 × 8 次 = 240 SMS/月 +- Twilio 美國 SMS ~$0.0079/則 → **約 $2/月** + +--- + +## 通知偏好設定(教友端) + +教友在 App 個人設定頁面可控制: + +``` +通知設定 +├── Email 通知 +│ ├── ✅ 服事排班(必開,不可關) +│ ├── ✅ 重要公告 +│ └── ☐ 活動資訊 +├── Push 通知 +│ ├── ✅ 服事提醒 +│ ├── ✅ 排班更新 +│ └── ☐ 生日祝福 +└── SMS 通知(預設關閉) + ├── ☐ 服事前一天提醒 + └── ☐ 緊急通知 +``` + +--- + +## 後端通知服務架構 + +```csharp +// INotificationService 統一介面 +public interface INotificationService +{ + Task SendEmailAsync(NotificationRequest request); + Task SendPushAsync(NotificationRequest request); + Task SendSmsAsync(NotificationRequest request); + Task SendAllAsync(NotificationRequest request); // 依偏好自動選渠道 +} + +// NotificationRequest +public record NotificationRequest( + string UserId, + string TemplateKey, // e.g. "roster.reminder" + Dictionary Data, + NotificationChannel[] Channels +); +``` + +**排程通知(Hangfire 或 Quartz.NET)** +- 服事提醒:每週六 09:00 掃描下週服事表,批次發送 +- SMS 提醒:每週六 16:00 補發 SMS(對已開啟 SMS 的教友) +- 生日提醒:每天 08:00 掃描當天生日教友 + +--- + +## 通知語言規則 + +1. 以教友個人語言偏好為準(`Member.LanguagePreference: en | zh-TW`) +2. 若未設定,預設英文 +3. 批次發送(如年度收據)依每人偏好分別產生對應語言的郵件 diff --git a/docs/PLANNING.md b/docs/PLANNING.md new file mode 100644 index 0000000..da8217c --- /dev/null +++ b/docs/PLANNING.md @@ -0,0 +1,1466 @@ +# ROLAC Church Management System — Project Planning + +**教會:** River Of Life Christian Church In Arcadia (ROLAC) +**規模:** 50–100 人 +**文件版本:** v0.2 (2026-05-24) + +--- + +## 目錄 + +1. [專案目標](#1-專案目標) +2. [系統模組總覽](#2-系統模組總覽) +3. [模組詳細規劃](#3-模組詳細規劃) + - 3.1 [教友管理 (Member Management)](#31-教友管理-member-management) + - 3.2 [權限控管 & 多角色 (RBAC)](#32-權限控管--多角色-rbac) + - 3.3 [小組架構 (Cell Groups / Ministry Structure)](#33-小組架構-cell-groups--ministry-structure) + - 3.4 [服事表 (Service Roster)](#34-服事表-service-roster) + - 3.5 [教會首頁 CMS](#35-教會首頁-cms) + - 3.6 [奉獻追蹤 (Giving / Donations)](#36-奉獻追蹤-giving--donations) + - 3.7 [年度收據 (Annual Giving Statement)](#37-年度收據-annual-giving-statement) + - 3.8 [Audit Log](#38-audit-log) + - 3.9 [報表 (Reports)](#39-報表-reports) + - 3.10 [Ministry Scope](#310-ministry-scope) + - 3.11 [未來 AI 整合](#311-未來-ai-整合) +4. [角色與權限矩陣](#4-角色與權限矩陣) +5. [資料模型草圖](#5-資料模型草圖) +6. [技術架構建議](#6-技術架構建議) +7. [開發階段規劃 (Roadmap)](#7-開發階段規劃-roadmap) +8. [開放議題 & 待決策](#8-開放議題--待決策) + +--- + +## 1. 專案目標 + +| 目標 | 說明 | +|------|------| +| 數位化教友資料 | 取代紙本或分散的試算表,集中管理教友資訊 | +| 透明化財務 | 讓奉獻記錄可查詢、可稽核,年底產生合規收據 | +| 強化小組連結 | 清楚呈現小組架構,追蹤出席與服事參與 | +| 簡化行政工作 | 服事排班、通知、報表自動化,減少人工作業 | +| 對外溝通 | CMS 管理教會網站,發佈消息/活動 | +| 安全與稽核 | 完整 Audit Log,細緻的角色權限,保護敏感資料 | +| 可擴展性 | 為未來 AI 功能(關懷提醒、智能報表)預留介面 | + +--- + +## 2. 系統模組總覽 + +``` +ROLAC CMS +├── 公開前台 (Public Website — rolac.org) +│ └── 教會首頁 CMS(雙語) +└── 後台管理 (Admin Portal — app.rolac.org) + ├── 教友管理 + ├── 小組架構 & Ministry Scope + ├── 主日出席記錄 + ├── 服事表 + ├── 敬拜歌曲庫 + ├── 代禱事項 + ├── 奉獻追蹤(含線上奉獻)& 年度收據 + ├── 支出追蹤 & 報銷 + ├── 月結對帳 + ├── 報表 + ├── Audit Log + ├── 權限控管 (RBAC) + └── [未來] AI 整合 +``` + +--- + +## 3. 模組詳細規劃 + +### 3.1 教友管理 (Member Management) + +**核心資料欄位** + +| 欄位群 | 欄位 | +|--------|------| +| 基本資料 | 姓名 (中/英)、生日、性別、照片 | +| 聯絡資訊 | 電話、Email、地址 | +| 教會狀態 | 會員/訪客/前會員、受洗日期、入會日期、離會原因 | +| 家庭關係 | 家庭單元 ID、配偶、子女 | +| 小組歸屬 | 所屬小組、負責牧者/組長 | +| 服事歷史 | 參與服事記錄 | +| 自定義標籤 | 關懷需求、恩賜標籤、語言偏好 (中/英) | + +**功能清單** +- 新增 / 編輯 / 軟刪除 教友資料 +- 家庭單元管理(一個家庭多位成員) +- 進階搜尋與篩選(按小組、狀態、生日月份…) +- 生日/週年紀念提醒 +- 匯入 / 匯出 CSV +- 個人資料變更歷史(誰改了什麼) + +--- + +### 3.2 權限控管 & 多角色 (RBAC) + +**設計原則** +- 最小權限原則:每個角色只能存取完成工作所需的資源 +- Ministry Scope:部分角色僅能看到自己負責的 Ministry 資料 +- 細粒度到 Resource + Action 層級 + +**預設角色(見第 4 節矩陣)** + +> ROLAC 為靈糧堂體制,無長老制。 + +| 角色 | 中文說明 | 層級 | +|------|----------|------| +| `super_admin` | 系統管理員,全權存取 | 系統 | +| `pastor` | 牧師,全覽教友與財務摘要 | 牧者 | +| `board_member` | 理事,教會治理委員 | 治理 | +| `coworker_chair` | 同工會主席,統籌各事工領袖 | 領導 | +| `ministry_leader` | 事工領袖,僅限自身 Ministry 範圍 | 事工 | +| `district_leader` | 區長,管理轄下多個小組 | 小組 | +| `cell_leader` | 小組長,僅限自身小組 | 小組 | +| `coworker` | 同工,參與指定事工的一般同工 | 同工 | +| `finance` | 財務同工,管理奉獻與支出報表 | 功能 | +| `secretary` | 行政秘書,管理教友資料與排班 | 功能 | +| `worship_leader` | 敬拜領袖,管理歌曲庫與敬拜歌單(Phase 暫緩)| 功能 | +| `member` | 一般教友,查看個人資料與服事表 | 教友 | +| `visitor` | 訪客,僅限公開頁面 | 教友 | + +--- + +### 3.3 小組架構 (Cell Groups / Ministry Structure) + +**資料結構(樹狀)** +``` +教會 (Church — River Of Life Christian Church In Arcadia) +└── Ministry (事工部門) + └── Cell Group (小組) + └── Member (教友) +``` + +**ROLAC 實際事工部門(初始種子資料)** + +| # | 英文名稱 | 中文名稱 | 主要職責 | +|---|----------|----------|----------| +| 1 | Administration | 行政 | 教會行政、秘書、文件管理 | +| 2 | Preaching | 講道 | 主日講道、查經帶領 | +| 3 | Emcee | 司會 | 主日流程主持 | +| 4 | Worship | 敬拜 | 敬拜帶領、詩歌、樂器 | +| 5 | PPT / Media | PPT/影音 | 投影片製作、直播、錄影 | +| 6 | Sound | 音控 | 現場音響、混音 | +| 7 | Facility | 場地組 | 場地佈置、清潔、設備 | +| 8 | Hospitality | 招待 | 門口招待、訪客關懷、奉獻袋收集 | +| 9 | Children | 兒牧 | 兒童主日學、托兒 | +| 10 | Catering | 餐飲 | 每週愛宴、餐飲採購 | + +> 這 10 個 Ministry 為系統初始種子資料(`DbInitializer`),可在後台新增 / 修改。 + +**功能清單** +- 建立 / 管理 Ministry 與小組層級 +- 指派組長與副組長 +- 成員歸屬管理(一個教友可屬於多個 Ministry) +- 出席記錄(每次小組聚會) +- 成員轉移與歷史追蹤 +- 小組狀態(活躍 / 暫停 / 解散) +- Ministry Scope 權限綁定 + +--- + +### 3.3b 主日出席記錄 (Sunday Service Attendance) + +> **記錄方式(已確定):** 招待同工(Greeting Team)統計總人數後,由秘書或招待組長手動輸入。 + +**統計分類** + +| 類別 | 說明 | +|------|------| +| 大人 (Adults) | 成年會眾人數 | +| 青少年 (Teenagers) | 青少年人數 | +| 兒童 (Children) | 兒童主日學人數 | +| 合計 (Total) | 系統自動加總 | + +**資料模型** +``` +SundayAttendance +├── Id +├── ServiceDate (日期) +├── ServiceType (主日崇拜 / 特別活動 / 節日崇拜) +├── AdultCount +├── TeenCount +├── ChildrenCount +├── TotalCount (computed: Adult + Teen + Children) +├── Notes (備註,例:聖誕主日特別人數) +├── RecordedByUserId (誰輸入) +└── CreatedAt / UpdatedAt +``` + +**功能清單** +- 每週輸入三個類別人數(簡單表單) +- 合計自動計算 +- 月 / 季 / 年 出席趨勢圖 +- 出席率計算(以平均或最高值為基準) +- 節日 / 特別活動標記 +- 匯出 CSV 供外部報表使用 + +--- + +### 3.4 服事表 (Service Roster) + +**ROLAC 主日服事槽位(初始種子資料)** + +每個槽位屬於一個 Ministry,一次服事可填入一個或多個同工。 + +| Ministry | 服事槽位 | 人數 | 頻率 | +|----------|---------|------|------| +| 講道 | 講員 Preacher | 1 | 每週 | +| 司會 | 主持人 Emcee | 1 | 每週 | +| 敬拜 | 敬拜帶領 Worship Leader | 1 | 每週 | +| 敬拜 | 敬拜團員 Worship Team | 2–5 | 每週 | +| 敬拜 | 司琴 Pianist | 1 | 每週 | +| PPT/影音 | PPT 操作 Slide Operator | 1 | 每週 | +| PPT/影音 | 攝影 / 直播 Livestream | 0–1 | 每週 | +| 音控 | 音控 Sound Engineer | 1 | 每週 | +| 場地組 | 場地佈置 Setup | 2–3 | 每週 | +| 場地組 | 場地收拾 Teardown | 2–3 | 每週 | +| 招待 | 門口招待 Greeter | 2–3 | 每週 | +| 招待 | 奉獻袋 Offering Collection | 2 | 每週 | +| 兒牧 | 兒童主日學老師 | 1–2 | 每週 | +| 兒牧 | 兒童助教 Assistant | 0–1 | 每週 | +| 餐飲 | 愛宴負責人 Catering Lead | 1 | 每週 | +| 餐飲 | 愛宴協助 Catering Help | 2–4 | 每週 | + +**功能清單** +- 定義服事槽位(綁定 Ministry,雙語名稱) +- 每週排班(從教友庫中指派到各槽位) +- 週期性排班範本(每週 / 隔週 / 每月輪換) +- 自動衝突檢查(同一人同一主日多個衝突槽位) +- 缺席替換流程(申請 → 系統建議替補 → 確認) +- Email / Push Notification 服事提醒(服事前 2 天) +- 公開服事表嵌入網站(確認後才公開) +- 服事統計(每人參與頻率) + +--- + +### 3.5 教會首頁 CMS + +> **雙語範圍(已確定):** 網站前台完整支援 **英文 (EN) + 繁體中文 (zh-TW)** 切換。 +> 所有可見文字均需提供兩個語言版本,包含 SEO meta 欄位。 + +**頁面 / 內容類型** + +| 類型 | 說明 | +|------|------| +| 首頁 Hero | 輪播橫幅、主題文字(雙語) | +| 關於我們 | 教會簡介、異象、牧師介紹(雙語) | +| 主日資訊 | 時間、地點、Google Map(地址雙語) | +| 消息公告 | 可分類標籤、發佈排程(標題 + 內文雙語) | +| 活動日曆 | 事件列表,可連結報名表單(雙語) | +| 媒體 | 主日講道影片/音訊嵌入(YouTube/Podcast)(標題雙語) | +| 奉獻頁面 | 線上奉獻說明(雙語) | +| 聯絡表單 | 訪客詢問,通知至指定 Email(表單標籤雙語) | + +**雙語內容資料模型** + +CMS 所有可編輯的文字欄位,一律使用平行欄位儲存: + +``` +CmsContent +├── Id +├── PageSlug (e.g. "home", "about", "announcements") +├── SectionKey (e.g. "hero.title", "hero.subtitle") +├── ContentType (Text | RichText | ImageUrl | Url) +├── ValueEn ← 英文版本 +├── ValueZh ← 繁體中文版本 +├── IsPublished +├── PublishAt (排程發佈) +├── UpdatedByUserId +└── UpdatedAt + +Announcement +├── Id +├── TitleEn / TitleZh +├── BodyEn / BodyZh (Rich text) +├── Category +├── IsPublished +├── PublishAt +├── ExpiresAt +└── CoverImageUrl + +Event +├── Id +├── TitleEn / TitleZh +├── DescriptionEn / DescriptionZh +├── Location (共用,或 LocationEn / LocationZh) +├── StartAt / EndAt +├── RegistrationUrl +└── CoverImageUrl +``` + +**後台 CMS 編輯器 UI(Admin App)** + +每個可編輯欄位都採用**並排雙欄**編輯介面: + +``` +┌──────────────────────┬──────────────────────┐ +│ English │ 繁體中文 │ +│ ───────────────── │ ───────────────── │ +│ [Welcome to ROLAC] │ [歡迎來到生命之河] │ +└──────────────────────┴──────────────────────┘ +``` + +- 兩欄同時顯示,編輯器可獨立輸入 +- 儲存時驗證:若只填一種語言,提示警告(但允許儲存) +- 預覽模式可切換 EN / zh-TW 即時檢視 + +**前台語言切換 UI** + +- Header 右上角 `EN | 中` 切換按鈕 +- 語言偏好存於 `localStorage`(訪客也記住) +- URL 策略:使用 Angular `i18n` route prefix 或 query param,二擇一: + - 方案 A(推薦):`rolac.org/en/about` vs `rolac.org/zh/about` — 對 SEO 最友善 + - 方案 B:`rolac.org/about?lang=zh` — 實作較簡單 + +**SEO 雙語支援** + +每個頁面在 `` 中加入: + +```html + + + +``` + +- Meta title / description 依當前語言動態注入 +- 使用 Angular `Meta` service 或 `ngx-seo` + +**技術需求** +- 富文本編輯器:**Quill** 或 **TipTap**(Angular 皆有 wrapper) +- 圖片上傳 → Azure Blob `cms/images/` +- 預覽草稿後才發佈 +- 發佈排程(`PublishAt` 欄位 + 背景排程 Job) + +--- + +### 3.6 奉獻追蹤 (Giving / Donations) + +**奉獻類型** +- 什一奉獻 (Tithe) +- 感恩奉獻 (General Offering) +- 特別奉獻 (Special/Project,可自訂名稱) +- 慣例奉獻 (Pledge) + +**支付方式(完整)** + +| 方式 | 類型 | 狀態 | 說明 | +|------|------|------|------| +| **現金 (Cash)** | 線下手動 | ✅ Phase 1 | 信封號碼(選填)、匿名旗標 | +| **支票 (Check)** | 線下手動 | ✅ Phase 1 | 支票號碼 | +| **Zelle** | 線下手動 | ✅ Phase 1 | 交易參考碼,無 API | +| **PayPal (手動)** | 線下手動 | ✅ Phase 1 | Transaction ID 手輸 | +| **Stripe (線上)** | 線上支付 | 🔜 Phase 4 | 信用卡 / Apple Pay / Google Pay | +| **PayPal Checkout (線上)** | 線上支付 | 🔜 Phase 4 | PayPal 按鈕嵌入 | + +**資料模型重點欄位** +``` +Giving +├── Id +├── MemberId (可為 null → 匿名 / 訪客線上奉獻) +├── GiverName (線上匿名奉獻者填寫的名字,可 null) +├── GiverEmail (線上奉獻用,發確認信) +├── GivingDate +├── Amount (decimal, 原始金額) +├── FeeAmount (decimal, 手續費,預設 0) +├── NetAmount (computed: Amount − FeeAmount) +├── GivingCategoryId (Tithe / Offering / Special…) +├── PaymentMethod (Cash | Check | Zelle | PayPal | Stripe | PayPalCheckout) +├── PaymentSource (Manual | OnlineStripe | OnlinePayPal) +├── ReferenceNumber (支票號 / Zelle ref / Stripe PaymentIntent ID / PayPal txn) +├── EnvelopeNumber (現金信封號碼,選填) +├── IsAnonymous (bool) +├── FundProjectId (特別奉獻專案,可 null) +├── IsRecurring (bool, 定期奉獻) +├── RecurringScheduleId (FK → GivingRecurringSchedule,可 null) +├── Notes +├── RecordedByUserId (手動輸入者;線上奉獻為 null) +└── StripeEventId (防重複,Webhook idempotency key) + +GivingRecurringSchedule ← 定期線上奉獻 +├── Id +├── MemberId +├── Amount +├── GivingCategoryId +├── Frequency (Weekly | BiWeekly | Monthly) +├── NextChargeDate +├── StripeSubscriptionId +├── Status (Active | Paused | Cancelled) +└── CreatedAt +``` + +**功能清單** +- 手動記錄奉獻(含現金/支票/Zelle/PayPal) +- **主日奉獻袋批次輸入**(Phase 1,見 §3.6c) +- 線上奉獻頁(Phase 4,見 §3.6b) +- 定期自動奉獻(Stripe Subscription,Phase 4) +- PayPal / Stripe 手續費分開記錄,IRS 收據用 **NetAmount** +- 奉獻目標 / 專案進度條 +- 個人奉獻歷史(本人可查) +- 月結對帳報表 + +--- + +#### 3.6c 主日奉獻袋批次輸入 (Sunday Offering Batch Entry — Phase 1) + +**業務場景** + +主日聚會結束後,財務同工收到奉獻袋,需要逐一開袋,將每筆現金信封或支票對應到教友姓名,登入系統記錄,年底才能產生個人奉獻收據。 + +**工作流程** + +``` +財務同工開啟「主日奉獻錄入」 + │ + ▼ +選擇日期(預設今天) + │ + ▼ +┌───────────────────────────────────────┐ +│ 逐筆快速錄入介面 │ +│ ───────────────────────────────── │ +│ 搜尋教友:[_______________] ← 即時搜尋 │ +│ 奉獻類型:[Tithe ▼] │ +│ 支付方式:[● 現金 ○ 支票] │ +│ └─ 現金:信封號碼 [___] (選填) │ +│ └─ 支票:支票號碼 [___] (必填) │ +│ 金額: [$________] │ +│ 備注: [_______________] (選填) │ +│ │ +│ [+ 新增] [匿名奉獻] │ +│ ───────────────────────────────── │ +│ 已錄入 12 筆 | 小計:$1,250.00 │ +└───────────────────────────────────────┘ + │ + ▼ +所有信封錄入完成 + │ + ▼ +對帳確認頁(顯示今日所有筆數 + 總計) +財務同工核對實際金額 = 系統合計 + │ + ▼ +確認送出 → 系統 Bulk Insert +``` + +**UI 設計要點** + +| 要點 | 說明 | +|------|------| +| 教友搜尋 | 即時搜尋(姓名中英文 / 拼音),顯示照片方便確認 | +| 鍵盤優先 | Tab 鍵在欄位間切換,Enter 新增,不需滑鼠 | +| 支付方式切換 | 選「支票」才出現支票號碼欄;選「現金」才出現信封號欄 | +| 匿名奉獻 | 一鍵匿名,不需搜尋教友,仍記錄金額供帳目平衡 | +| 即時小計 | 每新增一筆,當日累計金額即時更新 | +| 對帳視圖 | 最後一步顯示完整清單,供財務核對實際鈔票 / 支票 | +| 可編輯 | 送出前可點任一筆修改或刪除 | +| 送出後修改 | 需財務角色才可編輯,修改記錄進 Audit Log | + +**資料模型補充** + +``` +OfferingSession ← 每次主日錄入為一個 Session +├── Id +├── SessionDate (主日日期) +├── Status (Open | Submitted) +├── CashTotal (computed) +├── CheckTotal (computed) +├── TotalAmount (computed) +├── SubmittedByUserId +├── SubmittedAt +└── Givings[] (關聯當次錄入的所有 Giving 記錄) +``` + +> **為什麼需要 OfferingSession?** +> 將同一主日的所有奉獻筆數綁在一起,方便財務同工核對「這次錄入的總計」是否與實際收到的現金+支票金額吻合,在確認前可以整批修改,確認後進 Audit Log。 + +--- + +#### 3.6d 支出追蹤 & 報銷 (Expense Tracking & Reimbursement — Phase 1) + +**業務場景** + +| 場景 | 說明 | +|------|------| +| 代墊報銷 | 同工先自掏腰包買教會物品,事後憑收據向教會申請報帳 | +| 廠商付款 | 教會直接開支票給廠商(例:每週愛宴午餐外燴)| +| 雜支現金 | 小額現金支出(如停車費、文具)| + +**支出流程** + +``` +【場景 A:代墊報銷】 + +同工提交報銷申請 +(日期、金額、類別、說明、收據照片) + │ + ▼ +財務同工審核 + ├── 批准 → 開支票 / 現金還款給同工 + │ 記錄支票號碼 + 付款日期 + └── 退回 → 填寫退回原因 + +【場景 B:廠商直接付款(如愛宴)】 + +財務同工直接建立支出記錄 +(廠商名稱、金額、支票號碼、日期、類別) +→ 狀態直接為「已付款」 +``` + +**支出分類架構(兩層 + Ministry)** + +每筆支出標記三個維度: + +``` +Ministry(事工部門) × 大類(Category Group) × 子項目(Sub-Category) + +範例: + 敬拜事工 > 影音 > 設備 + 敬拜事工 > 影音 > 教育訓練 + 餐飲部門 > 餐飲 > 消耗品 + 餐飲部門 > 餐飲 > 出餐費用 + 行政 > 辦公 > 文具耗材 + 兒童事工 > 教材 > 印刷費用 +``` + +> **設計原則:** 大類(影音、餐飲)是跨 Ministry 共用的,同一大類可出現在不同 Ministry。 +> 例如:敬拜事工和兒童事工都可能有「影音 > 設備」的支出。 + +**資料模型** + +``` +ExpenseCategoryGroup ← 大類(Level 1) +├── Id +├── NameEn / NameZh (影音 A/V、餐飲 Food、辦公 Office…) +├── SortOrder +└── IsActive + +ExpenseSubCategory ← 子項目(Level 2) +├── Id +├── ExpenseCategoryGroupId (FK → ExpenseCategoryGroup) +├── NameEn / NameZh (設備 Equipment、教育訓練 Training、出餐費用 Catering…) +├── SortOrder +└── IsActive + +Expense ← 每筆支出 +├── Id +├── ExpenseDate +├── Description (說明) +├── Amount +├── MinistryId (哪個事工部門承擔此支出) +├── CategoryGroupId (大類,FK → ExpenseCategoryGroup) +├── SubCategoryId (子項目,FK → ExpenseSubCategory) +├── PaymentMethod (Cash | Check | CreditCard | BankTransfer) +├── CheckNumber (若 PaymentMethod = Check) +├── PayeeType (Vendor | StaffReimbursement) +├── PayeeName (廠商名稱 或 同工姓名) +├── MemberId (若 PayeeType = StaffReimbursement → 指向同工) +├── ReceiptBlobPath (收據照片,Azure Blob: finance/receipts/) +├── Status (Draft | PendingApproval | Approved | Paid | Rejected) +├── RejectionReason +├── SubmittedByUserId +├── ApprovedByUserId +├── ApprovedAt +├── PaidAt +├── Notes +└── CreatedAt +``` + +**預設大類 & 子項目(種子資料,可在後台自訂)** + +| 大類(EN / 中)| 子項目 | 常用於 Ministry | +|----------------|--------|-----------------| +| Equipment / 設備 | 購置 · 租借 · 維修 | 敬拜、PPT/影音、音控、場地組、兒牧 | +| Consumables / 耗材 | 電池 · 配件 · 清潔用品 · 文具 | 音控、PPT/影音、場地組、招待、行政 | +| Food & Beverage / 餐飲 | 出餐費用 · 食材採購 · 器具 · 消耗品 | 餐飲、兒牧 | +| Training / 教育訓練 | 課程費用 · 書籍 · 研討會 · 差旅 | 敬拜、PPT/影音、音控、兒牧 | +| Materials / 教材 | 印刷費用 · 手工材料 · 版權購買 | 兒牧、講道、司會 | +| Facility / 場地 | 場地租金 · 水電 · 保險 · 裝飾 | 場地組、行政 | +| Printing / 印刷 | 週報 · 程序單 · 海報 | 行政、招待、兒牧 | +| Missions / 宣教 | 奉獻轉帳 · 宣教士支援 · 差旅 | 行政 | +| Benevolence / 關懷 | 急難救助 · 慰問禮品 · 探訪費用 | 行政 | +| Other / 其他 | 雜支 | 所有 | + +**Ministry × 大類 常用對應(供財務設定參考)** + +| Ministry | 最常用大類 | +|----------|-----------| +| 行政 Administration | 辦公耗材 · 印刷 · 場地 · 宣教 · 關懷 | +| 講道 Preaching | 教材 · 書籍(Training) | +| 司會 Emcee | 印刷(程序單) | +| 敬拜 Worship | 設備 · 教育訓練 · 耗材 | +| PPT/影音 Media | 設備 · 耗材 · 教育訓練 | +| 音控 Sound | 設備 · 耗材 · 教育訓練 | +| 場地組 Facility | 設備 · 場地 · 耗材(清潔) | +| 招待 Hospitality | 耗材 · 印刷(程序單) | +| 兒牧 Children | 教材 · 設備 · 餐飲 · 教育訓練 | +| 餐飲 Catering | 餐飲(出餐費用 · 食材 · 器具) | + +**UI 選擇流程(錄入時)** + +``` +選擇 Ministry → 選擇大類 → 選擇子項目 +[餐飲 ▼] [餐飲 ▼] [出餐費用 ▼] +[敬拜 ▼] [設備 ▼] [購置 ▼] +``` + +- 大類清單不依 Ministry 過濾(保持靈活) +- 選定大類後,子項目自動篩選對應選項 +- 找不到適合子項目 → 選「其他」並填說明 + +**狀態流轉** + +``` +廠商直接付款: (finance 直接建立) → Paid +同工報銷申請: Draft → PendingApproval → Approved → Paid + └─────────────────→ Rejected +``` + +**月底對帳(月結報表)** + +這是整個財務閉環的核心: + +``` +月結對帳表(MonthlyStatement) + +期初餘額(Opening Balance) $X,XXX.XX + + 本月奉獻收入(從 Giving 表加總) $X,XXX.XX + + 其他收入(手動補充) $ .00 + − 本月支出(從 Expense 表加總) $X,XXX.XX + = 帳面期末餘額(Calculated Closing) $X,XXX.XX + +銀行對帳單餘額(Bank Statement Balance) $X,XXX.XX +差異(Difference) $ 0 ← 目標為零 +``` + +``` +MonthlyStatement +├── Id +├── Year / Month +├── OpeningBalance (手動輸入上月結餘,首月由財務設定) +├── TotalGiving (auto: SUM Giving WHERE month) +├── TotalOtherIncome (手動補充) +├── TotalExpenses (auto: SUM Expense WHERE month AND Status=Paid) +├── CalculatedClosingBalance (computed) +├── BankStatementBalance (財務同工手動輸入) +├── Difference (computed: Calculated − Bank) +├── Notes +├── Status (Draft | Reconciled) +├── ReconciledByUserId +└── ReconciledAt +``` + +> **Phase 3 預算功能(MinistryBudget)預留欄位:** +> 分類架構現在就做好,Phase 3 只需增加 `MinistryBudget` 表, +> 按 Ministry + CategoryGroup + SubCategory 設定年度預算額, +> 對比實際 Expense 計算使用率,不需改動 Expense 結構。 + +**收據照片上傳(Azure Blob)** + +``` +finance/ +└── receipts/ + └── {year}/{month}/{expenseId}-{filename}.jpg +``` + +- 使用 Capacitor Camera 拍攝,即時上傳 +- 或桌機版直接上傳圖片檔 +- Private,API 產生 SAS URL(限財務角色存取) + +**權限** + +| 操作 | 角色 | +|------|------| +| 提交報銷申請 | 所有登入用戶(含 member) | +| 審核 / 批准 | `finance`、`super_admin` | +| 建立廠商直接付款 | `finance`、`super_admin` | +| 查看所有支出 | `finance`、`pastor`、`super_admin` | +| 查看自己的申請 | 提交者本人 | +| 月底對帳 | `finance`、`super_admin` | + +--- + +#### 3.6b 線上奉獻頁 (Online Giving — Phase 4) + +**推薦:Stripe(主要)+ PayPal 按鈕(次要)** + +| 比較項目 | Stripe | PayPal | +|----------|--------|--------| +| Nonprofit 費率 | **1.5% + $0.30**(需申請 Stripe Nonprofit Rate) | **1.99% + $0.49**(PayPal Giving Fund for 501c3) | +| C# SDK | ✅ 官方 Stripe.net | ✅ PayPalCheckoutSdk | +| Apple Pay / Google Pay | ✅ 原生支援 | ❌ 需另外設定 | +| Webhook 可靠性 | ✅ 非常成熟 | ⚠️ 歷史上較多問題 | +| 定期奉獻 | ✅ Stripe Subscription | ⚠️ 較複雜 | + +**線上奉獻頁流程(rolac.org/give)** + +``` +訪客/教友進入 /give 頁面 + │ + ▼ +選擇奉獻��金(Tithe / General / Special) +選擇金額(預設選項 $50 / $100 / $200 / 自訂) +選擇頻率(一次性 / 每月) +填寫姓名、Email(非登入狀態) + │ + ▼ +Stripe Payment Element(信用卡 / Apple Pay / Google Pay) + │ + ▼ +Stripe 處理付款 + │ + ┌──┴──┐ + 成功 失敗 + │ + ▼ +Stripe Webhook → POST /api/webhooks/stripe +→ 自動建立 Giving 記錄(PaymentSource = OnlineStripe) +→ Email 確認信給奉獻者 +→ 若有 MemberId → 加入個人奉獻歷史 +``` + +**Stripe Webhook 安全驗證(C#)** +```csharp +[HttpPost("/api/webhooks/stripe")] +public async Task StripeWebhook() +{ + var json = await new StreamReader(Request.Body).ReadToEndAsync(); + var stripeEvent = EventUtility.ConstructEvent( + json, + Request.Headers["Stripe-Signature"], + _stripeConfig.WebhookSecret // 環境變數 + ); + + if (stripeEvent.Type == EventTypes.PaymentIntentSucceeded) + { + var intent = stripeEvent.Data.Object as PaymentIntent; + await _givingService.RecordOnlineGivingAsync(intent); + } + return Ok(); +} +``` + +**環境變數(加入 .env)** +``` +STRIPE_SECRET_KEY=sk_live_... +STRIPE_PUBLISHABLE_KEY=pk_live_... +STRIPE_WEBHOOK_SECRET=whsec_... +PAYPAL_CLIENT_ID=... +PAYPAL_CLIENT_SECRET=... +``` + +--- + +### 3.7 年度收據 (Annual Giving Statement) + +**功能清單** +- 依年度、教友產生正式奉獻收據 +- PDF 格式,含教會抬頭、稅務資訊(EIN)、奉獻明細 +- 批次產生(一次為所有符合條件的教友生成) +- Email 直接寄送給教友 +- 收據補發 / 作廢紀錄 +- 符合 IRS 501(c)(3) 格式要求 + +--- + +### 3.8 Audit Log + +**記錄範圍** + +| 事件類型 | 範例 | +|----------|------| +| 資料異動 | 教友資料修改、刪除 | +| 財務操作 | 奉獻新增、編輯、刪除 | +| 權限變更 | 角色指派、撤銷 | +| 認證事件 | 登入成功、失敗、密碼重設 | +| 系統設定 | 設定變更 | + +**記錄欄位** +``` +- timestamp (UTC) +- actor_id (who) +- actor_role +- action (CREATE / UPDATE / DELETE / LOGIN / …) +- resource_type +- resource_id +- before_snapshot (JSON) +- after_snapshot (JSON) +- ip_address +- user_agent +``` + +**存取控制** +- 只有 `super_admin` 與 `pastor` 可查看 Audit Log +- Audit Log 本身不可刪除(只能 Archive) + +--- + +### 3.9 報表 (Reports) + +**預設報表清單** + +| 報表名稱 | 說明 | 受眾 | +|----------|------|------| +| 教友統計 | 按狀態/性別/年齡/小組分佈 | 牧師、理事、同工會主席 | +| 新人追蹤 | 過去 N 個月新訪客/受洗趨勢 | 牧師 | +| 出席報表 | 主日 & 小組出席率趨勢 | 牧師、組長 | +| 奉獻摘要 | 月/季/年 奉獻總額、按類型分佈 | 財務、牧師 | +| 服事參與 | 每人服事頻率、空缺率 | 事工負責人 | +| 關懷追蹤 | 標記需關懷教友的跟進記錄 | 牧師、組長 | + +**技術需求** +- 圖表視覺化(Bar / Line / Pie) +- 可匯出 CSV / PDF +- 報表可設定排程自動寄送 + +--- + +### 3.10 Ministry Scope + +**概念說明** +Ministry Scope 讓 `ministry_leader` 與 `cell_leader` 只能存取屬於自己事工範圍的教友、服事與報表資料,不會看到其他事工的敏感資訊。 + +**實作要點** +- 每筆資源(教友、服事項目、奉獻記錄)綁定 `ministry_id` +- API 層強制過濾(後端 Row-Level Security 或 Middleware) +- 跨事工存取需由上層角色(coworker_chair / pastor)代為操作 +- Audit Log 記錄跨 Scope 操作 + +--- + +### 3.12 敬拜歌曲庫 (Worship Song Library) + +> **用途:** 集中管理教會使用的詩歌,整合歌詞庫、敬拜排序規劃、YouTube 參考連結與教會自錄影片。 + +--- + +#### 3.12a 歌曲資料庫 (Song Catalog) + +**核心欄位** + +``` +Song +├── Id +├── TitleEn / TitleZh +├── ArtistEn / ArtistZh (原唱/作者) +├── AlbumEn / AlbumZh (選填) +├── CcliNumber (string, 選填) ← CCLI 版權號碼 +├── DefaultKey (e.g. "G", "Ab") +├── Tempo (Slow | Mid | Fast) +├── TimeSignature (e.g. "4/4", "3/4") +├── Language (EN | ZH | Bilingual) +├── Tags (string[], e.g. ["Worship","Praise","Christmas"]) +├── IsActive (bool, 是否仍在使用) +├── CreatedByUserId +└── UpdatedAt + +SongLyrics ← 歌詞分段存放 +├── Id +├── SongId +├── SectionType (Verse | Chorus | Bridge | PreChorus | Outro | Intro) +├── SectionOrder (int, 排序) +├── LyricEn +└── LyricZh + +SongMedia ← 多媒體連結 +├── Id +├── SongId +├── MediaType (YouTubeReference | ChurchRecording | SheetMusic | ChordChart) +├── Title +├── YouTubeVideoId (只存 ID,e.g. "dQw4w9WgXcQ") +├── BlobPath (Azure Blob,教會自錄影片/樂譜 PDF) +└── UploadedAt +``` + +**功能清單** +- 新增 / 編輯 / 封存詩歌 +- 雙語歌詞分段輸入(Verse / Chorus / Bridge…) +- 搜尋:標題、標籤、CCLI 號碼、語言篩選 +- 上傳樂譜 PDF / 和弦表 → Azure Blob(`worship/sheets/`) +- 關聯 YouTube 參考影片(只存 Video ID,嵌入播放,不下載) +- 上傳教會自錄影片 → Azure Blob(`worship/recordings/`) +- 匯出歌詞 PDF(用於投影或印刷) + +--- + +#### 3.12b 敬拜規劃 / 歌單 (Worship Set / Setlist) + +**概念:** 每次主日或特別聚會,敬拜負責人規劃當次要唱的詩歌清單及順序。 + +``` +WorshipSet +├── Id +├── ServiceDate +├── ServiceType (主日崇拜 | 禱告會 | 特別聚會) +├── LeadWorshiperId (敬拜帶領人 → Member) +├── Status (Draft | Confirmed | Completed) +├── Notes +├── CreatedByUserId +└── SetItems[] + +WorshipSetItem +├── Id +├── WorshipSetId +├── SongId +├── ItemOrder (歌曲順序) +├── PlayKey (本次演唱調性,可與 Song.DefaultKey 不同) +├── Notes (e.g. "唱兩遍,不含橋段") +└── IsSkipped (bool, 臨時略過) +``` + +**功能清單** +- 建立歌單,從歌曲庫拖曳/搜尋新增詩歌 +- 調整歌曲順序(拖曳排序) +- 指定每首歌本次演唱調性 +- 歌單狀態:草稿 → 確認 → 完成 +- 一鍵產生 **歌單 PDF**(供投影、印刷或發給敬拜隊) +- 歌單 PDF 包含:歌名(雙語)、歌詞、本次調性、備注 +- **歷史歌單統計:** 哪些詩歌最常使用(供 AI 整合預留) + +--- + +#### 3.12c YouTube 連結策略 + +> **重要:直接下載 YouTube 影片違反 YouTube 服務條款(ToS),無論用途。** + +| 做法 | 合法性 | 建議 | +|------|--------|------| +| 嵌入 YouTube Player(`