Invoicing app in Laravel 9 using TALL stack – part 1

In this article, we’ll build the invoicing app using Laravel 9 TALL (Tailwind, Alpine, Laravel, Livewire) stack. If you want to develop this invoicing app in more traditional way using Bootstrap and jQuery, then follow this link. Or if you want to develop the same using VILT (Vue, Inertia, Laravel, Tailwind) stack, then follow this link.

In order to continue here, you need to go through a couple of articles where we defined models, migrations and invoice template. Following are the links to those previous articles which are prerequisite before proceeding ahead in this article:

  1. Invoicing app in Laravel 9 – migrations and models
  2. Invoicing app in Laravel 9 – invoice template using dompdf

By following through the above articles, we have got database along with models and invoice template ready to roll; so, let’s continue and add Jetstream package by executing following command in project’s parent folder:

composer require laravel/jetstream:*

Then, install Jetstream with TALL based scaffolding by executing following command:

php artisan jetstream:install livewire

Execute following command to run Jetstream related migrations:

php artisan migrate

Next, create a Livewire component Customers by executing:

php artisan make:livewire Customers

The above command will create two files: first, app/Http/Livewire/Customers.php and second, resources/views/livewire/customers.blade.php.

Let’s insert following code inside app/Http/Livewire/Customers.php:

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Customer;

class Customers extends Component
{
    public $customers, $name, $email, $phone, $address, $customer_id;
    public $isOpen = false;

    public function render()
    {
        $this->customers = Customer::all();
        return view('livewire.customers');
    }

    public function create()
    {
        $this->resetForm();
        $this->openModal();
    }

    public function openModal()
    {
        $this->isOpen = true;
    }

    public function closeModal()
    {
        $this->isOpen = false;
    }

    private function resetForm(){
        $this->customer_id = '';
        $this->name = '';
        $this->email = '';
        $this->phone = '';
        $this->address = '';
    }
    
    public function store()
    {
        $this->validate([
            'name' => 'required',
            'email' => 'required',
        ]);
    
        Customer::updateOrCreate(['id' => $this->customer_id], [
            'name' => $this->name,
            'email' => $this->email,
            'phone' => $this->phone,
            'address' => $this->address,
        ]);

        session()->flash('message', $this->customer_id ? 'Customer updated!' : 'Customer created!');

        $this->closeModal();
        $this->resetForm();
    }

    public function edit($id)
    {
        $customer = Customer::findOrFail($id);
        $this->customer_id = $id;
        $this->name = $customer->name;
        $this->email = $customer->email;
        $this->phone = $customer->phone;
        $this->address = $customer->address;
    
        $this->openModal();
    }
    
    public function delete($id)
    {
        Customer::find($id)->delete();
        session()->flash('message', 'Customer deleted!');
    }    
}

Then, open resources/views/livewire/customers.blade.php and insert following code:

<x-slot name="header">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        {{ __('Customers') }}
    </h2>
</x-slot>
<div class="py-6">
    <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg px-4 py-4">
            @if (session()->has('message'))
            <div class="bg-indigo-100 border-indigo-500 text-indigo-900 shadow-md mb-4 px-4 py-2"
                role="alert">
                <div class="flex">
                    <div>
                        <p class="text-sm">{{ session('message') }}</p>
                    </div>
                </div>
            </div>
            @endif
            <x-button wire:click="create()" class="mb-4">Add Customer</x-button>
            <x-modal wire:model.defer="isOpen">
            @include('livewire.customers-form')
            </x-modal>
            <table class="table-fixed w-full">
                <thead>
                    <tr class="bg-gray-400 text-white">
                        <th class="px-4 py-2 w-20">No.</th>
                        <th class="px-4 py-2">Name</th>
                        <th class="px-4 py-2">Email</th>
                        <th class="px-4 py-2">Phone</th>
                        <th class="px-4 py-2">Address</th>
                        <th class="px-4 py-2">Action</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach($customers as $item)
                    <tr>
                        <td class="border px-4 py-2">{{ $item->id }}</td>
                        <td class="border px-4 py-2">{{ $item->name }}</td>
                        <td class="border px-4 py-2">{{ $item->email}}</td>
                        <td class="border px-4 py-2">{{ $item->phone }}</td>
                        <td class="border px-4 py-2">{{ $item->address}}</td>
                        <td class="border px-4 py-2">
                            <x-secondary-button wire:click="edit({{ $item->id }})" class="mr-2">
                                Edit
                            </x-secondary-button>
                            <x-danger-button wire:click="delete({{ $item->id }})">
                                Delete
                            </x-danger-button>
                        </td>
                    </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>
