Backend DevelopmentFrontend DevelopmentVueJs Tutorials

Building Ecommerce Website With Lumen Laravel And Nuxtjs 24: Finishing

Building Ecommerce Website With PHP Lumen Laravel And Nuxtjs

This is the last part of the series of building e-commerce website using Lumen and Nuxtjs, and we will conclude in this part with some points that we did not talk about in the previous lessons.

 

 

 

We can say that i completed our series but there are some points that i will finish in this part which are:

  • Modifying the dashboard homepage sections so that to display real data in these sections.
  • Modifying the dashboard header to update the user account link to point to the correct page.
  • Update the ProductsController::destroy() method to check if the product have orders then reject this delete.

I will do the required updates in Lumen first then move to the Nuxt project later.

Open the Lumen project, we will create a new controller class for the dashboard that have only one method index(), this index() method will return a report with the total members, total earnings and so on.

Create app/Http/Controllers/DashboardController.php

<?php

namespace App\Http\Controllers;


use App\Models\Order;
use App\Models\User;
use Carbon\Carbon;

class DashboardController extends Controller
{
    public function __construct()
    {
        $this->middleware('super_admin_check:index');
    }

    public function index()
    {
        // all users count
        $totalMembers = User::where("is_super_admin", 0)->count();

        // total items sold
        $totalItemsSold = 0;
        $orders = Order::with("orderDetails")->where('status', 'success')->get();

        foreach ($orders as $order) {
            $totalItemsSold += $order->countOrderItems();
        }

        // total items this week
        $totalItemsSoldThisWeek = 0;
        $orders = Order::with("orderDetails")->where('status', 'success')
            ->whereBetween('updated_at', [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()])->get();
        foreach ($orders as $order) {
            $totalItemsSoldThisWeek += $order->countOrderItems();
        }

        // total earnings
        $totalEarnings = 0;
        $orders = Order::where('status', 'success')->get();
        foreach ($orders as $order) {
            $totalEarnings += $order->total_price;
        }

        return response()->json([
           "totalMembers" => $totalMembers,
           "totalItemsSold" => $totalItemsSold,
           "totalItemsThisWeek" => $totalItemsSoldThisWeek,
           "totalEarnings" => number_format($totalEarnings, 2)
        ]);
    }
}

The index() method is straightforward , it return a report of the total members, total items sold, total items in current week and the total earnings ever. We will display these data in the dashboard homepage.

To fetch the total items sold i query the Order model to get all orders with status=success and then iterate through all orders and using another method on the order model Order::countOrderItems() to get all items per order, we will add this method below.

In the same way to get all items sold per week i used the same query in addition i added another condition using laravel whereBetween(‘updated_at’) to get orders between start of week and end of week.

To get all earnings is a matter of iterating over all successful orders and incrementing the total_price attribute.

 

Now open the app/Models/Order.php model and add this method in the end of the class:

public function countOrderItems()
    {
        $amount = 0;

        foreach ($this->orderDetails as $detail) {
            $amount += $detail->amount;
        }

        return $amount;
    }

 

Modifying The Products Controller

Open app/Http/Controllers/ProductsController.php and update the destroy() method like so:

public function destroy($id)
    {
        try {
            $product = Product::with('gallery')->findOrFail($id);

            // check if this product have orders attached to it then reject delete
            if($product->orderDetails->count() > 0) {
                throw new \Exception("Can't delete the product as there already orders attached to it");
            }

            foreach ($product->gallery as $gallery) {
                if(!empty($gallery->image)) {
                    foreach ($gallery->image_url as $dir => $url) {
                        $this->deleteFile(base_path('public').'/uploads/' . $gallery->product_id . '/' . $dir . '/' . $gallery->image);
                    }

                    $this->deleteFile(base_path('public').'/uploads/' . $gallery->product_id . '/' . $gallery->image);
                }
            }

            $product->delete();

            return response()->json(['success' => 1, 'message' => 'Deleted successfully'], 200);
        } catch (\Exception $e) {
            return response()->json(['success' => 0, 'message' => $e->getMessage()], 500);
        }
    }

I just added a simple check before the delete code to ensure that this product doesn’t have orders using $product->orderDetails relationship, let’s create this relation in the Product model.

Open app/Models/Product.php and add this relation at the end of the class:

public function orderDetails()
    {
        return $this->hasMany(OrderDetail::class, "product_id");
    }

Now let’s add the route for the dashboard controller.

Open routes/web.php and update it as shown:

<?php


$router->get('/', function () use ($router) {
    return $router->app->version();
});

