split html's
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user