# QuickAdminPanel Docs


# Creating a Simple CRUD

{% hint style="info" %}
**Notice**: if you need the documentation for **Vue.js** QuickAdminPanel generator, released in 2020, please [click this link](https://helpdocs.quickadminpanel.com/vue.js-generator-version/installing-downloaded-vue-panel).&#x20;
{% endhint %}

## **Video version of the tutorial**

{% embed url="<https://www.youtube.com/watch?v=E_8Tp-fjggs>" %}

## **Text + screenshots version**

To create a new CRUD, click **Menus / CRUDs** on the left sidebar, and then **Create CRUD Menu Item** in the center.

![](/files/-M7nC0QMddQcMfDYKb6A)

You will see a form, like below.

![](/files/-M7nC_DthtqpuZcsUX7m)

On the top you need to enter the CRUD Model name (in singular, like "**Project**" or "**Transaction**"), choose Font Awesome icon and parent menu, if needed.

&#x20;At the bottom, some fields are pre-filled for you: **id**, **timestamps**, **soft-deletes**.

&#x20;To add a new field, click **Add new field** row. You will see this form:

![](/files/-M7pz_Fed2A5BOq_V02h)

There is a dozen of field types, in the most simple **Text** field type you only need to add **Key** and **Label**, and click **Save**.

Every field type has its own settings. For example, if we add another field with **Textarea** field type, we can choose whether to use [CKEditor](https://ckeditor.com/):

![](https://laraveldaily.com/wp-content/uploads/2019/03/create-crud-textarea.png)

After you finish entering all the fields, just click **Save CRUD** at the bottom of the fields.

![](https://laraveldaily.com/wp-content/uploads/2019/03/create-crud-save.png)

Then the code will start generating, and you will see the progress in the left sidebar - when you're able to View or Download new code.

## Table Settings

At the bottom of Create CRUD page, there are three settings for the visual table that will be shown for the list of that DB table:

* NEW! You can enable **column search** (to have input search for each individual column)
* You can modify amount of **entries per page** (default 100)
* You can change **ordering** (default is newest-to-oldest, so "*order by id desc*")

![](/files/-M8tZIiTUqQi_WxHG-FB)

## Additional Options

At the top of CRUD form, there is a link **Show more options** which opens up a few extra settings, which are all enabled by default, but you can disable them.

![](/files/-M7q-p5ThDufJQYqKc82)

Here, you can enable/disable:

* Soft Deletes - read more about them in [Laravel documentation](https://laravel.com/docs/master/eloquent#soft-deleting)
* To generate or not generate API routes/controllers
* What actions are needed for this CRUD - maybe you need only create but not edit form

Also, you may see more options here, depending on the modules you have installed.


# Radio/Checkbox/Select Fields

We have three so-called "Choice" field types:

* Radio button
* Checkbox
* Select Dropdown

If you prefer video, here's a quick video demo for all of them.

{% embed url="<https://www.youtube.com/watch?v=2fPYHp0jKXo>" %}

Now, separately about each one.

## Radio/Select Options

If you choose a field types **Radio** or **Select**, you should enter all different values in key-value pairs. One of them goes to the database value, another one is shown visually on the page, as a label. Also, you may specify **Default value**.

![Example for the Radio field type](/files/-M7q7nPlFg86h1bbl3u5)

![Example for the Select field type](/files/-M7q8N2fI9RdByC6MDKu)

## Customizing the options

The possible options with key-value pairs are saved in the Model, as constant array:

![](/files/-M7q6vUoiwsU9RODunEI)

So if you want to add more options or customize them, you need to edit those arrays.

In the database, those values are saved as **varchar** type, not as **enum**. That's why it's flexible to change in the model, without doing any manipulations in the database.

## Checkbox Default State

For the **Checkbox** field type, you can select whether it's checked by default:

![](/files/-M7q8q3exXZ1pDCf9bke)

## Visual Result

If we add all three fields shown above, here's how it will look in our panel:

![](/files/-M7q9g-rIjBrfQRkrsVw)

![](/files/-M7q9jeNua2L4LXgYRJr)


# Relationships fields: belongsTo/belongsToMany

QuickAdminPanel supports two types of relationships between CRUDs:&#x20;

* **One-to-Many:** belongsTo() + hasMany()
* **Many-to-Many:** belongsToMany().

To create a relationship, you just need to choose field type **belongsTo** or **belongsToMany.**

![](/files/-M7qHF8JNFo4Lstl8bCg)

You don't need to manually create separate field or pivot table, they will be created automatically:

* In case of **belongsTo**, QuickAdminPanel will automatically create DB column **xxxxxx\_id** with a foreign key to the parent table.
* In case of **belongsToMany**, QuickAdminPanel will automatically create a **pivot table**, you don't need to create that table manually.

Here are examples of filled-in forms for belongsTo and belongsToMany:

![This will automatically generate column vehicles.owner\_id with foreign key to users table](/files/-M7qHcL8Cw_WlsFTLJbc)

![This will automatically generate role\_user pivot table](/files/-M7qHuaLlRSIeNA9cuk_)

## Showing Related Children Records on Parent's Page

One more feature: you can enable the children's records to be shown in a **show()** method page of a parent CRUD.

![](/files/-MAAsCwwKKJWlDhWVwMn)

If you tick that checkbox, then on the View page of the Parent's CRUD you will see a table of children's record:

![You click "View" on parent's record](/files/-MAAuiZTHu8Z71PodWow)

![And it will show children records at the bottom](/files/-MAAupLAYRTfW9Io_1zZ)

## Video Demo of Relationships

This is a quick video demo of both belongsTo and belongsToMany.

{% embed url="<https://www.youtube.com/watch?v=Txjzcz19d0s>" %}

## BelongsTo Relationship to Itself: The Same CRUD

Quick video demo how you can create a relationship to itself, for parent-children "tree". For example, Category belongsTo Category with category\_id field.

{% embed url="<https://www.youtube.com/watch?v=kn-0yZU5mZ8>" %}

## Customizations of Relationships&#x20;

Articles that may also be helpful:

* [Master-Detail Form in Laravel + jQuery: Create Order With Products](https://quickadminpanel.com/blog/master-detail-form-in-laravel-jquery-create-order-with-products/)
* [Laravel BelongsTo and BelongsToMany with Same Table: Possible? Worth it?](https://quickadminpanel.com/blog/belongsto-and-belongstomany-with-same-table-possible-worth-it/)
* [One-To-Many with Soft-Deletes. Deleting Parent: Restrict or Cascade?](https://quickadminpanel.com/blog/one-to-many-with-soft-deletes-deleting-parent-restrict-or-cascade/)
* [Customization: How to Make Two Dependent Dropdowns, like Parent-Child?](https://helpdocs.quickadminpanel.com/customizing-the-code/dependent-dropdowns-parent-child)
* [Laravel BelongsToMany: Add Extra Fields to Pivot Table](https://quickadminpanel.com/blog/laravel-belongstomany-add-extra-fields-to-pivot-table/)


# File/Photo Upload Fields

In CRUDs Editor, we have two field types for files - called **File** and **Photo**. The difference is pretty small - **Photo** field has additional validation parameters for image size.

For uploading files, we're using a very popular package called [Laravel Medialibrary](https://github.com/spatie/laravel-medialibrary), you can view its [official documentation here](https://docs.spatie.be/laravel-medialibrary/v7/introduction).

&#x20;On the front-end, we're using [Dropzone Javascript library](https://www.dropzonejs.com/), here's how it looks together:

![](https://laraveldaily.com/wp-content/uploads/2019/03/file-photo-upload-form.png)

You can read our in-depth tutorial: [Multiple File Upload with Dropzone.js and Laravel MediaLibrary Package](https://laraveldaily.com/multiple-file-upload-with-dropzone-js-and-laravel-medialibrary-package/)

As per Laravel Medialibrary functionality, the files are stored by default in **storage/app/public** folder, dividing every file in its own subfolder with ID number:

![](https://laraveldaily.com/wp-content/uploads/2019/03/file-photo-upload-storage.png)

&#x20;By default, **storage/** internal folders are not available in the browser for public visitors, to change that - you need to run one important Artisan command:

```
php artisan storage:link
```

See more info in the official Laravel documentation: [The Public Disk](https://laravel.com/docs/master/filesystem#the-public-disk)

If you want to change the **location** of where files are stored, change your parameters in **config/filesystems.php** file:

```
'disks' => [

    'local' => [
        'driver' => 'local',
        'root' => storage_path('app'),
    ],

    'public' => [
        'driver' => 'local',
        'root' => storage_path('app/public'),
        'url' => env('APP_URL').'/storage',
        'visibility' => 'public',
    ],

    's3' => [
        'driver' => 's3',
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION'),
        'bucket' => env('AWS_BUCKET'),
        'url' => env('AWS_URL'),
    ],

],
```

In the database, filenames for all CRUDs are stores in one DB table **media** - this is how Laravel Medialibrary works.

![](https://laraveldaily.com/wp-content/uploads/2019/03/file-photo-media.png)

This table uses [Polymorphic Relations](https://www.youtube.com/watch?v=rx1DQBE01b0) to tie the media record to the CRUD Model it belongs to - see columns **model\_type** and **model\_id**.

## Troubleshooting

We can't ensure that all file uploads settings will be correct **on your web-server**. So if something doesn't work on your web-server after download, check out this guide on our blog: [Top 5 Questions/Answers About Spatie MediaLibrary](https://quickadminpanel.com/blog/top-5-questionsanswers-about-spatie-medialibrary/) - it answers these questions:

* Where are my files stored?&#x20;
* Why are my files not shown? 404 error?&#x20;
* How to generate/customize thumbnails?&#x20;
* How to query Media relationships in Eloquent?&#x20;
* Why artisan migrate doesn’t work with MediaLibrary?

Another article related to potential file/photo problem:&#x20;

{% embed url="<https://quickadminpanel.com/blog/why-its-important-to-change-app_url-in-laravel-env-file/>" %}

## Extra Customizations

If you want to save disk space, here's an article for you:

{% embed url="<https://quickadminpanel.com/blog/spatie-medialibrary-resize-original-uploaded-image/>" %}

If you want to change the **maximum file size** to upload, there will be a few places for that setting.

In **create.blade.php** and **edit.blade.php** of your CRUD, at the bottom there's a Dropzone setting **maxFilesize**, which by default is 2 MB:

```
@section('scripts')
<script>
    Dropzone.options.photoDropzone = {
    url: '{{ route('admin.products.storeMedia') }}',
    maxFilesize: 2, // MB
```

Also, you will need to update Laravel, PHP, and web-server settings on the back-end. For this, please read the detailed article:&#x20;

{% embed url="<https://laraveldaily.com/validate-max-file-size-in-laravel-php-and-web-server/>" %}


# Date/Time Picker Fields

## How it Looks

We have three field types related to date and/or time:

![](/files/-MRxU2L36yPSwCBUqNTq)

To make the input more convenient, we use a jQuery library called Datetimepicker, to power all of them. Here's how it looks visually.

**Date Picker:**

![](/files/-MRxV5wLrrkgbIa6rv_q)

**Date/Time Picker:**

![](/files/-MRxVDSwY2_t2aGgNS4c)

**Time Picker:**

![](/files/-MRxZAVpi2zdrC0oxOw4)

## How It Works

To make those pickers work, we're loading a [Bootstrap Date/Time Picker library](https://github.com/Eonasdan/tempus-dominus) directly from CDN:

```
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/css/bootstrap-datetimepicker.min.css" rel="stylesheet" />

...

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/js/bootstrap-datetimepicker.min.js"></script>
```

**Notice**: that library is now renamed to *"Tempus Dominus"* and is in process of creating a new version, but we're using older and stable v4.17.

We also load the [Moment.js library](https://momentjs.com/) from CDN, which helps with time manipulation in JavaScript:

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
```

Then, in the file **public/js/main.js** we have this jQuery code to enable pickers:

```
  moment.updateLocale('en', {
    week: {dow: 1} // Monday is the first day of the week
  })

  $('.date').datetimepicker({
    format: 'MM/DD/YYYY',
    locale: 'en',
    icons: {
      up: 'fas fa-chevron-up',
      down: 'fas fa-chevron-down',
      previous: 'fas fa-chevron-left',
      next: 'fas fa-chevron-right'
    }
  })

  $('.datetime').datetimepicker({
    format: 'MM/DD/YYYY HH:mm:ss',
    locale: 'en',
    sideBySide: true,
    icons: {
      up: 'fas fa-chevron-up',
      down: 'fas fa-chevron-down',
      previous: 'fas fa-chevron-left',
      next: 'fas fa-chevron-right'
    }
  })

  $('.timepicker').datetimepicker({
    format: 'HH:mm:ss',
    icons: {
      up: 'fas fa-chevron-up',
      down: 'fas fa-chevron-down',
      previous: 'fas fa-chevron-left',
      next: 'fas fa-chevron-right'
    }
  })
```

So, in Blade files, every picker input field has its class, like **\<input class="date" />,** or **\<input class="datetime" />**, or **\<input class="timepicker" />**.

## How to Configure/Customize

The most typical customization is changing the date/time formats. I will show you two examples.

First, you can configure your **date** format in the panel settings:

![](/files/-MRy8aFRBhHRvAsvzg3M)

After downloading the code, you can also make the changes. Besides the date formats you can see in the previous code sample (like, *"format: 'MM/DD/YYYY HH:mm:ss'"*), we also have the configuration on the back-end of Laravel, to convert the dates to/from the database.

So, here's **config/panel.php**:

```
return [
    'date_format' => 'm/d/Y',
    'time_format' => 'H:i:s',
    
    // ... other parameters
];
```

So, if you want to change the formats, you need to change them both in JavaScript, and in Laravel.&#x20;

Keep in mind that JavaScript formats are powered by [Moment.js](https://momentjs.com/docs/#/parsing/string-format/), and Laravel formats are powered by the [PHP date() function](https://www.php.net/manual/en/datetime.format.php).&#x20;

### **Example 1. 12-hour Clock instead of a 24-hour Clock**

To use hours like "3pm" instead of "15", you need to make these changes:

**config/panel.php**:

```
return [
    'date_format' => 'm/d/Y',
    'time_format' => 'h:i A',   // changed from 'H:i:s'
```

**public/js/main.js**:

```
$('.datetime').datetimepicker({
    format: 'MM/DD/YYYY hh:mm A',   // changed from 'MM/DD/YYYY HH:mm:ss'
```

### Example 2. Don't Display Seconds

If you want to skip seconds in datetime/time picker, here are the changes to make:

**config/panel.php**:

```
return [
    'date_format' => 'm/d/Y',
    'time_format' => 'H:i',   // changed from 'H:i:s'
```

**public/js/main.js**:

```
$('.datetime').datetimepicker({
    format: 'MM/DD/YYYY HH:mm',   // changed from 'MM/DD/YYYY HH:mm:ss'
```


# Multi-language Projects

QuickAdminPanel uses [Laravel localization system](https://laravel.com/docs/5.8/localization) out-of-the-box, with all translations generated in "**resources/lang**" folder and sub-folder for each language.

## How to Choose Primary Language

&#x20;You can choose the main language you want when creating the panel:

![](/files/-M7qN8I2psljbwS2nYqZ)

Later you can change main language in Settings.

![](/files/-M7qNRRBE2ojF1Q7ZDRe)

## How to Add Multiple Languages

If you want to have **multi-language** system, in **Settings** please specify all the languages you want.

![](/files/-M7qNjaohw1ZVX2FsIF-)

Then in downloaded panel you will have a language switcher dropdown in top-right corner.

![](https://laraveldaily.com/wp-content/uploads/2019/03/multi-lang-switcher.png)

## Who is Responsible for Translating?

Translations for all languages are performed by the **community**, any customer can go to **Settings -> Manage translations** and edit their own language(s), we don't guarantee the correct translations.

If you want to add a new language, which is not in the list, contact us via live-chat or email **<povilas@laraveldaily.com>**, and we will add it.

## What About Translations for Models?

Finally, if you want to have your Models translated with forms in several languages, QuickAdminPanel doesn't support that out-of-the-box. But we've written a detailed article/instruction [how to easily install Laravel Translatable package for that](https://quickadminpanel.com/blog/how-to-add-multi-language-models-to-laravel-quickadminpanel/).


# API Generator

![](/files/-M7qOVRi0ofXEvDw_qqm)

For every CRUD, by default, QuickAdminPanel creates **API Routes** and **Controllers** for your CRUD menu item, so you can use it from your mobile app or front-end.

Whenever you create or edit a CRUD, there's a checkbox whether to generate the API functionality (see above).

If checked, there's a separate Controller created in **app/Http/Controllers/Api/V1/Admin** folder.

```
namespace App\Http\Controllers\Api\V1\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Http\Resources\Admin\UserResource;
use App\User;
use Gate;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class UsersApiController extends Controller
{
    public function index()
    {
        abort_if(Gate::denies('user_access'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        return new UserResource(User::with(['roles'])->get());
    }

    public function store(StoreUserRequest $request)
    {
        $user = User::create($request->all());
        $user->roles()->sync($request->input('roles', []));

        return (new UserResource($user))
            ->response()
            ->setStatusCode(Response::HTTP_CREATED);
    }

    public function show(User $user)
    {
        abort_if(Gate::denies('user_show'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        return new UserResource($user->load(['roles']));
    }

    public function update(UpdateUserRequest $request, User $user)
    {
        $user->update($request->all());
        $user->roles()->sync($request->input('roles', []));

        return (new UserResource($user))
            ->response()
            ->setStatusCode(Response::HTTP_ACCEPTED);
    }

    public function destroy(User $user)
    {
        abort_if(Gate::denies('user_delete'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        $user->delete();

        return response(null, Response::HTTP_NO_CONTENT);
    }
}
```

&#x20;Also it's added to **routes/api.php** file, like this:

```
Route::group(['prefix' => 'admin', 'as' => 'admin.', 'namespace' => 'Api\V1\Admin'], function () {
    Route::apiResource('users', 'UsersApiController');
});
```

You can turn this function on/off for every CRUD separately.

## How to Use Generated API

Default URL endpoints for all CRUDs are **/api/v1/\[crud\_name]**. We generate all API Resourceful Controller methods, so these URLs apply - [see official Laravel docs](https://laravel.com/docs/master/controllers#resource-controllers):

* GET /api/v1/users - get the list of users
* POST /api/v1/users - create new user
* GET /api/v1/users/1 - get the user with users.id = 1
* PUT /api/v1/users/1 - update the user with users.id = 1
* DELETE /api/v1/users/1 - delete the user with users.id = 1

Detailed visual example is in this blog article: [QuickAdminPanel API Generator with Laravel Sanctum](https://blog.quickadminpanel.com/quickadminpanel-api-generator-with-laravel-sanctum/)&#x20;

## How to Customize What API Returns

We also generate [Eloquent API Resources](https://laravel.com/docs/master/eloquent-resources) with their default functionality. See, for example, **app/Http/Resources/Admin/UserResource.php**:

```
namespace App\Http\Resources\Admin;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray($request)
    {
        return parent::toArray($request);
    }
}
```

## Authentication with Laravel Sanctum

**Notice**: our tool is generating API routes that are protected by middleware **auth:sanctum** that comes from [Laravel Sanctum](https://laravel.com/docs/8.x/sanctum).

More information in this article: [QuickAdminPanel API Generator with Laravel Sanctum](https://blog.quickadminpanel.com/quickadminpanel-api-generator-with-laravel-sanctum/)&#x20;

## Uploading Files to API

Separate question from the customers was about uploading files to the API. For that, we have a separate article on our blog, which can be applied with or without QuickAdminPanel: [Laravel API: How to Upload File from Vue.js](https://quickadminpanel.com/blog/laravel-api-how-to-upload-file-from-vue-js/)

## Generating API Documentation

QuickAdminPanel doesn't generate API docs by default, but here's another article on our blog about the tool that we recommend: [Laravel API Documentation with OpenAPI/Swagger](https://quickadminpanel.com/blog/laravel-api-documentation-with-openapiswagger/)

![](/files/-M84zm_oc1aOBgLzFOjW)


# Roles and Permissions

In default QuickAdminPanel generator, we generate two user roles - **Administrator** and **Simple User**. They both have the same permissions for all CRUDs and Modules, except for User Management which is available **only** for administrator.

&#x20;The whole Permissions system is stored in the database in these DB tables:

* permissions
* roles
* permission\_role
* role\_user

![](https://laraveldaily.com/wp-content/uploads/2019/03/roles-permissions.png)

![](https://laraveldaily.com/wp-content/uploads/2019/03/roles-permissions-pivot.png)

&#x20;Every CRUD has five default permissions generated:

* **\*\_access** (whether user sees menu item in sidebar)
* **\*\_create** (whether user can access create form and add new record)
* **\*\_edit** (whether user can access edit form and update existing record)
* **\*\_show** (whether user can access "show" page of a record)
* **\*\_delete** (whether user can delete records)

\
&#x20;These records are seeded with Seeder files, see examples below:

![](https://laraveldaily.com/wp-content/uploads/2019/03/roles-permissions-seed-permission.png)

![](https://laraveldaily.com/wp-content/uploads/2019/03/roles-permissions-seed-pivot.png)

&#x20;If you want to change permissions in downloaded panel, you can log in as Administrator user and go to menu item User Management -> Roles, and then assign all permissions you want to a particular role, by editing it.

![](https://laraveldaily.com/wp-content/uploads/2019/03/roles-permissions-editing.png)

&#x20;In the generated code, we check the permissions in every method of Controller, see **Gate** and **abort\_unless()** methods in example:

```
class BooksController extends Controller
{
    public function index()
    {
        abort_unless(\Gate::allows('book_access'), 403);

        $books = Book::all();

        return view('admin.books.index', compact('books'));
    }

    public function create()
    {
        abort_unless(\Gate::allows('book_create'), 403);

        return view('admin.books.create');
    }

    public function store(StoreBookRequest $request)
    {
        abort_unless(\Gate::allows('book_create'), 403);

        $book = Book::create($request->all());

        return redirect()->route('admin.books.index');
    }

    public function edit(Book $book)
    {
        abort_unless(\Gate::allows('book_edit'), 403);

        return view('admin.books.edit', compact('book'));
    }

    public function update(UpdateBookRequest $request, Book $book)
    {
        abort_unless(\Gate::allows('book_edit'), 403);

        $book->update($request->all());

        return redirect()->route('admin.books.index');
    }

    public function show(Book $book)
    {
        abort_unless(\Gate::allows('book_show'), 403);

        return view('admin.books.show', compact('book'));
    }

    public function destroy(Book $book)
    {
        abort_unless(\Gate::allows('book_delete'), 403);

        $book->delete();

        return back();
    }
}
```

&#x20;On top of that, we add a check in [Form Request classes](https://laravel.com/docs/validation#creating-form-requests), see example:

```
class StoreBookRequest extends FormRequest
{
    public function authorize()
    {
        return \Gate::allows('book_create');
    }
}
```

&#x20;For more information, how Gates work in Laravel, see [official Laravel documentation](https://laravel.com/docs/authorization#writing-gates).


# How to Change Design Template/Theme

At the time of writing, you can choose one of three adminpanel design themes:

* [CoreUI v3](https://coreui.io/demo/3.4.0/):

![](/files/-MA0yhG-F7yHrIpgSpYB)

* [AdminLTE v3](https://adminlte.io/themes/v3/):

![](/files/-M97jWihj4KWPxODpvax)

* [AdminLTE v2](https://adminlte.io/themes/AdminLTE/index2.html)

![](/files/-M97k2_xaccI6pJT2Bu8)

You can choose one of the themes when creating the panel, but then, at any time, you can change the theme in settings:

![](/files/-MA0x0tPY96M73gFI5cq)

A few disclaimers:

1. As we use the theme "as is", we are not responsible for any front-end issues in their code, check their Github issues and documentation.
2. We are using only the **minimum** amount of elements from those themes - only overall design, forms, tables, so things needed for adminpanel. If you want to use more elements from the original theme, read the documentation on their page.
3. We are using the **exact version** (vX.Y.Z) of the theme that is hard-coded in our generator, so if they release some upgrades, it's not automatically reflected on our side. We even had [incident with CoreUI that moved their CDN to a newer v3 version](https://quickadminpanel.com/blog/for-customers-replace-coreui-theme-to-use-v2-1-16-instead-of-v3-0/), breaking the panels for our customers. We are trying to monitor such possible issues and fix them as quickly as possible.

## CoreUI: How to upgrade to CoreUI Pro?

Within our license boundaries, we are using Free version of all themes, including CoreUI. Unfortunately, we don't have any tutorial for the upgrade to Pro. Please [check their documentation](https://coreui.io/pro/).

## How to Change Theme to Some Other One?

It's very individual. From the time of working with Bootstrap-based themes, we found out that only on the surfaces they are similar, deep down under the hood many elements are structured and named differently, so there's no "simple change" of the theme, you need to do it manually based on the docs of current and your desired theme.

That said, all themes are based on [Bootstrap](https://getbootstrap.com/), so naming of classes should remain almost the same, that should help you in the process.

In older version of QuickAdminPanel, in 2017, we had released a tutorial: [Changing QuickAdminPanel theme: from AdminLTE to Gentelella](https://quickadminpanel.com/blog/changing-quickadminpanel-theme-from-adminlte-to-gentelella/). It is now outdated, but it should give you an overall impression, on what work is included in changing the Bootstrap theme.

## Are you planning to release new themes?

We had a few ideas for new themes, like Material Design, but after a while we realized that it takes us a lot of time to adopt (and support!) the new theme, so we better spend those weeks working on the back-end functionality of the Laravel generator, which is our overall mission.

Maybe in the future we will focus more on the design and front-end side of things, if we feel that customers demand this.


# Download Code and Install on Your Web-Server

If your code is finished generating, you should see a menu item in the left sidebar called **Download Full Code**.

![](https://laraveldaily.com/wp-content/uploads/2019/03/download-full-code-sidebar.png)

&#x20;Clicking it will initialize a download of full ZIP archive for your Laravel project, also showing a modal window with **Installation instructions**.

![](https://laraveldaily.com/wp-content/uploads/2019/03/download-full-code-instructions.png)

&#x20;Then, you should unzip the archive and put the files in your web-server folder, configured for Laravel. Code structure should look something like this:

![](https://laraveldaily.com/wp-content/uploads/2019/03/download-full-code-unarchive.png)

&#x20;Finally, you need to perform a set of Laravel-related commands in **Terminal** to install the project. These commands are typical to ANY Laravel project, and are not specific to QuickAdminPanel-generated code - we are trying to stick to standards.

1. Run **cp .env.example .env** command to copy example into real **.env** file, then edit it with DB credentials and other settings you want
2. Run **composer install** command
3. Run **php artisan migrate --seed** command. Seed is important, because it will create the first admin user for you.
4. Run **php artisan key:generate** command
5. If you have file/photo upload fields, run **php artisan storage:link** command
6. And that's it, go to your domain and login with these credentials: **<admin@admin.com> - password**

![](https://laraveldaily.com/wp-content/uploads/2019/03/download-full-code-install.png)

![](https://laraveldaily.com/wp-content/uploads/2019/03/download-full-code-login.png)

## System Requirements

We generate typical Laravel code, so it requires the same things as general Laravel installation - see [official Laravel documentaion](https://laravel.com/docs/master/installation#server-requirements).

&#x20;**Minimum PHP version**:

* PHP 7.2.5 for Laravel 7
* PHP 7.2 for Laravel 6
* PHP 7.1.3 for Laravel 5.8/5.7/5.6

If you have file/photo upload fields in CRUDs, we also require **MySQL 5.7.8+** or **MariaDB 10.2.7+** with JSON columns support, cause we use [Spatie Laravel Medialibrary](https://github.com/spatie/laravel-medialibrary) for file storage, and it requires JSON columns for "media" DB table with [polymorphic relationship](https://www.youtube.com/watch?v=rx1DQBE01b0).

Also, at the time of writing, Spatie Medialibrary v7 has dependency to its PDF-to-image package which requires **Imagemagick PHP library**. Here are instructions how to install Imagemagick - for [Ubuntu](https://ourcodeworld.com/articles/read/645/how-to-install-imagick-for-php-7-in-ubuntu-16-04), and for [Windows](https://mlocati.github.io/articles/php-windows-imagick.html). Or you can run installation with special flag:\
**composer install --ignore-platform-reqs**

## Configuring domain and public folder

Official [Laravel installation guide](https://laravel.com/docs/master/installation#configuration) says this:

After installing Laravel, you should configure your web server's document / web root to be the **public** directory. The **index.php** in this directory serves as the front controller for all HTTP requests entering your application.\
&#x20;So you need to configure your web-server domain to point to **/public** of the downloaded project.

Example domain configuration lines from [Laravel Homestead](https://laravel.com/docs/master/homestead) file **Homestead.yaml**:

```
- map: laravel-local.test
  to: /home/vagrant/Code/laravel-local/public
```

Here's domain setup instruction for other web-servers:

* [Laravel Valet](https://laravel.com/docs/master/valet#serving-sites)
* [Laragon](https://laragon.org/docs/pretty-urls.html)
* [XAMPP](https://coupontree.co/what-is-virtual-host-how-to-configure-virtual-host-in-xampp/)
* [MAMP Pro](https://documentation.mamp.info/en/MAMP-PRO-Mac/Settings/Hosts/General/)

&#x20;Lastly, don't forget to add your domain as **.env** file as **APP\_URL** value:

```
APP_URL=http://laravel-local.test
```

For more details about Laravel project installation on **production** server, please refer to our two articles:

* [How to Deploy Laravel Projects to Live Server: The Ultimate Guide](https://laraveldaily.com/how-to-deploy-laravel-projects-to-live-server-the-ultimate-guide/)
* [What Server is Needed to Deploy Laravel Projects](https://laraveldaily.com/what-server-is-needed-to-deploy-laravel-projects/)

If you encounter any error in the process, please read error message carefully - it may be related to misconfiguration in your server (older PHP version, missing config value etc.). If you can't resolve the problem by yourself, please message us via live-chat or email **<info@laraveldaily.com>**, describing the problem with as many details as you can.


# Push Code to Your Github

You can push the generated code directly to your Github repository.

![](/files/-M89IObz2IsmdGzE1qVT)

As a result, the system will create a new branch in your repository, and push the latest version of the code there.&#x20;

This won't make any changes to your old code, just create a new branch, and then you would have to merge manually, **after reviewing the changes**.

Here's a demo video:

{% embed url="<https://www.youtube.com/watch?v=pu1qYuSPU_M>" %}

{% hint style="info" %}
**Notice:** you shouldn't "blindly" merge that code with your **master** branch, or other branches, just by using Pull Request and Merge from Github. Our pushed code doesn't "know" anything about potential changes you had done manually outside of QuickAdminPanel, so our branch would **overwrite** all your changes. So, please use this function as a **Push** to Github, not as **Merge** via Github.
{% endhint %}


# Edit Code and Merge Changes

*"Can I edit the code after download and generate more CRUDs? How can I merge changes?"*

Pretty common questions we get asked by customers. Imagine you've built a panel, downloaded it and then decided to change some code locally. After a while, you need to build another new CRUD on top – how do you merge changes?

It’s tricky. We didn't build QuickAdminPanel for that scenario, our vision always was that we help to developer a "boilerplate" to download and then proceed manually, without getting back for more CRUDs/modules.

Also, QuickAdminPanel can't **accept/import** your code changes into generator, cause we can't guarantee the outside code is valid, correct and doesn't contain any security issues.&#x20;

So what should you do? Two ways we can help.

## 1. Use Github integration

&#x20;You have a “magic button” called [**Push to GitHub**](https://helpdocs.quickadminpanel.com/using-generated-code/push-code-to-your-github).

![](/files/-M89JnhINe5ysVKnXIZV)

What it does is pushes a new branch to your GitHub with the latest version of your panel code. System doesn’t know what changes you made in your repository or locally, it just pushes the latest version. From there you still need to merge the changes **manually**, but it’s much much easier with git merge tools than just doing manually.

It’s similar how [Laravel Shift](https://laravelshift.com/) works when upgrading your Laravel version – it puts a new branch with suggested code after upgrade, with a lot of comments, and then you decide on what actually gets merged.

## 2. Copy CRUD Files Manually

We have prepared a guide on what files are created inside of every CRUD. So, after using the generator to create a new CRUD, you could identify what exactly are the files to copy-paste, or what file contents to extract.

Read here and watch video: [What Files are Inside the CRUD](https://helpdocs.quickadminpanel.com/using-generated-code/what-files-are-inside-the-crud)

## Yes, it’s still manual

Even having those two ways to help you in merging, is still not that comfortable, you still need to do manual work comparing changes and picking the right ones.

We didn’t come up with a better way so far, cause our system doesn’t know what local changes you made and we can’t accept your code for merging online cause then our generator would break with your custom code. But if you have any ideas how we can make it better, please email me personally **<povilas@laraveldaily.com>** – more than happy to discuss.

What we do internally, when using QuickAdminPanel for client projects, we generate as much as possible with the generator (yes, it requires a lot of thinking upfront with DB schema and pen/paper), and then after download we don’t touch generator at all – all other changes are done locally in PHPStorm.

It not only allows to avoid such code conflicts, but also forces to think upfront, which is really beneficial for the project overall.


# What Files are Inside the CRUD

If you prefer a video version:

{% embed url="<https://www.youtube.com/watch?v=9TI8kzIsZZA>" %}

Sometimes there is a need to create a new CRUD for existing panel, even after a lot of manual code changes. How to add new CRUD's code into existing system? For that, you need to understand its structure.

## Default MVC Files

When you create a CRUD, minimum of **14 new files** are generated automatically - 10 new files, and 4 old ones re-generated.

There may be more changes, depending on CRUDs fields and modules involved.

For example, if you create CRUD called **Transactions** with a few simple columns like "amount" and "transaction\_date", here's the **minimum** list of generated files:

&#x20;**\[New Model]**

* app/Transaction.php

\
&#x20;**\[New Controller]**

* app/Http/Controllers/Admin/TransactionsController.php

\
&#x20;**\[New Form Requests]**

* app/Http/Requests/MassDestroyTransactionRequest.php
* app/Http/Requests/StoreTransactionRequest.php
* app/Http/Requests/UpdateTransactionRequest.php

\
&#x20;**\[New database migration]**

* database/migrations/2019\_12\_02\_000005\_create\_transactions\_table.php

\
&#x20;**\[New Blade views]**

* resources/views/admin/transactions/create.blade.php
* resources/views/admin/transactions/edit.blade.php
* resources/views/admin/transactions/index.blade.php
* resources/views/admin/transactions/show\.blade.php

\
&#x20;**\[Changed main menu Blade view]**

* resources/views/partials/menu.blade.php

\
&#x20;**\[Changed main routes]**

* routes/web.php

\
&#x20;**\[Changed Seeds for Permissions]**

* database/seeds/PermissionsTableSeeder.php

\
&#x20;**\[Changed Translation Files for new CRUD]**

* resources/lang/en/cruds.php

## Database Migrations: Important Notice

After every new or changed CRUD, we regenerate **all** migration files to make sure they are in the right order, to avoid creating foreign keys on non-existing tables. Therefore, keep in mind that you need to double-check the migration files manually, so they still work after you merge changes.

## Additional Files - Depending on Parameters

A few more files may change with each new CRUD.

For example, if you add a **belongsTo relationship** column to other CRUD like **Users**, then additional these files are touched:

* **\[Changed]** app/User.php
* \[New] database/migrations/2019\_12\_02\_relationships\_transactions\_table.php

\
&#x20;If you ticked the checkbox to generate **API**, another list of new files:

* **\[New]** app/Http/Controllers/Api/V1/Admin/TransactionsApiController.php
* **\[New]** app/Http/Resources/Admin/TransactionResource.php
* **\[Changed]** routes/api.php

\
And the list may be bigger, with additional field types or modules.\
But, in short, these are the minimum files you need to download and copy-paste or merge into your existing project code.

## Practical Example

If you want to view content of those files, here is an [example Github Pull Request](https://github.com/LaravelDaily/QuickAdminPanel-Demo-004/pull/1/commits/9092a8066c8a35dda873e9a1e046ab89f984c431) of changed files for one new CRUD, similar to the above example.


# Modules Overview

In addition to the CRUD generator, QuickAdminPanel contains 20+ modules you can install/uninstall and configure.

![List of modules](/files/-MabxU1ZLot71yGrSd7x)

There are 2 types of modules:

* **CRUD Templates Modules** - they just create ready-made CRUDs to save you time
* **Custom Modules** - with totally unique non-CRUD functionality

**Important:** not all the modules are available in all QuickAdminPanel generator versions. \
We have three generators:&#x20;

* jQuery "original" version (released in 2015)
* Vue.js + API version (released in 2020)
* Livewire + Tailwind version (released in 2021)

So, in the Vue and Livewire versions, we have created only the most important modules that customers requested, and may create more of them if there is a demand - email *<info@laraveldaily.com>* to ask for a specific module.

The list of Custom Modules availability per version:

| Non-CRUD Module                                                                           | jQuery Version | Vue + API Version | Livewire + Tailwind Version |
| ----------------------------------------------------------------------------------------- | -------------- | ----------------- | --------------------------- |
| [AJAX Datatables](https://helpdocs.quickadminpanel.com/modules/ajax-datatables-module)    | yes            | not needed        | not needed                  |
| [Dashboard & Reports](https://helpdocs.quickadminpanel.com/modules/dashboard-and-reports) | yes            | yes               | yes                         |
| [User Registration](https://helpdocs.quickadminpanel.com/modules/user-registration)       | yes            | yes               | yes                         |
| [Audit Changes Logs](https://helpdocs.quickadminpanel.com/modules/audit-changes-logs)     | yes            | -                 | yes                         |
| [Multi-Tenancy](https://helpdocs.quickadminpanel.com/modules/multi-tenancy)               | yes            | yes               | yes                         |
| [CSV Import](https://helpdocs.quickadminpanel.com/modules/csv-import)                     | yes            | -                 | yes                         |
| [User Alerts](https://www.youtube.com/watch?v=C9rr-_zWyBw)                                | yes            | -                 | yes                         |
| [Internal Messages](https://helpdocs.quickadminpanel.com/modules/internal-messages)       | yes            | -                 | yes                         |
| [Global Search](https://helpdocs.quickadminpanel.com/modules/global-search)               | yes            | -                 | yes                         |
| [System Calendar](https://helpdocs.quickadminpanel.com/modules/system-calendar)           | yes            | -                 | yes                         |
| [Tasks + Calendar](https://helpdocs.quickadminpanel.com/modules/tasks-+-calendar)         | yes            | -                 | yes                         |
| [Change Notifications](https://helpdocs.quickadminpanel.com/modules/change-notifications) | yes            | -                 | yes                         |
| [User Front-end](https://www.youtube.com/watch?v=I5BAqwdP2Dg)                             | yes            | -                 | -                           |
| [Two-Factor Auth (Email)](https://www.youtube.com/watch?v=OVx5g8ZMwLw)                    | yes            | -                 | -                           |

As you can see, the Vue + API Version has the least amount of modules. The reason is that, after the release of that version, we didn't receive many requests for them. So we decided not to invest in creating the modules for that version and switched our energy towards the newest Livewire version.

But again, we are flexible and we are trying to react to the demand and requests from our customers, so if we see that many people ask for something, it may get on top of our to-do list.


# AJAX Datatables

QuickAdminPanel uses [Datatables.net](https://datatables.net/) for listing the data. By default, it accepts **all entries** from the database, and then JavaScript takes care of pagination, search, filtering, ordering etc.

&#x20;And it is a problem for bigger amount of data, thousands of entries could slow down the page load significantly.

&#x20;This **AJAX Datatables** module uses the data loading page by page, at the time that it's needed, via AJAX.

&#x20;It's also called [Server-side processing](https://datatables.net/examples/data_sources/server_side.html) - Datatables script accept the URL of separate API script which actually loads the data.

```
$('#example').DataTable( {
    "processing": true,
    "serverSide": true,
    "ajax": "../server_side/scripts/server_processing.php"
} );
```

## How does the result look in QuickAdminPanel code?

&#x20;Without this module, generated code looks something like this.

&#x20;**Controller:**

```
public function index()
{
    $courses = Course::all();
    return view('admin.courses.index', compact('courses'));
}
```

&#x20;**View:**

```
<table class="table datatable">
<thead>
	<tr>
		<th>...</th>
		<th>...</th>
		<th>...</th>
		<th>...</th>
	</tr>
</thead>

<tbody>
@if (count($courses) > 0)
    @foreach ($courses as $course)
	<tr>
		<td>...</td>
		<td>...</td>
		<td>...</td>
		<td>...</td>
	</tr>
	@endforeach
@else
	<tr>
		<td colspan="4">No courses.</td>
	</tr>
@endif
</tbody>
</table>
```

&#x20;**JavaScript:**

```
$('.datatable').each(function () {
    $(this).dataTable(window.dtDefaultOptions);
});
```

&#x20;Now, imagine if there were 5000 courses in the table. It would put a heavy load on the browser with JavaScript trying to paginate and filter the data inside of the datatables.

&#x20;So how does it look with **AJAX Datatables**?

&#x20;**composer.json:**

```
...
"yajra/laravel-datatables-oracle": "^9.0",
...
```

&#x20;**Controller:**

```
public function index()
{
    if (request()->ajax()) {
        $query = Course::query();
        $query->with("teachers");
        $template = 'actionsTemplate';
        $table = Datatables::of($query);

        $table->addColumn('actions', ' ');
        $table->editColumn('actions', function ($row) use ($template) {
            $gateKey  = 'course_';
            $routeKey = 'admin.courses';
            return view($template, compact('row', 'gateKey', 'routeKey'));
        });

        // ... More columns described

        $table->editColumn('published', function ($row) {
            return \Form::checkbox("published", 1, $row->published == 1, ["disabled"]);
        });

        return $table->make(true);
    }

    return view('admin.courses.index');
}
```

&#x20;**View:**

```
<table class="table ajaxTable">
<thead>
	<tr>
		<th>...</th>
		<th>...</th>
		<th>...</th>
		<th>...</th>
	</tr>
</thead>
</table>
```

&#x20;**JavaScript:**

```
$('.ajaxTable').each(function () {
    window.dtDefaultOptions.processing = true;
    window.dtDefaultOptions.serverSide = true;
    $(this).DataTable(window.dtDefaultOptions);
});
```

&#x20;As you can see, a lot more logic now goes into **Controller** - it processes AJAX call to the API and parses the column.\
&#x20;In the Blade file the table itself is empty - there are no tr's or td's, it's all loaded via AJAX.

&#x20;In other words, whole page is loaded really fast with empty table, and then after a while the data is being loaded.

&#x20;We use [Laravel Datatables](https://github.com/yajra/laravel-datatables) package for this function.

## How to install/use the module?

&#x20;First, you go to your panel's **Modules** menu item, find the module in the list and click **Install**:

![](https://laraveldaily.com/wp-content/uploads/2019/03/modules-ajax-datatables-install.png)

&#x20;Then, you can switch AJAX function on-off for every CRUD - in Create/Edit menus, you will have a special checkbox for that:

![](https://laraveldaily.com/wp-content/uploads/2019/03/modules-ajax-datatables-crud.png)

## More information

* [Datatables.net](https://datatables.net/)
* [Server-side processing in Datatables](https://datatables.net/examples/data_sources/server_side.html)
* [Laravel Datatables package](https://github.com/yajra/laravel-datatables)

## From Our Blog

* [How to Customize Datatables: 6 Most-Requested Tips](https://quickadminpanel.com/blog/how-to-customize-datatables-6-most-requested-tips/)
* [Advanced Datatables with Laravel: Five Code Examples](https://quickadminpanel.com/blog/advanced-datatables-with-laravel-five-code-examples/)
* [Why you need AJAX (Server-Side) Datatables?](https://quickadminpanel.com/blog/why-you-need-ajax-server-side-datatables/)
* [AJAX Datatables: Move View/Edit/Delete Column from Right to Left Side](https://quickadminpanel.com/blog/ajax-datatables-move-vieweditdelete-column-from-right-to-left-side/)


# System Calendar

![](https://laraveldaily.com/wp-content/uploads/2019/03/modules-system-calendar-calendar.png)

&#x20;This module allows you to generate a calendar with events from one or more CRUDs - in this case, called **Calendar Sources**.

&#x20;You add CRUDs as sources online - as many as you want, and then you download the calendar as part of your generated code.

&#x20;**Notice:** Event sources can be generated only online, downloaded panel doesn't have function to add new CRUDs into calendar, you would have to add your custom code then.

&#x20;To view the data in a calendar form, we use [FullCalendar.io library](https://fullcalendar.io/).

## How does the result look in QuickAdminPanel code?

&#x20;We create one **SystemCalendarController** file which collects all the sources into one **$events** array.

```
class SystemCalendarController extends Controller
{
    public function index()
    {
        $events = [];

        foreach (\App\Job::all() as $job) {
            $crudFieldValue = $job->getOriginal('job_time');

            if (! $crudFieldValue) {
                continue;
            }

            $eventLabel     = $job->title;
            $prefix         = '';
            $suffix         = '';
            $dataFieldValue = trim($prefix . " " . $eventLabel . " " . $suffix);
            $events[]       = [
                'title' => $dataFieldValue,
                'start' => $crudFieldValue,
                'url'   => route('admin.jobs.edit', $job->id)
            ];
        }

        return view('admin.calendar' , compact('events'));
    }

}
```

&#x20;And View file **admin/calendar.blade.php** looks like this:

```
@section('content')
    <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.1.0/fullcalendar.min.css'/>

    <h3 class="page-title">Calendar</h3>

    <div id='calendar'></div>

@endsection

@section('javascript')
@parent
    <script src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.1.0/fullcalendar.min.js'></script>
    <script>
        $(document).ready(function () {
        // page is now ready, initialize the calendar...
        events={!! json_encode($events)  !!};
        $('#calendar').fullCalendar({
            // put your options and callbacks here
            events: events,
        })
    });
</script>
@endsection
```

&#x20;You can easily customize each of the reports after download by adding more logic in the files above.

## How to install/use the module?

&#x20;First, go to your panel's **Modules** menu item, find the module in the list and click **Install**.

![](https://laraveldaily.com/wp-content/uploads/2019/03/modules-system-calendar.png)

&#x20;Then you will see a new menu item **Calendar Sources** on the left, where you can add your CRUDs as sources.

&#x20;Each Source consists of CRUD field (date / datetime) and label field (what to show inside the calendar cell) with ability to add prefix/suffix there.

![](https://laraveldaily.com/wp-content/uploads/2019/03/modules-system-calendar-sources.png)

&#x20;After adding sources, you will see another new menu item **Calendar** - you can check the results there.

&#x20;As soon as you're happy with your panel, download the files, and Calendar menu will be among them.

## More information

Official documentation: [Fullcalendar.io library](https://www.fullcalendar.io/)

Another article that may help you:&#x20;

{% embed url="<https://quickadminpanel.com/blog/laravel-fullcalendar-createedit-recurring-events/>" %}


# Audit Changes Logs

![](https://laraveldaily.com/wp-content/uploads/2019/03/modules-audit-logs-table.png)

&#x20;This module logs all the actions by the users. Every time someone adds/updates/deletes an entry, it will log the action in database table **audit\_logs**, which will be shown in a separate menu item (see above).

![](https://laraveldaily.com/wp-content/uploads/2019/03/modules-audit-logs-database.png)

&#x20;For that, we use [Model Observer](https://laravel.com/docs/5.8/eloquent#observers) functionality of Laravel.

&#x20;Video demo of a module:

{% embed url="<https://www.youtube.com/watch?v=zUIA99xKyJQ>" %}

## How does the result look in QuickAdminPanel code?

&#x20;We create a new database table with migration:

```
Schema::create('audit_logs', function (Blueprint $table) {
    $table->increments('id');
    $table->text('description');
    $table->unsignedInteger('subject_id')->nullable();
    $table->string('subject_type')->nullable();
    $table->unsignedInteger('user_id')->nullable();
    $table->text('properties')->nullable();
    $table->string('host', 45)->nullable();
    $table->timestamps();
});
```

&#x20;Then we create a new model called **app/AuditLog.php**:

```
class AuditLog extends Model
{
    protected $fillable = [
        'description',
        'subject_id',
        'subject_type',
        'user_id',
        'properties',
        'host',
    ];

    protected $casts = [
        'properties' => 'collection',
    ];
}
```

&#x20;Then we create one new **Trait** class, with this code.\
&#x20;**app/Traits/Auditable.php**

```
namespace App\Traits;

use App\AuditLog;
use Illuminate\Database\Eloquent\Model;

trait Auditable
{
    public static function bootAuditable()
    {
        static::created(function (Model $model) {
            self::audit('created', $model);
        });

        static::updated(function (Model $model) {
            self::audit('updated', $model);
        });

        static::deleted(function (Model $model) {
            self::audit('deleted', $model);
        });
    }

    protected static function audit($description, $model)
    {
        AuditLog::create([
            'description'  => $description,
            'subject_id'   => $model->id ?? null,
            'subject_type' => get_class($model) ?? null,
            'user_id'      => auth()->id() ?? null,
            'properties'   => $model ?? null,
            'host'         => request()->ip() ?? null,
        ]);
    }
}
```

&#x20;Then we attach this trait for CRUD model: **app/Project.php:**

```
use App\Traits\Auditable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Project extends Model
{
    use SoftDeletes, Auditable;

    // ...
```

&#x20;Finally, we create a CRUD called **Audit Logs** to just view the table.\
&#x20;**resources/views/admin/auditLogs/index.blade.php**

```
<table class=" table table-bordered table-striped datatable">
<thead>
<tr>
<th>
    {{ trans('global.auditLog.fields.id') }}
</th>
<th>
    {{ trans('global.auditLog.fields.description') }}
</th>
<th>
    {{ trans('global.auditLog.fields.subject_id') }}
</th>
<th>
    {{ trans('global.auditLog.fields.subject_type') }}
</th>
<th>
    {{ trans('global.auditLog.fields.user_id') }}
</th>
<th>
    {{ trans('global.auditLog.fields.host') }}
</th>
<th>
    {{ trans('global.auditLog.fields.created_at') }}
</th>
<th>
 
</th>
</tr>
</thead>
<tbody>
@foreach($auditLogs as $key => $auditLog)
<tr>
    <td>
        {{ $auditLog->id ?? '' }}
    </td>
    <td>
        {{ $auditLog->description ?? '' }}
    </td>
    <td>
        {{ $auditLog->subject_id ?? '' }}
    </td>
    <td>
        {{ $auditLog->subject_type ?? '' }}
    </td>
    <td>
        {{ $auditLog->user_id ?? '' }}
    </td>
    <td>
        {{ $auditLog->host ?? '' }}
    </td>
    <td>
        {{ $auditLog->created_at ?? '' }}
    </td>
    <td>
        @can('audit_log_show')
            <a class="btn btn-xs btn-primary" href="{{ route('admin.audit-logs.show', $auditLog->id) }}">
                {{ trans('global.view') }}
            </a>
        @endcan
        @can('audit_log_edit')
            <a class="btn btn-xs btn-info" href="{{ route('admin.audit-logs.edit', $auditLog->id) }}">
                {{ trans('global.edit') }}
            </a>
        @endcan
        @can('audit_log_delete')
            <form action="{{ route('admin.audit-logs.destroy', $auditLog->id) }}" method="POST" onsubmit="return confirm('{{ trans('global.areYouSure') }}');" style="display: inline-block;">
                <input type="hidden" name="_method" value="DELETE">
                <input type="hidden" name="_token" value="{{ csrf_token() }}">
                <input type="submit" class="btn btn-xs btn-danger" value="{{ trans('global.delete') }}">
            </form>
        @endcan
    </td>

</tr>
@endforeach
</tbody>
</table>
```

&#x20;You can easily customize the module after download by adding more fields or more logic in the files above.

## How to install/use the module?

&#x20;All you need to do is go to your panel's **Modules** menu item, find the module in the list and click **Install**.

![](https://laraveldaily.com/wp-content/uploads/2019/03/modules-audit-logs-install.png)

&#x20;Then for every CRUD you may specify to enable Audit Logs for that CRUD.

![](https://laraveldaily.com/wp-content/uploads/2019/03/modules-audit-logs-crud.png)

## How to Customize the Module

{% embed url="<https://www.youtube.com/watch?v=8L2Jzo0tVpE>" %}

## More information

* [Eloquent Observers](https://laravel.com/docs/master/eloquent#observers)


# Dashboard and Reports

This module allows you to add multiple Report Widgets to your Dashboard. There are five types of Widgets, each with its own parameters.

![](https://laraveldaily.com/wp-content/uploads/2019/03/module-dashboard-reports-widget.png)

&#x20;You can create as many widgets as you want, specifying width for each of them, this way constructing your whole dashboard.

&#x20;Video demo of the module:

{% embed url="<https://www.youtube.com/watch?v=O3ESvZMF9C0>" %}

\
&#x20;In downloaded code, we generate **HomeController.php** which may look like this:

```
use LaravelDaily\LaravelCharts\Classes\LaravelChart;

class HomeController
{
    public function index()
    {
        $settings1 = [
            'chart_title'        => 'Users By Day',
            'chart_type'         => 'line',
            'report_type'        => 'group_by_date',
            'model'              => 'App\User',
            'group_by_field'     => 'created_at',
            'group_by_period'    => 'day',
            'aggregate_function' => 'count',
            'filter_field'       => 'created_at',
            'filter_days'        => '30',
            'column_class'       => 'col-md-12',
            'entries_number'     => '5',
        ];

        $chart1 = new LaravelChart($settings1);

        $settings2 = [
            'chart_title'        => 'Latest Users',
            'chart_type'         => 'latest_entries',
            'report_type'        => 'group_by_date',
            'model'              => 'App\User',
            'group_by_field'     => 'email_verified_at',
            'group_by_period'    => 'day',
            'aggregate_function' => 'count',
            'filter_field'       => 'created_at',
            'column_class'       => 'col-md-12',
            'entries_number'     => '5',
            'fields'             => [
                '0' => 'name',
                '1' => 'email',
                '2' => 'created_at',
            ],
        ];

        $settings2['data'] = $settings2['model']::latest()
            ->take($settings2['entries_number'])
            ->get();

        return view('home', compact('chart1', 'settings2'));
    }
}
```

\
&#x20;You can also group entries by relationships, see video:

{% embed url="<https://www.youtube.com/watch?v=geYLwf8jLCU>" %}

&#x20;For viewing charts, we use our own simple package called [Laravel Charts](https://github.com/LaravelDaily/laravel-charts).


# Multi-Tenancy

This module allows you to restrict access to CRUD entries only to the users who actually created them.<br>

## Video Demo

&#x20;If you want to see this module in action, here's a 5-minute video:

{% embed url="<https://www.youtube.com/watch?v=6puYh_FZE3M>" %}

## How Does It Work

\
&#x20;There are two types of Multi-Tenancy - while installing the module, you can choose:\
&#x20;\- **User Multi-Tenancy** (*every user sees only records they created*)\
&#x20;\- **Team Multi-Tenancy** (*every user sees all records created by any member of **their team***)

![](https://laraveldaily.com/wp-content/uploads/2019/11/Screen-Shot-2019-11-19-at-11.33.46-AM.png)

&#x20;After installing the module, you will see a checkbox for each CRUD separately, whether to use this restriction setting.

![](https://laraveldaily.com/wp-content/uploads/2019/11/Screen-Shot-2019-11-19-at-11.31.17-AM.png)

&#x20;First, let's talk about **User** Multi-Tenancy.

## How Does The Result Look In QuickAdminPanel Code?

\
&#x20;First, for every affected CRUD we add a field **created\_by\_id** with relationship to **User** model:

```
class Customer extends Model
{
    // ...

    public function created_by()
    {
        return $this->belongsTo(User::class, 'created_by_id');
    }
}
```

\
&#x20;Next, we have a special **Trait** for the filter by user:\
&#x20;**app/Traits/MultiTenantModelTrait.php**

```
trait MultiTenantModelTrait
{
    public static function bootMultiTenantModelTrait()
    {
        if (!app()->runningInConsole() && auth()->check()) {
            $isAdmin = auth()->user()->roles->contains(1);

            static::creating(function ($model) use ($isAdmin) {
                // Prevent admin from setting his own id - admin entries are global.

                // If required, remove the surrounding IF condition and admins will act as users
                if (!$isAdmin) {
                    $model->created_by_id = auth()->id();
                }
            });

            if (!$isAdmin) {
                static::addGlobalScope('created_by_id', function (Builder $builder) {
                    $builder->where('created_by_id', auth()->id())->orWhereNull('created_by_id');
                });
            }
        }
    }
}
```

&#x20;The code here may look complicated, but the logic is simple - whether to add global scope or not.

&#x20;Read more about Eloquent Query Scopes in [official Laravel documentaiton here](https://laravel.com/docs/master/eloquent#query-scopes).

&#x20;Finally, we use that Trait in the **model**, like **app/Customer.php**:

```
use App\Traits\MultiTenantModelTrait;

class Customer extends Model
{
    use SoftDeletes, MultiTenantModelTrait;

    // ...
```

## Team Multi-Tenancy

&#x20;For this setting, we generate separate CRUD called **Teams** where administrator can manage the teams. Also, every user may belong to multiple teams:<br>

```
class User extends Authenticatable
{
    // ...
    public function team()
    {
        return $this->belongsTo(Team::class, 'team_id');
    }
}
```

\
&#x20;And then the Trait **app/Traits/MultiTenantModelTrait.php** is expanded with logic about teams:<br>

```
trait MultiTenantModelTrait
{
    public static function bootMultiTenantModelTrait()
    {
        if (!app()->runningInConsole() && auth()->check()) {
            $isAdmin = auth()->user()->roles->contains(1);
            static::creating(function ($model) use ($isAdmin) {
                if (!$isAdmin) {
                    $model->team_id = auth()->user()->team_id;
                }
            });
            if (!$isAdmin) {
                static::addGlobalScope('team_id', function (Builder $builder) {
                    $field = sprintf('%s.%s', $builder->getQuery()->from, 'team_id');

                    $builder->where($field, auth()->user()->team_id)->orWhereNull($field);
                });
            }
        }
    }
}
```

## How To Customize The Module?

&#x20;All the logic of that module is inside of **app/Traits/MultiTenantModelTrait.php** file. So whatever you want to customize, you should do it there.

For example, you may want to customize how **administrator** records are treated. By default, entries created by administrator role user are visible to everyone, because they are considered "system records". If you want to change that, you need to delete **orWhereNull('created\_by\_id')** condition in the Trait:

```
$builder->where('created_by_id', auth()->id())->orWhereNull('created_by_id');
```

{% embed url="<https://www.youtube.com/watch?v=jobtwodnH84>" %}

Another customization example, in a blog article: [Teams Multi-Tenancy: Add “Team Admin” to Manage Users](https://quickadminpanel.com/blog/teams-multi-tenancy-add-team-admin-to-manage-users/)

For more complex logic, you can copy-paste some logic and create another trait, and use your trait in some models you want.

Finally, if on some models or some queries you want to **disable** that filtering, there is a method called **withoutGlobalScope()**:

```
User::withoutGlobalScopes()->get();
```


# CSV Import

{% embed url="<https://www.youtube.com/watch?v=tpZK2A98ig0>" %}

## How to Customize the Import

You can customize the imported fields and add a custom logic, see details in this blog article: [Customize CSV Import Module for Relationships and Passwords](https://quickadminpanel.com/blog/customize-csv-import-module-for-relationships-and-passwords/)&#x20;

Also, a video demo for customization:

{% embed url="<https://www.youtube.com/watch?v=reLzI7cj-Xw>" %}


# Global Search

First, a quick video demo of the module.

{% embed url="<https://www.youtube.com/watch?v=f0tm2TaDVW0>" %}

## How does it work?&#x20;

After installing the module, you can enable any individual **field** in any CRUD to be "searchable":&#x20;

![](/files/-MAFJ7gn0QVXsuCpxoRM)

And then, in the generated panel, in top-left corner, you will see a search field, which, after you type in at least 3 characters, will look for all records in all CRUDs/Fields you specified:

![](/files/-MAFKH-7diOAU0-iiNGR)

## Customization

If you want to customize the behavior of Global Search, it's all in the generated file **app/Http/Controllers/Admin/GlobalSearchController.php**, here's the main method:

```
    public function search(Request $request)
    {
        $search = $request->input('search');

        if ($search === null || !isset($search['term'])) {
            abort(400);
        }

        $term           = $search['term'];
        $searchableData = [];

        foreach ($this->models as $model => $translation) {
            $modelClass = 'App\\' . $model;
            $query      = $modelClass::query();

            $fields = $modelClass::$searchable;

            foreach ($fields as $field) {
                $query->orWhere($field, 'LIKE', '%' . $term . '%');
            }

            $results = $query->take(10)
                ->get();

            foreach ($results as $result) {
                $parsedData           = $result->only($fields);
                $parsedData['model']  = trans($translation);
                $parsedData['fields'] = $fields;
                $formattedFields      = [];

                foreach ($fields as $field) {
                    $formattedFields[$field] = Str::title(str_replace('_', ' ', $field));
                }

                $parsedData['fields_formated'] = $formattedFields;

                $parsedData['url'] = url('/admin/' . Str::plural(Str::snake($model, '-')) . '/' . $result->id . '/edit');

                $searchableData[] = $parsedData;
            }
        }

        return response()->json(['results' => $searchableData]);
    }
```

So if, for example, you want the default click to lead to SHOW method instead of default EDIT, you need to change this line:

```
$parsedData['url'] = url('/admin/' . Str::plural(Str::snake($model, '-')) . '/' . $result->id . '/edit');
```


# User Registration

{% embed url="<https://www.youtube.com/watch?v=gIeVzYl2uJE>" %}


# Internal Messages

{% embed url="<https://www.youtube.com/watch?v=MU2QKsYcul8>" %}


# Change Notifications

{% embed url="<https://www.youtube.com/watch?v=YS_gq9Gyfrw>" %}


# Tasks + Calendar

{% embed url="<https://www.youtube.com/watch?v=rwUna0fQqO8>" %}

Another article that may help you:

{% embed url="<https://quickadminpanel.com/blog/laravel-fullcalendar-createedit-recurring-events/>" %}


# Courses LMS

{% embed url="<https://www.youtube.com/watch?v=iMR8beHfbY8>" %}


# CRUD Templates Modules

We have a lot of modules which are actually a set of CRUDs pre-built for you, to save your time from creating all the fields manually.

## Basic CRM&#x20;

CRUDs for Customers, their Types, Documents and Notes

{% embed url="<https://www.youtube.com/watch?v=IKLNAIWXVOk>" %}

## Product Management

Simple management system for products, their categories, tags and photos.

{% embed url="<https://www.youtube.com/watch?v=YOLxWxg-3tY>" %}

## Asset Management

Simple asset management system for the organization - track your hardware's location and assign people.

{% embed url="<https://www.youtube.com/watch?v=wiFqhn6TfHo>" %}

## Content Management

Static pages management for your project - things like “About us”, “History” etc.

{% embed url="<https://www.youtube.com/watch?v=zkUpaTxG21c>" %}

## Expenses Management

Small project for tracking expenses, consists of CRUDs like Categories, Income, Expenses, Reports.

{% embed url="<https://www.youtube.com/watch?v=LVzjiDiGUHI>" %}

## Client Management (for freelancers)

Client management system for freelancers: clients, projects, income reports and some flexible settings.

{% embed url="<https://www.youtube.com/watch?v=X9U5q7EvF1Q>" %}

## FAQ Management

Frequently Asked Questions - manage questions/answers and assign them to categories.

{% embed url="<https://www.youtube.com/watch?v=h7UR9Sgv-Ac>" %}

## Contacts Management

Simple contact management - CRUDs for Companies and Contacts.

{% embed url="<https://www.youtube.com/watch?v=wnTF8r5Noio>" %}

## Time Management

Small project for time tracking, consists of CRUDs like Work Types, Project, Time Entries and Reports.

{% embed url="<https://www.youtube.com/watch?v=GtVZkOYe9h4>" %}


# Datatables Customizations

Here you see a list of the articles on our blog, about various customizations of Datatables.

{% embed url="<https://quickadminpanel.com/blog/how-to-customize-vieweditdelete-buttons-column-in-ajax-datatables/>" %}

{% embed url="<https://quickadminpanel.com/blog/laravel-datatables-position-re-ordering-with-dragdrop/>" %}

{% embed url="<https://quickadminpanel.com/blog/datatables-editdelete-buttons-hide-under-javascript-dropdown/>" %}

{% embed url="<https://quickadminpanel.com/blog/laravel-how-to-add-background-color-to-datatables-columns/>" %}

{% embed url="<https://quickadminpanel.com/blog/laravel-ajax-datatables-multi-rows-cells-with-images/>" %}

{% embed url="<https://quickadminpanel.com/blog/ajax-datatables-move-vieweditdelete-column-from-right-to-left-side/>" %}

{% embed url="<https://quickadminpanel.com/blog/demo-transactions-datatables-with-date-range-filter-and-chart-on-top/>" %}

{% embed url="<https://www.youtube.com/watch?v=e-HA2YQUoi0>" %}


# Upgrade Laravel version

If you want to upgrade a Laravel version in your project, you can do that by **Cloning** the project and choosing a different Laravel version (you can also *downgrade* version this way).

![](/files/-M7qXRBrcJYfdOoGbPNJ)

It will generate a new panel for you, starting from zero and with Laravel version you have chosen.


# Dependent Dropdowns: Parent-Child

In our QuickAdminPanel generator, we don't have such feature like parent-child dropdowns, for example Country-City relationship where change of Country value refreshes the values of Cities.

However, you can build it yourself quite easily - we have a blog article for you: [Laravel Forms: Select Dependent Dropdowns with jQuery and AJAX](https://quickadminpanel.com/blog/laravel-forms-select-dependent-dropdowns-with-jquery-and-ajax/)

&#x20;In summary, you need to add jQuery code with AJAX call, similar to this:

```
@section('scripts')
    <script type="text/javascript">
    $("#country").change(function(){
        $.ajax({
            url: "{{ route('admin.cities.get_by_country') }}?country_id=" + $(this).val(),
            method: 'GET',
            success: function(data) {
                $('#city').html(data.html);
            }
        });
    });
    </script>
@endsection
```

&#x20;Read more details in the article above.


# Add Front User Without Admin Permissions

By default, QuickAdminPanel generates two roles: **Admin** and **Simple User**, both being able to access the admin panel, with a bit different permissions. But what if you want to have Simple User as a **front-end user**, with only their own front pages you would build, and they wouldn't even see/access the admin panel?

## Step 1. Front Homepage: Remove default redirect to /login

&#x20;In the first line of **routes/web.php** file, we have this redirect:

```
Route::redirect('/', '/login');
```

&#x20;It means there is no front homepage, just adminpanel. But you can change it easily, to this:

```
Route::view('/', 'welcome');
```

The view **resources/views/welcome.blade.php** comes from default Laravel, and now you have your front-end homepage, with Login link on the top-right:&#x20;

![](/files/-M8-HrFBJt6zb4QPOMv9)

You can customize that Blade file however you want, to build a proper designed homepage.

## Step 2. Better "Welcome" page

Instead of default "empty" welcome page, let's take a bit more advanced, but still default Laravel template - from Laravel UI package (it used to be core Laravel before Laravel 7), and we need this file: [layouts/app.stub](https://github.com/laravel/ui/blob/2.x/src/Auth/bootstrap-stubs/layouts/app.stub) to become our **resources/views/layouts/user.blade.php**:

![](/files/-M8-HzmrzwRAY56hbyy9)

Then, we can change our **resources/views/welcome.blade.php** into this:

```
@extends('layouts.user')
@section('content')
<div class="row justify-content-center">
    <div class="col-md-6">
        <div class="card mx-4">
            <div class="card-body p-4">
                Welcome!
            </div>
        </div>
    </div>
</div>

@endsection	
```

**Notice**: the structure is partly taken from [default Laravel login page](https://github.com/laravel/ui/blob/2.x/src/Auth/bootstrap-stubs/auth/login.stub). And now, we have this homepage!

![](/files/-M8-IHCPHVvELImuC9sL)

Why did we do it? Not only to make homepage more structured, but so that we can **re-use** the same layout for the home page of a logged-in user.

## Step 3. Logged-in User's Homepage

Now, let's create an inside page that would be a homepage **after logging in**. We copy-paste the **welcome.blade.php** from above example, changing just the inner text. And this will become our **resources/views/user/home.blade.php**:

```
@extends('layouts.user')
@section('content')
<div class="row justify-content-center">
    <div class="col-md-6">
        <div class="card mx-4">
            <div class="card-body p-4">
                You are logged in!
            </div>
        </div>
    </div>
</div>

@endsection
```

To show that page, we will create a Controller, and separate it in a subfolder for all future front-user Controllers.

Here's **app/Http/Controllers/User/HomeController.php**:

```
namespace App\Http\Controllers\User;

class HomeController
{
    public function index()
    {
        return view('user.home');
    }
}
```

**Notice:** Don't forget the **namespace**! Cause we already have a **HomeController** with the same name in **Admin** namespace.&#x20;

Finally, we need to make a **route** for it. But let's create the whole Route Group for all the future front-user routes. We almost copy-paste the part of **Route::group()** for admins, just changing "admin" to "user" everywhere.

&#x20;**routes/web.php**:

```
// Old existing route group for administrators - we don't touch it
Route::group([
    'prefix' => 'admin', 
    'as' => 'admin.', 
    'namespace' => 'Admin', 
    'middleware' => ['auth']
], function () {
    Route::get('/', 'HomeController@index')->name('home');
    
    // Permissions
    Route::delete('permissions/destroy', 'PermissionsController@massDestroy')->name('permissions.massDestroy');
    Route::resource('permissions', 'PermissionsController');
    
    // ... other admin routes
});

// Our NEW group - for front-end users
Route::group([
    'prefix' => 'user', 
    'as' => 'user.', 
    'namespace' => 'User', 
    'middleware' => ['auth']
], function () {
    Route::get('/', 'HomeController@index')->name('home');
});	
```

**Notice:** we give the same name to the routes: **->name('home')**. But since Route::group() parts have different **prefix**, the actual route names will be different: **admin.home** and **user.home**.

&#x20;Now, we have two separate designs and pages for admins and users.

![Homepage for the administrators](/files/-M8-IiS4pFYvB1eweAti)

![Homepage for the simple users](/files/-M8-InaR33bK6zktjr7L)

## Step 4. Check Role: Redirect after login

By default in Laravel, the page to redirect after login is defined in **app/Http/Controllers/Auth/LoginController.php**, in property **$redirectTo**:

```
class LoginController extends Controller
{
    // This is code in Laravel 7 
    // In earlier version it may be different
    protected $redirectTo = RouteServiceProvider::HOME;	

    // ... other code
```

We need to override it, and we can do it by just defining a method called **redirectPath()** in the same LoginController.

And we need to check the role - if the user is administrator, or not. Luckily, QuickAdminPanel has generated a helper method inside of **app/User.php** model:

```
class User extends Authenticatable
{
    // ...

    public function getIsAdminAttribute()
    {
        return $this->roles()->where('id', 1)->exists();
    }
```

&#x20;So, here's what we would need to do in **LoginController**:

```
class LoginController extends Controller
{
    // ...

    public function redirectPath()
    {
        $user = auth()->user()->is_admin ? 'admin' : 'user';
        return route($user . '.home');
    }
}	
```

{% hint style="info" %}
If you don't understand how **getIsAdminAttribute()** became **auth()->user()->is\_admin**, you can read more about Eloquent Accessors [here in the official Laravel docs](https://laravel.com/docs/7.x/eloquent-mutators#defining-an-accessor).
{% endhint %}

So, now, every user will be redirected to their own page/section after login. So simple users wouldn't even see the administrator page design.

But wait, they can still access it if they enter the URL in the browser! Let's work on changing the permissions now - it will be our final step.

## Step 5. Remove Admin Permissions from User

First, let's remove a piece of code in **routes/web.php** which automatically redirects to **admin.home** - we don't need this anymore:

```
Route::get('/home', function () {
    if (session('status')) {
        return redirect()->route('admin.home')->with('status', session('status'));
    }

    return redirect()->route('admin.home');
});
```

Next, we need to remove the permissions from Users. They are assigned in a file **database/seeds/PermissionRoleTableSeeder.php**:

```
public function run()
{
    // These are administrator permissions, they should stay
    $admin_permissions = Permission::all();
    Role::findOrFail(1)->permissions()->sync($admin_permissions->pluck('id'));

    // These are users permissions to admin area
    // That block should be removed
    $user_permissions = $admin_permissions->filter(function ($permission) {
        return substr($permission->title, 0, 5) != 'user_' && substr($permission->title, 0, 5) != 'role_' && substr($permission->title, 0, 11) != 'permission_';
    });
    Role::findOrFail(2)->permissions()->sync($user_permissions);
}	
```

&#x20;After you remove everything related to **$user\_permissions** you need to re-seed the database from scratch, by running this command:

```
php artisan migrate:fresh --seed
```

{% hint style="warning" %}
**IMPORTANT!** This command above will delete ALL YOUR DATABASE and migrate/seed from zero. So if you already have important data, you better remove permissions manually - by deleting entries in **permission\_role** DB table with **role\_id = 2**.
{% endhint %}

Finally, let's create a **Middleware** class that we would assign to the Route::group of administrators - it will allow those routes to be accessed only by administrators.

```
php artisan make:middleware IsAdminMiddleware	
```

&#x20;And then we put this code in **app/Http/Middleware/IsAdminMiddleware.php**:

```
class IsAdminMiddleware
{
    public function handle($request, Closure $next)
    {
        if (!auth()->user()->is_admin) {
            return redirect()->route('user.home');
        }
        
        return $next($request);
    }
}
```

&#x20;We just redirect to user's area, if they are not an administrator.

Now, we need to assign that Middleware class to the routes. To do that, we need to give it a name. Let's call it "admin", and we register it in **app/Http/Kernel.php**:

```
class Kernel extends HttpKernel
{
    // ...

    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,

        // ... other routes

        'admin' => \App\Http\Middleware\IsAdminMiddleware::class,
    ];
}	
```

&#x20;And now we can assign it to the Route group in **routes/web.php**:

```
Route::group([
    'prefix' => 'admin', 
    'as' => 'admin.', 
    'namespace' => 'Admin', 
    'middleware' => ['auth', 'admin'] // <= This is our new middleware
], function () {
    Route::get('/', 'HomeController@index')->name('home');
    // ... other admin routes
});	
```

And that's it, if some simple user would try to load **/admin/\[something]** in the URL, they would automatically be redirected to **/user** homepage.

The code with all the changes, is in this repository: <https://github.com/LaravelDaily/QuickAdminPanel-Front-User-Permissions>

Also, we released a separate video on how to implement a front-end theme in 6 steps:

{% embed url="<https://www.youtube.com/watch?v=iDPDRgFlHS0>" %}


# How to Add Mass Actions to Datatable

In QuickAdminPanel tables, you can tick the checkbox on every row, and then click **Delete Selected:**

![](/files/-MAP72w6pfJbmEpCh_GL)

That's the only "mass action" available. But you can create more, pretty easily.

You just need to repeat a block of code from **index.blade.php** file.&#x20;

## How it Works Now: Delete Selected

Currently, in **resources/views/admin/courses/index.blade.php** we have this JavaScript code for **Delete selected** button:

```
@can('course_delete')
  let deleteButtonTrans = '{{ trans('global.datatables.delete') }}'
  let deleteButton = {
    text: deleteButtonTrans,
    url: "{{ route('admin.courses.massDestroy') }}",
    className: 'btn-danger',
    action: function (e, dt, node, config) {
      var ids = $.map(dt.rows({ selected: true }).nodes(), function (entry) {
          return $(entry).data('entry-id')
      });

      if (ids.length === 0) {
        alert('{{ trans('global.datatables.zero_selected') }}')

        return
      }

      if (confirm('{{ trans('global.areYouSure') }}')) {
        $.ajax({
          headers: {'x-csrf-token': _token},
          method: 'POST',
          url: config.url,
          data: { ids: ids, _method: 'DELETE' }})
          .done(function () { location.reload() })
      }
    }
  }
  dtButtons.push(deleteButton)
@endcan
```

Let's break it down - what is happening here:

1. Checking **@can('course\_delete')** for permission for this button - read more about roles/permissions [here](https://helpdocs.quickadminpanel.com/create-panel/roles-permissions).
2. Button label comes from translations - see **trans()** method
3. We need to assign the URL - what should happen after the click, so we specify **route('admin.courses.massDestroy')**
4. We choose button class - **btn-danger**
5. We specify the action: first we get the list of record IDs (every row has *\<tr data-entry-id="xxx">*), if that list is empty - we throw error alert that no rows are selected
6. We ask for confirmation - do you want to delete selected record?
7. If it's confirmed, we're making AJAX request to the URL we specified in Step 3, and then reload the whole page

That URL is specified in **routes/web.php**:

```
Route::delete('courses/destroy', 'CoursesController@massDestroy')
    ->name('courses.massDestroy');
```

Finally, here's how it works in **app/Http/Controllers/Admin/CoursesController.php**:

```
public function massDestroy(MassDestroyCourseRequest $request)
{
    Course::whereIn('id', request('ids'))->delete();

    return response(null, Response::HTTP_NO_CONTENT);
}
```

## Adding Another Mass-Action: Publish

For example, let's add another button **Publish** which will mass-publish the Courses from the table above.

We almost copy-paste the **index.blade.php** block to a new one, just below the old one:

```
let publishButtonTrans = 'Publish'
let publishButton = {
    text: publishButtonTrans,
    url: "{{ route('admin.courses.massPublish') }}",
    className: 'btn-warning',
    action: function (e, dt, node, config) {
        var ids = $.map(dt.rows({ selected: true }).nodes(), function (entry) {
            return $(entry).data('entry-id')
        });

        if (ids.length === 0) {
            alert('{{ trans('global.datatables.zero_selected') }}')

            return
        }

        if (confirm('{{ trans('global.areYouSure') }}')) {
            $.ajax({
                headers: {'x-csrf-token': _token},
                method: 'POST',
                url: config.url,
                data: { ids: ids }})
                .done(function () { location.reload() })
        }
    }
}
dtButtons.push(publishButton)
```

What I've changed here:

* **Button Label**: *publishButtonTrans = 'Publish'* (or you can use translations, too)
* **Different URL**: *route('admin.courses.massPublish')* - we will create it in a minute
* **Button Class**: instead of *btn-danger*, I specified *btn-warning*
* **AJAX Method:** instead of '*DELETE*' I use simple '*POST*'

**Notice**: if you're using [AJAX Datatables module](https://helpdocs.quickadminpanel.com/modules/ajax-datatables-module), you need to populate the **ids** array a bit differently:

```
var ids = $.map(dt.rows({ selected: true }).nodes(), function (entry) {
    return entry.id   // instead of return $(entry).data('entry-id')
});
```

And, that's it! We have a button:

![](/files/-MAPAFwZzAeFmZ7uTKSH)

To make that button work, we specify the URL in **routes/web.php**:

```
Route::post('courses/publish', 'CoursesController@massPublish')
    ->name('courses.massPublish');
```

**Notice**: that Route should come **before** the resource line of **Route::resource()** for the same Controller, otherwise it may conflict with Resourceful methods and be overridden.

Finally, we implement that **massPublish** in **app/Http/Controllers/Admin/CoursesController.php** - almost identical to **massDestroy** above:

```
public function massPublish(Request $request)
{
    Course::whereIn('id', request('ids'))->update(['is_published' => 1]);

    return response(null, Response::HTTP_NO_CONTENT);
}
```

That's it, you have you mass-action!


# QuickAdminPanel: Vue.js+Laravel Version

In August 2020, we released a new separate QuickAdminPanel version that generates Vue.js + Laravel API code.&#x20;

![](/files/-MYn_RVXoLbHB1PX2Gu5)

{% hint style="info" %}
**Notice**: this version is available only for the **Yearly Plan** customers.
{% endhint %}

Compared to the "classic" generator version with jQuery Datatables, this Vue+Laravel code is totally different.&#x20;

It's a SPA with a front-end-first approach, where most of the logic is inside Vue, using Vue Components, Vue Router, Vuex. Laravel serves only as an API layer, powered by Laravel Sanctum authentication.

Here are a few screenshots of a simple adminpanel, fully generated without writing a single line of code.&#x20;

![Login form page](/files/-MYnd2pnpw0t3xDp-sut)

![Simple Datatable Example](/files/-MYnd8kwXsRIQrGszJWY)

![Simple Form Example](/files/-MYndDhatgC2SaI1GaOE)

For the design, we're using a [Material Dashboard theme by Creative Tim](https://www.creative-tim.com/product/material-dashboard), based on Bootstrap 4.

## Structure of Generated Vue.js Code

As mentioned above, most of the logic of generated panel is inside of Vue.js SPA application.&#x20;

That said, the public non-auth part of the website is simple Laravel + Blade, without Vue.js at all, the SPA behavior starts only when you log into the panel.

So, inside of the main Blade file **resources/views/layouts/admin/app.blade.php** you will find this line:

```
<div id="app"></div>
```

And from there, everything happens with Vue, in the folder **resources/adminapp/js**

Here's the code of the main **resources/adminapp/js/app.js**:

```
/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap')

window.Vue = require('vue')
window.moment.updateLocale('en', { week: { dow: 1 } })

Vue.config.productionTip = false
Vue.prototype.$jquery = $

import App from './App.vue'

// Core
import router from './routes/routes'
import store from './store/store'
import i18n from './i18n'

// Plugins

import GlobalComponents from './globalComponents'
import GlobalDirectives from './globalDirectives'
import GlobalMixins from './mixins/global'
import { mapGetters, mapActions } from 'vuex'

Vue.use(GlobalComponents)
Vue.use(GlobalDirectives)
Vue.use(GlobalMixins)

/**
 * 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.
 */

const app = new Vue({
  el: '#app',
  render: h => h(App),
  router,
  store,
  i18n,
  created() {
    this.fetchLanguages()
  },
  methods: {
    ...mapActions('I18NStore', ['fetchLanguages'])
  }
})
```

Then, all the generated CRUDs are registered as Routes, in **resources/adminapp/js/routes/routes.js**:

```
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const View = { template: '<router-view></router-view>' }

const routes = [
  {
    path: '/',
    component: () => import('@pages/Layout/DashboardLayout.vue'),
    redirect: 'dashboard',
    children: [
      {
        path: 'dashboard',
        name: 'dashboard',
        component: () => import('@pages/Dashboard.vue'),
        meta: { title: 'global.dashboard' }
      },
      {
        path: 'user-management',
        name: 'user_management',
        component: View,
        redirect: { name: 'permissions.index' },
        children: [
          {
            path: 'permissions',
            name: 'permissions.index',
            component: () => import('@cruds/Permissions/Index.vue'),
            meta: { title: 'cruds.permission.title' }
          },
          {
            path: 'permissions/create',
            name: 'permissions.create',
            component: () => import('@cruds/Permissions/Create.vue'),
            meta: { title: 'cruds.permission.title' }
          },
          {
            path: 'permissions/:id',
            name: 'permissions.show',
            component: () => import('@cruds/Permissions/Show.vue'),
            meta: { title: 'cruds.permission.title' }
          },
          {
            path: 'permissions/:id/edit',
            name: 'permissions.edit',
            component: () => import('@cruds/Permissions/Edit.vue'),
            meta: { title: 'cruds.permission.title' }
          },
        ]
      },
      
      // ... More routes
    ]
  }
]

export default new VueRouter({
  mode: 'history',
  base: '/admin',
  routes
})
```

For every CRUD, we generate a set of Vue.js components, in the folder **resources/adminapp/js/components/\[CRUD Folder]**.

Here's an example of the list page of Transactions CRUD, in **resources/adminapp/js/components/Transactions/Index.vue**:

```
<template>
  <div class="container-fluid">
    <div class="row">
      <div class="col-md-12">
        <div class="card">
          <div class="card-header card-header-primary card-header-icon">
            <div class="card-icon">
              <i class="material-icons">assignment</i>
            </div>
            <h4 class="card-title">
              {{ $t('global.table') }}
              <strong>{{ $t('cruds.transaction.title') }}</strong>
            </h4>
          </div>
          <div class="card-body">
            <router-link
              class="btn btn-primary"
              v-if="$can(xprops.permission_prefix + 'create')"
              :to="{ name: xprops.route + '.create' }"
            >
              <i class="material-icons">
                add
              </i>
              {{ $t('global.add') }}
            </router-link>
            <button
              type="button"
              class="btn btn-default"
              @click="fetchIndexData"
              :disabled="loading"
              :class="{ disabled: loading }"
            >
              <i class="material-icons" :class="{ 'fa-spin': loading }">
                refresh
              </i>
              {{ $t('global.refresh') }}
            </button>
          </div>
          <div class="card-body">
            <div class="row">
              <div class="col-md-12">
                <div class="table-overlay" v-show="loading">
                  <div class="table-overlay-container">
                    <material-spinner></material-spinner>
                    <span>Loading...</span>
                  </div>
                </div>
                <datatable
                  :columns="columns"
                  :data="data"
                  :total="total"
                  :query="query"
                  :xprops="xprops"
                  :HeaderSettings="false"
                  :pageSizeOptions="[10, 25, 50, 100]"
                >
                  <global-search :query="query" class="pull-left" />
                  <header-settings :columns="columns" class="pull-right" />
                </datatable>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'
import DatatableActions from '@components/Datatables/DatatableActions'
import TranslatedHeader from '@components/Datatables/TranslatedHeader'
import HeaderSettings from '@components/Datatables/HeaderSettings'
import GlobalSearch from '@components/Datatables/GlobalSearch'

export default {
  components: {
    GlobalSearch,
    HeaderSettings
  },
  data() {
    return {
      columns: [
        {
          title: 'cruds.transaction.fields.id',
          field: 'id',
          thComp: TranslatedHeader,
          sortable: true,
          colStyle: 'width: 100px;'
        },
        {
          title: 'cruds.transaction.fields.amount',
          field: 'amount',
          thComp: TranslatedHeader,
          sortable: true
        },
        {
          title: 'cruds.transaction.fields.transaction_date',
          field: 'transaction_date',
          thComp: TranslatedHeader,
          sortable: true
        },
        {
          title: 'global.actions',
          thComp: TranslatedHeader,
          tdComp: DatatableActions,
          visible: true,
          thClass: 'text-right',
          tdClass: 'text-right td-actions',
          colStyle: 'width: 150px;'
        }
      ],
      query: { sort: 'id', order: 'desc', limit: 100, s: '' },
      xprops: {
        module: 'TransactionsIndex',
        route: 'transactions',
        permission_prefix: 'transaction_'
      }
    }
  },
  beforeDestroy() {
    this.resetState()
  },
  computed: {
    ...mapGetters('TransactionsIndex', ['data', 'total', 'loading'])
  },
  watch: {
    query: {
      handler(query) {
        this.setQuery(query)
        this.fetchIndexData()
      },
      deep: true
    }
  },
  methods: {
    ...mapActions('TransactionsIndex', [
      'fetchIndexData',
      'setQuery',
      'resetState'
    ])
  }
}
</script>
```

For the Datatables, we're using the Vue2-Datatable package, which we [forked under our own LaravelDaily name](https://github.com/LaravelDaily/vue2-datatable), to be able to have more control or fixes if needed.

You can see the contents of all other Vue files by checking out [the demo repository](https://github.com/LaravelDaily/QuickAdminPanel-Vue-Example).

## Laravel API Structure

On the back-end, in Laravel, we generate the API routes and Controllers.

Here's the example **routes/api.php**:

```
<?php

Route::group(['prefix' => 'v1', 'as' => 'api.', 'namespace' => 'Api\V1\Admin', 'middleware' => ['auth:sanctum']], function () {
    // Abilities
    Route::apiResource('abilities', 'AbilitiesController', ['only' => ['index']]);

    // Locales
    Route::get('locales/languages', 'LocalesController@languages')->name('locales.languages');
    Route::get('locales/messages', 'LocalesController@messages')->name('locales.messages');

    // Permissions
    Route::resource('permissions', 'PermissionsApiController');

    // Roles
    Route::resource('roles', 'RolesApiController');

    // Users
    Route::resource('users', 'UsersApiController');

    // Contact Company
    Route::resource('contact-companies', 'ContactCompanyApiController');

    // Contact Contacts
    Route::resource('contact-contacts', 'ContactContactsApiController');

    // Transactions
    Route::resource('transactions', 'TransactionsApiController');
});
```

And here's an example API Controller, in **app/Http/Controllers/Api/V1/Admin/TransactionsApiController.php**:

```
<?php

namespace App\Http\Controllers\Api\V1\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreTransactionRequest;
use App\Http\Requests\UpdateTransactionRequest;
use App\Http\Resources\Admin\TransactionResource;
use App\Models\Transaction;
use Gate;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class TransactionsApiController extends Controller
{
    public function index()
    {
        abort_if(Gate::denies('transaction_access'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        return new TransactionResource(Transaction::advancedFilter());
    }

    public function store(StoreTransactionRequest $request)
    {
        $transaction = Transaction::create($request->validated());

        return (new TransactionResource($transaction))
            ->response()
            ->setStatusCode(Response::HTTP_CREATED);
    }

    public function create(Transaction $transaction)
    {
        abort_if(Gate::denies('transaction_create'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        return response([
            'meta' => [],
        ]);
    }

    public function show(Transaction $transaction)
    {
        abort_if(Gate::denies('transaction_show'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        return new TransactionResource($transaction);
    }

    public function update(UpdateTransactionRequest $request, Transaction $transaction)
    {
        $transaction->update($request->validated());

        return (new TransactionResource($transaction))
            ->response()
            ->setStatusCode(Response::HTTP_ACCEPTED);
    }

    public function edit(Transaction $transaction)
    {
        abort_if(Gate::denies('transaction_edit'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        return response([
            'data' => new TransactionResource($transaction),
            'meta' => [],
        ]);
    }

    public function destroy(Transaction $transaction)
    {
        abort_if(Gate::denies('transaction_delete'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        $transaction->delete();

        return response(null, Response::HTTP_NO_CONTENT);
    }
}
```

We also generate [Laravel API Resources](https://laravel.com/docs/master/eloquent-resources), so it would be easier to customize in the future, but they contain mostly default Laravel code. Example from **app/Http/Resources/Admin/TransactionResource.php**:

```
<?php

namespace App\Http\Resources\Admin;

use Illuminate\Http\Resources\Json\JsonResource;

class TransactionResource extends JsonResource
{
    public function toArray($request)
    {
        return parent::toArray($request);
    }
}
```

As mentioned above, Authorization is powered by [Laravel Sanctum](https://laravel.com/docs/8.x/sanctum), and for Roles and Permissions on the front-end, we use [CASL Vue package](https://www.npmjs.com/package/@casl/vue), see video demo below:&#x20;

{% embed url="<https://www.youtube.com/watch?v=JatpAUl6_5E>" %}

You can look at the full code of this example adminpanel [in this public repository](https://github.com/LaravelDaily/QuickAdminPanel-Vue-Example).


# What Files are Inside Vue.js+Laravel CRUD?

When you create a CRUD in Vue QuickAdminPanel, minimum of **12 new files** are generated automatically, and **6 more files** are updated. Potentially more, if you use some advanced features/modules.

For example, if you create CRUD called **Transactions** with a few simple columns like "amount" and "transaction\_date", here's the **minimum** list of generated files.

### NEW Back-end Laravel Files

**\[New Model]**

* app/Transaction.php

**\[New API Controller]**

* app/Http/Controllers/Api/V1/Admin/TransactionsApiController.php

**\[New Form Requests for Validation]**

* app/Http/Requests/StoreTransactionRequest.php
* app/Http/Requests/UpdateTransactionRequest.php

**\[New Eloquent API Resource]**

* app/Http/Resources/Admin/TransactionResource.php

**\[New Database Migration]**

* database/migrations/2020\_09\_11\_000006\_create\_transactions\_table.php

**-----------------------------------------------------------**

### **UPDATED Back-end Laravel Files**

**\[Changed API Routes]**

* routes/api.php

**\[Changed Seeds for Permissions]**

* database/seeds/PermissionsTableSeeder.php

**\[Changed Translation File]**

* resources/lang/en/cruds.php

**-----------------------------------------------------------**

### NEW Front-end JavaScript/Vue Files

**\[New Vue Components for CRUD]**

* resources/adminapp/js/cruds/Transactions/Index.vue
* resources/adminapp/js/cruds/Transactions/Create.vue
* resources/adminapp/js/cruds/Transactions/Edit.vue
* resources/adminapp/js/cruds/Transactions/Show\.vue

**\[New Vuex Store Files]**

* resources/adminapp/js/store/cruds/Transactions/index.js
* resources/adminapp/js/store/cruds/Transactions/single.js

**-----------------------------------------------------------**

### UPDATED Front-end JavaScript/Vue Files

**\[Added Menu Item on Sidebar]**

* resources/adminapp/js/pages/Layout/DashboardLayout.Vue

**\[Added Vue Routes]**

* resources/adminapp/js/routes/routes.js

**\[Added Vuex Files to the List]**

* resources/adminapp/js/store/store.js

**-----------------------------------------------------------**

For a bit deeper explanation of those files, look at this video (specifically, section "Look at Vue/Laravel Generated Code", from 8:34):

{% embed url="<https://www.youtube.com/watch?v=yDr9PZNaZvM>" %}


# Installing Downloaded Vue.js+Laravel Panel

## Installation Commands

After unarchiving the ZIP file into the folder you prepared for your project, here's a sequence of Terminal commands you need to run (short version):

```
cp .env.example .env
vi .env

composer install

php artisan key:generate

php artisan migrate --seed

npm install
npm run dev
```

We have a much more detailed guide for non-Vue Laravel version of QuickAdminPanel: [Download Code and Install on Your Web-Server](https://helpdocs.quickadminpanel.com/using-generated-code/download-code-and-install-your-web-server). Most of that guide is applicable for Vue version, but additionally, you need to run Vue-related commands:

```
npm install
npm run dev
```

## Configuring Laravel Sanctum Domains

We're using [Laravel Sanctum](https://laravel.com/docs/8.x/sanctum) for API Authentication.

So, if your URL for the project is NOT **<http://localhost>**, then add your domain into **.env** file variable **SANCTUM\_STATEFUL\_DOMAINS**&#x20;

Example:

```
SANCTUM_STATEFUL_DOMAINS=myproject.test
```

**Important**: that **myproject.test** value should NOT contain any prefixes, and should be lowercase. So not **<http://myproject.test>**, and not **MyProject.test**&#x20;

{% hint style="warning" %}
Read more about Laravel Sanctum domains in [the official Laravel documentation](https://laravel.com/docs/8.x/sanctum#configuring-your-first-party-domains).
{% endhint %}

## Launching Project

If it's all successful, you should launch the homepage and see a login screen:

![](/files/-MGwi4bo3iLcfs_zx3D-)

Default credentials:\
\- Email: **<admin@admin.com>**\
\- Pass: **password**

After login, you can browse through menu items and you should see datatables like this one:

![](/files/-MGwiCCy-KVmcsNZR6hj)


# QuickAdminPanel: Livewire+Tailwind Version

In April 2021, we released a separate QuickAdminPanel version that generates the code with Livewire and Tailwind, using the modern TALL stack.

![](/files/-MYiK1_Dj2bYxvV6jOFA)

{% hint style="info" %}
**Notice**: this version is available only for the **Yearly Plan** customers.
{% endhint %}

Here are a few screenshots of a [simple adminpanel](https://github.com/LaravelDaily/QuickAdminPanel-Livewire-Tailwind-Example), fully generated without writing a single line of code:

![Login form page](/files/-MYiHbgkI03ci-7FrcE9)

![Simple Datatable Example](/files/-MYiHen9IqrDHs2QxanT)

![Simple Form Example](/files/-MYiHipXVXOJ975u5dnG)

For the design, we're using a theme [Notus JS by Creative Tim](https://www.creative-tim.com/product/notus-js).&#x20;

## Structure of Generated Code

We believe that Laravel Livewire is really powerful for dynamic elements on the pages, so every inner page in our adminpanel is a Livewire component that you can customize however you want.

So, the main project structure is good old Laravel with Routes and Blade, and inside of each **resources/views/admin/xxxxx.blade.php** you will find a line similar to this:

```
<div class="card-body">
    @livewire('transaction.create')
</div>
```

It means that there are three separate Livewire components for each CRUD:

* Create Page;
* Edit Page;
* Index Page.

Typical code of Livewire component:

**app/Http/Livewire/Transaction/Index.php**

```
<?php

namespace App\Http\Livewire\Transaction;

use App\Http\Livewire\WithConfirmation;
use App\Http\Livewire\WithSorting;
use App\Models\Transaction;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Gate;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;
    use WithSorting;
    use WithConfirmation;

    public int $perPage;

    public array $orderable;

    public string $search = '';

    public array $selected = [];

    public array $paginationOptions;

    protected $queryString = [
        'search' => [
            'except' => '',
        ],
        'sortBy' => [
            'except' => 'id',
        ],
        'sortDirection' => [
            'except' => 'desc',
        ],
    ];

    public function getSelectedCountProperty()
    {
        return count($this->selected);
    }

    public function updatingSearch()
    {
        $this->resetPage();
    }

    public function updatingPerPage()
    {
        $this->resetPage();
    }

    public function resetSelected()
    {
        $this->selected = [];
    }

    public function mount()
    {
        $this->sortBy            = 'id';
        $this->sortDirection     = 'desc';
        $this->perPage           = 100;
        $this->paginationOptions = config('project.pagination.options');
        $this->orderable         = (new Transaction())->orderable;
    }

    public function render()
    {
        $query = Transaction::advancedFilter([
            's'               => $this->search ?: null,
            'order_column'    => $this->sortBy,
            'order_direction' => $this->sortDirection,
        ]);

        $transactions = $query->paginate($this->perPage);

        return view('livewire.transaction.index', compact('query', 'transactions', 'transactions'));
    }

    public function deleteSelected()
    {
        abort_if(Gate::denies('transaction_delete'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        Transaction::whereIn('id', $this->selected)->delete();

        $this->resetSelected();
    }

    public function delete(Transaction $transaction)
    {
        abort_if(Gate::denies('transaction_delete'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        $transaction->delete();
    }
}
```

For Datatables, we don't use any external packages, as we believe it's not needed in this case - all the pagination/filtering/sorting could happen directly in the component, using the power of some reusable Traits.

For Roles/Permissions, we use the core Laravel Gate functionality, without any external packages. You can read more about it [here](https://helpdocs.quickadminpanel.com/create-panel/roles-permissions).

You can look at the full code of this example adminpanel [in this public repository](https://github.com/LaravelDaily/QuickAdminPanel-Livewire-Tailwind-Example).


# What Files are Inside Livewire+Tailwind CRUD?

When you create a CRUD in Livewire+Tailwind QuickAdminPanel, minimum of **12 new files** are generated automatically, and **4 more files** are updated. Potentially more, if you use some advanced features/modules.

For example, if you create CRUD called **Transactions** with a few simple columns like "amount" and "transaction\_date", here's the **minimum** list of generated files.

**\[New Model]**&#x20;

* app/Models/Transaction.php

```
<?php

namespace App\Models;

use \DateTimeInterface;
use App\Support\HasAdvancedFilter;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Transaction extends Model
{
    use HasFactory;
    use HasAdvancedFilter;
    use SoftDeletes;

    public $table = 'transactions';

    public $orderable = [
        'id',
        'amount',
        'transaction_date',
    ];

    public $filterable = [
        'id',
        'amount',
        'transaction_date',
    ];

    protected $fillable = [
        'amount',
        'transaction_date',
    ];

    protected $dates = [
        'transaction_date',
        'created_at',
        'updated_at',
        'deleted_at',
    ];

    public function getTransactionDateAttribute($value)
    {
        return $value ? Carbon::parse($value)->format(config('project.date_format')) : null;
    }

    public function setTransactionDateAttribute($value)
    {
        $this->attributes['transaction_date'] = $value ? Carbon::createFromFormat(config('project.date_format'), $value)->format('Y-m-d') : null;
    }

    protected function serializeDate(DateTimeInterface $date)
    {
        return $date->format('Y-m-d H:i:s');
    }
}
```

**\[New Controller]**&#x20;

* app/Http/Controllers/Admin/TransactionController.php

```
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Transaction;
use Gate;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class TransactionController extends Controller
{
    public function index()
    {
        abort_if(Gate::denies('transaction_access'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        return view('admin.transaction.index');
    }

    public function create()
    {
        abort_if(Gate::denies('transaction_create'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        return view('admin.transaction.create');
    }

    public function edit(Transaction $transaction)
    {
        abort_if(Gate::denies('transaction_edit'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        return view('admin.transaction.edit', compact('transaction'));
    }

    public function show(Transaction $transaction)
    {
        abort_if(Gate::denies('transaction_show'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        return view('admin.transaction.show', compact('transaction'));
    }
}

```

**\[New database migration]**&#x20;

* database/migrations/2021\_04\_18\_000006\_create\_transactions\_table.php

```
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTransactionsTable extends Migration
{
    public function up()
    {
        Schema::create('transactions', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->decimal('amount', 15, 2)->nullable();
            $table->date('transaction_date')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }
}
```

{% hint style="info" %}
**Notice about migrations:** after every new or changed CRUD, we regenerate **all** migration files to make sure they are in the right order, to avoid creating foreign keys on non-existing tables. Therefore, keep in mind that you need to double-check the migration files manually, so they still work after you merge changes.
{% endhint %}

**\[New Blade views]**&#x20;

* resources/views/admin/transaction/create.blade.php

```
@extends('layouts.admin')
@section('content')

<div class="card bg-blueGray-100">
    <div class="card-header">
        <div class="card-header-container">
            <h6 class="card-title">
                {{ trans('global.create') }}
                {{ trans('cruds.transaction.title_singular') }}
            </h6>
        </div>
    </div>

    <div class="card-body">
        @livewire('transaction.create')
    </div>
</div>
@endsection
```

* resources/views/admin/transaction/edit.blade.php

```
@extends('layouts.admin')
@section('content')

<div class="card bg-blueGray-100">
    <div class="card-header">
        <div class="card-header-container">
            <h6 class="card-title">
                {{ trans('global.edit') }}
                {{ trans('cruds.transaction.title_singular') }}:
                {{ trans('cruds.transaction.fields.id') }}
                {{ $transaction->id }}
            </h6>
        </div>
    </div>

    <div class="card-body">
        @livewire('transaction.edit', [$transaction])
    </div>
</div>
@endsection
```

* resources/views/admin/transaction/index.blade.php

```
@extends('layouts.admin')
@section('content')
<div class="card bg-white">
    <div class="card-header border-b border-blueGray-200">
        <div class="card-header-container">
            <h6 class="card-title">
                {{ trans('cruds.transaction.title_singular') }}
                {{ trans('global.list') }}
            </h6>

            @can('transaction_create')
                <a class="btn btn-indigo" href="{{ route('admin.transactions.create') }}">
                    {{ trans('global.add') }} {{ trans('cruds.transaction.title_singular') }}
                </a>
            @endcan
        </div>
    </div>
    @livewire('transaction.index')

</div>
@endsection
```

* resources/views/admin/transaction/show\.blade.php

```
@extends('layouts.admin')
@section('content')

<div class="card bg-blueGray-100">
    <div class="card-header">
        <div class="card-header-container">
            <h6 class="card-title">
                {{ trans('global.view') }}
                {{ trans('cruds.transaction.title_singular') }}:
                {{ trans('cruds.transaction.fields.id') }}
                {{ $transaction->id }}
            </h6>
        </div>
    </div>

    <div class="card-body">
        <div class="pt-3">
            <table class="table table-view">
                <tbody class="bg-white">
                    <tr>
                        <th>
                            {{ trans('cruds.transaction.fields.id') }}
                        </th>
                        <td>
                            {{ $transaction->id }}
                        </td>
                    </tr>
                    <tr>
                        <th>
                            {{ trans('cruds.transaction.fields.amount') }}
                        </th>
                        <td>
                            {{ $transaction->amount }}
                        </td>
                    </tr>
                    <tr>
                        <th>
                            {{ trans('cruds.transaction.fields.transaction_date') }}
                        </th>
                        <td>
                            {{ $transaction->transaction_date }}
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
        <div class="form-group">
            <a href="{{ route('admin.transactions.index') }}" class="btn btn-secondary">
                {{ trans('global.back') }}
            </a>
        </div>
    </div>
</div>
@endsection
```

**\[New Livewire Components]**&#x20;

* app/Http/Livewire/Transaction/Create.php&#x20;

```
<?php

namespace App\Http\Livewire\Transaction;

use App\Models\Transaction;
use Livewire\Component;

class Create extends Component
{
    public Transaction $transaction;

    public function mount(Transaction $transaction)
    {
        $this->transaction = $transaction;
    }

    public function render()
    {
        return view('livewire.transaction.create');
    }

    public function submit()
    {
        $this->validate();

        $this->transaction->save();

        return redirect()->route('admin.transactions.index');
    }

    protected function rules(): array
    {
        return [
            'transaction.amount' => [
                'numeric',
                'nullable',
            ],
            'transaction.transaction_date' => [
                'nullable',
                'date_format:' . config('project.date_format'),
            ],
        ];
    }
}
```

* app/Http/Livewire/Transaction/Edit.php&#x20;

```
class Edit extends Component
{
    public Transaction $transaction;

    public function mount(Transaction $transaction)
    {
        $this->transaction = $transaction;
    }

    public function render()
    {
        return view('livewire.transaction.edit');
    }

    public function submit()
    {
        $this->validate();

        $this->transaction->save();

        return redirect()->route('admin.transactions.index');
    }

    protected function rules(): array
    {
        return [
            'transaction.amount' => [
                'numeric',
                'nullable',
            ],
            'transaction.transaction_date' => [
                'nullable',
                'date_format:' . config('project.date_format'),
            ],
        ];
    }
}
```

* app/Http/Livewire/Transaction/Index.php

```
<?php

namespace App\Http\Livewire\Transaction;

use App\Http\Livewire\WithConfirmation;
use App\Http\Livewire\WithSorting;
use App\Models\Transaction;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Gate;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;
    use WithSorting;
    use WithConfirmation;

    public int $perPage;

    public array $orderable;

    public string $search = '';

    public array $selected = [];

    public array $paginationOptions;

    protected $queryString = [
        'search' => [
            'except' => '',
        ],
        'sortBy' => [
            'except' => 'id',
        ],
        'sortDirection' => [
            'except' => 'desc',
        ],
    ];

    public function getSelectedCountProperty()
    {
        return count($this->selected);
    }

    public function updatingSearch()
    {
        $this->resetPage();
    }

    public function updatingPerPage()
    {
        $this->resetPage();
    }

    public function resetSelected()
    {
        $this->selected = [];
    }

    public function mount()
    {
        $this->sortBy            = 'id';
        $this->sortDirection     = 'desc';
        $this->perPage           = 100;
        $this->paginationOptions = config('project.pagination.options');
        $this->orderable         = (new Transaction())->orderable;
    }

    public function render()
    {
        $query = Transaction::advancedFilter([
            's'               => $this->search ?: null,
            'order_column'    => $this->sortBy,
            'order_direction' => $this->sortDirection,
        ]);

        $transactions = $query->paginate($this->perPage);

        return view('livewire.transaction.index', compact('query', 'transactions', 'transactions'));
    }

    public function deleteSelected()
    {
        abort_if(Gate::denies('transaction_delete'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        Transaction::whereIn('id', $this->selected)->delete();

        $this->resetSelected();
    }

    public function delete(Transaction $transaction)
    {
        abort_if(Gate::denies('transaction_delete'), Response::HTTP_FORBIDDEN, '403 Forbidden');

        $transaction->delete();
    }
}
```

**\[New Livewire Blade views]**&#x20;

* resources/views/livewire/transaction/create.blade.php

```
<form wire:submit.prevent="submit" class="pt-3">

    <div class="form-group {{ $errors->has('transaction.amount') ? 'invalid' : '' }}">
        <label class="form-label" for="amount">{{ trans('cruds.transaction.fields.amount') }}</label>
        <input class="form-control" type="number" name="amount" id="amount" wire:model.defer="transaction.amount" step="0.01">
        <div class="validation-message">
            {{ $errors->first('transaction.amount') }}
        </div>
        <div class="help-block">
            {{ trans('cruds.transaction.fields.amount_helper') }}
        </div>
    </div>
    <div class="form-group {{ $errors->has('transaction.transaction_date') ? 'invalid' : '' }}">
        <label class="form-label" for="transaction_date">{{ trans('cruds.transaction.fields.transaction_date') }}</label>
        <x-date-picker class="form-control" wire:model="transaction.transaction_date" id="transaction_date" name="transaction_date" picker="date" />
        <div class="validation-message">
            {{ $errors->first('transaction.transaction_date') }}
        </div>
        <div class="help-block">
            {{ trans('cruds.transaction.fields.transaction_date_helper') }}
        </div>
    </div>

    <div class="form-group">
        <button class="btn btn-indigo mr-2" type="submit">
            {{ trans('global.save') }}
        </button>
        <a href="{{ route('admin.transactions.index') }}" class="btn btn-secondary">
            {{ trans('global.cancel') }}
        </a>
    </div>
</form>
```

* resources/views/livewire/transaction/edit.blade.php

```
<form wire:submit.prevent="submit" class="pt-3">

    <div class="form-group {{ $errors->has('transaction.amount') ? 'invalid' : '' }}">
        <label class="form-label" for="amount">{{ trans('cruds.transaction.fields.amount') }}</label>
        <input class="form-control" type="number" name="amount" id="amount" wire:model.defer="transaction.amount" step="0.01">
        <div class="validation-message">
            {{ $errors->first('transaction.amount') }}
        </div>
        <div class="help-block">
            {{ trans('cruds.transaction.fields.amount_helper') }}
        </div>
    </div>
    <div class="form-group {{ $errors->has('transaction.transaction_date') ? 'invalid' : '' }}">
        <label class="form-label" for="transaction_date">{{ trans('cruds.transaction.fields.transaction_date') }}</label>
        <x-date-picker class="form-control" wire:model="transaction.transaction_date" id="transaction_date" name="transaction_date" picker="date" />
        <div class="validation-message">
            {{ $errors->first('transaction.transaction_date') }}
        </div>
        <div class="help-block">
            {{ trans('cruds.transaction.fields.transaction_date_helper') }}
        </div>
    </div>

    <div class="form-group">
        <button class="btn btn-indigo mr-2" type="submit">
            {{ trans('global.save') }}
        </button>
        <a href="{{ route('admin.transactions.index') }}" class="btn btn-secondary">
            {{ trans('global.cancel') }}
        </a>
    </div>
</form>
```

* resources/views/livewire/transaction/index.blade.php

```
<div>
    <div class="card-controls sm:flex">
        <div class="w-full sm:w-1/2">
            Per page:
            <select wire:model="perPage" class="form-select w-full sm:w-1/6">
                @foreach($paginationOptions as $value)
                    <option value="{{ $value }}">{{ $value }}</option>
                @endforeach
            </select>

            <button class="btn btn-rose ml-3 disabled:opacity-50 disabled:cursor-not-allowed" type="button" wire:click="confirm('deleteSelected')" wire:loading.attr="disabled" {{ $this->selectedCount ? '' : 'disabled' }}>
                {{ __('Delete Selected') }}
            </button>

        </div>
        <div class="w-full sm:w-1/2 sm:text-right">
            Search:
            <input type="text" wire:model.debounce.300ms="search" class="w-full sm:w-1/3 inline-block" />
        </div>
    </div>
    <div wire:loading.delay class="col-12 alert alert-info">
        Loading...
    </div>
    <table class="table table-index w-full">
        <thead>
            <tr>
                <th class="w-9">
                </th>
                <th class="w-28">
                    {{ trans('cruds.transaction.fields.id') }}
                    @include('components.table.sort', ['field' => 'id'])
                </th>
                <th>
                    {{ trans('cruds.transaction.fields.amount') }}
                    @include('components.table.sort', ['field' => 'amount'])
                </th>
                <th>
                    {{ trans('cruds.transaction.fields.transaction_date') }}
                    @include('components.table.sort', ['field' => 'transaction_date'])
                </th>
                <th>
                </th>
            </tr>
        </thead>
        <tbody>
            @forelse($transactions as $transaction)
                <tr>
                    <td>
                        <input type="checkbox" value="{{ $transaction->id }}" wire:model="selected">
                    </td>
                    <td>
                        {{ $transaction->id }}
                    </td>
                    <td>
                        {{ $transaction->amount }}
                    </td>
                    <td>
                        {{ $transaction->transaction_date }}
                    </td>
                    <td>
                        <div class="flex justify-end">
                            @can('transaction_show')
                                <a class="btn btn-sm btn-info mr-2" href="{{ route('admin.transactions.show', $transaction) }}">
                                    {{ trans('global.view') }}
                                </a>
                            @endcan
                            @can('transaction_edit')
                                <a class="btn btn-sm btn-success mr-2" href="{{ route('admin.transactions.edit', $transaction) }}">
                                    {{ trans('global.edit') }}
                                </a>
                            @endcan
                            @can('transaction_delete')
                                <button class="btn btn-sm btn-rose mr-2" type="button" wire:click="confirm('delete', {{ $transaction->id }})" wire:loading.attr="disabled">
                                    {{ trans('global.delete') }}
                                </button>
                            @endcan
                        </div>
                    </td>
                </tr>
                @empty
                <tr>
                    <td colspan="10">No entries found.</td>
                </tr>
            @endforelse
        </tbody>
    </table>

    <div class="card-body">
        <div class="pt-3">
            @if($this->selectedCount)
                <p class="text-sm leading-5">
                    <span class="font-medium">
                        {{ $this->selectedCount }}
                    </span>
                    {{ __('Entries selected') }}
                </p>
            @endif
            {{ $transactions->links() }}
        </div>
    </div>
</div>

@push('scripts')
    <script>
        Livewire.on('confirm', e => {
    if (!confirm("{{ trans('global.areYouSure') }}")) {
        return
    }
@this[e.callback](...e.argv)
})
    </script>
@endpush
```

**\[Changed main menu Blade Component]**&#x20;

* resources/views/components/sidebar.blade.php

```
<nav class="md:left-0 md:block md:fixed md:top-0 md:bottom-0 md:overflow-y-auto md:flex-row md:flex-nowrap md:overflow-hidden shadow-xl bg-white flex flex-wrap items-center justify-between relative md:w-64 z-10 py-4 px-6">
    <div class="md:flex-col md:items-stretch md:min-h-full md:flex-nowrap px-0 flex flex-wrap items-center justify-between w-full mx-auto">
        <button class="cursor-pointer text-black opacity-50 md:hidden px-3 py-1 text-xl leading-none bg-transparent rounded border border-solid border-transparent" type="button" onclick="toggleNavbar('example-collapse-sidebar')">
            <i class="fas fa-bars"></i>
        </button>
        <a class="md:block text-left md:pb-2 text-blueGray-700 mr-0 inline-block whitespace-nowrap text-sm uppercase font-bold p-4 px-0" href="{{ route('admin.home') }}">
            {{ trans('panel.site_title') }}
        </a>
        <div class="md:flex md:flex-col md:items-stretch md:opacity-100 md:relative md:mt-4 md:shadow-none shadow absolute top-0 left-0 right-0 z-40 overflow-y-auto overflow-x-hidden h-auto items-center flex-1 rounded hidden" id="example-collapse-sidebar">
            <div class="md:min-w-full md:hidden block pb-4 mb-4 border-b border-solid border-blueGray-300">
                <div class="flex flex-wrap">
                    <div class="w-6/12">
                        <a class="md:block text-left md:pb-2 text-blueGray-700 mr-0 inline-block whitespace-nowrap text-sm uppercase font-bold p-4 px-0" href="{{ route('admin.home') }}">
                            {{ trans('panel.site_title') }}
                        </a>
                    </div>
                    <div class="w-6/12 flex justify-end">
                        <button type="button" class="cursor-pointer text-black opacity-50 md:hidden px-3 py-1 text-xl leading-none bg-transparent rounded border border-solid border-transparent" onclick="toggleNavbar('example-collapse-sidebar')">
                            <i class="fas fa-times"></i>
                        </button>
                    </div>
                </div>
            </div>
            <!-- Divider -->
            <hr class="mb-6 md:min-w-full" />
            <!-- Heading -->

            <ul class="md:flex-col md:min-w-full flex flex-col list-none">
                <li class="items-center">
                    <a href="{{ route("admin.home") }}" class="{{ request()->is("admin") ? "sidebar-nav-active" : "sidebar-nav" }}">
                        <i class="fas fa-tv"></i>
                        {{ trans('global.dashboard') }}
                    </a>
                </li>
                
                @can('transaction_access')
                    <li class="items-center">
                        <a class="{{ request()->is("admin/transactions*") ? "sidebar-nav-active" : "sidebar-nav" }}" href="{{ route("admin.transactions.index") }}">
                            <i class="fa-fw c-sidebar-nav-icon fas fa-cogs">
                            </i>
                            {{ trans('cruds.transaction.title') }}
                        </a>
                    </li>
                @endcan

                @if(file_exists(app_path('Http/Controllers/Auth/ChangePasswordController.php')))
                    @can('profile_password_edit')
                        <li class="items-center">
                            <a href="{{ route("profile.password.edit") }}" class="{{ request()->is("profile/password") || request()->is("profile/password/*") ? "sidebar-nav-active" : "sidebar-nav" }}">
                                <i class="fas fa-cogs"></i>
                                {{ trans('global.change_password') }}
                            </a>
                        </li>
                    @endcan
                @endif

                <li class="items-center">
                    <a href="#" onclick="event.preventDefault(); document.getElementById('logoutform').submit();" class="sidebar-nav">
                        <i class="fa-fw fas fa-sign-out-alt"></i>
                        {{ trans('global.logout') }}
                    </a>
                </li>
            </ul>
        </div>
    </div>
</nav>
```

**\[Changed main routes]**&#x20;

* routes/web.php

```
<?php

// ...

Route::group(['prefix' => 'admin', 'as' => 'admin.', 'middleware' => ['auth']], function () {
    // ... other routes

    // Transactions
    Route::resource('transactions', TransactionController::class, ['except' => ['store', 'update', 'destroy']]);
});
```

**\[Changed Seeds for Permissions]**&#x20;

* database/seeds/PermissionsTableSeeder.php

```
<?php

namespace Database\Seeders;

use App\Models\Permission;
use Illuminate\Database\Seeder;

class PermissionsTableSeeder extends Seeder
{
    public function run()
    {
        $permissions = [
            // ... other permissions
            
            [
                'id'    => 28,
                'title' => 'transaction_create',
            ],
            [
                'id'    => 29,
                'title' => 'transaction_edit',
            ],
            [
                'id'    => 30,
                'title' => 'transaction_show',
            ],
            [
                'id'    => 31,
                'title' => 'transaction_delete',
            ],
            [
                'id'    => 32,
                'title' => 'transaction_access',
            ],
        ];

        Permission::insert($permissions);
    }
}
```

**\[Changed Translation Files for new CRUD]**&#x20;

* resources/lang/en/cruds.php

```
<?php

return [
    // ... other translations

    'transaction' => [
        'title'          => 'Transactions',
        'title_singular' => 'Transaction',
        'fields'         => [
            'id'                      => 'ID',
            'id_helper'               => ' ',
            'amount'                  => 'Amount',
            'amount_helper'           => ' ',
            'transaction_date'        => 'Transaction Date',
            'transaction_date_helper' => ' ',
            'created_at'              => 'Created at',
            'created_at_helper'       => ' ',
            'updated_at'              => 'Updated at',
            'updated_at_helper'       => ' ',
            'deleted_at'              => 'Deleted at',
            'deleted_at_helper'       => ' ',
        ],
    ],
];
```


# Installing Downloaded Livewire+Tailwind Panel

The installation of the Livewire+Tailwind version of the panel is absolutely identical to the "original" jQuery version of the generator.

[Read all the instructions here](https://helpdocs.quickadminpanel.com/using-generated-code/download-code-and-install-your-web-server).


