Modules, VueJS (#2552)

* Fix for comparing delete contacts change diffKeys to diff()

* Client create

* Client Settings

* Working on localization

* Refactor DataTables

* protyping blade vs pure vue

* Rebuild test module

* Generic notes module

* Small Client Notes Module

* Tests for TabMenu Trait

* implements tab pills in client screen

* Integrate Modules
This commit is contained in:
David Bomba 2018-12-13 10:23:21 +11:00 committed by GitHub
parent 17a7f0564e
commit bdb0f43d33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 59413 additions and 296 deletions

View File

View File

@ -0,0 +1,5 @@
<?php
return [
'name' => 'Notes'
];

View File

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class NotesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('notes', function ($table) {
$table->increments('id');
$table->unsignedInteger('client_id')->index();
$table->unsignedInteger('user_id')->index();
$table->string('description');
$table->timestamps();
$table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

View File

@ -0,0 +1,21 @@
<?php
namespace Modules\Notes\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
class NotesDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Model::unguard();
// $this->call("OthersTableSeeder");
}
}

View File

View File

@ -0,0 +1,18 @@
<?php
namespace Modules\Notes\Entities;
use Illuminate\Database\Eloquent\Model;
class Note extends Model
{
protected $guarded = [
'id',
];
public function client()
{
$this->hasOne(App\Models\Client::class);
}
}

View File

View File

@ -0,0 +1,117 @@
<?php
namespace Modules\Notes\Http\Controllers;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\UserSessionAttributes;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use Modules\Notes\Entities\Note;
use Nwidart\Modules\Facades\Module;
use Yajra\DataTables\Html\Builder;
class NotesController extends Controller
{
use UserSessionAttributes;
use MakesHash;
/**
* Display a listing of the resource.
* @return Response
*/
public function index(Builder $builder)
{
if (request()->ajax()) {
$notes = Note::query()->where('company_id', '=', $this->getCurrentCompanyId());
return DataTables::of($notes->get())
->addColumn('created_at', function ($note) {
return $note->created_at;
})
->addColumn('description', function ($note) {
return $note->description;
})
->addColumn('action', function ($note) {
return '<a href="/notes/'. $this->encodePrimaryKey($note->id) .'/edit" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-edit"></i> Edit</a>';
})
->addColumn('checkbox', function ($note){
return '<input type="checkbox" name="bulk" value="'. $note->id .'"/>';
})
->rawColumns(['checkbox', 'action'])
->make(true);
}
$builder->addAction();
$builder->addCheckbox();
$html = $builder->columns([
['data' => 'created_at', 'name' => 'checkbox', 'title' => '', 'searchable' => false, 'orderable' => false],
['data' => 'description', 'name' => 'name', 'title' => trans('texts.name'), 'visible'=> true],
['data' => 'action', 'name' => 'action', 'title' => '', 'searchable' => false, 'orderable' => false],
]);
$builder->ajax([
'url' => route('notes.index'),
'type' => 'GET',
'data' => 'function(d) { d.key = "value"; }',
]);
$data['html'] = $html;
return view('notes::index', $data);
}
/**
* Show the form for creating a new resource.
* @return Response
*/
public function create()
{
return view('notes::create');
}
/**
* Store a newly created resource in storage.
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
}
/**
* Show the specified resource.
* @return Response
*/
public function show()
{
return view('notes::show');
}
/**
* Show the form for editing the specified resource.
* @return Response
*/
public function edit()
{
return view('notes::edit');
}
/**
* Update the specified resource in storage.
* @param Request $request
* @return Response
*/
public function update(Request $request)
{
}
/**
* Remove the specified resource from storage.
* @return Response
*/
public function destroy()
{
}
}

View File

View File

View File

@ -0,0 +1,30 @@
<?php
namespace Modules\Notes\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateNoteRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Modules\Notes\Policies;
use Illuminate\Auth\Access\HandlesAuthorization;
class NotePolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* @return void
*/
public function __construct()
{
//
}
}

View File

View File

@ -0,0 +1,113 @@
<?php
namespace Modules\Notes\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Factory;
class NotesServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Boot the application events.
*
* @return void
*/
public function boot()
{
$this->registerTranslations();
$this->registerConfig();
$this->registerViews();
$this->registerFactories();
$this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->register(RouteServiceProvider::class);
}
/**
* Register config.
*
* @return void
*/
protected function registerConfig()
{
$this->publishes([
__DIR__.'/../Config/config.php' => config_path('notes.php'),
], 'config');
$this->mergeConfigFrom(
__DIR__.'/../Config/config.php', 'notes'
);
}
/**
* Register views.
*
* @return void
*/
public function registerViews()
{
$viewPath = resource_path('views/modules/notes');
$sourcePath = __DIR__.'/../Resources/views';
$this->publishes([
$sourcePath => $viewPath
],'views');
$this->loadViewsFrom(array_merge(array_map(function ($path) {
return $path . '/modules/notes';
}, \Config::get('view.paths')), [$sourcePath]), 'notes');
}
/**
* Register translations.
*
* @return void
*/
public function registerTranslations()
{
$langPath = resource_path('lang/modules/notes');
if (is_dir($langPath)) {
$this->loadTranslationsFrom($langPath, 'notes');
} else {
$this->loadTranslationsFrom(__DIR__ .'/../Resources/lang', 'notes');
}
}
/**
* Register an additional directory of factories.
*
* @return void
*/
public function registerFactories()
{
if (! app()->environment('production')) {
app(Factory::class)->load(__DIR__ . '/../Database/factories');
}
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return [];
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Modules\Notes\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
/**
* The root namespace to assume when generating URLs to actions.
*
* @var string
*/
protected $namespace = 'Modules\Notes\Http\Controllers';
/**
* Called before routes are registered.
*
* Register any model bindings or pattern based filters.
*
* @return void
*/
public function boot()
{
parent::boot();
}
/**
* Define the routes for the application.
*
* @return void
*/
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::middleware('web')
->namespace($this->namespace)
->group(__DIR__ . '/../Routes/web.php');
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapApiRoutes()
{
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(__DIR__ . '/../Routes/api.php');
}
}

