The Basics Of Using Leaflet.js With Open Street Maps
February 21, 2022How And When To Ask Clients What Their Budget Is
March 7, 2022When creating projects for clients or working on personal that consist of a huge amount of data, it can be difficult to properly keep track of the data schedules.
There are examples such as FullCalendar Integration for Laravel Livewire where you can add your own events. However, there are some scenarios where the client only wants to see the calendar and not add any events.
In this tutorial, we will be going through the basic code required to only see events on the calendar based on the data stored in your database.
Requirements for this tutorial:
- Laravel 7 / 8
- Livewire
Index
1. Defining The Relationships:
For the data to be fully accurate, you will need to ensure that your database tables and Eloquent relationships are properly implemented and defined. Of course, each table and relationship will vary depending on the project you wish to implement the calendar.
For this tutorial, I will provide the following example to give the gist on what you will need to do to retrieve data from your tables via the relationships:
main_regional_offices (DB)
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateMainRegionalOfficesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('main_regional_offices', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('address_physical', 100)->nullable()->default(null); $table->double('gps_latitude', 10, 7)->nullable()->default(null); $table->double('gps_longitude', 10, 7)->nullable()->default(null); $table->enum('region', [ 'Auvergne-Rhône-Alpes', 'Bourgogne-Franche-Comté', 'Brittany', 'Centre-Val de Loire', 'Corsica', 'Grand Est', 'Paris Region', 'Normandie', 'Nouvelle-Aquitaine', 'Occitanie', 'Pays de la Loire', 'Provence Alpes Côte d’Azur' ])->nullable(true); $table->integer('country_area_id')->unsigned()->nullable()->default(null); $table->foreign('country_area_id')->references('id')->on('options_country_areas'); $table->integer('country_area_region_id')->unsigned()->nullable()->default(null); $table->foreign('country_area_region_id')->references('id')->on('options_country_area_regions'); $table->softDeletes(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('main_regional_offices'); } }
MainRegionalOffices (Model)
<?php namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use App\OfficeBranches; use Carbon\Carbon; class MainRegionalOffices extends Model { protected $fillable = [ 'name', 'gps_latitude', 'gps_longitude', 'address_physical', 'landmarks', 'region', 'country_area_id', 'country_area_region_id', ]; /* * Enable soft deletes for this model. * * @var array */ use SoftDeletes; protected $dates = ['deleted_at']; public function office_branches() { return $this->hasOne(\App\OfficeBranches::class, 'country_area_id', 'country_area_id'); } public static function getRegionalOfficeForSelect($main_regional_offices = null) { if (! $main_regional_offices) { $main_regional_offices = self::orderBy('name')->get(); } $options = []; foreach ($main_regional_offices as $main_regional_office) { $options[$main_regional_office->id] = $main_regional_office ->name; } return $options; } }
office_branches (DB)
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateOfficeBranchesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('office_branches', function (Blueprint $table) { $table->id(); $table->string('name'); $table->double('gps_latitude', 10, 7)->nullable()->default(null); $table->double('gps_longitude', 10, 7)->nullable()->default(null); $table->enum('region', [ 'Auvergne-Rhône-Alpes', 'Bourgogne-Franche-Comté', 'Brittany', 'Centre-Val de Loire', 'Corsica', 'Grand Est', 'Paris Region', 'Normandie', 'Nouvelle-Aquitaine', 'Occitanie', 'Pays de la Loire', 'Provence Alpes Côte d’Azur' ])->nullable(true); $table->string('address_physical', 100)->nullable()->default(null); $table->integer('country_area_id')->unsigned()->nullable()->default(null); $table->foreign('country_area_id')->references('id')->on('options_country_areas'); $table->integer('country_area_region_id')->unsigned()->nullable()->default(null); $table->foreign('country_area_region_id')->references('id')->on('options_country_area_regions'); $table->unsignedBigInteger('main_regional_office_id')->nullable(); $table->foreign('main_regional_office_id')->references('id')->on(' main_regional_offices'); $table->softDeletes(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('office_branches'); } }
OfficeBranches (Model)
<?php namespace App; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class OfficeBranches extends Model { /* * Enable soft deletes for this model. * * @var array */ use SoftDeletes; protected $dates = ['deleted_at']; protected $fillable = [ 'name', 'gps_latitude', 'gps_longitude', 'country_area_id', 'address_physical', 'region', 'country_area_region_id', 'main_regional_office_id', ]; public function product_delivery_schedule() { return $this->hasMany(\App\ProductDeliverySchedule::class, 'branch_office_id', 'id'); } }
product_delivery_schedules (DB)
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateProductDeliverySchedulesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('product_delivery_schedules', function (Blueprint $table) { $table->id(); $table->bigInteger('branch_office_id')->unsigned(); $table->string('year_month'); $table->enum('date_status',['date_unallocated', 'date_suggested', 'date_suggested_confirmed', 'date_allocated']); $table->enum('delivery_status',['date_allocation', 'in_progress', 'completed', 'failed']); $table->date('delivery_date')->nullable(); $table->time('delivery_time')->nullable(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('product_delivery_schedules'); } }
ProductDeliverySchedules (Model)
<?php namespace App; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; class ProductDeliverySchedules extends Model { public $fillable = [ 'branch_office_id', 'year_month', 'date_status', 'delivery_status', 'delivery_date', 'delivery_time', ]; public function office_branches() { return $this->hasOne(\App\OfficeBranches::class, 'id', 'branch_office_id'); } public function scopeIsDeliveryStatus($query, $delivery_status) { if (!$delivery_status) { return $query; } return $query->where('delivery_status', $delivery_status); } public function scopeHasDeliveryDate($query, $delivery_date) { if (!$delivery_date) { return $query; } return $query->whereDate('delivery_date', $delivery_date); }
By following the basic guidelines of the examples above, we have created the first table, which lists the company’s main offices in 13 French administrative regions. This table will be used to indicate which office branches belong to the regional office and the model will establish the relationship between the tables.
The second table has a similar purpose as the first table, but it’s id will be used in the third table, which will list product deliveries that are linked to the office_branches table.
2. Implementing Livewire Front-End
Now that we’ve implemented the necessary relationships, we can use the following command to create the livewire components:
php artisan make:livewire calendar.index
This command will create the Livewire front-end and back-end files, which are stored in the Calendar subfolder within the main Livewire folder.
After the command has been executed, you will need to create a basic Laravel screen, which will include the creation of a .blade file, the controller and the route on the web file. When you have finished implementing the basic screen, you will need to add the following line to your .blade file:
index.blade (views\private\…)
@extends('private.general-layout') @section('body') <livewire:office.office-calendar-view /> @livewireScripts @stack('scripts') @stop
By adding this code, you will be able to access the newly created livewire component. For this example, we will not be adding any backend code to the controller. For this implementation, the controller will only be used to access the .blade file.
For the following code, we will be implementing we will be implementing the dropdown, the calendar and JavaScript code on the livewire.blade:
office-calendar-view.blade (views\livewire\...)
... <div> <div class="row"> <h2 class="clc-color-black">Office Calendar</h2> @include('private.common.notices') <div class="row"> <div class="large-4 columns" id='external-events'> <label for="Office_select">Select Office: <select wire:model="main_offices" wire:change="officeSelectionUpdate" id="selectOffice"> <option value="">Choose Office</option> @foreach ($list_offices as $office) <option value="{{ $office->id }}">{{ $office->name }}</option> @endforeach </select> </label> </div> </div> <!-- CALENDAR --> <!-- DO NOT REMOVE wire:ignore >> Removing it will result in a CSS bug --> <div id='calendar-container' class="bottom-screen-spacing" wire:ignore> <div id='calendar'></div> </div> </div> </div> @push('scripts') <script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.3.1/main.min.js'> { let data = e.detail; console.log("updated event", e.detail); UpdateCalendar(data); }); function UpdateCalendar(data) { var Calendar = FullCalendar.Calendar; var containerEl = document.getElementById('external-events'); var calendarEl = document.getElementById('calendar'); var data_array = JSON.stringify(data) // initialize the calendar // ----------------------------------------------------------------- var calendar = new Calendar(calendarEl, { headerToolbar: { left: 'title' , center: '' , right: '' , } , buttonText: { today: 'Today' } , editable: false , displayEventTime: false , droppable: false, // this allows things to be dropped onto the calendar // Following code is used to ensure that events are displaying on all days // which is maintained with startRecur (When recurrences of this event start. Something that will parse into a Date. If not specified, recurrences will extend infinitely into the past.) // Event title will display the current value defined in livewire controller, where it should update the value // If office does not have any value based on the database, the default value will be 0 events: JSON.parse(data_array), loading: function(isLoading) { if (!isLoading) { // Reset custom events this.getEvents().forEach(function(e) { if (e.source === null) { e.remove(); } }); } } }); calendar.addEventSource({ url: '/calendar/events' , extraParams: function() { return { name: @this.name }; } }); console.log(data); calendar.render(); @this.on(`refreshCalendar`, () => { calendar.refetchEvents() }); } document.addEventListener('livewire:load', function() { var data = @json($events); UpdateCalendar(data); }); </script> <link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.3.1/main.min.css' rel='stylesheet' /> <style> .demo-topbar+#external-events { /* will get stripped out */ top: 60px; } #external-events .fc-event { cursor: move; margin: 3px 0; } #calendar-container { position: relative; z-index: 1; margin-left: 200px; } #calendar { max-width: 1100px; margin: 20px auto; } </style> @endpush
The dropdown displays all of the offices from the main_regional_offices table. By using this dropdown, the user will be able to select the specified main office, which will change the contents of the calendar based on the branch office linked to the main regional office.
Within the script tag, there are two major functions responsible for the calendar’s display. $(window).on('data-updated') is used to constantly update the calendar screen to be responsive when the user makes a decision on the dropdown. The UpdateCalendar function is used to display the calendar and the data on the from the livewire controller. Further details will be provided in the next section on how the data is being passed from livewire controller.
3. Implementing Backend of Livewire
Now that we have implemented the front-end for the calendar screen, we can now finally start working on the backend for the calendar to properly work.
In the following example, we will add the necessary functions that will retrieve the necessary data based on the dropdown selection and refresh the calendar so that the data can registered in the JavaScript functions mentioned above:
OfficeCalendarView
<?php namespace App\Http\Livewire\Warehouse; use App\OfficeBranches; use App\User; use Carbon\Carbon; use Carbon\CarbonPeriod; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Log; use Livewire\Component; class OfficeCalendarView extends Component { public $main_offices = ""; public $offices; public $list_offices; // Following code is used as default values public $events; public function mount() { $this->list_offices = MainRegionalOffices::all(); $this->officeSelectionUpdate(); } public function officeSelectionUpdate() { // If statement checks the selected dropdown value - If main_offices is null, all offices will be displayed // If main_office is not null, it will display the specified office with the office id value on the dropdown option if (empty($this->main_offices)) { $this->offices = MainRegionalOffices::whereNotNull('country_area_id')->get(); } else { $this->offices = MainRegionalOffices::where('id', $this->main_offices)->get(); } $periodDates = collect([]); $office_branches = NULL; // Following code plucks all ids of different office tables and places them in a collection array $office_branches = OfficeBranches::whereIn('main_regional_office_id', $this->offices->pluck('id'))->get(); // $period is used to display the current month of the calendar $period = CarbonPeriod::create(Carbon::now()->startOfMonth(), Carbon::now()->endOfMonth())->toArray(); // Loops over dates in period // checks all slots in current month foreach ($period as $date) { $total_branch_deliveries = 0; if ($office_branches) { foreach ($office_branches as $office_branch) { $total_branch_deliveries = $total_branch_deliveries + count($office_branch->product_delivery_schedule()->hasDeliveryDate($date->toDateString())->isDeliveryStatus('in_progress')->get()); } } // All slot data counted in foreach loops above are being added to the collection array // where the data will be transfered to the .blade file in javascript, with the help of dispatchBrowserEvent and json_encode $periodDates->add([ 'start' => $date->toDateString(), 'title' => $total_branch_deliveries, 'textColor' => '#318CE7', ]); } $this->events = $periodDates; $this->dispatchBrowserEvent('data-updated', $this->events); //Log::info('Event: ' . $this->events); return json_encode($this->events); } public function render() { $user = \Auth::user(); $read_only = false; $viewData = [ 'read_only' => $read_only, 'user' => $user, ]; return view('livewire.office.office-calendar-view', $viewData); } }
In the code above, we have implemented two functions – Mount and officeSelectionUpdate.
The mount function is used to load in the dropdown’s option selections and load in the officeSelectionUpdate function. Since no selection has been made on the dropdown, it will display all office branches linked to the main regional offices.
The officeSelectionUpdate function is used to retrieve the necessary data linked to a specific office branch. Within the first lines of the function, there is an if statement that determines which main office has been selected. If a user has selected a main office, it will filter the calendar to only display data linked to the selected office.
The code will ten proceed to pluck the ids of the main offices from the main_regional_office_id column (located on the office_branches table) and use the accumulated ids in a foreach loop.
This foreach loop uses the $period variable, which determines the number of days in the current month. The $total_branch_deliveries variable is used as a default value (0). Within this foreach loop, another foreach loop is added to count the number deliveries scheduled for a specific day.
Since we have established the relationship between the office_branches and product_delivery_schedules table, we are able to use scopes to narrow down the search query. If there is a scheduled delivery for the date, it will be added to the $total_branch_deliveries variable.
When $total_branch_deliveries has received it’s value, it will be added to a collection array that we named $periodDates. When the foreach loop has finished, all of the values in the array will be added to $this->events, which will be dispatched with dispatchBrowserEvent().
All of the data will be transferred to the JavaScript code on the livewire.blade screen and will be processed with the $(window).on('data-updated') function, which ensures that data is smoothly displayed as mentioned previously.
Finito! You have yourself a working calendar that only displays data.
I hope that this tutorial has been helpful in your project.