import { CommonModule, CurrencyPipe, DatePipe } from '@angular/common'; import { Component, OnInit, computed, inject, signal } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { CategoriesService } from '../../core/services/categories.service'; import { ShoppingListIntegrationService } from '../../core/services/shopping-list-integration.service'; import { ToastService } from '../../core/services/toast.service'; import { UiService } from '../../core/services/ui.service'; import type { Category, ShoppingListExpenseItem, ShoppingListRef, ShoppingListSummary } from '../../shared/models'; const currentMonth = () => new Date().toISOString().slice(0, 7); const today = () => new Date().toISOString().slice(0, 10); const monthRange = (period: string) => { const safe = /^\d{4}-\d{2}$/.test(period) ? period : currentMonth(); const [yearText, monthText] = safe.split('-'); const year = Number(yearText); const month = Number(monthText); const lastDay = new Date(year, month, 0).getDate(); return { start: `${safe}-01`, end: `${safe}-${String(lastDay).padStart(2, '0')}` }; }; @Component({ selector: 'app-integrations', standalone: true, imports: [CommonModule, ReactiveFormsModule, CurrencyPipe, DatePipe], templateUrl: './integrations.component.html' }) export class IntegrationsComponent implements OnInit { readonly ui = inject(UiService); private readonly fb = inject(FormBuilder); private readonly integration = inject(ShoppingListIntegrationService); private readonly categoriesService = inject(CategoriesService); private readonly toast = inject(ToastService); readonly categories = this.categoriesService.items; readonly configured = signal(false); readonly summary = signal(null); readonly allLists = signal([]); readonly selectedList = signal(null); readonly selectedListExpenses = signal([]); readonly form = this.fb.nonNullable.group({ enabled: [false], baseUrl: ['', Validators.required], apiToken: [''], authMode: ['both' as 'bearer' | 'x-api-token' | 'both', Validators.required], ownerId: [''], defaultListId: [''] }); readonly historyForm = this.fb.nonNullable.group({ period: [currentMonth(), Validators.required], limit: [80, [Validators.required, Validators.min(1), Validators.max(300)]] }); readonly importForm = this.fb.nonNullable.group({ categoryId: ['', Validators.required], status: ['PENDING' as 'DRAFT' | 'PENDING', Validators.required], merchant: ['Zakupy'] }); readonly summaryAmount = computed(() => Number(this.summary()?.total ?? this.summary()?.amount ?? this.summary()?.meta?.total_amount ?? 0)); readonly summaryCount = computed(() => Number(this.summary()?.count ?? this.summary()?.records ?? this.summary()?.meta?.total_count ?? 0)); readonly summaryListCount = computed(() => { const summary = this.summary(); const groups = [summary?.lists, summary?.totals, summary?.aggregates].find((value) => Array.isArray(value)); return Array.isArray(groups) ? groups.length : this.visibleLists().length; }); readonly selectedListTotal = computed(() => this.selectedListExpenses().reduce((sum: number, item: ShoppingListExpenseItem) => sum + this.itemAmount(item), 0)); readonly selectedListCount = computed(() => this.selectedListExpenses().length); ngOnInit() { this.categoriesService.list().subscribe({ next: (response: { items: Category[] }) => { const first = response.items[0]; if (first && !this.importForm.controls.categoryId.value) this.importForm.controls.categoryId.setValue(first.id); }, error: () => undefined }); this.integration.getSettings().subscribe({ next: (response) => { const item = response.item; 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(); }, error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.loadError')) }); } 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) => { 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() { this.integration.test().subscribe({ next: () => this.toast.success(this.ui.t('integrations.testSuccess')), error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.testError')) }); } refresh() { if (!this.configured()) return; 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) => this.summary.set(response), error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.loadError')) }); this.integration.lists({ owner_id: raw.ownerId || undefined, limit: 200 }).subscribe({ next: (response) => { const items = this.pickItems(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([]); }, error: () => { this.allLists.set([]); this.selectedList.set(null); this.selectedListExpenses.set([]); } }); } visibleLists(source?: ShoppingListRef[]) { const period = this.historyForm.controls.period.value; const items = source ?? this.allLists(); return items.filter((item) => { const createdAt = this.listCreatedAt(item); return createdAt ? createdAt.slice(0, 7) === period : true; }); } selectList(item: ShoppingListRef) { this.selectedList.set(item); this.loadListExpenses(item); } importPeriod() { if (this.importForm.invalid) return; const raw = this.importForm.getRawValue(); this.integration.importPeriod({ period: this.historyForm.controls.period.value, categoryId: raw.categoryId, status: raw.status, merchant: raw.merchant || 'Zakupy' }).subscribe({ next: (response) => { this.toast.success(this.ui.t('integrations.importPeriodSuccess')); this.emitWarnings(response.warnings); }, error: (error) => this.toast.error(error.error?.message ?? this.ui.t('integrations.importError')) }); } 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: ['shopping-list'] }).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')) }); } 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); } 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(response)), error: () => this.selectedListExpenses.set([]) }); } private emitWarnings(warnings?: string[]) { (warnings ?? []).forEach((message) => this.toast.warning(message)); } private pickItems(response: { items?: T[]; data?: T[] }) { return response.items ?? response.data ?? []; } }