View File

View File

View File

View File

View File

View File

@ -0,0 +1,32 @@
@section('head')
@parent
<link rel="stylesheet" href="//cdn.datatables.net/1.10.18/css/dataTables.bootstrap4.min.css">
<script src="//cdn.datatables.net/1.10.18/js/jquery.dataTables.min.js"></script>
<script src="//cdn.datatables.net/1.10.18/js/dataTables.bootstrap4.min.js"></script>
@endsection
@section('body')
@parent
<main class="main" >
<!-- Breadcrumb-->
{{ Breadcrumbs::render('clients') }}
<div class="container-fluid">
<div id="ui-view">
<div class="animated fadeIn">
<div class="row col-lg-12 card">
{!! $html->table() !!}
</div>
</div>
</div>
</div>
</main>
@endsection
@section('footer')
@parent
{!! $html->scripts() !!}
@endsection

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Module Notes</title>
{{-- Laravel Mix - CSS File --}}
{{-- <link rel="stylesheet" href="{{ mix('css/notes.css') }}"> --}}
</head>
<body>
@yield('content')
{{-- Laravel Mix - JS File --}}
{{-- <script src="{{ mix('js/notes.js') }}"></script> --}}
</body>
</html>

View File

@ -0,0 +1,18 @@
<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
Route::middleware('auth:api')->get('/notes', function (Request $request) {
return $request->user();
});
*/

View File

@ -0,0 +1,16 @@
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::group(['middleware' => ['auth:user', 'db'], 'prefix' => 'notes'], function() {
Route::get('/', 'NotesController@index')->name('notes.index');
});

View File

View File

@ -0,0 +1,80 @@
<?php
namespace Modules\Notes\Tests;
use App\Models\Client;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Nwidart\Modules\Facades\Module;
use Tests\TestCase;
class CheckMenuModulesTest extends TestCase
{
public function setUp()
{
parent::setUp();
}
/**
* A basic test example.
*
* @return void
*/
public function testModulesAreDetected()
{
$this->assertGreaterThan(0, Module::count());
}
public function testNotesModuleExists()
{
$module = Module::find('Notes');
$this->assertNotNull($module);
}
public function testNoSideBarVariableExists()
{
$module = Module::find('Notes');
$this->assertNotNull($module->get('sidebar'));
}
public function testViewsVariableExistsAndIsArray()
{
$module = Module::find('Notes');
$this->assertTrue(is_array($module->get('views')));
}
public function testViewsVariableExistsAndContainsClients()
{
$module = Module::find('Notes');
$array = $module->get('views');
$this->assertTrue(in_array('client', $array));
}
public function testViewsVariableExistsAndDoesNotContainRandomObject()
{
$module = Module::find('Notes');
$array = $module->get('views');
$this->assertFalse(in_array('foo', $array));
}
public function testResolvingTabMenuCorrectly()
{
$entity = Client::class;
$tabs = [];
foreach (Module::getCached() as $module)
{
if(!$module['sidebar']
&& $module['active'] == 1
&& in_array( strtolower( class_basename($entity) ), $module['views']))
{
$tabs[] = $module;
}
}
$this->assertFalse($module['sidebar']);
$this->assertEquals(1,$module['active']);
$this->assertEquals('client', strtolower(class_basename(Client::class)));
$this->assertTrue( in_array(strtolower(class_basename(Client::class)), $module['views']) );
$this->assertEquals(1, count($tabs));
}
}

View File

@ -0,0 +1,30 @@
{
"name": "invoiceninja/notes",
"description": "",
"authors": [
{
"name": "David Bomba",
"email": "david@invoiceninja.com"
}
],
"extra": {
"laravel": {
"providers": [
"Modules\\Notes\\Providers\\NotesServiceProvider"
],
"aliases": {
}
}
},
"autoload": {
"psr-4": {
"Modules\\Notes\\": ""
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "Tests/"
}
},
}

19
Modules/Notes/module.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "Notes",
"alias": "notes",
"description": "Generic Notes Module",
"keywords": [],
"active": 1,
"order": 0,
"providers": [
"Modules\\Notes\\Providers\\NotesServiceProvider"
],
"aliases": [],
"icon": "bell",
"plural": "notes",
"base-route": "notes",
"settings": false,
"sidebar": false,
"requires": [],
"views": ["client","invoice"]
}

View File

@ -0,0 +1,17 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^5.1.4",
"laravel-mix": "^2.1",
"laravel-mix-merge-manifest": "^0.1.1"
}
}

11
Modules/Notes/webpack.mix.js vendored Normal file
View File

@ -0,0 +1,11 @@
const { mix } = require('laravel-mix');
require('laravel-mix-merge-manifest');
mix.setPublicPath('../../public').mergeManifest();
mix.js(__dirname + '/Resources/assets/js/app.js', 'js/notes.js')
.sass( __dirname + '/Resources/assets/sass/app.scss', 'css/notes.css');
if (mix.inProduction()) {
mix.version();
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Datatables;
class ClientDatatable
{
}

View File

@ -0,0 +1,38 @@
<?php
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()
{
}
}

View File

@ -11,6 +11,7 @@ use App\Models\Client;
use App\Models\ClientContact;
use App\Repositories\ClientRepository;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesMenu;
use App\Utils\Traits\UserSessionAttributes;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
@ -21,6 +22,7 @@ class ClientController extends Controller
{
use UserSessionAttributes;
use MakesHash;
use MakesMenu;
protected $clientRepo;
@ -168,6 +170,8 @@ class ClientController extends Controller
$data = [
'client' => $client,
'settings' => [],
'pills' => $this->makeEntityTabMenu(Client::class),
'hashed_id' => $this->encodePrimarykey($client->id)
];

View File

@ -29,7 +29,7 @@ class TranslationController extends Controller
});
header('Content-Type: text/javascript');
echo('window.i18n = ' . json_encode($strings) . ';');
echo('i18n = ' . json_encode($strings) . ';');
exit();
}

