This commit is contained in:
Mateusz Gruszczyński
2026-04-06 14:37:42 +02:00
parent 237596bd52
commit 80e181ea3f
41 changed files with 14959 additions and 1023 deletions

View File

@@ -0,0 +1,27 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import type { Budget, BudgetListResponse } from '../../shared/models';
@Injectable({ providedIn: 'root' })
export class BudgetsService {
private readonly http = inject(HttpClient);
list(month?: string) {
let params = new HttpParams();
if (month) params = params.set('month', month);
return this.http.get<BudgetListResponse>(`${environment.apiBaseUrl}/budgets`, { params });
}
create(payload: { month: string; name?: string; amount: number; categoryId?: string | null; alertThresholds: number[]; isActive: boolean }) {
return this.http.post<{ item: Budget }>(`${environment.apiBaseUrl}/budgets`, payload);
}
update(id: string, payload: { month: string; name?: string; amount: number; categoryId?: string | null; alertThresholds: number[]; isActive: boolean }) {
return this.http.put<{ item: Budget }>(`${environment.apiBaseUrl}/budgets/${id}`, payload);
}
delete(id: string) {
return this.http.delete<void>(`${environment.apiBaseUrl}/budgets/${id}`);
}
}

View File

@@ -1,13 +1,41 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import type { Expense, Proof } from '../../shared/models';
import type { DuplicateGroup, Expense, Proof } from '../../shared/models';
@Injectable({ providedIn: 'root' })
export class ExpensesService {
private readonly http = inject(HttpClient);
list(filters: { startDate?: string; endDate?: string; categoryId?: string; search?: string } = {}) { let params = new HttpParams(); Object.entries(filters).forEach(([key, value]) => { if (value) params = params.set(key, value); }); return this.http.get<{ items: Expense[] }>(`${environment.apiBaseUrl}/expenses`, { params }); }
create(formData: FormData) { return this.http.post<{ item: Expense }>(`${environment.apiBaseUrl}/expenses`, formData); }
update(id: string, payload: Partial<Expense> & { categoryId: string }) { return this.http.put<{ item: Expense }>(`${environment.apiBaseUrl}/expenses/${id}`, payload); }
delete(id: string) { return this.http.delete<void>(`${environment.apiBaseUrl}/expenses/${id}`); }
addProof(id: string, formData: FormData) { return this.http.post<{ proof: Proof; expense: Expense }>(`${environment.apiBaseUrl}/expenses/${id}/proofs`, formData); }
list(filters: { startDate?: string; endDate?: string; categoryId?: string; search?: string; status?: string; tags?: string; duplicatesOnly?: boolean } = {}) {
let params = new HttpParams();
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') params = params.set(key, String(value));
});
return this.http.get<{ items: Expense[] }>(`${environment.apiBaseUrl}/expenses`, { params });
}
duplicates() {
return this.http.get<{ items: DuplicateGroup[] }>(`${environment.apiBaseUrl}/expenses/duplicates`);
}
create(formData: FormData) {
return this.http.post<{ item: Expense; warnings?: string[] }>(`${environment.apiBaseUrl}/expenses`, formData);
}
update(id: string, payload: Partial<Expense> & { categoryId: string }) {
return this.http.put<{ item: Expense; warnings?: string[] }>(`${environment.apiBaseUrl}/expenses/${id}`, payload);
}
reviewDuplicate(id: string, action: 'CONFIRM' | 'DISMISS' | 'REOPEN') {
return this.http.post<{ item: Expense }>(`${environment.apiBaseUrl}/expenses/${id}/duplicate-review`, { action });
}
delete(id: string) {
return this.http.delete<void>(`${environment.apiBaseUrl}/expenses/${id}`);
}
addProof(id: string, formData: FormData) {
return this.http.post<{ proofs: Proof[]; expense: Expense }>(`${environment.apiBaseUrl}/expenses/${id}/proofs`, formData);
}
}

View File

