Update the dashboard with an organisations widget

This commit is contained in:
2022-03-07 16:27:52 +00:00
parent 2005a17eef
commit 012b633eaf
10 changed files with 554 additions and 0 deletions

View File

@@ -10,6 +10,17 @@
}
}
},
"js/pages/dashboard": {
"scripts": [
"organisations/js/widgets/organisations.js",
"organisations/js/pages/dashboard.js"
],
"options": {
"sprinkle": {
"onCollision": "merge"
}
}
},
"js/pages/organisation": {
"scripts": [
"userfrosting/js/widgets/users.js",

View File

@@ -7,3 +7,15 @@
.membership-pending {
color: #ffd24a !important;
}
.widget-organisations .widget-user-username, .widget-organisations .widget-user-desc {
margin-left: 0px;
}
.widget-organisations .widget-user-desc {
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
displaY: inline-block;
white-space: nowrap;
}

View File

@@ -0,0 +1,17 @@
/**
* Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template.
* example: {{ assets.js('js/pages/sign-in-or-register') | raw }}
*
* Target page: /dashboard
*/
$(document).ready(function() {
// Table of organisations
var organisations = $("#widget-organisations");
if (organisations.length) {
organisations.ufTable({
dataUrl: site.uri.public + "/api/organisations",
useLoadingTransition: site.uf_table.use_loading_transition
});
}
});

View File

@@ -17,6 +17,9 @@ return [
1 => 'Organisation',
2 => 'Organisations',
'LATEST' => 'Latest Organisations',
'VIEW_ALL' => 'View all organisations',
'PAGE_DESCRIPTION' => 'A listing of the organisations for your site. Provides management tools for editing and deleting organisations.',
'DELETED_PAGE_DESCRIPTION' => 'A listing of the deleted organisations for your site. Provides management tools for restoring and permenently deleting organisations.',
'INFO_PAGE' => 'Organisation information page for {{name}}',
@@ -164,6 +167,8 @@ return [
2 => 'Administrators',
],
'CREATED_ON' => 'Created on',
'MERGE' => 'Merge',
'MERGE_INTO' => 'Merge into',
'MERGE_CANNOT_UNDONE' => 'This action cannot be undone.',

18
routes/dashboard.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
/*
* AVSDev UF Organisations (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
*/
use UserFrosting\Sprinkle\Core\Util\NoCache;
/*
* Routes for dashbaord.
*/
$app->group('/dashboard', function () {
$this->get('', 'UserFrosting\Sprinkle\Organisations\Controller\DashboardController:pageDashboard')
->setName('dashboard');
})->add('authGuard')->add(new NoCache());

View File

@@ -0,0 +1,109 @@
<?php
/*
* AVSDev UF Organisations (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Organisations\Controller;
use Carbon\Carbon;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use UserFrosting\Sprinkle\Core\Controller\SimpleController;
use UserFrosting\Sprinkle\Core\Util\EnvironmentInfo;
use UserFrosting\Support\Exception\ForbiddenException;
/**
* Controller class for organisation-related requests, including listing organisations, CRUD for organisations, etc.
*
* @author Craig Williams (https://avsdev.uk)
*/
class DashboardController extends SimpleController
{
/**
* Renders the admin panel dashboard.
*
* @param Request $request
* @param Response $response
* @param array $args
*/
public function pageDashboard(Request $request, Response $response, $args)
{
//** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
$authorizer = $this->ci->authorizer;
/** @var \Illuminate\Cache\Repository $cache */
$cache = $this->ci->cache;
/** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
$classMapper = $this->ci->classMapper;
/** @var \UserFrosting\Support\Repository\Repository $config */
$config = $this->ci->config;
/** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */
$currentUser = $this->ci->currentUser;
// Access-controlled page
if (!$authorizer->checkAccess($currentUser, 'uri_dashboard')) {
throw new ForbiddenException();
}
// Probably a better way to do this
$users = $classMapper->getClassMapping('user')::orderBy('created_at', 'desc')
->take(8)
->get();
// Transform the `create_at` date in "x days ago" type of string
$users->transform(function ($item, $key) {
$item->registered = Carbon::parse($item->created_at)->diffForHumans();
return $item;
});
// Probably a better way to do this
$organisations = $classMapper->getClassMapping('organisation')::orderBy('created_at', 'desc')
->take(4)
->get();
// Transform the `create_at` date in "x days ago" type of string
$organisations->transform(function ($item, $key) {
$item->registered = Carbon::parse($item->created_at)->diffForHumans();
return $item;
});
// Get each sprinkle db version
$sprinkles = $this->ci->sprinkleManager->getSprinkleNames();
return $this->ci->view->render($response, 'pages/dashboard.html.twig', [
'counter' => [
'users' => $classMapper->getClassMapping('user')::count(),
'roles' => $classMapper->getClassMapping('role')::count(),
'groups' => $classMapper->getClassMapping('group')::count(),
'organisations' => $classMapper->getClassMapping('organisation')::count(),
],
'info' => [
'version' => [
'UF' => \UserFrosting\VERSION,
'php' => phpversion(),
'database' => EnvironmentInfo::database(),
],
'database' => [
'name' => $config['db.default.database'],
],
'environment' => $this->ci->environment,
'path' => [
'project' => \UserFrosting\ROOT_DIR,
],
],
'sprinkles' => $sprinkles,
'users' => $users,
'organisations' => $organisations,
]);
}
}

View File

@@ -135,6 +135,19 @@ class Organisation extends Model implements OrganisationInterface, TokenOwnerInt
->where('flag_admin', true);
}
/**
* Get a list of pending members within this organisation.
*/
public function pendingMembers()
{
/** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
$classMapper = static::$ci->classMapper;
return $this
->belongsToMany($classMapper->getClassMapping('user'), 'organisation_members', 'organisation_id', 'user_id')
->where('flag_approved', false);
}
/**
* Delete this organisation from the database, along with any linked objects.
*

View File

@@ -33,6 +33,7 @@ class OrganisationSprunje extends Sprunje
'member_count',
'admin_count',
'status',
'created_at',
];
protected $filterable = [
@@ -42,6 +43,7 @@ class OrganisationSprunje extends Sprunje
'admin_count',
'status',
'info',
'created_at',
];
protected $excludeForAll = [
@@ -136,4 +138,20 @@ class OrganisationSprunje extends Sprunje
return $this;
}
/**
* Sort based on created date.
*
* @param Builder $query
* @param string $direction
*
* @return self
*/
protected function sortCreatedAt($query, $direction)
{
$query->orderBy('organisations.created_at', $direction)
->orderby('organisations.id', $direction);
return $this;
}
}

View File

@@ -0,0 +1,345 @@
{% extends "@admin/pages/dashboard.html.twig" %}
{% block body_matter %}
{% block info_boxes %}
<!-- Info boxes -->
{% if hasRole('site-admin') or hasRole('organisations-admin') %}
<div class="row">
{% if checkAccess('uri_users') %}
{% block info_box_users %}
<div class="col-lg-3 col-md-6 col-xs-12">
<a href="{{site.uri.public}}/users">
<div class="info-box">
<span class="info-box-icon bg-aqua"><i class="fas fa-user fa-fw"></i></span>
<div class="info-box-content">
<span class="info-box-text">{{ translate("USER", 2) }}</span>
<span class="info-box-number">{{counter.users}}</span>
</div>
<!-- /.info-box-content -->
</div>
<!-- /.info-box -->
</a>
</div>
<!-- /.col -->
{% endblock %}
{% endif %}
{% if checkAccess('uri_roles') %}
{% block info_box_roles %}
<div class="col-lg-3 col-md-6 col-xs-12">
<a href="{{site.uri.public}}/roles">
<div class="info-box">
<span class="info-box-icon bg-red"><i class="fas fa-id-card"></i></span>
<div class="info-box-content">
<span class="info-box-text">{{ translate("ROLE", 2) }}</span>
<span class="info-box-number">{{counter.roles}}</span>
</div>
<!-- /.info-box-content -->
</div>
<!-- /.info-box -->
</a>
</div>
<!-- /.col -->
{% endblock %}
{% endif %}
{% if checkAccess('uri_groups') %}
{% block info_box_groups %}
<div class="col-lg-3 col-md-6 col-xs-12">
<a href="{{site.uri.public}}/groups">
<div class="info-box">
<span class="info-box-icon bg-yellow"><i class="fas fa-users"></i></span>
<div class="info-box-content">
<span class="info-box-text">{{ translate("GROUP", 2) }}</span>
<span class="info-box-number">{{counter.groups}}</span>
</div>
<!-- /.info-box-content -->
</div>
<!-- /.info-box -->
</a>
</div>
<!-- /.col -->
{% endblock %}
{% endif %}
{% if checkAccess('uri_organisations') %}
{% block info_box_organisations %}
<div class="col-lg-3 col-md-6 col-xs-12">
<a href="{{site.uri.public}}/organisations">
<div class="info-box">
<span class="info-box-icon bg-green"><i class="fas fa-users"></i></span>
<div class="info-box-content">
<span class="info-box-text">{{ translate("ORGANISATION", 2) }}</span>
<span class="info-box-number">{{counter.organisations}}</span>
</div>
<!-- /.info-box-content -->
</div>
<!-- /.info-box -->
</a>
</div>
<!-- /.col -->
{% endblock %}
{% endif %}
</div>
<!-- /.row -->
{% elseif checkAccess('uri_group', {
'group': current_user.group
}) %}
<div class="row">
{% block info_box_group %}
<div class="col-sm-6 col-xs-12">
<div class="info-box">
<span class="info-box-icon bg-aqua"><i class="{{current_user.group.icon}}"></i></span>
<div class="info-box-content">
<h1>{{current_user.group.name}}</h1>
</div>
<!-- /.info-box-content -->
</div>
<!-- /.info-box -->
</div>
<!-- /.col -->
{% endblock %}
{% block info_box_group_users %}
<div class="col-sm-6 col-xs-12">
<div class="info-box">
<span class="info-box-icon bg-aqua"><i class="fas fa-user fa-fw"></i></span>
<div class="info-box-content">
<span class="info-box-text">{{ translate("USER", 2) }}</span>
<span class="info-box-number">{{current_user.group.users.count}}</span>
</div>
<!-- /.info-box-content -->
</div>
<!-- /.info-box -->
</div>
<!-- /.col -->
{% endblock %}
</div>
<!-- /.row -->
{% endif %}
{% endblock %}
<!-- Main panels -->
<div class="row">
{% if checkAccess('uri_users') or checkAccess('view_system_info') or (hasRole('site-admin') or hasRole('organisations-admin')) %}
<div class="col-md-6 col-sm-12 col-xs-12">
{% if checkAccess('uri_users') %}
<div class="row">
<div class="col-sm-12">
<!-- USERS LIST -->
<div class="box box-info">
<div class="box-header with-border">
<h3 class="box-title">{{translate("USER.LATEST")}}</h3>
</div>
<!-- /.box-header -->
<div class="box-body no-padding">
<ul class="users-list clearfix">
{% for user in users %}
<li>
<img src="{{ user.avatar }}" alt="User Image">
<a class="users-list-name" href="{{site.uri.public}}/users/u/{{user.user_name}}">{{user.first_name}} {{user.last_name}}</a>
<span class="users-list-date">{{ user.registered }}</span>
</li>
{% endfor %}
</ul>
<!-- /.users-list -->
</div>
<!-- /.box-body -->
<div class="box-footer text-center">
<a href="{{site.uri.public}}/users" class="uppercase">{{translate("USER.VIEW_ALL")}}</a>
</div>
<!-- /.box-footer -->
</div>
<!--/.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
{% endif %}
{% if (hasRole('site-admin') or hasRole('organisations-admin')) and checkAccess('uri_activities') %}
{% block latest_organisations %}
<div class="row">
<div class="col-sm-12">
<!-- ORGANISTIONS LIST -->
<div class="box box-info">
<div class="box-header with-border">
<h3 class="box-title">{{translate("ORGANISATION.LATEST")}}</h3>
</div>
<!-- /.box-header -->
<div class="box-body no-padding clearfix">
{% for organisation in organisations %}
<div class="col-sm-6 col-xs-12">
<div class="box box-widget widget-user-2 widget-organisations">
<div class="widget-user-header bg-green">
<h3 class="widget-user-username">{{organisation.name}}</h3>
<h5 class="widget-user-desc">{{organisation.description}}</h5>
</div>
<div class="box-footer no-padding">
<ul class="nav nav-stacked">
<li><a href="{{site.uri.public}}/organisations/o/{{organisation.slug}}">{{translate("CREATED_ON")}} <span class="pull-right badge {% if organisation.flag_approved %}bg-green{% else %}bg-yellow{% endif %}">{{organisation.created_at}}</span></a></li>
<li><a href="{{site.uri.public}}/organisations/o/{{organisation.slug}}">{{translate("MEMBER", 2)}} <span class="pull-right badge bg-aqua">{{organisation.members.count}}</span></a></li>
<li><a href="{{site.uri.public}}/organisations/o/{{organisation.slug}}">{{translate("ADMIN", 2)}} <span class="pull-right badge bg-aqua">{{organisation.administrators.count}}</span></a></li>
<li><a href="{{site.uri.public}}/organisations/o/{{organisation.slug}}">{{translate("PENDING", 2)}} <span class="pull-right badge {% if organisation.pendingMembers.count == 0 %}bg-green{% else %}bg-yellow{% endif %}">{{organisation.pendingMembers.count}}</span></a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
<!-- /.organistions-list -->
</div>
<!-- /.box-body -->
<div class="box-footer text-center">
<a href="{{site.uri.public}}/organisations" class="uppercase">{{translate("ORGANISATION.VIEW_ALL")}}</a>
</div>
<!-- /.box-footer -->
</div>
<!--/.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
{% endblock %}
{% endif %}
{% if checkAccess('view_system_info') %}
<div class="row">
<div class="col-sm-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{translate("SYSTEM_INFO")}}</h3>
</div>
<!-- /.box-header -->
<div class="box-body">
<dl class="dl-horizontal">
<dt>{{translate("SYSTEM_INFO.UF_VERSION")}}</dt>
<dd>{{info.version.UF}}</dd>
<dt>{{translate("SYSTEM_INFO.PHP_VERSION")}}</dt>
<dd>{{info.version.php}}</dd>
<dt>{{translate("SYSTEM_INFO.SERVER")}}</dt>
<dd>{{info.environment.SERVER_SOFTWARE}}</dd>
<dt>{{translate("SYSTEM_INFO.DB_VERSION")}}</dt>
<dd>{{info.version.database.type}} {{info.version.database.version}}</dd>
<dt>{{translate("SYSTEM_INFO.DB_NAME")}}</dt>
<dd>{{info.database.name}}</dd>
<dt>{{translate("SYSTEM_INFO.DIRECTORY")}}</dt>
<dd>{{info.path.project}}</dd>
<dt>{{translate("SYSTEM_INFO.URL")}}</dt>
<dd>{{site.uri.public}}</dd>
<dt>{{translate("SYSTEM_INFO.SPRINKLES")}}</dt>
<dd>
<ul class="list-unstyled">
{% for sprinkle in sprinkles %}
<li>
{{sprinkle}}
</li>
{% endfor %}
</ul>
</dd>
</dl>
</div>
<!-- /.box-body -->
<div class="box-footer text-center">
<a href="javascript:void(0)" class="js-clear-cache uppercase">{{ translate("CACHE.CLEAR") }}</a>
</div>
<!-- /.box-footer -->
</div>
<!--/.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
{% endif %}
</div>
<!-- /.col -->
{% endif %}
{% if checkAccess('uri_activities') %}
<div class="col-md-6 col-sm-12 col-xs-12">
<div id="widget-activities" class="box box-primary">
<div class="box-header">
<h3 class="box-title"><i class="fas fa-tasks fa-fw"></i> {{translate('ACTIVITY', 2)}}</h3>
{% include "tables/table-tool-menu.html.twig" %}
</div>
<div class="box-body">
{% include "tables/activities.html.twig" with {
"table" : {
"id" : "table-activities",
"columns" : ["user"]
}
}
%}
</div>
</div>
</div>
{% elseif (hasRole('site-admin') or hasRole('organisations-admin')) %}
<div class="col-md-6 col-sm-12 col-xs-12">
{{block('latest_organisations')}}
</div>
<!-- /.col -->
{% elseif checkAccess('view_group_field', {
'group': current_user.group,
'property': 'users'
}) %}
<div class="col-md-6 col-sm-12 col-xs-12">
<div id="widget-group-users" class="box box-primary">
<div class="box-header">
<h3 class="box-title"><i class="fas fa-fw fa-user"></i> {{translate('USER', 2)}}</h3>
{% include "tables/table-tool-menu.html.twig" %}
</div>
<div class="box-body">
{% include "tables/users.html.twig" with {
"table" : {
"id" : "table-group-users"
}
}
%}
</div>
<div class="box-footer">
<button type="button" class="btn btn-success js-user-create">
<i class="fas fa-plus-square"></i> {{translate("USER.CREATE")}}
</button>
</div>
</div>
</div>
{% else %}
<div class="col-sm-4 col-sm-offset-4 col-xs-12">
<div class="box box-widget widget-user">
<!-- Add the bg color to the header using any of the bg-* classes -->
<div class="widget-user-header bg-black-active">
<h3 class="widget-user-username">
{{translate("WELCOME", {
'first_name': current_user.first_name
})}}
</h3>
</div>
<div class="widget-user-image">
<img class="img-circle" src="{{assets.url('assets://userfrosting/images/cupcake.png')}}" alt="User Avatar">
</div>
<div class="box-footer">
<h4>
{{translate("WELCOME_TO", {
'title': site.title
})}}
</h4>
<p>
{{translate("NO_FEATURES_YET")}}
</p>
</div>
</div>
<!-- /.widget-user -->
</div>
<!-- /.row -->
{% endif %}
</div>
<!-- /.row -->
{% endblock %}

View File

@@ -20,7 +20,9 @@
<thead>
<tr>
<th class="sorter-metatext" data-column-name="name" data-column-template="#{{table.id}}-column-info" data-priority="1">{{translate('ORGANISATION')}} <i class="fas fa-sort"></i></th>
{% if 'description' not in table.hidden_columns %}
<th class="sorter-metatext" data-column-name="description" data-column-template="#{{table.id}}-column-description" data-priority="2">{{translate("DESCRIPTION")}} <i class="fas fa-sort"></i></th>
{% endif %}
{% if hasRole('site-admin') or hasRole('organisations-admin') or (current_user.adminForOrganisations.count > 0) -%}
<th class="filter-select filter-metatext" data-column-name="status" data-column-template="#{{table.id}}-column-status" data-priority="2">{{translate("STATUS")}} <i class="fas fa-sort"></i></th>
{% endif -%}
@@ -28,10 +30,14 @@
<th class="sorter-metanum" data-column-name="member_count" data-column-template="#{{table.id}}-column-member_count" data-priority="2" style="min-width: 180px">{{translate("ORGANISATION.MEMBER_COUNT")}} <i class="fas fa-sort"></i></th>
<th class="sorter-metanum" data-column-name="admin_count" data-column-template="#{{table.id}}-column-admin_count" data-priority="2">{{translate("ORGANISATION.ADMIN_COUNT")}} <i class="fas fa-sort"></i></th>
{% endif -%}
{% if 'join' in table.columns %}
<th data-column-template="#{{table.id}}-column-join" data-column-name="join" data-sorter="false" data-filter="false" data-priority="3">{{translate("JOIN")}}/{{translate("LEAVE")}}</th>
{% endif %}
{% if 'actions' in table.columns %}
{% if hasRole('site-admin') or hasRole('organisations-admin') or (current_user.adminForOrganisations.count > 0) -%}
<th data-column-template="#{{table.id}}-column-actions" data-sorter="false" data-filter="false" data-priority="1">{{translate("ACTIONS")}}</th>
{% endif -%}
{% endif %}
</tr>
</thead>
<tbody>