View File

@ -40,8 +40,9 @@ class UpdateClientRequest extends Request
{
return [
'unique' => trans('validation.unique', ['attribute' => 'email']),
'email' => trans('validation.email', ['attribute' => 'email']),
'name.required' =>trans('validation.required', ['attribute' => 'name']),
'required' => trans('validation.required', ['attribute' => 'email']),
'email' => trans('validation.email', ['attribute' => 'email'])
];
}

View File

@ -36,6 +36,16 @@ class RouteServiceProvider extends ServiceProvider
});
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);
});

View File

@ -0,0 +1,47 @@
<?php
namespace App\Utils\Traits;
use Nwidart\Modules\Facades\Module;
/**
* Class MakesMenu
* @package App\Utils\Traits
*/
trait MakesMenu
{
/**
* Builds an array of available modules for this view
* @param string $entity Class name
* @return array of modules
*/
public function makeEntityTabMenu(string $entity) : array
{
$tabs = [];
foreach (Module::getCached() as $module)
{
if(!$module['sidebar']
&& $module['active'] == 1
&& in_array( strtolower( class_basename($entity) ), $module['views']))
{
$tabs[] = $module;
}
}
return $tabs;
}
/**
* Builds an array items to be presented on the sidebar
* @return array menu items
*/
public function makeSideBarMenu()
{
}
}

View File

@ -50,7 +50,8 @@
"database/factories"
],
"psr-4": {
"App\\": "app/"
"App\\": "app/",
"Modules\\": "Modules/"
},
"files": [
"app/Libraries/OFX.php",

188
config/modules.php Normal file
View File

@ -0,0 +1,188 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Module Namespace
|--------------------------------------------------------------------------
|
| Default module namespace.
|
*/
'namespace' => 'Modules',
/*
|--------------------------------------------------------------------------
| Module Stubs
|--------------------------------------------------------------------------
|
| Default module stubs.
|
*/
'stubs' => [
'enabled' => false,
'path' => base_path() . '/vendor/nwidart/laravel-modules/src/Commands/stubs',
'files' => [
'routes/web' => 'Routes/web.php',
'routes/api' => 'Routes/api.php',
'views/index' => 'Resources/views/index.blade.php',
'views/master' => 'Resources/views/layouts/master.blade.php',
'scaffold/config' => 'Config/config.php',
'composer' => 'composer.json',
'assets/js/app' => 'Resources/assets/js/app.js',
'assets/sass/app' => 'Resources/assets/sass/app.scss',
'webpack' => 'webpack.mix.js',
'package' => 'package.json',
],
'replacements' => [
'routes/web' => ['LOWER_NAME', 'STUDLY_NAME'],
'routes/api' => ['LOWER_NAME'],
'webpack' => ['LOWER_NAME'],
'json' => ['LOWER_NAME', 'STUDLY_NAME', 'MODULE_NAMESPACE'],
'views/index' => ['LOWER_NAME'],
'views/master' => ['LOWER_NAME', 'STUDLY_NAME'],
'scaffold/config' => ['STUDLY_NAME'],
'composer' => [
'LOWER_NAME',
'STUDLY_NAME',
'VENDOR',
'AUTHOR_NAME',
'AUTHOR_EMAIL',
'MODULE_NAMESPACE',
],
],
'gitkeep' => true,
],
'paths' => [
/*
|--------------------------------------------------------------------------
| Modules path
|--------------------------------------------------------------------------
|
| This path used for save the generated module. This path also will be added
| automatically to list of scanned folders.
|
*/
'modules' => base_path('Modules'),
/*
|--------------------------------------------------------------------------
| Modules assets path
|--------------------------------------------------------------------------
|
| Here you may update the modules assets path.
|
*/
'assets' => public_path('modules'),
/*
|--------------------------------------------------------------------------
| The migrations path
|--------------------------------------------------------------------------
|
| Where you run 'module:publish-migration' command, where do you publish the
| the migration files?
|
*/
'migration' => base_path('database/migrations'),
/*
|--------------------------------------------------------------------------
| Generator path
|--------------------------------------------------------------------------
| Customise the paths where the folders will be generated.
| Set the generate key to false to not generate that folder
*/
'generator' => [
'config' => ['path' => 'Config', 'generate' => true],
'command' => ['path' => 'Console', 'generate' => true],
'migration' => ['path' => 'Database/Migrations', 'generate' => true],
'seeder' => ['path' => 'Database/Seeders', 'generate' => true],
'factory' => ['path' => 'Database/factories', 'generate' => true],
'model' => ['path' => 'Entities', 'generate' => true],
'controller' => ['path' => 'Http/Controllers', 'generate' => true],
'filter' => ['path' => 'Http/Middleware', 'generate' => true],
'request' => ['path' => 'Http/Requests', 'generate' => true],
'provider' => ['path' => 'Providers', 'generate' => true],
'assets' => ['path' => 'Resources/assets', 'generate' => true],
'lang' => ['path' => 'Resources/lang', 'generate' => true],
'views' => ['path' => 'Resources/views', 'generate' => true],
'test' => ['path' => 'Tests', 'generate' => true],
'repository' => ['path' => 'Repositories', 'generate' => false],
'event' => ['path' => 'Events', 'generate' => false],
'listener' => ['path' => 'Listeners', 'generate' => false],
'policies' => ['path' => 'Policies', 'generate' => false],
'rules' => ['path' => 'Rules', 'generate' => false],
'jobs' => ['path' => 'Jobs', 'generate' => false],
'emails' => ['path' => 'Emails', 'generate' => false],
'notifications' => ['path' => 'Notifications', 'generate' => false],
'resource' => ['path' => 'Transformers', 'generate' => false],
],
],
/*
|--------------------------------------------------------------------------
| Scan Path
|--------------------------------------------------------------------------
|
| Here you define which folder will be scanned. By default will scan vendor
| directory. This is useful if you host the package in packagist website.
|
*/
'scan' => [
'enabled' => false,
'paths' => [
base_path('vendor/*/*'),
],
],
/*
|--------------------------------------------------------------------------
| Composer File Template
|--------------------------------------------------------------------------
|
| Here is the config for composer.json file, generated by this package
|
*/
'composer' => [
'vendor' => 'nwidart',
'author' => [
'name' => 'Nicolas Widart',
'email' => 'n.widart@gmail.com',
],
],
/*
|--------------------------------------------------------------------------
| Caching
|--------------------------------------------------------------------------
|
| Here is the config for setting up caching feature.
|
*/
'cache' => [
'enabled' => true,
'key' => 'laravel-modules',
'lifetime' => 60,
],
/*
|--------------------------------------------------------------------------
| Choose what laravel-modules will register as custom namespaces.
| Setting one to false will require you to register that part
| in your own Service Provider class.
|--------------------------------------------------------------------------
*/
'register' => [
'translations' => true,
/**
* load files on boot or register method
*
* Note: boot not compatible with asgardcms
*
* @example boot|register
*/
'files' => 'register',
],
];

