zmiany cd
This commit is contained in:
@@ -5,7 +5,7 @@ import { AdminService } from '../../core/services/admin.service';
|
||||
import { AppSettingsService } from '../../core/services/app-settings.service';
|
||||
import { ToastService } from '../../core/services/toast.service';
|
||||
import { UiService } from '../../core/services/ui.service';
|
||||
import type { AppSettings, User } from '../../shared/models';
|
||||
import type { AdminSystemInfo, AppSettings, User } from '../../shared/models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
@@ -21,6 +21,62 @@ import type { AppSettings, User } from '../../shared/models';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (systemInfo()) {
|
||||
<div class="row row-cards mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card pv-card overflow-hidden ec-accent-card ec-accent-card-info">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<h3 class="card-title mb-1">{{ ui.t('admin.techTitle') }}</h3>
|
||||
<div class="text-secondary small">{{ ui.t('admin.techSubtitle') }}</div>
|
||||
</div>
|
||||
<span class="badge bg-info">{{ systemInfo()!.environment }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-6 col-xl-3"><div class="ec-stat-tile"><div class="ec-stat-label">{{ ui.t('admin.appVersion') }}</div><div class="ec-stat-value">{{ systemInfo()!.suiteVersion }}</div></div></div>
|
||||
<div class="col-sm-6 col-xl-3"><div class="ec-stat-tile"><div class="ec-stat-label">API</div><div class="ec-stat-value">{{ systemInfo()!.apiVersion }}</div></div></div>
|
||||
<div class="col-sm-6 col-xl-3"><div class="ec-stat-tile"><div class="ec-stat-label">Web</div><div class="ec-stat-value">{{ systemInfo()!.webVersion }}</div></div></div>
|
||||
<div class="col-sm-6 col-xl-3"><div class="ec-stat-tile"><div class="ec-stat-label">Node.js</div><div class="ec-stat-value">{{ systemInfo()!.nodeVersion }}</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-7">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-vcenter mb-0">
|
||||
<tbody>
|
||||
<tr><td class="text-secondary">{{ ui.t('admin.database') }}</td><td class="fw-semibold">{{ systemInfo()!.database }}</td></tr>
|
||||
<tr><td class="text-secondary">Upload dir</td><td class="fw-semibold text-break">{{ systemInfo()!.uploadDir }}</td></tr>
|
||||
<tr><td class="text-secondary">{{ ui.t('admin.registration') }}</td><td><span class="badge" [class.bg-success]="systemInfo()!.registrationEnabled" [class.bg-secondary]="!systemInfo()!.registrationEnabled">{{ systemInfo()!.registrationEnabled ? ui.t('common.active') : ui.t('common.blocked') }}</span></td></tr>
|
||||
<tr><td class="text-secondary">SMTP</td><td><span class="badge" [class.bg-success]="systemInfo()!.smtpConfigured" [class.bg-warning]="!systemInfo()!.smtpConfigured">{{ systemInfo()!.smtpConfigured ? ui.t('admin.smtpReady') : ui.t('admin.smtpNotReady') }}</span></td></tr>
|
||||
<tr><td class="text-secondary">API base</td><td class="fw-semibold">{{ systemInfo()!.sources.apiBasePath }}</td></tr>
|
||||
<tr><td class="text-secondary">{{ ui.t('table.date') }}</td><td class="fw-semibold">{{ systemInfo()!.checkedAt | date:'yyyy-MM-dd HH:mm:ss' }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="row g-2">
|
||||
<div class="col-6"><div class="ec-mini-kpi"><span>{{ ui.t('admin.kpi.users') }}</span><strong>{{ systemInfo()!.counters.users }}</strong></div></div>
|
||||
<div class="col-6"><div class="ec-mini-kpi"><span>{{ ui.t('admin.kpi.expenses') }}</span><strong>{{ systemInfo()!.counters.expenses }}</strong></div></div>
|
||||
<div class="col-6"><div class="ec-mini-kpi"><span>{{ ui.t('admin.kpi.categories') }}</span><strong>{{ systemInfo()!.counters.categories }}</strong></div></div>
|
||||
<div class="col-6"><div class="ec-mini-kpi"><span>{{ ui.t('admin.kpi.merchants') }}</span><strong>{{ systemInfo()!.counters.merchants }}</strong></div></div>
|
||||
<div class="col-6"><div class="ec-mini-kpi"><span>{{ ui.t('admin.kpi.budgets') }}</span><strong>{{ systemInfo()!.counters.budgets }}</strong></div></div>
|
||||
<div class="col-6"><div class="ec-mini-kpi"><span>{{ ui.t('admin.kpi.recurring') }}</span><strong>{{ systemInfo()!.counters.recurring }}</strong></div></div>
|
||||
<div class="col-12"><div class="ec-mini-kpi"><span>{{ ui.t('admin.kpi.integrations') }}</span><strong>{{ systemInfo()!.counters.shoppingIntegrations }}</strong></div></div>
|
||||
</div>
|
||||
<div class="mt-3 d-flex gap-2 flex-wrap">
|
||||
<a class="btn btn-outline-primary btn-sm" href="https://git.linuxiarz.pl/gru/expense-control" target="_blank" rel="noreferrer">{{ ui.t('footer.source') }}</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="https://git.linuxiarz.pl/gru/lista_zakupowa_live" target="_blank" rel="noreferrer">{{ ui.t('footer.shoppingSource') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row row-cards align-items-start">
|
||||
<div class="col-xl-5">
|
||||
<div class="card pv-card overflow-hidden">
|
||||
@@ -130,6 +186,7 @@ export class AdminComponent implements OnInit {
|
||||
|
||||
readonly users = signal<User[]>([]);
|
||||
readonly settings = signal<AppSettings | null>(null);
|
||||
readonly systemInfo = signal<AdminSystemInfo | null>(null);
|
||||
readonly saving = signal(false);
|
||||
|
||||
readonly form = this.fb.nonNullable.group({
|
||||
@@ -147,9 +204,7 @@ export class AdminComponent implements OnInit {
|
||||
smtpFromEmail: ['']
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.load();
|
||||
}
|
||||
ngOnInit() { this.load(); }
|
||||
|
||||
load() {
|
||||
this.admin.getSettings().subscribe({
|
||||
@@ -174,40 +229,40 @@ export class AdminComponent implements OnInit {
|
||||
});
|
||||
|
||||
this.admin.listUsers().subscribe({ next: (response) => this.users.set(response.items) });
|
||||
this.admin.getSystemInfo().subscribe({ next: (response) => this.systemInfo.set(response.item) });
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.form.invalid) return;
|
||||
this.saving.set(true);
|
||||
const raw = this.form.getRawValue();
|
||||
this.admin
|
||||
.updateSettings({
|
||||
appName: raw.appName,
|
||||
defaultCurrency: raw.defaultCurrency,
|
||||
registrationEnabled: raw.registrationEnabled,
|
||||
allowedProofTypes: raw.allowedProofTypes.split(',').map((item) => item.trim()).filter(Boolean),
|
||||
uiPreferences: { theme: 'dark', density: 'comfortable', defaultStatsPeriod: 'month' },
|
||||
smtpEnabled: raw.smtpEnabled,
|
||||
smtpHost: raw.smtpHost || null,
|
||||
smtpPort: Number(raw.smtpPort),
|
||||
smtpSecure: raw.smtpSecure,
|
||||
smtpUser: raw.smtpUser || null,
|
||||
smtpPassword: raw.smtpPassword || null,
|
||||
smtpFromName: raw.smtpFromName || null,
|
||||
smtpFromEmail: raw.smtpFromEmail || null
|
||||
})
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.saving.set(false);
|
||||
this.settings.set(response.item);
|
||||
this.appSettings.applySettings(response.item);
|
||||
this.toast.success(this.ui.t('admin.settingsSaved'));
|
||||
},
|
||||
error: (error) => {
|
||||
this.saving.set(false);
|
||||
this.toast.error(error.error?.message ?? this.ui.t('admin.settingsError'));
|
||||
}
|
||||
});
|
||||
this.admin.updateSettings({
|
||||
appName: raw.appName,
|
||||
defaultCurrency: raw.defaultCurrency,
|
||||
registrationEnabled: raw.registrationEnabled,
|
||||
allowedProofTypes: raw.allowedProofTypes.split(',').map((item) => item.trim()).filter(Boolean),
|
||||
uiPreferences: { theme: 'dark', density: 'comfortable', defaultStatsPeriod: 'month' },
|
||||
smtpEnabled: raw.smtpEnabled,
|
||||
smtpHost: raw.smtpHost || null,
|
||||
smtpPort: Number(raw.smtpPort),
|
||||
smtpSecure: raw.smtpSecure,
|
||||
smtpUser: raw.smtpUser || null,
|
||||
smtpPassword: raw.smtpPassword || null,
|
||||
smtpFromName: raw.smtpFromName || null,
|
||||
smtpFromEmail: raw.smtpFromEmail || null
|
||||
}).subscribe({
|
||||
next: (response) => {
|
||||
this.saving.set(false);
|
||||
this.settings.set(response.item);
|
||||
this.appSettings.applySettings(response.item);
|
||||
this.toast.success(this.ui.t('admin.settingsSaved'));
|
||||
this.admin.getSystemInfo().subscribe({ next: (systemResponse) => this.systemInfo.set(systemResponse.item) });
|
||||
},
|
||||
error: (error) => {
|
||||
this.saving.set(false);
|
||||
this.toast.error(error.error?.message ?? this.ui.t('admin.settingsError'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sendTest() {
|
||||
|
||||
@@ -35,9 +35,16 @@ const monthRange = (period: string) => {
|
||||
|
||||
<div class="row row-cards mb-3">
|
||||
<div class="col-lg-5">
|
||||
<div class="card overflow-hidden h-100">
|
||||
<div class="card-header"><h3 class="card-title">{{ ui.t('integrations.shoppingList') }}</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="card overflow-hidden ec-accent-card ec-accent-card-primary h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<h3 class="card-title mb-0">{{ ui.t('integrations.shoppingList') }}</h3>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="https://git.linuxiarz.pl/gru/lista_zakupowa_live" target="_blank" rel="noreferrer">{{ ui.t('integrations.projectLink') }}</a>
|
||||
</div>
|
||||
<div class="card-body d-grid gap-3">
|
||||
<div class="alert alert-info mb-0">
|
||||
<div class="fw-semibold mb-1">{{ ui.t('integrations.selfHostedTitle') }}</div>
|
||||
<div>{{ ui.t('integrations.selfHostedHint') }}</div>
|
||||
</div>
|
||||
<form [formGroup]="form" (ngSubmit)="save()" class="d-grid gap-3">
|
||||
<label class="form-check">
|
||||
<input class="form-check-input" type="checkbox" formControlName="enabled" />
|
||||
@@ -100,22 +107,8 @@ const monthRange = (period: string) => {
|
||||
</form>
|
||||
|
||||
<div class="row row-cards">
|
||||
<div class="col-md-6">
|
||||
<div class="card overflow-hidden">
|
||||
<div class="card-body">
|
||||
<div class="text-secondary">{{ ui.t('integrations.externalSpend') }}</div>
|
||||
<div class="display-6">{{ summaryAmount() | currency:'PLN':'symbol':'1.2-2' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card overflow-hidden">
|
||||
<div class="card-body">
|
||||
<div class="text-secondary">{{ ui.t('integrations.externalCount') }}</div>
|
||||
<div class="display-6">{{ summaryCount() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6"><div class="ec-stat-tile ec-stat-tile-primary"><div class="ec-stat-label">{{ ui.t('integrations.externalSpend') }}</div><div class="ec-stat-value">{{ summaryAmount() | currency:'PLN':'symbol':'1.2-2' }}</div></div></div>
|
||||
<div class="col-md-6"><div class="ec-stat-tile ec-stat-tile-success"><div class="ec-stat-label">{{ ui.t('integrations.externalCount') }}</div><div class="ec-stat-value">{{ summaryCount() }}</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="border rounded-3 p-3 bg-body-tertiary">
|
||||
@@ -154,9 +147,14 @@ const monthRange = (period: string) => {
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="card overflow-hidden h-100">
|
||||
<div class="card overflow-hidden ec-accent-card ec-accent-card-success h-100">
|
||||
<div class="card-header"><h3 class="card-title">{{ ui.t('integrations.importTitle') }}</h3></div>
|
||||
<div class="card-body d-grid gap-3">
|
||||
<div class="alert alert-warning mb-0">
|
||||
<div class="fw-semibold mb-1">{{ ui.t('integrations.importExplainTitle') }}</div>
|
||||
<div>{{ ui.t('integrations.importExplainBody') }}</div>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="importForm" class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">{{ ui.t('expenses.field.category') }}</label>
|
||||
@@ -186,7 +184,7 @@ const monthRange = (period: string) => {
|
||||
</form>
|
||||
|
||||
@if (selectedList()) {
|
||||
<div class="border rounded-3 p-3">
|
||||
<div class="border rounded-3 p-3 bg-body-tertiary">
|
||||
<div class="d-flex justify-content-between gap-3 flex-wrap align-items-start">
|
||||
<div>
|
||||
<div class="fw-semibold">{{ listTitle(selectedList()!) }}</div>
|
||||
@@ -213,12 +211,7 @@ const monthRange = (period: string) => {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ ui.t('table.title') }}</th>
|
||||
<th>{{ ui.t('table.date') }}</th>
|
||||
<th class="text-end">{{ ui.t('table.amount') }}</th>
|
||||
<th class="text-end">{{ ui.t('table.actions') }}</th>
|
||||
</tr>
|
||||
<tr><th>{{ ui.t('table.title') }}</th><th>{{ ui.t('table.date') }}</th><th class="text-end">{{ ui.t('table.amount') }}</th><th class="text-end">{{ ui.t('table.actions') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (item of latestExpenses(); track $index) {
|
||||
@@ -246,12 +239,7 @@ const monthRange = (period: string) => {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ ui.t('table.title') }}</th>
|
||||
<th>{{ ui.t('table.date') }}</th>
|
||||
<th class="text-end">{{ ui.t('table.amount') }}</th>
|
||||
<th class="text-end">{{ ui.t('table.actions') }}</th>
|
||||
</tr>
|
||||
<tr><th>{{ ui.t('table.title') }}</th><th>{{ ui.t('table.date') }}</th><th class="text-end">{{ ui.t('table.amount') }}</th><th class="text-end">{{ ui.t('table.actions') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (item of selectedListExpenses(); track $index) {
|
||||
@@ -329,14 +317,7 @@ export class IntegrationsComponent implements OnInit {
|
||||
this.integration.getSettings().subscribe({
|
||||
next: (response: { item: { enabled: boolean; baseUrl: string; hasToken: boolean; authMode: 'bearer' | 'x-api-token' | 'both'; ownerId: string | null; defaultListId: string | null } }) => {
|
||||
const item = response.item;
|
||||
this.form.reset({
|
||||
enabled: item.enabled,
|
||||
baseUrl: item.baseUrl || '',
|
||||
apiToken: '',
|
||||
authMode: item.authMode,
|
||||
ownerId: item.ownerId || '',
|
||||
defaultListId: item.defaultListId || ''
|
||||
});
|
||||
this.form.reset({ enabled: item.enabled, baseUrl: item.baseUrl || '', apiToken: '', authMode: item.authMode, ownerId: item.ownerId || '', defaultListId: item.defaultListId || '' });
|
||||
this.configured.set(Boolean(item.enabled && item.baseUrl && item.hasToken));
|
||||
if (this.configured()) this.refresh();
|
||||
},
|
||||
@@ -347,23 +328,14 @@ export class IntegrationsComponent implements OnInit {
|
||||
save() {
|
||||
if (this.form.invalid) return;
|
||||
const raw = this.form.getRawValue();
|
||||
this.integration
|
||||
.updateSettings({
|
||||
enabled: raw.enabled,
|
||||
baseUrl: raw.baseUrl || null,
|
||||
apiToken: raw.apiToken || undefined,
|
||||
authMode: raw.authMode,
|
||||
ownerId: raw.ownerId || null,
|
||||
defaultListId: raw.defaultListId || null
|
||||
})
|
||||
.subscribe({
|
||||
next: (response: { item: { enabled: boolean; baseUrl: string; hasToken: boolean } }) => {
|
||||
this.configured.set(Boolean(response.item.enabled && response.item.baseUrl && response.item.hasToken));
|
||||
this.toast.success(this.ui.t('integrations.saveSuccess'));
|
||||
if (this.configured()) this.refresh();
|
||||
},
|
||||
error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.saveError'))
|
||||
});
|
||||
this.integration.updateSettings({ enabled: raw.enabled, baseUrl: raw.baseUrl || null, apiToken: raw.apiToken || undefined, authMode: raw.authMode, ownerId: raw.ownerId || null, defaultListId: raw.defaultListId || null }).subscribe({
|
||||
next: (response: { item: { enabled: boolean; baseUrl: string; hasToken: boolean } }) => {
|
||||
this.configured.set(Boolean(response.item.enabled && response.item.baseUrl && response.item.hasToken));
|
||||
this.toast.success(this.ui.t('integrations.saveSuccess'));
|
||||
if (this.configured()) this.refresh();
|
||||
},
|
||||
error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.saveError'))
|
||||
});
|
||||
}
|
||||
|
||||
test() {
|
||||
@@ -378,41 +350,22 @@ export class IntegrationsComponent implements OnInit {
|
||||
const raw = this.form.getRawValue();
|
||||
const history = this.historyForm.getRawValue();
|
||||
const range = monthRange(history.period);
|
||||
const filters = {
|
||||
start_date: range.start,
|
||||
end_date: range.end,
|
||||
owner_id: raw.ownerId || undefined,
|
||||
list_id: raw.defaultListId || undefined
|
||||
};
|
||||
|
||||
this.integration.summary(filters).subscribe({
|
||||
next: (response: ShoppingListSummary) => this.summary.set(response),
|
||||
error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.loadError'))
|
||||
});
|
||||
|
||||
this.integration.latest({ ...filters, limit: history.limit }).subscribe({
|
||||
next: (response: { items?: ShoppingListExpenseItem[]; data?: ShoppingListExpenseItem[] }) => this.latestExpenses.set(this.pickItems<ShoppingListExpenseItem>(response)),
|
||||
error: () => this.latestExpenses.set([])
|
||||
});
|
||||
const filters = { start_date: range.start, end_date: range.end, owner_id: raw.ownerId || undefined, list_id: raw.defaultListId || undefined };
|
||||
|
||||
this.integration.summary(filters).subscribe({ next: (response: ShoppingListSummary) => this.summary.set(response), error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.loadError')) });
|
||||
this.integration.latest({ ...filters, limit: history.limit }).subscribe({ next: (response) => this.latestExpenses.set(this.pickItems<ShoppingListExpenseItem>(response)), error: () => this.latestExpenses.set([]) });
|
||||
this.integration.lists({ owner_id: raw.ownerId || undefined, limit: 200 }).subscribe({
|
||||
next: (response: { items?: ShoppingListRef[]; data?: ShoppingListRef[] }) => {
|
||||
next: (response) => {
|
||||
const items = this.pickItems<ShoppingListRef>(response);
|
||||
this.allLists.set(items);
|
||||
const visible = this.visibleLists(items);
|
||||
const currentId = String(this.selectedList()?.id ?? '');
|
||||
const nextSelected = visible.find((item) => String(item.id) === currentId) ?? visible[0] ?? null;
|
||||
this.selectedList.set(nextSelected);
|
||||
if (nextSelected) {
|
||||
this.loadListExpenses(nextSelected);
|
||||
} else {
|
||||
this.selectedListExpenses.set([]);
|
||||
}
|
||||
if (nextSelected) this.loadListExpenses(nextSelected); else this.selectedListExpenses.set([]);
|
||||
},
|
||||
error: () => {
|
||||
this.allLists.set([]);
|
||||
this.selectedList.set(null);
|
||||
this.selectedListExpenses.set([]);
|
||||
this.allLists.set([]); this.selectedList.set(null); this.selectedListExpenses.set([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -426,116 +379,45 @@ export class IntegrationsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
selectList(item: ShoppingListRef) {
|
||||
this.selectedList.set(item);
|
||||
this.loadListExpenses(item);
|
||||
}
|
||||
selectList(item: ShoppingListRef) { this.selectedList.set(item); this.loadListExpenses(item); }
|
||||
|
||||
importSelectedList() {
|
||||
const list = this.selectedList();
|
||||
if (!list || this.importForm.invalid) return;
|
||||
const raw = this.importForm.getRawValue();
|
||||
this.integration
|
||||
.importList({
|
||||
listId: list.id,
|
||||
listTitle: this.listTitle(list),
|
||||
listCreatedAt: this.listCreatedAt(list),
|
||||
categoryId: raw.categoryId,
|
||||
status: raw.status,
|
||||
merchant: raw.merchant || this.listTitle(list),
|
||||
tags: this.normalizedTags()
|
||||
})
|
||||
.subscribe({
|
||||
next: (response: { item: unknown; warnings?: string[] }) => {
|
||||
this.toast.success(this.ui.t('integrations.importListSuccess'));
|
||||
this.emitWarnings(response.warnings);
|
||||
},
|
||||
error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.importError'))
|
||||
});
|
||||
this.integration.importList({ listId: list.id, listTitle: this.listTitle(list), listCreatedAt: this.listCreatedAt(list), categoryId: raw.categoryId, status: raw.status, merchant: raw.merchant || this.listTitle(list), tags: this.normalizedTags() }).subscribe({
|
||||
next: (response) => { this.toast.success(this.ui.t('integrations.importListSuccess')); this.emitWarnings(response.warnings); },
|
||||
error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.importError'))
|
||||
});
|
||||
}
|
||||
|
||||
importItem(item: ShoppingListExpenseItem) {
|
||||
if (this.importForm.invalid) return;
|
||||
const raw = this.importForm.getRawValue();
|
||||
this.integration
|
||||
.importItem({
|
||||
expenseId: item.expense_id ?? item.id ?? null,
|
||||
listId: item.list?.id ?? this.selectedList()?.id ?? null,
|
||||
listTitle: this.listTitle(item.list ?? this.selectedList()),
|
||||
categoryId: raw.categoryId,
|
||||
status: raw.status,
|
||||
title: this.itemTitle(item),
|
||||
amount: this.itemAmount(item),
|
||||
expenseDate: this.itemDate(item),
|
||||
merchant: raw.merchant || this.listTitle(item.list ?? this.selectedList()),
|
||||
ownerName: this.ownerName(item),
|
||||
tags: this.normalizedTags()
|
||||
})
|
||||
.subscribe({
|
||||
next: (response: { item: unknown; warnings?: string[] }) => {
|
||||
this.toast.success(this.ui.t('integrations.importItemSuccess'));
|
||||
this.emitWarnings(response.warnings);
|
||||
},
|
||||
error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.importError'))
|
||||
});
|
||||
}
|
||||
|
||||
isSelectedList(item: ShoppingListRef) {
|
||||
return String(this.selectedList()?.id ?? '') === String(item.id);
|
||||
}
|
||||
|
||||
listTitle(item?: ShoppingListRef | null) {
|
||||
return item?.title || item?.name || (item?.id !== undefined ? String(item.id) : '-');
|
||||
}
|
||||
|
||||
listOwner(item?: ShoppingListRef | null) {
|
||||
return item?.owner?.username || item?.owner?.fullName || item?.owner?.name || item?.owner?.email || null;
|
||||
}
|
||||
|
||||
listCreatedAt(item?: ShoppingListRef | null) {
|
||||
return item?.created_at || null;
|
||||
}
|
||||
|
||||
itemTitle(item: ShoppingListExpenseItem) {
|
||||
return item.title || item.name || item.list?.title || item.list?.name || `Expense #${item.expense_id ?? item.id ?? '-'}`;
|
||||
}
|
||||
|
||||
itemDate(item: ShoppingListExpenseItem) {
|
||||
return (item.expense_date || item.added_at || item.created_at || today()).slice(0, 10);
|
||||
}
|
||||
|
||||
itemAmount(item: ShoppingListExpenseItem) {
|
||||
return Number(item.amount ?? item.total ?? 0);
|
||||
}
|
||||
|
||||
ownerName(item: ShoppingListExpenseItem) {
|
||||
return item.owner?.fullName || item.owner?.name || item.owner?.username || item.owner?.email || null;
|
||||
}
|
||||
|
||||
private loadListExpenses(item: ShoppingListRef) {
|
||||
const limit = this.historyForm.controls.limit.value;
|
||||
this.integration.listExpenses(item.id, limit).subscribe({
|
||||
next: (response: { items?: ShoppingListExpenseItem[]; data?: ShoppingListExpenseItem[] }) => this.selectedListExpenses.set(this.pickItems<ShoppingListExpenseItem>(response)),
|
||||
error: () => this.selectedListExpenses.set([])
|
||||
this.integration.importItem({ expenseId: item.expense_id ?? item.id ?? null, listId: item.list?.id ?? this.selectedList()?.id ?? null, listTitle: this.listTitle(item.list ?? this.selectedList()), categoryId: raw.categoryId, status: raw.status, title: this.itemTitle(item), amount: this.itemAmount(item), expenseDate: this.itemDate(item), merchant: raw.merchant || this.listTitle(item.list ?? this.selectedList()), ownerName: this.ownerName(item), tags: this.normalizedTags() }).subscribe({
|
||||
next: (response) => { this.toast.success(this.ui.t('integrations.importItemSuccess')); this.emitWarnings(response.warnings); },
|
||||
error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.importError'))
|
||||
});
|
||||
}
|
||||
|
||||
isSelectedList(item: ShoppingListRef) { return String(this.selectedList()?.id ?? '') === String(item.id); }
|
||||
listTitle(item?: ShoppingListRef | null) { return item?.title || item?.name || (item?.id !== undefined ? String(item.id) : '-'); }
|
||||
listOwner(item?: ShoppingListRef | null) { return item?.owner?.username || item?.owner?.fullName || item?.owner?.name || item?.owner?.email || null; }
|
||||
listCreatedAt(item?: ShoppingListRef | null) { return item?.created_at || null; }
|
||||
itemTitle(item: ShoppingListExpenseItem) { return item.title || item.name || item.list?.title || item.list?.name || `Expense #${item.expense_id ?? item.id ?? '-'}`; }
|
||||
itemDate(item: ShoppingListExpenseItem) { return (item.expense_date || item.added_at || item.created_at || today()).slice(0, 10); }
|
||||
itemAmount(item: ShoppingListExpenseItem) { return Number(item.amount ?? item.total ?? 0); }
|
||||
ownerName(item: ShoppingListExpenseItem) { return item.owner?.fullName || item.owner?.name || item.owner?.username || item.owner?.email || null; }
|
||||
|
||||
private loadListExpenses(item: ShoppingListRef) {
|
||||
const limit = this.historyForm.controls.limit.value;
|
||||
this.integration.listExpenses(item.id, limit).subscribe({ next: (response) => this.selectedListExpenses.set(this.pickItems<ShoppingListExpenseItem>(response)), error: () => this.selectedListExpenses.set([]) });
|
||||
}
|
||||
|
||||
private normalizedTags() {
|
||||
return Array.from(
|
||||
new Set(
|
||||
this.importForm.controls.tags.value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
return Array.from(new Set(this.importForm.controls.tags.value.split(',').map((item) => item.trim()).filter(Boolean)));
|
||||
}
|
||||
|
||||
private emitWarnings(warnings?: string[]) {
|
||||
(warnings ?? []).forEach((message) => this.toast.warning(message));
|
||||
}
|
||||
|
||||
private pickItems<T extends { id?: string | number }>(response: { items?: T[]; data?: T[] }) {
|
||||
return response.items ?? response.data ?? [];
|
||||
}
|
||||
private emitWarnings(warnings?: string[]) { (warnings ?? []).forEach((message) => this.toast.warning(message)); }
|
||||
private pickItems<T extends { id?: string | number }>(response: { items?: T[]; data?: T[] }) { return response.items ?? response.data ?? []; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user