$router->group(['prefix' => 'api'], function () use ($router) {
    $router->post('/login', 'Auth\\LoginController@login');
    $router->post('/register', 'Auth\\RegisterController@register');

    $router->group(['prefix' => 'category'], function () use ($router) {
        $router->get('/', 'CategoriesController@index');
        $router->get('/htmltree', 'CategoriesController@getCategoryHtmlTree');
        $router->get('/menutree', 'CategoriesController@getCategoryMenuHtmlTree');
        $router->get('/featured-categories', 'CategoriesController@featuredCategories');
        $router->get('/{id}', 'CategoriesController@show');
    });

    $router->group(['prefix' => 'brand'], function () use ($router) {
        $router->get('/', 'BrandsController@index');
        $router->get('/brands-by-category', 'BrandsController@brandsByCategory');
        $router->get('/{id}', 'BrandsController@show');
    });

    $router->group(['prefix' => 'product'], function () use ($router) {
        $router->get('/', 'ProductsController@index');
        $router->get('/slider-products', 'ProductsController@sliderProducts');
        $router->get('/latest-products', 'ProductsController@latestProducts');
        $router->get('/featured-products', 'ProductsController@featuredProducts');
        $router->get('/search-products', 'ProductsController@searchProducts');
        $router->get('/products-by-ids', 'ProductsController@productsByIds');
        $router->get('/{id}', 'ProductsController@show');
    });

    $router->group(['prefix' => 'user'], function () use ($router) {
        $router->get('/', 'UsersController@index');
        $router->get('/{id}', 'UsersController@show');
    });

    $router->group(['prefix' => 'contact'], function () use ($router) {
       $router->post('/', 'ContactController@store');
    });

    $router->group(['middleware' => 'auth:api'], function () use ($router) {
        $router->get('/me', 'Auth\\LoginController@userDetails');
        $router->get('/logout', 'Auth\\LoginController@logout');
        $router->get('/check-login', 'Auth\\LoginController@checkLogin');
        $router->post('/update-profile', 'Auth\\LoginController@updateProfile');

        $router->group(['prefix' => 'category'], function () use ($router) {
            $router->post('/', 'CategoriesController@store');
            $router->put('/{id}', 'CategoriesController@update');
            $router->delete('/{id}', 'CategoriesController@destroy');
        });

        $router->group(['prefix' => 'brand'], function () use ($router) {
            $router->post('/', 'BrandsController@store');
            $router->put('/{id}', 'BrandsController@update');
            $router->delete('/{id}', 'BrandsController@destroy');
        });

        $router->group(['prefix' => 'product'], function () use ($router) {
           $router->post('/', 'ProductsController@store');
            $router->put('/{id}', 'ProductsController@update');
            $router->delete('/delete-image/{id}', 'ProductsController@destroyImage');
            $router->delete('/{id}', 'ProductsController@destroy');
        });

        $router->group(['prefix' => 'user'], function () use ($router) {
            $router->post('/', 'UsersController@store');
            $router->put('/{id}', 'UsersController@update');
            $router->delete('/{id}', 'UsersController@destroy');
        });

        $router->group(['prefix' => 'cart'], function () use ($router) {
            $router->get('/', 'ShoppingCartController@index');
            $router->post('/', 'ShoppingCartController@store');
            $router->put('/', 'ShoppingCartController@update');
            $router->get('/{id}', 'ShoppingCartController@show');
            $router->delete('/clearAll', 'ShoppingCartController@clearAll');
            $router->delete('/{id}', 'ShoppingCartController@destroy');
        });

        $router->group(['prefix' => 'shippingAddress'], function () use ($router) {
            $router->get('/', 'ShippingAddressesController@index');
            $router->post('/', 'ShippingAddressesController@store');
            $router->get('/{id}', 'ShippingAddressesController@show');
            $router->put('/{id}', 'ShippingAddressesController@update');
            $router->delete('/{id}', 'ShippingAddressesController@destroy');
        });

        $router->group(['prefix' => 'paymentMethods'], function () use ($router) {
           $router->get('/', 'PaymentMethodsController@index');
        });

        $router->group(['prefix' => 'orders'], function () use($router) {
            $router->get('/', 'OrdersController@index');
            $router->post('/', 'OrdersController@store');
            $router->get('/latest-pending-orders', 'OrdersController@getLatestPendingOrders');
            $router->get('/{id}', 'OrdersController@show');
            $router->put('/{id}', 'OrdersController@update');
        });

        $router->group(['prefix' => 'dashboard'], function () use($router) {
            $router->get('/', 'DashboardController@index');
        });
    });
});

The backend now is complete, we have to switch to Nuxt project and make the required updates.

 

 

Updating The Nuxt Project

Firstt open the Nuxt project and go to the admin/ directory.

admin/pages/index.vue