View File

@ -35,6 +35,7 @@
"vue": "^2.5.17"
},
"dependencies": {
"@types/lodash": "^4.14.118",
"@types/node": "^10.12.10",
"hashids": "^1.2.2",
"laravel-echo": "^1.4.0",
@ -43,6 +44,7 @@
"ts-loader": "3.5.0",
"typescript": "^3.1.6",
"vue-i18n": "^8.3.0",
"vue-select": "^2.5.1",
"vue-toastr": "^2.0.16"
}
}

View File

@ -20,6 +20,9 @@
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
<testsuite name="Modules">
<directory suffix="Test.php">./Modules</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">

34984
public/js/client-edit.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -60,7 +60,7 @@
/******/ __webpack_require__.p = "/";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ return __webpack_require__(__webpack_require__.s = 2);
/******/ })
/************************************************************************/
/******/ ({
@ -15244,7 +15244,7 @@ exports.default = Form;
/***/ }),
/***/ 1:
/***/ 2:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("./resources/js/src/client/client_create.ts");

View File

@ -60,7 +60,7 @@
/******/ __webpack_require__.p = "/";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ return __webpack_require__(__webpack_require__.s = 2);
/******/ })
/************************************************************************/
/******/ ({
@ -15244,7 +15244,7 @@ exports.default = Form;
/***/ }),
/***/ 1:
/***/ 2:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("./resources/js/src/client/client_create.ts");

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 = 5);
/******/ })
/************************************************************************/
/******/ ({
@ -12576,7 +12576,7 @@ PerfectScrollbar.prototype.removePsClasses = function removePsClasses () {
/***/ }),
/***/ 3:
/***/ 5:
/***/ (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 = 5);
/******/ })
/************************************************************************/
/******/ ({
@ -12576,7 +12576,7 @@ PerfectScrollbar.prototype.removePsClasses = function removePsClasses () {
/***/ }),
/***/ 3:
/***/ 5:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("./node_modules/@coreui/coreui/dist/js/coreui.js");

11559
public/js/localization.js vendored Normal file

File diff suppressed because one or more lines are too long

11559
public/js/localization.min.js vendored Normal file

File diff suppressed because one or more lines are too long

28
public/js/ninja.js vendored
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 = 4);
/******/ })
/************************************************************************/
/******/ ({
@ -13124,30 +13124,6 @@ module.exports = g;
__webpack_require__("./resources/js/bootstrap.js");
/* Allows us to use our native translation easily using {{ trans() }} syntax */
//const _ = require('lodash');
//Vue.prototype.trans = string => _.get(window.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
* or customize the JavaScript scaffolding to fit your unique needs.
Vue.component('example-component', require('./components/ExampleComponent.vue'));
Vue.component('client-edit', require('./components/client/ClientEdit.vue'));
Vue.component('client-primary-address', require('./components/client/ClientPrimaryAddress.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 () {
var app = new Vue({
el: '#app'
});
};
/***/ }),
/***/ "./resources/js/bootstrap.js":
@ -13204,7 +13180,7 @@ if (token) {
/***/ }),
/***/ 2:
/***/ 4:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("./resources/js/app.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 = 4);
/******/ })
/************************************************************************/
/******/ ({
@ -13124,30 +13124,6 @@ module.exports = g;
__webpack_require__("./resources/js/bootstrap.js");
/* Allows us to use our native translation easily using {{ trans() }} syntax */
//const _ = require('lodash');
//Vue.prototype.trans = string => _.get(window.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
* or customize the JavaScript scaffolding to fit your unique needs.
Vue.component('example-component', require('./components/ExampleComponent.vue'));
Vue.component('client-edit', require('./components/client/ClientEdit.vue'));
Vue.component('client-primary-address', require('./components/client/ClientPrimaryAddress.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 () {
var app = new Vue({
el: '#app'
});
};
/***/ }),
/***/ "./resources/js/bootstrap.js":
@ -13204,7 +13180,7 @@ if (token) {
/***/ }),
/***/ 2:
/***/ 4:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("./resources/js/app.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 = 4);
/******/ })
/************************************************************************/
/******/ ({
@ -13124,30 +13124,6 @@ module.exports = g;
__webpack_require__("./resources/js/bootstrap.js");
/* Allows us to use our native translation easily using {{ trans() }} syntax */
//const _ = require('lodash');
//Vue.prototype.trans = string => _.get(window.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
* or customize the JavaScript scaffolding to fit your unique needs.
Vue.component('example-component', require('./components/ExampleComponent.vue'));
Vue.component('client-edit', require('./components/client/ClientEdit.vue'));
Vue.component('client-primary-address', require('./components/client/ClientPrimaryAddress.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 () {
var app = new Vue({
el: '#app'
});
};
/***/ }),
/***/ "./resources/js/bootstrap.js":
@ -13204,7 +13180,7 @@ if (token) {
/***/ }),
/***/ 2:
/***/ 4:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("./resources/js/app.js");

