Invoicing app in Laravel 9 using TALL stack – part 2

We continue from Part 1 where we created Livewire components for Customer and Item models; now, let’s move onto Invoice model and get the real stuff done. If you happened to land here randomly, then first go through the following articles chronologically since these are the prerequisite before you proceed here any further:

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

Having followed the steps given in the above articles, we’re ready to build the actual invoicing functionality; so, let’s execute following command in parent (app) folder to create Invoices Livewire component:

php artisan make:livewire Invoices

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

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

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Invoice;
use App\Models\Customer;
use App\Models\Item;
use App\Models\InvoiceItem;
use Illuminate\Support\Facades\DB;

class Invoices extends Component
{
    public $invoices, $items, $customers; 
    public $customer_id, $invoice_date, $description, $invoice_id;
    public $isOpen = false;
    public $rows = [];

    protected $rules = [
        'customer_id' => 'required',
        'invoice_date' => 'required',
        'rows.*.item_id' => 'required',
        'rows.*.quantity' => 'required',
    ];

    public function render()
    {

        $this->invoices = Invoice::all()
		->map(function ($item) {
			$item->customer = $item->customer->name;
			return $item;
		});

        $this->customers = Customer::all('id','name');
        $this->items = Item::all('id','name');
        return view('livewire.invoices');
    }

    public function create()
    {
        $this->resetForm();
        array_push($this->rows ,['item_id'=>'', 'quantity'=>'', 'rate'=>'', 'amount'=>'']);
        $this->openModal();
    }

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

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

    private function resetForm(){
        $this->invoice_id = '';
        $this->customer_id = '';
        $this->invoice_date = '';
        $this->description = '';
        $this->rows = [];
    }
    
    public function store()
    {
        $this->validate();
    
        DB::transaction(function () {
            $invoice = Invoice::create([
                'customer_id' => $this->customer_id,
                'invoice_number' => $this->createRef(),
                'invoice_date' => $this->invoice_date,
                'amount' => 0,
                'description' => $this->description,
            ]);

            foreach($this->rows as $row){
                InvoiceItem::create([
                    'invoice_id' => $invoice->id,
                    'item_id' => $row['item_id'],
                    'quantity' => $row['quantity'],
                    'rate' => $row['rate'],
                    'amount' => $row['quantity'] * $row['rate'],
                ]);
            }

            $invoice->amount = $invoice->invoiceItems->sum('amount');
            $invoice->save();

        });
        
        session()->flash('message', 'Invoice created!');
        $this->closeModal();
        $this->resetForm();
    }

    public function delete($id)
    {
        $invoice = Invoice::find($id);
        DB::transaction(function () use ($invoice) {
            foreach ($invoice->invoiceItems as $item) {
                $item->delete();
            }
            $invoice->delete();
        });

        session()->flash('message', 'Invoice deleted!');
    }

    public function add()
    {
        array_push($this->rows ,['item_id'=>'', 'quantity'=>'', 'rate'=>'', 'amount'=>'']);
    }
 
    public function remove($key)
    {
        unset($this->rows[$key]);
    }

    public function rate($value, $key)
    {
        $rate = Item::find($value);
        $this->rows[$key]['rate'] = $rate->sale_rate;
        $this->rows[$key]['amount'] = $this->rows[$key]['quantity']? ($this->rows[$key]['quantity'] * $this->rows[$key]['rate']) : null;   
    }

    public function quantity($value, $key)
    {
        $this->rows[$key]['quantity'] = $value;
        $this->rows[$key]['amount'] = $this->rows[$key]['rate']? ($this->rows[$key]['quantity'] * $this->rows[$key]['rate']) : null;   
    }