@@ -0,0 +1,29 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import type { RecurringExpense } from '../../shared/models';
@Injectable({ providedIn: 'root' })
export class RecurringExpensesService {
private readonly http = inject(HttpClient);
list() {
return this.http.get<{ items: RecurringExpense[] }>(`${environment.apiBaseUrl}/recurring-expenses`);
}
create(payload: Partial<RecurringExpense> & { categoryId: string }) {
return this.http.post<{ item: RecurringExpense }>(`${environment.apiBaseUrl}/recurring-expenses`, payload);
}
update(id: string, payload: Partial<RecurringExpense> & { categoryId: string }) {
return this.http.put<{ item: RecurringExpense }>(`${environment.apiBaseUrl}/recurring-expenses/${id}`, payload);
}
delete(id: string) {
return this.http.delete<void>(`${environment.apiBaseUrl}/recurring-expenses/${id}`);
}
runNow() {
return this.http.post<{ message: string }>(`${environment.apiBaseUrl}/recurring-expenses/run`, {});
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import type { ReportPreferences, StatsResponse } from '../../shared/models';
@@ -25,4 +25,12 @@ export class ReportsService {
send() {
return this.http.post<{ message: string; sentTo: string }>(`${environment.apiBaseUrl}/reports/send`, {});
}
export(filters: { format: 'csv' | 'json' | 'html' | 'pdf'; startDate?: string; endDate?: string; categoryIds?: string; status?: string; tag?: string }) {
let params = new HttpParams().set('format', filters.format);
Object.entries(filters).forEach(([key, value]) => {
if (key !== 'format' && value) params = params.set(key, value);
});
return this.http.get(`${environment.apiBaseUrl}/reports/export`, { params, responseType: 'blob' });
}
}

View File

@@ -0,0 +1,82 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import type { Expense, ShoppingListExpenseItem, ShoppingListIntegrationSettings, ShoppingListRef, ShoppingListSummary } from '../../shared/models';
@Injectable({ providedIn: 'root' })
export class ShoppingListIntegrationService {
private readonly http = inject(HttpClient);
getSettings() {
return this.http.get<{ item: ShoppingListIntegrationSettings }>(`${environment.apiBaseUrl}/integrations/shopping-list`);
}
updateSettings(payload: { enabled: boolean; baseUrl?: string | null; apiToken?: string; authMode: 'bearer' | 'x-api-token' | 'both'; ownerId?: string | null; defaultListId?: string | null }) {
return this.http.put<{ item: ShoppingListIntegrationSettings }>(`${environment.apiBaseUrl}/integrations/shopping-list`, payload);
}
test() {
return this.http.post<{ ok: boolean; payload: unknown }>(`${environment.apiBaseUrl}/integrations/shopping-list/test`, {});
}
summary(filters: { start_date?: string; end_date?: string; list_id?: string; owner_id?: string } = {}) {
let params = new HttpParams();
Object.entries(filters).forEach(([key, value]) => {
if (value) params = params.set(key, value);
});
return this.http.get<ShoppingListSummary>(`${environment.apiBaseUrl}/integrations/shopping-list/summary`, { params });
}
latest(filters: { start_date?: string; end_date?: string; list_id?: string; owner_id?: string; limit?: number } = {}) {
let params = new HttpParams();
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') params = params.set(key, String(value));
});
return this.http.get<{ items?: ShoppingListExpenseItem[]; data?: ShoppingListExpenseItem[] }>(`${environment.apiBaseUrl}/integrations/shopping-list/latest`, { params });
}
lists(filters: { owner_id?: string; limit?: number } = {}) {
let params = new HttpParams();
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') params = params.set(key, String(value));
});
return this.http.get<{ items?: ShoppingListRef[]; data?: ShoppingListRef[] }>(`${environment.apiBaseUrl}/integrations/shopping-list/lists`, { params });
}
listExpenses(id: string | number, limit = 50) {
const params = new HttpParams().set('limit', String(limit));
return this.http.get<{ items?: ShoppingListExpenseItem[]; data?: ShoppingListExpenseItem[] }>(`${environment.apiBaseUrl}/integrations/shopping-list/lists/${id}/expenses`, { params });
}
importList(payload: {
listId: string | number;
listTitle?: string | null;
listCreatedAt?: string | null;
categoryId: string;
status: 'DRAFT' | 'PENDING';
merchant?: string | null;
title?: string | null;
description?: string | null;
expenseDate?: string | null;
tags?: string[];
}) {
return this.http.post<{ item: Expense; warnings?: string[] }>(`${environment.apiBaseUrl}/integrations/shopping-list/import-list`, payload);
}
importItem(payload: {
expenseId?: string | number | null;
listId?: string | number | null;
listTitle?: string | null;
categoryId: string;
status: 'DRAFT' | 'PENDING';
title: string;
amount: number;
expenseDate: string;
merchant?: string | null;
ownerName?: string | null;
description?: string | null;
tags?: string[];
}) {
return this.http.post<{ item: Expense; warnings?: string[] }>(`${environment.apiBaseUrl}/integrations/shopping-list/import-item`, payload);
}
}