<template>
  <div class="main-content">
    <div class="section__content section__content--p30">
      <div class="container-fluid">
        <div class="row">
          <div class="col-md-12">
            <div class="overview-wrap">
              <h2 class="title-1">overview</h2>
            </div>
          </div>
        </div>
        <div class="row m-t-25">
          <div class="col-sm-6 col-lg-3">
            <div class="overview-item overview-item--c1">
              <div class="overview__inner">
                <div class="overview-box clearfix">
                  <div class="icon">
                    <i class="zmdi zmdi-account-o"></i>
                  </div>
                  <div class="text">
                    <h2>{{ this.total_members > 0 ? this.total_members : 'none' }}</h2>
                    <span>members online</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div class="col-sm-6 col-lg-3">
            <div class="overview-item overview-item--c2">
              <div class="overview__inner">
                <div class="overview-box clearfix">
                  <div class="icon">
                    <i class="zmdi zmdi-shopping-cart"></i>
                  </div>
                  <div class="text">
                    <h2>{{ this.total_items_sold > 0 ? this.total_items_sold : 'none' }}</h2>
                    <span>items solid</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div class="col-sm-6 col-lg-3">
            <div class="overview-item overview-item--c3">
              <div class="overview__inner">
                <div class="overview-box clearfix">
                  <div class="icon">
                    <i class="zmdi zmdi-calendar-note"></i>
                  </div>
                  <div class="text">
                    <h2>{{ this.total_items_week > 0 ? this.total_items_week : 'none' }}</h2>
                    <span>this week</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div class="col-sm-6 col-lg-3">
            <div class="overview-item overview-item--c4">
              <div class="overview__inner">
                <div class="overview-box clearfix">
                  <div class="icon">
                    <i class="zmdi zmdi-money"></i>
                  </div>
                  <div class="text">
                    <h2>{{ this.total_earnings !== 0 ? '$' + this.total_earnings : 'none' }}</h2>
                    <span>total earnings</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: "index",
    middleware: "auth",
    data() {
      return {
        total_members: 0,
        total_items_sold: 0,
        total_items_week: 0,
        total_earnings: 0
      }
    },
    mounted() {
      this.$axios.$get('/api/dashboard').then(response => {
        this.total_members = response.totalMembers;
        this.total_items_sold = response.totalItemsSold;
        this.total_items_week = response.totalItemsThisWeek;
        this.total_earnings = response.totalEarnings;
      });
    }
  }
</script>

If you run the application and go to the dashboard homepage you will see the real data in the homepage.

 

Next let’s modify the header component, so open admin/components/partials/AdminHeader.vue and update like so:

<template>
    <header class="header-desktop">
      <div class="section__content section__content--p30">
        <div class="container-fluid">
          <div class="header-wrap" style="float: right">
            <div class="header-button">
              <div class="noti-wrap">
                <div class="noti__item js-item-menu">
                  <i class="zmdi zmdi-notifications" :title="this.countNotifications > 0 ? this.countNotifications + ' pending orders' : ''"></i>
                  <span class="quantity" v-if="this.countNotifications > 0">{{ this.countNotifications }}</span>
                  <div class="notifi-dropdown js-dropdown">
                    <div class="notifi__title">
                      <p>Latest top {{ this.notifications.length }} Pending Orders</p>
                    </div>
                    <div class="notifi__item" v-for="notification in this.notifications" :key="notification.id">
                      <div class="bg-c3 img-cir img-40">
                        <i class="zmdi zmdi-shopping-cart"></i>
                      </div>
                      <div class="content">
                        <p>{{ notification.status_message }} by {{ notification.user.name  + " (" + notification.user.email + ")" }}</p>
                        <span class="date">{{ notification.created_at_formatted }}</span>
                      </div>
                    </div>
                    <div class="notifi__footer" v-if="this.countNotifications > 0">
                      <nuxt-link to="/orders">All orders</nuxt-link>
                      <nuxt-link to="/orders/pending">Pending orders</nuxt-link>
                    </div>
                  </div>
                </div>
              </div>
              <div class="account-wrap">
                <div class="account-item clearfix js-item-menu">
                  <div class="image">
                    <img src="/imgs/avatar.png?93be8c&93be8c" :alt="getUserData().name">
                  </div>
                  <div class="content">
                    <a class="js-acc-btn" href="#">{{ getUserData().name }}</a>
                  </div>
                  <div class="account-dropdown js-dropdown">
                    <div class="info clearfix">
                      <div class="image">
                        <a href="#">
                          <img src="/imgs/avatar.png?93be8c&93be8c" :alt="getUserData().name">
                        </a>
                      </div>
                      <div class="content">
                        <h5 class="name">
                          <a href="#">{{ getUserData().name }}</a>
                        </h5>
                        <span class="email">{{ getUserData().email }}</span>
                      </div>
                    </div>
                    <div class="account-dropdown__body">
                      <div class="account-dropdown__item">
                        <nuxt-link :to="'/user/' + getUserData().id">
                          <i class="zmdi zmdi-account"></i>Account
                        </nuxt-link>
                      </div>
                    </div>
                    <div class="account-dropdown__footer">
                      <a href="#" v-on:click.prevent="logout()">
                        <i class="zmdi zmdi-power"></i>Logout</a>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <audio hidden id="notification_alert">
          <source src="/audio/notification_bell.mp3" type="audio/mpeg">
        </audio>
      </div>
    </header>
