mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-12-01 02:21:45 +00:00
Allow account grouping
This commit is contained in:
@@ -27,6 +27,7 @@ namespace FireflyIII\Repositories\UserGroups\Account;
|
|||||||
use FireflyIII\Models\Account;
|
use FireflyIII\Models\Account;
|
||||||
use FireflyIII\Models\AccountMeta;
|
use FireflyIII\Models\AccountMeta;
|
||||||
use FireflyIII\Models\AccountType;
|
use FireflyIII\Models\AccountType;
|
||||||
|
use FireflyIII\Models\ObjectGroup;
|
||||||
use FireflyIII\Models\Transaction;
|
use FireflyIII\Models\Transaction;
|
||||||
use FireflyIII\Models\TransactionCurrency;
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
use FireflyIII\Services\Internal\Update\AccountUpdateService;
|
use FireflyIII\Services\Internal\Update\AccountUpdateService;
|
||||||
@@ -65,8 +66,7 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
$q1->where('account_meta.name', '=', 'account_number');
|
$q1->where('account_meta.name', '=', 'account_number');
|
||||||
$q1->where('account_meta.data', '=', $json);
|
$q1->where('account_meta.data', '=', $json);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
;
|
|
||||||
|
|
||||||
if (0 !== count($types)) {
|
if (0 !== count($types)) {
|
||||||
$dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
|
$dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
|
||||||
@@ -92,7 +92,7 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
|
|
||||||
public function findByName(string $name, array $types): ?Account
|
public function findByName(string $name, array $types): ?Account
|
||||||
{
|
{
|
||||||
$query = $this->userGroup->accounts();
|
$query = $this->userGroup->accounts();
|
||||||
|
|
||||||
if (0 !== count($types)) {
|
if (0 !== count($types)) {
|
||||||
$query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
|
$query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
|
||||||
@@ -116,8 +116,8 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
|
|
||||||
public function getAccountCurrency(Account $account): ?TransactionCurrency
|
public function getAccountCurrency(Account $account): ?TransactionCurrency
|
||||||
{
|
{
|
||||||
$type = $account->accountType->type;
|
$type = $account->accountType->type;
|
||||||
$list = config('firefly.valid_currency_account_types');
|
$list = config('firefly.valid_currency_account_types');
|
||||||
|
|
||||||
// return null if not in this list.
|
// return null if not in this list.
|
||||||
if (!in_array($type, $list, true)) {
|
if (!in_array($type, $list, true)) {
|
||||||
@@ -242,9 +242,9 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
|
|
||||||
public function getAccountsByType(array $types, ?array $sort = [], ?array $filters = []): Collection
|
public function getAccountsByType(array $types, ?array $sort = [], ?array $filters = []): Collection
|
||||||
{
|
{
|
||||||
$sortable = ['name', 'active']; // TODO yes this is a duplicate array.
|
$sortable = ['name', 'active']; // TODO yes this is a duplicate array.
|
||||||
$res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types);
|
$res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types);
|
||||||
$query = $this->userGroup->accounts();
|
$query = $this->userGroup->accounts();
|
||||||
if (0 !== count($types)) {
|
if (0 !== count($types)) {
|
||||||
$query->accountTypeIn($types);
|
$query->accountTypeIn($types);
|
||||||
}
|
}
|
||||||
@@ -252,15 +252,15 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
// process filters
|
// process filters
|
||||||
// TODO this should be repeatable, it feels like a hack when you do it here.
|
// TODO this should be repeatable, it feels like a hack when you do it here.
|
||||||
// TODO some fields cannot be filtered using the query, and a second filter must be applied on the collection.
|
// TODO some fields cannot be filtered using the query, and a second filter must be applied on the collection.
|
||||||
foreach($filters as $column => $value) {
|
foreach ($filters as $column => $value) {
|
||||||
// filter on NULL values
|
// filter on NULL values
|
||||||
if(null === $value) {
|
if (null === $value) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ('active' === $column) {
|
if ('active' === $column) {
|
||||||
$query->where('accounts.active', $value);
|
$query->where('accounts.active', $value);
|
||||||
}
|
}
|
||||||
if('name' === $column) {
|
if ('name' === $column) {
|
||||||
$query->where('accounts.name', 'LIKE', sprintf('%%%s%%', $value));
|
$query->where('accounts.name', 'LIKE', sprintf('%%%s%%', $value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,12 +294,11 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
{
|
{
|
||||||
// search by group, not by user
|
// search by group, not by user
|
||||||
$dbQuery = $this->userGroup->accounts()
|
$dbQuery = $this->userGroup->accounts()
|
||||||
->where('active', true)
|
->where('active', true)
|
||||||
->orderBy('accounts.order', 'ASC')
|
->orderBy('accounts.order', 'ASC')
|
||||||
->orderBy('accounts.account_type_id', 'ASC')
|
->orderBy('accounts.account_type_id', 'ASC')
|
||||||
->orderBy('accounts.name', 'ASC')
|
->orderBy('accounts.name', 'ASC')
|
||||||
->with(['accountType'])
|
->with(['accountType']);
|
||||||
;
|
|
||||||
if ('' !== $query) {
|
if ('' !== $query) {
|
||||||
// split query on spaces just in case:
|
// split query on spaces just in case:
|
||||||
$parts = explode(' ', $query);
|
$parts = explode(' ', $query);
|
||||||
@@ -340,18 +339,42 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
public function getAccountTypes(Collection $accounts): Collection
|
public function getAccountTypes(Collection $accounts): Collection
|
||||||
{
|
{
|
||||||
return AccountType::leftJoin('accounts', 'accounts.account_type_id', '=', 'account_types.id')
|
return AccountType::leftJoin('accounts', 'accounts.account_type_id', '=', 'account_types.id')
|
||||||
->whereIn('accounts.id', $accounts->pluck('id')->toArray())
|
->whereIn('accounts.id', $accounts->pluck('id')->toArray())
|
||||||
->get(['accounts.id', 'account_types.type'])
|
->get(['accounts.id', 'account_types.type']);
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[\Override]
|
#[\Override]
|
||||||
public function getLastActivity(Collection $accounts): array
|
public function getLastActivity(Collection $accounts): array
|
||||||
{
|
{
|
||||||
return Transaction::whereIn('account_id', $accounts->pluck('id')->toArray())
|
return Transaction::whereIn('account_id', $accounts->pluck('id')->toArray())
|
||||||
->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id')
|
->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id')
|
||||||
->groupBy('transactions.account_id')
|
->groupBy('transactions.account_id')
|
||||||
->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) as date_max')])->toArray() // @phpstan-ignore-line
|
->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) as date_max')])->toArray() // @phpstan-ignore-line
|
||||||
;
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\Override] public function getObjectGroups(Collection $accounts): array
|
||||||
|
{
|
||||||
|
$groupIds = [];
|
||||||
|
$return = [];
|
||||||
|
$set = DB::table('object_groupables')->where('object_groupable_type', Account::class)
|
||||||
|
->whereIn('object_groupable_id', $accounts->pluck('id')->toArray())->get();
|
||||||
|
/** @var \stdClass $row */
|
||||||
|
foreach ($set as $row) {
|
||||||
|
$groupIds[] = $row->object_group_id;
|
||||||
|
}
|
||||||
|
$groupIds = array_unique($groupIds);
|
||||||
|
$groups = ObjectGroup::whereIn('id', $groupIds)->get();
|
||||||
|
/** @var \stdClass $row */
|
||||||
|
foreach ($set as $row) {
|
||||||
|
if (!array_key_exists($row->object_groupable_id, $return)) {
|
||||||
|
/** @var ObjectGroup|null $group */
|
||||||
|
$group = $groups->firstWhere('id', '=', $row->object_group_id);
|
||||||
|
if (null !== $group) {
|
||||||
|
$return[$row->object_groupable_id] = ['title' => $group->title, 'order' => $group->order, 'id' => $group->id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ interface AccountRepositoryInterface
|
|||||||
*/
|
*/
|
||||||
public function getMetaValue(Account $account, string $field): ?string;
|
public function getMetaValue(Account $account, string $field): ?string;
|
||||||
|
|
||||||
|
public function getObjectGroups(Collection $accounts) : array;
|
||||||
|
|
||||||
public function getUserGroup(): UserGroup;
|
public function getUserGroup(): UserGroup;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class AccountTransformer extends AbstractTransformer
|
|||||||
private array $currencies;
|
private array $currencies;
|
||||||
private TransactionCurrency $default;
|
private TransactionCurrency $default;
|
||||||
private array $lastActivity;
|
private array $lastActivity;
|
||||||
|
private array $objectGroups;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method collects meta-data for one or all accounts in the transformer's collection.
|
* This method collects meta-data for one or all accounts in the transformer's collection.
|
||||||
@@ -58,6 +59,7 @@ class AccountTransformer extends AbstractTransformer
|
|||||||
$this->accountTypes = [];
|
$this->accountTypes = [];
|
||||||
$this->fullTypes = [];
|
$this->fullTypes = [];
|
||||||
$this->lastActivity = [];
|
$this->lastActivity = [];
|
||||||
|
$this->objectGroups = [];
|
||||||
$this->convertedBalances = [];
|
$this->convertedBalances = [];
|
||||||
$this->balanceDifferences = [];
|
$this->balanceDifferences = [];
|
||||||
|
|
||||||
@@ -81,6 +83,9 @@ class AccountTransformer extends AbstractTransformer
|
|||||||
$this->getBalanceDifference($objects, $this->parameters->get('start'), $this->parameters->get('end'));
|
$this->getBalanceDifference($objects, $this->parameters->get('start'), $this->parameters->get('end'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get object groups"
|
||||||
|
$this->getObjectGroups($objects);
|
||||||
|
|
||||||
return $this->sortAccounts($objects);
|
return $this->sortAccounts($objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +132,11 @@ class AccountTransformer extends AbstractTransformer
|
|||||||
$order = null;
|
$order = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// object group
|
||||||
|
$objectGroupId = $this->objectGroups[$id]['id'] ?? null;
|
||||||
|
$objectGroupOrder = $this->objectGroups[$id]['order'] ?? null;
|
||||||
|
$objectGroupTitle = $this->objectGroups[$id]['title'] ?? null;
|
||||||
|
|
||||||
// balance difference
|
// balance difference
|
||||||
$diffStart = null;
|
$diffStart = null;
|
||||||
$diffEnd = null;
|
$diffEnd = null;
|
||||||
@@ -181,6 +191,11 @@ class AccountTransformer extends AbstractTransformer
|
|||||||
'interest_period' => $interestPeriod,
|
'interest_period' => $interestPeriod,
|
||||||
'current_debt' => $currentDebt,
|
'current_debt' => $currentDebt,
|
||||||
|
|
||||||
|
// object group
|
||||||
|
'object_group_id' => null !== $objectGroupId ? (string) $objectGroupId : null,
|
||||||
|
'object_group_order' => $objectGroupOrder,
|
||||||
|
'object_group_title' => $objectGroupTitle,
|
||||||
|
|
||||||
// 'notes' => $this->repository->getNoteText($account),
|
// 'notes' => $this->repository->getNoteText($account),
|
||||||
// 'monthly_payment_date' => $monthlyPaymentDate,
|
// 'monthly_payment_date' => $monthlyPaymentDate,
|
||||||
// 'credit_card_type' => $creditCardType,
|
// 'credit_card_type' => $creditCardType,
|
||||||
@@ -394,4 +409,11 @@ class AccountTransformer extends AbstractTransformer
|
|||||||
return $rightCurrent <=> $leftCurrent;
|
return $rightCurrent <=> $leftCurrent;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getObjectGroups(Collection $accounts): void
|
||||||
|
{
|
||||||
|
/** @var AccountRepositoryInterface $accountRepository */
|
||||||
|
$accountRepository = app(AccountRepositoryInterface::class);
|
||||||
|
$this->objectGroups = $accountRepository->getObjectGroups($accounts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,7 +157,11 @@ let index = function () {
|
|||||||
// hide modal
|
// hide modal
|
||||||
window.bootstrap.Modal.getInstance(document.getElementById('filterModal')).hide();
|
window.bootstrap.Modal.getInstance(document.getElementById('filterModal')).hide();
|
||||||
this.loadAccounts();
|
this.loadAccounts();
|
||||||
|
},
|
||||||
|
saveGroupedAccounts() {
|
||||||
|
setVariable(this.getPreferenceKey('grouped'), this.pageOptions.groupedAccounts);
|
||||||
|
this.page = 1;
|
||||||
|
this.loadAccounts();
|
||||||
},
|
},
|
||||||
removeFilter(field) {
|
removeFilter(field) {
|
||||||
this.filters[field] = null;
|
this.filters[field] = null;
|
||||||
@@ -236,7 +240,6 @@ let index = function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// some opts
|
// some opts
|
||||||
this.pageOptions.isLoading = true;
|
this.pageOptions.isLoading = true;
|
||||||
this.notifications.wait.show = true;
|
this.notifications.wait.show = true;
|
||||||
@@ -249,6 +252,7 @@ let index = function () {
|
|||||||
{name: this.getPreferenceKey('sc'), default: ''},
|
{name: this.getPreferenceKey('sc'), default: ''},
|
||||||
{name: this.getPreferenceKey('sd'), default: ''},
|
{name: this.getPreferenceKey('sd'), default: ''},
|
||||||
{name: this.getPreferenceKey('filters'), default: this.filters},
|
{name: this.getPreferenceKey('filters'), default: this.filters},
|
||||||
|
{name: this.getPreferenceKey('grouped'), default: true},
|
||||||
]).then((res) => {
|
]).then((res) => {
|
||||||
// process columns:
|
// process columns:
|
||||||
for (let k in res[0]) {
|
for (let k in res[0]) {
|
||||||
@@ -270,6 +274,9 @@ let index = function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// group accounts
|
||||||
|
this.pageOptions.groupedAccounts = res[4];
|
||||||
|
|
||||||
this.loadAccounts();
|
this.loadAccounts();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -360,10 +367,8 @@ let index = function () {
|
|||||||
delete params.start;
|
delete params.start;
|
||||||
delete params.end;
|
delete params.end;
|
||||||
}
|
}
|
||||||
// check if cache is present:
|
|
||||||
|
|
||||||
this.accounts = [];
|
this.accounts = [];
|
||||||
|
let groupedAccounts = {};
|
||||||
// one page only.o
|
// one page only.o
|
||||||
(new Get()).index(params).then(response => {
|
(new Get()).index(params).then(response => {
|
||||||
this.totalPages = response.meta.pagination.total_pages;
|
this.totalPages = response.meta.pagination.total_pages;
|
||||||
@@ -392,12 +397,38 @@ let index = function () {
|
|||||||
interest_period: current.attributes.interest_period,
|
interest_period: current.attributes.interest_period,
|
||||||
current_debt: current.attributes.current_debt,
|
current_debt: current.attributes.current_debt,
|
||||||
};
|
};
|
||||||
this.accounts.push(account);
|
|
||||||
|
// get group info:
|
||||||
|
let groupId = current.attributes.object_group_id;
|
||||||
|
if(!this.pageOptions.groupedAccounts) {
|
||||||
|
groupId = '0';
|
||||||
|
}
|
||||||
|
if (!groupedAccounts.hasOwnProperty(groupId)) {
|
||||||
|
groupedAccounts[groupId] = {
|
||||||
|
group: {
|
||||||
|
id: '0' === groupId || null === groupId ? null : parseInt(groupId),
|
||||||
|
title: current.attributes.object_group_title, // are ignored if group id is null.
|
||||||
|
order: current.attributes.object_group_order,
|
||||||
|
},
|
||||||
|
accounts: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groupedAccounts[groupId].accounts.push(account);
|
||||||
|
|
||||||
|
//this.accounts.push(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// order grouped accounts by order.
|
||||||
|
let sortable = [];
|
||||||
|
for (let set in groupedAccounts) {
|
||||||
|
sortable.push(groupedAccounts[set]);
|
||||||
|
}
|
||||||
|
sortable.sort(function(a, b) {
|
||||||
|
return a.group.order - b.group.order;
|
||||||
|
});
|
||||||
|
this.accounts = sortable;
|
||||||
this.notifications.wait.show = false;
|
this.notifications.wait.show = false;
|
||||||
this.pageOptions.isLoading = false;
|
this.pageOptions.isLoading = false;
|
||||||
// add click trigger thing.
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,16 +37,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div x-html="pageNavigation()">
|
<div x-html="pageNavigation">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<template x-for="(set, rootIndex) in accounts" :key="rootIndex">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="card-title">{{ __('firefly.undefined_accounts') }}</h3>
|
<h3 class="card-title">
|
||||||
|
<span x-show="null === set.group.id">{{ __('firefly.undefined_accounts') }}</span>
|
||||||
|
<span x-show="null !== set.group.id" x-text="set.group.title"></span>
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-end">
|
<div class="col text-end">
|
||||||
</div>
|
</div>
|
||||||
@@ -173,7 +177,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template x-for="(account, index) in accounts" :key="index">
|
<template x-for="(account, index) in set.accounts" :key="index">
|
||||||
<tr>
|
<tr>
|
||||||
<td x-show="tableColumns.drag_and_drop.visible && tableColumns.drag_and_drop.enabled">
|
<td x-show="tableColumns.drag_and_drop.visible && tableColumns.drag_and_drop.enabled">
|
||||||
<em class="fa-solid fa-bars"></em>
|
<em class="fa-solid fa-bars"></em>
|
||||||
@@ -312,9 +316,10 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div x-html="pageNavigation()">
|
<div x-html="pageNavigation">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -353,19 +358,17 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<label class="col-sm-4 col-form-label">Group accounts</label>
|
<label class="col-sm-4 col-form-label">Group accounts</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<label>
|
<label>
|
||||||
<input class="form-check-input" type="checkbox"
|
<input class="form-check-input" type="checkbox" @change="saveGroupedAccounts"
|
||||||
x-model="pageOptions.groupedAccounts"><span>Group accounts</span>
|
x-model="pageOptions.groupedAccounts"><span>Group accounts</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
<!--
|
<!--
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<label for="inputEmail3" class="col-sm-4 col-form-label">Show info boxes</label>
|
<label for="inputEmail3" class="col-sm-4 col-form-label">Show info boxes</label>
|
||||||
@@ -408,14 +411,14 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="filterInput" class="form-label">Search in column: <span x-text="lastClickedFilter"></span></label>
|
<label for="filterInput" class="form-label">Search in column: <span x-text="lastClickedFilter"></span></label>
|
||||||
<input @keyup.enter="applyFilter()" type="text" class="form-control" id="filterInput" placeholder="" x-model="lastFilterInput">
|
<input @keyup.enter="applyFilter" type="text" class="form-control" id="filterInput" placeholder="" x-model="lastFilterInput">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><em
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><em
|
||||||
class="fa-solid fa-right-from-bracket"></em> Cancel
|
class="fa-solid fa-right-from-bracket"></em> Cancel
|
||||||
</button>
|
</button>
|
||||||
<button @click="applyFilter()" type="button" class="btn btn-primary" data-bs-dismiss="modal"><em
|
<button @click="applyFilter" type="button" class="btn btn-primary" data-bs-dismiss="modal"><em
|
||||||
class="fa-solid fa-magnifying-glass"></em> Search
|
class="fa-solid fa-magnifying-glass"></em> Search
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user