    private function createRef()
    {
        $offset = 5 * 60 * 60;
        $timeNdate = gmdate("d-m-Y:H:i", (time() + $offset));

        $inv = "";
        $last = Invoice::latest()->first();
        $expNum = [];
        if ($last)
        {
            $expNum = explode('/', $last->invoice_number);
        }
        $dateInfo = date_parse_from_format('d-m-Y:H:i', $timeNdate);

        if (!$last) {
            $inv = 'CO/INV/' . $dateInfo['year'] . '/' . $dateInfo['month'] . '/1';
        } else {
            $inv = 'CO/INV/' . $dateInfo['year'] . '/' . $dateInfo['month'] . '/' . ($expNum[4] + 1);
        }
        return $inv;
    }
}

Let’s analyze the above code a bit. By employing Livewire, we’ve majorly shifted the handling of interactivity of UI towards backend. For example, we’ve defined $isOpen and $rows[] properties at the backend side of component which will trigger the behavior of frontend elements. $isOpen is data bound with <x-modal> element in the frontend side using wire-model directive – whenever $isOpen gets the value true, the input form will be included to the render. $rows[] holds the arrays of data representing each item line within invoice. Again, we’ve synchronized $rows[] using wire-model with form’s input elements and since wire-model supports dot notation, nested data inside arrays can be assessed easily.

Besides properties, we’ve defined few public functions which handle the events being triggered at frontend side such as openModal(), closeModal(), add(), remove(). We’ve also defined a couple of operational functions i.e. rate() and quantity() in order to update values and totals; these are triggered whenever there’s a change in item select dropdown or quantity input field respectively. All these interactivity related functions are linked through wire:click or wire:change front-end side directives.

In the store() function, we’ve utilized DB::transaction() which makes sure that all the code within the DB::transaction() function gets executed in its entirety. This functionality is crucial when developing a financial app because on a number of occasions we have to make sure that all the entries related to a transaction are completely inserted / updated / deleted before moving ahead; if any of those entries are not entered due to some error, then we have to roll back the transaction in its entirety. Therefore, by employing DB::transaction() in the above code we have made sure that all the invoice items are properly inserted into the database along with invoice entry.

We’ve also created a separate private function createRef() for the purpose of generating invoice reference for each new invoice based on current year, month and the last invoice’s reference number.

Next, open resources/views/livewire/invoices.blade.php and insert following code:

<x-slot name="header">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        {{ __('Invoices') }}
    </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 Invoice</x-button>
            <x-modal wire:model.defer="isOpen">
            @include('livewire.invoices-form')
            </x-modal>
            <table class="table-fixed w-full">
                <thead>
                    <tr class="bg-gray-400 text-white">
                        <th class="px-4 py-2 w-12">Ref</th>
                        <th class="px-4 py-2 w-8">Inv. date</th>
                        <th class="px-4 py-2 w-10">Customer</th>
                        <th class="px-4 py-2 w-8">Amount</th>
                        <th class="px-4 py-2 w-20">Description</th>
                        <th class="px-4 py-2 w-20">Actions</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach($invoices as $item)
                    <tr>
                        <td class="border px-2 py-1">{{ $item->invoice_number }}</td>
                        <td class="border px-2 py-1">{{ $item->invoice_date}}</td>
                        <td class="border px-2 py-1">{{ $item->customer }}</td>
                        <td class="border px-2 py-1">{{ $item->amount }}</td>
                        <td class="border px-2 py-1">{{ $item->description}}</td>
                        <td class="border px-2 py-1">
                            <a class="px-3 py-2 mr-4 rounded-md text-white bg-gray-500 hover:bg-gray-600" href="{{ route('pdf', $item->id) }}" target="_blank">Generate PDF</a>
                            <x-danger-button wire:click="delete({{ $item->id }})">
                                Delete
                            </x-danger-button>
                        </td>
                    </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>
</div>

Then, create a file invoices-form.blade.php inside resources/views/livewire folder by executing:

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