</div>

Now, create a new file customers-form.blade.php inside resources/views/livewire folder by executing:

touch resources/views/livewire/customers-form.blade.php

And then, insert following code inside resources/views/livewire/customers-form.blade.php:

<x-validation-errors class="m-4" />
<form>
    <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
        <x-input type="text" class="mb-4 w-full" placeholder="Name" wire:model="name" />
        <x-input type="text" class="mb-4 w-full" placeholder="Email" wire:model="email" />
        <x-input type="text" class="mb-4 w-full" placeholder="Phone" wire:model="phone" />
        <x-input type="text" class="mb-4 w-full" placeholder="Address" wire:model="address" />
    </div>
    <div class="ml-6 mb-4">
        <x-button wire:click.prevent="store()" type="button" class="mr-2">Save</x-button>
        <x-secondary-button wire:click="closeModal()" type="button">Cancel</x-secondary-button>
    </div>
</form>

Similarly, repeat above steps for Item model. So, create Livewire component Items by executing:

php artisan make:livewire Items

The above command will create two files: first, app/Http/Livewire/Items.php and second, resources/views/livewire/items.blade.php.

Let’s insert following code inside app/Http/Livewire/Items.php:

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Item;

class Items extends Component
{
    public $items, $name, $unit, $description, $sale_rate, $purchase_rate, $quantity, $item_id;
    public $isOpen = false;

    public function render()
    {
        $this->items = Item::all();
        return view('livewire.items');
    }

    public function create()
    {
        $this->resetForm();
        $this->openModal();
    }

    public function openModal()
    {
        $this->isOpen = true;
    }

    public function closeModal()
    {
        $this->isOpen = false;
    }

    private function resetForm(){
        $this->item_id = '';
        $this->name = '';
        $this->unit = '';
        $this->description = '';
        $this->sale_rate = '';
        $this->purchase_rate = '';
        $this->quantity = '';
    }
    
    public function store()
    {
        $this->validate([
            'name' => 'required',
            'unit' => 'required',
            'description' => 'required',
            'sale_rate' => 'required',
        ]);
    
        Item::updateOrCreate(['id' => $this->item_id], [
            'name' => $this->name,
            'unit' => $this->unit,
            'description' => $this->description,
            'sale_rate' => $this->sale_rate,
            'purchase_rate' => $this->purchase_rate,
            'quantity' => $this->quantity,
        ]);

        session()->flash('message', $this->item_id ? 'Item updated!' : 'Item created!');

        $this->closeModal();
        $this->resetForm();
    }

    public function edit($id)
    {
        $item = Item::findOrFail($id);
        $this->item_id = $id;
        $this->name = $item->name;
        $this->unit = $item->unit;
        $this->description = $item->description;
        $this->sale_rate = $item->sale_rate;
        $this->purchase_rate = $item->purchase_rate;
        $this->quantity = $item->quantity;
    
        $this->openModal();
    }
    
    public function delete($id)
    {
        Item::find($id)->delete();
        session()->flash('message', 'Item deleted!');
    }    
}

Then, open resources/views/livewire/items.blade.php and insert following code:

<x-slot name="header">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        {{ __('Items') }}
    </h2>