24
resources/js/app.js vendored
View File

@ -7,27 +7,3 @@
require('./bootstrap');
/* Allows us to use our native translation easily using {{ trans() }} syntax */
//const _ = require('lodash');
//Vue.prototype.trans = string => _.get(window.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
* or customize the JavaScript scaffolding to fit your unique needs.
Vue.component('example-component', require('./components/ExampleComponent.vue'));
Vue.component('client-edit', require('./components/client/ClientEdit.vue'));
Vue.component('client-primary-address', require('./components/client/ClientPrimaryAddress.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 () {
const app = new Vue({
el: '#app'
});
}

View File

@ -0,0 +1,123 @@
<template>
<div>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#billing" role="tab" aria-controls="billing">{{ trans('texts.billing_address') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#shipping" role="tab" aria-controls="shipping">{{ trans('texts.shipping_address') }}</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="billing" role="tabpanel">
<button type="button" class="btn btn-sm btn-light" v-on:click="$emit('copy', 'copy_shipping')"> {{ trans('texts.copy_shipping') }}</button>
<div class="card-body">
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.address1') }}</label>
<div class="col-sm-9">
<input type="text" :placeholder="trans('texts.address1')" v-model="client.address1" class="form-control">
<div v-if="client.errors.has('address1')" class="text-danger" v-text="client.errors.get('address1')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.address2') }}</label>
<div class="col-sm-9">
<input type="text":placeholder="trans('texts.address2')" v-model="client.address2" class="form-control">
<div v-if="client.errors.has('address2')" class="text-danger" v-text="client.errors.get('address2')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.city') }}</label>
<div class="col-sm-9">
<input type="text":placeholder="trans('texts.city')" v-model="client.city" class="form-control">
<div v-if="client.errors.has('city')" class="text-danger" v-text="client.errors.get('city')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.state') }}</label>
<div class="col-sm-9">
<input type="text" :placeholder="trans('texts.state')" v-model="client.state" class="form-control">
<div v-if="client.errors.has('state')" class="text-danger" v-text="client.errors.get('state')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.postal_code') }}</label>
<div class="col-sm-9">
<input type="text" :placeholder="trans('texts.postal_code')" v-model="client.postal_code" class="form-control">
<div v-if="client.errors.has('postal_code')" class="text-danger" v-text="client.errors.get('postal_code')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.country') }}</label>
<div class="col-sm-9">
<input type="text" :placeholder="trans('texts.country')" v-model="client.country" class="form-control">
<div v-if="client.errors.has('country')" class="text-danger" v-text="client.errors.get('country')"></div>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="shipping" role="tabpanel">
<button type="button" class="btn btn-sm btn-light" v-on:click="$emit('copy',' copy_billing')"> {{ trans('texts.copy_billing') }}</button>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.address1') }}</label>
<div class="col-sm-9">
<input type="text" :placeholder="trans('texts.address1')" v-model="client.shipping_address1" class="form-control">
<div v-if="client.errors.has('shipping_address1')" class="text-danger" v-text="client.errors.get('shipping_address1')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.address2') }}</label>
<div class="col-sm-9">
<input type="text" :placeholder="trans('texts.address2')" v-model="client.shipping_address2" class="form-control">
<div v-if="client.errors.has('shipping_address2')" class="text-danger" v-text="client.errors.get('shipping_address2')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.city') }}</label>
<div class="col-sm-9">
<input type="text" :placeholder="trans('texts.city')" v-model="client.shipping_city" class="form-control">
<div v-if="client.errors.has('shipping_city')" class="text-danger" v-text="client.errors.get('shipping_city')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.state') }}</label>
<div class="col-sm-9">
<input type="text" :placeholder="trans('texts.state')" v-model="client.shipping_state" class="form-control">
<div v-if="client.errors.has('shipping_state')" class="text-danger" v-text="client.errors.get('shipping_state')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.postal_code') }}</label>
<div class="col-sm-9">
<input type="text" :placeholder="trans('texts.postal_code')" v-model="client.shipping_postal_code" class="form-control">
<div v-if="client.errors.has('shipping_postal_code')" class="text-danger" v-text="client.errors.get('shipping_postal_code')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.country') }}</label>
<div class="col-sm-9">
<input type="text" :placeholder="trans('texts.country')" v-model="client.shipping_country" class="form-control">
<div v-if="client.errors.has('shipping_country')" class="text-danger" v-text="client.errors.get('shipping_country')"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['client']
}
</script>

View File