And then, insert following code inside resources/views/livewire/invoices-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">
        <select wire:model="customer_id" class="mb-4 w-full rounded-md">
            <option value="">Select customer:</option>
            @foreach($customers as $customer)
            <option value="{{$customer->id}}">
                {{ $customer->name }}
            </option>
            @endforeach
        </select>
        <x-input type="text" class="mb-4 w-full" placeholder="Date (YYYY-MM-DD)" wire:model="invoice_date" />
        <x-input type="text" class="mb-4 w-full" placeholder="Description" wire:model="description" />
    </div>

    @foreach($rows as $key => $value)
    <div class="grid grid-cols-12 ml-6">
        <div class="col-span-3 m-1">
            <select wire:model="rows.{{ $key }}.item_id" wire:change="rate($event.target.value, {{$key}} )" class="w-full rounded-md mb-2">
                <option value="">Select item:</option>
                @foreach($items as $item)
                <option value="{{$item->id}}">
                    {{ $item->name }}
                </option>
                @endforeach
            </select>
        </div>
        <div class="col-span-2 m-1">
            <x-input type="text" class="w-full" wire:model="rows.{{ $key }}.quantity" wire:change="quantity($event.target.value, {{$key}} )" placeholder="Quantity"/>
        </div>
        <div class="col-span-2 m-1">
            <x-input type="text" class="w-full" wire:model="rows.{{ $key }}.rate" placeholder="Rate" readonly/>
        </div>
        <div class="col-span-3 m-1">
            <x-input type="text" class="w-full" wire:model="rows.{{ $key }}.amount" placeholder="Amount" readonly/>
        </div>
        <div class="col-span-2 m-1" x-show="{{$key}} > 0">
            <x-danger-button wire:click.prevent="remove({{$key}})" class="">X</x-danger-button>
        </div>
    </div>
    @endforeach
    <div class="p-4 ml-2">
        <x-secondary-button wire:click.prevent="add">Add Row</x-secondary-button>
    </div>

    <div class="ml-6 mb-4">
        <x-button wire:click.prevent="store" class="mr-2">Save</x-button>
        <x-secondary-button wire:click="closeModal">Cancel</x-secondary-button>
    </div>
</form>

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

//...
use App\Http\Livewire\Invoices;
//...
Route::get('invoices', Invoices::class)->middleware('auth')->name('invoices');

We now wrap up our invoicing app and as a final step we provide a navigation menu to our main routes that we’ve created so far; so, let’s open resources/views/navigation-menu.blade.php and insert navigation links therein right after the comments <!– Navigation Links –> as given below:

                <!-- Navigation Links -->
                <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
                    <x-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
                        {{ __('Dashboard') }}
                    </x-nav-link>
                    <x-nav-link href="{{ route('customers') }}" :active="request()->routeIs('customers')">
                        {{ __('Customers') }}
                    </x-nav-link>
                    <x-nav-link href="{{ route('items') }}" :active="request()->routeIs('items')">
                        {{ __('Items') }}
                    </x-nav-link>
                    <x-nav-link href="{{ route('invoices') }}" :active="request()->routeIs('invoices')">
                        {{ __('Invoices') }}
                    </x-nav-link>
                </div>

For smaller screens, you should also change the Hamburger menu in similar fashion which is down below in resources/views/navigation-menu.blade.php right where it’s commented <!– Responsive Navigation Menu –>

As usual, compile client side assets (JS and CSS files) by executing following command:

npm run build

Run PHP dev server by executing:

php artisan serv

And then, enter the following URL in your browser to access Invoices Livewire component (don’t forget to create a user login first, if not already done):

localhost:8000/invoices

Create some invoices, generate PDFs and chill – now you have a decent boilerplate to create most of the business solutions using Laravel 9 TALL stack. But remember, there are a lot of things missing here such as datatable with capabilities to search, filter, and paginate etc.; the purpose here is to demonstrate the core of a business app as we’d normally develop using this stack (Laravel 9 TALL stack).

Caution: This article is for educational purposes only; technologies like Livewire should cautiously be used only in scenarios where they are appropriate enough to use, such as fetching data from back-end based on search query or filling in simple forms. Obviously, front-end tasks such as hiding / unhiding a menu bar should not be dependent upon a variable defined in back-end code – this approach will result in sluggish UI experience.

Leave a Comment