feat(church-profile): AI 設定 tab (provider/model/key) with masked keys
This commit is contained in:
@@ -49,6 +49,11 @@ export interface ChurchProfileDto {
|
|||||||
email: string | null; website: string | null; address: string | null; city: string | null;
|
email: string | null; website: string | null; address: string | null; city: string | null;
|
||||||
state: string | null; zipCode: string | null; bankName: string | null;
|
state: string | null; zipCode: string | null; bankName: string | null;
|
||||||
bankAccountNumber: string | null; bankRoutingNumber: string | null; nextCheckNumber: number;
|
bankAccountNumber: string | null; bankRoutingNumber: string | null; nextCheckNumber: number;
|
||||||
|
aiProvider: string;
|
||||||
|
claudeModel: string | null; claudeApiKeyMasked: string | null;
|
||||||
|
geminiModel: string | null; geminiApiKeyMasked: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateChurchProfileRequest = Omit<ChurchProfileDto, 'id'>;
|
export type UpdateChurchProfileRequest =
|
||||||
|
Omit<ChurchProfileDto, 'id' | 'claudeApiKeyMasked' | 'geminiApiKeyMasked'>
|
||||||
|
& { claudeApiKey: string | null; geminiApiKey: string | null };
|
||||||
|
|||||||
+47
@@ -82,5 +82,52 @@
|
|||||||
<app-notification-settings-tab></app-notification-settings-tab>
|
<app-notification-settings-tab></app-notification-settings-tab>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</kendo-tabstrip-tab>
|
</kendo-tabstrip-tab>
|
||||||
|
|
||||||
|
<!-- ── Tab 4: AI Settings (Settings permission) ───────────────────────── -->
|
||||||
|
<kendo-tabstrip-tab title="AI 設定 / AI Settings" *appHasPermission="settingsPermission">
|
||||||
|
<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 md:col-span-2">
|
||||||
|
AI Provider / AI 供應商
|
||||||
|
<kendo-dropdownlist
|
||||||
|
[data]="aiProviders" textField="text" valueField="value" [valuePrimitive]="true"
|
||||||
|
[(ngModel)]="model.aiProvider"></kendo-dropdownlist>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
Claude Model / Claude 模型
|
||||||
|
<kendo-textbox [(ngModel)]="model.claudeModel"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
Claude API Key / Claude 金鑰
|
||||||
|
<input kendoTextBox type="password" autocomplete="new-password"
|
||||||
|
[(ngModel)]="claudeApiKeyInput"
|
||||||
|
[placeholder]="model.claudeApiKeyMasked || 'Enter key / 輸入金鑰'" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
Gemini Model / Gemini 模型
|
||||||
|
<kendo-textbox [(ngModel)]="model.geminiModel"></kendo-textbox>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
Gemini API Key / Gemini 金鑰
|
||||||
|
<input kendoTextBox type="password" autocomplete="new-password"
|
||||||
|
[(ngModel)]="geminiApiKeyInput"
|
||||||
|
[placeholder]="model.geminiApiKeyMasked || 'Enter key / 輸入金鑰'" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p class="md:col-span-2 text-sm" style="color:#6b7280;">
|
||||||
|
Leave a key blank to keep the saved one. / 金鑰留空表示沿用已儲存的設定。
|
||||||
|
</p>
|
||||||
|
</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>
|
||||||
</kendo-tabstrip>
|
</kendo-tabstrip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+26
-4
@@ -4,8 +4,9 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||||
import { LayoutModule } from '@progress/kendo-angular-layout';
|
import { LayoutModule } from '@progress/kendo-angular-layout';
|
||||||
|
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
|
||||||
import { DisbursementApiService } from '../../services/disbursement-api.service';
|
import { DisbursementApiService } from '../../services/disbursement-api.service';
|
||||||
import { ChurchProfileDto } from '../../models/disbursement.model';
|
import { ChurchProfileDto, UpdateChurchProfileRequest } from '../../models/disbursement.model';
|
||||||
import { HasPermissionDirective } from '../../../../core/directives/has-permission.directive';
|
import { HasPermissionDirective } from '../../../../core/directives/has-permission.directive';
|
||||||
import { PermissionModules } from '../../../../core/models/permission.model';
|
import { PermissionModules } from '../../../../core/models/permission.model';
|
||||||
import { SiteSettingsTabComponent } from '../../../settings/components/site-settings-tab/site-settings-tab.component';
|
import { SiteSettingsTabComponent } from '../../../settings/components/site-settings-tab/site-settings-tab.component';
|
||||||
@@ -15,7 +16,7 @@ import { NotificationSettingsTabComponent } from '../../../settings/components/n
|
|||||||
selector: 'app-church-profile-page',
|
selector: 'app-church-profile-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule, FormsModule, ButtonsModule, InputsModule, LayoutModule,
|
CommonModule, FormsModule, ButtonsModule, InputsModule, LayoutModule, DropDownsModule,
|
||||||
HasPermissionDirective, SiteSettingsTabComponent, NotificationSettingsTabComponent,
|
HasPermissionDirective, SiteSettingsTabComponent, NotificationSettingsTabComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './church-profile-page.component.html',
|
templateUrl: './church-profile-page.component.html',
|
||||||
@@ -25,6 +26,15 @@ export class ChurchProfilePageComponent implements OnInit {
|
|||||||
saving = false;
|
saving = false;
|
||||||
savedMsg = '';
|
savedMsg = '';
|
||||||
|
|
||||||
|
/** Bound to the password inputs; blank means "keep the saved key". Reset after each save. */
|
||||||
|
claudeApiKeyInput = '';
|
||||||
|
geminiApiKeyInput = '';
|
||||||
|
|
||||||
|
readonly aiProviders = [
|
||||||
|
{ text: 'Claude', value: 'Claude' },
|
||||||
|
{ text: 'Gemini', value: 'Gemini' },
|
||||||
|
];
|
||||||
|
|
||||||
/** Settings module gates the Site / Notification tabs. */
|
/** Settings module gates the Site / Notification tabs. */
|
||||||
readonly settingsPermission = { module: PermissionModules.Settings, action: 'read' as const };
|
readonly settingsPermission = { module: PermissionModules.Settings, action: 'read' as const };
|
||||||
|
|
||||||
@@ -38,9 +48,21 @@ export class ChurchProfilePageComponent implements OnInit {
|
|||||||
if (!this.model || this.saving) return;
|
if (!this.model || this.saving) return;
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
this.savedMsg = '';
|
this.savedMsg = '';
|
||||||
const { id, ...req } = this.model;
|
const { id, claudeApiKeyMasked, geminiApiKeyMasked, ...rest } = this.model;
|
||||||
|
const req: UpdateChurchProfileRequest = {
|
||||||
|
...rest,
|
||||||
|
claudeApiKey: this.claudeApiKeyInput.trim() || null,
|
||||||
|
geminiApiKey: this.geminiApiKeyInput.trim() || null,
|
||||||
|
};
|
||||||
this.api.updateChurchProfile(req).subscribe({
|
this.api.updateChurchProfile(req).subscribe({
|
||||||
next: () => { this.saving = false; this.savedMsg = 'Saved / 已儲存'; },
|
next: () => {
|
||||||
|
this.saving = false;
|
||||||
|
this.savedMsg = 'Saved / 已儲存';
|
||||||
|
// Clear the key inputs and reload so the masked placeholders reflect the new keys.
|
||||||
|
this.claudeApiKeyInput = '';
|
||||||
|
this.geminiApiKeyInput = '';
|
||||||
|
this.api.getChurchProfile().subscribe(p => (this.model = p));
|
||||||
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
// Error message is shown globally by httpErrorInterceptor.
|
// Error message is shown globally by httpErrorInterceptor.
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user