menu refactoring

This commit is contained in:
Michele Artini 2020-10-13 14:48:04 +02:00
parent 5a76d32f15
commit aa24a5475d
23 changed files with 538 additions and 419 deletions

View File

@ -216,13 +216,18 @@ public class OrganizationController {
: organizationSimpleViewRepository.searchForUser(q, authentication.getName(), PageRequest.of(page, size));
}
@GetMapping("/byCountry/{code}/{page}/{size}")
public Page<OrganizationSimpleView> findByCountry(@PathVariable final String code,
@GetMapping("/byCountry/{status}/{code}/{page}/{size}")
public Page<OrganizationSimpleView> findByCountry(@PathVariable final String status,
@PathVariable final String code,
@PathVariable final int page,
@PathVariable final int size,
final Authentication authentication) {
if (UserInfo.isSuperAdmin(authentication) || userCountryRepository.verifyAuthorizationForCountry(code, authentication.getName())) {
return organizationSimpleViewRepository.findByCountryOrderByName(code, PageRequest.of(page, size));
if (status.equalsIgnoreCase("all")) {
return organizationSimpleViewRepository.findByCountryOrderByName(code, PageRequest.of(page, size));
} else {
return organizationSimpleViewRepository.findByCountryAndStatusOrderByName(code, status, PageRequest.of(page, size));
}
} else {
throw new RuntimeException("User not authorized");
}
@ -243,14 +248,27 @@ public class OrganizationController {
}
}
@GetMapping("/byType/{type}/{page}/{size}")
public Page<OrganizationSimpleView> findByType(@PathVariable final String type,
@GetMapping("/byType/{status}/{type}/{page}/{size}")
public Page<OrganizationSimpleView> findByType(@PathVariable final String status,
@PathVariable final String type,
@PathVariable final int page,
@PathVariable final int size,
final Authentication authentication) {
return UserInfo.isSuperAdmin(authentication)
? organizationSimpleViewRepository.findByTypeOrderByName(type, PageRequest.of(page, size))
: organizationSimpleViewRepository.findByTypeForUser(type, authentication.getName(), PageRequest.of(page, size));
if (UserInfo.isSuperAdmin(authentication)) {
if (status.equalsIgnoreCase("all")) {
return organizationSimpleViewRepository.findByTypeOrderByName(type, PageRequest.of(page, size));
} else {
return organizationSimpleViewRepository.findByTypeAndStatusOrderByName(type, status, PageRequest.of(page, size));
}
} else {
if (status.equalsIgnoreCase("all")) {
return organizationSimpleViewRepository.findByTypeForUser(type, authentication.getName(), PageRequest.of(page, size));
} else {
return organizationSimpleViewRepository.findByTypeAndStatusForUser(type, status, authentication.getName(), PageRequest.of(page, size));
}
}
}
@GetMapping("/browse/countries")

View File

@ -1,6 +1,8 @@
package eu.dnetlib.organizations.model.utils;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
public class BrowseEntry implements Serializable {
@ -9,17 +11,16 @@ public class BrowseEntry implements Serializable {
*/
private static final long serialVersionUID = 8854955977257064470L;
private String value;
private String code;
private String name;
private int approved;
private int pending;
private Map<String, Integer> values = new LinkedHashMap<>();
public String getValue() {
return value;
public String getCode() {
return code;
}
public void setValue(final String value) {
this.value = value;
public void setCode(final String code) {
this.code = code;
}
public String getName() {
@ -30,20 +31,12 @@ public class BrowseEntry implements Serializable {
this.name = name;
}
public int getApproved() {
return approved;
public Map<String, Integer> getValues() {
return values;
}
public void setApproved(final int approved) {
this.approved = approved;
}
public int getPending() {
return pending;
}
public void setPending(final int pending) {
this.pending = pending;
public void setValues(final Map<String, Integer> values) {
this.values = values;
}
}

View File

@ -0,0 +1,48 @@
package eu.dnetlib.organizations.model.utils;
import java.io.Serializable;
public class TempBrowseEntry implements Serializable {
/**
*
*/
private static final long serialVersionUID = -2233409680550512318L;
private String code;
private String name;
private String group;
private Integer count;
public String getCode() {
return code;
}
public void setCode(final String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public String getGroup() {
return group;
}
public void setGroup(final String group) {
this.group = group;
}
public Integer getCount() {
return count;
}
public void setCount(final Integer count) {
this.count = count;
}
}

View File

@ -25,9 +25,16 @@ public interface OrganizationSimpleViewRepository extends ReadOnlyRepository<Org
Iterable<OrganizationSimpleView> findByCountryAndStatusOrderByName(String code, String status);
Page<OrganizationSimpleView> findByCountryAndStatusOrderByName(String code, String status, Pageable pageable);
Page<OrganizationSimpleView> findByTypeOrderByName(String type, Pageable pageable);
Page<OrganizationSimpleView> findByTypeAndStatusOrderByName(String type, String status, Pageable pageable);
@Query(value = "select o.* from organizations_simple_view o left outer join user_countries uc on (uc.country = o.country) where uc.email = ?2 and o.type = ?1 order by o.name", nativeQuery = true)
Page<OrganizationSimpleView> findByTypeForUser(String type, String name, Pageable pageable);
@Query(value = "select o.* from organizations_simple_view o left outer join user_countries uc on (uc.country = o.country) where o.type = ?1 and o.status = ?2 and uc.email = ?3 order by o.name", nativeQuery = true)
Page<OrganizationSimpleView> findByTypeAndStatusForUser(String type, String status, String name, Pageable pageable);
}

View File

@ -38,6 +38,7 @@ import eu.dnetlib.organizations.model.User;
import eu.dnetlib.organizations.model.UserCountry;
import eu.dnetlib.organizations.model.utils.BrowseEntry;
import eu.dnetlib.organizations.model.utils.OrganizationConflict;
import eu.dnetlib.organizations.model.utils.TempBrowseEntry;
import eu.dnetlib.organizations.model.utils.VocabularyTerm;
import eu.dnetlib.organizations.model.view.OrganizationView;
import eu.dnetlib.organizations.model.view.UserView;
@ -280,36 +281,51 @@ public class DatabaseUtils {
// BROWSE BY COUNTRY
public List<BrowseEntry> browseCountries() {
final String sql =
"select o.country as value, c.name as name, sum(case when status='approved' then 1 else 0 end) as approved, sum(case when status='pending' then 1 else 0 end) as pending from organizations o left outer join countries c on (o.country = c.val) group by o.country, c.name order by approved desc";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(BrowseEntry.class));
"select o.country as code, c.name as name, o.status as group, count(o.status) as count from organizations o left outer join countries c on (o.country = c.val) group by o.country, c.name, o.status";
return listBrowseEntries(sql);
}
// BROWSE BY COUNTRY FOR USER
public List<BrowseEntry> browseCountriesForUser(final String email) {
final String sql =
"select o.country as value, c.name as name, sum(case when status='approved' then 1 else 0 end) as approved, sum(case when status='pending' then 1 else 0 end) as pending from user_countries uc left outer join organizations o on (uc.country = o.country) left outer join countries c on (o.country = c.val) where uc.email=? group by o.country, c.name order by approved desc";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(BrowseEntry.class), email);
"select o.country as code, c.name as name, o.status as group, count(o.status) as count from user_countries uc left outer join organizations o on (uc.country = o.country) left outer join countries c on (o.country = c.val) where uc.email=? group by o.country, c.name, o.status";
return listBrowseEntries(sql, email);
}
// BROWSE BY ORG TYPE
public List<BrowseEntry> browseTypes() {
final String sql =
"select type as value, type as name, sum(case when status='approved' then 1 else 0 end) as approved, sum(case when status='pending' then 1 else 0 end) as pending from organizations group by type order by approved desc";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(BrowseEntry.class));
"select type as code, type as name, status as group, count(status) as count from organizations group by type, status";
return listBrowseEntries(sql);
}
// BROWSE BY ORG TYPE FOR USER
public List<BrowseEntry> browseTypesForUser(final String email) {
final String sql = "select o.type as value, o.type as name,"
+ "sum(case when status='approved' then 1 else 0 end) as approved, "
+ "sum(case when status='pending' then 1 else 0 end) as pending "
final String sql = "select o.type as code, o.type as name,"
+ "o.status as group, count(o.status) as count "
+ "from organizations o "
+ "left outer join user_countries uc on (uc.country = o.country) "
+ "where uc.email=? "
+ "group by o.type "
+ "order by approved desc;";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(BrowseEntry.class), email);
+ "group by o.type, o.status";
return listBrowseEntries(sql, email);
}
private List<BrowseEntry> listBrowseEntries(final String sql, final Object... params) {
final Map<String, BrowseEntry> map = new HashMap<>();
for (final TempBrowseEntry t : jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(TempBrowseEntry.class), params)) {
if (StringUtils.isNotBlank(t.getCode())) {
if (!map.containsKey(t.getCode())) {
final BrowseEntry e = new BrowseEntry();
e.setCode(t.getCode());
e.setName(t.getName());
map.put(t.getCode(), e);
}
map.get(t.getCode()).getValues().put(t.getGroup(), t.getCount());
}
}
return map.values().stream().sorted((o1, o2) -> StringUtils.compare(o1.getName(), o2.getName())).collect(Collectors.toList());
}
public List<OrganizationConflict> listConflictsForId(final String id) {

View File

@ -1,16 +0,0 @@
<table class="table table-sm table-hover col-lg-5">
<thead class="thead-light">
<tr>
<th>{{field}}</th>
<th class="text-right text-nowrap"># approved</th>
<th class="text-right text-nowrap"># pending</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="e in entries">
<td><a href="#!{{resultsBasePath}}/0/50/{{e.value}}">{{e.name}}<span ng-if="e.value != e.name"> ({{e.value}})</span></a></td>
<td class="text-right">{{e.approved}}</td>
<td class="text-right">{{e.pending}}</td>
</tr>
</tbody>
</table>

View File

@ -1,42 +0,0 @@
<div>
<div class="text-muted" ng-if="conflicts.length == 0">No suggestions</div>
<div class="input-group input-group-sm mb-3" ng-show="conflicts.length > 0">
<input type="text" class="form-control" ng-model="conflictFilter" placeholder="Filter...">
<div class="input-group-append">
<span class="input-group-text text-outline-primary">Country:</span>
<button class="btn btn-outline-primary dropdown-toggle" data-toggle="dropdown">{{country}}</button>
<div class="dropdown-menu">
<small>
<a class="dropdown-item" href="#!/suggestions/{{c}}/2"
ng-repeat="(c, vals) in info.byCountry"
ng-if="vals.nConflicts > 0">
{{c}} <span class="badge badge-danger float-right">{{vals.nConflicts}}</span>
</a>
</small>
</div>
</div>
</div>
<div class="card text-white mb-3" ng-repeat="w in conflicts | filter:conflictFilter" class="mb-2">
<div class="card-header bg-primary text-white py-1">Group {{$index+1}}</div>
<table class="table table-sm">
<tr ng-repeat="o in w">
<th style="width:40px" class="text-center">#{{$index+1}}</th>
<td><a href="#!/edit/0/{{o.id}}" title="{{o.id}}">{{o.name}}</a></td>
<td style="width:250px"><img ng-src="resources/images/flags/{{o.country}}.gif" /> {{o.city || '-'}}, {{o.country}}</td>
<td style="width:80px" class="text-right">{{o.type}}</td>
</tr>
</table>
<div class="card-footer bg-secondary py-1">
<button type="button"
class="btn btn-sm btn-primary"
data-toggle="modal" data-target="#resolveConflictsModal"
ng-click="prepareConflictsModal(w)">resolve</button>
</div>
</div>
</div>
<resolve-conflicts-modal modal-id="resolveConflictsModal" orgs="orgs" selected-orgs="selectedOrgs"></resolve-conflicts-modal>

View File

@ -1,62 +0,0 @@
<div>
<div class="text-muted" ng-if="duplicates.length == 0">No suggestions</div>
<div class="input-group input-group-sm mb-3" ng-show="duplicates.length > 0">
<input type="text" class="form-control" ng-model="duplicateFilter" placeholder="Filter...">
<div class="input-group-append">
<span class="input-group-text text-outline-primary">Country:</span>
<button class="btn btn-outline-primary dropdown-toggle" data-toggle="dropdown">{{country}}</button>
<div class="dropdown-menu">
<small>
<a class="dropdown-item" href="#!/suggestions/{{c}}/1"
ng-repeat="(c, vals) in info.byCountry"
ng-if="vals.nDuplicates > 0">
{{c}} <span class="badge badge-primary float-right">{{vals.nDuplicates}}</span>
</a>
</small>
</div>
</div>
</div>
<table class="table table-sm table-hover" ng-if="duplicates.length > 0">
<thead class="thead-light">
<tr class="d-flex">
<th class="col-8">Organization</th>
<th class="col-3">Place</th>
<th class="col-1 text-right"># pending duplicates</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="d in duplicates | filter:duplicateFilter" class="d-flex">
<td class="col-8">
<a href="javascript:void(0)" title="{{d.id}}" ng-click="prepareDuplicatesModal(d)" data-toggle="modal" data-target="#duplicatesModal">{{d.name}}</a>
<a href="#!/edit/0/{{d.id}}" title="edit"><i class="fa fa-edit"></i></a>
</td>
<td class="col-3"><img ng-src="resources/images/flags/{{d.country}}.gif" /> {{d.city || '-'}}, {{d.country}}</td>
<td class="col-1 text-right">{{d.numberOfDuplicates}}</td>
</tr>
</tbody>
</table>
</div>
<div class="modal fade" id="duplicatesModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{currentOrg.name}}</h5>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<div class="text-muted" ng-if="currentDuplicates.length == 0">No duplicates</div>
<org-details org="currentOrgDetails" org-title="Registered organization" show="default"></org-details>
<org-form-duplicates duplicates="currentDuplicates"></org-form-duplicates>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-dismiss="modal" ng-click="saveCurrentDuplicates()" ng-if="currentDuplicates.length > 0">Save changes</button>
</div>
</div>
</div>
</div>

View File

@ -1,43 +0,0 @@
<div>
<div class="text-muted" ng-if="orgs.length == 0">No suggestions</div>
<div class="input-group input-group-sm mb-3" ng-show="orgs.length > 0">
<input type="text" class="form-control" ng-model="orgFilter" placeholder="Filter..."/>
<div class="input-group-append">
<span class="input-group-text text-outline-primary">Country:</span>
<button class="btn btn-outline-primary dropdown-toggle" data-toggle="dropdown">{{country}}</button>
<div class="dropdown-menu">
<small>
<a class="dropdown-item" href="#!/suggestions/{{c}}/0"
ng-repeat="(c, vals) in info.byCountry"
ng-if="vals.nPendingOrgs > 0">
{{c}} <span class="badge badge-primary float-right">{{vals.nPendingOrgs}}</span>
</a>
</small>
</div>
</div>
</div>
<table class="table table-sm table-hover" ng-if="orgs.length > 0">
<thead class="thead-light">
<tr class="d-flex">
<th class="col-6">Organization name</th>
<th class="col-4">Place</th>
<th class="col-1 text-center">Acronyms</th>
<th class="col-1 text-right">Type</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="o in orgs | filter:orgFilter" class="d-flex">
<td class="col-6">
<a href="#!/edit/0/{{o.id}}" title="{{o.id}}">{{o.name}}</a>
</td>
<td class="col-4"><img ng-src="resources/images/flags/{{o.country}}.gif" /> {{o.city || '-'}}, {{o.country}}</td>
<td class="col-1 text-center">{{o.acronyms.join()}}</td>
<td class="col-1 text-right">{{o.type}}</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,40 @@
<h2>Conflicts</h2>
<h4 class="text-muted" ng-if="conflicts.length == 0">No suggestions</h4>
<div class="input-group input-group-sm mb-3" ng-show="conflicts.length > 0">
<input type="text" class="form-control" ng-model="conflictFilter" placeholder="Filter...">
<div class="input-group-append">
<span class="input-group-text text-outline-primary">Country:</span>
<button class="btn btn-outline-primary dropdown-toggle" data-toggle="dropdown">{{country}}</button>
<div class="dropdown-menu">
<small>
<a class="dropdown-item" href="#!/conflicts/{{c}}"
ng-repeat="(c, vals) in info.data.byCountry"
ng-if="vals.nConflicts > 0">
{{c}} <span class="badge badge-danger float-right">{{vals.nConflicts}}</span>
</a>
</small>
</div>
</div>
</div>
<div class="card text-white mb-3" ng-repeat="w in conflicts | filter:conflictFilter" class="mb-2">
<div class="card-header bg-primary text-white py-1">Group {{$index+1}}</div>
<table class="table table-sm">
<tr ng-repeat="o in w">
<th style="width:40px" class="text-center">#{{$index+1}}</th>
<td><a href="#!/edit/0/{{o.id}}" title="{{o.id}}">{{o.name}}</a></td>
<td style="width:250px"><img ng-src="resources/images/flags/{{o.country}}.gif" /> {{o.city || '-'}}, {{o.country}}</td>
<td style="width:80px" class="text-right">{{o.type}}</td>
</tr>
</table>
<div class="card-footer bg-secondary py-1">
<button type="button"
class="btn btn-sm btn-primary"
data-toggle="modal" data-target="#resolveConflictsModal"
ng-click="prepareConflictsModal(w)">resolve</button>
</div>
</div>
<resolve-conflicts-modal modal-id="resolveConflictsModal" orgs="orgs" selected-orgs="selectedOrgs"></resolve-conflicts-modal>

View File

@ -0,0 +1,60 @@
<h2>Duplicates</h2>
<h4 class="text-muted" ng-if="duplicates.length == 0">No duplicates</h4>
<div class="input-group input-group-sm mb-3" ng-show="duplicates.length > 0">
<input type="text" class="form-control" ng-model="duplicateFilter" placeholder="Filter...">
<div class="input-group-append">
<span class="input-group-text text-outline-primary">Country:</span>
<button class="btn btn-outline-primary dropdown-toggle" data-toggle="dropdown">{{country}}</button>
<div class="dropdown-menu">
<small>
<a class="dropdown-item" href="#!/duplicates/{{c}}"
ng-repeat="(c, vals) in info.data.byCountry"
ng-if="vals.nDuplicates > 0">
{{c}} <span class="badge badge-primary float-right">{{vals.nDuplicates}}</span>
</a>
</small>
</div>
</div>
</div>
<table class="table table-sm table-hover" ng-if="duplicates.length > 0">
<thead class="thead-light">
<tr class="d-flex">
<th class="col-8">Organization</th>
<th class="col-3">Place</th>
<th class="col-1 text-right"># pending duplicates</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="d in duplicates | filter:duplicateFilter" class="d-flex">
<td class="col-8">
<a href="javascript:void(0)" title="{{d.id}}" ng-click="prepareDuplicatesModal(d)" data-toggle="modal" data-target="#duplicatesModal">{{d.name}}</a>
<a href="#!/edit/0/{{d.id}}" title="edit"><i class="fa fa-edit"></i></a>
</td>
<td class="col-3"><img ng-src="resources/images/flags/{{d.country}}.gif" /> {{d.city || '-'}}, {{d.country}}</td>
<td class="col-1 text-right">{{d.numberOfDuplicates}}</td>
</tr>
</tbody>
</table>
<div class="modal fade" id="duplicatesModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{currentOrg.name}}</h5>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<div class="text-muted" ng-if="currentDuplicates.length == 0">No duplicates</div>
<org-details org="currentOrgDetails" org-title="Registered organization" show="default"></org-details>
<org-form-duplicates duplicates="currentDuplicates"></org-form-duplicates>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-dismiss="modal" ng-click="saveCurrentDuplicates()" ng-if="currentDuplicates.length > 0">Save changes</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,42 @@
<h2>Pending Organizations</h2>
<h4 class="text-muted" ng-if="orgs.length == 0">No pending organizations</h4>
<div class="input-group input-group-sm mb-3" ng-show="orgs.length > 0">
<input type="text" class="form-control" ng-model="orgFilter" placeholder="Filter..."/>
<div class="input-group-append">
<span class="input-group-text text-outline-primary">Country:</span>
<button class="btn btn-outline-primary dropdown-toggle" data-toggle="dropdown">{{country}}</button>
<div class="dropdown-menu">
<small>
<a class="dropdown-item" href="#!/pendings/{{c}}"
ng-repeat="(c, vals) in info.data.byCountry"
ng-if="vals.nPendingOrgs > 0">
{{c}} <span class="badge badge-primary float-right">{{vals.nPendingOrgs}}</span>
</a>
</small>
</div>
</div>
</div>
<table class="table table-sm table-hover" ng-if="orgs.length > 0">
<thead class="thead-light">
<tr class="d-flex">
<th class="col-6">Organization name</th>
<th class="col-4">Place</th>
<th class="col-1 text-center">Acronyms</th>
<th class="col-1 text-right">Type</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="o in orgs | filter:orgFilter" class="d-flex">
<td class="col-6">
<a href="#!/edit/0/{{o.id}}" title="{{o.id}}">{{o.name}}</a>
</td>
<td class="col-4"><img ng-src="resources/images/flags/{{o.country}}.gif" /> {{o.city || '-'}}, {{o.country}}</td>
<td class="col-1 text-center">{{o.acronyms.join()}}</td>
<td class="col-1 text-right">{{o.type}}</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,50 @@
<h2>{{title}}</h2>
<h4 class="text-muted" ng-if="entries.length == 0">No entries</h4>
<p>
<input type="text" class="form-control col-lg-8 col-xs-12" ng-show="entries.length > 0" ng-model="browseFilter" placeholder="Filter...">
</p>
<table class="table table-sm table-bordered table-hover col-lg-8 col-xs-12">
<thead class="thead-light">
<tr>
<th class="col-6">{{field}}</th>
<th class="col-1 text-right text-nowrap"># approved</th>
<th class="col-1 text-right text-nowrap"># pending</th>
<th class="col-1 text-right text-nowrap"># deleted</th>
<th class="col-1 text-right text-nowrap"># duplicated</th>
<th class="col-1 text-right text-nowrap"># discarded</th>
<th class="col-1 text-right text-nowrap"># hidden</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="e in entries | filter: browseFilter">
<th>{{e.name}} <span ng-if="e.code != e.name"> ({{e.code}})</span></th>
<td class="text-right">
<a href="#!{{resultsBasePath}}/0/50/approved/{{e.code}}" ng-if="e.values.approved && e.values.approved > 0">{{e.values.approved}}</a>
<span ng-if="!e.values.approved || e.values.approved == 0">-</span>
</td>
<td class="text-right">
<a href="#!{{resultsBasePath}}/0/50/pending/{{e.code}}" ng-if="e.values.pending && e.values.pending > 0">{{e.values.pending}}</a>
<span ng-if="!e.values.pending || e.values.pending == 0">-</span>
</td>
<td class="text-right">
<a href="#!{{resultsBasePath}}/0/50/deleted/{{e.code}}" ng-if="e.values.deleted && e.values.deleted > 0">{{e.values.deleted}}</a>
<span ng-if="!e.values.deleted || e.values.deleted == 0">-</span>
</td>
<td class="text-right">
<a href="#!{{resultsBasePath}}/0/50/duplicate/{{e.code}}" ng-if="e.values.duplicate && e.values.duplicate > 0">{{e.values.duplicate}}</a>
<span ng-if="!e.values.duplicate || e.values.duplicate == 0">-</span>
</td>
<td class="text-right">
<a href="#!{{resultsBasePath}}/0/50/discarded/{{e.code}}" ng-if="e.values.discarded && e.values.discarded > 0">{{e.values.discarded}}</a>
<span ng-if="!e.values.discarded || e.values.discarded == 0">-</span>
</td>
<td class="text-right">
<a href="#!{{resultsBasePath}}/0/50/hidden/{{e.code}}" ng-if="e.values.hidden && e.values.hidden > 0">{{e.values.hidden}}</a>
<span ng-if="!e.values.hidden || e.values.hidden == 0">-</span>
</td>
</tr>
</tbody>
</table>

View File

@ -1,23 +0,0 @@
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item text-nowrap">
<a href="#!/suggestions/_/0" class="nav-link" ng-class="{'active': currentTab == 0}">Pending Organizations <span class="badge badge-primary ml-2">{{info.total.nPendingOrgs}}</span></a>
</li>
<li class="nav-item text-nowrap">
<a href="#!/suggestions/_/1" class="nav-link" ng-class="{'active': currentTab == 1}">New Duplicates <span class="badge badge-primary ml-2">{{info.total.nDuplicates}}</span></a>
</li>
<li class="nav-item text-nowrap">
<a href="#!/suggestions/_/2" class="nav-link" ng-class="{'active': currentTab == 2}">New Conflicts <span class="badge badge-danger ml-2">{{info.total.nConflicts}}</span></a>
</li>
</ul>
</div>
<div class="card-body">
<pending-orgs orgs="pendingOrgs" country="{{country}}" info="info" info-method="getInfo()" ng-if="currentTab == 0"></pending-orgs>
<all-duplicates duplicates="duplicates" country="{{country}}" info="info" info-method="getInfo()" ng-if="currentTab == 1"></all-duplicates>
<all-conflicts conflicts="conflicts" country="{{country}}" info="info" info-method="getInfo()" ng-if="currentTab == 2"></all-conflicts>
</div>
</div>

View File

@ -17,6 +17,34 @@ orgsModule.service('vocabulariesService', function($http) {
};
});
orgsModule.factory('suggestionInfo', function($http) {
var info = { data : {} };
var getInfo = function() { return info; };
var updateInfo = function(callback) {
$http.get('api/organizations/suggestionsInfo').then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
info.data = res.data;
if (callback) { callback(info); }
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
};
return {
getInfo: getInfo,
updateInfo: updateInfo
};
});
orgsModule.controller('menuCtrl', function ($scope, suggestionInfo) {
$scope.info = suggestionInfo.getInfo();
suggestionInfo.updateInfo(null);
});
orgsModule.directive('selectOrgModal', function($http) {
return {
restrict: 'E',
@ -265,52 +293,6 @@ orgsModule.directive('orgResultsPage', function($http, $location, $route) {
}
});
orgsModule.directive('allConflicts', function($http, $location, $route, $q) {
return {
restrict: 'E',
scope: {
'conflicts' : '=',
'country' : '@',
'info' : '=',
'infoMethod' : '&'
},
templateUrl: 'resources/html/forms/all_conflicts.html',
link: function(scope, element, attrs, ctrl) {
scope.orgs = [];
scope.prepareConflictsModal = function(list) {
scope.orgs = [];
scope.selectedOrgs = [];
var gets = list.map((o) => $http.get('api/organizations/get?id=' + o.id));
$q.all(gets).then(function(responses) {
scope.orgs = responses.map((resp) => resp.data);
angular.forEach(scope.orgs, function(org) { org.show = 'secondary'; });
});
}
}
}
});
orgsModule.directive('pendingOrgs', function($http, $location, $route, $q) {
return {
restrict: 'E',
scope: {
'orgs' : '=',
'country' : '@',
'info' : '=',
'infoMethod' : '&'
},
templateUrl: 'resources/html/forms/pending_orgs.html',
link: function(scope, element, attrs, ctrl) {
}
}
});
orgsModule.directive('orgFormDuplicates', function($http, $location, $route) {
return {
restrict: 'E',
@ -354,83 +336,21 @@ orgsModule.directive('orgFormConflicts', function($http, $location, $route, $q)
}
});
orgsModule.directive('allDuplicates', function($http, $location, $route, $timeout) {
return {
restrict: 'E',
scope: {
'duplicates' : '=',
'country' : '@',
'info' : '=',
'infoMethod' : '&'
},
templateUrl: 'resources/html/forms/all_duplicates.html',
link: function(scope, element, attrs, ctrl) {
scope.currentOrg = {};
scope.currentOrgDetails = {};
scope.currentDuplicates = [];
scope.prepareDuplicatesModal = function(org) {
scope.currentOrg = org;
scope.currentOrgDetails = {};
scope.currentDuplicates = [];
$http.get('api/organizations/get?id=' + org.id).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
scope.currentOrgDetails = res.data;
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
$http.get('api/organizations/duplicates?id=' + org.id).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
scope.currentDuplicates = res.data;
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
};
scope.saveCurrentDuplicates = function() {
$http.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8";
$http.post('api/organizations/duplicates', scope.currentDuplicates).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
if (scope.infoMethod) { scope.infoMethod(); }
scope.currentOrg.numberOfDuplicates = 0;
for (var i=0; i<res.data.length; i++) {
if (res.data[i].relType == 'suggested') {
scope.currentOrg.numberOfDuplicates++;
}
}
scope.currentDuplicates = [];
$timeout(function() {
if (scope.duplicates.length > 1) {
$route.reload();
} else {
$location.url('/suggestions/_/1');
}
}, 600);
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
};
}
}
});
orgsModule.config(function($routeProvider) {
$routeProvider
.when('/new', { templateUrl: 'resources/html/new.html', controller: 'newOrgCtrl' })
.when('/search', { templateUrl: 'resources/html/search.html', controller: 'searchCtrl' })
.when('/searchResults/:page/:size/:text*', { templateUrl: 'resources/html/searchResults.html', controller: 'searchResultsCtrl' })
.when('/countries', { templateUrl: 'resources/html/browse.html', controller: 'countriesCtrl' })
.when('/byCountry/:page/:size/:code*', { templateUrl: 'resources/html/resultsByCountry.html', controller: 'byCountryCtrl' })
.when('/types', { templateUrl: 'resources/html/browse.html', controller: 'typesCtrl' })
.when('/byType/:page/:size/:type*', { templateUrl: 'resources/html/resultsByType.html', controller: 'byTypeCtrl' })
.when('/edit/:msg/:id', { templateUrl: 'resources/html/edit.html', controller: 'showEditCtrl' })
.when('/suggestions/:country/:tab', { templateUrl: 'resources/html/suggestions.html', controller: 'showSuggestionsCtrl' })
.when('/users', { templateUrl: 'resources/html/users.html', controller: 'usersCtrl' })
.otherwise({ redirectTo: '/suggestions/_/0' });
.when('/search', { templateUrl: 'resources/html/pages/search/search.html', controller: 'searchCtrl' })
.when('/searchResults/:page/:size/:text*', { templateUrl: 'resources/html/pages/search/searchResults.html', controller: 'searchResultsCtrl' })
.when('/countries', { templateUrl: 'resources/html/pages/search/browse.html', controller: 'countriesCtrl' })
.when('/byCountry/:page/:size/:status/:code*', { templateUrl: 'resources/html/pages/search/resultsByCountry.html', controller: 'byCountryCtrl' })
.when('/types', { templateUrl: 'resources/html/pages/search/browse.html', controller: 'typesCtrl' })
.when('/byType/:page/:size/:status/:type*', { templateUrl: 'resources/html/pages/search/resultsByType.html', controller: 'byTypeCtrl' })
.when('/edit/:msg/:id', { templateUrl: 'resources/html/pages/edit/edit.html', controller: 'showEditCtrl' })
.when('/new', { templateUrl: 'resources/html/pages/advanced/new.html', controller: 'newOrgCtrl' })
.when('/pendings/:country', { templateUrl: 'resources/html/pages/advanced/pendingOrgs.html', controller: 'pendingOrgsCtrl' })
.when('/duplicates/:country', { templateUrl: 'resources/html/pages/advanced/duplicates.html', controller: 'duplicatesCtrl' })
.when('/conflicts/:country', { templateUrl: 'resources/html/pages/advanced/conflicts.html', controller: 'conflictsCtrl' })
.when('/users', { templateUrl: 'resources/html/pages/admin/users.html', controller: 'usersCtrl' })
.otherwise({ redirectTo: '/search' });
});
orgsModule.filter('escape', function() {
@ -511,6 +431,8 @@ orgsModule.controller('searchResultsCtrl', function ($scope, $http, $routeParams
});
orgsModule.controller('countriesCtrl', function ($scope, $http, $routeParams) {
$scope.title = 'Countries';
$scope.field = 'Country';
$scope.resultsBasePath = '/byCountry'
$scope.entries = [];
@ -528,7 +450,7 @@ orgsModule.controller('byCountryCtrl', function ($scope, $http, $routeParams, $l
$scope.fieldValue = decodeURIComponent($routeParams.code);
$scope.orgs = {};
$http.get('api/organizations/byCountry/' + $routeParams.code + '/' + $routeParams.page + '/' + $routeParams.size).then(function successCallback(res) {
$http.get('api/organizations/byCountry/' + $routeParams.status + '/' + $routeParams.code + '/' + $routeParams.page + '/' + $routeParams.size).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.orgs = res.data;
}, function errorCallback(res) {
@ -536,16 +458,17 @@ orgsModule.controller('byCountryCtrl', function ($scope, $http, $routeParams, $l
});
$scope.prev = function() {
$location.url('/byCountry/' + ($scope.orgs.number - 1) + '/' + $scope.orgs.size + '/' + encodeURIComponent($scope.fieldValue));
$location.url('/byCountry/' + ($scope.orgs.number - 1) + '/' + $scope.orgs.size + '/' + $routeParams.status + '/' + encodeURIComponent($scope.fieldValue));
}
$scope.next = function() {
$location.url('/byCountry/' + ($scope.orgs.number + 1) + '/' + $scope.orgs.size + '/' + encodeURIComponent($scope.fieldValue));
$location.url('/byCountry/' + ($scope.orgs.number + 1) + '/' + $scope.orgs.size + '/' + $routeParams.status + '/' + encodeURIComponent($scope.fieldValue));
}
});
orgsModule.controller('typesCtrl', function ($scope, $http, $routeParams) {
$scope.title = 'Organization types';
$scope.field = 'Organization type';
$scope.resultsBasePath = '/byType'
$scope.entries = [];
@ -565,7 +488,7 @@ orgsModule.controller('byTypeCtrl', function ($scope, $http, $routeParams, $loca
$scope.orgs = {};
$http.get('api/organizations/byType/' + $routeParams.type + '/' + $routeParams.page + '/' + $routeParams.size).then(function successCallback(res) {
$http.get('api/organizations/byType/' + $routeParams.status + '/' + $routeParams.type + '/' + $routeParams.page + '/' + $routeParams.size).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.orgs = res.data;
}, function errorCallback(res) {
@ -573,11 +496,11 @@ orgsModule.controller('byTypeCtrl', function ($scope, $http, $routeParams, $loca
});
$scope.prev = function() {
$location.url('/byType/' + ($scope.orgs.number - 1) + '/' + $scope.orgs.size + '/' + encodeURIComponent($scope.fieldValue));
$location.url('/byType/' + ($scope.orgs.number - 1) + '/' + $scope.orgs.size + '/' + $routeParams.status + '/' + encodeURIComponent($scope.fieldValue));
}
$scope.next = function() {
$location.url('/byType/' + ($scope.orgs.number + 1) + '/' + $scope.orgs.size + '/' + encodeURIComponent($scope.fieldValue));
$location.url('/byType/' + ($scope.orgs.number + 1) + '/' + $scope.orgs.size + '/' + $routeParams.status + '/' + encodeURIComponent($scope.fieldValue));
}
});
@ -612,62 +535,35 @@ orgsModule.controller('showEditCtrl', function ($scope, $http, $routeParams, $ro
});
orgsModule.controller('showSuggestionsCtrl', function ($scope, $http, $routeParams, $location) {
$scope.info = {};
$scope.pendingOrgs = [];
$scope.duplicates = [];
$scope.conflicts = [];
$scope.currentTab = $routeParams.tab;
orgsModule.controller('pendingOrgsCtrl', function ($scope, $http, $routeParams, $location, suggestionInfo) {
$scope.info = suggestionInfo.getInfo();
$scope.orgs = [];
$scope.country = $routeParams.country;
$scope.getInfo = function() {
$http.get('api/organizations/suggestionsInfo').then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.info = res.data;
if ($scope.country == '_') {
var found = '';
angular.forEach($scope.info.byCountry, function(values, c) {
if (!found && (($scope.currentTab == 0 && values.nPendingOrgs > 0) || ($scope.currentTab == 1 && values.nDuplicates > 0) || ($scope.currentTab == 2 && values.nConflicts > 0))) {
found = c;
}
});
if (found) { $location.url('/suggestions/' + found + '/' + $scope.currentTab); }
}
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
suggestionInfo.updateInfo(function(info) {
if ($scope.country == '_') {
var found = '';
angular.forEach(info.data.byCountry, function(values, c) {
if (!found && values.nPendingOrgs > 0) {
found = c;
}
});
if (found) { $location.url('/pendings/' + found); }
}
});
};
$scope.refresh = function() {
$scope.pendingOrgs = [];
$scope.duplicates = [];
$scope.conflicts = [];
$scope.orgs = [];
if ($scope.country != '_') {
if ($scope.currentTab == 0) {
$http.get('api/organizations/byCountry/pending/' + $scope.country).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.pendingOrgs = res.data;
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
} else if ($scope.currentTab == 1) {
$http.get('api/organizations/duplicates/byCountry/' + $scope.country).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.duplicates = res.data;
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
} else if ($scope.currentTab == 2) {
$http.get('api/organizations/conflicts/byCountry/' + $scope.country).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.conflicts = res.data;
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
} else { }
$http.get('api/organizations/byCountry/pending/' + $scope.country).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.orgs = res.data;
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
}
$scope.getInfo();
}
@ -675,6 +571,143 @@ orgsModule.controller('showSuggestionsCtrl', function ($scope, $http, $routePara
$scope.refresh();
});
orgsModule.controller('duplicatesCtrl', function ($scope, $http, $routeParams, $location, $timeout, $route, suggestionInfo) {
$scope.info = suggestionInfo.getInfo();
$scope.duplicates = [];
$scope.country = $routeParams.country;
$scope.currentOrg = {};
$scope.currentOrgDetails = {};
$scope.prepareDuplicatesModal = function(org) {
$scope.currentOrg = org;
$scope.currentOrgDetails = {};
$scope.currentDuplicates = [];
$http.get('api/organizations/get?id=' + org.id).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.currentOrgDetails = res.data;
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
$http.get('api/organizations/duplicates?id=' + org.id).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.currentDuplicates = res.data;
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
};
$scope.getInfo = function() {
suggestionInfo.updateInfo(function(info) {
if ($scope.country == '_') {
var found = '';
angular.forEach(info.data.byCountry, function(values, c) {
if (!found && values.nDuplicates > 0) {
found = c;
}
});
if (found) { $location.url('/duplicates/' + found); }
}
});
};
$scope.saveCurrentDuplicates = function() {
$http.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8";
$http.post('api/organizations/duplicates', $scope.currentDuplicates).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.getInfo();
$scope.currentOrg.numberOfDuplicates = 0;
for (var i=0; i<res.data.length; i++) {
if (res.data[i].relType == 'suggested') {
$scope.currentOrg.numberOfDuplicates++;
}
}
$scope.currentDuplicates = [];
$timeout(function() {
if ($scope.duplicates.length > 1) {
$route.reload();
} else {
$location.url('/duplicates/_');
}
}, 600);
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
};
$scope.refresh = function() {
$scope.duplicates = [];
if ($scope.country != '_') {
$http.get('api/organizations/duplicates/byCountry/' + $scope.country).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.duplicates = res.data;
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
}
$scope.getInfo();
}
$scope.refresh();
});
orgsModule.controller('conflictsCtrl', function ($scope, $http, $routeParams, $location, $route, $q, suggestionInfo) {
$scope.info = suggestionInfo.getInfo();
$scope.conflicts = [];
$scope.country = $routeParams.country;
$scope.orgs = [];
$scope.prepareConflictsModal = function(list) {
$scope.orgs = [];
$scope.selectedOrgs = [];
var gets = list.map((o) => $http.get('api/organizations/get?id=' + o.id));
$q.all(gets).then(function(responses) {
$scope.orgs = responses.map((resp) => resp.data);
angular.forEach($scope.orgs, function(org) { org.show = 'secondary'; });
});
}
$scope.getInfo = function() {
suggestionInfo.updateInfo(function(info) {
if ($scope.country == '_') {
var found = '';
angular.forEach(info.data.byCountry, function(values, c) {
if (!found && values.nConflicts > 0) {
found = c;
}
});
if (found) { $location.url('/conflicts/' + found); }
}
});
};
$scope.refresh = function() {
$scope.conflicts = [];
if ($scope.country != '_') {
$http.get('api/organizations/conflicts/byCountry/' + $scope.country).then(function successCallback(res) {
if((typeof res.data) == 'string') { alert("Session expired !"); location.reload(true); }
$scope.conflicts = res.data;
}, function errorCallback(res) {
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
}
$scope.getInfo();
}
$scope.refresh();
});
orgsModule.controller('usersCtrl', function ($scope, $http, $timeout, $route, vocabulariesService) {
$scope.users = [];

View File

@ -15,16 +15,9 @@
<link rel="stylesheet" href="resources/css/fontawesome-all.min.css">
<style type="text/css">
.table > tbody > tr > td {
vertical-align: middle;
}
.card > .table {
margin-bottom: 0 !important;
}
fieldset > legend {
font-size: 1.2rem !important;
}
.table > tbody > tr > td { vertical-align : middle !important; }
.card > .table { margin-bottom : 0 !important; }
fieldset > legend { font-size : 1.2rem !important; }
</style>
@ -33,7 +26,7 @@ fieldset > legend {
</head>
<body ng-app="orgs" sec:authorize="isAuthenticated()">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary" ng-controller="menuCtrl">
<a class="navbar-brand" href="#"> <img
src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenOrgs Database">
OpenOrgs Database
@ -45,7 +38,6 @@ fieldset > legend {
<div class="collapse navbar-collapse w-100 order-1" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item"><a class="nav-link" href="#!/suggestions/_/0">Last suggestions</a></li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="javascript:void(0)" data-toggle="dropdown">Search</a>
<div class="dropdown-menu">
@ -55,17 +47,23 @@ fieldset > legend {
<a class="dropdown-item" href="#!/types">browse by type</a>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="javascript:void(0)" data-toggle="dropdown">Actions</a>
<li class="nav-item dropdown" sec:authorize="hasRole('ROLE_ADMIN') or hasRole('ROLE_NATIONAL_ADMIN')">
<a class="nav-link dropdown-toggle" href="javascript:void(0)" data-toggle="dropdown">Advanced</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="#!/new">register an organization</a>
<a class="dropdown-item d-flex justify-content-between align-items-center" href="#!/pendings/_"><span class="pr-2">pending organizations</span><span class="badge badge-primary badge-pill">{{info.data.total.nPendingOrgs}}</span></a>
<a class="dropdown-item d-flex justify-content-between align-items-center" href="#!/duplicates/_"><span class="pr-2">duplicates</span><span class="badge badge-primary badge-pill">{{info.data.total.nDuplicates}}</span></a>
<a class="dropdown-item d-flex justify-content-between align-items-center" href="#!/conflicts/_"><span class="pr-2">conflicts</span><span class="badge badge-danger badge-pill">{{info.data.total.nConflicts}}</span></a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#!/new">new from scratch</a>
</div>
</li>
<li class="nav-item dropdown" sec:authorize="hasRole('ROLE_USER')">
<a class="nav-link dropdown-toggle" href="javascript:void(0)" data-toggle="dropdown">Advanced</a>
<div class="dropdown-menu">
<a class="dropdown-item d-flex justify-content-between align-items-center" href="#!/duplicates/_"><span class="pr-2">duplicates</span><span class="badge badge-primary badge-pill">{{info.total.nDuplicates}}</span></a>
</div>
</li>
</ul>
</div>
<div class="navbar-collapse collapse w-100 order-2">
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown" sec:authorize="hasRole('ROLE_ADMIN') or hasRole('ROLE_NATIONAL_ADMIN')">
<a class="nav-link dropdown-toggle" href="javascript:void(0)" data-toggle="dropdown"><i class="fa fa-cog"></i></a>
@ -84,7 +82,7 @@ fieldset > legend {
<a class="dropdown-item" th:href="@{/logout}">Logout</a>
</div>
</li>
<li class="nav-item"><a class="btn btn-outline-secondary" href="doc">API</a></li>
<li class="nav-item"><a class="btn btn-outline-secondary" href="doc" target="_blank">API</a></li>
</ul>
</div>