changes, global search (clients, cleintCases)
This commit is contained in:
parent
c45751c1e2
commit
3ae70bf340
25
app/Console/Commands/ImportPosts.php
Normal file
25
app/Console/Commands/ImportPosts.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Post;
|
||||
|
||||
class ImportPosts extends Command
|
||||
{
|
||||
protected $signature = 'import:posts';
|
||||
protected $description = 'Import posts into Algolia without clearing the index';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$posts = Post::all();
|
||||
$posts->searchable();
|
||||
$this->info('Posts have been imported into Algolia.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ public function index(ClientCase $clientCase, Request $request)
|
|||
)
|
||||
->where('active', 1)
|
||||
->orderByDesc('created_at')
|
||||
->paginate(15)
|
||||
->paginate(15, ['*'], 'client-cases-page')
|
||||
->withQueryString(),
|
||||
'filters' => $request->only(['search'])
|
||||
]);
|
||||
|
|
@ -164,7 +164,7 @@ public function show(ClientCase $clientCase)
|
|||
->orderByDesc('created_at')->get(),
|
||||
'activities' => $case->activities()->with(['action', 'decision'])
|
||||
->orderByDesc('created_at')
|
||||
->paginate(15),
|
||||
->paginate(20, ['*'], 'activities'),
|
||||
'contract_types' => \App\Models\ContractType::whereNull('deleted_at')->get(),
|
||||
'actions' => \App\Models\Action::with('decisions')->get()
|
||||
]);
|
||||
|
|
|
|||
66
app/Http/Controllers/PostController.php
Normal file
66
app/Http/Controllers/PostController.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Post;
|
||||
use App\Http\Requests\StorePostRequest;
|
||||
use App\Http\Requests\UpdatePostRequest;
|
||||
|
||||
class PostController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(StorePostRequest $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Post $post)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Post $post)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(UpdatePostRequest $request, Post $post)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Post $post)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/StorePostRequest.php
Normal file
28
app/Http/Requests/StorePostRequest.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StorePostRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/UpdatePostRequest.php
Normal file
28
app/Http/Requests/UpdatePostRequest.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdatePostRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -6,11 +6,13 @@
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Action extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ActionFactory> */
|
||||
use HasFactory;
|
||||
use Searchable;
|
||||
|
||||
public function decisions(): BelongsToMany
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Client extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ClientFactory> */
|
||||
use HasFactory;
|
||||
use Uuid;
|
||||
use Searchable;
|
||||
|
||||
protected $fillable = [
|
||||
'person_id'
|
||||
|
|
@ -23,6 +26,26 @@ class Client extends Model
|
|||
'person_id',
|
||||
];
|
||||
|
||||
|
||||
protected function makeAllSearchableUsing(Builder $query): Builder
|
||||
{
|
||||
return $query->with('person');
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$person = [
|
||||
'full_name' => $this->person->full_name,
|
||||
'addresses' => $this->person->addresses,
|
||||
'phones' => $this->person->phones
|
||||
];
|
||||
|
||||
return [
|
||||
'person' => $person
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function person(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Person\Person::class);
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class ClientCase extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ClientCaseFactory> */
|
||||
use HasFactory;
|
||||
use Uuid;
|
||||
use Searchable;
|
||||
|
||||
protected $fillable = [
|
||||
'client_id'
|
||||
|
|
@ -24,6 +27,20 @@ class ClientCase extends Model
|
|||
'person_id'
|
||||
];
|
||||
|
||||
protected function makeAllSearchableUsing(Builder $query): Builder
|
||||
{
|
||||
return $query->with('person');
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'person' => $this->person,
|
||||
'addresses' => $this->person->addresses,
|
||||
'phones' => $this->person->phones
|
||||
];
|
||||
}
|
||||
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Client::class);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Person extends Model
|
||||
{
|
||||
|
|
@ -16,6 +18,7 @@ class Person extends Model
|
|||
/** @use HasFactory<\Database\Factories\Person/PersonFactory> */
|
||||
use HasFactory;
|
||||
use Uuid;
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
@ -52,6 +55,22 @@ protected static function booted(){
|
|||
});
|
||||
}
|
||||
|
||||
protected function makeAllSearchableUsing(Builder $query): Builder
|
||||
{
|
||||
return $query->with(['addresses', 'phones']);
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'first_name' => $this->first_name,
|
||||
'last_name' => $this->last_name,
|
||||
'full_name' => $this->full_name,
|
||||
'addresses' => $this->addresses,
|
||||
'phones' => $this->phones
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function phones(): HasMany
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class PersonAddress extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\Person/PersonAddressFactory> */
|
||||
use HasFactory;
|
||||
use Searchable;
|
||||
|
||||
protected $fillable = [
|
||||
'address',
|
||||
|
|
@ -26,6 +28,14 @@ class PersonAddress extends Model
|
|||
'deleted'
|
||||
];
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'address' => $this->address,
|
||||
'country' => $this->country
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted(){
|
||||
static::creating(function (PersonAddress $address) {
|
||||
$address->user_id = auth()->id();
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class PersonPhone extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\Person/PersonPhoneFactory> */
|
||||
use HasFactory;
|
||||
use Searchable;
|
||||
|
||||
protected $fillable = [
|
||||
'nu',
|
||||
|
|
@ -27,6 +29,13 @@ class PersonPhone extends Model
|
|||
'deleted'
|
||||
];
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'nu' => $this->nu
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted(){
|
||||
static::creating(function (PersonPhone $personPhone) {
|
||||
if(!isset($personPhone->user_id)){
|
||||
|
|
|
|||
18
app/Models/Post.php
Normal file
18
app/Models/Post.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use HasFactory, Searchable;
|
||||
|
||||
public function toSearchableArray()
|
||||
{
|
||||
$array = $this->toArray();
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
66
app/Policies/PostPolicy.php
Normal file
66
app/Policies/PostPolicy.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class PostPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Post $post): bool
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Post $post): bool
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Post $post): bool
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Post $post): bool
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Post $post): bool
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,17 @@
|
|||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"algolia/scout-extended": "3.1",
|
||||
"arielmejiadev/larapex-charts": "^2.1",
|
||||
"diglactic/laravel-breadcrumbs": "^9.0",
|
||||
"http-interop/http-factory-guzzle": "^1.2",
|
||||
"inertiajs/inertia-laravel": "^1.0",
|
||||
"laravel/framework": "^11.9",
|
||||
"laravel/jetstream": "^5.2",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/scout": "^10.11",
|
||||
"laravel/tinker": "^2.9",
|
||||
"meilisearch/meilisearch-php": "^1.11",
|
||||
"robertboes/inertia-breadcrumbs": "^0.6.0",
|
||||
"tightenco/ziggy": "^2.0"
|
||||
},
|
||||
|
|
|
|||
871
composer.lock
generated
871
composer.lock
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -95,6 +95,10 @@
|
|||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
'options' => [
|
||||
'LC_COLLATE' => env('PGSQL_LC_COLLATE', 'en_US.UTF-8'),
|
||||
'LC_CTYPE' => env('PGSQL_LC_CTYPE', 'en_US.UTF-8'),
|
||||
]
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
|
|
|
|||
203
config/scout.php
Normal file
203
config/scout.php
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Search Engine
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default search connection that gets used while
|
||||
| using Laravel Scout. This connection is used when syncing all models
|
||||
| to the search service. You should adjust this based on your needs.
|
||||
|
|
||||
| Supported: "algolia", "meilisearch", "typesense",
|
||||
| "database", "collection", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SCOUT_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify a prefix that will be applied to all search index
|
||||
| names used by Scout. This prefix may be useful if you have multiple
|
||||
| "tenants" or applications sharing the same search infrastructure.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('SCOUT_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Data Syncing
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control if the operations that sync your data
|
||||
| with your search engines are queued. When this is set to "true" then
|
||||
| all automatic data syncing will get queued for better performance.
|
||||
|
|
||||
*/
|
||||
|
||||
'queue' => env('SCOUT_QUEUE', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Transactions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration option determines if your data will only be synced
|
||||
| with your search indexes after every open database transaction has
|
||||
| been committed, thus preventing any discarded data from syncing.
|
||||
|
|
||||
*/
|
||||
|
||||
'after_commit' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Chunk Sizes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options allow you to control the maximum chunk size when you are
|
||||
| mass importing data into the search engine. This allows you to fine
|
||||
| tune each of these chunk sizes based on the power of the servers.
|
||||
|
|
||||
*/
|
||||
|
||||
'chunk' => [
|
||||
'searchable' => 500,
|
||||
'unsearchable' => 500,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Soft Deletes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows to control whether to keep soft deleted records in
|
||||
| the search indexes. Maintaining soft deleted records can be useful
|
||||
| if your application still needs to search for the records later.
|
||||
|
|
||||
*/
|
||||
|
||||
'soft_delete' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Identify User
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control whether to notify the search engine
|
||||
| of the user performing the search. This is sometimes useful if the
|
||||
| engine supports any analytics based on this application's users.
|
||||
|
|
||||
| Supported engines: "algolia"
|
||||
|
|
||||
*/
|
||||
|
||||
'identify' => env('SCOUT_IDENTIFY', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Algolia Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Algolia settings. Algolia is a cloud hosted
|
||||
| search engine which works great with Scout out of the box. Just plug
|
||||
| in your application ID and admin API key to get started searching.
|
||||
|
|
||||
*/
|
||||
|
||||
'algolia' => [
|
||||
'id' => env('ALGOLIA_APP_ID', 'ZDAXR87LZV'),
|
||||
'secret' => env('ALGOLIA_SECRET', '8797318d18e10541ad15d49ae1e64db2'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Meilisearch Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Meilisearch settings. Meilisearch is an open
|
||||
| source search engine with minimal configuration. Below, you can state
|
||||
| the host and key information for your own Meilisearch installation.
|
||||
|
|
||||
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
||||
|
|
||||
*/
|
||||
|
||||
'meilisearch' => [
|
||||
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
|
||||
'key' => env('MEILISEARCH_KEY'),
|
||||
'index-settings' => [
|
||||
// 'users' => [
|
||||
// 'filterableAttributes'=> ['id', 'name', 'email'],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Typesense Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Typesense settings. Typesense is an open
|
||||
| source search engine using minimal configuration. Below, you will
|
||||
| state the host, key, and schema configuration for the instance.
|
||||
|
|
||||
*/
|
||||
|
||||
'typesense' => [
|
||||
'client-settings' => [
|
||||
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
|
||||
'nodes' => [
|
||||
[
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
],
|
||||
'nearest_node' => [
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
|
||||
'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
|
||||
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
|
||||
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
|
||||
],
|
||||
// 'max_total_results' => env('TYPESENSE_MAX_TOTAL_RESULTS', 1000),
|
||||
'model-settings' => [
|
||||
// User::class => [
|
||||
// 'collection-schema' => [
|
||||
// 'fields' => [
|
||||
// [
|
||||
// 'name' => 'id',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'name',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'created_at',
|
||||
// 'type' => 'int64',
|
||||
// ],
|
||||
// ],
|
||||
// 'default_sorting_field' => 'created_at',
|
||||
// ],
|
||||
// 'search-parameters' => [
|
||||
// 'query_by' => 'name'
|
||||
// ],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
25
database/factories/PostFactory.php
Normal file
25
database/factories/PostFactory.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
|
||||
*/
|
||||
class PostFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'title' => fake()->sentence(),
|
||||
'body' => fake()->paragraph(),
|
||||
'slug' => fake()->slug(),
|
||||
];
|
||||
}
|
||||
}
|
||||
30
database/migrations/2024_11_17_123507_create_posts_table.php
Normal file
30
database/migrations/2024_11_17_123507_create_posts_table.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title');
|
||||
$table->text('body');
|
||||
$table->string('slug');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('posts');
|
||||
}
|
||||
};
|
||||
|
|
@ -69,7 +69,7 @@ public function run(): void
|
|||
'nu' => rand(100000,200000),
|
||||
'first_name' => '',
|
||||
'last_name' => '',
|
||||
'full_name' => 'test d.o.o.',
|
||||
'full_name' => 'Naročnik d.o.o.',
|
||||
'gender' => 'm',
|
||||
'birthday' => '2000-01-01',
|
||||
'tax_number' => '22424345',
|
||||
|
|
|
|||
17
database/seeders/PostSeeder.php
Normal file
17
database/seeders/PostSeeder.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class PostSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
\App\Models\Post::factory(500)->create();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"target": "ES6",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["resources/js/*"]
|
||||
|
|
|
|||
115
package-lock.json
generated
115
package-lock.json
generated
|
|
@ -5,6 +5,11 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@vuepic/vue-datepicker": "^9.0.3",
|
||||
|
|
@ -12,11 +17,15 @@
|
|||
"flowbite": "^2.5.2",
|
||||
"flowbite-vue": "^0.1.6",
|
||||
"lodash": "^4.17.21",
|
||||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"tailwindcss-inner-border": "^0.2.0",
|
||||
"vue3-apexcharts": "^1.7.0"
|
||||
"vue-search-input": "^1.1.16",
|
||||
"vue3-apexcharts": "^1.7.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@inertiajs/vue3": "^1.0.14",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
|
|
@ -515,6 +524,73 @@
|
|||
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
|
||||
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
||||
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/vue-fontawesome": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.8.tgz",
|
||||
"integrity": "sha512-yyHHAj4G8pQIDfaIsMvQpwKMboIZtcHTUvPqXjOHyldh1O1vZfH4W03VDPv5RvI9P6DLTzJQlmVgj9wCf7c2Fw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"vue": ">= 3.0.0 < 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/vue": {
|
||||
"version": "1.7.23",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz",
|
||||
|
|
@ -632,6 +708,13 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdi/js": {
|
||||
"version": "7.4.47",
|
||||
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
|
||||
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
|
@ -2335,6 +2418,12 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/material-design-icons-iconfont": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz",
|
||||
"integrity": "sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
|
@ -2974,6 +3063,12 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
|
||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
|
@ -3466,6 +3561,12 @@
|
|||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-search-input": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/vue-search-input/-/vue-search-input-1.1.16.tgz",
|
||||
"integrity": "sha512-hMZJHN/HppamwF24vPVhWVy4334WEmChkuAEnmDuiR3SeKrAPjK5mYb0vKBiAWP4QL1p2d+JhvbUe444uDTEMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue3-apexcharts": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.7.0.tgz",
|
||||
|
|
@ -3476,6 +3577,18 @@
|
|||
"vue": "> 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuedraggable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
|
||||
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sortablejs": "1.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -7,6 +7,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@inertiajs/vue3": "^1.0.14",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
|
|
@ -19,6 +20,11 @@
|
|||
"vue": "^3.3.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@vuepic/vue-datepicker": "^9.0.3",
|
||||
|
|
@ -26,7 +32,10 @@
|
|||
"flowbite": "^2.5.2",
|
||||
"flowbite-vue": "^0.1.6",
|
||||
"lodash": "^4.17.21",
|
||||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"tailwindcss-inner-border": "^0.2.0",
|
||||
"vue3-apexcharts": "^1.7.0"
|
||||
"vue-search-input": "^1.1.16",
|
||||
"vue3-apexcharts": "^1.7.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
@import '/node_modules/floating-vue/dist/style.css';
|
||||
@import '/node_modules/vue-search-input/dist/styles.css';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
|
|||
135
resources/js/Components/DragDropList.vue
Normal file
135
resources/js/Components/DragDropList.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<script setup>
|
||||
import { FwbListGroup, FwbListGroupItem } from 'flowbite-vue';
|
||||
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import draggable from 'vuedraggable';
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
list1: Array,
|
||||
list2: Array
|
||||
});
|
||||
|
||||
const item1 = {
|
||||
id: 1,
|
||||
name: 'Item 1'
|
||||
}
|
||||
|
||||
const item2 = {
|
||||
id: 2,
|
||||
name: 'Item 2'
|
||||
}
|
||||
|
||||
const item3 = {
|
||||
id: 3,
|
||||
name: 'Item 3'
|
||||
}
|
||||
|
||||
const isDragging = ref(false);
|
||||
|
||||
let containerOne = ref([item1, item2])
|
||||
let containerTwo = ref([item3])
|
||||
|
||||
const dragOptions = computed(() => {
|
||||
return {
|
||||
animation: 200,
|
||||
group: "actions",
|
||||
disabled: false,
|
||||
ghostClass: "ghost"
|
||||
}
|
||||
});
|
||||
|
||||
const removeAt = (idx, targeContainer) => {
|
||||
targeContainer.splice(idx, 1);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => containerOne.value,
|
||||
(value) => {
|
||||
console.log(value)
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<draggable
|
||||
class="list-group"
|
||||
:list="containerOne"
|
||||
item-key="id"
|
||||
:component-data="{
|
||||
tag: 'ul',
|
||||
type: 'transition-group',
|
||||
name: !isDragging ? 'flip-list' : null
|
||||
}"
|
||||
v-bind="dragOptions"
|
||||
@start="isDragging = true"
|
||||
@end="isDragging = false"
|
||||
group="actions"
|
||||
>
|
||||
<template #item="{element, index}">
|
||||
<fwb-list-group-item class="flex justify-between">
|
||||
<span class="text">{{ element.name }} </span>
|
||||
|
||||
<i class=" cursor-pointer" @click="removeAt(index, containerOne)"><svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/>
|
||||
</svg></i>
|
||||
</fwb-list-group-item>
|
||||
</template>
|
||||
</draggable>
|
||||
<draggable
|
||||
class="list-group"
|
||||
:list="containerTwo"
|
||||
:component-data="{
|
||||
tag: 'ul',
|
||||
type: 'transition-group',
|
||||
name: !isDragging ? 'flip-list' : null
|
||||
}"
|
||||
item-key="id"
|
||||
v-bind="dragOptions"
|
||||
@start="isDragging = true"
|
||||
@end="isDragging = false"
|
||||
group="actions"
|
||||
>
|
||||
<template #item="{element, index}">
|
||||
<fwb-list-group-item class="flex justify-between">
|
||||
<span class="text">{{ element.name }} </span>
|
||||
|
||||
<i class="cursor-pointer" @click="removeAt(index, containerOne)"><svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/>
|
||||
</svg></i>
|
||||
</fwb-list-group-item>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="css">
|
||||
.button {
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.flip-list-move {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
.no-move {
|
||||
transition: transform 0s;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #c8ebfb;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.list-group-item i {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
79
resources/js/Components/DragDropListEx.vue
Normal file
79
resources/js/Components/DragDropListEx.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const item1 = {
|
||||
id: 1,
|
||||
text: 'Item 1'
|
||||
};
|
||||
const item2 = {
|
||||
id: 2,
|
||||
text: 'Item 2'
|
||||
};
|
||||
|
||||
let containerOne = ref([item1, item2])
|
||||
let containerTwo = ref([])
|
||||
|
||||
const handleDragStart = (event, container, itemData) => {
|
||||
event.dataTransfer.setData('application/json', JSON.stringify(itemData));
|
||||
|
||||
};
|
||||
|
||||
const handleDrop = (event, targetContainer) => {
|
||||
const itemData = JSON.parse(event.dataTransfer.getData('application/json'));
|
||||
if (targetContainer === containerOne.value) {
|
||||
containerTwo.value = containerTwo.value.filter(i => i.id !== itemData.id);
|
||||
} else if (targetContainer === containerTwo.value) {
|
||||
containerOne.value = containerOne.value.filter(i => i.id !== itemData.id);
|
||||
}
|
||||
targetContainer.push(itemData);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="drag-drop-container">
|
||||
<div class="container-one"
|
||||
v-on:dragover.prevent
|
||||
v-on:drop="handleDrop($event, containerOne)">
|
||||
<div class="item"
|
||||
v-for="item in containerOne"
|
||||
:key="item.id"
|
||||
draggable="true"
|
||||
v-on:dragstart="handleDragStart($event, containerOne, item)">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-two"
|
||||
v-on:dragover.prevent
|
||||
v-on:drop="handleDrop($event, containerTwo)">
|
||||
<div class="item"
|
||||
v-for="item in containerTwo"
|
||||
:key="item.id"
|
||||
draggable="true"
|
||||
v-on:dragstart="handleDragStart($event, containerTwo, item)">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.drag-drop-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container-one, .container-two {
|
||||
border: 1px solid #1a1a1a;
|
||||
width: 600px;
|
||||
height: 800px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 10px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
width: 90%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { Head, Link, router, usePage } from '@inertiajs/vue3';
|
||||
import ApplicationMark from '@/Components/ApplicationMark.vue';
|
||||
import Banner from '@/Components/Banner.vue';
|
||||
|
|
@ -8,16 +8,66 @@ import DropdownLink from '@/Components/DropdownLink.vue';
|
|||
import NavLink from '@/Components/NavLink.vue';
|
||||
import ResponsiveNavLink from '@/Components/ResponsiveNavLink.vue';
|
||||
import Breadcrumbs from '@/Components/Breadcrumbs.vue';
|
||||
import axios from 'axios';
|
||||
import { debounce } from 'lodash';
|
||||
import { FwbButton, FwbInput, FwbListGroup, FwbListGroupItem } from 'flowbite-vue';
|
||||
import { AdjustmentIcon, SearchIcon } from '@/Utilities/Icons';
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
});
|
||||
|
||||
const showingNavigationDropdown = ref(false);
|
||||
const querySearchDiv = ref(null);
|
||||
|
||||
const querySearch = ref('');
|
||||
|
||||
const querySearchList = ref(true);
|
||||
const querySearchResult = ref([]);
|
||||
|
||||
const logout = () => {
|
||||
router.post(route('logout'));
|
||||
};
|
||||
|
||||
const setRoute = (index) => {
|
||||
return route('client.show', index);
|
||||
}
|
||||
|
||||
const onSearchFocus = () => {
|
||||
if(querySearch.value.length > 0) {
|
||||
querySearchList.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const onSearchBlue = () => {
|
||||
setTimeout(() => querySearchList.value = true, 100)
|
||||
}
|
||||
|
||||
watch(querySearch, debounce((value) => {
|
||||
axios.get(
|
||||
route('search'),
|
||||
{
|
||||
params: {
|
||||
query: value,
|
||||
limit: 5,
|
||||
tag: ''
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(function(res) {
|
||||
querySearchResult.value = res.data
|
||||
querySearchList.value = false;
|
||||
console.log(res);
|
||||
})
|
||||
.catch(function(error){
|
||||
console.log(error)
|
||||
})
|
||||
.finally(function(){
|
||||
|
||||
});
|
||||
}, 300));
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -65,6 +115,42 @@ const logout = () => {
|
|||
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<!-- Settings Dropdown -->
|
||||
<div>
|
||||
<fwb-input
|
||||
v-model="querySearch"
|
||||
placeholder="Iskalnik..."
|
||||
size="md"
|
||||
@focus="onSearchFocus"
|
||||
@blur="onSearchBlue"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchIcon />
|
||||
</template>
|
||||
|
||||
</fwb-input>
|
||||
<fwb-list-group ref="querySearchDiv" class="absolute" :hidden="querySearchList">
|
||||
<fwb-list-group-item>
|
||||
<div>
|
||||
<p>Naročniki:</p>
|
||||
<fwb-list-group>
|
||||
<fwb-list-group-item hover v-for="client in querySearchResult.clients">
|
||||
<a :href="route('client.show', {uuid: client.uuid})">{{ client.person.full_name }}</a>
|
||||
</fwb-list-group-item>
|
||||
</fwb-list-group>
|
||||
</div>
|
||||
</fwb-list-group-item>
|
||||
<fwb-list-group-item>
|
||||
<div>
|
||||
<p>Primeri:</p>
|
||||
<fwb-list-group>
|
||||
<fwb-list-group-item hover v-for="clientCase in querySearchResult.client_cases">
|
||||
<a :href="route('clientCase.show', {uuid: clientCase.uuid})">{{ clientCase.person.full_name }}</a>
|
||||
</fwb-list-group-item>
|
||||
</fwb-list-group>
|
||||
</div>
|
||||
</fwb-list-group-item>
|
||||
</fwb-list-group>
|
||||
</div>
|
||||
<div class="ms-3 relative">
|
||||
<Dropdown align="right" width="48">
|
||||
<template #trigger>
|
||||
|
|
@ -154,7 +240,6 @@ const logout = () => {
|
|||
Nastavitve
|
||||
</ResponsiveNavLink>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-gray-200">
|
||||
<div class="flex items-center px-4">
|
||||
|
|
@ -233,11 +318,12 @@ const logout = () => {
|
|||
</nav>
|
||||
|
||||
<!-- Page Heading -->
|
||||
<header v-if="$slots.header" class="bg-white shadow">
|
||||
<header v-if="$slots.header" class="bg-white shadow ">
|
||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
<Breadcrumbs :breadcrumbs="$page.props.breadcrumbs"></Breadcrumbs>
|
||||
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<!-- Page Content -->
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ const closeDrawer = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-1">
|
||||
<!--div class="pt-1">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-blue-400">
|
||||
<div class="mx-auto max-w-4x1 p-3">
|
||||
|
|
@ -104,8 +104,8 @@ const closeDrawer = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-12">
|
||||
</div-->
|
||||
<div class="pt-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-red-400">
|
||||
<div class="mx-auto max-w-4x1 p-3">
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ const activeTab = ref('actions')
|
|||
<div class="pt-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||
|
||||
<fwb-tabs v-model="activeTab" variant="underline" >
|
||||
<fwb-tab name="actions" title="Actions">
|
||||
<ActionTable :actions="actions" />
|
||||
|
|
@ -28,7 +27,6 @@ const activeTab = ref('actions')
|
|||
<DecisionTable :decisions="decisions" />
|
||||
</fwb-tab>
|
||||
</fwb-tabs>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,36 @@
|
|||
<script setup>
|
||||
import BasicTable from '@/Components/BasicTable.vue';
|
||||
import { LinkOptions as C_LINK, TableColumn as C_TD, TableRow as C_TR} from '@/Shared/AppObjects';
|
||||
import { FwbTable, FwbTableBody, FwbTableHead, FwbTableHeadCell, FwbTableCell, FwbTableRow } from 'flowbite-vue';
|
||||
import { EditIcon, TrashBinIcon } from '@/Utilities/Icons';
|
||||
|
||||
const props = defineProps({
|
||||
actions: Array
|
||||
});
|
||||
|
||||
const header = [
|
||||
C_TD.make('#', 'header'),
|
||||
C_TD.make('Name', 'header'),
|
||||
C_TD.make('Color tag', 'header'),
|
||||
C_TD.make('Decisions', 'header')
|
||||
];
|
||||
|
||||
const createBody = (data) => {
|
||||
let content = [];
|
||||
|
||||
data.forEach((a) => {
|
||||
const cols = [
|
||||
C_TD.make(a.id, 'body'),
|
||||
C_TD.make(a.name, 'body'),
|
||||
C_TD.make(a.color_tag, 'body'),
|
||||
C_TD.make(a.decisions.length, 'body')
|
||||
];
|
||||
|
||||
content.push(C_TR.make(cols));
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<BasicTable :header="header" :body="createBody(actions)" />
|
||||
<fwb-table>
|
||||
<fwb-table-head>
|
||||
<fwb-table-head-cell>#</fwb-table-head-cell>
|
||||
<fwb-table-head-cell>Name</fwb-table-head-cell>
|
||||
<fwb-table-head-cell>Color tag</fwb-table-head-cell>
|
||||
<fwb-table-head-cell>Decisions</fwb-table-head-cell>
|
||||
<fwb-table-head-cell>
|
||||
<span class="sr-only">Edit</span>
|
||||
</fwb-table-head-cell>
|
||||
</fwb-table-head>
|
||||
<fwb-table-body>
|
||||
<fwb-table-row v-for="act in actions" :key="act.id">
|
||||
<fwb-table-cell>{{ act.id }}</fwb-table-cell>
|
||||
<fwb-table-cell>{{ act.name }}</fwb-table-cell>
|
||||
<fwb-table-cell>{{ act.color_tag }}</fwb-table-cell>
|
||||
<fwb-table-cell>{{ act.decisions.length }}</fwb-table-cell>
|
||||
<fwb-table-cell>
|
||||
<button><EditIcon /></button>
|
||||
<button><TrashBinIcon /></button>
|
||||
</fwb-table-cell>
|
||||
</fwb-table-row>
|
||||
</fwb-table-body>
|
||||
</fwb-table>
|
||||
</template>
|
||||
19
resources/js/Pages/Testing.vue
Normal file
19
resources/js/Pages/Testing.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script setup>
|
||||
import DragDropListEx from '@/Components/DragDropListEx.vue';
|
||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<AppLayout title="Testing">
|
||||
<div class="pt-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||
<div class="grid grid-flow-col">
|
||||
<DragDropListEx />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
66
resources/js/Utilities/Icons.js
Normal file
66
resources/js/Utilities/Icons.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
|
||||
import { ref } from "vue";
|
||||
|
||||
const Icon = {
|
||||
props: {
|
||||
css: {
|
||||
type: String,
|
||||
default: 'text-gray-800'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const AddressBookIcon = {
|
||||
__proto__: Icon,
|
||||
setup: () => {
|
||||
console.log(this.props)
|
||||
},
|
||||
template: `<svg class="w-6 h-6 dark:text-white" :class="css" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 6H5m2 3H5m2 3H5m2 3H5m2 3H5m11-1a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2M7 3h11a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Zm8 7a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"/>
|
||||
</svg>`
|
||||
};
|
||||
|
||||
const AlignCenterIcon = {
|
||||
__proto__: Icon,
|
||||
template: `<svg class="w-6 h-6 dark:text-white" :class="css" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 6h8M6 10h12M8 14h8M6 18h12"/>
|
||||
</svg>`
|
||||
}
|
||||
|
||||
const EditIcon = {
|
||||
__proto__: Icon,
|
||||
template: `<svg class="w-6 h-6 dark:text-white" :class="css" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m14.304 4.844 2.852 2.852M7 7H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-4.5m2.409-9.91a2.017 2.017 0 0 1 0 2.853l-6.844 6.844L8 14l.713-3.565 6.844-6.844a2.015 2.015 0 0 1 2.852 0Z"/>
|
||||
</svg>`
|
||||
}
|
||||
|
||||
const TrashBinIcon = {
|
||||
__proto__: Icon,
|
||||
template: `<svg class="w-6 h-6 dark:text-white" :class="css" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M8.586 2.586A2 2 0 0 1 10 2h4a2 2 0 0 1 2 2v2h3a1 1 0 1 1 0 2v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a1 1 0 0 1 0-2h3V4a2 2 0 0 1 .586-1.414ZM10 6h4V4h-4v2Zm1 4a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Zm4 0a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Z" clip-rule="evenodd"/>
|
||||
</svg>`
|
||||
}
|
||||
|
||||
const SearchIcon = {
|
||||
__proto__: Icon,
|
||||
template: `<svg aria-hidden="true" class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" />
|
||||
</svg>`
|
||||
}
|
||||
|
||||
const AdjustmentIcon = {
|
||||
__proto__: Icon,
|
||||
template: `<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M20 6H10m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4m16 6h-2m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4m16 6H10m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4"/>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
|
||||
export {
|
||||
AddressBookIcon,
|
||||
AlignCenterIcon,
|
||||
EditIcon,
|
||||
TrashBinIcon,
|
||||
SearchIcon,
|
||||
AdjustmentIcon
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import './bootstrap';
|
||||
import '../css/app.css';
|
||||
|
||||
import { createApp, h } from 'vue';
|
||||
import { createApp, h } from 'vue/dist/vue.esm-bundler.js';
|
||||
import { createInertiaApp } from '@inertiajs/vue3';
|
||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
|
||||
|
|
|
|||
|
|
@ -12,3 +12,11 @@
|
|||
Route::get('/person', function(){
|
||||
return new PersonCollection(Person::all());
|
||||
})->middleware('auth:sanctum');
|
||||
|
||||
Route::get('/search', function(Request $request){
|
||||
$query = '41242523';
|
||||
|
||||
$persons = App\Models\Person\Person::search($query)->get();
|
||||
|
||||
return $persons;
|
||||
})->middleware('auth:sanctum');
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
use App\Http\Controllers\ContractController;
|
||||
use App\Http\Controllers\SettingController;
|
||||
use App\Models\Person\Person;
|
||||
use Illuminate\Http\Request;
|
||||
use ArielMejiaDev\LarapexCharts\LarapexChart;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
|
|
@ -36,6 +37,35 @@
|
|||
);
|
||||
})->name('dashboard');
|
||||
|
||||
Route::get('testing', function() {
|
||||
return Inertia::render('Testing', []);
|
||||
});
|
||||
|
||||
Route::get('search', function(Request $request) {
|
||||
|
||||
if( !empty($request->input('query')) ) {
|
||||
$clients = App\Models\Client::search($request->input('query'))
|
||||
->query(function($q) use($request) {
|
||||
$q->with('person')->limit($request->input('limit'));
|
||||
})
|
||||
->get();
|
||||
|
||||
$clientCases = App\Models\ClientCase::search($request->input('query'))
|
||||
->query(function($q) use($request) {
|
||||
$q->with('person')->limit($request->input('limit'));
|
||||
})
|
||||
->get();
|
||||
|
||||
return [
|
||||
'clients' => $clients,
|
||||
'client_cases' => $clientCases
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
return [];
|
||||
})->name('search');
|
||||
|
||||
//client
|
||||
Route::get('clients', [ClientController::class, 'index'])->name('client');
|
||||
Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show');
|
||||
|
|
|
|||
28
tests/Feature/AlgoliaSearchTest.php
Normal file
28
tests/Feature/AlgoliaSearchTest.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AlgoliaSearchTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic feature test example.
|
||||
*/
|
||||
public function test_example(): void
|
||||
{
|
||||
|
||||
$client = \Algolia\AlgoliaSearch\SearchClient::create('ZDAXR87LZV','8797318d18e10541ad15d49ae1e64db2');
|
||||
|
||||
$index = $client->initIndex('myposts_index');
|
||||
|
||||
$index->saveObject([
|
||||
'objectID' => 1,
|
||||
'name' => 'Test record'
|
||||
]);
|
||||
|
||||
//$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user