</template>

<script>
    import {OrdersApi} from "../../api/orders";

    export default {
        name: "admin-header",
        data() {
          return {
            notifications: [],
            countNotifications: 0
          }
        },
        methods: {
          getUserData() {
            return JSON.parse(localStorage.getItem("user_data"));
          },
          logout() {
            this.$axios.setHeader('Authorization', "Bearer " + localStorage.getItem('auth_token'));
            this.$axios.get('/api/logout').then(response => {
              if(response.data.success) {

                localStorage.removeItem('auth_token');
                localStorage.removeItem('is_authenticated');
                localStorage.removeItem('user_data');

                this.$router.push('/login');
              }
            }).catch(err => {
              console.log(err.response);
            });
          },
          reinitializeNotifDrop() {
            try {
              var menu = $('.js-item-menu');
              var sub_menu_is_showed = -1;

              for (var i = 0; i < menu.length; i++) {
                $(menu[i]).on('click', function (e) {
                  e.preventDefault();
                  $('.js-right-sidebar').removeClass("show-sidebar");
                  if (jQuery.inArray(this, menu) == sub_menu_is_showed) {
                    $(this).toggleClass('show-dropdown');
                    sub_menu_is_showed = -1;
                  }
                  else {
                    for (var i = 0; i < menu.length; i++) {
                      $(menu[i]).removeClass("show-dropdown");
                    }
                    $(this).toggleClass('show-dropdown');
                    sub_menu_is_showed = jQuery.inArray(this, menu);
                  }
                });
              }
              $(".js-item-menu, .js-dropdown").click(function (event) {
                event.stopPropagation();
              });

              $("body,html").on("click", function () {
                for (var i = 0; i < menu.length; i++) {
                  menu[i].classList.remove("show-dropdown");
                }
                sub_menu_is_showed = -1;
              });

            } catch (error) {
              console.log(error);
            }
          },
          playNotifAlert() {
            document.getElementById("notification_alert").play();
          }
        },
        created() {
          var channel = this.$pusher().subscribe('notifications');
          channel.bind('orderCreated', (data) => {
            const order = data.order;

            this.playNotifAlert();

            if(order.status === 'pending') {
              this.$toastr.i(`New pending order added by ` + order.user.name, `order Id ${order.id}`);

              this.notifications.unshift(order);
              this.countNotifications++;
              this.reinitializeNotifDrop();
            } else {
              if(order.status === 'success') {
                this.$toastr.s("New successful order payment by " + order.user.name, `order Id ${order.id}`);
              } else {
                this.$toastr.w("New cancelled order payment by " + order.user.name, `order Id ${order.id}`);
              }
            }
          });
        },
        mounted() {
          OrdersApi.getLatestPending(this.$axios).then(response => {
            this.notifications = response.topOrders;
            this.countNotifications = response.countAllPending;
          });
        }
    }
</script>

<style scoped>
  .notifi__item {
    padding-top: 5px !important;
    padding-bottom: 5px !important;
  }

  .notifi__item .content p {
    line-height: 1.5;
    font-size: 13px;
  }

  .mess__title, .email__title, .notifi__title {
    padding: 11px;
  }

  .mess__footer a, .email__footer a, .notifi__footer a {
    padding: 9px 0;
  }
</style>

In the above code i fixed the Account link to point to the correct edit user page, also i modified the user avatar image path you can download this image and put it inside admin/static/imgs/ directory.

 

Finally open admin/store/product.js find the delete() action and add this line in the catch() callback:

dispatch('showValidationErrors', err);

All updates now done. below is the repository links for the Lumen and Nuxt project and html template used in the project.

 

 

Source Code

You can clone the source code of the lumen and nuxt projects from these links with the steps to install the use the project.

Download Lumen ProjectDownload NUXTJS ProjectDownload Website Template

 

Conclusion

In this series, you learned a lot of concepts in this course about using Lumen and Nuxt frameworks and how to apply these concepts and skills in this practical project. There are still a lot of things that you can add on this commercial site, such as adding product reviews, the ability to duplicate a specific product on the control panel, adding other payment methods, etc.

 

0 0 votes
Article Rating

What's your reaction?

Excited
1
Happy
1
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments