@@ -45,7 +45,8 @@ export interface CheckDetailDto extends CheckListItemDto {
|
||||
}
|
||||
|
||||
export interface ChurchProfileDto {
|
||||
id: number; name: string; address: string | null; city: string | null;
|
||||
id: number; name: string; nameZh: string | null; phone: string | null;
|
||||
email: string | null; website: string | null; address: string | null; city: string | null;
|
||||
state: string | null; zipCode: string | null; bankName: string | null;
|
||||
bankAccountNumber: string | null; bankRoutingNumber: string | null; nextCheckNumber: number;
|
||||
}
|
||||
|
||||
+83
-46
@@ -1,49 +1,86 @@
|
||||
<div class="page">
|
||||
<div *ngIf="model" class="max-w-3xl">
|
||||
<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">
|
||||
Church Name / 教會名稱
|
||||
<kendo-textbox [(ngModel)]="model.name"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 md:col-span-2">
|
||||
Address / 地址
|
||||
<kendo-textbox [(ngModel)]="model.address"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
City / 城市
|
||||
<kendo-textbox [(ngModel)]="model.city"></kendo-textbox>
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="flex flex-col gap-1">
|
||||
State / 州
|
||||
<kendo-textbox [(ngModel)]="model.state"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Zip / 郵遞區號
|
||||
<kendo-textbox [(ngModel)]="model.zipCode"></kendo-textbox>
|
||||
</label>
|
||||
</div>
|
||||
<label class="flex flex-col gap-1">
|
||||
Bank Name / 銀行名稱
|
||||
<kendo-textbox [(ngModel)]="model.bankName"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Bank Account # / 銀行帳號
|
||||
<kendo-textbox [(ngModel)]="model.bankAccountNumber"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Routing # / 路由號碼
|
||||
<kendo-textbox [(ngModel)]="model.bankRoutingNumber"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Next Check # / 下一張支票號碼
|
||||
<kendo-numerictextbox [(ngModel)]="model.nextCheckNumber" [min]="1" [decimals]="0" format="#"></kendo-numerictextbox>
|
||||
</label>
|
||||
</div>
|
||||
<kendo-tabstrip>
|
||||
<!-- ── Tab 1: Church Info (existing ChurchProfile permission) ──────────── -->
|
||||
<kendo-tabstrip-tab title="Church Info / 教會資料" [selected]="true">
|
||||
<ng-template kendoTabContent>
|
||||
<div *ngIf="model" class="max-w-3xl pt-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
|
||||
<label class="flex flex-col gap-1">
|
||||
Church Name / 教會名稱
|
||||
<kendo-textbox [(ngModel)]="model.name"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Church Name (ZH) / 教會名稱(中)
|
||||
<kendo-textbox [(ngModel)]="model.nameZh"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Phone / 電話
|
||||
<kendo-textbox [(ngModel)]="model.phone"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Email / 電子郵件
|
||||
<kendo-textbox [(ngModel)]="model.email"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 md:col-span-2">
|
||||
Website / 網站
|
||||
<kendo-textbox [(ngModel)]="model.website" placeholder="https://"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 md:col-span-2">
|
||||
Address / 地址
|
||||
<kendo-textbox [(ngModel)]="model.address"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
City / 城市
|
||||
<kendo-textbox [(ngModel)]="model.city"></kendo-textbox>
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="flex flex-col gap-1">
|
||||
State / 州
|
||||
<kendo-textbox [(ngModel)]="model.state"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Zip / 郵遞區號
|
||||
<kendo-textbox [(ngModel)]="model.zipCode"></kendo-textbox>
|
||||
</label>
|
||||
</div>
|
||||
<label class="flex flex-col gap-1">
|
||||
Bank Name / 銀行名稱
|
||||
<kendo-textbox [(ngModel)]="model.bankName"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Bank Account # / 銀行帳號
|
||||
<kendo-textbox [(ngModel)]="model.bankAccountNumber"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Routing # / 路由號碼
|
||||
<kendo-textbox [(ngModel)]="model.bankRoutingNumber"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Next Check # / 下一張支票號碼
|
||||
<kendo-numerictextbox [(ngModel)]="model.nextCheckNumber" [min]="1" [decimals]="0" format="#"></kendo-numerictextbox>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 mt-4">
|
||||
<button kendoButton themeColor="primary" [disabled]="saving" (click)="save()">Save / 儲存</button>
|
||||
<span class="text-sm" style="color:#065f46;">{{ savedMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mt-4">
|
||||
<button kendoButton themeColor="primary" [disabled]="saving" (click)="save()">Save / 儲存</button>
|
||||
<span class="text-sm" style="color:#065f46;">{{ savedMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</kendo-tabstrip-tab>
|
||||
|
||||
<!-- ── Tab 2: Site Settings (Settings permission) ─────────────────────── -->
|
||||
<kendo-tabstrip-tab title="Site Settings / 網站設定" *appHasPermission="settingsPermission">
|
||||
<ng-template kendoTabContent>
|
||||
<app-site-settings-tab></app-site-settings-tab>
|
||||
</ng-template>
|
||||
</kendo-tabstrip-tab>
|
||||
|
||||
<!-- ── Tab 3: Notification Settings (Settings permission) ─────────────── -->
|
||||
<kendo-tabstrip-tab title="Notifications / 通知設定" *appHasPermission="settingsPermission">
|
||||
<ng-template kendoTabContent>
|
||||
<app-notification-settings-tab></app-notification-settings-tab>
|
||||
</ng-template>
|
||||
</kendo-tabstrip-tab>
|
||||
</kendo-tabstrip>
|
||||
</div>
|
||||
|
||||
+12
-1
@@ -3,13 +3,21 @@ import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||
import { LayoutModule } from '@progress/kendo-angular-layout';
|
||||
import { DisbursementApiService } from '../../services/disbursement-api.service';
|
||||
import { ChurchProfileDto } from '../../models/disbursement.model';
|
||||
import { HasPermissionDirective } from '../../../../core/directives/has-permission.directive';
|
||||
import { PermissionModules } from '../../../../core/models/permission.model';
|
||||
import { SiteSettingsTabComponent } from '../../../settings/components/site-settings-tab/site-settings-tab.component';
|
||||
import { NotificationSettingsTabComponent } from '../../../settings/components/notification-settings-tab/notification-settings-tab.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-church-profile-page',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ButtonsModule, InputsModule],
|
||||
imports: [
|
||||
CommonModule, FormsModule, ButtonsModule, InputsModule, LayoutModule,
|
||||
HasPermissionDirective, SiteSettingsTabComponent, NotificationSettingsTabComponent,
|
||||
],
|
||||
templateUrl: './church-profile-page.component.html',
|
||||
})
|
||||
export class ChurchProfilePageComponent implements OnInit {
|
||||
@@ -17,6 +25,9 @@ export class ChurchProfilePageComponent implements OnInit {
|
||||
saving = false;
|
||||
savedMsg = '';
|
||||
|
||||
/** Settings module gates the Site / Notification tabs. */
|
||||
readonly settingsPermission = { module: PermissionModules.Settings, action: 'read' as const };
|
||||
|
||||
constructor(private api: DisbursementApiService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<div *ngIf="model" class="max-w-3xl pt-4 flex flex-col gap-6">
|
||||
|
||||
<!-- ── Email (SMTP) ─────────────────────────────────────────────────────── -->
|
||||
<section class="flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold">Email (SMTP) / 電子郵件</h3>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
Enabled / 啟用
|
||||
<kendo-switch [(ngModel)]="model.enableEmail"></kendo-switch>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
|
||||
<label class="flex flex-col gap-1">
|
||||
SMTP Host / 主機
|
||||
<kendo-textbox [(ngModel)]="model.smtpHost" placeholder="smtp.example.com"></kendo-textbox>
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="flex flex-col gap-1">
|
||||
Port / 連接埠
|
||||
<kendo-numerictextbox [(ngModel)]="model.smtpPort" [min]="0" [max]="65535" [decimals]="0" format="#"></kendo-numerictextbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Use SSL / 加密
|
||||
<kendo-switch [(ngModel)]="model.smtpUseSsl"></kendo-switch>
|
||||
</label>
|
||||
</div>
|
||||
<label class="flex flex-col gap-1">
|
||||
SMTP User / 帳號
|
||||
<kendo-textbox [(ngModel)]="model.smtpUser"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
SMTP Password / 密碼
|
||||
<kendo-textbox [(ngModel)]="smtpPassword" type="password"
|
||||
[placeholder]="model.hasSmtpPassword ? '•••••• stored (blank = keep) / 已設定' : ''"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
From Address / 寄件地址
|
||||
<kendo-textbox [(ngModel)]="model.fromAddress" placeholder="noreply@church.org"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
From Name / 寄件人名稱
|
||||
<kendo-textbox [(ngModel)]="model.fromName"></kendo-textbox>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<label class="flex flex-col gap-1 grow max-w-xs">
|
||||
<span class="text-sm">Test recipient (blank = you) / 測試收件人</span>
|
||||
<kendo-textbox [(ngModel)]="testEmailTo" placeholder="you@example.com"></kendo-textbox>
|
||||
</label>
|
||||
<button kendoButton [disabled]="testingEmail" (click)="sendTestEmail()">Send test email / 寄送測試</button>
|
||||
<span class="text-sm" [style.color]="testEmailMsg.startsWith('Sent') ? '#065f46' : '#b91c1c'">{{ testEmailMsg }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="border-gray-200" />
|
||||
|
||||
<!-- ── Line ─────────────────────────────────────────────────────────────── -->
|
||||
<section class="flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold">Line / Line 通知</h3>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
Enabled / 啟用
|
||||
<kendo-switch [(ngModel)]="model.enableLine"></kendo-switch>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-y-3">
|
||||
<label class="flex flex-col gap-1">
|
||||
Channel Access Token / 頻道存取權杖
|
||||
<kendo-textbox [(ngModel)]="lineToken" type="password"
|
||||
[placeholder]="model.hasLineChannelAccessToken ? '•••••• stored (blank = keep) / 已設定' : ''"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Channel Secret / 頻道密鑰
|
||||
<kendo-textbox [(ngModel)]="lineSecret" type="password"
|
||||
[placeholder]="model.hasLineChannelSecret ? '•••••• stored (blank = keep) / 已設定' : ''"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Webhook URL (register in Line console) / Webhook 網址
|
||||
<kendo-textbox [ngModel]="model.webhookUrl" [readonly]="true"></kendo-textbox>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm">Test member ID / 會員編號</span>
|
||||
<kendo-numerictextbox [(ngModel)]="testLineMemberId" [decimals]="0" format="#" [min]="1" class="w-40"></kendo-numerictextbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm">or Group ID / 群組編號</span>
|
||||
<kendo-numerictextbox [(ngModel)]="testLineGroupId" [decimals]="0" format="#" [min]="1" class="w-40"></kendo-numerictextbox>
|
||||
</label>
|
||||
<button kendoButton [disabled]="testingLine" (click)="sendTestLine()">Send test Line / 寄送測試</button>
|
||||
<span class="text-sm" [style.color]="testLineMsg.startsWith('Sent') ? '#065f46' : '#b91c1c'">{{ testLineMsg }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button kendoButton themeColor="primary" [disabled]="saving" (click)="save()">Save / 儲存</button>
|
||||
<span class="text-sm" style="color:#065f46;">{{ savedMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||
import { SettingsApiService } from '../../services/settings-api.service';
|
||||
import {
|
||||
NotificationSettingDto, UpdateNotificationSettingRequest, NotificationResult,
|
||||
} from '../../models/settings.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-notification-settings-tab',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ButtonsModule, InputsModule],
|
||||
templateUrl: './notification-settings-tab.component.html',
|
||||
})
|
||||
export class NotificationSettingsTabComponent implements OnInit {
|
||||
model: NotificationSettingDto | null = null;
|
||||
saving = false;
|
||||
savedMsg = '';
|
||||
|
||||
// Write-only secret inputs — blank means "keep the stored value".
|
||||
smtpPassword = '';
|
||||
lineToken = '';
|
||||
lineSecret = '';
|
||||
|
||||
// Test-send state.
|
||||
testEmailTo = '';
|
||||
testEmailMsg = '';
|
||||
testingEmail = false;
|
||||
|
||||
testLineMemberId: number | null = null;
|
||||
testLineGroupId: number | null = null;
|
||||
testLineMsg = '';
|
||||
testingLine = false;
|
||||
|
||||
constructor(private api: SettingsApiService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
this.api.getNotification().subscribe(n => (this.model = n));
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.model || this.saving) return;
|
||||
this.saving = true;
|
||||
this.savedMsg = '';
|
||||
const m = this.model;
|
||||
const request: UpdateNotificationSettingRequest = {
|
||||
enableEmail: m.enableEmail,
|
||||
smtpHost: m.smtpHost,
|
||||
smtpPort: m.smtpPort,
|
||||
smtpUseSsl: m.smtpUseSsl,
|
||||
smtpUser: m.smtpUser,
|
||||
fromAddress: m.fromAddress,
|
||||
fromName: m.fromName,
|
||||
smtpPassword: this.smtpPassword || null,
|
||||
enableLine: m.enableLine,
|
||||
lineChannelAccessToken: this.lineToken || null,
|
||||
lineChannelSecret: this.lineSecret || null,
|
||||
};
|
||||
this.api.updateNotification(request).subscribe({
|
||||
next: () => {
|
||||
this.saving = false;
|
||||
this.savedMsg = 'Saved / 已儲存';
|
||||
// Clear secret inputs and refresh the "configured" flags.
|
||||
this.smtpPassword = this.lineToken = this.lineSecret = '';
|
||||
this.load();
|
||||
},
|
||||
error: () => { this.saving = false; },
|
||||
});
|
||||
}
|
||||
|
||||
sendTestEmail(): void {
|
||||
if (this.testingEmail) return;
|
||||
this.testingEmail = true;
|
||||
this.testEmailMsg = '';
|
||||
this.api.testEmail({ toAddress: this.testEmailTo || null }).subscribe({
|
||||
next: result => { this.testingEmail = false; this.testEmailMsg = this.describe(result); },
|
||||
error: err => { this.testingEmail = false; this.testEmailMsg = this.errorText(err); },
|
||||
});
|
||||
}
|
||||
|
||||
sendTestLine(): void {
|
||||
if (this.testingLine) return;
|
||||
this.testingLine = true;
|
||||
this.testLineMsg = '';
|
||||
this.api.testLine({ memberId: this.testLineMemberId, groupId: this.testLineGroupId }).subscribe({
|
||||
next: result => { this.testingLine = false; this.testLineMsg = this.describe(result); },
|
||||
error: err => { this.testingLine = false; this.testLineMsg = this.errorText(err); },
|
||||
});
|
||||
}
|
||||
|
||||
private describe(result: NotificationResult): string {
|
||||
if (result.failedCount > 0)
|
||||
return `Failed / 失敗:${result.failures.map(f => f.error).join('; ')}`;
|
||||
return result.sentCount > 0 ? 'Sent ✓ / 已發送' : 'Nothing sent / 未發送';
|
||||
}
|
||||
|
||||
private errorText(err: { error?: { message?: string } }): string {
|
||||
return err?.error?.message ?? 'Failed / 失敗';
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<div *ngIf="model" class="max-w-3xl pt-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
|
||||
<label class="flex flex-col gap-1">
|
||||
Site Title (EN) / 網站名稱
|
||||
<kendo-textbox [(ngModel)]="model.siteTitle"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Site Title (ZH) / 網站名稱(中)
|
||||
<kendo-textbox [(ngModel)]="model.siteTitleZh"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Default Language / 預設語言
|
||||
<kendo-dropdownlist
|
||||
[data]="languages" textField="text" valueField="value" [valuePrimitive]="true"
|
||||
[(ngModel)]="model.defaultLanguage"></kendo-dropdownlist>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Time Zone / 時區
|
||||
<kendo-textbox [(ngModel)]="model.timeZone" placeholder="America/Los_Angeles"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Date Format / 日期格式
|
||||
<kendo-textbox [(ngModel)]="model.dateFormat" placeholder="yyyy-MM-dd"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Currency / 貨幣
|
||||
<kendo-textbox [(ngModel)]="model.currency" placeholder="USD"></kendo-textbox>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 mt-4">
|
||||
<button kendoButton themeColor="primary" [disabled]="saving" (click)="save()">Save / 儲存</button>
|
||||
<span class="text-sm" style="color:#065f46;">{{ savedMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
|
||||
import { SettingsApiService } from '../../services/settings-api.service';
|
||||
import { SiteSettingDto } from '../../models/settings.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-site-settings-tab',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ButtonsModule, InputsModule, DropDownsModule],
|
||||
templateUrl: './site-settings-tab.component.html',
|
||||
})
|
||||
export class SiteSettingsTabComponent implements OnInit {
|
||||
model: SiteSettingDto | null = null;
|
||||
saving = false;
|
||||
savedMsg = '';
|
||||
|
||||
readonly languages = [
|
||||
{ text: 'English', value: 'en' },
|
||||
{ text: '中文 Chinese', value: 'zh' },
|
||||
];
|
||||
|
||||
constructor(private api: SettingsApiService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.api.getSite().subscribe(s => (this.model = s));
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.model || this.saving) return;
|
||||
this.saving = true;
|
||||
this.savedMsg = '';
|
||||
this.api.updateSite(this.model).subscribe({
|
||||
next: () => { this.saving = false; this.savedMsg = 'Saved / 已儲存'; },
|
||||
// Errors surface globally via httpErrorInterceptor.
|
||||
error: () => { this.saving = false; },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Mirrors ROLAC.API.DTOs.Settings — site + notification settings edited from the
|
||||
// Church Profile tabbed page (Settings permission module).
|
||||
|
||||
export interface SiteSettingDto {
|
||||
siteTitle: string;
|
||||
siteTitleZh: string | null;
|
||||
defaultLanguage: string; // 'en' | 'zh'
|
||||
timeZone: string;
|
||||
dateFormat: string;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export type UpdateSiteSettingRequest = SiteSettingDto;
|
||||
|
||||
export interface NotificationSettingDto {
|
||||
enableEmail: boolean;
|
||||
smtpHost: string;
|
||||
smtpPort: number;
|
||||
smtpUseSsl: boolean;
|
||||
smtpUser: string;
|
||||
fromAddress: string;
|
||||
fromName: string;
|
||||
/** True when a password is stored — secrets themselves are never returned. */
|
||||
hasSmtpPassword: boolean;
|
||||
|
||||
enableLine: boolean;
|
||||
hasLineChannelAccessToken: boolean;
|
||||
hasLineChannelSecret: boolean;
|
||||
|
||||
/** Read-only webhook URL to register in the Line console. */
|
||||
webhookUrl: string;
|
||||
}
|
||||
|
||||
export interface UpdateNotificationSettingRequest {
|
||||
enableEmail: boolean;
|
||||
smtpHost: string;
|
||||
smtpPort: number;
|
||||
smtpUseSsl: boolean;
|
||||
smtpUser: string;
|
||||
fromAddress: string | null;
|
||||
fromName: string | null;
|
||||
/** Leave blank/omit to keep the stored password. */
|
||||
smtpPassword?: string | null;
|
||||
|
||||
enableLine: boolean;
|
||||
/** Leave blank/omit to keep the stored token. */
|
||||
lineChannelAccessToken?: string | null;
|
||||
/** Leave blank/omit to keep the stored secret. */
|
||||
lineChannelSecret?: string | null;
|
||||
}
|
||||
|
||||
export interface TestEmailRequest {
|
||||
toAddress?: string | null;
|
||||
}
|
||||
|
||||
export interface TestLineRequest {
|
||||
memberId?: number | null;
|
||||
groupId?: number | null;
|
||||
}
|
||||
|
||||
/** Mirrors ROLAC.API NotificationResult. */
|
||||
export interface NotificationResult {
|
||||
sentCount: number;
|
||||
failedCount: number;
|
||||
failures: { target: string; error: string }[];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiConfigService } from '../../../core/services/api-config.service';
|
||||
import {
|
||||
SiteSettingDto, UpdateSiteSettingRequest,
|
||||
NotificationSettingDto, UpdateNotificationSettingRequest,
|
||||
TestEmailRequest, TestLineRequest, NotificationResult,
|
||||
} from '../models/settings.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SettingsApiService {
|
||||
private readonly endpoint: string;
|
||||
|
||||
constructor(private http: HttpClient, apiConfig: ApiConfigService) {
|
||||
this.endpoint = apiConfig.getApiUrl('settings');
|
||||
}
|
||||
|
||||
getSite(): Observable<SiteSettingDto> {
|
||||
return this.http.get<SiteSettingDto>(`${this.endpoint}/site`);
|
||||
}
|
||||
|
||||
updateSite(request: UpdateSiteSettingRequest): Observable<void> {
|
||||
return this.http.put<void>(`${this.endpoint}/site`, request);
|
||||
}
|
||||
|
||||
getNotification(): Observable<NotificationSettingDto> {
|
||||
return this.http.get<NotificationSettingDto>(`${this.endpoint}/notification`);
|
||||
}
|
||||
|
||||
updateNotification(request: UpdateNotificationSettingRequest): Observable<void> {
|
||||
return this.http.put<void>(`${this.endpoint}/notification`, request);
|
||||
}
|
||||
|
||||
testEmail(request: TestEmailRequest): Observable<NotificationResult> {
|
||||
return this.http.post<NotificationResult>(`${this.endpoint}/notification/test-email`, request);
|
||||
}
|
||||
|
||||
testLine(request: TestLineRequest): Observable<NotificationResult> {
|
||||
return this.http.post<NotificationResult>(`${this.endpoint}/notification/test-line`, request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user