@ -3,42 +3,48 @@
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.first_name') }}</label>
<div class="col-sm-9">
<input type="text" name="first_name" :placeholder="trans('texts.first_name')" v-model="contact.first_name" class="form-control" id="first_name">
<input ref="first_name" name="first_name" type="text" :placeholder="trans('texts.first_name')" v-model="contact.first_name" class="form-control">
<div v-if="form.errors.has('contacts.'+error_index+'.first_name')" class="text-danger" v-text="form.errors.get('contacts.'+error_index+'.first_name')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.last_name') }}</label>
<div class="col-sm-9">
<input type="text" name="last_name" :placeholder="trans('texts.last_name')" v-model="contact.last_name" class="form-control" id="last_name">
<input type="text" :placeholder="trans('texts.last_name')" v-model="contact.last_name" class="form-control">
<div v-if="form.errors.has('contacts.'+error_index+'.last_name')" class="text-danger" v-text="form.errors.get('contacts.'+error_index+'.last_name')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.email') }}</label>
<div class="col-sm-9">
<input type="email" name="email" :placeholder="trans('texts.email')" v-model="contact.email" class="form-control" id="email">
<input type="email" :placeholder="trans('texts.email')" v-model="contact.email" class="form-control">
<div v-if="form.errors.has('contacts.'+error_index+'.email')" class="text-danger" v-text="form.errors.get('contacts.'+error_index+'.email')"></div>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.phone') }}</label>
<div class="col-sm-9">
<input type="text" name="phone" :placeholder="trans('texts.phone')" v-model="contact.phone" class="form-control" id="phone">
<input type="text" :placeholder="trans('texts.phone')" v-model="contact.phone" class="form-control">
<div v-if="form.errors.has('contacts.'+error_index+'.phone')" class="text-danger" v-text="form.errors.get('contacts.'+error_index+'.phone')"></div>
</div>
</div>
<div class="form-group row" v-if="contact.custom_value1">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.custom_value1') }}</label>
<div class="col-sm-9">
<input type="text" name="custom_value1" :placeholder="trans('texts.custom_value1')" v-model="contact.custom_value1" class="form-control" id="custom_value1">
<input type="text" :placeholder="trans('texts.custom_value1')" v-model="contact.custom_value1" class="form-control">
<div v-if="form.errors.has('contacts.'+error_index+'.custom_value1')" class="text-danger" v-text="form.errors.get('contacts.'+error_index+'.custom_value1')"></div>
</div>
</div>
<div class="form-group row" v-if="contact.custom_value1">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.custom_value2') }}</label>
<div class="col-sm-9">
<input type="text" name="custom_value2" :placeholder="trans('texts.custom_value2')" v-model="contact.custom_value2" class="form-control" id="custom_value2">
<input type="text" :placeholder="trans('texts.custom_value2')" v-model="contact.custom_value2" class="form-control">
<div v-if="form.errors.has('contacts.'+error_index+'.custom_value2')" class="text-danger" v-text="form.errors.get('contacts.'+error_index+'.custom_value2')"></div>
</div>
</div>
@ -50,6 +56,6 @@
<script>
export default {
props: ['contact']
props: ['contact', 'form', 'error_index']
}
</script>

View File

@ -3,8 +3,8 @@
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.client_name') }}</label>
<div class="col-sm-9">
<input type="text" name="name" :placeholder="trans('texts.client_name')" v-model="client.name" class="form-control" id="name">
<div v-if="errors && errors.name" class="text-danger">{{ errors.name[0] }}</div>
<input type="text" :placeholder="trans('texts.client_name')" v-model="client.name" class="form-control">
<div v-if="client.errors.has('name')" class="text-danger" v-text="client.errors.get('name')"></div>
</div>
</div>
@ -12,7 +12,7 @@
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.id_number') }}</label>
<div class="col-sm-9">
<input type="text" name="id_number" :placeholder="trans('texts.id_number')" v-model="client.id_number" class="form-control" id="id_number">
<div v-if="errors && errors.id_number" class="text-danger">{{ errors.id_number[0] }}</div>
<div v-if="client.errors.has('id_number')" class="text-danger" v-text="client.errors.get('id_number')"></div>
</div>
</div>
@ -20,7 +20,7 @@
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.vat_number') }}</label>
<div class="col-sm-9">
<input type="text" name="vat_number" :placeholder="trans('texts.vat_number')" v-model="client.vat_number" class="form-control" id="vat_number">
<div v-if="errors && errors.vat_number" class="text-danger">{{ errors.vat_number[0] }}</div>
<div v-if="client.errors.has('vat_number')" class="text-danger" v-text="client.errors.get('vat_number')"></div>
</div>
</div>
@ -28,7 +28,7 @@
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.website') }}</label>
<div class="col-sm-9">
<input type="text" name="website" :placeholder="trans('texts.website')" v-model="client.website" class="form-control" id="websites">
<div v-if="errors && errors.website" class="text-danger">{{ errors.website[0] }}</div>
<div v-if="client.errors.has('website')" class="text-danger" v-text="client.errors.get('website')"></div>
</div>
</div>
@ -36,7 +36,7 @@
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.custom_value1') }}</label>
<div class="col-sm-9">
<input type="text" name="custom_value1" :placeholder="trans('texts.custom_value1')" v-model="client.custom_value1" class="form-control" id="custom_value1">
<div v-if="errors && errors.custom_value1" class="text-danger">{{ errors.custom_value1[0] }}</div>
<div v-if="client.errors.has('custom_value1')" class="text-danger" v-text="client.errors.get('custom_value1')"></div>
</div>
</div>
@ -44,7 +44,7 @@
<label for="name" class="col-sm-3 col-form-label text-right">{{ trans('texts.custom_value2') }}</label>
<div class="col-sm-9">
<input type="text" name="custom_value2" :placeholder="trans('texts.custom_value2')" v-model="client.custom_value2" class="form-control" id="custom_value2">
<div v-if="errors && errors.custom_value2" class="text-danger">{{ errors.custom_value2[0] }}</div>
<div v-if="client.errors.has('custom_value2')" class="text-danger" v-text="client.errors.get('custom_value2')"></div>
</div>
</div>
</div>

View File