View File

@@ -1,9 +1,21 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import type { StatsResponse } from '../../shared/models';
import type { CashflowResponse, StatsResponse } from '../../shared/models';
@Injectable({ providedIn: 'root' })
export class StatsService {
private readonly http = inject(HttpClient);
overview(filters: { startDate?: string; endDate?: string; categoryIds?: string; bucket?: 'month' | 'quarter' | 'year' }) { let params = new HttpParams(); Object.entries(filters).forEach(([key, value]) => { if (value) params = params.set(key, value); }); return this.http.get<StatsResponse>(`${environment.apiBaseUrl}/statistics/overview`, { params }); }
overview(filters: { startDate?: string; endDate?: string; categoryIds?: string; bucket?: 'month' | 'quarter' | 'year'; tag?: string; status?: string }) {
let params = new HttpParams();
Object.entries(filters).forEach(([key, value]) => {
if (value) params = params.set(key, value);
});
return this.http.get<StatsResponse>(`${environment.apiBaseUrl}/statistics/overview`, { params });
}
cashflow() {
return this.http.get<CashflowResponse>(`${environment.apiBaseUrl}/statistics/cashflow`);
}
}

View File

@@ -44,6 +44,7 @@ const translations: Record<UiLanguage, Record<string, string>> = {
'action.unblock': 'Odblokuj',
'action.setUser': 'Ustaw USER',
'action.setAdmin': 'Ustaw ADMIN',
'action.import': 'Importuj',
'theme.label': 'Motyw',
'theme.dark': 'Ciemny',
@@ -227,6 +228,87 @@ const translations: Record<UiLanguage, Record<string, string>> = {
'admin.statusUpdated': 'Status konta został zaktualizowany.',
'admin.statusError': 'Nie udało się zmienić statusu.',
'nav.cashflow': 'Cashflow',
'nav.budgets': 'Budżety',
'nav.recurring': 'Cykliczne',
'action.saveDraft': 'Zapisz szkic',
'status.draft': 'Szkic',
'status.pending': 'Oczekuje',
'status.approved': 'Zatwierdzony',
'status.rejected': 'Odrzucony',
'dashboard.cashflowHint': 'Przegląd kosztów, budżetów, duplikatów i przyszłych obciążeń.',
'dashboard.budgetUsage': 'Wykorzystanie budżetu',
'expenses.field.status': 'Status',
'expenses.field.tags': 'Tagi',
'expenses.field.customFields': 'Własne pola',
'expenses.field.customKey': 'Nazwa pola',
'expenses.field.customValue': 'Wartość',
'expenses.tagPlaceholder': 'np. projekt-x, marketing',
'expenses.noCustomFields': 'Brak własnych pól.',
'expenses.attachmentsSelected': 'Wybrane załączniki',
'expenses.duplicatesTitle': 'Wykryte potencjalne duplikaty',
'expenses.potentialMatches': 'podobnych pozycji',
'expenses.duplicatesOnly': 'Pokaż tylko potencjalne duplikaty',
'expenses.duplicate': 'Duplikat',
'expenses.draftSaved': 'Szkic wydatku został zapisany.',
'stats.tags': 'Analiza tagów',
'reports.exportTitle': 'Eksport raportów',
'reports.exportError': 'Nie udało się wyeksportować raportu.',
'budget.title': 'Budżety miesięczne',
'budget.subtitle': 'Limity miesięczne ogólne i per kategoria z alertami zużycia.',
'budget.new': 'Nowy budżet',
'budget.edit': 'Edytuj budżet',
'budget.month': 'Miesiąc',
'budget.name': 'Nazwa',
'budget.amount': 'Kwota budżetu',
'budget.category': 'Kategoria',
'budget.overall': 'Budżet ogólny',
'budget.thresholds': 'Progi alertów',
'budget.total': 'Łączny budżet',
'budget.spent': 'Wydano',
'budget.usage': 'Zużycie',
'budget.alerts': 'Alerty budżetowe',
'budget.saved': 'Budżet został zapisany.',
'budget.saveError': 'Nie udało się zapisać budżetu.',
'budget.deleted': 'Budżet został usunięty.',
'budget.deleteError': 'Nie udało się usunąć budżetu.',
'recurring.title': 'Cykliczne wydatki',
'recurring.subtitle': 'Szablony kosztów generowanych automatycznie w czasie.',
'recurring.new': 'Nowy harmonogram',
'recurring.edit': 'Edytuj harmonogram',
'recurring.frequency': 'Częstotliwość',
'recurring.weekly': 'Co tydzień',
'recurring.monthly': 'Co miesiąc',
'recurring.yearly': 'Co rok',
'recurring.interval': 'Interwał',
'recurring.startDate': 'Data startu',
'recurring.nextRunDate': 'Następne utworzenie',
'recurring.runNow': 'Uruchom teraz',
'recurring.saved': 'Harmonogram został zapisany.',
'recurring.saveError': 'Nie udało się zapisać harmonogramu.',
'recurring.deleted': 'Harmonogram został usunięty.',
'recurring.deleteError': 'Nie udało się usunąć harmonogramu.',
'recurring.ran': 'Cykliczne wydatki zostały przetworzone.',
'recurring.badge': 'Cykliczny',
'cashflow.subtitle': 'Rzeczywiste koszty, budżet, prognoza i najbliższe cykliczne obciążenia.',
'cashflow.actual': 'Rzeczywiste koszty',
'cashflow.budget': 'Budżet',
'cashflow.forecast': 'Prognoza miesiąca',
'cashflow.pending': 'Do akceptacji',
'cashflow.duplicates': 'Duplikaty',
'cashflow.trend': 'Trend cashflow',
'cashflow.statusSummary': 'Statusy wydatków',
'cashflow.upcomingRecurring': 'Nadchodzące cykliczne',
'common.none': 'Brak',
'common.select': 'Wybierz',
'common.noData': 'Brak danych.',
@@ -237,12 +319,67 @@ const translations: Record<UiLanguage, Record<string, string>> = {
'common.blocked': 'Zablokowany',
'common.selected': 'OK',
'nav.integrations': 'Integracje',
'action.testConnection': 'Test połączenia',
'action.refresh': 'Odśwież',
'expenses.duplicateDismissed': 'Duplikat został odrzucony.',
'expenses.duplicateConfirmed': 'Wydatek został oznaczony jako potwierdzony duplikat.',
'expenses.duplicateReopened': 'Sprawdzenie duplikatu zostało przywrócone.',
'expenses.duplicateStatus.open': 'Do sprawdzenia',
'expenses.duplicateStatus.confirmed': 'Potwierdzony',
'expenses.duplicateStatus.dismissed': 'Odrzucony',
'recurring.endDate': 'Data końcowa',
'recurring.maxOccurrences': 'Maks. liczba utworzeń',
'recurring.generatedCount': 'Utworzono',
'integrations.title': 'Integracje',
'integrations.subtitle': 'Połączenia per użytkownik z zewnętrznymi źródłami danych oraz import historyczny.',
'integrations.shoppingList': 'Lista zakupów API',
'integrations.enabled': 'Włącz integrację dla tego użytkownika',
'integrations.baseUrl': 'URL API',
'integrations.apiToken': 'Token API',
'integrations.keepToken': 'Zostaw puste, aby zachować obecny token.',
'integrations.authMode': 'Tryb autoryzacji',
'integrations.ownerId': 'Domyślny owner ID',
'integrations.defaultListId': 'Domyślne list ID',
'integrations.history': 'Import historyczny',
'integrations.period': 'Miesiąc / rok',
'integrations.limit': 'Limit rekordów',
'integrations.summary': 'Podsumowanie zewnętrzne',
'integrations.latest': 'Wydatki z wybranego okresu',
'integrations.lists': 'Listy zakupowe z okresu',
'integrations.listExpenses': 'Pozycje wybranej listy',
'integrations.importTitle': 'Import do lokalnych wydatków',
'integrations.importSelectedList': 'Importuj wybraną listę jako 1 wydatek',
'integrations.selectListHint': 'Wybierz listę po lewej, aby podejrzeć pozycje i zaimportować całą listę lub pojedyncze wydatki.',
'integrations.selectedListSummary': 'Pozycje / suma',
'integrations.tags': 'Tagi importu',
'integrations.tagsHint': 'Oddzielaj tagi przecinkami.',
'integrations.externalSpend': 'Suma zewnętrzna',
'integrations.externalCount': 'Rekordy zewnętrzne',
'integrations.notConfigured': 'Skonfiguruj integrację i zapisz ustawienia, aby pobrać dane.',
'integrations.saveSuccess': 'Ustawienia integracji zostały zapisane.',
'integrations.saveError': 'Nie udało się zapisać ustawień integracji.',
'integrations.testSuccess': 'Połączenie z zewnętrznym API działa.',
'integrations.testError': 'Nie udało się połączyć z zewnętrznym API.',
'integrations.loadError': 'Nie udało się pobrać danych integracji.',
'integrations.importListSuccess': 'Lista zakupowa została zaimportowana jako lokalny wydatek.',
'integrations.importItemSuccess': 'Pozycja z listy zakupowej została zaimportowana.',
'integrations.importError': 'Nie udało się zaimportować danych z list zakupowych.',
'dashboard.externalSpend': 'Zewnętrzna suma',
'dashboard.externalRecords': 'Zewnętrzne rekordy',
'table.title': 'Tytuł',
'table.merchant': 'Kontrahent',
'table.date': 'Data',
'table.amount': 'Kwota',
'table.count': 'Liczba',
'table.category': 'Kategoria',
'table.actions': 'Akcje',
'toast.ready': 'Gotowe',
'toast.error': 'Błąd',
@@ -288,6 +425,7 @@ const translations: Record<UiLanguage, Record<string, string>> = {
'action.unblock': 'Unblock',
'action.setUser': 'Set USER',
'action.setAdmin': 'Set ADMIN',
'action.import': 'Import',
'theme.label': 'Theme',
'theme.dark': 'Dark',
@@ -471,6 +609,87 @@ const translations: Record<UiLanguage, Record<string, string>> = {
'admin.statusUpdated': 'Account status updated successfully.',
'admin.statusError': 'Failed to change the account status.',
'nav.cashflow': 'Cashflow',
'nav.budgets': 'Budgets',
'nav.recurring': 'Recurring',
'action.saveDraft': 'Save draft',
'status.draft': 'Draft',
'status.pending': 'Pending',
'status.approved': 'Approved',
'status.rejected': 'Rejected',
'dashboard.cashflowHint': 'Overview of spend, budgets, duplicates, and upcoming recurring charges.',
'dashboard.budgetUsage': 'Budget usage',
'expenses.field.status': 'Status',
'expenses.field.tags': 'Tags',
'expenses.field.customFields': 'Custom fields',
'expenses.field.customKey': 'Field name',
'expenses.field.customValue': 'Value',
'expenses.tagPlaceholder': 'e.g. project-x, marketing',
'expenses.noCustomFields': 'No custom fields.',
'expenses.attachmentsSelected': 'Selected attachments',
'expenses.duplicatesTitle': 'Potential duplicates detected',
'expenses.potentialMatches': 'similar entries',
'expenses.duplicatesOnly': 'Show only potential duplicates',
'expenses.duplicate': 'Duplicate',
'expenses.draftSaved': 'Expense draft was saved.',
'stats.tags': 'Tag analysis',
'reports.exportTitle': 'Report export',
'reports.exportError': 'Failed to export the report.',
'budget.title': 'Monthly budgets',
'budget.subtitle': 'Monthly limits overall and per category with usage alerts.',
'budget.new': 'New budget',
'budget.edit': 'Edit budget',
'budget.month': 'Month',
'budget.name': 'Name',
'budget.amount': 'Budget amount',
'budget.category': 'Category',
'budget.overall': 'Overall budget',
'budget.thresholds': 'Alert thresholds',
'budget.total': 'Total budget',
'budget.spent': 'Spent',
'budget.usage': 'Usage',
'budget.alerts': 'Budget alerts',
'budget.saved': 'Budget was saved.',
'budget.saveError': 'Failed to save budget.',
'budget.deleted': 'Budget was deleted.',
'budget.deleteError': 'Failed to delete budget.',
'recurring.title': 'Recurring expenses',
'recurring.subtitle': 'Templates for costs generated automatically over time.',
'recurring.new': 'New schedule',
'recurring.edit': 'Edit schedule',
'recurring.frequency': 'Frequency',
'recurring.weekly': 'Weekly',
'recurring.monthly': 'Monthly',
'recurring.yearly': 'Yearly',
'recurring.interval': 'Interval',
'recurring.startDate': 'Start date',
'recurring.nextRunDate': 'Next run date',
'recurring.runNow': 'Run now',
'recurring.saved': 'Recurring schedule was saved.',
'recurring.saveError': 'Failed to save recurring schedule.',
'recurring.deleted': 'Recurring schedule was deleted.',
'recurring.deleteError': 'Failed to delete recurring schedule.',
'recurring.ran': 'Recurring expenses were processed.',
'recurring.badge': 'Recurring',
'cashflow.subtitle': 'Actual spend, budget, forecast, and upcoming recurring charges.',
'cashflow.actual': 'Actual spend',
'cashflow.budget': 'Budget',
'cashflow.forecast': 'Month forecast',
'cashflow.pending': 'Pending approval',
'cashflow.duplicates': 'Duplicates',
'cashflow.trend': 'Cashflow trend',
'cashflow.statusSummary': 'Expense statuses',
'cashflow.upcomingRecurring': 'Upcoming recurring',
'common.none': 'None',
'common.select': 'Select',
'common.noData': 'No data.',
@@ -481,12 +700,67 @@ const translations: Record<UiLanguage, Record<string, string>> = {
'common.blocked': 'Blocked',
'common.selected': 'OK',
'nav.integrations': 'Integrations',
'action.testConnection': 'Test connection',
'action.refresh': 'Refresh',
'expenses.duplicateDismissed': 'Duplicate flag was dismissed.',
'expenses.duplicateConfirmed': 'Expense was marked as a confirmed duplicate.',
'expenses.duplicateReopened': 'Duplicate review was reopened.',
'expenses.duplicateStatus.open': 'Needs review',
'expenses.duplicateStatus.confirmed': 'Confirmed',
'expenses.duplicateStatus.dismissed': 'Dismissed',
'recurring.endDate': 'End date',
'recurring.maxOccurrences': 'Max occurrences',
'recurring.generatedCount': 'Generated',
'integrations.title': 'Integrations',
'integrations.subtitle': 'Per-user connections to external data sources with historical backfill.',
'integrations.shoppingList': 'Shopping list API',
'integrations.enabled': 'Enable integration for this user',
'integrations.baseUrl': 'API URL',
'integrations.apiToken': 'API token',
'integrations.keepToken': 'Leave blank to keep the current token.',
'integrations.authMode': 'Authorization mode',
'integrations.ownerId': 'Default owner ID',
'integrations.defaultListId': 'Default list ID',
'integrations.history': 'Historical import',
'integrations.period': 'Month / year',
'integrations.limit': 'Record limit',
'integrations.summary': 'External summary',
'integrations.latest': 'Expenses for selected period',
'integrations.lists': 'Shopping lists for period',
'integrations.listExpenses': 'Entries for selected list',
'integrations.importTitle': 'Import into local expenses',
'integrations.importSelectedList': 'Import selected list as 1 expense',
'integrations.selectListHint': 'Select a list on the left to preview entries and import the whole list or individual expenses.',
'integrations.selectedListSummary': 'Entries / total',
'integrations.tags': 'Import tags',
'integrations.tagsHint': 'Separate tags with commas.',
'integrations.externalSpend': 'External spend',
'integrations.externalCount': 'External records',
'integrations.notConfigured': 'Configure the integration and save settings to load data.',
'integrations.saveSuccess': 'Integration settings were saved.',
'integrations.saveError': 'Failed to save integration settings.',
'integrations.testSuccess': 'Connection to the external API works.',
'integrations.testError': 'Failed to connect to the external API.',
'integrations.loadError': 'Failed to load integration data.',
'integrations.importListSuccess': 'The shopping list was imported as a local expense.',
'integrations.importItemSuccess': 'The shopping list entry was imported.',
'integrations.importError': 'Failed to import data from the shopping list API.',
'dashboard.externalSpend': 'External spend',
'dashboard.externalRecords': 'External records',
'table.title': 'Title',
'table.merchant': 'Merchant',
'table.date': 'Date',
'table.amount': 'Amount',
'table.count': 'Count',
'table.category': 'Category',
'table.actions': 'Actions',
'toast.ready': 'Done',
'toast.error': 'Error',