diff --git a/.travis.yml b/.travis.yml index 83953d065523..ce469aa3596f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,6 +54,7 @@ before_script: - php artisan migrate --database=db-ninja-02 --seed --no-interaction - php artisan optimize - npm install + - npm install @types/bluebird @types/core-js@0.9.36 - npm run production # migrate and seed the database # Start webserver on ninja.test:8000 diff --git a/README.md b/README.md index e026d83f7734..c2cf042b1471 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![codecov](https://codecov.io/gh/invoiceninja/invoiceninja/branch/v5.0/graph/badge.svg)](https://codecov.io/gh/invoiceninja/invoiceninja) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/d39acb4bf0f74a0698dc77f382769ba5)](https://www.codacy.com/app/turbo124/invoiceninja?utm_source=github.com&utm_medium=referral&utm_content=invoiceninja/invoiceninja&utm_campaign=Badge_Grade) -**Invoice Ninja v 5.0** is coming soon! +**Invoice Ninja v 2.0** is coming soon! We will be using the lessons learnt in Invoice Ninja 4.0 to build a bigger better platform to work from. If you would like to contribute to the project we will gladly accept contributions for code, user guides, bug tracking and feedback! Please consider the following guidelines prior to submitting a pull request: @@ -22,8 +22,8 @@ Where practical code should be strongly typed, ie your methods must return a typ `public function doThis() : void` -PHP > 7.1 allows the return type Nullable so there should be no circumstance a type cannot be return by using the following: +PHP >= 7.1 allows the return type Nullable so there should be no circumstance a type cannot be return by using the following: `public function doThat() ?:string` -Please include tests with PRs to ensure your code works well and integrates with the rest of the project. Please ensure suitable unit/functional/acceptance tests are included to provide code coverage. +To improve chances of PRs being merged please include teststo ensure your code works well and integrates with the rest of the project. diff --git a/app/Datatables/ClientDatatable.php b/app/Datatables/ClientDatatable.php index 35b3ff48f991..dd4673e72c96 100644 --- a/app/Datatables/ClientDatatable.php +++ b/app/Datatables/ClientDatatable.php @@ -3,94 +3,158 @@ namespace App\Datatables; use App\Models\Client; +use App\Utils\Traits\MakesHash; use App\Utils\Traits\UserSessionAttributes; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; -class ClientDatatable +class ClientDatatable extends EntityDatatable { + use MakesHash; + use MakesActionMenu; - /** - * ?sort=&page=1&per_page=20 - */ - public static function query(Request $request, int $company_id) - { - /** - * - * $sort_col is returned col|asc - * needs to be exploded - * - */ - $sort_col = explode("|", $request->input('sort')); + /** + * ?sort=&page=1&per_page=20 + */ + public function query(Request $request, int $company_id) + { + /** + * + * $sort_col is returned col|asc + * needs to be exploded + * + */ + $sort_col = explode("|", $request->input('sort')); - return response()->json(self::find($company_id, $request->input('filter'))->orderBy($sort_col[0], $sort_col[1])->paginate($request->input('per_page')), 200); - } + $data = $this->find($company_id, $request->input('filter')) + ->orderBy($sort_col[0], $sort_col[1]) + ->paginate($request->input('per_page')); + + return response() + ->json($this->buildActionColumn($data), 200); + + } - private static function find(int $company_id, $filter, $userId = false) - { - $query = DB::table('clients') - ->join('companies', 'companies.id', '=', 'clients.company_id') - ->join('client_contacts', 'client_contacts.client_id', '=', 'clients.id') - ->where('clients.company_id', '=', $company_id) - ->where('client_contacts.is_primary', '=', true) - ->where('client_contacts.deleted_at', '=', null) - //->whereRaw('(clients.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices - ->select( - DB::raw('COALESCE(clients.currency_id, companies.currency_id) currency_id'), - DB::raw('COALESCE(clients.country_id, companies.country_id) country_id'), - DB::raw("CONCAT(COALESCE(client_contacts.first_name, ''), ' ', COALESCE(client_contacts.last_name, '')) contact"), - 'clients.id', - 'clients.name', - 'clients.private_notes', - 'client_contacts.first_name', - 'client_contacts.last_name', - 'clients.balance', - 'clients.last_login', - 'clients.created_at', - 'clients.created_at as client_created_at', - 'client_contacts.phone', - 'client_contacts.email', - 'clients.deleted_at', - 'clients.is_deleted', - 'clients.user_id', - 'clients.id_number' - ); + private function find(int $company_id, $filter, $userId = false) + { + $query = DB::table('clients') + ->join('companies', 'companies.id', '=', 'clients.company_id') + ->join('client_contacts', 'client_contacts.client_id', '=', 'clients.id') + ->where('clients.company_id', '=', $company_id) + ->where('client_contacts.is_primary', '=', true) + ->where('client_contacts.deleted_at', '=', null) + //->whereRaw('(clients.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices + ->select( + DB::raw('COALESCE(clients.currency_id, companies.currency_id) currency_id'), + DB::raw('COALESCE(clients.country_id, companies.country_id) country_id'), + DB::raw("CONCAT(COALESCE(client_contacts.first_name, ''), ' ', COALESCE(client_contacts.last_name, '')) contact"), + 'clients.id', + 'clients.name', + 'clients.private_notes', + 'client_contacts.first_name', + 'client_contacts.last_name', + 'clients.balance', + 'clients.last_login', + 'clients.created_at', + 'clients.created_at as client_created_at', + 'client_contacts.phone', + 'client_contacts.email', + 'clients.deleted_at', + 'clients.is_deleted', + 'clients.user_id', + 'clients.id_number' + ); /* - if(Auth::user()->account->customFieldsOption('client1_filter')) { - $query->addSelect('clients.custom_value1'); - } + if(Auth::user()->account->customFieldsOption('client1_filter')) { + $query->addSelect('clients.custom_value1'); + } - if(Auth::user()->account->customFieldsOption('client2_filter')) { - $query->addSelect('clients.custom_value2'); - } + if(Auth::user()->account->customFieldsOption('client2_filter')) { + $query->addSelect('clients.custom_value2'); + } - $this->applyFilters($query, ENTITY_CLIENT); + $this->applyFilters($query, ENTITY_CLIENT); */ - if ($filter) { - $query->where(function ($query) use ($filter) { - $query->where('clients.name', 'like', '%'.$filter.'%') - ->orWhere('clients.id_number', 'like', '%'.$filter.'%') - ->orWhere('client_contacts.first_name', 'like', '%'.$filter.'%') - ->orWhere('client_contacts.last_name', 'like', '%'.$filter.'%') - ->orWhere('client_contacts.email', 'like', '%'.$filter.'%'); - }); + if ($filter) { + $query->where(function ($query) use ($filter) { + $query->where('clients.name', 'like', '%'.$filter.'%') + ->orWhere('clients.id_number', 'like', '%'.$filter.'%') + ->orWhere('client_contacts.first_name', 'like', '%'.$filter.'%') + ->orWhere('client_contacts.last_name', 'like', '%'.$filter.'%') + ->orWhere('client_contacts.email', 'like', '%'.$filter.'%'); + }); /* - if(Auth::user()->account->customFieldsOption('client1_filter')) { - $query->orWhere('clients.custom_value1', 'like' , '%'.$filter.'%'); - } + if(Auth::user()->account->customFieldsOption('client1_filter')) { + $query->orWhere('clients.custom_value1', 'like' , '%'.$filter.'%'); + } - if(Auth::user()->account->customFieldsOption('client2_filter')) { - $query->orWhere('clients.custom_value2', 'like' , '%'.$filter.'%'); - } + if(Auth::user()->account->customFieldsOption('client2_filter')) { + $query->orWhere('clients.custom_value2', 'like' , '%'.$filter.'%'); + } */ - } + } - if ($userId) { - $query->where('clients.user_id', '=', $userId); - } + if ($userId) { + $query->where('clients.user_id', '=', $userId); + } + + return $query; + } + + /** + * Returns the action dropdown menu + * + * @param $data Std Class of client datatable rows + * @return object Rendered action column items + */ + private function buildActionColumn($data) : object + { + + //if(auth()->user()->is_admin()) + //todo permissions are only mocked here, when user permissions have been implemented this needs to be refactored. + + $permissions = [ + 'view_client', + 'edit_client', + 'create_task', + 'create_invoice', + 'create_payment', + 'create_credit', + 'create_expense' + ]; + + $requested_actions = [ + 'view_client_client_id', + 'edit_client_client_id', + 'create_task_client_id', + 'create_invoice_client_id', + 'create_payment_client_id', + 'create_credit_client_id', + 'create_expense_client_id' + ]; + + $is_admin = false; + + $actions = $this->filterActions($requested_actions, $permissions, $is_admin); + + $data->map(function ($row) use ($actions) { + + $updated_actions = $actions->map(function ($action) use($row){ + + $action['url'] = route($action['route'], [$action['key'] => $this->encodePrimaryKey($row->id)]); + return $action; + + }); + + $row->actions = $updated_actions; + + return $row; + }); + + return $data; + + } - return $query; - } } \ No newline at end of file diff --git a/app/Datatables/EntityDataTable.php b/app/Datatables/EntityDataTable.php index 9e943f1afaaa..fef3431fb031 100644 --- a/app/Datatables/EntityDataTable.php +++ b/app/Datatables/EntityDataTable.php @@ -6,33 +6,6 @@ namespace App\Datatables; class EntityDatatable { - /** - * Returns the columns to be displayed and their key/values - * @return array Columns and key/value option pairs - * - * To be used to show/hide columns - */ - public function columns() - { - - } - - /** - * Display options for the ajax request - * @return array url, type, data - */ - public function ajax() - { - - } - - /** - * Builds the datatable - * @return DataTable returns a DataTable instance - */ - public function build() - { - - } + } \ No newline at end of file diff --git a/app/Datatables/MakesActionMenu.php b/app/Datatables/MakesActionMenu.php new file mode 100644 index 000000000000..48394d7b11c2 --- /dev/null +++ b/app/Datatables/MakesActionMenu.php @@ -0,0 +1,61 @@ + 'view_client_client_id', 'permission' => 'view_client', 'route' => 'clients.show', 'key' => 'client_id', 'name' => trans('texts.view')], + ['action' => 'edit_client_client_id', 'permission' => 'edit_client', 'route' => 'clients.edit', 'key' => 'client_id', 'name' => trans('texts.edit')], + ['action' => 'create_task_client_id', 'permission' => 'create_task', 'route' => 'tasks.create', 'key' => 'client_id', 'name' => trans('texts.new_task')], + ['action' => 'create_invoice_client_id', 'permission' => 'create_invoice', 'route' => 'invoices.create', 'key' => 'client_id', 'name' => trans('texts.new_invoice')], + ['action' => 'enter_payment_client_id', 'permission' => 'create_payment', 'route' => 'payments.create', 'key' => 'client_id', 'name' => trans('texts.enter_payment')], + ['action' => 'enter_credit_client_id', 'permission' => 'create_credit', 'route' => 'credits.create', 'key' => 'client_id', 'name' => trans('texts.enter_credit')], + ['action' => 'enter_expense_client_id', 'permission' => 'create_expense', 'route' => 'expenses.create', 'key' => 'client_id', 'name' => trans('texts.enter_expense')] + ]); + + } + + /** + * Checks the user permissions against the collection and returns + * a Collection of available actions\. + * + * @param Collection $actions collection of possible actions + * @param bool $is_admin boolean defining if user is an administrator + * @return Collection collection of filtered actions + */ + private function checkPermissions(Collection $actions, array $permissions, bool $is_admin) :Collection + { + + if($is_admin === TRUE) + return $actions; + + return $actions->whereIn('permission', $permissions); + + } + + /** + * Filters the main actions collection down to the requested + * actions for this menu + * + * @param array $actions Array of actions requested + * @param array $permissions Array of user permissions + * @param bool $is_admin Boolean is_admin + * @return Collection collection of filtered actions available to the user + */ + public function filterActions(array $actions, array $permissions, bool $is_admin) :Collection + { + + return $this->checkPermissions($this->actions()->whereIn('action', $actions), $permissions, $is_admin); + } +} \ No newline at end of file diff --git a/app/Filters/ClientFilters.php b/app/Filters/ClientFilters.php new file mode 100644 index 000000000000..93781926f54e --- /dev/null +++ b/app/Filters/ClientFilters.php @@ -0,0 +1,58 @@ +split($balance); + + return $this->builder->where('balance', $parts->operator, $parts->value); + } + + + /** + * Filter by popularity. + * + //* @param string $order + //* @return Builder + + public function popular($order = 'desc') + { + return $this->builder->orderBy('views', $order); + } + + /** + * Filter by difficulty. + * + * @param string $level + * @return Builder + + public function difficulty($level) + { + return $this->builder->where('difficulty', $level); + } + + /** + * Filter by length. + * + * @param string $order + * @return Builder + + public function length($order = 'asc') + { + return $this->builder->orderBy('length', $order); + } + + */ +} \ No newline at end of file diff --git a/app/Filters/QueryFilters.php b/app/Filters/QueryFilters.php new file mode 100644 index 000000000000..cb6516817d7b --- /dev/null +++ b/app/Filters/QueryFilters.php @@ -0,0 +1,110 @@ +request = $request; + } + + /** + * Apply the filters to the builder. + * + * @param Builder $builder + * @return Builder + */ + public function apply(Builder $builder) + { + $this->builder = $builder; + + foreach ($this->filters() as $name => $value) { + if (! method_exists($this, $name)) { + continue; + } + + if (strlen($value)) { + $this->$name($value); + } else { + $this->$name(); + } + } + + return $this->builder; + } + + /** + * Get all request filters data. + * + * @return array + */ + public function filters() + { + return $this->request->all(); + } + + /** + * Explodes the value by delimiter + * + * @param string $value + * @return array + */ + public function split($value) : stdClass + { + $exploded_array = explode(":", $value); + + $parts = new stdClass; + + $parts->value = $exploded_array[0]; + $parts->operator = $this->operatorConvertor($exploded_array[1]); + + return $parts; + } + + private function operatorConvertor(string $operator) : string + { + switch ($operator) { + case 'lt': + return '<'; + break; + case 'gt': + return '>'; + break; + case 'lte': + return '<='; + break; + case 'gte': + return '>='; + break; + case 'eq': + return '='; + break; + default: + return '='; + break; + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 795ee3348057..3631337baf6d 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -27,16 +27,19 @@ class ClientController extends Controller protected $clientRepo; - public function __construct(ClientRepository $clientRepo) + protected $clientDatatable; + + public function __construct(ClientRepository $clientRepo, ClientDatatable $clientDatatable) { $this->clientRepo = $clientRepo; + $this->clientDatatable = $clientDatatable; } public function index() { if(request('page')) - return ClientDatatable::query(request(), $this->getCurrentCompanyId()); + return $this->clientDatatable->query(request(), $this->getCurrentCompanyId()); return view('client.vue_list'); /* diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index bbd363c6c032..3454edaa059a 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -23,6 +23,7 @@ class DashboardController extends Controller */ public function index() { + // dd(json_decode(auth()->user()->permissions(),true)); return view('dashboard.index'); } diff --git a/app/Models/CompanyUser.php b/app/Models/CompanyUser.php index e5c3d846ce08..f0e7291ba746 100644 --- a/app/Models/CompanyUser.php +++ b/app/Models/CompanyUser.php @@ -13,6 +13,11 @@ class CompanyUser extends BaseModel public function user() { - return $this->hasOne(User::class); + return $this->hasOne(User::class)->withPivot('permissions'); + } + + public function company() + { + return $this->hasOne(Company::class)->withPivot('permissions'); } } diff --git a/app/Models/User.php b/app/Models/User.php index 370ee7c33688..d41306d46a5c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,9 +2,9 @@ namespace App\Models; -use App\Models\Traits\SetsUserSessionAttributes; use App\Models\Traits\UserTrait; use App\Utils\Traits\MakesHash; +use App\Utils\Traits\UserSessionAttributes; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -17,7 +17,8 @@ class User extends Authenticatable implements MustVerifyEmail use SoftDeletes; use PresentableTrait; use MakesHash; - + use UserSessionAttributes; + protected $guard = 'user'; protected $dates = ['deleted_at']; @@ -54,11 +55,30 @@ class User extends Authenticatable implements MustVerifyEmail 'slack_webhook_url', ]; - - public function companies() { - return $this->belongsToMany(Company::class); + return $this->belongsToMany(Company::class)->withPivot('permissions'); + } + + public function company() + { + return $this->companies()->where('company_id', $this->getCurrentCompanyId())->first(); + } + + public function permissions() + { + + $permissions = json_decode($this->company()->pivot->permissions); + + if (! $permissions) + return []; + + return $permissions; + } + + public function is_admin() + { + return $this->company()->pivot->is_admin; } public function contacts() @@ -66,9 +86,22 @@ class User extends Authenticatable implements MustVerifyEmail return $this->hasMany(Contact::class); } - - public function owns($entity) + public function owns($entity) : bool { return ! empty($entity->user_id) && $entity->user_id == $this->id; } + + public function permissionsFlat() + { + return collect($this->permissions())->flatten(); + } + + public function permissionsMap() + { + + $keys = array_values((array) $this->permissions()); + $values = array_fill(0, count($keys), true); + + return array_combine($keys, $values); + } } diff --git a/database/seeds/UsersTableSeeder.php b/database/seeds/UsersTableSeeder.php index d937dc0ae3d8..854da855f6c9 100644 --- a/database/seeds/UsersTableSeeder.php +++ b/database/seeds/UsersTableSeeder.php @@ -37,10 +37,21 @@ class UsersTableSeeder extends Seeder 'confirmation_code' => $this->createDbHash(config('database.default')) ]); + + $userPermissions = collect([ + 'view_invoice', + 'view_client', + 'edit_client', + 'edit_invoice', + 'create_invoice', + 'create_client' + ]); + $user->companies()->attach($company->id, [ 'account_id' => $account->id, 'is_owner' => 1, 'is_admin' => 1, + 'permissions' => $userPermissions->toJson(), 'is_locked' => 0, ]); diff --git a/public/js/client_list.js b/public/js/client_list.js index 8a5b81317bc1..3ab44614667a 100644 --- a/public/js/client_list.js +++ b/public/js/client_list.js @@ -1190,6 +1190,45 @@ Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /***/ }), +/***/ "./node_modules/babel-loader/lib/index.js?{\"cacheDirectory\":true,\"presets\":[[\"env\",{\"modules\":false,\"targets\":{\"browsers\":[\"> 2%\"],\"uglify\":true}}]],\"plugins\":[\"transform-object-rest-spread\",[\"transform-runtime\",{\"polyfill\":false,\"helpers\":false}]]}!./node_modules/vue-loader/lib/selector.js?type=script&index=0!./resources/js/src/components/client/ClientActions.vue": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +/* harmony default export */ __webpack_exports__["default"] = ({ + props: { + rowData: { + type: Object, + required: true + }, + rowIndex: { + type: Number + } + }, + methods: { + itemAction: function itemAction(action, data, index) { + console.log('custom-actions: ' + action, data.name, index); + } + } +}); + +/***/ }), + /***/ "./node_modules/babel-loader/lib/index.js?{\"cacheDirectory\":true,\"presets\":[[\"env\",{\"modules\":false,\"targets\":{\"browsers\":[\"> 2%\"],\"uglify\":true}}]],\"plugins\":[\"transform-object-rest-spread\",[\"transform-runtime\",{\"polyfill\":false,\"helpers\":false}]]}!./node_modules/vue-loader/lib/selector.js?type=script&index=0!./resources/js/src/components/util/VuetablePaginationBootstrap.vue": /***/ (function(module, __webpack_exports__, __webpack_require__) { @@ -2694,6 +2733,21 @@ exports.push([module.i, "\n.form-inline > * {\n margin:5px 10px;\n}\n", ""]); // exports +/***/ }), + +/***/ "./node_modules/css-loader/index.js!./node_modules/vue-loader/lib/style-compiler/index.js?{\"vue\":true,\"id\":\"data-v-d9a40d24\",\"scoped\":false,\"hasInlineConfig\":true}!./node_modules/vue-loader/lib/selector.js?type=styles&index=0!./resources/js/src/components/client/ClientActions.vue": +/***/ (function(module, exports, __webpack_require__) { + +exports = module.exports = __webpack_require__("./node_modules/css-loader/lib/css-base.js")(false); +// imports + + +// module +exports.push([module.i, "\n.custom-actions button.ui.button {\n padding: 8px 8px;\n}\n.custom-actions button.ui.button > i.icon {\n margin: auto !important;\n}\n", ""]); + +// exports + + /***/ }), /***/ "./node_modules/css-loader/lib/css-base.js": @@ -3249,6 +3303,7 @@ var VuetablePagination_vue_1 = __importDefault(__webpack_require__("./node_modul var VuetablePaginationInfo_vue_1 = __importDefault(__webpack_require__("./node_modules/vuetable-2/src/components/VuetablePaginationInfo.vue")); var vue_1 = __importDefault(__webpack_require__("./node_modules/vue/dist/vue.common.js")); var vue_events_1 = __importDefault(__webpack_require__("./node_modules/vue-events/dist/index.js")); +var VuetableCss_1 = __importDefault(__webpack_require__("./resources/js/src/components/util/VuetableCss.ts")); vue_1.default.use(vue_events_1.default); exports.default = { components: { @@ -3258,6 +3313,7 @@ exports.default = { }, data: function () { return { + css: VuetableCss_1.default, sortOrder: [ { field: 'name', @@ -3304,31 +3360,14 @@ exports.default = { name: 'balance', sortField: 'balance', dataClass: 'center aligned' - } - ], - css: { - table: { - tableClass: 'table table-striped table-bordered table-hovered', - loadingClass: 'loading', - ascendingIcon: 'glyphicon glyphicon-chevron-up', - descendingIcon: 'glyphicon glyphicon-chevron-down', - handleIcon: 'glyphicon glyphicon-menu-hamburger', }, - pagination: { - infoClass: 'pull-left', - wrapperClass: 'vuetable-pagination pull-right', - activeClass: 'btn-primary', - disabledClass: 'disabled', - pageClass: 'btn btn-border', - linkClass: 'btn btn-border', - icons: { - first: '', - prev: '', - next: '', - last: '', - }, + { + name: '__component:client-actions', + title: '', + titleClass: 'center aligned', + dataClass: 'center aligned' } - } + ] }; }, //props: ['list'], @@ -4795,6 +4834,7 @@ var render = function() { "per-page": 20, "sort-order": _vm.sortOrder, "append-params": _vm.moreParams, + css: _vm.css.table, "pagination-path": "" }, on: { "vuetable:pagination-data": _vm.onPaginationData } @@ -5060,6 +5100,74 @@ if (false) { /***/ }), +/***/ "./node_modules/vue-loader/lib/template-compiler/index.js?{\"id\":\"data-v-d9a40d24\",\"hasScoped\":false,\"buble\":{\"transforms\":{}}}!./node_modules/vue-loader/lib/selector.js?type=template&index=0!./resources/js/src/components/client/ClientActions.vue": +/***/ (function(module, exports, __webpack_require__) { + +var render = function() { + var _vm = this + var _h = _vm.$createElement + var _c = _vm._self._c || _h + return _c("div", { staticClass: "dropdown" }, [ + _c( + "button", + { + staticClass: "btn btn-secondary dropdown-toggle", + attrs: { + type: "button", + id: "dropdownMenu", + "data-toggle": "dropdown", + "aria-haspopup": "true", + "aria-expanded": "false" + } + }, + [_vm._v("\n\tSelect\n\t")] + ), + _vm._v(" "), + _c( + "div", + { + staticClass: "dropdown-menu", + attrs: { "aria-labelledby": "dropdownMenu" } + }, + [ + _vm._l(_vm.rowData.actions, function(action) { + return _c( + "a", + { staticClass: "dropdown-item", attrs: { href: action.url } }, + [_vm._v(_vm._s(action.name))] + ) + }), + _vm._v(" "), + _c( + "a", + { + staticClass: "dropdown-item", + attrs: { href: "#" }, + on: { + click: function($event) { + _vm.itemAction("view-item", _vm.rowData, _vm.rowIndex) + } + } + }, + [_vm._v("One more item")] + ) + ], + 2 + ) + ]) +} +var staticRenderFns = [] +render._withStripped = true +module.exports = { render: render, staticRenderFns: staticRenderFns } +if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api") .rerender("data-v-d9a40d24", module.exports) + } +} + +/***/ }), + /***/ "./node_modules/vue-style-loader/index.js!./node_modules/css-loader/index.js!./node_modules/vue-loader/lib/style-compiler/index.js?{\"vue\":true,\"id\":\"data-v-15965e3b\",\"scoped\":true,\"hasInlineConfig\":true}!./node_modules/vue-loader/lib/selector.js?type=styles&index=0!./node_modules/vuetable-2/src/components/Vuetable.vue": /***/ (function(module, exports, __webpack_require__) { @@ -5141,6 +5249,33 @@ if(false) { /***/ }), +/***/ "./node_modules/vue-style-loader/index.js!./node_modules/css-loader/index.js!./node_modules/vue-loader/lib/style-compiler/index.js?{\"vue\":true,\"id\":\"data-v-d9a40d24\",\"scoped\":false,\"hasInlineConfig\":true}!./node_modules/vue-loader/lib/selector.js?type=styles&index=0!./resources/js/src/components/client/ClientActions.vue": +/***/ (function(module, exports, __webpack_require__) { + +// style-loader: Adds some css to the DOM by adding a \ No newline at end of file diff --git a/resources/js/src/components/client/ClientList.vue b/resources/js/src/components/client/ClientList.vue index 7db1460846c5..2c675d051a23 100644 --- a/resources/js/src/components/client/ClientList.vue +++ b/resources/js/src/components/client/ClientList.vue @@ -9,6 +9,7 @@ :per-page="20" :sort-order="sortOrder" :append-params="moreParams" + :css="css.table" pagination-path="" @vuetable:pagination-data="onPaginationData"> @@ -33,6 +34,7 @@ import VuetablePagination from 'vuetable-2/src/components/VuetablePagination.vue import VuetablePaginationInfo from 'vuetable-2/src/components/VuetablePaginationInfo.vue' import Vue from 'vue' import VueEvents from 'vue-events' +import VuetableCss from '../util/VuetableCss' Vue.use(VueEvents) @@ -44,6 +46,7 @@ export default { }, data () { return { + css: VuetableCss, sortOrder: [ { field: 'name', @@ -90,31 +93,14 @@ export default { name: 'balance', sortField: 'balance', dataClass: 'center aligned' - } - ], - css: { - table: { - tableClass: 'table table-striped table-bordered table-hovered', - loadingClass: 'loading', - ascendingIcon: 'glyphicon glyphicon-chevron-up', - descendingIcon: 'glyphicon glyphicon-chevron-down', - handleIcon: 'glyphicon glyphicon-menu-hamburger', }, - pagination: { - infoClass: 'pull-left', - wrapperClass: 'vuetable-pagination pull-right', - activeClass: 'btn-primary', - disabledClass: 'disabled', - pageClass: 'btn btn-border', - linkClass: 'btn btn-border', - icons: { - first: '', - prev: '', - next: '', - last: '', - }, + { + name: '__component:client-actions', // <---- + title: '', + titleClass: 'center aligned', + dataClass: 'center aligned' } - } + ] } }, //props: ['list'], diff --git a/resources/js/src/components/util/VuetableCss.ts b/resources/js/src/components/util/VuetableCss.ts new file mode 100644 index 000000000000..1e684a5de88c --- /dev/null +++ b/resources/js/src/components/util/VuetableCss.ts @@ -0,0 +1,23 @@ +export default { + table: { + tableClass: 'table table-bordered table-hover', + loadingClass: 'loading', + ascendingIcon: 'fa fa-angle-double-up', + descendingIcon: 'fa fa-angle-double-down', + handleIcon: 'glyphicon glyphicon-menu-hamburger', + }, + pagination: { + infoClass: 'pull-left', + wrapperClass: 'vuetable-pagination pull-right', + activeClass: 'btn-primary', + disabledClass: 'disabled', + pageClass: 'btn btn-border', + linkClass: 'btn btn-border', + icons: { + first: '', + prev: '', + next: '', + last: '', + }, + } +} \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index f7878084286e..4785206425bf 100644 --- a/routes/web.php +++ b/routes/web.php @@ -41,10 +41,14 @@ Route::group(['middleware' => ['auth:user', 'db']], function () { Route::resource('dashboard', 'DashboardController'); // name = (dashboard. index / create / show / update / destroy / edit Route::get('logout', 'Auth\LoginController@logout')->name('user.logout'); - Route::resource('invoices', 'InvoiceController'); // name = (invoices. index / create / show / update / destroy / edit - Route::resource('clients', 'ClientController'); // name = (clients. index / create / show / update / destroy / edit - Route::resource('user', 'UserProfileController'); // name = (clients. index / create / show / update / destroy / edit - Route::get('settings', 'SettingsController@index')->name('user.settings'); + Route::resource('invoices', 'InvoiceController'); // name = (invoices. index / create / show / update / destroy / edit + Route::resource('clients', 'ClientController'); // name = (clients. index / create / show / update / destroy / edit + Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit + Route::resource('payments', 'PaymentController'); // name = (payments. index / create / show / update / destroy / edit + Route::resource('credits', 'CreditController'); // name = (credits. index / create / show / update / destroy / edit + Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit + Route::resource('user', 'UserProfileController'); // name = (clients. index / create / show / update / destroy / edit + Route::get('settings', 'SettingsController@index')->name('user.settings'); diff --git a/tests/Feature/LoginTest.php b/tests/Feature/LoginTest.php index fa3dd1373d4f..1d0c56b4bc82 100644 --- a/tests/Feature/LoginTest.php +++ b/tests/Feature/LoginTest.php @@ -5,10 +5,10 @@ namespace Tests\Feature; use App\Models\Account; use App\Models\User; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Support\Facades\Session; use Tests\TestCase; -use Illuminate\Foundation\Testing\WithFaker; -use Illuminate\Foundation\Testing\RefreshDatabase; class LoginTest extends TestCase { diff --git a/tests/Unit/CompareCollectionTest.php b/tests/Unit/CompareCollectionTest.php new file mode 100644 index 000000000000..1c6c000f0633 --- /dev/null +++ b/tests/Unit/CompareCollectionTest.php @@ -0,0 +1,138 @@ +map = collect([ + ['action' => 'view_client_client_id', 'permission' => 'view_client', 'route' => 'clients.show', 'key' => 'client_id', 'name' => trans('texts.view')], + ['action' => 'edit_client_client_id', 'permission' => 'edit_client', 'route' => 'clients.edit', 'key' => 'client_id', 'name' => trans('texts.edit')], + ['action' => 'create_task_client_id', 'permission' => 'create_task', 'route' => 'task.create', 'key' => 'client_id', 'name' => trans('texts.new_task')], + ['action' => 'create_invoice_client_id', 'permission' => 'create_invoice', 'route' => 'invoice.create', 'key' => 'client_id', 'name' => trans('texts.new_invoice')], + ['action' => 'enter_payment_client_id', 'permission' => 'create_payment', 'route' => 'payment.create', 'key' => 'client_id', 'name' => trans('texts.enter_payment')], + ['action' => 'enter_credit_client_id', 'permission' => 'create_credit', 'route' => 'credit.create', 'key' => 'client_id', 'name' => trans('texts.enter_credit')], + ['action' => 'enter_expense_client_id', 'permission' => 'create_expense', 'route' => 'expense.create', 'key' => 'client_id', 'name' => trans('texts.enter_expense')] + ]); + + $this->view_permission = ['view_client']; + + $this->edit_permission = ['view_client', 'edit_client']; + + $this->is_admin = true; + + $this->is_not_admin = false; + + } + + public function testCompareResultOfComparison() + { + + $this->assertEquals(7, $this->map->count()); + + } + + public function testViewPermission() + { + + $this->assertEquals(1, $this->checkPermissions($this->view_permission, $this->is_not_admin)->count()); + + } + + public function testViewAndEditPermission() + { + + $this->assertEquals(2, $this->checkPermissions($this->edit_permission, $this->is_not_admin)->count()); + + } + + public function testAdminPermissions() + { + + $this->assertEquals(7, $this->checkPermissions($this->view_permission, $this->is_admin)->count()); + + } + + public function testActionViewClientFilter() + { + $actions = [ + 'view_client_client_id' + ]; + + $this->assertEquals(1, $this->map->whereIn('action', $actions)->count()); + } + + public function testNoActionClientFilter() + { + $actions = [ + '' + ]; + + $this->assertEquals(0, $this->map->whereIn('action', $actions)->count()); + } + + public function testActionsAndPermissionsFilter() + { + $actions = [ + 'view_client_client_id' + + ]; + + $this->filterActions($actions); + + $this->assertEquals(1, $this->checkPermissions($this->view_permission, $this->is_not_admin)->count()); + + } + + public function testActionAndPermissionsFilterFailure() + { + $actions = [ + 'edit_client_client_id' + + ]; + + $data = $this->filterActions($actions); + + $this->assertEquals(0, $data->whereIn('permission', $this->view_permission)->count()); + + } + + public function testEditActionAndPermissionsFilter() + { + $actions = [ + 'edit_client_client_id' + + ]; + + $data = $this->filterActions($actions); + $this->assertEquals(1, $data->whereIn('permission', $this->edit_permission)->count()); + + } + + public function checkPermissions($permission, $is_admin) + { + + if($is_admin === TRUE) + return $this->map; + + return $this->map->whereIn('permission', $permission); + + } + + public function filterActions($actions) + { + return $this->map->whereIn('action', $actions); + } + +}