@ -1,36 +1,18 @@
<template>
<form @submit.prevent="submit">
<div class="container-fluid">
<div class="row form-group">
<div class="col-md-12">
<span class="float-right">
<div class="btn-group ml-2">
<button class="btn btn-lg btn-success" type="button" @click="submit"><i class="fa fa-save"></i> {{ trans('texts.save') }}</button>
<button class="btn btn-lg btn-success dropdown-toggle dropdown-toggle-split" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="#"><i class="fa fa-plus-circle"></i> {{ trans('texts.add_contact') }}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">{{ trans('texts.archive_client') }}</a>
<a class="dropdown-item" href="#">{{ trans('texts.delete_client') }}</a>
</div>
</div>
</span>
</div>
</div>
<form @submit.prevent="onSubmit" @keydown="form.errors.clear($event.target.name)">
<div class="row">
<!-- Client Details and Address Column -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary2">{{ trans('texts.edit_client') }}</div>
<client-edit :client="client" :errors="errors"></client-edit>
<client-edit :client="form"></client-edit>
</div>
<div class="card">
<div class="card-header bg-primary2">{{ trans('texts.address') }}</div>
<client-primary-address v-bind:client="client" @copy="copy"></client-primary-address>
<client-address v-bind:client="form" @copy="copy"></client-address>
</div>
</div>
<!-- End Client Details and Address Column -->
@ -40,75 +22,100 @@
<div class="card">
<div class="card-header bg-primary2">{{ trans('texts.contact_information') }}
<span class="float-right">
<button type="button" class="btn btn-primary btn-sm"><i class="fa fa-plus-circle"></i> {{ trans('texts.add_contact') }}</button>
<button type="button" class="btn btn-primary btn-sm" @click="add"><i class="fa fa-plus-circle"></i> {{ trans('texts.add_contact') }}</button>
</span>
</div>
<contact-edit v-for="(contact, index) in client.contacts"
v-bind:contact="contact"
v-bind:index="index"
:key="contact.id"
<contact-edit v-for="(contact, key, index) in form.contacts"
:contact="contact"
:form="form"
:key="contact.id"
:error_index="key"
@remove="remove"></contact-edit>
</div>
</div>
<!-- End Contact Details Column -->
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 text-center">
<button class="btn btn-lg btn-success" type="button" @click="onSubmit"><i class="fa fa-save"></i> {{ trans('texts.save') }}</button>
</div>
</div>
</form>
</template>
<script>
<script lang="ts">
import Vue from 'vue';
import axios from 'axios';
import Form from '../../src/utils/form';
import Client from '../../src/models/client-model';
export default {
data: function () {
return {
'client': [],
'errors': [],
}
},
props: {
clientdata: {
type: [Object,Array],
default: () => []
form: new Form(<Client>this.clientdata)
}
},
props: ['hashed_id', 'clientdata'],
beforeMount: function () {
this.client = this.clientdata;
},
methods: {
copy(type) {
methods:{
remove(this:any, contact:any){
let index = this.form.contacts.indexOf(contact);
this.form.contacts.splice(index, 1);
},
add(this: any){
this.form.contacts.push({first_name: '', last_name: '', email: '', phone: '', id: 0});
window.scrollTo(0, document.body.scrollHeight || document.documentElement.scrollHeight);
this.$nextTick(() => {
let index = this.form.contacts.length - 1;
//this.$refs.first_name[index].$el.focus();
//this.$refs.first_name[index].focus();
});
},
onSubmit() {
this.form.put('/clients/' + this.hashed_id)
.then(response => this.$root.$refs.toastr.s("Saved client"))
.catch(error => {
this.$root.$refs.toastr.e("Error saving client");
});
},
copy(type: any) {
if(type.includes('copy_billing')){
this.client.primary_shipping_location = Object.assign({}, this.client.primary_billing_location);
}else {
this.client.primary_billing_location = Object.assign({}, this.client.primary_shipping_location);
this.form.shipping_address1 = this.form.address1;
this.form.shipping_address2 = this.form.address2;
this.form.shipping_city = this.form.city;
this.form.shipping_state = this.form.state;
this.form.shipping_postal_code = this.form.postal_code;
this.form.shipping_country_id = this.form.country_id;
}else {
this.form.address1 = this.form.shipping_address1;
this.form.address2 = this.form.shipping_address2;
this.form.city = this.form.shipping_city;
this.form.state = this.form.shipping_state;
this.form.postal_code = this.form.shipping_postal_code;
this.form.country_id = this.form.shipping_country_id;
}
},
remove (itemId) {
this.client.contacts = this.client.contacts.filter(function (item) {
return itemId != item.id;
});
},
submit() {
this.errors = {};
axios.put('/clients/' + this.client.hash_id, this.client).then(response => {
this.client = response.data;
console.dir(response);
}).catch(error => {
if (error.response.status === 422) {
this.errors = error.response.data.errors || {};
}
else if(error.response.status === 419) {
//csrf token has expired, we'll need to force a page reload
}
});
},
}
},
created:function() {
//console.dir('created');
},
updated:function() {
//console.dir('updated');
}
}

View File

@ -1,28 +0,0 @@
<template>
<div>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#billing" role="tab" aria-controls="billing">{{ trans('texts.billing_address') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#shipping" role="tab" aria-controls="shipping">{{ trans('texts.shipping_address') }}</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="billing" role="tabpanel">
<button type="button" class="btn btn-sm btn-light" v-on:click="$emit('copy', 'copy_shipping')"> {{ trans('texts.copy_shipping') }}</button>
<generic-address :data="client.primary_billing_location ? client.primary_billing_location : 'null'"></generic-address>
</div>
<div class="tab-pane" id="shipping" role="tabpanel">
<button type="button" class="btn btn-sm btn-light" v-on:click="$emit('copy',' copy_billing')"> {{ trans('texts.copy_billing') }}</button>
<generic-address :data="client.primary_shipping_location ? client.primary_shipping_location : 'null'"></generic-address>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['client']
}
</script>

View File

@ -0,0 +1,37 @@
/* Allows us to use our native translation easily using {{ trans() }} syntax */
//const _ = require('lodash');
import * as _ from "lodash"
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
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
Vue.component('example-component', require('../../components/ExampleComponent.vue'));
Vue.component('client-edit', require('../../components/client/ClientEdit.vue'));
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 () {
const app = new Vue({
el: '#client_e'
});
}

View File