</x-slot>
<div class="py-6">
    <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg px-4 py-4">
            @if (session()->has('message'))
            <div class="bg-indigo-100 border-indigo-500 text-indigo-900 shadow-md mb-4 px-4 py-2"
                role="alert">
                <div class="flex">
                    <div>
                        <p class="text-sm">{{ session('message') }}</p>
                    </div>
                </div>
            </div>
            @endif
            <x-button wire:click="create()" class="mb-4">Add Item</x-button>
            <x-modal wire:model.defer="isOpen">
            @include('livewire.items-form')
            </x-modal>
            <table class="table-fixed w-full">
                <thead>
                    <tr class="bg-gray-400 text-white">
                        <th class="px-4 py-2 w-10">No.</th>
                        <th class="px-4 py-2">Name</th>
                        <th class="px-4 py-2 w-20">Unit</th>
                        <th class="px-4 py-2">Description</th>
                        <th class="px-4 py-2 w-24">Sale Price</th>
                        <th class="px-4 py-2 w-24">Purchase Price</th>
                        <th class="px-4 py-2 w-24">Quantity</th>
                        <th class="px-4 py-2">Action</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach($items as $item)
                    <tr>
                        <td class="border px-4 py-2">{{ $item->id }}</td>
                        <td class="border px-4 py-2">{{ $item->name }}</td>
                        <td class="border px-4 py-2">{{ $item->unit}}</td>
                        <td class="border px-4 py-2">{{ $item->description }}</td>
                        <td class="border px-4 py-2">{{ $item->sale_rate}}</td>
                        <td class="border px-4 py-2">{{ $item->purchase_rate}}</td>
                        <td class="border px-4 py-2">{{ $item->quantity }}</td>
                        <td class="border px-4 py-2">
                            <x-secondary-button wire:click="edit({{ $item->id }})" class="mr-2">
                                Edit
                            </x-secondary-button>
                            <x-danger-button wire:click="delete({{ $item->id }})">
                                Delete
                            </x-danger-button>
                        </td>
                    </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>
</div>

Now, create a new file items-form.blade.php inside resources/views/livewire folder by executing:

touch resources/views/livewire/items-form.blade.php

And then, insert following code inside resources/views/livewire/items-form.blade.php:

<x-validation-errors class="m-4" />
<form>
    <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
        <x-input type="text" class="mb-4 w-full" placeholder="Name" wire:model="name" />
        <x-input type="text" class="mb-4 w-full" placeholder="Unit" wire:model="unit" />
        <x-input type="text" class="mb-4 w-full" placeholder="Description" wire:model="description" />
        <x-input type="text" class="mb-4 w-full" placeholder="Sale Price" wire:model="sale_rate" />
        <x-input type="text" class="mb-4 w-full" placeholder="Purchase Price" wire:model="purchase_rate" />
        <x-input type="text" class="mb-4 w-full" placeholder="Quantity" wire:model="quantity" />
    </div>
    <div class="ml-6 mb-4">
        <x-button wire:click.prevent="store()" type="button" class="mr-2">Save</x-button>
        <x-secondary-button wire:click="closeModal()" type="button">Cancel</x-secondary-button>
    </div>
</form>

Finally, open the routes/web.php and amend it as follows:

//...
use App\Http\Livewire\Customers;
use App\Http\Livewire\Items;
//...
Route::get('customers', Customers::class)->middleware('auth')->name('customers');
Route::get('items', Items::class)->middleware('auth')->name('items');

Compile client side assets (JS and CSS files) by executing following command:

npm run build

You may run PHP dev server by executing following command in order to check what you have accomplished so far:

php artisan serv

Enter the following URL in your browser to access Items Livewire component (don’t forget to create a user login first):

localhost:8000/items

OK, we’re done with Part 1; we’ll continue in Part 2, where the real fun begins…

1 thought on “Invoicing app in Laravel 9 using TALL stack – part 1”

Leave a Comment