This commit is contained in:
Chris Chen
2026-05-29 20:54:08 -07:00
parent 95fa37ebdf
commit fe50ea3d30
6 changed files with 99 additions and 295 deletions
@@ -23,7 +23,7 @@
<!-- Card B — Add giving --> <!-- Card B — Add giving -->
<section class="card"> <section class="card">
<h3 class="section-title">Add Giving / 錄入奉獻</h3> <h3 class="section-title">Add Giving / 錄入奉獻</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-x-4 gap-y-3"> <div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
<label class="flex flex-col gap-1 md:col-span-2">Giver <label class="flex flex-col gap-1 md:col-span-2">Giver
<kendo-dropdownlist *ngIf="!entry.isAnonymous" [data]="memberResults" textField="displayName" valueField="id" <kendo-dropdownlist *ngIf="!entry.isAnonymous" [data]="memberResults" textField="displayName" valueField="id"
[valuePrimitive]="true" [filterable]="true" (filterChange)="onMemberFilter($event)" [valuePrimitive]="true" [filterable]="true" (filterChange)="onMemberFilter($event)"
@@ -1,67 +0,0 @@
<kendo-drawer-container>
<kendo-drawer [mode]="'overlay'" [expanded]="layoutService.drawerExpanded()" [width]="280">
<kendo-drawer-content>
<div class="drawer-content">
<div class="drawer-header">
<h3>User Portal</h3>
<p>ROLCC AC Portal</p>
</div>
<nav class="drawer-nav">
<div class="nav-section">
<h4>Main</h4>
<button *ngFor="let item of mainNavItems" kendoButton
[fillMode]="item.active ? 'solid' : 'flat'" [themeColor]="item.active ? 'primary' : 'base'"
[svgIcon]="item.icon" class="nav-button" (click)="navigateTo(item.path)">
{{ item.text }}
</button>
</div>
<div class="nav-section">
<h4>Management</h4>
<button *ngFor="let item of managementNavItems" kendoButton
[fillMode]="item.active ? 'solid' : 'flat'" [themeColor]="item.active ? 'primary' : 'base'"
[svgIcon]="item.icon" class="nav-button" (click)="navigateTo(item.path)">
{{ item.text }}
</button>
</div>
<div class="nav-section">
<h4>Support</h4>
<button *ngFor="let item of supportNavItems" kendoButton
[fillMode]="item.active ? 'solid' : 'flat'" [themeColor]="item.active ? 'primary' : 'base'"
[svgIcon]="item.icon" class="nav-button" (click)="navigateTo(item.path)">
{{ item.text }}
</button>
</div>
<div class="nav-section" *ngIf="showMemberAdminSection || showUserAdminSection">
<h4>Administration</h4>
<button *ngFor="let item of memberAdminNavItems" kendoButton
[fillMode]="item.active ? 'solid' : 'flat'" [themeColor]="item.active ? 'primary' : 'base'"
[svgIcon]="item.icon" class="nav-button" (click)="navigateTo(item.path)">
{{ item.text }}
</button>
<ng-container *ngIf="showUserAdminSection">
<button *ngFor="let item of userAdminNavItems" kendoButton
[fillMode]="item.active ? 'solid' : 'flat'" [themeColor]="item.active ? 'primary' : 'base'"
[svgIcon]="item.icon" class="nav-button" (click)="navigateTo(item.path)">
{{ item.text }}
</button>
</ng-container>
</div>
<div class="nav-section" *ngIf="showFinanceSection">
<h4>Finance</h4>
<button *ngFor="let item of financeNavItems" kendoButton
[fillMode]="item.active ? 'solid' : 'flat'" [themeColor]="item.active ? 'primary' : 'base'"
[svgIcon]="item.icon" class="nav-button" (click)="navigateTo(item.path)">
{{ item.text }}
</button>
</div>
</nav>
</div>
</kendo-drawer-content>
</kendo-drawer>
</kendo-drawer-container>
@@ -1,66 +0,0 @@
.drawer-content {
height: 100%;
display: flex;
flex-direction: column;
}
.drawer-header {
padding: 1.5rem 1rem;
border-bottom: 1px solid #e0e0e0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
h3 {
margin: 0 0 0.25rem 0;
font-size: 1.25rem;
font-weight: 600;
}
p {
margin: 0;
font-size: 0.875rem;
opacity: 0.9;
}
}
.drawer-nav {
flex: 1;
padding: 1rem 0;
overflow-y: auto;
}
.nav-section {
margin-bottom: 1.5rem;
h4 {
margin: 0 0 0.5rem 0;
padding: 0 1rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #666;
}
}
.nav-button {
width: 100%;
justify-content: flex-start;
text-align: left;
padding: 0.75rem 1rem;
margin: 0.125rem 0;
border-radius: 0;
&:hover {
background-color: #f8f9fa;
}
&.k-button-solid {
background-color: #e3f2fd;
color: #1976d2;
&:hover {
background-color: #bbdefb;
}
}
}
@@ -1,136 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, NavigationEnd, RouterModule } from '@angular/router';
import { LayoutModule } from '@progress/kendo-angular-layout';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { IconsModule } from '@progress/kendo-angular-icons';
import { SVGIcon, homeIcon, calendarIcon, userIcon, groupIcon } from '@progress/kendo-svg-icons';
import { LayoutService } from '../../../../layout/services/layout.service';
import { AuthService, UserInfo } from '../../../../shared/services/auth.service';
import { Subject, takeUntil, filter } from 'rxjs';
interface NavItem {
text: string;
icon: SVGIcon;
path: string;
active?: boolean;
}
@Component({
selector: 'app-user-navbar',
standalone: true,
imports: [
CommonModule,
RouterModule,
LayoutModule,
ButtonsModule,
IconsModule
],
templateUrl: './user-navbar.component.html',
styleUrls: ['./user-navbar.component.scss']
})
export class UserNavbarComponent implements OnInit, OnDestroy {
public homeIcon: SVGIcon = homeIcon;
public calendarIcon: SVGIcon = calendarIcon;
public peopleIcon: SVGIcon = userIcon; // Using userIcon as fallback
public bedIcon: SVGIcon = userIcon; // Using userIcon as fallback
public userIcon: SVGIcon = userIcon;
public pillIcon: SVGIcon = userIcon; // Using userIcon as fallback
public chartIcon: SVGIcon = userIcon; // Using userIcon as fallback
public buildingIcon: SVGIcon = userIcon; // Using userIcon as fallback
public creditCardIcon: SVGIcon = userIcon; // Using userIcon as fallback
public supportIcon: SVGIcon = userIcon; // Using userIcon as fallback
public mainNavItems: NavItem[] = [
{ text: 'Dashboard', icon: this.homeIcon, path: '/user-portal/dashboard' },
{ text: 'Schedule', icon: this.calendarIcon, path: '/user-portal/schedule' },
{ text: 'Patients', icon: this.peopleIcon, path: '/user-portal/patients' }
];
public managementNavItems: NavItem[] = [
{ text: 'Bed Management', icon: this.bedIcon, path: '/user-portal/bed-management' },
{ text: 'Staff', icon: this.userIcon, path: '/user-portal/staff' },
{ text: 'Pharmacy', icon: this.pillIcon, path: '/user-portal/pharmacy' },
{ text: 'Reports', icon: this.chartIcon, path: '/user-portal/reports' },
{ text: 'Departments', icon: this.buildingIcon, path: '/user-portal/departments' },
{ text: 'Payments', icon: this.creditCardIcon, path: '/user-portal/payments' }
];
public supportNavItems: NavItem[] = [
{ text: 'Support', icon: this.supportIcon, path: '/user-portal/support' }
];
public memberAdminNavItems: NavItem[] = [
{ text: 'Members', icon: groupIcon, path: '/user-portal/admin/members' },
];
public userAdminNavItems: NavItem[] = [
{ text: 'User Management', icon: userIcon, path: '/user-portal/admin/users' },
];
public financeNavItems: NavItem[] = [
{ text: 'Offering Entry', icon: this.creditCardIcon, path: '/user-portal/finance/offering-session' },
{ text: 'Givings', icon: this.chartIcon, path: '/user-portal/finance/givings' },
{ text: 'Giving Types', icon: this.buildingIcon, path: '/user-portal/finance/giving-categories' },
];
public showMemberAdminSection = false;
public showUserAdminSection = false;
public showFinanceSection = false;
private destroy$ = new Subject<void>();
constructor(
public layoutService: LayoutService,
private router: Router,
private authService: AuthService
) { }
ngOnInit(): void {
// Listen to route changes to update active states
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.destroy$)
)
.subscribe((event: NavigationEnd) => {
this.updateActiveStates(event.url);
});
// Set initial active state
this.updateActiveStates(this.router.url);
this.authService.currentUser$.pipe(takeUntil(this.destroy$)).subscribe(user => {
const roles = user?.roles ?? [];
this.showMemberAdminSection = roles.some(r => r === 'super_admin' || r === 'secretary');
this.showUserAdminSection = roles.includes('super_admin');
this.showFinanceSection = roles.some(r => r === 'finance' || r === 'super_admin');
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
public navigateTo(path: string): void {
this.router.navigate([path]);
this.layoutService.closeDrawer();
}
private updateActiveStates(currentUrl: string): void {
// Reset all active states
[...this.mainNavItems, ...this.managementNavItems, ...this.supportNavItems,
...this.memberAdminNavItems, ...this.userAdminNavItems, ...this.financeNavItems]
.forEach(item => item.active = false);
// Set active state for current route
const activeItem = [...this.mainNavItems, ...this.managementNavItems, ...this.supportNavItems,
...this.memberAdminNavItems, ...this.userAdminNavItems, ...this.financeNavItems]
.find(item => currentUrl.startsWith(item.path));
if (activeItem) {
activeItem.active = true;
}
}
}
+43 -16
View File
@@ -509,7 +509,7 @@ Table: ExpenseCategoryGroups
| SortOrder | int NOT NULL DEFAULT 0 | | | SortOrder | int NOT NULL DEFAULT 0 | |
| IsActive | bool NOT NULL DEFAULT true | | | IsActive | bool NOT NULL DEFAULT true | |
**Seed 大類(10 個):** **Seed 大類(11 個):**
| Id | Name_en | Name_zh | | Id | Name_en | Name_zh |
|----|---------|---------| |----|---------|---------|
@@ -523,6 +523,7 @@ Table: ExpenseCategoryGroups
| 8 | Missions | 宣教 | | 8 | Missions | 宣教 |
| 9 | Benevolence | 關懷救助 | | 9 | Benevolence | 關懷救助 |
| 10 | Other | 其他 | | 10 | Other | 其他 |
| 11 | Personnel | 人事 |
### ExpenseSubCategory(支出子項目) ### ExpenseSubCategory(支出子項目)
@@ -539,25 +540,51 @@ Table: ExpenseSubCategories
| SortOrder | int NOT NULL DEFAULT 0 | | | SortOrder | int NOT NULL DEFAULT 0 | |
| IsActive | bool NOT NULL DEFAULT true | | | IsActive | bool NOT NULL DEFAULT true | |
**Seed 子項目(部分範例):** **Seed 子項目(完整種子):**
| GroupId | Name_en | Name_zh | | GroupId | Name_en | Name_zh |
|---------|---------|---------| |---------|---------|---------|
| 1 Equipment | Audio/Visual Equipment | 影音設備 | | 1 Equipment | Purchase | 購置 |
| 1 Equipment | Computer & Peripherals | 電腦周邊 | | 1 Equipment | Rental | 租借 |
| 1 Equipment | Musical Instruments | 樂器 | | 1 Equipment | Maintenance & Repair | 維修 |
| 2 Consumables | Office Supplies | 辦公用品 | | 2 Consumables | Batteries | 電池 |
| 2 Consumables | Accessories | 配件 |
| 2 Consumables | Cleaning Supplies | 清潔用品 | | 2 Consumables | Cleaning Supplies | 清潔用品 |
| 3 Food & Beverage | Sunday Agape Meal | 愛宴餐費 | | 2 Consumables | Office Supplies | 文具 |
| 3 Food & Beverage | Fellowship Snacks | 交誼茶點 | | 3 Food & Beverage | Catering | 出餐費用 |
| 4 Training | Conference Registration | 會議報名費 | | 3 Food & Beverage | Food Ingredients | 食材採購 |
| 4 Training | Books & Resources | 書籍教材 | | 3 Food & Beverage | Utensils | 器具 |
| 5 Materials | Children's Curriculum | 兒童教材 | | 3 Food & Beverage | Consumables | 消耗品 |
| 5 Materials | Bibles & Hymnals | 聖經/詩本 | | 4 Training | Course Fees | 課程費用 |
| 4 Training | Books | 書籍 |
| 4 Training | Conference | 研討會 |
| 4 Training | Travel | 差旅 |
| 5 Materials | Printing | 印刷費用 |
| 5 Materials | Craft Supplies | 手工材料 |
| 5 Materials | Copyright & Licensing | 版權購買 |
| 6 Facility | Rent | 場地租金 | | 6 Facility | Rent | 場地租金 |
| 6 Facility | Utilities | 水電 | | 6 Facility | Utilities | 水電 |
| 7 Printing | Bulletins | 週報印刷 | | 6 Facility | Property Insurance | 財產保險 |
| 8 Missions | Missionary Support | 宣教士支持 | | 6 Facility | Decoration | 裝飾 |
| 7 Printing | Bulletins | 週報 |
| 7 Printing | Order of Service | 程序單 |
| 7 Printing | Posters | 海報 |
| 8 Missions | Offering Transfer | 奉獻轉帳 |
| 8 Missions | Missionary Support | 宣教士支援 |
| 8 Missions | Travel | 差旅 |
| 9 Benevolence | Emergency Aid | 急難救助 |
| 9 Benevolence | Condolence Gifts | 慰問禮品 |
| 9 Benevolence | Visit Expenses | 探訪費用 |
| 10 Other | Miscellaneous | 雜支 |
| 11 Personnel | Salary & Wages | 薪資 |
| 11 Personnel | Payroll Taxes | 薪資稅費 |
| 11 Personnel | Employee Benefits | 員工福利 |
| 11 Personnel | Workers Compensation | 勞工保險 |
| 11 Personnel | Honorarium | 酬庸 |
| 11 Personnel | Staff Training | 同工進修 |
| 11 Personnel | Contract Labor | 外包勞務 |
> **備注:** `Facility > 財產保險` 指建築物/場地責任險;員工健保、團體保險等歸 `Personnel > 員工福利`。同工代墊報銷依**實際購買物**選大類,不歸人事。
### Expense(支出記錄) ### Expense(支出記錄)
@@ -954,7 +981,7 @@ super_admin, pastor, board_member, coworker_chair, ministry_leader, district_lea
5. Mission / 宣教奉獻 5. Mission / 宣教奉獻
``` ```
### ExpenseCategoryGroups10 個大類) ### ExpenseCategoryGroups11 個大類)
``` ```
見 §8 Seed 大類列表 見 §8 Seed 大類列表
``` ```
+55 -9
View File
@@ -2,7 +2,7 @@
**教會:** River Of Life Christian Church In Arcadia (ROLAC) **教會:** River Of Life Christian Church In Arcadia (ROLAC)
**規模:** 50100 人 **規模:** 50100 人
**文件版本:** v0.2 (2026-05-24) **文件版本:** v0.3 (2026-05-29)
--- ---
@@ -522,6 +522,7 @@ Ministry(事工部門) × 大類(Category Group × 子項目(Sub-
餐飲部門 > 餐飲 > 消耗品 餐飲部門 > 餐飲 > 消耗品
餐飲部門 > 餐飲 > 出餐費用 餐飲部門 > 餐飲 > 出餐費用
行政 > 辦公 > 文具耗材 行政 > 辦公 > 文具耗材
行政 > 人事 > 薪資
兒童事工 > 教材 > 印刷費用 兒童事工 > 教材 > 印刷費用
``` ```
@@ -577,17 +578,20 @@ Expense ← 每筆支出
| Food & Beverage / 餐飲 | 出餐費用 · 食材採購 · 器具 · 消耗品 | 餐飲、兒牧 | | Food & Beverage / 餐飲 | 出餐費用 · 食材採購 · 器具 · 消耗品 | 餐飲、兒牧 |
| Training / 教育訓練 | 課程費用 · 書籍 · 研討會 · 差旅 | 敬拜、PPT/影音、音控、兒牧 | | Training / 教育訓練 | 課程費用 · 書籍 · 研討會 · 差旅 | 敬拜、PPT/影音、音控、兒牧 |
| Materials / 教材 | 印刷費用 · 手工材料 · 版權購買 | 兒牧、講道、司會 | | Materials / 教材 | 印刷費用 · 手工材料 · 版權購買 | 兒牧、講道、司會 |
| Facility / 場地 | 場地租金 · 水電 · 保險 · 裝飾 | 場地組、行政 | | Facility / 場地 | 場地租金 · 水電 · 財產保險 · 裝飾 | 場地組、行政 |
| Printing / 印刷 | 週報 · 程序單 · 海報 | 行政、招待、兒牧 | | Printing / 印刷 | 週報 · 程序單 · 海報 | 行政、招待、兒牧 |
| Missions / 宣教 | 奉獻轉帳 · 宣教士支援 · 差旅 | 行政 | | Missions / 宣教 | 奉獻轉帳 · 宣教士支援 · 差旅 | 行政 |
| Benevolence / 關懷 | 急難救助 · 慰問禮品 · 探訪費用 | 行政 | | Benevolence / 關懷 | 急難救助 · 慰問禮品 · 探訪費用 | 行政 |
| Other / 其他 | 雜支 | 所有 | | Other / 其他 | 雜支 | 所有 |
| Personnel / 人事 | 薪資 · 薪資稅費 · 員工福利 · 勞工保險 · 酬庸 · 同工進修 · 外包勞務 | 行政 |
> **分類備注:** `Facility > 財產保險` 為建築物/場地責任險;員工健保等歸 `Personnel > 員工福利`。同工代墊報銷(`StaffReimbursement`)依實際購買物選大類,薪資/福利付款才歸人事。
**Ministry × 大類 常用對應(供財務設定參考)** **Ministry × 大類 常用對應(供財務設定參考)**
| Ministry | 最常用大類 | | Ministry | 最常用大類 |
|----------|-----------| |----------|-----------|
| 行政 Administration | 辦公耗材 · 印刷 · 場地 · 宣教 · 關懷 | | 行政 Administration | 人事 · 辦公耗材 · 印刷 · 場地 · 宣教 · 關懷 |
| 講道 Preaching | 教材 · 書籍(Training | | 講道 Preaching | 教材 · 書籍(Training |
| 司會 Emcee | 印刷(程序單) | | 司會 Emcee | 印刷(程序單) |
| 敬拜 Worship | 設備 · 教育訓練 · 耗材 | | 敬拜 Worship | 設備 · 教育訓練 · 耗材 |
@@ -677,6 +681,7 @@ finance/
| 審核 / 批准 | `finance``super_admin` | | 審核 / 批准 | `finance``super_admin` |
| 建立廠商直接付款 | `finance``super_admin` | | 建立廠商直接付款 | `finance``super_admin` |
| 查看所有支出 | `finance``pastor``super_admin` | | 查看所有支出 | `finance``pastor``super_admin` |
| 查看人事類支出 | `finance``pastor``super_admin`(事工領袖不可見) |
| 查看自己的申請 | 提交者本人 | | 查看自己的申請 | 提交者本人 |
| 月底對帳 | `finance``super_admin` | | 月底對帳 | `finance``super_admin` |
@@ -1223,7 +1228,7 @@ AuditLogbigint PKimmutable,所有操作均記錄)
| 後端 API | **ASP.NET Core (C#)** | REST API | | 後端 API | **ASP.NET Core (C#)** | REST API |
| ORM | **Entity Framework Core** | Code-first migrations | | ORM | **Entity Framework Core** | Code-first migrations |
| 資料庫 | **PostgreSQL** | 自架於 Docker | | 資料庫 | **PostgreSQL** | 自架於 Docker |
| 檔案儲存 | **Azure Blob Storage** | 教友照片、PDF 收據、CMS 媒體 | | 檔案儲存 | **本地檔案儲存(現階段)→ Azure Blob Storage(未來)** | 透過 `IFileOperationService` 抽象,現以 `LocalFileOperationService`base folder 由 config 設定)實作,未來切換 `AzureFileOperationService`教友照片、PDF 收據、CMS 媒體 |
| 原始碼管理 | **Gitea** | 自架,Docker 容器 | | 原始碼管理 | **Gitea** | 自架,Docker 容器 |
| CI/CD | **Jenkins** | 自架,Docker 容器 | | CI/CD | **Jenkins** | 自架,Docker 容器 |
| 容器化 | **Docker + Docker Compose** | 所有服務容器化 | | 容器化 | **Docker + Docker Compose** | 所有服務容器化 |
@@ -1310,6 +1315,45 @@ assets/i18n/zh-TW.json
- **ASP.NET Core Identity** — 使用者管理、密碼雜湊 - **ASP.NET Core Identity** — 使用者管理、密碼雜湊
- **全新資料庫** — 無舊資料遷移需求,直接 EF Code-First Migration - **全新資料庫** — 無舊資料遷移需求,直接 EF Code-First Migration
### 檔案儲存抽象層 (File Storage Abstraction)
> **背景(2026-05-29 決定):** Microsoft 拒絕了我們的 Nonprofit / Azure 申請,**現階段無法使用 Azure Blob Storage**。
> 因此先改用**本地檔案儲存**base folder 由 config 設定),未來取得 Azure 額度後再切換為 Azure Blob,**業務程式碼不需改動**。
**設計:以介面抽象隔離儲存後端**
所有檔案讀寫(教友照片、PDF 收據、CMS 媒體、收據照片、敬拜樂譜/影片等)一律透過 `IFileOperationService` 介面操作,業務層不直接依賴任何特定儲存實作。
```
IFileOperationService ← 抽象介面(業務層僅依賴此介面)
├── SaveAsync(path, stream) → 儲存檔案,回傳相對路徑 / 識別碼
├── GetAsync(path) → 讀取檔案串流
├── DeleteAsync(path) → 刪除檔案
├── ExistsAsync(path) → 是否存在
└── GetUrlAsync(path, expiry?) → 取得存取 URL(本地:API 代理路徑;AzureSAS URL
LocalFileOperationService ← 現階段實作(✅ Phase 1)
├── Base folder 從 config 讀取(e.g. FileStorage:BasePath
├── 路徑沿用既有結構(cms/images/、finance/receipts/、worship/… 相對於 base folder
├── Private 檔案經由 API 代理 + 角色授權提供存取(取代 SAS URL)
└── 容器化時 base folder 對映到 Docker volume,確保持久化
AzureFileOperationService ← 未來實作(🔜 取得 Azure 額度後)
├── 對映到 Azure Blob Container
├── GetUrlAsync 回傳具時效的 SAS URL
└── 切換方式:DI 註冊改綁此實作 + 一次性資料搬移,業務程式碼零改動
```
**設定範例(appsettings**
```jsonc
"FileStorage": {
"Provider": "Local", // Local | Azure(未來)
"BasePath": "/data/rolac-files" // 本地 base folder(容器內對映 volume
}
```
> **與本文件其他段落的關係:** 文中所有提到 `Azure Blob`blob 路徑的地方(§3.5 CMS 圖片、§3.6d `finance/receipts/`、§3.12e `worship/...`),現階段一律改由 `IFileOperationService` + `LocalFileOperationService` 以對應相對路徑儲存於 config 的 base folder 下;待 Azure 開通後切換 `AzureFileOperationService` 即還原為 Blob 儲存,路徑結構不變。
--- ---
## 7. 開發階段規劃 (Roadmap) ## 7. 開發階段規劃 (Roadmap)
@@ -1322,6 +1366,7 @@ assets/i18n/zh-TW.json
- [X] 認證系統(JWT + Refresh Token + ASP.NET Identity - [X] 認證系統(JWT + Refresh Token + ASP.NET Identity
- [X] RBAC 框架(角色 + Ministry Scope middleware - [X] RBAC 框架(角色 + Ministry Scope middleware
- [ ] Audit Log 基礎建設 - [ ] Audit Log 基礎建設
- [ ] 檔案儲存抽象層(`IFileOperationService` + `LocalFileOperationService`base folder 從 config 讀取)
- [ ] Mobile-first UI 元件庫設定(底部導覽、touch target 規範) - [ ] Mobile-first UI 元件庫設定(底部導覽、touch target 規範)
### Phase 1 — 六月上線 MVP5 週,目標 2026-06-30 ### Phase 1 — 六月上線 MVP5 週,目標 2026-06-30
@@ -1334,22 +1379,22 @@ assets/i18n/zh-TW.json
- [ ] 聯絡表單 - [ ] 聯絡表單
#### 教友管理 #### 教友管理
- [ ] 教友 CRUD(基本資料、照片上傳至 Azure Blob - [ ] 教友 CRUD(基本資料、照片上傳,經 `IFileOperationService` → 本地儲存
- [ ] 家庭單元管理 - [ ] 家庭單元管理
- [ ] 搜尋 / 篩選(姓名、狀態、小組) - [ ] 搜尋 / 篩選(姓名、狀態、小組)
- [ ] 教友狀態(會員 / 訪客 / 前會員) - [ ] 教友狀態(會員 / 訪客 / 前會員)
- [ ] i18n 語言切換 UIEN / 中 按鈕) - [ ] i18n 語言切換 UIEN / 中 按鈕)
#### 奉獻追蹤(手動) #### 奉獻追蹤(手動)
- [ ] 奉獻類型設定(Tithe / Offering / Special - [X] 奉獻類型設定(Tithe / Offering / Special
- [ ] 單筆奉獻記錄(現金 / 支票 / Zelle / PayPal - [X] 單筆奉獻記錄(現金 / 支票 / Zelle / PayPal
- [ ] **主日奉獻袋批次輸入(OfferingSession** ← 鍵盤優先快速錄入 - [X] **主日奉獻袋批次輸入(OfferingSession** ← 鍵盤優先快速錄入
- [ ] 個人奉獻歷史查詢(教友 App 端) - [ ] 個人奉獻歷史查詢(教友 App 端)
- [ ] 年度收據 PDF 產生(QuestPDFEIN 42-2682968 - [ ] 年度收據 PDF 產生(QuestPDFEIN 42-2682968
- [ ] 年度收據 Email 批次寄送 - [ ] 年度收據 Email 批次寄送
#### 支出追蹤 & 報銷 #### 支出追蹤 & 報銷
- [ ] 支出類別設定(愛宴 / 辦公用品 / 宣教…) - [ ] 支出類別設定(11 大類種子:設備 / 餐飲 / 人事 / 宣教…)
- [ ] 廠商直接付款記錄(含支票號碼) - [ ] 廠商直接付款記錄(含支票號碼)
- [ ] 同工代墊報銷申請(含收據照片上傳) - [ ] 同工代墊報銷申請(含收據照片上傳)
- [ ] 財務審核流程(Pending → Approved → Paid - [ ] 財務審核流程(Pending → Approved → Paid
@@ -1464,3 +1509,4 @@ assets/i18n/zh-TW.json
| 17 | 兒童特殊欄位 | 緊急聯絡人、過敏、接送授權,暫緩 | 🟢 低 | ⏳ 未來再評估 | | 17 | 兒童特殊欄位 | 緊急聯絡人、過敏、接送授權,暫緩 | 🟢 低 | ⏳ 未來再評估 |
| 18 | CCLI License 號碼 | 教會是否已有 CCLI License?需填入系統設定 | 🟡 中 | ⏳ 待確認 | | 18 | CCLI License 號碼 | 教會是否已有 CCLI License?需填入系統設定 | 🟡 中 | ⏳ 待確認 |
| 19 | ~~Phase 2/3 模組分配~~ | **已決定**Phase 2 = 服事表排班 + 主日出席統計 + 小組管理;Phase 3 = 事工預算 | 🔴 高 | ✅ 決定 | | 19 | ~~Phase 2/3 模組分配~~ | **已決定**Phase 2 = 服事表排班 + 主日出席統計 + 小組管理;Phase 3 = 事工預算 | 🔴 高 | ✅ 決定 |
| 20 | ~~檔案儲存後端~~ | **已決定**:Microsoft 拒絕申請,暫無法用 Azure Blob。改以 `IFileOperationService` 抽象,現階段用 `LocalFileOperationService`base folder 由 config 設定),未來取得 Azure 額度後切換 `AzureFileOperationService`(見 §6 檔案儲存抽象層) | 🔴 高 | ✅ 決定 |