@ -14,7 +14,6 @@ Vue.component('vue-toastr',Toastr);
declare var client_object: any;
declare var hashed_id: string;
new Vue({
el : '#client_edit',
data: function () {

View File

@ -0,0 +1,13 @@
import Vue from 'vue';
import VueSelect from 'vue-select';
Vue.component('v-select', VueSelect.VueSelect)
new Vue({
el: '#localization',
data: {
options: ['jim','bob','frank'],
selected: 'frank',
}
})

View File

@ -1,11 +1,11 @@
@extends('layouts.master', ['header' => $header])
@section('body')
<main class="main" id="client_edit">
<main class="main" id="client_e">
<!-- Breadcrumb-->
{{ Breadcrumbs::render('clients.edit', $client) }}
<vue-toastr ref="toastr"></vue-toastr>
<div class="container-fluid">
@ -13,77 +13,40 @@
<div class="col" style="padding: 0px;">
<ul class="nav nav-pills mb-1" id="pills-tab" role="tablist">
<li class="nav-item">
<a class="nav-link active show" id="pills-home-tab" data-toggle="pill" href="#pills-home" role="tab" aria-controls="pills-home" aria-selected="true"><i class="icon-user"></i> Client</a>
<a class="nav-link active show" id="pills-home-tab" data-toggle="pill" href="#pills-home" role="tab" aria-controls="pills-home" aria-selected="true"><i class="icon-user"></i> {{ trans('texts.client') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" id="pills-contact-tab" data-toggle="pill" href="#pills-contact" role="tab" aria-controls="pills-contact" aria-selected="false"><i class="icon-settings"></i> Settings</a>
<a class="nav-link" id="pills-settings-tab" data-toggle="pill" href="#pills-settings" role="tab" aria-controls="pills-settings" aria-selected="false"><i class="icon-settings"></i> {{ trans('texts.settings') }}</a>
</li>
@foreach($pills as $pill)
<li class="nav-item">
<a class="nav-link" id="pills-{{ $pill['alias'] }}-tab" data-toggle="pill" href="#pills-{{ $pill['alias'] }}" role="tab" aria-controls="pills-{{ $pill['alias'] }}" aria-selected="false"><i class="icon-{{$pill['icon'] }}"></i> {{ $pill['name'] }}</a>
</li>
@endforeach
</ul>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade active show" id="pills-home" role="tabpanel" aria-labelledby="pills-home-tab" style="background-color: #e4e5e6; padding: 0px;">
<form @submit.prevent="onSubmit" @keydown="form.errors.clear($event.target.name)">
<div class="row">
<!-- Client Details and Address Column -->
<div class="col-md-6">
@include('client.partial.client_details', $client)
@include('client.partial.client_location')
</div>
<!-- End Client Details and Address Column -->
<!-- Contact Details Column -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary2">{{ trans('texts.contact_information') }}
<span class="float-right">
<button type="button" class="btn btn-primary btn-sm" @click="add()"><i class="fa fa-plus-circle"></i> {{ trans('texts.add_contact') }}</button>
</span>
</div>
<template v-for="(contact, key, index) in form.contacts">
@include('client.partial.contact_details')
</template>
</div>
</div>
<!-- End Contact Details Column -->
</div>
<div class="row">
<div class="col-md-12 text-center">
<button class="btn btn-lg btn-success" type="button" @click="onSubmit"><i class="fa fa-save"></i> {{ trans('texts.save') }}</button>
</div>
</div>
</form>
<client-edit-form :clientdata="{{ $client }}" :hashed_id="'{{ $hashed_id }}'"></client-edit-form>
</div>
<div class="tab-pane fade" id="pills-contact" role="tabpanel" aria-labelledby="pills-contact-tab">
@foreach($pills as $pill)
<div class="tab-pane fade" id="pills-{{ $pill['alias'] }}" role="tabpanel" aria-labelledby="pills-{{ $pill['alias'] }}-tab">
{{$pill['name']}}
</div>
@endforeach
<div class="tab-pane fade" id="pills-settings" role="tabpanel" aria-labelledby="pills-settings-tab">
Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy
hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice blog. Scenester cred you probably haven't
heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable tofu synth chambray yr.
</div>
@ -93,10 +56,6 @@
</main>
<script>
var client_object = {!! $client !!};
var hashed_id = '{{ $hashed_id }}';
</script>
<script defer src=" {{ mix('/js/client-edit.js') }}"></script>
<script defer src=" {{ mix('/js/client_edit.min.js') }}"></script>
@endsection

View File

@ -0,0 +1,19 @@
<div class="card" id="localization">
<div class="card-header bg-primary2">@lang('texts.localization')</div>
<div class="card-body">
<div class="form-group row">
<label for="name" class="col-sm-3 col-form-label text-right">@lang('texts.client_name')</label>
<div class="col-sm-9">
<v-select v-model="selected" :options="options"></v-select>
<div v-if="form.errors.has('name')" class="text-danger" v-text="form.errors.get('name')"></div>
</div>
</div>
</div>
</div>
<script>
</script>
<script defer src=" {{ mix('/js/localization.min.js') }}"></script>

View File

@ -13,11 +13,6 @@
<i class="nav-icon icon-user"></i> @lang('texts.clients')</a>
</li>
<li class="nav-item">
<a class="nav-link" href="typography.html">
<i class="nav-icon icon-present"></i> @lang('texts.products')</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('invoices.index') }}">
<i class="nav-icon icon-notebook"></i> @lang('texts.invoices')</a>

View File

@ -19,3 +19,4 @@ Breadcrumbs::for('clients.edit', function($trail, $client) {
Breadcrumbs::for('clients.create', function($trail) {
$trail->parent('clients');
});

View File

@ -43,6 +43,7 @@ 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');

View File

@ -57,6 +57,6 @@
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"include": [
"resources/js/src/**/*"
"resources/js/**/*"
]
}

7
webpack.mix.js vendored
View File

@ -19,14 +19,18 @@ mix.webpackConfig({
rules: [
{
test: /\.ts$/,
loader: 'ts-loader'
loader: 'ts-loader',
options: { appendTsSuffixTo: [/\.vue$/] },
exclude: /node_modules/,
}
]
}
});
mix.js('resources/js/src/client/client_edit.ts', 'public/js');
mix.js('resources/js/src/c/client-edit.ts', 'public/js');
mix.js('resources/js/src/client/client_create.ts', 'public/js');
mix.js('resources/js/src/settings/localization.ts', 'public/js');
mix.js('resources/js/app.js', 'public/js/vendor');
mix.js('node_modules/@coreui/coreui/dist/js/coreui.js', 'public/js');
@ -38,6 +42,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/localization.js');
mix.styles([
'node_modules/@coreui/coreui/dist/css/coreui.css',