1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

Datatables using Vue (#2568)

* Vue DataTables

* Vue Datatables - Pagination

* Sort Vue Tables

* Working on Vue Datatables

* Apply filter to vue table

* Search implementation for vue datatables

* Clean up
This commit is contained in:
David Bomba 2018-12-24 08:45:55 +08:00 committed by GitHub
parent f9ea784d63
commit 43342fb98b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 38624 additions and 323 deletions

View File

@ -2,8 +2,95 @@
namespace App\Datatables;
use App\Models\Client;
use App\Utils\Traits\UserSessionAttributes;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ClientDatatable
{
/**
* ?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'));
return response()->json(self::find($company_id, $request->input('filter'))->orderBy($sort_col[0], $sort_col[1])->paginate($request->input('per_page')), 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'
);
/*
if(Auth::user()->account->customFieldsOption('client1_filter')) {
$query->addSelect('clients.custom_value1');
}
if(Auth::user()->account->customFieldsOption('client2_filter')) {
$query->addSelect('clients.custom_value2');
}
$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(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 ($userId) {
$query->where('clients.user_id', '=', $userId);
}
return $query;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Datatables\ClientDatatable;
use App\Http\Requests\Client\EditClientRequest;
use App\Http\Requests\Client\StoreClientRequest;
use App\Http\Requests\Client\UpdateClientRequest;
@ -31,13 +32,14 @@ class ClientController extends Controller
$this->clientRepo = $clientRepo;
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Builder $builder)
public function index()
{
if(request('page'))
return ClientDatatable::query(request(), $this->getCurrentCompanyId());
return view('client.vue_list');
/*
if (request()->ajax()) {
/*
@ -49,7 +51,8 @@ class ClientController extends Controller
});
*/
$clients = Client::query()->where('company_id', '=', $this->getCurrentCompanyId());
/*
$clients = Client::query();
return DataTables::of($clients->get())
->addColumn('full_name', function ($clients) {
@ -95,6 +98,7 @@ class ClientController extends Controller
$data['html'] = $html;
return view('client.list', $data);
*/
}
/**

View File

@ -31,21 +31,9 @@ class RouteServiceProvider extends ServiceProvider
Route::bind('client', function ($value) {
$client = \App\Models\Client::where('id', $this->decodePrimaryKey($value))->first() ?? abort(404);
$client->load('contacts', 'primary_contact');
return $client;
});
Route::bind('c', function ($value) {
$client = \App\Models\Client::where('id', $this->decodePrimaryKey($value))->first() ?? abort(404);
$client->load('contacts', 'primary_contact');
return $client;
});
Route::bind('invoice', function ($value) {
return \App\Models\Invoice::where('id', $this->decodePrimaryKey($value))->first() ?? abort(404);
});

977
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@
"@types/core-js": "^0.9.36",
"@types/jest": "^23.3.9",
"axios": "^0.18",
"babel-preset-stage-2": "^6.24.1",
"bootstrap": "^4.0.0",
"chart.js": "^2.7.2",
"cross-env": "^5.1",
@ -32,7 +33,8 @@
"popper.js": "^1.12",
"simple-line-icons": "2.4.1",
"ts-jest": "^23.10.5",
"vue": "^2.5.17"
"vue": "^2.5.17",
"vuetable-2": "^1.7.5"
},
"dependencies": {
"@types/lodash": "^4.14.118",
@ -43,6 +45,7 @@
"socket.io-client": "^2.1.1",
"ts-loader": "3.5.0",
"typescript": "^3.1.6",
"vue-events": "^3.1.0",
"vue-i18n": "^8.3.0",
"vue-select": "^2.5.1",
"vue-toastr": "^2.0.16"

View File

@ -14979,18 +14979,6 @@ new vue_1.default({
form: new form_1.default(client_object)
};
},
mounted: function () {
//console.log('mounted')
},
beforeMount: function () {
//console.log('before mount')
},
created: function () {
//console.dir('created')
},
updated: function () {
//console.dir('updated')
},
methods: {
remove: function (contact) {
var index = this.form.contacts.indexOf(contact);

View File

@ -14979,18 +14979,6 @@ new vue_1.default({
form: new form_1.default(client_object)
};
},
mounted: function () {
//console.log('mounted')
},
beforeMount: function () {
//console.log('before mount')
},
created: function () {
//console.dir('created')
},
updated: function () {
//console.dir('updated')
},
methods: {
remove: function (contact) {
var index = this.form.contacts.indexOf(contact);

View File

@ -34363,6 +34363,78 @@ module.exports = function(module) {
};
/***/ }),
/***/ "./resources/js/src/bootstrap.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_lodash__ = __webpack_require__("./node_modules/lodash/lodash.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_lodash___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_toastr__ = __webpack_require__("./node_modules/vue-toastr/dist/vue-toastr.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_toastr___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_vue_toastr__);
// lodash handles our translations
// import Toastr
// Import toastr scss file: need webpack sass-loader
__webpack_require__("./node_modules/vue-toastr/src/vue-toastr.scss");
// Register vue component
Vue.component('vue-toastr', __WEBPACK_IMPORTED_MODULE_1_vue_toastr___default.a);
// Global translation helper
Vue.prototype.trans = function (string) {
return __WEBPACK_IMPORTED_MODULE_0_lodash__["get"](i18n, string);
};
window.axios = __webpack_require__("./node_modules/axios/index.js");
window.Vue = __webpack_require__("./node_modules/vue/dist/vue.common.js");
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/* Development only*/
Vue.config.devtools = true;
window.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
};
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just
* a simple convenience so we don't have to attach every token manually.
*/
var token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo'
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// encrypted: true
// });
/***/ }),
/***/ "./resources/js/src/client/client_edit.ts":
@ -34370,28 +34442,14 @@ module.exports = function(module) {
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
/* Allows us to use our native translation easily using {{ trans() }} syntax */
//const _ = require('lodash');
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* Allows us to use our native translation easily using {{ trans() }} syntax */
//const _ = require('lodash');
var _ = __importStar(__webpack_require__("./node_modules/lodash/lodash.js"));
__webpack_require__("./resources/js/src/bootstrap.js");
var vue_1 = __importDefault(__webpack_require__("./node_modules/vue/dist/vue.common.js"));
// import Toastr
var vue_toastr_1 = __importDefault(__webpack_require__("./node_modules/vue-toastr/dist/vue-toastr.js"));
// import toastr scss file: need webpack sass-loader
__webpack_require__("./node_modules/vue-toastr/src/vue-toastr.scss");
// Register vue component
vue_1.default.component('vue-toastr', vue_toastr_1.default);
vue_1.default.prototype.trans = function (string) { return _.get(i18n, string); };
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application

View File

@ -34363,6 +34363,78 @@ module.exports = function(module) {
};
/***/ }),
/***/ "./resources/js/src/bootstrap.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_lodash__ = __webpack_require__("./node_modules/lodash/lodash.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_lodash___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_toastr__ = __webpack_require__("./node_modules/vue-toastr/dist/vue-toastr.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_toastr___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_vue_toastr__);
// lodash handles our translations
// import Toastr
// Import toastr scss file: need webpack sass-loader
__webpack_require__("./node_modules/vue-toastr/src/vue-toastr.scss");
// Register vue component
Vue.component('vue-toastr', __WEBPACK_IMPORTED_MODULE_1_vue_toastr___default.a);
// Global translation helper
Vue.prototype.trans = function (string) {
return __WEBPACK_IMPORTED_MODULE_0_lodash__["get"](i18n, string);
};
window.axios = __webpack_require__("./node_modules/axios/index.js");
window.Vue = __webpack_require__("./node_modules/vue/dist/vue.common.js");
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/* Development only*/
Vue.config.devtools = true;
window.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
};
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just
* a simple convenience so we don't have to attach every token manually.
*/
var token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo'
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// encrypted: true
// });
/***/ }),
/***/ "./resources/js/src/client/client_edit.ts":
@ -34370,28 +34442,14 @@ module.exports = function(module) {
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
/* Allows us to use our native translation easily using {{ trans() }} syntax */
//const _ = require('lodash');
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* Allows us to use our native translation easily using {{ trans() }} syntax */
//const _ = require('lodash');
var _ = __importStar(__webpack_require__("./node_modules/lodash/lodash.js"));
__webpack_require__("./resources/js/src/bootstrap.js");
var vue_1 = __importDefault(__webpack_require__("./node_modules/vue/dist/vue.common.js"));
// import Toastr
var vue_toastr_1 = __importDefault(__webpack_require__("./node_modules/vue-toastr/dist/vue-toastr.js"));
// import toastr scss file: need webpack sass-loader
__webpack_require__("./node_modules/vue-toastr/src/vue-toastr.scss");
// Register vue component
vue_1.default.component('vue-toastr', vue_toastr_1.default);
vue_1.default.prototype.trans = function (string) { return _.get(i18n, string); };
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application

18368
public/js/client_list.js vendored Normal file

File diff suppressed because it is too large Load Diff

18368
public/js/client_list.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

4
public/js/coreui.js vendored
View File

@ -60,7 +60,7 @@
/******/ __webpack_require__.p = "/";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 3);
/******/ return __webpack_require__(__webpack_require__.s = 4);
/******/ })
/************************************************************************/
/******/ ({
@ -12576,7 +12576,7 @@ PerfectScrollbar.prototype.removePsClasses = function removePsClasses () {
/***/ }),
/***/ 3:
/***/ 4:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("./node_modules/@coreui/coreui/dist/js/coreui.js");

View File

@ -60,7 +60,7 @@
/******/ __webpack_require__.p = "/";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 3);
/******/ return __webpack_require__(__webpack_require__.s = 4);
/******/ })
/************************************************************************/
/******/ ({
@ -12576,7 +12576,7 @@ PerfectScrollbar.prototype.removePsClasses = function removePsClasses () {
/***/ }),
/***/ 3:
/***/ 4:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("./node_modules/@coreui/coreui/dist/js/coreui.js");

View File

@ -60,7 +60,7 @@
/******/ __webpack_require__.p = "/";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 2);
/******/ return __webpack_require__(__webpack_require__.s = 3);
/******/ })
/************************************************************************/
/******/ ({
@ -11548,7 +11548,7 @@ new vue_1.default({
/***/ }),
/***/ 2:
/***/ 3:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("./resources/js/src/settings/localization.ts");

View File

@ -60,7 +60,7 @@
/******/ __webpack_require__.p = "/";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 2);
/******/ return __webpack_require__(__webpack_require__.s = 3);
/******/ })
/************************************************************************/
/******/ ({
@ -11548,7 +11548,7 @@ new vue_1.default({
/***/ }),
/***/ 2:
/***/ 3:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("./resources/js/src/settings/localization.ts");

View File

@ -1,8 +1,17 @@
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
// lodash handles our translations
import * as _ from "lodash"
// import Toastr
import Toastr from 'vue-toastr';
// Import toastr scss file: need webpack sass-loader
require('vue-toastr/src/vue-toastr.scss');
// Register vue component
Vue.component('vue-toastr',Toastr);
// Global translation helper
Vue.prototype.trans = string => _.get(i18n, string);
window.axios = require('axios');
window.Vue = require('vue');
@ -16,6 +25,7 @@ window.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN' : document.querySelector('meta[name="csrf-token"]').getAttribute('content')
};
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just

View File

@ -21,18 +21,6 @@ declare var hashed_id: string;
form: new Form(<Client>client_object)
}
},
mounted(this: any) {
//console.log('mounted')
},
beforeMount: function () {
//console.log('before mount')
},
created:function() {
//console.dir('created')
},
updated:function() {
//console.dir('updated')
},
methods:{
remove(this:any, contact:any){
let index = this.form.contacts.indexOf(contact);

View File

@ -1,19 +1,12 @@
/* Allows us to use our native translation easily using {{ trans() }} syntax */
//const _ = require('lodash');
import * as _ from "lodash"
require('../bootstrap');
/* Must be declare in every child view*/
declare var i18n;
import Vue from 'vue';
import axios from 'axios';
// import Toastr
import Toastr from 'vue-toastr';
// import toastr scss file: need webpack sass-loader
require('vue-toastr/src/vue-toastr.scss');
// Register vue component
Vue.component('vue-toastr',Toastr);
Vue.prototype.trans = string => _.get(i18n, string);
/**
* Next, we will create a fresh Vue application instance and attach it to
@ -25,7 +18,6 @@ Vue.component('client-address', require('../components/client/ClientAddress.vue'
Vue.component('generic-address', require('../components/generic/Address.vue'));
Vue.component('client-edit-form', require('../components/client/ClientEditForm.vue'));
Vue.component('contact-edit', require('../components/client/ClientContactEdit.vue'));
window.onload = function () {

View File

@ -0,0 +1,17 @@
//import * as Vue from 'vue';
import Vue from 'vue';
import axios from 'axios';
Vue.component('client-list', require('../components/client/ClientList.vue'));
Vue.component('vuetable', require('vuetable-2/src/components/Vuetable'));
Vue.component('vuetable-pagination', require('vuetable-2/src/components/VuetablePagination'));
Vue.component('vuetable-pagination-bootstrap', require('../components/util/VuetablePaginationBootstrap'));
Vue.component('vuetable-filter-bar', require('../components/util/VuetableFilterBar'));
window.onload = function () {
const app = new Vue({
el: '#client_list'
});
}

View File

@ -1,7 +1,6 @@
<template>
<form @submit.prevent="onSubmit" @keydown="form.errors.clear($event.target.name)">
<div class="row">
<!-- Client Details and Address Column -->
<div class="col-md-6">
@ -46,17 +45,12 @@
</div>
</form>
</template>
<script lang="ts">
import Vue from 'vue';
import axios from 'axios';
import Form from '../../utils/form';
import Client from '../../models/client-model';

View File

@ -0,0 +1,197 @@
<template>
<div>
<vuetable-filter-bar></vuetable-filter-bar>
<vuetable ref="vuetable"
api-url="/clients"
:fields="fields"
:per-page="20"
:sort-order="sortOrder"
:append-params="moreParams"
pagination-path=""
@vuetable:pagination-data="onPaginationData"></vuetable>
<div class="vuetable-pagination ui basic segment grid">
<vuetable-pagination-info ref="paginationInfo"></vuetable-pagination-info>
<vuetable-pagination ref="pagination"
:css="css.pagination"
@vuetable-pagination:change-page="onChangePage"></vuetable-pagination>
</div>
</div>
</template>
<script lang="ts">
import Vuetable from 'vuetable-2/src/components/Vuetable.vue'
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'
Vue.use(VueEvents)
export default {
components: {
Vuetable,
VuetablePagination,
VuetablePaginationInfo
},
data () {
return {
sortOrder: [
{
field: 'name',
sortField: 'name',
direction: 'asc'
}
],
moreParams: {},
fields: [
{
name: '__checkbox', // <----
title: '',
titleClass: 'center aligned',
dataClass: 'center aligned'
},
{
name: 'name',
sortField: 'name',
dataClass: 'center aligned'
},
{
name: 'contact',
sortField: 'contact',
dataClass: 'center aligned'
},
{
name: 'email',
sortField: 'email',
dataClass: 'center aligned'
},
{
name: 'client_created_at',
title: 'Date created',
sortField: 'client_created_at',
dataClass: 'center aligned'
},
{
name: 'last_login',
title: 'Last login',
sortField: 'last_login',
dataClass: 'center aligned'
},
{
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: '',
},
}
}
}
},
//props: ['list'],
mounted() {
this.$events.$on('filter-set', eventData => this.onFilterSet(eventData))
this.$events.$on('filter-reset', e => this.onFilterReset())
},
beforeMount: function () {
},
methods: {
onPaginationData (paginationData : any) {
this.$refs.pagination.setPaginationData(paginationData)
this.$refs.paginationInfo.setPaginationData(paginationData)
},
onChangePage (page : any) {
this.$refs.vuetable.changePage(page)
},
onFilterSet (filterText) {
this.moreParams = {
'filter': filterText
}
Vue.nextTick( () => this.$refs.vuetable.refresh())
},
onFilterReset () {
this.moreParams = {}
Vue.nextTick( () => this.$refs.vuetable.refresh())
}
}
}
</script>
<style>
.pagination {
margin: 0;
float: right;
}
.pagination a.page {
border: 1px solid lightgray;
border-radius: 3px;
padding: 5px 10px;
margin-right: 2px;
}
.pagination a.page.active {
color: white;
background-color: #337ab7;
border: 1px solid lightgray;
border-radius: 3px;
padding: 5px 10px;
margin-right: 2px;
}
.pagination a.btn-nav {
border: 1px solid lightgray;
border-radius: 3px;
padding: 5px 7px;
margin-right: 2px;
}
.pagination a.btn-nav.disabled {
color: lightgray;
border: 1px solid lightgray;
border-radius: 3px;
padding: 5px 7px;
margin-right: 2px;
cursor: not-allowed;
}
.pagination-info {
float: left;
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div class="container">
<div class="row">
<div class="col-md-9" style="padding:10px;">
<div class="input-group">
<input type="text" v-model="filterText" class="form-control" @keyup.enter="doFilter" placeholder="search">
<button class="btn btn-primary" @click="doFilter">Go</button>
<button class="btn btn-light" @click="resetFilter">Reset</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default {
data () {
return {
filterText: ''
}
},
methods: {
doFilter () {
this.$events.fire('filter-set', this.filterText)
},
resetFilter () {
this.filterText = '' // clear the text in text input
this.$events.fire('filter-reset')
}
}
}
</script>
<style>
.form-inline > * {
margin:5px 10px;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<ul class="pagination">
<li :class="{'disabled': isOnFirstPage}">
<a href="" @click.prevent="loadPage('prev')">
<span>&laquo;</span>
</a>
</li>
<template v-if="notEnoughPages">
<li v-for="n in totalPage" :class="{'active': isCurrentPage(n)}">
<a @click.prevent="loadPage(n)" v-html="n"></a>
</li>
</template>
<template v-else>
<li v-for="n in windowSize" :class="{'active': isCurrentPage(windowStart+n-1)}">
<a @click.prevent="loadPage(windowStart+n-1)" v-html="windowStart+n-1"></a>
</li>
</template>
<li :class="{'disabled': isOnLastPage}">
<a href="" @click.prevent="loadPage('next')">
<span>&raquo;</span>
</a>
</li>
</ul>
</template>
<script>
import VuetablePaginationMixin from 'vuetable-2/src/components/VuetablePaginationMixin'
export default {
mixins: [VuetablePaginationMixin]
}
</script>

4
resources/js/src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

View File

@ -20,9 +20,9 @@
</div>
</div>
<div id="ui-view">
<div id="ui-view" style="padding-top:20px;">
<div class="animated fadeIn">
<div class="row col-md-12 card">
<div class="col-md-12 card">
{!! $html->table() !!}

View File

@ -0,0 +1,38 @@
@extends('layouts.master', ['header' => $header])
@section('head')
@endsection
@section('body')
@parent
<main class="main" >
<!-- Breadcrumb-->
{{ Breadcrumbs::render('clients') }}
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<button class="btn btn-primary btn-lg pull-right">{{ trans('texts.new_client') }}</button>
</div>
</div>
<div id="client_list" style="padding-top:20px;">
<div class="animated fadeIn">
<div class="col-md-12 card">
<client-list></client-list>
</div>
</div>
</div>
</div>
</main>
<script defer src=" {{ mix('/js/client_list.min.js') }}"></script>
@endsection
@section('footer')
@endsection

View File

@ -0,0 +1,19 @@
<table class="action" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<table width="100%" border="0" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<a href="{{ $url }}" class="button button-{{ $color ?? 'primary' }}" target="_blank">{{ $slot }}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@ -0,0 +1,11 @@
<tr>
<td>
<table class="footer" align="center" width="570" cellpadding="0" cellspacing="0">
<tr>
<td class="content-cell" align="center">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>

View File

@ -0,0 +1,7 @@
<tr>
<td class="header">
<a href="{{ $url }}">
{{ $slot }}
</a>
</td>
</tr>

View File

@ -0,0 +1,54 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<style>
@media only screen and (max-width: 600px) {
.inner-body {
width: 100% !important;
}
.footer {
width: 100% !important;
}
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
</style>
<table class="wrapper" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<table class="content" width="100%" cellpadding="0" cellspacing="0">
<!-- Email Body -->
<tr>
<td class="body" width="100%" cellpadding="0" cellspacing="0">
<table class="inner-body" align="center" width="570" cellpadding="0" cellspacing="0">
<!-- Body content -->
<tr>
<td class="content-cell">
{{ Illuminate\Mail\Markdown::parse($slot) }}
{{ $subcopy ?? '' }}
</td>
</tr>
</table>
</td>
</tr>
{{ $footer ?? '' }}
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,27 @@
@component('mail::layout')
{{-- Header --}}
@slot('header')
@component('mail::header', ['url' => config('app.url')])
{{ config('app.name') }}
@endcomponent
@endslot
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
{{ $subcopy }}
@endcomponent
@endslot
@endisset
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endcomponent
@endslot
@endcomponent

View File

@ -0,0 +1,13 @@
<table class="panel" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td class="panel-content">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td class="panel-item">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@ -0,0 +1,7 @@
<table class="promotion" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>

View File

@ -0,0 +1,13 @@
<table width="100%" border="0" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<a href="{{ $url }}" class="button button-green" target="_blank">{{ $slot }}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@ -0,0 +1,7 @@
<table class="subcopy" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td>
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>

View File

@ -0,0 +1,3 @@
<div class="table">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</div>

View File

@ -0,0 +1,290 @@
/* Base */
body, body *:not(html):not(style):not(br):not(tr):not(code) {
font-family: Avenir, Helvetica, sans-serif;
box-sizing: border-box;
}
body {
background-color: #f5f8fa;
color: #74787E;
height: 100%;
hyphens: auto;
line-height: 1.4;
margin: 0;
-moz-hyphens: auto;
-ms-word-break: break-all;
width: 100% !important;
-webkit-hyphens: auto;
-webkit-text-size-adjust: none;
word-break: break-all;
word-break: break-word;
}
p,
ul,
ol,
blockquote {
line-height: 1.4;
text-align: left;
}
a {
color: #3869D4;
}
a img {
border: none;
}
/* Typography */
h1 {
color: #2F3133;
font-size: 19px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h2 {
color: #2F3133;
font-size: 16px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h3 {
color: #2F3133;
font-size: 14px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
p {
color: #74787E;
font-size: 16px;
line-height: 1.5em;
margin-top: 0;
text-align: left;
}
p.sub {
font-size: 12px;
}
img {
max-width: 100%;
}
/* Layout */
.wrapper {
background-color: #f5f8fa;
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.content {
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
/* Header */
.header {
padding: 25px 0;
text-align: center;
}
.header a {
color: #bbbfc3;
font-size: 19px;
font-weight: bold;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
/* Body */
.body {
background-color: #FFFFFF;
border-bottom: 1px solid #EDEFF2;
border-top: 1px solid #EDEFF2;
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.inner-body {
background-color: #FFFFFF;
margin: 0 auto;
padding: 0;
width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
}
/* Subcopy */
.subcopy {
border-top: 1px solid #EDEFF2;
margin-top: 25px;
padding-top: 25px;
}
.subcopy p {
font-size: 12px;
}
/* Footer */
.footer {
margin: 0 auto;
padding: 0;
text-align: center;
width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
}
.footer p {
color: #AEAEAE;
font-size: 12px;
text-align: center;
}
/* Tables */
.table table {
margin: 30px auto;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.table th {
border-bottom: 1px solid #EDEFF2;
padding-bottom: 8px;
margin: 0;
}
.table td {
color: #74787E;
font-size: 15px;
line-height: 18px;
padding: 10px 0;
margin: 0;
}
.content-cell {
padding: 35px;
}
/* Buttons */
.action {
margin: 30px auto;
padding: 0;
text-align: center;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.button {
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
color: #FFF;
display: inline-block;
text-decoration: none;
-webkit-text-size-adjust: none;
}
.button-blue,
.button-primary {
background-color: #3097D1;
border-top: 10px solid #3097D1;
border-right: 18px solid #3097D1;
border-bottom: 10px solid #3097D1;
border-left: 18px solid #3097D1;
}
.button-green,
.button-success {
background-color: #2ab27b;
border-top: 10px solid #2ab27b;
border-right: 18px solid #2ab27b;
border-bottom: 10px solid #2ab27b;
border-left: 18px solid #2ab27b;
}
.button-red,
.button-error {
background-color: #bf5329;
border-top: 10px solid #bf5329;
border-right: 18px solid #bf5329;
border-bottom: 10px solid #bf5329;
border-left: 18px solid #bf5329;
}
/* Panels */
.panel {
margin: 0 0 21px;
}
.panel-content {
background-color: #EDEFF2;
padding: 16px;
}
.panel-item {
padding: 0;
}
.panel-item p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}
/* Promotions */
.promotion {
background-color: #FFFFFF;
border: 2px dashed #9BA2AB;
margin: 0;
margin-bottom: 25px;
margin-top: 25px;
padding: 24px;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.promotion h1 {
text-align: center;
}
.promotion p {
font-size: 15px;
text-align: center;
}

View File

@ -0,0 +1 @@
{{ $slot }}: {{ $url }}

View File

@ -0,0 +1 @@
{{ $slot }}

View File

@ -0,0 +1 @@
[{{ $slot }}]({{ $url }})

View File

@ -0,0 +1,9 @@
{!! strip_tags($header) !!}
{!! strip_tags($slot) !!}
@isset($subcopy)
{!! strip_tags($subcopy) !!}
@endisset
{!! strip_tags($footer) !!}

View File

@ -0,0 +1,27 @@
@component('mail::layout')
{{-- Header --}}
@slot('header')
@component('mail::header', ['url' => config('app.url')])
{{ config('app.name') }}
@endcomponent
@endslot
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
{{ $subcopy }}
@endcomponent
@endslot
@endisset
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endcomponent
@endslot
@endcomponent

View File

@ -0,0 +1 @@
{{ $slot }}

View File

@ -0,0 +1 @@
{{ $slot }}

View File

@ -0,0 +1 @@
[{{ $slot }}]({{ $url }})

View File

@ -0,0 +1 @@
{{ $slot }}

View File

@ -0,0 +1 @@
{{ $slot }}

View File

@ -43,7 +43,6 @@ Route::group(['middleware' => ['auth:user', 'db']], function () {
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('c', 'CController'); // 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');

2
webpack.mix.js vendored
View File

@ -29,6 +29,7 @@ mix.webpackConfig({
mix.js('resources/js/src/client/client_edit.ts', 'public/js');
mix.js('resources/js/src/client/client_create.ts', 'public/js');
mix.js('resources/js/src/client/client_list.ts', 'public/js');
mix.js('resources/js/src/settings/localization.ts', 'public/js');
mix.js('node_modules/@coreui/coreui/dist/js/coreui.js', 'public/js');
@ -40,6 +41,7 @@ mix.minify('public/js/ninja.js');
mix.minify('public/js/coreui.js');
mix.minify('public/js/client_edit.js');
mix.minify('public/js/client_create.js');
mix.minify('public/js/client_list.js');
mix.minify('public/js/localization.js');
mix.styles([