split html's

This commit is contained in:
Mateusz Gruszczyński
2026-04-08 11:08:41 +02:00
parent f0f20e416e
commit 57cc30427a
38 changed files with 1625 additions and 1643 deletions

View File

@@ -43,186 +43,7 @@ const defaultState: ListState = {
selector: 'app-expense-list',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterLink, CurrencyPipe, DatePipe],
template: `
<div class="page-header d-print-none mb-3 ec-page-header">
<div class="row align-items-center g-3">
<div class="col">
<h2 class="page-title mb-1">{{ ui.t('expenses.title') }}</h2>
<div class="text-secondary">{{ ui.t('expenses.listSubtitle') }}</div>
</div>
</div>
</div>
<div class="mb-3">
<nav class="nav nav-pills gap-2">
<a class="nav-link" [routerLink]="['/expenses/add']">{{ ui.t('action.addExpense') }}</a>
<a class="nav-link active" [routerLink]="['/expenses/list']">{{ ui.t('expenses.listTitle') }}</a>
</nav>
</div>
@if (duplicateGroups().length) {
<div class="alert alert-warning">
<div class="fw-semibold mb-2">{{ ui.t('expenses.duplicatesTitle') }}</div>
<div class="d-grid gap-1">
@for (group of duplicateGroups().slice(0, 3); track group.source.id) {
<div>{{ group.source.title }} · {{ group.matches.length }} {{ ui.t('expenses.potentialMatches') }}</div>
}
</div>
</div>
}
<div class="card overflow-hidden mb-3">
<div class="card-header d-flex justify-content-between align-items-center gap-2 flex-wrap">
<h3 class="card-title mb-0">{{ ui.t('expenses.filters') }}</h3>
@if (hasActiveFilters()) {
<span class="badge text-bg-primary">{{ ui.t('action.filter') }}</span>
}
</div>
<div class="card-body">
<form [formGroup]="filterForm" (ngSubmit)="applyFilters()" class="row g-3 align-items-end">
<div class="col-sm-6 col-lg-2"><label class="form-label">{{ ui.t('stats.from') }}</label><input class="form-control" type="date" formControlName="startDate" /></div>
<div class="col-sm-6 col-lg-2"><label class="form-label">{{ ui.t('stats.to') }}</label><input class="form-control" type="date" formControlName="endDate" /></div>
<div class="col-sm-6 col-lg-2"><label class="form-label">{{ ui.t('expenses.field.category') }}</label><select class="form-select" formControlName="categoryId"><option value="">{{ ui.t('expenses.allCategories') }}</option>@for (category of categories(); track category.id) { <option [value]="category.id">{{ category.name }}</option> }</select></div>
<div class="col-sm-6 col-lg-2"><label class="form-label">{{ ui.t('expenses.field.status') }}</label><select class="form-select" formControlName="status"><option value="">{{ ui.t('common.none') }}</option><option value="DRAFT">{{ ui.t('status.draft') }}</option><option value="PENDING">{{ ui.t('status.pending') }}</option><option value="APPROVED">{{ ui.t('status.approved') }}</option><option value="REJECTED">{{ ui.t('status.rejected') }}</option></select></div>
<div class="col-sm-6 col-lg-2"><label class="form-label">{{ ui.t('expenses.field.tags') }}</label><input class="form-control" formControlName="tags" /></div>
<div class="col-sm-6 col-lg-2"><label class="form-label">{{ ui.t('expenses.search') }}</label><input class="form-control" formControlName="search" /></div>
<div class="col-12"><label class="form-check"><input class="form-check-input" type="checkbox" formControlName="duplicatesOnly" /><span class="form-check-label">{{ ui.t('expenses.duplicatesOnly') }}</span></label></div>
<div class="col-12 d-flex gap-2 flex-wrap"><button class="btn btn-primary" type="submit">{{ ui.t('action.filter') }}</button><button class="btn btn-outline-secondary" type="button" (click)="resetFilters()">{{ ui.t('action.reset') }}</button></div>
</form>
</div>
</div>
<div class="card overflow-hidden">
<div class="card-header d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div>
<h3 class="card-title mb-0">{{ ui.t('expenses.listTitle') }}</h3>
<div class="small text-secondary">{{ ui.t('expenses.totalItems') }}: {{ pagination().total }}</div>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<select class="form-select form-select-sm w-auto" [value]="pagination().pageSize" (change)="changePageSize($any($event.target).value)">
@for (size of pageSizeOptions; track size) {
<option [value]="size">{{ size }} / {{ ui.t('expenses.perPage') }}</option>
}
</select>
</div>
</div>
@if (selectedIds().length) {
<div class="card-body border-bottom bg-body-tertiary py-3">
<div class="d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div class="fw-semibold">{{ ui.t('expenses.selectedCount') }}: {{ selectedIds().length }}</div>
<div class="btn-list flex-wrap">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="clearSelection()">{{ ui.t('action.clearSelection') }}</button>
<button class="btn btn-sm btn-outline-warning" type="button" (click)="bulkUpdateStatus('PENDING')">{{ ui.t('status.pending') }}</button>
<button class="btn btn-sm btn-outline-success" type="button" (click)="bulkUpdateStatus('APPROVED')">{{ ui.t('status.approved') }}</button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="bulkDelete()">{{ ui.t('action.delete') }}</button>
</div>
</div>
</div>
}
<div class="table-responsive">
<table class="table table-vcenter card-table mb-0">
<thead>
<tr>
<th class="w-1">
<input class="form-check-input" type="checkbox" [checked]="allVisibleSelected()" [indeterminate]="someVisibleSelected()" (change)="toggleAllVisible($any($event.target).checked)" />
</th>
<th><button class="btn btn-link p-0 text-decoration-none fw-semibold" type="button" (click)="setSort('expenseDate')">{{ ui.t('expenses.field.date') }} {{ sortIndicator('expenseDate') }}</button></th>
<th><button class="btn btn-link p-0 text-decoration-none fw-semibold" type="button" (click)="setSort('title')">{{ ui.t('table.title') }} {{ sortIndicator('title') }}</button></th>
<th><button class="btn btn-link p-0 text-decoration-none fw-semibold" type="button" (click)="setSort('category')">{{ ui.t('expenses.field.category') }} {{ sortIndicator('category') }}</button></th>
<th><button class="btn btn-link p-0 text-decoration-none fw-semibold" type="button" (click)="setSort('status')">{{ ui.t('expenses.field.status') }} {{ sortIndicator('status') }}</button></th>
<th class="text-end"><button class="btn btn-link p-0 text-decoration-none fw-semibold" type="button" (click)="setSort('amount')">{{ ui.t('table.amount') }} {{ sortIndicator('amount') }}</button></th>
<th class="text-end">{{ ui.t('table.actions') }}</th>
</tr>
</thead>
<tbody>
@for (item of expenses(); track item.id) {
<tr>
<td><input class="form-check-input" type="checkbox" [checked]="isSelected(item.id)" (change)="toggleSelection(item.id, $any($event.target).checked)" /></td>
<td>{{ item.expenseDate | date:'yyyy-MM-dd' }}</td>
<td>
<div class="fw-semibold d-flex align-items-center gap-2 flex-wrap">
<a class="link-body-emphasis text-decoration-none" [routerLink]="['/expenses', item.id]">{{ item.title }}</a>
@if (item.possibleDuplicate || item.duplicateStatus) {
<span class="badge" [ngClass]="duplicateBadgeClass(item)">{{ duplicateLabel(item) }}</span>
}
@if (item.recurringSourceId) {
<span class="badge text-bg-info">{{ ui.t('recurring.badge') }}</span>
}
</div>
<div class="text-secondary small">{{ item.merchant || ui.t('expenses.noMerchant') }}</div>
@if (item.tags.length) { <div class="mt-1 d-flex flex-wrap gap-1">@for (tag of item.tags; track tag) { <span class="badge text-bg-secondary">#{{ tag }}</span> }</div> }
@if (customFieldEntries(item).length) { <div class="small text-secondary mt-1">@for (field of customFieldEntries(item); track field[0]) { <span class="me-2">{{ field[0] }}: {{ field[1] }}</span> }</div> }
@if (item.proofs.length) { <div class="mt-2 d-flex flex-wrap gap-2">@for (proof of item.proofs; track proof.id) { <button class="btn btn-sm btn-outline-secondary" type="button" (click)="openProof(proof)">{{ proof.label || proof.originalName || ui.t('expenses.proof') }}</button> }</div> }
</td>
<td>{{ item.category.name }}</td>
<td>
<div class="d-grid gap-2">
<span class="badge d-inline-flex align-items-center justify-content-center" [ngClass]="statusBadgeClass(item.status)">{{ ui.t('status.' + item.status.toLowerCase()) }}</span>
<select class="form-select form-select-sm" [value]="item.status" [disabled]="statusSavingId() === item.id" (change)="quickChangeStatus(item, $any($event.target).value)">
<option value="DRAFT">{{ ui.t('status.draft') }}</option>
<option value="PENDING">{{ ui.t('status.pending') }}</option>
<option value="APPROVED">{{ ui.t('status.approved') }}</option>
<option value="REJECTED">{{ ui.t('status.rejected') }}</option>
</select>
</div>
</td>
<td class="text-end">{{ item.amount | currency:'PLN':'symbol':'1.2-2' }}</td>
<td class="text-end">
<div class="btn-list justify-content-end flex-wrap">
<a class="btn btn-sm btn-outline-secondary" [routerLink]="['/expenses', item.id]">{{ ui.t('action.view') }}</a>
@if (item.possibleDuplicate && item.duplicateStatus !== 'CONFIRMED') {
<button class="btn btn-sm btn-outline-success" type="button" (click)="reviewDuplicate(item, 'CONFIRM')">OK</button>
}
@if (item.possibleDuplicate && item.duplicateStatus !== 'DISMISSED') {
<button class="btn btn-sm btn-outline-warning" type="button" (click)="reviewDuplicate(item, 'DISMISS')">X</button>
}
@if (item.duplicateStatus === 'DISMISSED' || item.duplicateStatus === 'CONFIRMED') {
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="reviewDuplicate(item, 'REOPEN')">↺</button>
}
<button class="btn btn-sm btn-outline-primary" type="button" (click)="startEdit(item)">{{ ui.t('action.edit') }}</button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="removeExpense(item)">{{ ui.t('action.delete') }}</button>
</div>
</td>
</tr>
} @empty { <tr><td colspan="7" class="text-secondary">{{ ui.t('expenses.noItems') }}</td></tr> }
</tbody>
</table>
</div>
<div class="card-footer d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div class="small text-secondary">{{ pageStart() }}-{{ pageEnd() }} / {{ pagination().total }}</div>
<div class="btn-group">
<button class="btn btn-outline-secondary btn-sm" type="button" [disabled]="!pagination().hasPrev" (click)="changePage(1)">«</button>
<button class="btn btn-outline-secondary btn-sm" type="button" [disabled]="!pagination().hasPrev" (click)="changePage(pagination().page - 1)"></button>
<button class="btn btn-outline-secondary btn-sm" type="button" disabled>{{ pagination().page }} / {{ pagination().totalPages }}</button>
<button class="btn btn-outline-secondary btn-sm" type="button" [disabled]="!pagination().hasNext" (click)="changePage(pagination().page + 1)"></button>
<button class="btn btn-outline-secondary btn-sm" type="button" [disabled]="!pagination().hasNext" (click)="changePage(pagination().totalPages)">»</button>
</div>
</div>
</div>
@if (proofPreview()) {
<div class="modal modal-blur fade show d-block" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ proofPreview()?.label || proofPreview()?.originalName || ui.t('expenses.proof') }}</h5>
<button class="btn-close ec-modal-close" type="button" (click)="closeProofPreview()"></button>
</div>
<div class="modal-body ec-proof-modal-body">
@if (isPdf(proofPreview()!)) {
<iframe class="ec-proof-frame" [src]="proofPreviewUrl()"></iframe>
} @else {
<img class="img-fluid ec-proof-preview" [src]="proofPreview()?.previewUrl || proofPreview()?.fileUrl" [alt]="ui.t('expenses.proof')" />
}
</div>
</div>
</div>
</div>
<div class="modal-backdrop fade show" (click)="closeProofPreview()"></div>
}
`
templateUrl: './expense-list.component.html'
})
export class ExpenseListComponent implements OnInit {
readonly ui = inject(UiService);