diff --git a/api/package-lock.json b/api/package-lock.json index ea3bdf6..8c9fb86 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "expense-control-api", - "version": "1.0.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "expense-control-api", - "version": "1.0.0", + "version": "0.0.1", "dependencies": { "bcryptjs": "^3.0.2", "cors": "^2.8.5", diff --git a/web/package-lock.json b/web/package-lock.json index f71b36a..9b240c8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "expense-control-web", - "version": "0.0.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "expense-control-web", - "version": "0.0.0", + "version": "0.0.1", "dependencies": { "@angular/common": "^21.2.7", "@angular/compiler": "^21.2.7", diff --git a/web/src/app/app.html b/web/src/app/app.html new file mode 100644 index 0000000..e194f53 --- /dev/null +++ b/web/src/app/app.html @@ -0,0 +1,2 @@ + + diff --git a/web/src/app/app.ts b/web/src/app/app.ts index 9dc3c19..542bb8d 100644 --- a/web/src/app/app.ts +++ b/web/src/app/app.ts @@ -8,10 +8,7 @@ import { ToastOutletComponent } from './shared/ui/toast-outlet.component'; selector: 'app-root', standalone: true, imports: [RouterOutlet, ToastOutletComponent], - template: ` - - - ` + templateUrl: './app.html' }) export class App { private readonly auth = inject(AuthService); diff --git a/web/src/app/features/admin/admin.component.html b/web/src/app/features/admin/admin.component.html new file mode 100644 index 0000000..978372a --- /dev/null +++ b/web/src/app/features/admin/admin.component.html @@ -0,0 +1,211 @@ + + +@if (systemInfo()) { +
+
+
+
+
+

{{ ui.t('admin.techTitle') }}

+
{{ ui.t('admin.techSubtitle') }}
+
+ {{ systemInfo()!.environment }} +
+
+
+
{{ ui.t('admin.appVersion') }}
{{ systemInfo()!.suiteVersion }}
+
API
{{ systemInfo()!.apiVersion }}
+
Web
{{ systemInfo()!.webVersion }}
+
Node.js
{{ systemInfo()!.nodeVersion }}
+
+ +
+
+
+ + + + + + + + + +
{{ ui.t('admin.database') }}{{ systemInfo()!.database }}
Upload dir{{ systemInfo()!.uploadDir }}
{{ ui.t('admin.registration') }}{{ systemInfo()!.registrationEnabled ? ui.t('common.active') : ui.t('common.blocked') }}
SMTP{{ systemInfo()!.smtpConfigured ? ui.t('admin.smtpReady') : ui.t('admin.smtpNotReady') }}
API base{{ systemInfo()!.sources.apiBasePath }}
{{ ui.t('table.date') }}{{ systemInfo()!.checkedAt | date:'yyyy-MM-dd HH:mm:ss' }}
+
+
+
+
+
{{ ui.t('admin.kpi.users') }}{{ systemInfo()!.counters.users }}
+
{{ ui.t('admin.kpi.expenses') }}{{ systemInfo()!.counters.expenses }}
+
{{ ui.t('admin.kpi.categories') }}{{ systemInfo()!.counters.categories }}
+
{{ ui.t('admin.kpi.merchants') }}{{ systemInfo()!.counters.merchants }}
+
{{ ui.t('admin.kpi.budgets') }}{{ systemInfo()!.counters.budgets }}
+
{{ ui.t('admin.kpi.recurring') }}{{ systemInfo()!.counters.recurring }}
+
{{ ui.t('admin.kpi.integrations') }}{{ systemInfo()!.counters.shoppingIntegrations }}
+
+ +
+
+
+
+
+
+} + +
+
+
+

{{ ui.t('admin.settings') }}

+
+
+
+ + +
+ +
+
+
+
+ + + +
+
{{ ui.t('admin.smtp') }}
+ + + +
+
+
+
+
+
+
+
+ + +
{{ ui.t('admin.smtpHint') }}
+ +
+ + +
+
+
+
+
+ +
+
+
+

{{ ui.t('admin.users') }}

+ {{ users().length }} +
+
+ + + + @for (user of users(); track user.id) { + + + + + + + + + @if (editingUserId() === user.id) { + + + + } + } @empty { + + } + +
{{ ui.t('admin.userLabel') }}{{ ui.t('admin.role') }}{{ ui.t('admin.status') }}{{ ui.t('admin.integrationsAccess') }}{{ ui.t('admin.date') }}
+
{{ user.fullName }}
+
{{ user.email }}
+
{{ user.role }} + + {{ user.isActive ? ui.t('common.active') : ui.t('common.blocked') }} + + {{ user.integrationsEnabled ? ui.t('common.active') : ui.t('common.blocked') }}{{ user.createdAt | date:'short' }} +
+ + + + +
+
+
+
+
+
+
+
+
{{ ui.t('admin.passwordHint') }}
+
+ + +
+
+ + +
+
+
{{ ui.t('admin.noUsers') }}
+
+
+ +
+

{{ ui.t('admin.newUser') }}

+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
diff --git a/web/src/app/features/admin/admin.component.ts b/web/src/app/features/admin/admin.component.ts index 4a84521..56db4ba 100644 --- a/web/src/app/features/admin/admin.component.ts +++ b/web/src/app/features/admin/admin.component.ts @@ -11,219 +11,7 @@ import type { AdminSystemInfo, AppSettings, User } from '../../shared/models'; selector: 'app-admin', standalone: true, imports: [CommonModule, ReactiveFormsModule, DatePipe], - template: ` - - - @if (systemInfo()) { -
-
-
-
-
-

{{ ui.t('admin.techTitle') }}

-
{{ ui.t('admin.techSubtitle') }}
-
- {{ systemInfo()!.environment }} -
-
-
-
{{ ui.t('admin.appVersion') }}
{{ systemInfo()!.suiteVersion }}
-
API
{{ systemInfo()!.apiVersion }}
-
Web
{{ systemInfo()!.webVersion }}
-
Node.js
{{ systemInfo()!.nodeVersion }}
-
- -
-
-
- - - - - - - - - -
{{ ui.t('admin.database') }}{{ systemInfo()!.database }}
Upload dir{{ systemInfo()!.uploadDir }}
{{ ui.t('admin.registration') }}{{ systemInfo()!.registrationEnabled ? ui.t('common.active') : ui.t('common.blocked') }}
SMTP{{ systemInfo()!.smtpConfigured ? ui.t('admin.smtpReady') : ui.t('admin.smtpNotReady') }}
API base{{ systemInfo()!.sources.apiBasePath }}
{{ ui.t('table.date') }}{{ systemInfo()!.checkedAt | date:'yyyy-MM-dd HH:mm:ss' }}
-
-
-
-
-
{{ ui.t('admin.kpi.users') }}{{ systemInfo()!.counters.users }}
-
{{ ui.t('admin.kpi.expenses') }}{{ systemInfo()!.counters.expenses }}
-
{{ ui.t('admin.kpi.categories') }}{{ systemInfo()!.counters.categories }}
-
{{ ui.t('admin.kpi.merchants') }}{{ systemInfo()!.counters.merchants }}
-
{{ ui.t('admin.kpi.budgets') }}{{ systemInfo()!.counters.budgets }}
-
{{ ui.t('admin.kpi.recurring') }}{{ systemInfo()!.counters.recurring }}
-
{{ ui.t('admin.kpi.integrations') }}{{ systemInfo()!.counters.shoppingIntegrations }}
-
- -
-
-
-
-
-
- } - -
-
-
-

{{ ui.t('admin.settings') }}

-
-
-
- - -
- -
-
-
-
- - - -
-
{{ ui.t('admin.smtp') }}
- - - -
-
-
-
-
-
-
-
- - -
{{ ui.t('admin.smtpHint') }}
- -
- - -
-
-
-
-
- -
-
-
-

{{ ui.t('admin.users') }}

- {{ users().length }} -
-
- - - - @for (user of users(); track user.id) { - - - - - - - - - @if (editingUserId() === user.id) { - - - - } - } @empty { - - } - -
{{ ui.t('admin.userLabel') }}{{ ui.t('admin.role') }}{{ ui.t('admin.status') }}{{ ui.t('admin.integrationsAccess') }}{{ ui.t('admin.date') }}
-
{{ user.fullName }}
-
{{ user.email }}
-
{{ user.role }} - - {{ user.isActive ? ui.t('common.active') : ui.t('common.blocked') }} - - {{ user.integrationsEnabled ? ui.t('common.active') : ui.t('common.blocked') }}{{ user.createdAt | date:'short' }} -
- - - - -
-
-
-
-
-
-
-
-
{{ ui.t('admin.passwordHint') }}
-
- - -
-
- - -
-
-
{{ ui.t('admin.noUsers') }}
-
-
- -
-

{{ ui.t('admin.newUser') }}

-
-
-
-
-
-
-
-
- - -
-
- -
-
-
-
-
-
- ` + templateUrl: './admin.component.html' }) export class AdminComponent implements OnInit { readonly ui = inject(UiService); diff --git a/web/src/app/features/auth/login.component.html b/web/src/app/features/auth/login.component.html new file mode 100644 index 0000000..8856115 --- /dev/null +++ b/web/src/app/features/auth/login.component.html @@ -0,0 +1,81 @@ +
+
+ +
+
diff --git a/web/src/app/features/auth/login.component.ts b/web/src/app/features/auth/login.component.ts index 72d5d03..5b37fcf 100644 --- a/web/src/app/features/auth/login.component.ts +++ b/web/src/app/features/auth/login.component.ts @@ -11,89 +11,7 @@ import { UiService } from '../../core/services/ui.service'; selector: 'app-login', standalone: true, imports: [CommonModule, ReactiveFormsModule], - template: ` -
-
- -
-
- ` + templateUrl: './login.component.html' }) export class LoginComponent { private readonly fb = inject(FormBuilder); diff --git a/web/src/app/features/budgets/budgets.component.html b/web/src/app/features/budgets/budgets.component.html new file mode 100644 index 0000000..7346513 --- /dev/null +++ b/web/src/app/features/budgets/budgets.component.html @@ -0,0 +1,76 @@ + + +
+
+
+
+

{{ editingId() ? ui.t('budget.edit') : ui.t('budget.new') }}

+ @if (editingId()) { + + } +
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+
{{ ui.t('budget.total') }}
{{ summary()?.totalBudget || 0 | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('budget.spent') }}
{{ summary()?.totalSpent || 0 | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('budget.alerts') }}
{{ summary()?.alerts?.length || 0 }}
+
+ + @if (summary()?.alerts?.length) { +
+
{{ ui.t('budget.alerts') }}
+
@for (alert of summary()!.alerts; track alert.budgetId) {
{{ alert.message }}
}
+
+ } + +
+
+

{{ ui.t('budget.title') }}

+ +
+
+ + + + @for (item of items(); track item.id) { + + + + + + + + } @empty { } + +
{{ ui.t('budget.name') }}{{ ui.t('table.category') }}{{ ui.t('budget.usage') }}{{ ui.t('table.amount') }}
{{ item.name || item.category?.name || ui.t('budget.overall') }}
{{ item.category?.name || ui.t('budget.overall') }}{{ item.usagePercent }}%
{{ item.spent | currency:'PLN':'symbol':'1.2-2' }}
/ {{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
+
+
+
+
diff --git a/web/src/app/features/budgets/budgets.component.ts b/web/src/app/features/budgets/budgets.component.ts index 59a5ee4..a7c022a 100644 --- a/web/src/app/features/budgets/budgets.component.ts +++ b/web/src/app/features/budgets/budgets.component.ts @@ -16,84 +16,7 @@ const currentMonth = () => { selector: 'app-budgets', standalone: true, imports: [CommonModule, ReactiveFormsModule, CurrencyPipe], - template: ` - - -
-
-
-
-

{{ editingId() ? ui.t('budget.edit') : ui.t('budget.new') }}

- @if (editingId()) { - - } -
-
-
-
-
-
-
- - -
-
- - -
-
-
-
- -
-
-
{{ ui.t('budget.total') }}
{{ summary()?.totalBudget || 0 | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('budget.spent') }}
{{ summary()?.totalSpent || 0 | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('budget.alerts') }}
{{ summary()?.alerts?.length || 0 }}
-
- - @if (summary()?.alerts?.length) { -
-
{{ ui.t('budget.alerts') }}
-
@for (alert of summary()!.alerts; track alert.budgetId) {
{{ alert.message }}
}
-
- } - -
-
-

{{ ui.t('budget.title') }}

- -
-
- - - - @for (item of items(); track item.id) { - - - - - - - - } @empty { } - -
{{ ui.t('budget.name') }}{{ ui.t('table.category') }}{{ ui.t('budget.usage') }}{{ ui.t('table.amount') }}
{{ item.name || item.category?.name || ui.t('budget.overall') }}
{{ item.category?.name || ui.t('budget.overall') }}{{ item.usagePercent }}%
{{ item.spent | currency:'PLN':'symbol':'1.2-2' }}
/ {{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
-
-
-
-
- ` + templateUrl: './budgets.component.html' }) export class BudgetsComponent implements OnInit { readonly ui = inject(UiService); diff --git a/web/src/app/features/cashflow/cashflow.component.html b/web/src/app/features/cashflow/cashflow.component.html new file mode 100644 index 0000000..0231897 --- /dev/null +++ b/web/src/app/features/cashflow/cashflow.component.html @@ -0,0 +1,42 @@ + + +
+
{{ ui.t('cashflow.actual') }}
{{ (data()?.actualCurrent || 0) | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('cashflow.budget') }}
{{ (data()?.totalBudget || 0) | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('cashflow.forecast') }}
{{ (data()?.forecastCurrentMonth || 0) | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('cashflow.pending') }}
{{ data()?.pendingApproval || 0 }}
+ +
+
+

{{ ui.t('cashflow.trend') }}

+
+
+
+
+
+

{{ ui.t('budget.alerts') }}

+
+ @for (alert of data()?.alerts || []; track alert.id) { +
{{ alert.name }} · {{ alert.usagePercent }}%
+ } @empty { +
{{ ui.t('common.noData') }}
+ } +
+
+
+ +
+
+

{{ ui.t('cashflow.statusSummary') }}

+
@for (item of data()?.statusSummary || []; track item.status) { } @empty { }
{{ ui.t('expenses.field.status') }}{{ ui.t('table.count') }}
{{ ui.t('status.' + item.status.toLowerCase()) }}{{ item.count }}
{{ ui.t('common.noData') }}
+
+
+
+
+

{{ ui.t('cashflow.upcomingRecurring') }}

+
@for (item of data()?.upcomingRecurring || []; track item.id) { } @empty { }
{{ ui.t('table.title') }}{{ ui.t('table.date') }}{{ ui.t('table.amount') }}
{{ item.title }}{{ item.nextRunDate | date:'yyyy-MM-dd' }}{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
+
+
+
diff --git a/web/src/app/features/cashflow/cashflow.component.ts b/web/src/app/features/cashflow/cashflow.component.ts index c84676d..83df1b4 100644 --- a/web/src/app/features/cashflow/cashflow.component.ts +++ b/web/src/app/features/cashflow/cashflow.component.ts @@ -11,50 +11,7 @@ Chart.register(LineController, LineElement, PointElement, CategoryScale, LinearS selector: 'app-cashflow', standalone: true, imports: [CommonModule, CurrencyPipe, DatePipe], - template: ` - - -
-
{{ ui.t('cashflow.actual') }}
{{ (data()?.actualCurrent || 0) | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('cashflow.budget') }}
{{ (data()?.totalBudget || 0) | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('cashflow.forecast') }}
{{ (data()?.forecastCurrentMonth || 0) | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('cashflow.pending') }}
{{ data()?.pendingApproval || 0 }}
- -
-
-

{{ ui.t('cashflow.trend') }}

-
-
-
-
-
-

{{ ui.t('budget.alerts') }}

-
- @for (alert of data()?.alerts || []; track alert.id) { -
{{ alert.name }} · {{ alert.usagePercent }}%
- } @empty { -
{{ ui.t('common.noData') }}
- } -
-
-
- -
-
-

{{ ui.t('cashflow.statusSummary') }}

-
@for (item of data()?.statusSummary || []; track item.status) { } @empty { }
{{ ui.t('expenses.field.status') }}{{ ui.t('table.count') }}
{{ ui.t('status.' + item.status.toLowerCase()) }}{{ item.count }}
{{ ui.t('common.noData') }}
-
-
-
-
-

{{ ui.t('cashflow.upcomingRecurring') }}

-
@for (item of data()?.upcomingRecurring || []; track item.id) { } @empty { }
{{ ui.t('table.title') }}{{ ui.t('table.date') }}{{ ui.t('table.amount') }}
{{ item.title }}{{ item.nextRunDate | date:'yyyy-MM-dd' }}{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
-
-
-
- ` + templateUrl: './cashflow.component.html' }) export class CashflowComponent implements OnInit, OnDestroy { readonly ui = inject(UiService); diff --git a/web/src/app/features/categories/categories.component.html b/web/src/app/features/categories/categories.component.html new file mode 100644 index 0000000..edf87f1 --- /dev/null +++ b/web/src/app/features/categories/categories.component.html @@ -0,0 +1,81 @@ + + +
+
+
+

{{ editingId ? ui.t('categories.edit') : ui.t('categories.new') }}

+
+
+
+ + +
+ +
+ +
+ + + +
+
+ @for (preset of presets; track preset) { + + } +
+
+ +
+ + @if (editingId) { + + } +
+
+
+
+
+ +
+
+
+ + + + + + @for (item of items(); track item.id) { + + + + + + + } @empty { + + } + +
{{ ui.t('categories.name') }}{{ ui.t('categories.color') }}{{ ui.t('categories.type') }}
{{ item.name }} {{ item.color }}{{ item.isSystem ? ui.t('categories.system') : ui.t('categories.custom') }} +
+ + @if (!item.isSystem) { + + } +
+
{{ ui.t('common.noCategories') }}
+
+
+
+
diff --git a/web/src/app/features/categories/categories.component.ts b/web/src/app/features/categories/categories.component.ts index b9ffe54..f2051b0 100644 --- a/web/src/app/features/categories/categories.component.ts +++ b/web/src/app/features/categories/categories.component.ts @@ -12,89 +12,7 @@ const presets = ['#b91c1c', '#2563eb', '#0891b2', '#16a34a', '#7c3aed', '#f59e0b selector: 'app-categories', standalone: true, imports: [CommonModule, ReactiveFormsModule], - template: ` - - -
-
-
-

{{ editingId ? ui.t('categories.edit') : ui.t('categories.new') }}

-
-
-
- - -
- -
- -
- - - -
-
- @for (preset of presets; track preset) { - - } -
-
- -
- - @if (editingId) { - - } -
-
-
-
-
- -
-
-
- - - - - - @for (item of items(); track item.id) { - - - - - - - } @empty { - - } - -
{{ ui.t('categories.name') }}{{ ui.t('categories.color') }}{{ ui.t('categories.type') }}
{{ item.name }} {{ item.color }}{{ item.isSystem ? ui.t('categories.system') : ui.t('categories.custom') }} -
- - @if (!item.isSystem) { - - } -
-
{{ ui.t('common.noCategories') }}
-
-
-
-
- ` + templateUrl: './categories.component.html' }) export class CategoriesComponent implements OnInit { readonly ui = inject(UiService); diff --git a/web/src/app/features/dashboard/dashboard.component.html b/web/src/app/features/dashboard/dashboard.component.html new file mode 100644 index 0000000..fe38468 --- /dev/null +++ b/web/src/app/features/dashboard/dashboard.component.html @@ -0,0 +1,125 @@ + + +
+
{{ ui.t('dashboard.total') }}
{{ (stats?.total || 0) | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('dashboard.count') }}
{{ stats?.count || 0 }}
+
{{ ui.t('dashboard.budgetUsage') }}
{{ cashflow?.budgetUsagePercent || 0 }}%
+
{{ ui.t('cashflow.forecast') }}
{{ (cashflow?.forecastCurrentMonth || 0) | currency:'PLN':'symbol':'1.2-2' }}
+ + @if (canShowExternalStats()) { +
{{ ui.t('dashboard.externalSpend') }}
{{ externalAmount() | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('dashboard.externalRecords') }}
{{ externalCount() }}
+ } + +
+
+
+

{{ ui.t('dashboard.trend') }}

+
+ @for (option of timelineRangeOptions; track option.value) { + + } +
+
+
+ @if (timelineStats?.timeline?.length) { +
+ } @else { +
{{ ui.t('dashboard.noTrendData') }}
+ } +
+
+
+ +
+
+

{{ ui.t('dashboard.share') }}

+
+ @if (stats?.byCategory?.length) { +
+ } @else { +
{{ ui.t('dashboard.noChartData') }}
+ } +
+
+
+ +
+
+

{{ ui.t('nav.cashflow') }}

+
+
{{ ui.t('cashflow.actual') }}{{ (cashflow?.actualCurrent || 0) | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('cashflow.budget') }}{{ (cashflow?.totalBudget || 0) | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('cashflow.pending') }}{{ cashflow?.pendingApproval || 0 }}
+
{{ ui.t('cashflow.duplicates') }}{{ cashflow?.duplicateCount || 0 }}
+
+
{{ ui.t('budget.alerts') }}
+
+ @for (alert of cashflow?.alerts || []; track alert.id) { +
{{ alert.name }} · {{ alert.usagePercent }}%
+ } @empty { +
{{ ui.t('common.noData') }}
+ } +
+
+
+
+
+ +
+
+

{{ ui.t('dashboard.recent') }}

+
+ + + + @for (item of recentExpenses; track item.id) { + + + + + + + } @empty { + + } + +
{{ ui.t('table.title') }}{{ ui.t('table.category') }}{{ ui.t('expenses.field.status') }}{{ ui.t('table.amount') }}
+
{{ item.title }}
+
{{ item.merchant || ui.t('expenses.noMerchant') }}
+
{{ item.category.name }}{{ ui.t('status.' + item.status.toLowerCase()) }}{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noExpenses') }}
+
+
+
+ +
+
+

{{ ui.t('cashflow.upcomingRecurring') }}

+
+ + + + @for (item of cashflow?.upcomingRecurring || []; track item.id) { + + } @empty { + + } + +
{{ ui.t('table.title') }}{{ ui.t('table.date') }}{{ ui.t('table.amount') }}
{{ item.title }}{{ item.nextRunDate | date:'yyyy-MM-dd' }}{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
+
+
+
+
diff --git a/web/src/app/features/dashboard/dashboard.component.ts b/web/src/app/features/dashboard/dashboard.component.ts index f2ebdec..a12aabe 100644 --- a/web/src/app/features/dashboard/dashboard.component.ts +++ b/web/src/app/features/dashboard/dashboard.component.ts @@ -55,133 +55,7 @@ const getTimelineRange = (range: TimelineRangeKey) => { selector: 'app-dashboard', standalone: true, imports: [CommonModule, CurrencyPipe, DatePipe], - template: ` - - -
-
{{ ui.t('dashboard.total') }}
{{ (stats?.total || 0) | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('dashboard.count') }}
{{ stats?.count || 0 }}
-
{{ ui.t('dashboard.budgetUsage') }}
{{ cashflow?.budgetUsagePercent || 0 }}%
-
{{ ui.t('cashflow.forecast') }}
{{ (cashflow?.forecastCurrentMonth || 0) | currency:'PLN':'symbol':'1.2-2' }}
- - @if (canShowExternalStats()) { -
{{ ui.t('dashboard.externalSpend') }}
{{ externalAmount() | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('dashboard.externalRecords') }}
{{ externalCount() }}
- } - -
-
-
-

{{ ui.t('dashboard.trend') }}

-
- @for (option of timelineRangeOptions; track option.value) { - - } -
-
-
- @if (timelineStats?.timeline?.length) { -
- } @else { -
{{ ui.t('dashboard.noTrendData') }}
- } -
-
-
- -
-
-

{{ ui.t('dashboard.share') }}

-
- @if (stats?.byCategory?.length) { -
- } @else { -
{{ ui.t('dashboard.noChartData') }}
- } -
-
-
- -
-
-

{{ ui.t('nav.cashflow') }}

-
-
{{ ui.t('cashflow.actual') }}{{ (cashflow?.actualCurrent || 0) | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('cashflow.budget') }}{{ (cashflow?.totalBudget || 0) | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('cashflow.pending') }}{{ cashflow?.pendingApproval || 0 }}
-
{{ ui.t('cashflow.duplicates') }}{{ cashflow?.duplicateCount || 0 }}
-
-
{{ ui.t('budget.alerts') }}
-
- @for (alert of cashflow?.alerts || []; track alert.id) { -
{{ alert.name }} · {{ alert.usagePercent }}%
- } @empty { -
{{ ui.t('common.noData') }}
- } -
-
-
-
-
- -
-
-

{{ ui.t('dashboard.recent') }}

-
- - - - @for (item of recentExpenses; track item.id) { - - - - - - - } @empty { - - } - -
{{ ui.t('table.title') }}{{ ui.t('table.category') }}{{ ui.t('expenses.field.status') }}{{ ui.t('table.amount') }}
-
{{ item.title }}
-
{{ item.merchant || ui.t('expenses.noMerchant') }}
-
{{ item.category.name }}{{ ui.t('status.' + item.status.toLowerCase()) }}{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noExpenses') }}
-
-
-
- -
-
-

{{ ui.t('cashflow.upcomingRecurring') }}

-
- - - - @for (item of cashflow?.upcomingRecurring || []; track item.id) { - - } @empty { - - } - -
{{ ui.t('table.title') }}{{ ui.t('table.date') }}{{ ui.t('table.amount') }}
{{ item.title }}{{ item.nextRunDate | date:'yyyy-MM-dd' }}{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
-
-
-
-
- ` + templateUrl: './dashboard.component.html' }) export class DashboardComponent implements OnInit, AfterViewChecked, OnDestroy { readonly ui = inject(UiService); diff --git a/web/src/app/features/expenses/expense-detail.component.html b/web/src/app/features/expenses/expense-detail.component.html new file mode 100644 index 0000000..1a5caf5 --- /dev/null +++ b/web/src/app/features/expenses/expense-detail.component.html @@ -0,0 +1,108 @@ + + +@if (loadError()) { +
{{ loadError() }}
+} @else if (loading()) { +
{{ ui.t('common.loading') }}
+} @else if (expense(); as item) { +
+
+
+

{{ item.title }}

+
+
+
{{ ui.t('expenses.field.date') }}
{{ item.expenseDate | date:'yyyy-MM-dd' }}
+
{{ ui.t('expenses.field.amount') }}
{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('expenses.field.status') }}
{{ ui.t('status.' + item.status.toLowerCase()) }}
+
{{ ui.t('expenses.field.category') }}
{{ item.category.name }}
+
{{ ui.t('expenses.field.payment') }}
{{ paymentLabel(item.paymentMethod) }}
+
{{ ui.t('expenses.field.merchantName') }}
{{ item.merchant || ui.t('expenses.noMerchant') }}
+
+ + @if (item.description) { +
+
{{ ui.t('expenses.field.description') }}
+
{{ item.description }}
+
+ } + + @if (item.tags.length) { +
+
{{ ui.t('expenses.field.tags') }}
+
@for (tag of item.tags; track tag) { #{{ tag }} }
+
+ } + + @if (customFieldEntries(item).length) { +
+
{{ ui.t('expenses.field.customFields') }}
+
@for (field of customFieldEntries(item); track field[0]) {
{{ field[0] }}
{{ field[1] }}
}
+
+ } +
+
+
+ +
+
+

{{ ui.t('expenses.existingProofs') }}

+
+ @if (item.proofs.length) { +
+ @for (proof of item.proofs; track proof.id) { + + } +
+ } @else { +
{{ ui.t('expenses.noProofs') }}
+ } +
+
+ +
+

{{ ui.t('expenses.meta') }}

+
+
ID: {{ item.id }}
+
{{ ui.t('table.createdAt') || 'Utworzono' }}: {{ item.createdAt | date:'yyyy-MM-dd HH:mm' }}
+
{{ ui.t('table.updatedAt') || 'Zmieniono' }}: {{ item.updatedAt | date:'yyyy-MM-dd HH:mm' }}
+
+
+
+
+} + +@if (proofPreview()) { + + +} diff --git a/web/src/app/features/expenses/expense-detail.component.ts b/web/src/app/features/expenses/expense-detail.component.ts index de9f923..0cb2792 100644 --- a/web/src/app/features/expenses/expense-detail.component.ts +++ b/web/src/app/features/expenses/expense-detail.component.ts @@ -12,116 +12,7 @@ import type { Expense, Proof } from '../../shared/models'; selector: 'app-expense-detail', standalone: true, imports: [CommonModule, RouterLink, CurrencyPipe, DatePipe], - template: ` - - - @if (loadError()) { -
{{ loadError() }}
- } @else if (loading()) { -
{{ ui.t('common.loading') }}
- } @else if (expense(); as item) { -
-
-
-

{{ item.title }}

-
-
-
{{ ui.t('expenses.field.date') }}
{{ item.expenseDate | date:'yyyy-MM-dd' }}
-
{{ ui.t('expenses.field.amount') }}
{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('expenses.field.status') }}
{{ ui.t('status.' + item.status.toLowerCase()) }}
-
{{ ui.t('expenses.field.category') }}
{{ item.category.name }}
-
{{ ui.t('expenses.field.payment') }}
{{ paymentLabel(item.paymentMethod) }}
-
{{ ui.t('expenses.field.merchantName') }}
{{ item.merchant || ui.t('expenses.noMerchant') }}
-
- - @if (item.description) { -
-
{{ ui.t('expenses.field.description') }}
-
{{ item.description }}
-
- } - - @if (item.tags.length) { -
-
{{ ui.t('expenses.field.tags') }}
-
@for (tag of item.tags; track tag) { #{{ tag }} }
-
- } - - @if (customFieldEntries(item).length) { -
-
{{ ui.t('expenses.field.customFields') }}
-
@for (field of customFieldEntries(item); track field[0]) {
{{ field[0] }}
{{ field[1] }}
}
-
- } -
-
-
- -
-
-

{{ ui.t('expenses.existingProofs') }}

-
- @if (item.proofs.length) { -
- @for (proof of item.proofs; track proof.id) { - - } -
- } @else { -
{{ ui.t('expenses.noProofs') }}
- } -
-
- -
-

{{ ui.t('expenses.meta') }}

-
-
ID: {{ item.id }}
-
{{ ui.t('table.createdAt') || 'Utworzono' }}: {{ item.createdAt | date:'yyyy-MM-dd HH:mm' }}
-
{{ ui.t('table.updatedAt') || 'Zmieniono' }}: {{ item.updatedAt | date:'yyyy-MM-dd HH:mm' }}
-
-
-
-
- } - - @if (proofPreview()) { - - - } - ` + templateUrl: './expense-detail.component.html' }) export class ExpenseDetailComponent implements OnInit { readonly ui = inject(UiService); diff --git a/web/src/app/features/expenses/expense-list.component.html b/web/src/app/features/expenses/expense-list.component.html new file mode 100644 index 0000000..8f7561c --- /dev/null +++ b/web/src/app/features/expenses/expense-list.component.html @@ -0,0 +1,178 @@ + + +
+ +
+ +@if (duplicateGroups().length) { +
+
{{ ui.t('expenses.duplicatesTitle') }}
+
+ @for (group of duplicateGroups().slice(0, 3); track group.source.id) { +
{{ group.source.title }} · {{ group.matches.length }} {{ ui.t('expenses.potentialMatches') }}
+ } +
+
+} + +
+
+

{{ ui.t('expenses.filters') }}

+ @if (hasActiveFilters()) { + {{ ui.t('action.filter') }} + } +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+

{{ ui.t('expenses.listTitle') }}

+
{{ ui.t('expenses.totalItems') }}: {{ pagination().total }}
+
+
+ +
+
+ + @if (selectedIds().length) { +
+
+
{{ ui.t('expenses.selectedCount') }}: {{ selectedIds().length }}
+
+ + + + +
+
+
+ } + +
+ + + + + + + + + + + + + + @for (item of expenses(); track item.id) { + + + + + + + + + + } @empty { } + +
+ + {{ ui.t('table.actions') }}
{{ item.expenseDate | date:'yyyy-MM-dd' }} +
+ {{ item.title }} + @if (item.possibleDuplicate || item.duplicateStatus) { + {{ duplicateLabel(item) }} + } + @if (item.recurringSourceId) { + {{ ui.t('recurring.badge') }} + } +
+
{{ item.merchant || ui.t('expenses.noMerchant') }}
+ @if (item.tags.length) {
@for (tag of item.tags; track tag) { #{{ tag }} }
} + @if (customFieldEntries(item).length) {
@for (field of customFieldEntries(item); track field[0]) { {{ field[0] }}: {{ field[1] }} }
} + @if (item.proofs.length) {
@for (proof of item.proofs; track proof.id) { }
} +
{{ item.category.name }} +
+ {{ ui.t('status.' + item.status.toLowerCase()) }} + +
+
{{ item.amount | currency:'PLN':'symbol':'1.2-2' }} +
+ {{ ui.t('action.view') }} + @if (item.possibleDuplicate && item.duplicateStatus !== 'CONFIRMED') { + + } + @if (item.possibleDuplicate && item.duplicateStatus !== 'DISMISSED') { + + } + @if (item.duplicateStatus === 'DISMISSED' || item.duplicateStatus === 'CONFIRMED') { + + } + + +
+
{{ ui.t('expenses.noItems') }}
+
+ +
+ +@if (proofPreview()) { + + +} diff --git a/web/src/app/features/expenses/expense-list.component.ts b/web/src/app/features/expenses/expense-list.component.ts index 575433e..5bb152e 100644 --- a/web/src/app/features/expenses/expense-list.component.ts +++ b/web/src/app/features/expenses/expense-list.component.ts @@ -43,186 +43,7 @@ const defaultState: ListState = { selector: 'app-expense-list', standalone: true, imports: [CommonModule, ReactiveFormsModule, RouterLink, CurrencyPipe, DatePipe], - template: ` - - -
- -
- - @if (duplicateGroups().length) { -
-
{{ ui.t('expenses.duplicatesTitle') }}
-
- @for (group of duplicateGroups().slice(0, 3); track group.source.id) { -
{{ group.source.title }} · {{ group.matches.length }} {{ ui.t('expenses.potentialMatches') }}
- } -
-
- } - -
-
-

{{ ui.t('expenses.filters') }}

- @if (hasActiveFilters()) { - {{ ui.t('action.filter') }} - } -
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-

{{ ui.t('expenses.listTitle') }}

-
{{ ui.t('expenses.totalItems') }}: {{ pagination().total }}
-
-
- -
-
- - @if (selectedIds().length) { -
-
-
{{ ui.t('expenses.selectedCount') }}: {{ selectedIds().length }}
-
- - - - -
-
-
- } - -
- - - - - - - - - - - - - - @for (item of expenses(); track item.id) { - - - - - - - - - - } @empty { } - -
- - {{ ui.t('table.actions') }}
{{ item.expenseDate | date:'yyyy-MM-dd' }} -
- {{ item.title }} - @if (item.possibleDuplicate || item.duplicateStatus) { - {{ duplicateLabel(item) }} - } - @if (item.recurringSourceId) { - {{ ui.t('recurring.badge') }} - } -
-
{{ item.merchant || ui.t('expenses.noMerchant') }}
- @if (item.tags.length) {
@for (tag of item.tags; track tag) { #{{ tag }} }
} - @if (customFieldEntries(item).length) {
@for (field of customFieldEntries(item); track field[0]) { {{ field[0] }}: {{ field[1] }} }
} - @if (item.proofs.length) {
@for (proof of item.proofs; track proof.id) { }
} -
{{ item.category.name }} -
- {{ ui.t('status.' + item.status.toLowerCase()) }} - -
-
{{ item.amount | currency:'PLN':'symbol':'1.2-2' }} -
- {{ ui.t('action.view') }} - @if (item.possibleDuplicate && item.duplicateStatus !== 'CONFIRMED') { - - } - @if (item.possibleDuplicate && item.duplicateStatus !== 'DISMISSED') { - - } - @if (item.duplicateStatus === 'DISMISSED' || item.duplicateStatus === 'CONFIRMED') { - - } - - -
-
{{ ui.t('expenses.noItems') }}
-
- -
- - @if (proofPreview()) { - - - } - ` + templateUrl: './expense-list.component.html' }) export class ExpenseListComponent implements OnInit { readonly ui = inject(UiService); diff --git a/web/src/app/features/expenses/expenses.component.html b/web/src/app/features/expenses/expenses.component.html new file mode 100644 index 0000000..6d70477 --- /dev/null +++ b/web/src/app/features/expenses/expenses.component.html @@ -0,0 +1,108 @@ + + +
+ +
+ +
+
+
+
+

{{ editingExpenseId() ? ui.t('expenses.edit') : ui.t('expenses.new') }}

+ @if (editingExpenseId()) { + + } +
+
+
+ @if (submitted() && expenseForm.invalid) { +
{{ ui.t('expenses.requiredHint') }}
+ } + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
{{ ui.t('expenses.field.customFields') }}
+
+ @for (group of customFields.controls; track $index) { +
+
+
+
+
+ } @empty { +
{{ ui.t('expenses.noCustomFields') }}
+ } +
+
+ +
+
+
+
+
+
+
+ + @if (editingExpenseId() && editingProofs().length) { +
+
{{ ui.t('expenses.existingProofs') }}
+
+ @for (proof of editingProofs(); track proof.id) { +
+ + +
+ } +
+
+ } + + @if (showCropper()) { +
{{ ui.t('expenses.field.crop') }}
+ } + @if (croppedPreview()) { +
{{ ui.t('expenses.field.cropPreview') }}
+ } + @if (selectedFiles().length) { +
{{ ui.t('expenses.attachmentsSelected') }}
@for (file of selectedFiles(); track file.name + $index) { {{ file.name }} }
+ } +
+ +
+ + +
+
+
+
+
+ +@if (merchantModalOpen()) { + +} + +@if (proofPreview()) { + +} diff --git a/web/src/app/features/expenses/expenses.component.ts b/web/src/app/features/expenses/expenses.component.ts index 01d3ea7..740139f 100644 --- a/web/src/app/features/expenses/expenses.component.ts +++ b/web/src/app/features/expenses/expenses.component.ts @@ -24,116 +24,7 @@ const today = formatLocalDate(new Date()); selector: 'app-expenses', standalone: true, imports: [CommonModule, ReactiveFormsModule, RouterLink, ImageCropperComponent], - template: ` - - -
- -
- -
-
-
-
-

{{ editingExpenseId() ? ui.t('expenses.edit') : ui.t('expenses.new') }}

- @if (editingExpenseId()) { - - } -
-
-
- @if (submitted() && expenseForm.invalid) { -
{{ ui.t('expenses.requiredHint') }}
- } - -
-
-
-
-
-
-
-
-
-
-
-
- -
-
{{ ui.t('expenses.field.customFields') }}
-
- @for (group of customFields.controls; track $index) { -
-
-
-
-
- } @empty { -
{{ ui.t('expenses.noCustomFields') }}
- } -
-
- -
-
-
-
-
-
-
- - @if (editingExpenseId() && editingProofs().length) { -
-
{{ ui.t('expenses.existingProofs') }}
-
- @for (proof of editingProofs(); track proof.id) { -
- - -
- } -
-
- } - - @if (showCropper()) { -
{{ ui.t('expenses.field.crop') }}
- } - @if (croppedPreview()) { -
{{ ui.t('expenses.field.cropPreview') }}
- } - @if (selectedFiles().length) { -
{{ ui.t('expenses.attachmentsSelected') }}
@for (file of selectedFiles(); track file.name + $index) { {{ file.name }} }
- } -
- -
- - -
-
-
-
-
- - @if (merchantModalOpen()) { - - } - - @if (proofPreview()) { - - } - ` + templateUrl: './expenses.component.html' }) export class ExpensesComponent implements OnInit { readonly ui = inject(UiService); diff --git a/web/src/app/features/integrations/integrations.component.html b/web/src/app/features/integrations/integrations.component.html new file mode 100644 index 0000000..b897536 --- /dev/null +++ b/web/src/app/features/integrations/integrations.component.html @@ -0,0 +1,213 @@ + + +
+
+
+
+

{{ ui.t('integrations.shoppingList') }}

+ {{ ui.t('integrations.projectLink') }} +
+
+
+
{{ ui.t('integrations.selfHostedTitle') }}
+
{{ ui.t('integrations.selfHostedHint') }}
+
+
+ +
+ + +
+
+ + +
{{ ui.t('integrations.keepToken') }}
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ +
+
+

{{ ui.t('integrations.history') }}

+
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+ +
+
{{ ui.t('integrations.externalLists') }}
{{ summaryListCount() }}
+
{{ ui.t('integrations.externalSpend') }}
{{ summaryAmount() | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('integrations.externalCount') }}
{{ summaryCount() }}
+
+ +
+ @if (configured()) { +
+
+
{{ ui.t('integrations.summary') }}
+
{{ historyForm.controls.period.value }}
+
+
+ {{ ui.t('integrations.summaryLists') }}: {{ summaryListCount() }} · + {{ ui.t('integrations.summarySpend') }}: {{ summaryAmount() | number:'1.2-2' }} PLN +
+
+ } @else { +
{{ ui.t('integrations.notConfigured') }}
+ } +
+
+
+
+
+ +
+
+
+
+

{{ ui.t('integrations.lists') }}

+ {{ visibleLists().length }} +
+
+ @for (item of visibleLists(); track item.id) { + + } @empty { +
{{ ui.t('common.noData') }}
+ } +
+
+
+ +
+
+

{{ ui.t('integrations.importTitle') }}

+
+
+
{{ ui.t('integrations.importExplainTitle') }}
+
{{ ui.t('integrations.importExplainBodySimple') }}
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
{{ ui.t('integrations.importMonthTitle') }}
+
{{ ui.t('integrations.importMonthHint') }}
+ +
+
+
+
+
{{ ui.t('integrations.importListTitle') }}
+
{{ selectedList() ? listTitle(selectedList()) : ui.t('integrations.selectListHintSimple') }}
+ +
+
+
+ + @if (selectedList()) { +
+
+
+
{{ listTitle(selectedList()) }}
+
{{ listCreatedAt(selectedList()) | date:'yyyy-MM-dd' }} · {{ listOwner(selectedList()) || ui.t('common.none') }}
+
+
+
{{ ui.t('integrations.selectedListSummary') }}
+
{{ selectedListCount() }} / {{ selectedListTotal() | currency:'PLN':'symbol':'1.2-2' }}
+
+
+ +
+ + + + @for (item of selectedListExpenses(); track $index) { + + + + + + } @empty { + + } + +
{{ ui.t('table.title') }}{{ ui.t('table.date') }}{{ ui.t('table.amount') }}
{{ itemTitle(item) }}{{ itemDate(item) }}{{ itemAmount(item) | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
+
+
+ } +
+
+
+
diff --git a/web/src/app/features/integrations/integrations.component.ts b/web/src/app/features/integrations/integrations.component.ts index e948e87..94f3d04 100644 --- a/web/src/app/features/integrations/integrations.component.ts +++ b/web/src/app/features/integrations/integrations.component.ts @@ -22,221 +22,7 @@ const monthRange = (period: string) => { selector: 'app-integrations', standalone: true, imports: [CommonModule, ReactiveFormsModule, CurrencyPipe, DatePipe], - template: ` - - -
-
-
-
-

{{ ui.t('integrations.shoppingList') }}

- {{ ui.t('integrations.projectLink') }} -
-
-
-
{{ ui.t('integrations.selfHostedTitle') }}
-
{{ ui.t('integrations.selfHostedHint') }}
-
-
- -
- - -
-
- - -
{{ ui.t('integrations.keepToken') }}
-
-
- - -
-
-
- - -
-
- - -
-
-
- - -
-
-
-
-
- -
-
-

{{ ui.t('integrations.history') }}

-
-
-
- - -
-
- - -
-
-
- -
-
-
- -
-
{{ ui.t('integrations.externalLists') }}
{{ summaryListCount() }}
-
{{ ui.t('integrations.externalSpend') }}
{{ summaryAmount() | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('integrations.externalCount') }}
{{ summaryCount() }}
-
- -
- @if (configured()) { -
-
-
{{ ui.t('integrations.summary') }}
-
{{ historyForm.controls.period.value }}
-
-
- {{ ui.t('integrations.summaryLists') }}: {{ summaryListCount() }} · - {{ ui.t('integrations.summarySpend') }}: {{ summaryAmount() | number:'1.2-2' }} PLN -
-
- } @else { -
{{ ui.t('integrations.notConfigured') }}
- } -
-
-
-
-
- -
-
-
-
-

{{ ui.t('integrations.lists') }}

- {{ visibleLists().length }} -
-
- @for (item of visibleLists(); track item.id) { - - } @empty { -
{{ ui.t('common.noData') }}
- } -
-
-
- -
-
-

{{ ui.t('integrations.importTitle') }}

-
-
-
{{ ui.t('integrations.importExplainTitle') }}
-
{{ ui.t('integrations.importExplainBodySimple') }}
-
- -
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
{{ ui.t('integrations.importMonthTitle') }}
-
{{ ui.t('integrations.importMonthHint') }}
- -
-
-
-
-
{{ ui.t('integrations.importListTitle') }}
-
{{ selectedList() ? listTitle(selectedList()) : ui.t('integrations.selectListHintSimple') }}
- -
-
-
- - @if (selectedList()) { -
-
-
-
{{ listTitle(selectedList()) }}
-
{{ listCreatedAt(selectedList()) | date:'yyyy-MM-dd' }} · {{ listOwner(selectedList()) || ui.t('common.none') }}
-
-
-
{{ ui.t('integrations.selectedListSummary') }}
-
{{ selectedListCount() }} / {{ selectedListTotal() | currency:'PLN':'symbol':'1.2-2' }}
-
-
- -
- - - - @for (item of selectedListExpenses(); track $index) { - - - - - - } @empty { - - } - -
{{ ui.t('table.title') }}{{ ui.t('table.date') }}{{ ui.t('table.amount') }}
{{ itemTitle(item) }}{{ itemDate(item) }}{{ itemAmount(item) | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
-
-
- } -
-
-
-
- ` + templateUrl: './integrations.component.html' }) export class IntegrationsComponent implements OnInit { readonly ui = inject(UiService); diff --git a/web/src/app/features/merchants/merchants.component.html b/web/src/app/features/merchants/merchants.component.html new file mode 100644 index 0000000..99e19c0 --- /dev/null +++ b/web/src/app/features/merchants/merchants.component.html @@ -0,0 +1,90 @@ + + +
+
+ + + + + + @for (item of items(); track item.id) { + + + + + + + + } @empty { + + } + +
{{ ui.t('merchant.name') }}{{ ui.t('merchant.type') }}{{ ui.t('admin.status') }}{{ ui.t('merchant.notes') }}
{{ item.name }}{{ labelKind(item.kind) }}{{ item.isActive ? ui.t('common.active') : ui.t('common.hidden') }}{{ item.notes || ui.t('common.none') }} +
+ + +
+
{{ ui.t('merchant.noneSaved') }}
+
+
+ +@if (modalOpen()) { + + +} diff --git a/web/src/app/features/merchants/merchants.component.ts b/web/src/app/features/merchants/merchants.component.ts index ebc2a74..ca6ad46 100644 --- a/web/src/app/features/merchants/merchants.component.ts +++ b/web/src/app/features/merchants/merchants.component.ts @@ -10,98 +10,7 @@ import type { Merchant } from '../../shared/models'; selector: 'app-merchants', standalone: true, imports: [CommonModule, ReactiveFormsModule], - template: ` - - -
-
- - - - - - @for (item of items(); track item.id) { - - - - - - - - } @empty { - - } - -
{{ ui.t('merchant.name') }}{{ ui.t('merchant.type') }}{{ ui.t('admin.status') }}{{ ui.t('merchant.notes') }}
{{ item.name }}{{ labelKind(item.kind) }}{{ item.isActive ? ui.t('common.active') : ui.t('common.hidden') }}{{ item.notes || ui.t('common.none') }} -
- - -
-
{{ ui.t('merchant.noneSaved') }}
-
-
- - @if (modalOpen()) { - - - } - ` + templateUrl: './merchants.component.html' }) export class MerchantsComponent implements OnInit { readonly ui = inject(UiService); diff --git a/web/src/app/features/recurring/recurring.component.html b/web/src/app/features/recurring/recurring.component.html new file mode 100644 index 0000000..efeb905 --- /dev/null +++ b/web/src/app/features/recurring/recurring.component.html @@ -0,0 +1,84 @@ + + +
+
+
+
+

{{ editingId() ? ui.t('recurring.edit') : ui.t('recurring.new') }}

+
+ @if (editingId()) { } + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
{{ ui.t('expenses.field.customFields') }}
+ @for (group of customFields.controls; track $index) { +
+
+
+
+
+ } @empty { +
{{ ui.t('expenses.noCustomFields') }}
+ } +
+ + + +
+
+
+
+ +
+
+

{{ ui.t('recurring.title') }}

+
+ + + + @for (item of items(); track item.id) { + + + + + + + + } @empty { } + +
{{ ui.t('table.title') }}{{ ui.t('recurring.frequency') }}{{ ui.t('recurring.nextRunDate') }}{{ ui.t('table.amount') }}
+
{{ item.title }}
+
{{ item.category.name }} · {{ item.merchant || ui.t('expenses.noMerchant') }}
+
{{ ui.t('recurring.generatedCount') }}: {{ item.generatedCount }} · {{ ui.t('recurring.endDate') }}: {{ item.endDate || ui.t('common.none') }}
+
@for (tag of item.tags; track tag) { #{{ tag }} }
+
{{ ui.t('recurring.' + item.frequency.toLowerCase()) }}{{ item.nextRunDate | date:'yyyy-MM-dd' }}{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
+
+
+
+
diff --git a/web/src/app/features/recurring/recurring.component.ts b/web/src/app/features/recurring/recurring.component.ts index e719c46..b802678 100644 --- a/web/src/app/features/recurring/recurring.component.ts +++ b/web/src/app/features/recurring/recurring.component.ts @@ -13,92 +13,7 @@ const today = () => new Date().toISOString().slice(0, 10); selector: 'app-recurring', standalone: true, imports: [CommonModule, ReactiveFormsModule, CurrencyPipe, DatePipe], - template: ` - - -
-
-
-
-

{{ editingId() ? ui.t('recurring.edit') : ui.t('recurring.new') }}

-
- @if (editingId()) { } - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
{{ ui.t('expenses.field.customFields') }}
- @for (group of customFields.controls; track $index) { -
-
-
-
-
- } @empty { -
{{ ui.t('expenses.noCustomFields') }}
- } -
- - - -
-
-
-
- -
-
-

{{ ui.t('recurring.title') }}

-
- - - - @for (item of items(); track item.id) { - - - - - - - - } @empty { } - -
{{ ui.t('table.title') }}{{ ui.t('recurring.frequency') }}{{ ui.t('recurring.nextRunDate') }}{{ ui.t('table.amount') }}
-
{{ item.title }}
-
{{ item.category.name }} · {{ item.merchant || ui.t('expenses.noMerchant') }}
-
{{ ui.t('recurring.generatedCount') }}: {{ item.generatedCount }} · {{ ui.t('recurring.endDate') }}: {{ item.endDate || ui.t('common.none') }}
-
@for (tag of item.tags; track tag) { #{{ tag }} }
-
{{ ui.t('recurring.' + item.frequency.toLowerCase()) }}{{ item.nextRunDate | date:'yyyy-MM-dd' }}{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
-
-
-
-
- ` + templateUrl: './recurring.component.html' }) export class RecurringComponent implements OnInit { readonly ui = inject(UiService); diff --git a/web/src/app/features/reports/reports.component.html b/web/src/app/features/reports/reports.component.html new file mode 100644 index 0000000..9156a6c --- /dev/null +++ b/web/src/app/features/reports/reports.component.html @@ -0,0 +1,51 @@ + + +
+
+
+

{{ ui.t('reports.emailTitle') }}

+
+
+ +
+
+
+
+
+
+
+
+ +
+

{{ ui.t('reports.exportTitle') }}

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+

{{ ui.t('reports.preview') }}

+
+ @if (summary()) { +
+
{{ ui.t('stats.sum') }}
{{ summary()!.total.toFixed(2) }}
+
{{ ui.t('dashboard.count') }}
{{ summary()!.count }}
+
{{ ui.t('stats.average') }}
{{ summary()!.average.toFixed(2) }}
+
+ } +
+
+
+
+
diff --git a/web/src/app/features/reports/reports.component.ts b/web/src/app/features/reports/reports.component.ts index 1fb43f8..f5b94d6 100644 --- a/web/src/app/features/reports/reports.component.ts +++ b/web/src/app/features/reports/reports.component.ts @@ -12,59 +12,7 @@ import { CategoryPickerComponent } from '../../shared/ui/category-picker.compone selector: 'app-reports', standalone: true, imports: [CommonModule, ReactiveFormsModule, CategoryPickerComponent], - template: ` - - -
-
-
-

{{ ui.t('reports.emailTitle') }}

-
-
- -
-
-
-
-
-
-
-
- -
-

{{ ui.t('reports.exportTitle') }}

-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-

{{ ui.t('reports.preview') }}

-
- @if (summary()) { -
-
{{ ui.t('stats.sum') }}
{{ summary()!.total.toFixed(2) }}
-
{{ ui.t('dashboard.count') }}
{{ summary()!.count }}
-
{{ ui.t('stats.average') }}
{{ summary()!.average.toFixed(2) }}
-
- } -
-
-
-
-
- ` + templateUrl: './reports.component.html' }) export class ReportsComponent implements OnInit { readonly ui = inject(UiService); diff --git a/web/src/app/features/stats/stats.component.html b/web/src/app/features/stats/stats.component.html new file mode 100644 index 0000000..5434713 --- /dev/null +++ b/web/src/app/features/stats/stats.component.html @@ -0,0 +1,23 @@ + + +
+
+
+
+
+
+
+
+
+
+ +
{{ ui.t('stats.sum') }}
{{ (stats()?.total || 0) | currency:'PLN':'symbol':'1.2-2' }}
+
{{ ui.t('dashboard.count') }}
{{ stats()?.count || 0 }}
+
{{ ui.t('stats.average') }}
{{ (stats()?.average || 0) | currency:'PLN':'symbol':'1.2-2' }}
+ +

{{ ui.t('stats.share') }}

@if (hasCategoryData()) {
} @else {
{{ ui.t('stats.noCategoryChart') }}
}
+

{{ ui.t('stats.trend') }}

@if (hasTimelineData()) {
} @else {
{{ ui.t('stats.noTrendChart') }}
}
+ +

{{ ui.t('stats.breakdown') }}

@for (row of stats()?.byCategory || []; track row.categoryId) { } @empty { }
{{ ui.t('table.category') }}{{ ui.t('table.amount') }}{{ ui.t('table.count') }}
{{ row.categoryName }}{{ row.total | currency:'PLN':'symbol':'1.2-2' }}{{ row.count }}
{{ ui.t('common.noData') }}
+

{{ ui.t('stats.tags') }}

@for (row of stats()?.byTag || []; track row.tag) { } @empty { }
{{ ui.t('expenses.field.tags') }}{{ ui.t('table.amount') }}
#{{ row.tag }}{{ row.total | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
+
diff --git a/web/src/app/features/stats/stats.component.ts b/web/src/app/features/stats/stats.component.ts index e696add..bc29b47 100644 --- a/web/src/app/features/stats/stats.component.ts +++ b/web/src/app/features/stats/stats.component.ts @@ -15,31 +15,7 @@ const chartPalette = ['#206bc4', '#2fb344', '#f59f00', '#d63939', '#9b4dca', '#4 selector: 'app-stats', standalone: true, imports: [CommonModule, ReactiveFormsModule, CurrencyPipe, CategoryPickerComponent], - template: ` - - -
-
-
-
-
-
-
-
-
-
- -
{{ ui.t('stats.sum') }}
{{ (stats()?.total || 0) | currency:'PLN':'symbol':'1.2-2' }}
-
{{ ui.t('dashboard.count') }}
{{ stats()?.count || 0 }}
-
{{ ui.t('stats.average') }}
{{ (stats()?.average || 0) | currency:'PLN':'symbol':'1.2-2' }}
- -

{{ ui.t('stats.share') }}

@if (hasCategoryData()) {
} @else {
{{ ui.t('stats.noCategoryChart') }}
}
-

{{ ui.t('stats.trend') }}

@if (hasTimelineData()) {
} @else {
{{ ui.t('stats.noTrendChart') }}
}
- -

{{ ui.t('stats.breakdown') }}

@for (row of stats()?.byCategory || []; track row.categoryId) { } @empty { }
{{ ui.t('table.category') }}{{ ui.t('table.amount') }}{{ ui.t('table.count') }}
{{ row.categoryName }}{{ row.total | currency:'PLN':'symbol':'1.2-2' }}{{ row.count }}
{{ ui.t('common.noData') }}
-

{{ ui.t('stats.tags') }}

@for (row of stats()?.byTag || []; track row.tag) { } @empty { }
{{ ui.t('expenses.field.tags') }}{{ ui.t('table.amount') }}
#{{ row.tag }}{{ row.total | currency:'PLN':'symbol':'1.2-2' }}
{{ ui.t('common.noData') }}
-
- ` + templateUrl: './stats.component.html' }) export class StatsComponent implements OnInit, OnDestroy { readonly ui = inject(UiService); diff --git a/web/src/app/layout/shell.component.html b/web/src/app/layout/shell.component.html new file mode 100644 index 0000000..776d48d --- /dev/null +++ b/web/src/app/layout/shell.component.html @@ -0,0 +1,73 @@ +
+ + +
+
+
+
+
{{ ui.t('nav.navigation') }}
+ +
+
+
+
+ +
+
+
+
+ +
+
diff --git a/web/src/app/layout/shell.component.ts b/web/src/app/layout/shell.component.ts index a633dae..edcf847 100644 --- a/web/src/app/layout/shell.component.ts +++ b/web/src/app/layout/shell.component.ts @@ -12,81 +12,7 @@ import { UiService } from '../core/services/ui.service'; selector: 'app-shell', standalone: true, imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive], - template: ` -
- - -
-
-
-
-
{{ ui.t('nav.navigation') }}
- -
-
-
-
- -
-
-
-
- -
-
- ` + templateUrl: './shell.component.html' }) export class ShellComponent { readonly auth = inject(AuthService); diff --git a/web/src/app/shared/ui/category-picker.component.html b/web/src/app/shared/ui/category-picker.component.html new file mode 100644 index 0000000..ad8ac8d --- /dev/null +++ b/web/src/app/shared/ui/category-picker.component.html @@ -0,0 +1,38 @@ + diff --git a/web/src/app/shared/ui/category-picker.component.ts b/web/src/app/shared/ui/category-picker.component.ts index effa642..51dd80d 100644 --- a/web/src/app/shared/ui/category-picker.component.ts +++ b/web/src/app/shared/ui/category-picker.component.ts @@ -7,46 +7,7 @@ import type { Category } from '../models'; selector: 'app-category-picker', standalone: true, imports: [CommonModule], - template: ` - - ` + templateUrl: './category-picker.component.html' }) export class CategoryPickerComponent { readonly ui = inject(UiService); diff --git a/web/src/app/shared/ui/toast-outlet.component.html b/web/src/app/shared/ui/toast-outlet.component.html new file mode 100644 index 0000000..016cd26 --- /dev/null +++ b/web/src/app/shared/ui/toast-outlet.component.html @@ -0,0 +1,19 @@ +
+ @for (item of toast.items(); track item.id) { + + } +
diff --git a/web/src/app/shared/ui/toast-outlet.component.ts b/web/src/app/shared/ui/toast-outlet.component.ts index 32206c4..e979a91 100644 --- a/web/src/app/shared/ui/toast-outlet.component.ts +++ b/web/src/app/shared/ui/toast-outlet.component.ts @@ -6,27 +6,7 @@ import { ToastItem, ToastService } from '../../core/services/toast.service'; selector: 'app-toast-outlet', standalone: true, imports: [CommonModule], - template: ` -
- @for (item of toast.items(); track item.id) { - - } -
- ` + templateUrl: './toast-outlet.component.html' }) export class ToastOutletComponent { readonly toast = inject(ToastService);