Преглед на файлове

Merge pull request #285 from ControlPanel-gg/development

Development
AVMG преди 3 години
родител
ревизия
0c51471e5d
променени са 53 файла, в които са добавени 15947 реда и са изтрити 597 реда
  1. 1 0
      .gitignore
  2. 45 47
      app/Classes/Pterodactyl.php
  3. 5 0
      app/Http/Controllers/Admin/NestsController.php
  4. 5 0
      app/Http/Controllers/Admin/NodeController.php
  5. 64 0
      app/Http/Controllers/Admin/OverViewController.php
  6. 32 5
      app/Http/Controllers/Admin/PaymentController.php
  7. 5 5
      app/Http/Controllers/Admin/PaypalProductController.php
  8. 45 7
      app/Http/Controllers/Admin/ProductController.php
  9. 2 2
      app/Http/Controllers/Admin/ServerController.php
  10. 10 16
      app/Http/Controllers/Admin/SettingsController.php
  11. 0 3
      app/Http/Controllers/Admin/UserController.php
  12. 51 55
      app/Http/Controllers/HomeController.php
  13. 100 0
      app/Http/Controllers/ProductController.php
  14. 1 1
      app/Http/Controllers/ProfileController.php
  15. 83 56
      app/Http/Controllers/ServerController.php
  16. 17 3
      app/Models/Configuration.php
  17. 65 25
      app/Models/Egg.php
  18. 47 7
      app/Models/Location.php
  19. 48 5
      app/Models/Nest.php
  20. 57 13
      app/Models/Node.php
  21. 11 2
      app/Models/Payment.php
  22. 37 3
      app/Models/PaypalProduct.php
  23. 24 2
      app/Models/Product.php
  24. 3 3
      app/Models/User.php
  25. 1 0
      composer.json
  26. 220 2
      composer.lock
  27. 32 0
      database/migrations/2021_07_06_152319_create_egg_product_table.php
  28. 32 0
      database/migrations/2021_07_06_154314_create_node_product_table.php
  29. 32 0
      database/migrations/2021_07_06_154658_add_disabled_to_eggs_table.php
  30. 36 0
      database/migrations/2021_11_05_071456_add_tax_to_paymentlogs.php
  31. 8 0
      database/seeders/Seeds/ConfigurationSeeder.php
  32. 13803 1
      package-lock.json
  33. 0 0
      public/css/app.css.map
  34. 0 0
      public/js/app.js
  35. 0 0
      public/js/app.js.map
  36. 0 0
      resources/js/alpine.js
  37. 1 0
      resources/js/app.js
  38. 4 4
      resources/js/bootstrap.js
  39. 153 0
      resources/views/admin/overview/index.blade.php
  40. 7 1
      resources/views/admin/payments/index.blade.php
  41. 179 90
      resources/views/admin/products/create.blade.php
  42. 105 31
      resources/views/admin/products/edit.blade.php
  43. 5 4
      resources/views/admin/products/index.blade.php
  44. 37 31
      resources/views/admin/settings/index.blade.php
  45. 0 2
      resources/views/admin/users/index.blade.php
  46. 1 0
      resources/views/admin/users/show.blade.php
  47. 86 56
      resources/views/layouts/main.blade.php
  48. 1 1
      resources/views/mail/payment/confirmed.blade.php
  49. 419 99
      resources/views/servers/create.blade.php
  50. 5 5
      resources/views/store/checkout.blade.php
  51. 1 1
      resources/views/store/index.blade.php
  52. 19 9
      routes/web.php
  53. 2 0
      storage/debugbar/.gitignore

+ 1 - 0
.gitignore

@@ -4,6 +4,7 @@
 /storage/*.key
 /vendor
 /storage/credit_deduction_log
+storage/debugbar
 .env
 .env.testing
 .env.backup

+ 45 - 47
app/Classes/Pterodactyl.php

@@ -11,10 +11,11 @@ use Exception;
 use Illuminate\Http\Client\PendingRequest;
 use Illuminate\Http\Client\Response;
 use Illuminate\Support\Facades\Http;
-use Illuminate\Validation\Validator;
 
 class Pterodactyl
 {
+    //TODO: Extend error handling (maybe logger for more errors when debugging)
+
     /**
      * @return PendingRequest
      */
@@ -27,77 +28,48 @@ class Pterodactyl
         ])->baseUrl(env('PTERODACTYL_URL') . '/api');
     }
 
-    //TODO: Extend error handling (maybe logger for more errors when debugging)
     /**
-     * Get user by pterodactyl id
-     * @param int $pterodactylId
-     * @return mixed
-     */
-    public function getUser(int $pterodactylId)
-    {
-        $response = self::client()->get("/application/users/{$pterodactylId}");
-
-        if ($response->failed()) return $response->json();
-        return $response->json()['attributes'];
-    }
-
-    /**
-     * @param Node $node
-     * @return array|mixed|null
-     * @throws Exception
+     * @return Exception
      */
-    public static function getFreeAllocations(Node $node)
+    private static function getException(): Exception
     {
-        $response = self::getAllocations($node);
-        $freeAllocations = [];
-
-        if (isset($response['data'])) {
-            if (!empty($response['data'])) {
-                foreach ($response['data'] as $allocation) {
-                    if (!$allocation['attributes']['assigned']) array_push($freeAllocations, $allocation);
-                }
-            }
-        }
-
-        return $freeAllocations;
+        return new Exception('Request Failed, is pterodactyl set-up correctly?');
     }
 
     /**
-     * @return null
+     * @param Nest $nest
+     * @return mixed
      * @throws Exception
      */
-    public static function getNests()
+    public static function getEggs(Nest $nest)
     {
-        $response = self::client()->get('/application/nests');
+        $response = self::client()->get("/application/nests/{$nest->id}/eggs?include=nest,variables");
         if ($response->failed()) throw self::getException();
         return $response->json()['data'];
     }
 
     /**
-     * @param Nest $nest
      * @return mixed
      * @throws Exception
      */
-    public static function getEggs(Nest $nest)
+    public static function getNodes()
     {
-        $response = self::client()->get("/application/nests/{$nest->id}/eggs?include=nest,variables");
+        $response = self::client()->get('/application/nodes');
         if ($response->failed()) throw self::getException();
         return $response->json()['data'];
     }
 
-
     /**
-     * @return mixed
+     * @return null
      * @throws Exception
      */
-    public static function getNodes()
+    public static function getNests()
     {
-        $response = self::client()->get('/application/nodes');
+        $response = self::client()->get('/application/nests');
         if ($response->failed()) throw self::getException();
         return $response->json()['data'];
     }
 
-
     /**
      * @return mixed
      * @throws Exception
@@ -112,16 +84,37 @@ class Pterodactyl
     /**
      * @param Node $node
      * @return mixed
+     * @throws Exception
      */
     public static function getFreeAllocationId(Node $node)
     {
-
         return self::getFreeAllocations($node)[0]['attributes']['id'] ?? null;
     }
 
+    /**
+     * @param Node $node
+     * @return array|mixed|null
+     * @throws Exception
+     */
+    public static function getFreeAllocations(Node $node)
+    {
+        $response = self::getAllocations($node);
+        $freeAllocations = [];
+
+        if (isset($response['data'])) {
+            if (!empty($response['data'])) {
+                foreach ($response['data'] as $allocation) {
+                    if (!$allocation['attributes']['assigned']) array_push($freeAllocations, $allocation);
+                }
+            }
+        }
+
+        return $freeAllocations;
+    }
 
     /**
      * @param Node $node
+     * @return array|mixed
      * @throws Exception
      */
     public static function getAllocations(Node $node)
@@ -132,7 +125,6 @@ class Pterodactyl
         return $response->json();
     }
 
-
     /**
      * @param String $route
      * @return string
@@ -174,6 +166,7 @@ class Pterodactyl
                 "default" => $allocationId
             ]
         ]);
+
     }
 
     public static function suspendServer(Server $server)
@@ -191,10 +184,15 @@ class Pterodactyl
     }
 
     /**
-     * @return Exception
+     * Get user by pterodactyl id
+     * @param int $pterodactylId
+     * @return mixed
      */
-    private static function getException(): Exception
+    public function getUser(int $pterodactylId)
     {
-        return new Exception('Request Failed, is pterodactyl set-up correctly?');
+        $response = self::client()->get("/application/users/{$pterodactylId}");
+
+        if ($response->failed()) return $response->json();
+        return $response->json()['attributes'];
     }
 }

+ 5 - 0
app/Http/Controllers/Admin/NestsController.php

@@ -14,6 +14,11 @@ use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
 
+/**
+ * @deprecated
+ * Class NestsController
+ * @package App\Http\Controllers\Admin
+ */
 class NestsController extends Controller
 {
     /**

+ 5 - 0
app/Http/Controllers/Admin/NodeController.php

@@ -14,6 +14,11 @@ use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
 
+/**
+ * @deprecated
+ * Class NodeController
+ * @package App\Http\Controllers\Admin
+ */
 class NodeController extends Controller
 {
     /**

+ 64 - 0
app/Http/Controllers/Admin/OverViewController.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use App\Models\Egg;
+use App\Models\Location;
+use App\Models\Nest;
+use App\Models\Node;
+use App\Models\Payment;
+use App\Models\Server;
+use App\Models\User;
+use Illuminate\Support\Facades\Cache;
+
+class OverViewController extends Controller
+{
+    public const TTL = 86400;
+
+    public function index()
+    {
+        $userCount = Cache::remember('user:count', self::TTL, function () {
+            return User::query()->count();
+        });
+
+        $creditCount = Cache::remember('credit:count', self::TTL, function () {
+            return User::query()->sum('credits');
+        });
+
+        $paymentCount = Cache::remember('payment:count', self::TTL, function () {
+            return Payment::query()->count();
+        });
+
+        $serverCount = Cache::remember('server:count', self::TTL, function () {
+            return Server::query()->count();
+        });
+
+        $lastEgg = Egg::query()->latest('updated_at')->first();
+        $syncLastUpdate = $lastEgg ? $lastEgg->updated_at->isoFormat('LLL') : __('unknown');
+
+        return view('admin.overview.index', [
+            'serverCount'  => $serverCount,
+            'userCount'    => $userCount,
+            'paymentCount' => $paymentCount,
+            'creditCount'  => number_format($creditCount, 2, '.', ''),
+
+            'locationCount'  => Location::query()->count(),
+            'nodeCount'      => Node::query()->count(),
+            'nestCount'      => Nest::query()->count(),
+            'eggCount'       => Egg::query()->count(),
+            'syncLastUpdate' => $syncLastUpdate
+        ]);
+    }
+
+    /**
+     * @description Sync locations,nodes,nests,eggs with the linked pterodactyl panel
+     */
+    public function syncPterodactyl()
+    {
+        Node::syncNodes();
+        Egg::syncEggs();
+
+        return redirect()->back()->with('success', __('Pterodactyl synced'));
+    }
+}

+ 32 - 5
app/Http/Controllers/Admin/PaymentController.php

@@ -27,6 +27,7 @@ use PayPalHttp\HttpException;
 
 class PaymentController extends Controller
 {
+
     /**
      * @return Application|Factory|View
      */
@@ -45,7 +46,10 @@ class PaymentController extends Controller
     public function checkOut(Request $request, PaypalProduct $paypalProduct)
     {
         return view('store.checkout')->with([
-            'product' => $paypalProduct
+            'product'      => $paypalProduct,
+            'taxvalue'     => $paypalProduct->getTaxValue(),
+            'taxpercent'   => $paypalProduct->getTaxPercent(),
+            'total'        => $paypalProduct->getTotalPrice()
         ]);
     }
 
@@ -65,8 +69,20 @@ class PaymentController extends Controller
                     "reference_id" => uniqid(),
                     "description" => $paypalProduct->description,
                     "amount"       => [
-                        "value"         => $paypalProduct->price,
-                        "currency_code" => strtoupper($paypalProduct->currency_code)
+                        "value"         => $paypalProduct->getTotalPrice(),
+                        'currency_code' => strtoupper($paypalProduct->currency_code),
+                        'breakdown' =>[
+                            'item_total' =>
+                               [
+                                    'currency_code' => strtoupper($paypalProduct->currency_code),
+                                    'value' => $paypalProduct->price,
+                                ],
+                            'tax_total' =>
+                                [
+                                    'currency_code' => strtoupper($paypalProduct->currency_code),
+                                    'value' => $paypalProduct->getTaxValue(),
+                                ]
+                        ]
                     ]
                 ]
             ],
@@ -76,6 +92,8 @@ class PaymentController extends Controller
                 'brand_name' =>  config('app.name', 'Laravel'),
                 'shipping_preference'  => 'NO_SHIPPING'
             ]
+
+        
         ];
 
 
@@ -161,6 +179,9 @@ class PaymentController extends Controller
                     'status' => $response->result->status,
                     'amount' => $paypalProduct->quantity,
                     'price' => $paypalProduct->price,
+                    'tax_value' => $paypalProduct->getTaxValue(),
+                    'tax_percent' => $paypalProduct->getTaxPercent(),
+                    'total_price' => $paypalProduct->getTotalPrice(),
                     'currency_code' => $paypalProduct->currency_code,
                     'payer' => json_encode($response->result->payer),
                 ]);
@@ -199,7 +220,7 @@ class PaymentController extends Controller
      */
     public function cancel(Request $request)
     {
-        return redirect()->route('store.index')->with('success', 'Payment was Cannceled');
+        return redirect()->route('store.index')->with('success', 'Payment was Canceled');
     }
 
 
@@ -216,7 +237,13 @@ class PaymentController extends Controller
                 return $payment->user->name;
             })
             ->editColumn('price', function (Payment $payment) {
-                return $payment->formatCurrency();
+                return $payment->formatToCurrency($payment->price);
+            })
+            ->editColumn('tax_value', function (Payment $payment) {
+                return $payment->formatToCurrency($payment->tax_value);
+            })
+            ->editColumn('total_price', function (Payment $payment) {
+                return $payment->formatToCurrency($payment->total_price);
             })
             ->editColumn('created_at', function (Payment $payment) {
                 return $payment->created_at ? $payment->created_at->diffForHumans() : '';

+ 5 - 5
app/Http/Controllers/Admin/PaypalProductController.php

@@ -62,7 +62,7 @@ class PaypalProductController extends Controller
         $disabled = !is_null($request->input('disabled'));
         PaypalProduct::create(array_merge($request->all(), ['disabled' => $disabled]));
 
-        return redirect()->route('admin.store.index')->with('success', 'store item has been created!');
+        return redirect()->route('admin.store.index')->with('success', 'Store item has been created!');
     }
 
     /**
@@ -112,7 +112,7 @@ class PaypalProductController extends Controller
         $disabled = !is_null($request->input('disabled'));
         $paypalProduct->update(array_merge($request->all(), ['disabled' => $disabled]));
 
-        return redirect()->route('admin.store.index')->with('success', 'store item has been updated!');
+        return redirect()->route('admin.store.index')->with('success', 'Store item has been updated!');
     }
 
     /**
@@ -124,7 +124,7 @@ class PaypalProductController extends Controller
     {
         $paypalProduct->update(['disabled' => !$paypalProduct->disabled]);
 
-        return redirect()->route('admin.store.index')->with('success', 'product has been updated!');
+        return redirect()->route('admin.store.index')->with('success', 'Product has been updated!');
     }
 
     /**
@@ -136,7 +136,7 @@ class PaypalProductController extends Controller
     public function destroy(PaypalProduct $paypalProduct)
     {
         $paypalProduct->delete();
-        return redirect()->back()->with('success', 'store item has been removed!');
+        return redirect()->back()->with('success', 'Store item has been removed!');
     }
 
 
@@ -173,7 +173,7 @@ class PaypalProductController extends Controller
                 return $paypalProduct->created_at ? $paypalProduct->created_at->diffForHumans() : '';
             })
             ->editColumn('price', function (PaypalProduct $paypalProduct) {
-                return $paypalProduct->formatCurrency();
+                return $paypalProduct->formatToCurrency($paypalProduct->price);
             })
             ->rawColumns(['actions', 'disabled'])
             ->make();

+ 45 - 7
app/Http/Controllers/Admin/ProductController.php

@@ -3,6 +3,10 @@
 namespace App\Http\Controllers\Admin;
 
 use App\Http\Controllers\Controller;
+use App\Models\Egg;
+use App\Models\Location;
+use App\Models\Nest;
+use App\Models\Node;
 use App\Models\Configuration;
 use App\Models\Product;
 use Exception;
@@ -33,7 +37,18 @@ class ProductController extends Controller
      */
     public function create()
     {
-        return view('admin.products.create');
+        return view('admin.products.create' , [
+            'locations' => Location::with('nodes')->get(),
+            'nests' => Nest::with('eggs')->get(),
+        ]);
+    }
+
+    public function clone(Request $request , Product $product){
+        return view('admin.products.create' , [
+            'product' => $product,
+            'locations' => Location::with('nodes')->get(),
+            'nests' => Nest::with('eggs')->get(),
+        ]);
     }
 
     /**
@@ -57,13 +72,19 @@ class ProductController extends Controller
             "databases" => "required|numeric|max:1000000|min:0",
             "backups" => "required|numeric|max:1000000|min:0",
             "allocations" => "required|numeric|max:1000000|min:0",
+            "nodes.*" => "required|exists:nodes,id",
+            "eggs.*" => "required|exists:eggs,id",
             "disabled" => "nullable",
         ]);
 
         $disabled = !is_null($request->input('disabled'));
-        Product::create(array_merge($request->all(), ['disabled' => $disabled]));
+        $product = Product::create(array_merge($request->all(), ['disabled' => $disabled]));
 
-        return redirect()->route('admin.products.index')->with('success', 'product has been created!');
+        #link nodes and eggs
+        $product->eggs()->attach($request->input('eggs'));
+        $product->nodes()->attach($request->input('nodes'));
+
+        return redirect()->route('admin.products.index')->with('success', 'Product has been created!');
     }
 
     /**
@@ -89,7 +110,9 @@ class ProductController extends Controller
     public function edit(Product $product)
     {
         return view('admin.products.edit', [
-            'product' => $product
+            'product' => $product,
+            'locations' => Location::with('nodes')->get(),
+            'nests' => Nest::with('eggs')->get(),
         ]);
     }
 
@@ -115,13 +138,21 @@ class ProductController extends Controller
             "databases" => "required|numeric|max:1000000|min:0",
             "backups" => "required|numeric|max:1000000|min:0",
             "allocations" => "required|numeric|max:1000000|min:0",
+            "nodes.*" => "required|exists:nodes,id",
+            "eggs.*" => "required|exists:eggs,id",
             "disabled" => "nullable",
         ]);
 
         $disabled = !is_null($request->input('disabled'));
         $product->update(array_merge($request->all(), ['disabled' => $disabled]));
 
-        return redirect()->route('admin.products.index')->with('success', 'product has been updated!');
+        #link nodes and eggs
+        $product->eggs()->detach();
+        $product->nodes()->detach();
+        $product->eggs()->attach($request->input('eggs'));
+        $product->nodes()->attach($request->input('nodes'));
+
+        return redirect()->route('admin.products.index')->with('success', 'Product has been updated!');
     }
 
     /**
@@ -133,7 +164,7 @@ class ProductController extends Controller
     {
         $product->update(['disabled' => !$product->disabled]);
 
-        return redirect()->route('admin.products.index')->with('success', 'product has been updated!');
+        return redirect()->route('admin.products.index')->with('success', 'Product has been updated!');
     }
 
     /**
@@ -150,7 +181,7 @@ class ProductController extends Controller
         }
 
         $product->delete();
-        return redirect()->back()->with('success', 'product has been removed!');
+        return redirect()->back()->with('success', 'Product has been removed!');
     }
 
 
@@ -166,6 +197,7 @@ class ProductController extends Controller
             ->addColumn('actions', function (Product $product) {
                 return '
                             <a data-content="Show" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.products.show', $product->id) . '" class="btn btn-sm text-white btn-warning mr-1"><i class="fas fa-eye"></i></a>
+                            <a data-content="Clone" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.products.clone', $product->id) . '" class="btn btn-sm text-white btn-primary mr-1"><i class="fas fa-clone"></i></a>
                             <a data-content="Edit" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.products.edit', $product->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
 
                            <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.products.destroy', $product->id) . '">
@@ -179,6 +211,12 @@ class ProductController extends Controller
             ->addColumn('servers', function (Product $product) {
                 return $product->servers()->count();
             })
+            ->addColumn('nodes', function (Product $product) {
+                return $product->nodes()->count();
+            })
+            ->addColumn('eggs', function (Product $product) {
+                return $product->eggs()->count();
+            })
             ->addColumn('disabled', function (Product $product) {
                 $checked = $product->disabled == false ? "checked" : "";
                 return '

+ 2 - 2
app/Http/Controllers/Admin/ServerController.php

@@ -92,7 +92,7 @@ class ServerController extends Controller
     {
         try {
             $server->delete();
-            return redirect()->route('admin.servers.index')->with('success', 'server removed');
+            return redirect()->route('admin.servers.index')->with('success', 'Server removed');
         } catch (Exception $e) {
             return redirect()->route('admin.servers.index')->with('error', 'An exception has occurred while trying to remove a resource "' . $e->getMessage() . '"');
         }
@@ -109,7 +109,7 @@ class ServerController extends Controller
             return redirect()->back()->with('error', $exception->getMessage());
         }
 
-        return redirect()->back()->with('success', 'server has been updated!');
+        return redirect()->back()->with('success', 'Server has been updated!');
     }
 
     /**

+ 10 - 16
app/Http/Controllers/Admin/SettingsController.php

@@ -8,7 +8,6 @@ use Illuminate\Contracts\View\Factory;
 use Illuminate\Contracts\View\View;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
-use Illuminate\Support\Facades\Storage;
 
 class SettingsController extends Controller
 {
@@ -22,25 +21,20 @@ class SettingsController extends Controller
         return view('admin.settings.index');
     }
 
-    public function updateIcons(Request $request){
-
+    public function updateIcons(Request $request)
+    {
         $request->validate([
-            'favicon' => 'required',
-            'icon' => 'required',
+            'icon' => 'nullable|max:10000|mimes:jpg,png,jpeg',
+            'favicon' => 'nullable|max:10000|mimes:ico',
         ]);
 
-        //store favicon
-        $favicon = $request->input('favicon');
-        $favicon = json_decode($favicon);
-        $favicon = explode(",",$favicon->output->image)[1];
-        Storage::disk('public')->put('favicon.ico' , base64_decode($favicon));
-
-        //store dashboard icon
-        $icon = $request->input('icon');
-        $icon = json_decode($icon);
-        $icon = explode(",",$icon->output->image)[1];
+        if ($request->hasFile('icon')) {
+            $request->file('icon')->storeAs('public', 'icon.png');
+        }
 
-        Storage::disk('public')->put('icon.png' , base64_decode($icon));
+        if ($request->hasFile('favicon')) {
+            $request->file('favicon')->storeAs('public', 'favicon.ico');
+        }
 
         return redirect()->route('admin.settings.index')->with('success', 'Icons updated!');
     }

+ 0 - 3
app/Http/Controllers/Admin/UserController.php

@@ -250,9 +250,6 @@ class UserController extends Controller
             ->addColumn('credits', function (User $user) {
                 return '<i class="fas fa-coins mr-2"></i> ' . $user->credits();
             })
-            ->addColumn('usage', function (User $user) {
-                return '<i class="fas fa-coins mr-2"></i> ' . $user->creditUsage();
-            })
             ->addColumn('verified', function (User $user) {
                 return $user->getVerifiedStatus();
             })

+ 51 - 55
app/Http/Controllers/HomeController.php

@@ -1,16 +1,20 @@
 <?php
+
 namespace App\Http\Controllers;
 
+use App\Models\Egg;
+use App\Models\Product;
 use App\Models\UsefulLink;
 use App\Models\Configuration;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
 
+
 class HomeController extends Controller
 {
-    const TIME_LEFT_BG_SUCCESS          = "bg-success";
-    const TIME_LEFT_BG_WARNING          = "bg-warning";
-    const TIME_LEFT_BG_DANGER           = "bg-danger";
+    const TIME_LEFT_BG_SUCCESS = "bg-success";
+    const TIME_LEFT_BG_WARNING = "bg-warning";
+    const TIME_LEFT_BG_DANGER = "bg-danger";
     const TIME_LEFT_OUT_OF_CREDITS_TEXT = "You ran out of Credits";
 
     public function __construct()
@@ -19,55 +23,50 @@ class HomeController extends Controller
     }
 
     /**
-    * @description Get the Background Color for the Days-Left-Box in HomeView
-    *
-    * @param  float  $days
-    *
-    * @return string
-    */
-    public function getTimeLeftBoxBackground(float $days)
+     * @description Get the Background Color for the Days-Left-Box in HomeView
+     *
+     * @param float $daysLeft
+     *
+     * @return string
+     */
+    public function getTimeLeftBoxBackground(float $daysLeft): string
     {
-        switch ($days)
-        {
-            case ($days >= 15):
-                return $this::TIME_LEFT_BG_SUCCESS;
-            break;
-
-            case ($days >= 8 && $days <= 14):
-                return $this::TIME_LEFT_BG_WARNING;
-            break;
-
-            case ($days <= 7):
-                return $this::TIME_LEFT_BG_DANGER;
-            break;
-
-            default:
-                return $this::TIME_LEFT_BG_WARNING;
+        if ($daysLeft >= 15) {
+            return $this::TIME_LEFT_BG_SUCCESS;
+        }
+        if ($daysLeft <= 7) {
+            return $this::TIME_LEFT_BG_DANGER;
         }
+        return $this::TIME_LEFT_BG_WARNING;
     }
 
+
     /**
-    * @description Get the Text for the Days-Left-Box in HomeView
-    *
-    * @param  float  $days
-    * @param  float  $hours
-    *
-    * @return string
-    */
-    public function getTimeLeftBoxText(float $days, float $hours)
+     * @description Set "hours", "days" or nothing behind the remaining time
+     *
+     * @param float $daysLeft
+     * @param float $hoursLeft
+     *
+     * @return string|void
+     */
+    public function getTimeLeftBoxUnit(float $daysLeft, float $hoursLeft)
     {
-        if ($days < 1)
-        {
-            if ($hours < 1)
-            {
-                return $this::TIME_LEFT_OUT_OF_CREDITS_TEXT;
-            }
-            else
-            {
-                return strval($hours);
-            }
-        }
-        return strval(number_format($days, 0));
+        if ($daysLeft > 1) return 'days';
+        return $hoursLeft < 1 ? null : "hours";
+    }
+
+    /**
+     * @description Get the Text for the Days-Left-Box in HomeView
+     *
+     * @param float $daysLeft
+     * @param float $hoursLeft
+     *
+     * @return string
+     */
+    public function getTimeLeftBoxText(float $daysLeft, float $hoursLeft)
+    {
+        if ($daysLeft > 1) return strval(number_format($daysLeft, 0));
+        return ($hoursLeft < 1 ? $this::TIME_LEFT_OUT_OF_CREDITS_TEXT : strval($hoursLeft));
     }
 
     /** Show the application dashboard. */
@@ -80,19 +79,16 @@ class HomeController extends Controller
         $unit = "";
 
         /** Build our Time-Left-Box */
-        if ($credits > 0.01 and $usage > 0)
-        {
-            $days = number_format(($credits * 30) / $usage, 2, '.', '');
-            $hours = number_format($credits / ($usage / 30 / 24) , 2, '.', '');
-
-            $bg = $this->getTimeLeftBoxBackground($days);
-            $boxText = $this->getTimeLeftBoxText($days, $hours);
-            $unit = $days < 1 ? 'hours' : 'days';
+        if ($credits > 0.01 and $usage > 0) {
+            $daysLeft = number_format(($credits * 30) / $usage, 2, '.', '');
+            $hoursLeft = number_format($credits / ($usage / 30 / 24), 2, '.', '');
 
+            $bg = $this->getTimeLeftBoxBackground($daysLeft);
+            $boxText = $this->getTimeLeftBoxText($daysLeft, $hoursLeft);
+            $unit = $daysLeft < 1 ? ($hoursLeft < 1 ? null : "hours") : "days";
         }
 
 
-
         // RETURN ALL VALUES
         return view('home')->with([
             'useage' => $usage,

+ 100 - 0
app/Http/Controllers/ProductController.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Egg;
+use App\Models\Location;
+use App\Models\Node;
+use App\Models\Product;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Support\Collection;
+
+class ProductController extends Controller
+{
+    /**
+     * @description get product locations based on selected egg
+     * @param Request $request
+     * @param Egg $egg
+     * @return Collection|JsonResponse
+     */
+    public function getNodesBasedOnEgg(Request $request, Egg $egg)
+    {
+        if (is_null($egg->id)) return response()->json('Egg ID is required', '400');
+
+        #get products that include this egg
+        $products = Product::query()
+            ->with('nodes')
+            ->where('disabled', '=', false)
+            ->whereHas('eggs', function (Builder $builder) use ($egg) {
+                $builder->where('id', '=', $egg->id);
+            })->get();
+
+        $nodes = collect();
+
+        #filter unique nodes
+        $products->each(function (Product $product) use ($nodes) {
+            $product->nodes->each(function (Node $node) use ($nodes) {
+                if (!$nodes->contains('id', $node->id) && !$node->disabled) {
+                    $nodes->add($node);
+                }
+            });
+        });
+
+
+        return $nodes;
+    }
+
+    /**
+     * @description get product locations based on selected egg
+     * @param Request $request
+     * @param Egg $egg
+     * @return Collection|JsonResponse
+     */
+    public function getLocationsBasedOnEgg(Request $request, Egg $egg)
+    {
+        $nodes = $this->getNodesBasedOnEgg($request, $egg);
+        $locations = collect();
+
+        //locations
+        $nodes->each(function (Node $node) use ($nodes, $locations) {
+            /** @var Location $location */
+            $location = $node->location;
+
+            if (!$locations->contains('id', $location->id)) {
+                $nodeIds = $nodes->map(function ($node) {
+                    return $node->id;
+                });
+
+                $location->nodes = $location->nodes()
+                    ->whereIn('id', $nodeIds)
+                    ->get();
+
+                $locations->add($location);
+            }
+        });
+
+        return $locations;
+    }
+
+    /**
+     * @param Node $node
+     * @param Egg $egg
+     * @return Collection|JsonResponse
+     */
+    public function getProductsBasedOnNode(Egg $egg, Node $node)
+    {
+        if (is_null($egg->id) || is_null($node->id)) return response()->json('node and egg id is required', '400');
+
+        return Product::query()
+            ->where('disabled', '=', false)
+            ->whereHas('nodes', function (Builder $builder) use ($node) {
+                $builder->where('id', '=', $node->id);
+            })
+            ->whereHas('eggs', function (Builder $builder) use ($egg) {
+                $builder->where('id', '=', $egg->id);
+            })
+            ->get();
+    }
+}

+ 1 - 1
app/Http/Controllers/ProfileController.php

@@ -86,6 +86,6 @@ class ProfileController extends Controller
             'email' => $request->input('email'),
         ]);
 
-        return redirect()->route('profile.index')->with('success' , 'profile updated');
+        return redirect()->route('profile.index')->with('success' , 'Profile updated');
     }
 }

+ 83 - 56
app/Http/Controllers/ServerController.php

@@ -12,11 +12,11 @@ use App\Models\Product;
 use App\Models\Server;
 use App\Notifications\ServerCreationError;
 use Exception;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Http\Client\Response;
 use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Request as FacadesRequest;
 
 class ServerController extends Controller
@@ -34,55 +34,34 @@ class ServerController extends Controller
     {
         if (!is_null($this->validateConfigurationRules())) return $this->validateConfigurationRules();
 
-        return view('servers.create')->with([
-            'products' => Product::where('disabled', '=', false)->orderBy('price', 'asc')->get(),
-            'locations' => Location::whereHas('nodes', function ($query) {
-                $query->where('disabled', '=', false);
-            })->get(),
-            'nests' => Nest::where('disabled', '=', false)->get(),
-            'minimum_credits' => Configuration::getValueByKey('MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER', 50)
-        ]);
-    }
-
-    /** Store a newly created resource in storage. */
-    public function store(Request $request)
-    {
-        if (!is_null($this->validateConfigurationRules())) return $this->validateConfigurationRules();
-
-        $request->validate([
-            "name" => "required|max:191",
-            "description" => "nullable|max:191",
-            "node_id" => "required|exists:nodes,id",
-            "egg_id" => "required|exists:eggs,id",
-            "product_id" => "required|exists:products,id"
-        ]);
+        $productCount = Product::query()->where('disabled', '=', false)->count();
+        $locations = Location::all();
 
-        //get required resources
-        $egg = Egg::findOrFail($request->input('egg_id'));
-        $node = Node::findOrFail($request->input('node_id'));
-        $server = Auth::user()->servers()->create($request->all());
+        $nodeCount = Node::query()
+            ->whereHas('products', function (Builder $builder) {
+                $builder->where('disabled', '=', false);
+            })->count();
 
-        //get free allocation ID
-        $allocationId = Pterodactyl::getFreeAllocationId($node);
-        if (!$allocationId) return $this->noAllocationsError($server);
+        $eggs = Egg::query()
+            ->whereHas('products', function (Builder $builder) {
+                $builder->where('disabled', '=', false);
+            })->get();
 
-        //create server on pterodactyl
-        $response = Pterodactyl::createServer($server, $egg, $allocationId);
-        if ($response->failed()) return $this->serverCreationFailed($response, $server);
+        $nests = Nest::query()
+            ->whereHas('eggs', function (Builder $builder) {
+                $builder->whereHas('products', function (Builder $builder) {
+                    $builder->where('disabled', '=', false);
+                });
+            })->get();
 
-        //update server with pterodactyl_id
-        $server->update([
-            'pterodactyl_id' => $response->json()['attributes']['id'],
-            'identifier' => $response->json()['attributes']['identifier']
+        return view('servers.create')->with([
+            'productCount' => $productCount,
+            'nodeCount'    => $nodeCount,
+            'nests'        => $nests,
+            'locations'    => $locations,
+            'eggs'         => $eggs,
+            'user'         => Auth::user(),
         ]);
-
-        if (Configuration::getValueByKey('SERVER_CREATE_CHARGE_FIRST_HOUR', 'true') == 'true') {
-            if (Auth::user()->credits >= $server->product->getHourlyPrice()) {
-                Auth::user()->decrement('credits', $server->product->getHourlyPrice());
-            }
-        }
-
-        return redirect()->route('servers.index')->with('success', 'server created');
     }
 
     /**
@@ -96,8 +75,8 @@ class ServerController extends Controller
         }
 
         // minimum credits
-        if (FacadesRequest::has("product_id")) {
-            $product = Product::findOrFail(FacadesRequest::input("product_id"));
+        if (FacadesRequest::has("product")) {
+            $product = Product::findOrFail(FacadesRequest::input("product"));
             if (
                 Auth::user()->credits <
                 ($product->minimum_credits == -1
@@ -121,17 +100,54 @@ class ServerController extends Controller
         return null;
     }
 
-    /** Remove the specified resource from storage. */
-    public function destroy(Server $server)
+    /** Store a newly created resource in storage. */
+    public function store(Request $request)
     {
-        try {
-            $server->delete();
-            return redirect()->route('servers.index')->with('success', 'server removed');
-        } catch (Exception $e) {
-            return redirect()->route('servers.index')->with('error', 'An exception has occurred while trying to remove a resource "' . $e->getMessage() . '"');
+        /** @var Node $node */
+        /** @var Egg $egg */
+        /** @var Product $product */
+
+        if (!is_null($this->validateConfigurationRules())) return $this->validateConfigurationRules();
+
+        $request->validate([
+            "name"    => "required|max:191",
+            "node"    => "required|exists:nodes,id",
+            "egg"     => "required|exists:eggs,id",
+            "product" => "required|exists:products,id"
+        ]);
+
+        //get required resources
+        $product = Product::query()->findOrFail($request->input('product'));
+        $egg = $product->eggs()->findOrFail($request->input('egg'));
+        $node = $product->nodes()->findOrFail($request->input('node'));
+
+        $server = $request->user()->servers()->create([
+            'name'       => $request->input('name'),
+            'product_id' => $request->input('product'),
+        ]);
+
+        //get free allocation ID
+        $allocationId = Pterodactyl::getFreeAllocationId($node);
+        if (!$allocationId) return $this->noAllocationsError($server);
+
+        //create server on pterodactyl
+        $response = Pterodactyl::createServer($server, $egg, $allocationId);
+        if ($response->failed()) return $this->serverCreationFailed($response, $server);
+
+        //update server with pterodactyl_id
+        $server->update([
+            'pterodactyl_id' => $response->json()['attributes']['id'],
+            'identifier'     => $response->json()['attributes']['identifier']
+        ]);
+
+        if (Configuration::getValueByKey('SERVER_CREATE_CHARGE_FIRST_HOUR', 'true') == 'true') {
+            if ($request->user()->credits >= $server->product->getHourlyPrice()) {
+                $request->user()->decrement('credits', $server->product->getHourlyPrice());
+            }
         }
-    }
 
+        return redirect()->route('servers.index')->with('success', 'Server created');
+    }
 
     /**
      * return redirect with error
@@ -143,7 +159,7 @@ class ServerController extends Controller
         $server->delete();
 
         Auth::user()->notify(new ServerCreationError($server));
-        return redirect()->route('servers.index')->with('error', 'No allocations satisfying the requirements for automatic deployment were found.');
+        return redirect()->route('servers.index')->with('error', 'No allocations satisfying the requirements for automatic deployment on this node were found.');
     }
 
     /**
@@ -158,4 +174,15 @@ class ServerController extends Controller
 
         return redirect()->route('servers.index')->with('error', json_encode($response->json()));
     }
+
+    /** Remove the specified resource from storage. */
+    public function destroy(Server $server)
+    {
+        try {
+            $server->delete();
+            return redirect()->route('servers.index')->with('success', 'Server removed');
+        } catch (Exception $e) {
+            return redirect()->route('servers.index')->with('error', 'An exception has occurred while trying to remove a resource "' . $e->getMessage() . '"');
+        }
+    }
 }

+ 17 - 3
app/Models/Configuration.php

@@ -4,11 +4,14 @@ namespace App\Models;
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Cache;
 
 class Configuration extends Model
 {
     use HasFactory;
 
+    public const CACHE_TAG = 'configuration';
+
     public $primaryKey = 'key';
 
     public $incrementing = false;
@@ -21,14 +24,25 @@ class Configuration extends Model
         'type',
     ];
 
+    public static function boot()
+    {
+        parent::boot();
+
+        static::updated(function (Configuration $configuration) {
+            Cache::forget(self::CACHE_TAG .':'. $configuration->key);
+        });
+    }
+
     /**
      * @param string $key
      * @param $default
      * @return mixed
      */
-    public static function getValueByKey(string $key , $default = null)
+    public static function getValueByKey(string $key, $default = null)
     {
-        $configuration = self::find($key);
-        return $configuration ? $configuration->value : $default;
+        return Cache::rememberForever(self::CACHE_TAG .':'. $key, function () use ($default, $key) {
+            $configuration = self::find($key);
+            return $configuration ? $configuration->value : $default;
+        });
     }
 }

+ 65 - 25
app/Models/Egg.php

@@ -6,6 +6,7 @@ use App\Classes\Pterodactyl;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
 class Egg extends Model
 {
@@ -21,38 +22,26 @@ class Egg extends Model
         'docker_image',
         'startup',
         'environment',
+        'updated_at',
     ];
 
-    /**
-     * @return BelongsTo
-     */
-    public function nest()
+    public static function boot()
     {
-        return $this->belongsTo(Nest::class, 'id', 'nest_id');
-    }
-
-    /**
-     * @return array
-     */
-    public function getEnvironmentVariables()
-    {
-        $array = [];
-
-        foreach (json_decode($this->environment) as $variable) {
-            foreach ($variable as $key => $value){
-                $array[$key] = $value;
-            }
-        }
+        parent::boot(); // TODO: Change the autogenerated stub
 
-        return $array;
+        static::deleting(function (Egg $egg) {
+            $egg->products()->detach();
+        });
     }
 
-    public static function syncEggs(){
+    public static function syncEggs()
+    {
+        Nest::syncNests();
 
-         Nest::all()->each(function (Nest $nest) {
+        Nest::all()->each(function (Nest $nest) {
             $eggs = Pterodactyl::getEggs($nest);
 
-            foreach ($eggs as $egg){
+            foreach ($eggs as $egg) {
                 $array = [];
                 $environment = [];
 
@@ -62,19 +51,70 @@ class Egg extends Model
                 $array['description'] = $egg['attributes']['description'];
                 $array['docker_image'] = $egg['attributes']['docker_image'];
                 $array['startup'] = $egg['attributes']['startup'];
+                $array['updated_at'] = now();
 
                 //get environment variables
-                foreach ($egg['attributes']['relationships']['variables']['data'] as $variable){
+                foreach ($egg['attributes']['relationships']['variables']['data'] as $variable) {
                     $environment[$variable['attributes']['env_variable']] = $variable['attributes']['default_value'];
                 }
 
                 $array['environment'] = json_encode([$environment]);
 
-                self::firstOrCreate(['id' => $array['id']] , $array);
+                self::query()->updateOrCreate([
+                    'id' => $array['id']
+                ], array_diff_key($array, array_flip(["id"]))
+                );
             }
 
+            self::removeDeletedEggs($nest, $eggs);
+        });
+    }
+
+    /**
+     * @description remove eggs that have been deleted on pterodactyl
+     * @param Nest $nest
+     * @param array $eggs
+     */
+    private static function removeDeletedEggs(Nest $nest, array $eggs): void
+    {
+        $ids = array_map(function ($data) {
+            return $data['attributes']['id'];
+        }, $eggs);
+
+        $nest->eggs()->each(function (Egg $egg) use ($ids) {
+            if (!in_array($egg->id, $ids)) $egg->delete();
         });
+    }
+
+    /**
+     * @return array
+     */
+    public function getEnvironmentVariables()
+    {
+        $array = [];
+
+        foreach (json_decode($this->environment) as $variable) {
+            foreach ($variable as $key => $value) {
+                $array[$key] = $value;
+            }
+        }
 
+        return $array;
+    }
 
+    /**
+     * @return BelongsTo
+     */
+    public function nest()
+    {
+        return $this->belongsTo(Nest::class);
+    }
+
+    /**
+     * @return BelongsToMany
+     */
+    public function products()
+    {
+        return $this->belongsToMany(Product::class);
     }
 }

+ 47 - 7
app/Models/Location.php

@@ -15,28 +15,68 @@ class Location extends Model
 
     public $guarded = [];
 
+    public static function boot()
+    {
+        parent::boot(); // TODO: Change the autogenerated stub
 
-    public function nodes(){
-        return $this->hasMany(Node::class , 'location_id' , 'id');
+        static::deleting(function (Location $location) {
+            $location->nodes()->each(function (Node $node) {
+                $node->delete();
+            });
+        });
     }
 
     /**
      * Sync locations with pterodactyl panel
      * @throws Exception
      */
-    public static function syncLocations(){
+    public static function syncLocations()
+    {
         $locations = Pterodactyl::getLocations();
 
-        $locations = array_map(function($val) {
+        //map response
+        $locations = array_map(function ($val) {
             return array(
-                'id' => $val['attributes']['id'],
-                'name' => $val['attributes']['short'],
+                'id'          => $val['attributes']['id'],
+                'name'        => $val['attributes']['short'],
                 'description' => $val['attributes']['long']
             );
         }, $locations);
 
+        //update or create
         foreach ($locations as $location) {
-            self::firstOrCreate(['id' => $location['id']] , $location);
+            self::query()->updateOrCreate(
+                [
+                    'id' => $location['id']
+                ],
+                [
+                    'name'        => $location['name'],
+                    'description' => $location['name'],
+                ]
+            );
         }
+
+        self::removeDeletedLocation($locations);
     }
+
+    /**
+     * @description remove locations that have been deleted on pterodactyl
+     * @param array $locations
+     */
+    private static function removeDeletedLocation(array $locations): void
+    {
+        $ids = array_map(function ($data) {
+            return $data['id'];
+        }, $locations);
+
+        self::all()->each(function (Location $location) use ($ids) {
+            if (!in_array($location->id, $ids)) $location->delete();
+        });
+    }
+
+    public function nodes()
+    {
+        return $this->hasMany(Node::class, 'location_id', 'id');
+    }
+
 }

+ 48 - 5
app/Models/Nest.php

@@ -19,17 +19,60 @@ class Nest extends Model
         'disabled',
     ];
 
+    public static function boot()
+    {
+        parent::boot(); // TODO: Change the autogenerated stub
 
-    public function eggs(){
-        return $this->hasMany(Egg::class);
+        static::deleting(function (Nest $nest) {
+            $nest->eggs()->each(function (Egg $egg) {
+                $egg->delete();
+            });
+        });
     }
 
-    public static function syncNests(){
-        self::query()->delete();
+    public static function syncNests()
+    {
         $nests = Pterodactyl::getNests();
 
+        //map response
+        $nests = array_map(function ($nest) {
+            return array(
+                'id'          => $nest['attributes']['id'],
+                'name'        => $nest['attributes']['name'],
+                'description' => $nest['attributes']['description'],
+            );
+        }, $nests);
+
         foreach ($nests as $nest) {
-            self::firstOrCreate(['id' => $nest['attributes']['id']] , array_merge($nest['attributes'] , ['disabled' => '1']));
+            self::query()->updateOrCreate([
+                'id' => $nest['id']
+            ], [
+                'name'        => $nest['name'],
+                'description' => $nest['description'],
+                'disabled'    => false
+            ]);
         }
+
+        self::removeDeletedNests($nests);
+    }
+
+    /**
+     * @description remove nests that have been deleted on pterodactyl
+     * @param array $nests
+     */
+    private static function removeDeletedNests(array $nests): void
+    {
+        $ids = array_map(function ($data) {
+            return $data['id'];
+        }, $nests);
+
+        self::all()->each(function (Nest $nest) use ($ids) {
+            if (!in_array($nest->id, $ids)) $nest->delete();
+        });
+    }
+
+    public function eggs()
+    {
+        return $this->hasMany(Egg::class);
     }
 }

+ 57 - 13
app/Models/Node.php

@@ -7,6 +7,7 @@ use Exception;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
 class Node extends Model
 {
@@ -16,36 +17,79 @@ class Node extends Model
 
     public $guarded = [];
 
-
-    /**
-     * @return BelongsTo
-     */
-    public function location(): BelongsTo
+    public static function boot()
     {
-        return $this->belongsTo(Location::class);
+        parent::boot(); // TODO: Change the autogenerated stub
+
+        static::deleting(function (Node $node) {
+            $node->products()->detach();
+        });
     }
 
+
     /**
      * @throws Exception
      */
-    public static function syncNodes(){
+    public static function syncNodes()
+    {
         Location::syncLocations();
         $nodes = Pterodactyl::getNodes();
 
-
-        $nodes = array_map(function($node) {
+        //map response
+        $nodes = array_map(function ($node) {
             return array(
-                'id' => $node['attributes']['id'],
+                'id'          => $node['attributes']['id'],
                 'location_id' => $node['attributes']['location_id'],
-                'name' => $node['attributes']['name'],
+                'name'        => $node['attributes']['name'],
                 'description' => $node['attributes']['description'],
-                'disabled' => '1'
             );
         }, $nodes);
 
+        //update or create
         foreach ($nodes as $node) {
-            self::firstOrCreate(['id' => $node['id']] , $node);
+            self::query()->updateOrCreate(
+                [
+                    'id' => $node['id']
+                ],
+                [
+                    'name'        => $node['name'],
+                    'description' => $node['description'],
+                    'location_id' => $node['location_id'],
+                    'disabled'    => false
+                ]);
         }
 
+        self::removeDeletedNodes($nodes);
+    }
+
+    /**
+     * @description remove nodes that have been deleted on pterodactyl
+     * @param array $nodes
+     */
+    private static function removeDeletedNodes(array $nodes): void
+    {
+        $ids = array_map(function ($data) {
+            return $data['id'];
+        }, $nodes);
+
+        self::all()->each(function (Node $node) use ($ids) {
+            if (!in_array($node->id, $ids)) $node->delete();
+        });
+    }
+
+    /**
+     * @return BelongsTo
+     */
+    public function location(): BelongsTo
+    {
+        return $this->belongsTo(Location::class);
+    }
+
+    /**
+     * @return BelongsToMany
+     */
+    public function products()
+    {
+        return $this->belongsToMany(Product::class);
     }
 }

+ 11 - 2
app/Models/Payment.php

@@ -29,6 +29,9 @@ class Payment extends Model
         'type',
         'amount',
         'price',
+        'tax_value',
+        'total_price',
+        'tax_percent',
         'currency_code',
     ];
 
@@ -51,9 +54,15 @@ class Payment extends Model
         return $this->belongsTo(User::class);
     }
 
-    public function formatCurrency($locale = 'en_US')
+    /**
+     * @param mixed $value
+     * @param string $locale
+     * 
+     * @return float
+     */
+    public function formatToCurrency($value,$locale = 'en_US')
     {
         $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
-        return $formatter->formatCurrency($this->price, $this->currency_code);
+        return $formatter->formatCurrency($value, $this->currency_code);
     }
 }

+ 37 - 3
app/Models/PaypalProduct.php

@@ -6,6 +6,7 @@ use Hidehalo\Nanoid\Client;
 use Illuminate\Database\Eloquent\Model;
 use NumberFormatter;
 use Spatie\Activitylog\Traits\LogsActivity;
+use App\Models\Configuration;
 
 class PaypalProduct extends Model
 {
@@ -40,12 +41,45 @@ class PaypalProduct extends Model
     }
 
     /**
+     * @param mixed $value
      * @param string $locale
-     * @return string
+     * 
+     * @return float
      */
-    public function formatCurrency($locale = 'en_US')
+    public function formatToCurrency($value,$locale = 'en_US')
     {
         $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
-        return $formatter->formatCurrency($this->price, $this->currency_code);
+        return $formatter->formatCurrency($value, $this->currency_code);
+    }
+
+    /**
+    * @description Returns the tax in % taken from the Configuration
+    *
+    * @return int
+    */
+    public function getTaxPercent()
+    {
+        $tax = Configuration::getValueByKey("SALES_TAX");
+        return $tax < 0 ? 0 : $tax;
+    }
+
+    /**
+    * @description Returns the tax as Number
+    *
+    * @return float
+    */
+    public function getTaxValue()
+    {
+        return $this->price*$this->getTaxPercent()/100;
+    }
+
+    /**
+    * @description Returns the full price of a Product including tax
+    *
+    * @return float
+    */
+    public function getTotalPrice() 
+    {
+        return $this->price+($this->getTaxValue());
     }
 }

+ 24 - 2
app/Models/Product.php

@@ -6,11 +6,14 @@ use Hidehalo\Nanoid\Client;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Database\Eloquent\Relations\HasMany;
 use Spatie\Activitylog\Traits\LogsActivity;
 
 class Product extends Model
 {
-    use HasFactory, LogsActivity;
+    use HasFactory;
+    use LogsActivity;
 
     public $incrementing = false;
 
@@ -25,6 +28,11 @@ class Product extends Model
 
             $product->{$product->getKeyName()} = $client->generateId($size = 21);
         });
+
+        static::deleting(function(Product $product) {
+            $product->nodes()->detach();
+            $product->eggs()->detach();
+        });
     }
 
     public function getHourlyPrice()
@@ -45,8 +53,22 @@ class Product extends Model
     /**
      * @return BelongsTo
      */
-    public function servers(): BelongsTo
+    public function servers()
     {
         return $this->belongsTo(Server::class, 'id', 'product_id');
     }
+
+    /**
+     * @return BelongsToMany
+     */
+    public function eggs() {
+        return $this->belongsToMany(Egg::class);
+    }
+
+    /**
+     * @return BelongsToMany
+     */
+    public function nodes() {
+        return $this->belongsToMany(Node::class);
+    }
 }

+ 3 - 3
app/Models/User.php

@@ -80,9 +80,9 @@ class User extends Authenticatable implements MustVerifyEmail
      */
     protected $casts = [
         'email_verified_at' => 'datetime',
-        'last_seen'         => 'datetime',
-        'credits'           => 'float',
-        'server_limit'      => 'float',
+        'last_seen' => 'datetime',
+        'credits' => 'float',
+        'server_limit' => 'float',
     ];
 
     /**

+ 1 - 0
composer.json

@@ -28,6 +28,7 @@
         "yajra/laravel-datatables-oracle": "~9.0"
     },
     "require-dev": {
+        "barryvdh/laravel-debugbar": "^3.6",
         "facade/ignition": "^2.5",
         "fakerphp/faker": "^1.9.1",
         "laravel/sail": "^1.0.1",

+ 220 - 2
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "f7ba581ff6641d3ab79d558070e99f3c",
+    "content-hash": "500346cc4a4a83b162e07bb0071d1602",
     "packages": [
         {
             "name": "asm89/stack-cors",
@@ -6202,6 +6202,91 @@
         }
     ],
     "packages-dev": [
+        {
+            "name": "barryvdh/laravel-debugbar",
+            "version": "v3.6.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/barryvdh/laravel-debugbar.git",
+                "reference": "3c2d678269ba60e178bcd93e36f6a91c36b727f1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/3c2d678269ba60e178bcd93e36f6a91c36b727f1",
+                "reference": "3c2d678269ba60e178bcd93e36f6a91c36b727f1",
+                "shasum": ""
+            },
+            "require": {
+                "illuminate/routing": "^6|^7|^8",
+                "illuminate/session": "^6|^7|^8",
+                "illuminate/support": "^6|^7|^8",
+                "maximebf/debugbar": "^1.17.2",
+                "php": ">=7.2",
+                "symfony/debug": "^4.3|^5",
+                "symfony/finder": "^4.3|^5"
+            },
+            "require-dev": {
+                "mockery/mockery": "^1.3.3",
+                "orchestra/testbench-dusk": "^4|^5|^6",
+                "phpunit/phpunit": "^8.5|^9.0",
+                "squizlabs/php_codesniffer": "^3.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.6-dev"
+                },
+                "laravel": {
+                    "providers": [
+                        "Barryvdh\\Debugbar\\ServiceProvider"
+                    ],
+                    "aliases": {
+                        "Debugbar": "Barryvdh\\Debugbar\\Facade"
+                    }
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Barryvdh\\Debugbar\\": "src/"
+                },
+                "files": [
+                    "src/helpers.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Barry vd. Heuvel",
+                    "email": "barryvdh@gmail.com"
+                }
+            ],
+            "description": "PHP Debugbar integration for Laravel",
+            "keywords": [
+                "debug",
+                "debugbar",
+                "laravel",
+                "profiler",
+                "webprofiler"
+            ],
+            "support": {
+                "issues": "https://github.com/barryvdh/laravel-debugbar/issues",
+                "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.6.4"
+            },
+            "funding": [
+                {
+                    "url": "https://fruitcake.nl",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/barryvdh",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-10-21T10:57:31+00:00"
+        },
         {
             "name": "doctrine/instantiator",
             "version": "1.4.0",
@@ -6713,6 +6798,71 @@
             },
             "time": "2021-05-25T16:41:13+00:00"
         },
+        {
+            "name": "maximebf/debugbar",
+            "version": "v1.17.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/maximebf/php-debugbar.git",
+                "reference": "e8ac3499af0ea5b440908e06cc0abe5898008b3c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/e8ac3499af0ea5b440908e06cc0abe5898008b3c",
+                "reference": "e8ac3499af0ea5b440908e06cc0abe5898008b3c",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1|^8",
+                "psr/log": "^1|^2|^3",
+                "symfony/var-dumper": "^2.6|^3|^4|^5"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.5.20 || ^9.4.2"
+            },
+            "suggest": {
+                "kriswallsmith/assetic": "The best way to manage assets",
+                "monolog/monolog": "Log using Monolog",
+                "predis/predis": "Redis storage"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.17-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "DebugBar\\": "src/DebugBar/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Maxime Bouroumeau-Fuseau",
+                    "email": "maxime.bouroumeau@gmail.com",
+                    "homepage": "http://maximebf.com"
+                },
+                {
+                    "name": "Barry vd. Heuvel",
+                    "email": "barryvdh@gmail.com"
+                }
+            ],
+            "description": "Debug bar in the browser for php application",
+            "homepage": "https://github.com/maximebf/php-debugbar",
+            "keywords": [
+                "debug",
+                "debugbar"
+            ],
+            "support": {
+                "issues": "https://github.com/maximebf/php-debugbar/issues",
+                "source": "https://github.com/maximebf/php-debugbar/tree/v1.17.3"
+            },
+            "time": "2021-10-19T12:33:27+00:00"
+        },
         {
             "name": "mockery/mockery",
             "version": "1.4.3",
@@ -8652,6 +8802,74 @@
             ],
             "time": "2020-09-28T06:39:44+00:00"
         },
+        {
+            "name": "symfony/debug",
+            "version": "v4.4.31",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/debug.git",
+                "reference": "43ede438d4cb52cd589ae5dc070e9323866ba8e0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/43ede438d4cb52cd589ae5dc070e9323866ba8e0",
+                "reference": "43ede438d4cb52cd589ae5dc070e9323866ba8e0",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "psr/log": "^1|^2|^3"
+            },
+            "conflict": {
+                "symfony/http-kernel": "<3.4"
+            },
+            "require-dev": {
+                "symfony/http-kernel": "^3.4|^4.0|^5.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Debug\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides tools to ease debugging PHP code",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/debug/tree/v4.4.31"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-09-24T13:30:14+00:00"
+        },
         {
             "name": "theseer/tokenizer",
             "version": "1.2.0",
@@ -8713,5 +8931,5 @@
         "ext-intl": "*"
     },
     "platform-dev": [],
-    "plugin-api-version": "2.1.0"
+    "plugin-api-version": "2.0.0"
 }

+ 32 - 0
database/migrations/2021_07_06_152319_create_egg_product_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateEggProductTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('egg_product', function (Blueprint $table) {
+            $table->foreignId('egg_id')->constrained();
+            $table->foreignUuid('product_id')->constrained();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('egg_product');
+    }
+}

+ 32 - 0
database/migrations/2021_07_06_154314_create_node_product_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateNodeProductTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('node_product', function (Blueprint $table) {
+            $table->foreignId('node_id')->constrained();
+            $table->foreignUuid('product_id')->constrained();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('node_product');
+    }
+}

+ 32 - 0
database/migrations/2021_07_06_154658_add_disabled_to_eggs_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddDisabledToEggsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('eggs', function (Blueprint $table) {
+            $table->boolean('disabled')->default(false);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('eggs', function (Blueprint $table) {
+            $table->dropColumn('disabled');
+        });
+    }
+}

+ 36 - 0
database/migrations/2021_11_05_071456_add_tax_to_paymentlogs.php

@@ -0,0 +1,36 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddTaxToPaymentlogs extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('payments', function (Blueprint $table) {
+            $table->decimal('tax_value',8,2)->after('price')->nullable();
+            $table->integer('tax_percent')->after('tax_value')->nullable();
+            $table->decimal('total_price',8,2)->after('tax_percent')->nullable();    
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('payments', function (Blueprint $table) {
+            $table->dropColumn('tax_value');
+            $table->dropColumn('tax_percent');
+            $table->dropColumn('total_price');
+        });
+    }
+}

+ 8 - 0
database/seeders/Seeds/ConfigurationSeeder.php

@@ -136,6 +136,14 @@ class ConfigurationSeeder extends Seeder
             'type'        => 'boolean',
             'description' => 'Charges the first hour worth of credits upon creating a server.'
         ]);
+        //sales tax
+        Configuration::firstOrCreate([
+            'key'   => 'SALES_TAX',
+        ], [
+            'value' => '0',
+            'type'  => 'integer',
+            'description'  => 'The %-value of tax that will be added to the product price on checkout'
+        ]);
 
     }
 }

Файловите разлики са ограничени, защото са твърде много
+ 13803 - 1
package-lock.json


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
public/css/app.css.map


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
public/js/app.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
public/js/app.js.map


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
resources/js/alpine.js


+ 1 - 0
resources/js/app.js

@@ -3,3 +3,4 @@ require('./slim.kickstart.min')
 require('./bootstrap');
 
 
+

+ 4 - 4
resources/js/bootstrap.js

@@ -18,10 +18,10 @@ try {
  * to our Laravel back-end. This library automatically handles sending the
  * CSRF token as a header based on the value of the "XSRF" token cookie.
  */
-//
-// window.axios = require('axios');
-//
-// window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
+
+window.axios = require('axios');
+
+window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
 
 /**
  * Echo exposes an expressive API for subscribing to channels and listening

+ 153 - 0
resources/views/admin/overview/index.blade.php

@@ -0,0 +1,153 @@
+@extends('layouts.main')
+
+@section('content')
+    <!-- CONTENT HEADER -->
+    <section class="content-header">
+        <div class="container-fluid">
+            <div class="row mb-2">
+                <div class="col-sm-6">
+                    <h1>Admin Overview</h1>
+                </div>
+                <div class="col-sm-6">
+                    <ol class="breadcrumb float-sm-right">
+                        <li class="breadcrumb-item"><a href="{{route('home')}}">Dashboard</a></li>
+                        <li class="breadcrumb-item"><a class="text-muted"
+                                                       href="{{route('admin.overview.index')}}">Admin Overview</a></li>
+                    </ol>
+                </div>
+            </div>
+        </div>
+    </section>
+    <!-- END CONTENT HEADER -->
+
+    <!-- MAIN CONTENT -->
+    <section class="content">
+        <div class="container-fluid">
+
+            <div class="row mb-3">
+                <div class="col-md-3">
+                    <a href="https://discord.gg/4Y6HjD2uyU" class="btn btn-dark btn-block px-3"><i
+                            class="fab fa-discord mr-2"></i> {{__('Support server')}}</a>
+                </div>
+                <div class="col-md-3">
+                    <a href="https://controlpanel.gg/docs/intro" class="btn btn-dark btn-block px-3"><i
+                            class="fas fa-link mr-2"></i> {{__('Documentation')}}</a>
+                </div>
+                <div class="col-md-3">
+                    <a href="https://github.com/ControlPanel-gg/dashboard" class="btn btn-dark btn-block px-3"><i
+                            class="fab fa-github mr-2"></i> {{__('Github')}}</a>
+                </div>
+                <div class="col-md-3">
+                    <a href="https://controlpanel.gg/docs/Contributing/donating" class="btn btn-dark btn-block px-3"><i
+                            class="fas fa-money-bill mr-2"></i> {{__('Support ControlPanel')}}</a>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-12 col-sm-6 col-md-3">
+                    <div class="info-box">
+                        <span class="info-box-icon bg-info elevation-1"><i class="fas fa-server"></i></span>
+
+                        <div class="info-box-content">
+                            <span class="info-box-text">{{__('Servers')}}</span>
+                            <span class="info-box-number">{{$serverCount}}</span>
+                        </div>
+                        <!-- /.info-box-content -->
+                    </div>
+                    <!-- /.info-box -->
+                </div>
+
+                <div class="col-12 col-sm-6 col-md-3">
+                    <div class="info-box">
+                        <span class="info-box-icon bg-primary elevation-1"><i class="fas fa-users"></i></span>
+
+                        <div class="info-box-content">
+                            <span class="info-box-text">{{__('Users')}}</span>
+                            <span class="info-box-number">{{$userCount}}</span>
+                        </div>
+                        <!-- /.info-box-content -->
+                    </div>
+                    <!-- /.info-box -->
+                </div>
+
+                <div class="col-12 col-sm-6 col-md-3">
+                    <div class="info-box">
+                        <span class="info-box-icon bg-warning elevation-1"><i
+                                class="fas fa-coins text-white"></i></span>
+
+                        <div class="info-box-content">
+                            <span class="info-box-text">{{__('Total')}} {{CREDITS_DISPLAY_NAME}}</span>
+                            <span class="info-box-number">{{$creditCount}}</span>
+                        </div>
+                        <!-- /.info-box-content -->
+                    </div>
+                    <!-- /.info-box -->
+                </div>
+
+                <div class="col-12 col-sm-6 col-md-3">
+                    <div class="info-box">
+                        <span class="info-box-icon bg-success elevation-1"><i class="fas fa-money-bill"></i></span>
+
+                        <div class="info-box-content">
+                            <span class="info-box-text">{{__('Payments')}}</span>
+                            <span class="info-box-number">{{$paymentCount}}</span>
+                        </div>
+                        <!-- /.info-box-content -->
+                    </div>
+                    <!-- /.info-box -->
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="card">
+                        <div class="card-header">
+                            <div class="d-flex justify-content-between">
+                                <div class="card-title ">
+                                    <span><i class="fas fa-kiwi-bird mr-2"></i>{{__('Pterodactyl')}}</span>
+                                </div>
+                                <a href="{{route('admin.overview.sync')}}" class="btn btn-primary btn-sm"><i
+                                        class="fas fa-sync mr-2"></i>{{__('Sync')}}</a>
+                            </div>
+                        </div>
+                        <div class="card-body py-1">
+                            <table class="table">
+                                <thead>
+                                <tr>
+                                    <th>{{__('Resources')}}</th>
+                                    <th>{{__('Count')}}</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                <tr>
+                                    <td>{{__('Locations')}}</td>
+                                    <td>{{$locationCount}}</td>
+                                </tr>
+                                <tr>
+                                    <td>{{__('Nodes')}}</td>
+                                    <td>{{$nodeCount}}</td>
+                                </tr>
+                                <tr>
+                                    <td>{{__('Nests')}}</td>
+                                    <td>{{$nestCount}}</td>
+                                </tr>
+                                <tr>
+                                    <td>{{__('Eggs')}}</td>
+                                    <td>{{$eggCount}}</td>
+                                </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                        <div class="card-footer">
+                            <span><i class="fas fa-sync mr-2"></i>{{__('Last updated :date', ['date' => $syncLastUpdate])}}</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+        </div>
+        <!-- END CUSTOM CONTENT -->
+
+    </section>
+    <!-- END CONTENT -->
+@endsection

+ 7 - 1
resources/views/admin/payments/index.blade.php

@@ -37,7 +37,10 @@
                             <th>User</th>
                             <th>Type</th>
                             <th>Amount</th>
-                            <th>Price</th>
+                            <th>Product Price</th>
+                            <th>Tax</th>
+                            <th>Tax(%)</th>
+                            <th>Total Price</th>
                             <th>Payment_ID</th>
                             <th>Payer_ID</th>
                             <th>Created at</th>
@@ -68,6 +71,9 @@
                     {data: 'type'},
                     {data: 'amount'},
                     {data: 'price'},
+                    {data: 'tax_value'},
+                    {data: 'tax_percent'},
+                    {data: 'total_price'},
                     {data: 'payment_id'},
                     {data: 'payer_id'},
                     {data: 'created_at'},

+ 179 - 90
resources/views/admin/products/create.blade.php

@@ -13,7 +13,7 @@
                         <li class="breadcrumb-item"><a href="{{ route('home') }}">Dashboard</a></li>
                         <li class="breadcrumb-item"><a href="{{ route('admin.products.index') }}">Products</a></li>
                         <li class="breadcrumb-item"><a class="text-muted"
-                                href="{{ route('admin.products.create') }}">Create</a>
+                                                       href="{{ route('admin.products.create') }}">Create</a>
                         </li>
                     </ol>
                 </div>
@@ -25,19 +25,22 @@
     <!-- MAIN CONTENT -->
     <section class="content">
         <div class="container-fluid">
+            <form action="{{route('admin.products.store')}}" method="POST">
+                @csrf
+                <div class="row">
+                    <div class="col-lg-6">
+                        <div class="card">
+                            <div class="card-header">
+                                <h5 class="card-title">Product Details</h5>
+                            </div>
+                            <div class="card-body">
 
-            <div class="row">
-                <div class="col-lg-6">
-                    <div class="card">
-                        <div class="card-body">
-                            <form action="{{ route('admin.products.store') }}" method="POST">
-                                @csrf
                                 <div class="d-flex flex-row-reverse">
                                     <div class="custom-control custom-switch">
                                         <input type="checkbox" name="disabled"
-                                            class="custom-control-input custom-control-input-danger" id="switch1">
-                                        <label class="custom-control-label" for="switch1">Disabled <i data-toggle="popover"
-                                                data-trigger="hover"
+                                               class="custom-control-input custom-control-input-danger" id="switch1">
+                                        <label class="custom-control-label" for="switch1">Disabled <i
+                                                data-toggle="popover" data-trigger="hover"
                                                 data-content="Will hide this option from being selected"
                                                 class="fas fa-info-circle"></i></label>
                                     </div>
@@ -47,75 +50,84 @@
                                     <div class="col-lg-6">
                                         <div class="form-group">
                                             <label for="name">Name</label>
-                                            <input value="{{ old('name') }}" id="name" name="name" type="text"
-                                                class="form-control @error('name') is-invalid @enderror"
-                                                required="required">
+                                            <input value="{{$product->name ?? old('name')}}" id="name" name="name"
+                                                   type="text"
+                                                   class="form-control @error('name') is-invalid @enderror"
+                                                   required="required">
                                             @error('name')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
 
                                         <div class="form-group">
-                                            <label for="price">Price in {{ CREDITS_DISPLAY_NAME }}</label>
-                                            <input value="{{ old('price') }}" id="price" name="price" type="number"
-                                                class="form-control @error('price') is-invalid @enderror"
-                                                required="required">
+                                            <label for="price">Price in credits</label>
+                                            <input value="{{$product->price ??  old('price')}}" id="price" name="price"
+                                                   type="number"
+                                                   class="form-control @error('price') is-invalid @enderror"
+                                                   required="required">
                                             @error('price')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
 
 
                                         <div class="form-group">
                                             <label for="memory">Memory</label>
-                                            <input value="{{ old('memory') }}" id="memory" name="memory" type="number"
-                                                class="form-control @error('memory') is-invalid @enderror"
-                                                required="required">
+                                            <input value="{{$product->memory ?? old('memory')}}" id="memory"
+                                                   name="memory"
+                                                   type="number"
+                                                   class="form-control @error('memory') is-invalid @enderror"
+                                                   required="required">
                                             @error('memory')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
 
                                         <div class="form-group">
                                             <label for="cpu">Cpu</label>
-                                            <input value="{{ old('cpu') }}" id="cpu" name="cpu" type="number"
-                                                class="form-control @error('cpu') is-invalid @enderror" required="required">
+                                            <input value="{{$product->cpu ?? old('cpu')}}" id="cpu" name="cpu"
+                                                   type="number"
+                                                   class="form-control @error('cpu') is-invalid @enderror"
+                                                   required="required">
                                             @error('cpu')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
 
                                         <div class="form-group">
                                             <label for="swap">Swap</label>
-                                            <input value="{{ old('swap') }}" id="swap" name="swap" type="number"
-                                                class="form-control @error('swap') is-invalid @enderror"
-                                                required="required">
+                                            <input value="{{$product->swap ?? old('swap')}}" id="swap" name="swap"
+                                                   type="number"
+                                                   class="form-control @error('swap') is-invalid @enderror"
+                                                   required="required">
                                             @error('swap')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
 
                                         <div class="form-group">
                                             <label for="description">Description <i data-toggle="popover"
-                                                    data-trigger="hover" data-content="This is what the users sees"
-                                                    class="fas fa-info-circle"></i></label>
-                                            <textarea id="description" name="description" type="text"
-                                                class="form-control @error('description') is-invalid @enderror"
-                                                required="required">{{ old('description') }}</textarea>
+                                                                                    data-trigger="hover"
+                                                                                    data-content="This is what the users sees"
+                                                                                    class="fas fa-info-circle"></i></label>
+                                            <textarea id="description" name="description"
+                                                      type="text"
+                                                      class="form-control @error('description') is-invalid @enderror"
+                                                      required="required">{{$product->description ?? old('description')}}</textarea>
                                             @error('description')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
 
@@ -123,13 +135,15 @@
                                     <div class="col-lg-6">
                                         <div class="form-group">
                                             <label for="disk">Disk</label>
-                                            <input value="{{ old('disk') ?? 1000 }}" id="disk" name="disk" type="number"
-                                                class="form-control @error('disk') is-invalid @enderror"
-                                                required="required">
+                                            <input value="{{$product->disk ?? old('disk') ?? 1000}}" id="disk"
+                                                   name="disk"
+                                                   type="number"
+                                                   class="form-control @error('disk') is-invalid @enderror"
+                                                   required="required">
                                             @error('disk')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
 
@@ -138,59 +152,67 @@
                                                     data-toggle="popover" data-trigger="hover"
                                                     data-content="Setting to -1 will use the value from configuration."
                                                     class="fas fa-info-circle"></i></label>
-                                            <input value="{{ old('minimum_credits') ?? -1 }}" id="minimum_credits"
-                                                name="minimum_credits" type="number"
-                                                class="form-control @error('minimum_credits') is-invalid @enderror"
-                                                required="required">
+                                            <input value="{{ $product->minimum_credits ?? old('minimum_credits') ?? -1 }}" id="minimum_credits"
+                                                   name="minimum_credits" type="number"
+                                                   class="form-control @error('minimum_credits') is-invalid @enderror"
+                                                   required="required">
                                             @error('minimum_credits')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
 
                                         <div class="form-group">
                                             <label for="io">IO</label>
-                                            <input value="{{ old('io') ?? 500 }}" id="io" name="io" type="number"
-                                                class="form-control @error('io') is-invalid @enderror" required="required">
+                                            <input value="{{$product->io ?? old('io') ?? 500}}" id="io" name="io"
+                                                   type="number"
+                                                   class="form-control @error('io') is-invalid @enderror"
+                                                   required="required">
                                             @error('io')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
                                         <div class="form-group">
                                             <label for="databases">Databases</label>
-                                            <input value="{{ old('databases') ?? 1 }}" id="databases" name="databases"
-                                                type="number" class="form-control @error('databases') is-invalid @enderror"
-                                                required="required">
+                                            <input value="{{$product->databases ?? old('databases') ?? 1}}"
+                                                   id="databases"
+                                                   name="databases"
+                                                   type="number"
+                                                   class="form-control @error('databases') is-invalid @enderror"
+                                                   required="required">
                                             @error('databases')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
                                         <div class="form-group">
                                             <label for="backups">Backups</label>
-                                            <input value="{{ old('backups') ?? 1 }}" id="backups" name="backups"
-                                                type="number" class="form-control @error('backups') is-invalid @enderror"
-                                                required="required">
+                                            <input value="{{$product->backups ?? old('backups') ?? 1}}" id="backups"
+                                                   name="backups"
+                                                   type="number"
+                                                   class="form-control @error('backups') is-invalid @enderror"
+                                                   required="required">
                                             @error('backups')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
                                         <div class="form-group">
                                             <label for="allocations">Allocations</label>
-                                            <input value="{{ old('allocations') ?? 0 }}" id="allocations"
-                                                name="allocations" type="number"
-                                                class="form-control @error('allocations') is-invalid @enderror"
-                                                required="required">
+                                            <input value="{{$product->allocations ?? old('allocations') ?? 0}}"
+                                                   id="allocations" name="allocations"
+                                                   type="number"
+                                                   class="form-control @error('allocations') is-invalid @enderror"
+                                                   required="required">
                                             @error('allocations')
-                                                <div class="invalid-feedback">
-                                                    {{ $message }}
-                                                </div>
+                                            <div class="invalid-feedback">
+                                                {{ $message }}
+                                            </div>
                                             @enderror
                                         </div>
                                     </div>
@@ -201,21 +223,88 @@
                                         Submit
                                     </button>
                                 </div>
-                            </form>
+
+                            </div>
                         </div>
                     </div>
+
+                    <div class="col-lg-6">
+                        <div class="card">
+                            <div class="card-header">
+                                <h5 class="card-title">Product Linking
+                                    <i data-toggle="popover"
+                                       data-trigger="hover"
+                                       data-content="Link your products to nodes and eggs to create dynamic pricing for each option"
+                                       class="fas fa-info-circle"></i></h5>
+                            </div>
+                            <div class="card-body">
+
+                                <div class="form-group">
+                                    <label for="nodes">Nodes</label>
+                                    <select id="nodes" style="width:100%"
+                                            class="custom-select @error('nodes') is-invalid @enderror"
+                                            name="nodes[]" multiple="multiple" autocomplete="off">
+                                        @foreach($locations as $location)
+                                            <optgroup label="{{$location->name}}">
+                                                @foreach($location->nodes as $node)
+                                                    <option
+                                                        @if(isset($product)) @if($product->nodes->contains('id' , $node->id)) selected
+                                                        @endif @endif value="{{$node->id}}">{{$node->name}}</option>
+                                                @endforeach
+                                            </optgroup>
+                                        @endforeach
+                                    </select>
+                                    @error('nodes')
+                                    <div class="text-danger">
+                                        {{$message}}
+                                    </div>
+                                    @enderror
+                                    <div class="text-muted">
+                                        This product will only be available for these nodes
+                                    </div>
+                                </div>
+
+
+                                <div class="form-group">
+                                    <label for="eggs">Eggs</label>
+                                    <select id="eggs" style="width:100%"
+                                            class="custom-select @error('eggs') is-invalid @enderror"
+                                            name="eggs[]" multiple="multiple" autocomplete="off">
+                                        @foreach($nests as $nest)
+                                            <optgroup label="{{$nest->name}}">
+                                                @foreach($nest->eggs as $egg)
+                                                    <option
+                                                        @if(isset($product)) @if($product->eggs->contains('id' , $egg->id)) selected
+                                                        @endif @endif  value="{{$egg->id}}">{{$egg->name}}</option>
+                                                @endforeach
+                                            </optgroup>
+                                        @endforeach
+                                    </select>
+                                    @error('eggs')
+                                    <div class="text-danger">
+                                        {{$message}}
+                                    </div>
+                                    @enderror
+                                    <div class="text-muted">
+                                        This product will only be available for these eggs
+                                    </div>
+                                </div>
+
+                            </div>
+                        </div>
+                    </div>
+
                 </div>
-            </div>
+            </form>
 
         </div>
     </section>
     <!-- END CONTENT -->
 
     <script>
-        document.addEventListener('DOMContentLoaded', function() {
+        document.addEventListener('DOMContentLoaded', function () {
             $('[data-toggle="popover"]').popover();
+            $('.custom-select').select2();
         });
     </script>
-
-
 @endsection

+ 105 - 31
resources/views/admin/products/edit.blade.php

@@ -10,10 +10,10 @@
                 </div>
                 <div class="col-sm-6">
                     <ol class="breadcrumb float-sm-right">
-                        <li class="breadcrumb-item"><a href="{{ route('home') }}">Dashboard</a></li>
-                        <li class="breadcrumb-item"><a href="{{ route('admin.products.index') }}">Products</a></li>
+                        <li class="breadcrumb-item"><a href="{{route('home')}}">Dashboard</a></li>
+                        <li class="breadcrumb-item"><a href="{{route('admin.products.index')}}">Products</a></li>
                         <li class="breadcrumb-item"><a class="text-muted"
-                                href="{{ route('admin.products.edit', $product->id) }}">Edit</a>
+                                                       href="{{route('admin.products.edit' , $product->id)}}">Edit</a>
                         </li>
                     </ol>
                 </div>
@@ -25,31 +25,34 @@
     <!-- MAIN CONTENT -->
     <section class="content">
         <div class="container-fluid">
+            <form action="{{route('admin.products.update' , $product->id)}}" method="POST">
+                @csrf
+                @method('PATCH')
 
+                <div class="row">
+                    <div class="col-lg-6">
 
-            <div class="row">
-                <div class="col-lg-6">
+                        @if($product->servers()->count() > 0)
+                            <div class="callout callout-danger">
+                                <h4>Editing the resource options will not automatically update the servers on
+                                    pterodactyl's side!</h4>
+                                <p class="text-muted">Automatically updating resource options on pterodactyl side is on
+                                    my todo list :)</p>
+                            </div>
+                        @endif
 
-                    @if ($product->servers()->count() > 0)
-                        <div class="callout callout-danger">
-                            <h4>Editing the resource options will not automatically update the servers on pterodactyl's
-                                side!</h4>
-                            <p class="text-muted">Automatically updating resource options on pterodactyl side is on my
-                                todo list :)</p>
-                        </div>
-                    @endif
+                        <div class="card">
+                            <div class="card-header">
+                                <h5 class="card-title">Product Details</h5>
+                            </div>
+                            <div class="card-body">
 
-                    <div class="card">
-                        <div class="card-body">
-                            <form action="{{ route('admin.products.update', $product->id) }}" method="POST">
-                                @csrf
-                                @method('PATCH')
                                 <div class="d-flex flex-row-reverse">
                                     <div class="custom-control custom-switch">
-                                        <input type="checkbox" @if ($product->disabled) checked @endif name="disabled"
-                                            class="custom-control-input custom-control-input-danger" id="switch1">
-                                        <label class="custom-control-label" for="switch1">Disabled <i data-toggle="popover"
-                                                data-trigger="hover"
+                                        <input type="checkbox" @if($product->disabled) checked @endif name="disabled"
+                                               class="custom-control-input custom-control-input-danger" id="switch1">
+                                        <label class="custom-control-label" for="switch1">Disabled <i
+                                                data-toggle="popover" data-trigger="hover"
                                                 data-content="Will hide this option from being selected"
                                                 class="fas fa-info-circle"></i></label>
                                     </div>
@@ -57,6 +60,7 @@
 
                                 <div class="row">
                                     <div class="col-lg-6">
+
                                         <div class="form-group">
                                             <label for="name">Name</label>
                                             <input value="{{ $product->name }}" id="name" name="name" type="text"
@@ -118,11 +122,13 @@
 
                                         <div class="form-group">
                                             <label for="description">Description <i data-toggle="popover"
-                                                    data-trigger="hover" data-content="This is what the users sees"
-                                                    class="fas fa-info-circle"></i></label>
-                                            <textarea id="description" name="description" type="text"
-                                                class="form-control @error('description') is-invalid @enderror"
-                                                required="required">{{ $product->description }}</textarea>
+                                                                                    data-trigger="hover"
+                                                                                    data-content="This is what the users sees"
+                                                                                    class="fas fa-info-circle"></i></label>
+                                            <textarea id="description" name="description"
+                                                      type="text"
+                                                      class="form-control @error('description') is-invalid @enderror"
+                                                      required="required">{{$product->description}}</textarea>
                                             @error('description')
                                                 <div class="invalid-feedback">
                                                     {{ $message }}
@@ -210,21 +216,89 @@
                                         Submit
                                     </button>
                                 </div>
-                            </form>
+
+                            </div>
                         </div>
                     </div>
-                </div>
-            </div>
 
+                    <div class="col-lg-6">
+                        <div class="card">
+                            <div class="card-header">
+                                <h5 class="card-title">Product Linking
+                                    <i data-toggle="popover"
+                                       data-trigger="hover"
+                                       data-content="Link your products to nodes and eggs to create dynamic pricing for each option"
+                                       class="fas fa-info-circle"></i></h5>
+                            </div>
+                            <div class="card-body">
+
+                                <div class="form-group">
+                                    <label for="nodes">Nodes</label>
+                                    <select id="nodes" style="width:100%"
+                                            class="custom-select @error('nodes') is-invalid @enderror" name="nodes[]"
+                                            multiple="multiple" autocomplete="off">
+                                        @foreach($locations as $location)
+                                            <optgroup label="{{$location->name}}">
+                                                @foreach($location->nodes as $node)
+                                                    <option @if($product->nodes->contains('id' , $node->id)) selected
+                                                            @endif value="{{$node->id}}">{{$node->name}}</option>
+                                                @endforeach
+                                            </optgroup>
+                                        @endforeach
+                                    </select>
+                                    @error('nodes')
+                                    <div class="text-danger">
+                                        {{$message}}
+                                    </div>
+                                    @enderror
+                                    <div class="text-muted">
+                                        This product will only be available for these nodes
+                                    </div>
+                                </div>
+
+                                <div class="form-group">
+                                    <label for="eggs">Eggs</label>
+                                    <select id="eggs" style="width:100%"
+                                            class="custom-select @error('eggs') is-invalid @enderror" name="eggs[]"
+                                            multiple="multiple" autocomplete="off">
+                                        @foreach($nests as $nest)
+                                            <optgroup label="{{$nest->name}}">
+                                                @foreach($nest->eggs as $egg)
+                                                    <option @if($product->eggs->contains('id' , $egg->id)) selected
+                                                            @endif value="{{$egg->id}}">{{$egg->name}}</option>
+                                                @endforeach
+                                            </optgroup>
+                                        @endforeach
+                                    </select>
+                                    @error('eggs')
+                                    <div class="text-danger">
+                                        {{$message}}
+                                    </div>
+                                    @enderror
+                                    <div class="text-muted">
+                                        This product will only be available for these eggs
+                                    </div>
+                                </div>
+
+                            </div>
+                        </div>
+                    </div>
+
+                </div>
+            </form>
         </div>
     </section>
     <!-- END CONTENT -->
 
+    <script>
+        document.addEventListener('DOMContentLoaded', (event) => {
+            $('.custom-select').select2();
+        })
+    </script>
     <script>
         document.addEventListener('DOMContentLoaded', function() {
             $('[data-toggle="popover"]').popover();
         });
     </script>
 
-
 @endsection

+ 5 - 4
resources/views/admin/products/index.blade.php

@@ -46,10 +46,10 @@
                             <th>Cpu</th>
                             <th>Swap</th>
                             <th>Disk</th>
-                            <th>IO</th>
                             <th>Databases</th>
                             <th>Backups</th>
-                            <th>Allocations</th>
+                            <th>Eggs</th>
+                            <th>Nodes</th>
                             <th>Servers</th>
                             <th>Created at</th>
                             <th></th>
@@ -79,6 +79,7 @@
                 processing: true,
                 serverSide: true,
                 stateSave: true,
+                order: [[ 2, "asc" ]],
                 ajax: "{{route('admin.products.datatable')}}",
                 columns: [
                     {data: 'disabled'},
@@ -88,10 +89,10 @@
                     {data: 'cpu'},
                     {data: 'swap'},
                     {data: 'disk'},
-                    {data: 'io'},
                     {data: 'databases'},
                     {data: 'backups'},
-                    {data: 'allocations'},
+                    {data: 'nodes', sortable: false},
+                    {data: 'eggs', sortable: false},
                     {data: 'servers', sortable: false},
                     {data: 'created_at'},
                     {data: 'actions', sortable: false},

+ 37 - 31
resources/views/admin/settings/index.blade.php

@@ -45,41 +45,38 @@
                     <div class="tab-content">
                         <div class="tab-pane mt-3 active" id="dashboard-icons">
 
-                            <form method="POST" class="mb-3" action="{{route('admin.settings.update.icons')}}">
+                            <form method="POST" enctype="multipart/form-data" class="mb-3"
+                                  action="{{route('admin.settings.update.icons')}}">
                                 @csrf
                                 @method('PATCH')
 
-                                <div class="d-flex">
-                                    <div class="form-group">
-                                        <div class="text-center mb-2">Dashboard Icon</div>
-                                        <div class="avatar">
-                                            <div class="slim rounded-circle"
-                                                 data-size="128,128"
-                                                 data-ratio="1:1"
-                                                 style="width: 140px;height:140px; cursor: pointer"
-                                                 data-save-initial-image="true">
-                                                <input type="file" name="icon"/>
-                                                <img
-                                                    src="{{\Illuminate\Support\Facades\Storage::disk('public')->exists('icon.png') ? asset('storage/icon.png') : asset('images/bitsec.png')}}"
-                                                    alt="icon">
+                                <div class="row">
+                                    <div class="col-md-6 col-lg-4 col-12">
+                                        <div class="form-group">
+                                            <div class="custom-file mb-3 mt-3">
+                                                <input type="file" accept="image/png,image/jpeg,image/jpg"
+                                                       class="custom-file-input" name="icon" id="icon">
+                                                <label class="custom-file-label selected"
+                                                       for="icon">{{__('Select panel icon')}}</label>
                                             </div>
+                                            @error('icon')
+                                            <span class="text-danger">
+                                                   {{$message}}
+                                               </span>
+                                            @enderror
                                         </div>
-                                    </div>
-
-
-                                    <div class="form-group ml-5">
-                                        <div class="text-center mb-2">Favicon</div>
-                                        <div class="avatar">
-                                            <div class="slim rounded-circle"
-                                                 data-size="64,64"
-                                                 data-ratio="1:1"
-                                                 style="width: 140px;height:140px; cursor: pointer"
-                                                 data-save-initial-image="true">
-                                                <input accept="image/x-icon" type="file" name="favicon"/>
-                                                <img
-                                                    src="{{\Illuminate\Support\Facades\Storage::disk('public')->exists('favicon.ico') ? asset('storage/favicon.ico') : asset('favicon.ico')}}"
-                                                    alt="favicon">
+                                        <div class="form-group">
+                                            <div class="custom-file mb-3">
+                                                <input type="file" accept="image/x-icon" class="custom-file-input"
+                                                       name="favicon" id="favicon">
+                                                <label class="custom-file-label selected"
+                                                       for="favicon">{{__('Select panel favicon')}}</label>
                                             </div>
+                                            @error('favicon')
+                                            <span class="text-danger">
+                                                   {{$message}}
+                                               </span>
+                                            @enderror
                                         </div>
                                     </div>
                                 </div>
@@ -87,7 +84,8 @@
                                 <button class="btn btn-primary">Submit</button>
                             </form>
 
-                            <p class="text-muted">Images and Icons may be cached, use <code>CNTRL + F5</code><sup>(google chrome hotkey)</sup> to reload without cache to see your changes appear :)</p>
+                            <p class="text-muted">Images and Icons may be cached, use <code>CNTRL + F5</code><sup>(google
+                                    chrome hotkey)</sup> to reload without cache to see your changes appear :)</p>
 
                         </div>
                     </div>
@@ -103,7 +101,15 @@
     </section>
     <!-- END CONTENT -->
 
-
+    <script>
+        // Add the following code if you want the name of the file appear on select
+        document.addEventListener('DOMContentLoaded', ()=>{
+            $(".custom-file-input").on("change", function () {
+                var fileName = $(this).val().split("\\").pop();
+                $(this).siblings(".custom-file-label").addClass("selected").html(fileName);
+            });
+        })
+    </script>
 
 
 @endsection

+ 0 - 2
resources/views/admin/users/index.blade.php

@@ -46,7 +46,6 @@
                             <th>Role</th>
                             <th>Email</th>
                             <th>{{CREDITS_DISPLAY_NAME}}</th>
-                            <th>Usage</th>
                             <th>Servers</th>
                             <th>Verified</th>
                             <th>Last seen</th>
@@ -87,7 +86,6 @@
                     {data: 'role'},
                     {data: 'email', name: 'users.email'},
                     {data: 'credits' , name : 'users.credits'},
-                    {data: 'usage' , sortable : false},
                     {data: 'servers' , sortable : false},
                     {data: 'verified' , sortable : false},
                     {data: 'last_seen'},

+ 1 - 0
resources/views/admin/users/show.blade.php

@@ -184,6 +184,7 @@
                             </div>
                         </div>
 
+
                         <div class="col-lg-6">
                             <div class="row">
                                 <div class="col-lg-4">

+ 86 - 56
resources/views/layouts/main.blade.php

@@ -10,6 +10,8 @@
           href="{{\Illuminate\Support\Facades\Storage::disk('public')->exists('favicon.ico') ? asset('storage/favicon.ico') : asset('favicon.ico')}}"
           type="image/x-icon">
 
+    <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+
     {{--    <link rel="stylesheet" href="{{asset('css/adminlte.min.css')}}">--}}
     <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.10.24/datatables.min.css"/>
 
@@ -76,11 +78,33 @@
                 </div>
             </li>
 
+            <li class="nav-item dropdown">
+                <a class="nav-link" href="#" id="userDropdown" role="button" data-toggle="dropdown"
+                   aria-haspopup="true"
+                   aria-expanded="false">
+                    <span class="mr-1 d-lg-inline text-gray-600">
+                        <small><i class="fas fa-coins mr-2"></i></small>{{Auth::user()->credits()}}
+                    </span>
+                </a>
+                <div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
+                    <a class="dropdown-item" href="{{route('store.index')}}">
+                        <i class="fas fa-coins fa-sm fa-fw mr-2 text-gray-400"></i>
+                        {{__('Store')}}
+                    </a>
+                    <div class="dropdown-divider"></div>
+                    <a class="dropdown-item" data-toggle="modal" data-target="#redeemVoucherModal"
+                       href="javascript:void(0)">
+                        <i class="fas fa-money-check-alt fa-sm fa-fw mr-2 text-gray-400"></i>
+                        {{__('Redeem code')}}
+                    </a>
+                </div>
+            </li>
+
             <li class="nav-item dropdown no-arrow">
                 <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown"
                    aria-haspopup="true"
                    aria-expanded="false">
-                    <span class="mr-1 d-none d-lg-inline text-gray-600 small">
+                    <span class="mr-1 d-lg-inline text-gray-600 small">
                         {{Auth::user()->name}}
                         <img width="28px" height="28px" class="rounded-circle ml-1" src="{{Auth::user()->getAvatar()}}">
                     </span>
@@ -102,11 +126,6 @@
                             Log back in
                         </a>
                     @endif
-                    <a class="dropdown-item" data-toggle="modal" data-target="#redeemVoucherModal"
-                       href="javascript:void(0)">
-                        <i class="fas fa-money-check-alt fa-sm fa-fw mr-2 text-gray-400"></i>
-                        Redeem code
-                    </a>
                     <div class="dropdown-divider"></div>
                     <form method="post" action="{{route('logout')}}">
                         @csrf
@@ -168,7 +187,42 @@
                     @endif
 
                     @if(Auth::user()->role == 'admin')
-                        <li class="nav-header">Admin</li>
+
+                        <li class="nav-header">Administration</li>
+
+                        <li class="nav-item">
+                            <a href="{{route('admin.overview.index')}}"
+                               class="nav-link @if(Request::routeIs('admin.overview.*')) active @endif">
+                                <i class="nav-icon fa fa-home"></i>
+                                <p>Overview</p>
+                            </a>
+                        </li>
+
+                        <li class="nav-item">
+                            <a href="{{route('admin.configurations.index')}}"
+                               class="nav-link @if(Request::routeIs('admin.configurations.*')) active @endif">
+                                <i class="nav-icon fas fa-cogs"></i>
+                                <p>Configurations</p>
+                            </a>
+                        </li>
+
+                        <li class="nav-item">
+                            <a href="{{route('admin.settings.index')}}"
+                               class="nav-link @if(Request::routeIs('admin.settings.*')) active @endif">
+                                <i class="nav-icon fas fa-tools"></i>
+                                <p>Settings</p>
+                            </a>
+                        </li>
+
+                        <li class="nav-item">
+                            <a href="{{route('admin.api.index')}}"
+                               class="nav-link @if(Request::routeIs('admin.api.*')) active @endif">
+                                <i class="nav-icon fa fa-gamepad"></i>
+                                <p>Application API</p>
+                            </a>
+                        </li>
+
+                        <li class="nav-header">Management</li>
 
                         <li class="nav-item">
                             <a href="{{route('admin.users.index')}}"
@@ -210,21 +264,32 @@
                             </a>
                         </li>
 
-                        <li class="nav-header">Pterodactyl</li>
+{{--                        <li class="nav-header">Pterodactyl</li>--}}
 
-                        <li class="nav-item">
-                            <a href="{{route('admin.nodes.index')}}"
-                               class="nav-link @if(Request::routeIs('admin.nodes.*')) active @endif">
-                                <i class="nav-icon fas fa-sitemap"></i>
-                                <p>Nodes</p>
-                            </a>
-                        </li>
+{{--                        <li class="nav-item">--}}
+{{--                            <a href="{{route('admin.nodes.index')}}"--}}
+{{--                               class="nav-link @if(Request::routeIs('admin.nodes.*')) active @endif">--}}
+{{--                                <i class="nav-icon fas fa-sitemap"></i>--}}
+{{--                                <p>Nodes</p>--}}
+{{--                            </a>--}}
+{{--                        </li>--}}
+
+{{--                        <li class="nav-item">--}}
+{{--                            <a href="{{route('admin.nests.index')}}"--}}
+{{--                               class="nav-link @if(Request::routeIs('admin.nests.*')) active @endif">--}}
+{{--                                <i class="nav-icon fas fa-th-large"></i>--}}
+{{--                                <p>Nests</p>--}}
+{{--                            </a>--}}
+{{--                        </li>--}}
+
+
+                        <li class="nav-header">Other</li>
 
                         <li class="nav-item">
-                            <a href="{{route('admin.nests.index')}}"
-                               class="nav-link @if(Request::routeIs('admin.nests.*')) active @endif">
-                                <i class="nav-icon fas fa-th-large"></i>
-                                <p>Nests</p>
+                            <a href="{{route('admin.usefullinks.index')}}"
+                               class="nav-link @if(Request::routeIs('admin.usefullinks.*')) active @endif">
+                                <i class="nav-icon fas fa-link"></i>
+                                <p>Useful Links</p>
                             </a>
                         </li>
 
@@ -248,43 +313,6 @@
                             </a>
                         </li>
 
-
-                        <li class="nav-header">Dashboard</li>
-
-                        <li class="nav-item">
-                            <a href="{{route('admin.api.index')}}"
-                               class="nav-link @if(Request::routeIs('admin.api.*')) active @endif">
-                                <i class="nav-icon fa fa-gamepad"></i>
-                                <p>Application API</p>
-                            </a>
-                        </li>
-
-                        <li class="nav-item">
-                            <a href="{{route('admin.usefullinks.index')}}"
-                               class="nav-link @if(Request::routeIs('admin.usefullinks.*')) active @endif">
-                                <i class="nav-icon fas fa-link"></i>
-                                <p>Useful Links</p>
-                            </a>
-                        </li>
-
-                        <li class="nav-header">Settings</li>
-
-                        <li class="nav-item">
-                            <a href="{{route('admin.configurations.index')}}"
-                               class="nav-link @if(Request::routeIs('admin.configurations.*')) active @endif">
-                                <i class="nav-icon fas fa-cogs"></i>
-                                <p>Configurations</p>
-                            </a>
-                        </li>
-
-                        <li class="nav-item">
-                            <a href="{{route('admin.settings.index')}}"
-                               class="nav-link @if(Request::routeIs('admin.settings.*')) active @endif">
-                                <i class="nav-icon fas fa-tools"></i>
-                                <p>Settings</p>
-                            </a>
-                        </li>
-
                     @endif
 
                 </ul>
@@ -340,6 +368,8 @@
 <script type="text/javascript" src="https://cdn.datatables.net/v/bs4/dt-1.10.24/datatables.min.js"></script>
 <!-- Summernote -->
 <script src="{{asset('plugins/summernote/summernote-bs4.min.js')}}"></script>
+<!-- select2 -->
+<script src="{{asset('plugins/select2/js/select2.min.js')}}"></script>
 
 <!-- Moment.js -->
 <script src="{{asset('plugins/moment/moment.min.js')}}"></script>

+ 1 - 1
resources/views/mail/payment/confirmed.blade.php

@@ -6,7 +6,7 @@ Your payment has been confirmed; Your credit balance has been updated.<br>
 ___
 ### Payment ID: **{{$payment->id}}**<br>
 ### Status:     **{{$payment->status}}**<br>
-### Price:      **{{$payment->formatCurrency()}}**<br>
+### Price:      **{{$payment->formatToCurrency($payment->total_price)}}**<br>
 ### Type:       **{{$payment->type}}**<br>
 ### Amount:     **{{$payment->amount}}**<br>
 ### Balance:    **{{$payment->user->credits}}**<br>

+ 419 - 99
resources/views/servers/create.blade.php

@@ -13,7 +13,7 @@
                         <li class="breadcrumb-item"><a href="{{ route('home') }}">Dashboard</a></li>
                         <li class="breadcrumb-item"><a href="{{ route('servers.index') }}">Servers</a>
                         <li class="breadcrumb-item"><a class="text-muted"
-                                href="{{ route('servers.create') }}">Create</a>
+                                                       href="{{ route('servers.create') }}">Create</a>
                         </li>
                     </ol>
                 </div>
@@ -23,130 +23,450 @@
     <!-- END CONTENT HEADER -->
 
     <!-- MAIN CONTENT -->
-    <section class="content">
-        <div class="container-fluid">
+    <section x-data="serverApp()" class="content">
+        <div class="container">
 
-            <!-- CUSTOM CONTENT -->
-            <div class="row justify-content-center">
-                <div class="card col-lg-8 col-md-12 mb-5">
-                    <div class="card-header">
-                        <h5 class="card-title"><i class="fa fa-server mr-2"></i>Create Server</h5>
-                    </div>
-                    <div class="card-body">
-                        <form method="post" action="{{ route('servers.store') }}">
-                            @csrf
-                            <div class="form-group">
-                                <label for="name">* Name</label>
-                                <input id="name" name="name" type="text" required="required"
-                                    class="form-control @error('name') is-invalid @enderror">
+            <!-- FORM -->
+            <form action="{{route('servers.store')}}" method="post" class="row">
+                @csrf
+                <div class="col-md-8">
+                    <div class="card">
+                        <div class="card-header">
+                            <div class="card-title"><i class="fas fa-cogs mr-2"></i>{{__('Server configuration')}}</div>
+                        </div>
 
-                                @error('name')
-                                    <div class="invalid-feedback">
-                                        Please fill out this field.
-                                    </div>
-                                @enderror
+                        @if($productCount === 0 || $nodeCount === 0 || count($nests) === 0 || count($eggs) === 0 )
+                            <div class="alert alert-danger p-2 m-2">
+                                <h5><i class="icon fas fa-exclamation-circle"></i>{{__('Error!')}}</h5>
+                                <p class="pl-4">
+                                    @if(Auth::user()->role == 'admin')
+                                        {{__('Make sure to link your products to nodes and eggs.')}} <br>
+                                        {{__('There has to be at least 1 valid product for server creation')}}
+                                    @endif
+                                </p>
+                                <ul>
+                                    @if($productCount === 0 )
+                                        <li> {{__('No products available!')}}</li>
+                                    @endif
 
-                            </div>
-                            <div class="form-group">
-                                <label for="description">Description</label>
-                                <input id="description" name="description" type="text"
-                                    class="form-control @error('description') is-invalid @enderror">
+                                    @if($nodeCount === 0 )
+                                        <li>{{__('No nodes have been linked!')}}</li>
+                                    @endif
 
-                                @error('description')
-                                    <div class="invalid-feedback">
-                                        Please fill out this field.
-                                    </div>
-                                @enderror
+                                    @if(count($nests) === 0 )
+                                        <li>{{__('No nests available!')}}</li>
+                                    @endif
 
+                                    @if(count($eggs) === 0 )
+                                        <li>{{__('No eggs have been linked!')}}</li>
+                                    @endif
+                                </ul>
                             </div>
-                            <div class="form-group">
-                                <label for="location_id">* Server location</label>
-                                <div>
-
-                                    <select id="node_id" name="node_id" required="required"
-                                        class="custom-select @error('node_id') is-invalid @enderror">
-                                        <option selected disabled hidden value="">Please Select ...</option>    
-                                        @foreach ($locations as $location)
-                                            <optgroup label="{{ $location->name }}">
-                                                @foreach ($location->nodes as $node)
-                                                    @if (!$node->disabled)
-                                                        <option value="{{ $node->id }}">{{ $node->name }}</option>
-                                                    @endif
-                                                @endforeach
-                                            </optgroup>
-                                        @endforeach
+                        @endif
+
 
-                                    </select>
+                        <div x-show="loading" class="overlay dark">
+                            <i class="fas fa-2x fa-sync-alt"></i>
+                        </div>
+                        <div class="card-body">
+
+                            @if ($errors->any())
+                                <div class="alert alert-danger">
+                                    <ul class="list-group pl-3">
+                                        @foreach ($errors->all() as $error)
+                                            <li>{{ $error }}</li>
+                                        @endforeach
+                                    </ul>
                                 </div>
+                            @endif
 
-                                @error('node_id')
-                                    <div class="invalid-feedback">
-                                        Please fill out this field.
-                                    </div>
+                            <div class="form-group">
+                                <label for="name">{{__('Name')}}</label>
+                                <input x-model="name" id="name" name="name" type="text" required="required"
+                                       class="form-control @error('name') is-invalid @enderror">
+
+                                @error('name')
+                                <div class="invalid-feedback">
+                                    {{ $message }}
+                                </div>
                                 @enderror
                             </div>
-                            <div class="form-group">
-                                <label for="egg_id">* Server configuration</label>
-                                <div>
-                                    <select id="egg_id" name="egg_id" required="required"
-                                        class="custom-select @error('egg_id') is-invalid @enderror">
-                                       <option selected disabled hidden value="">Please Select ...</option>    
-                                        @foreach ($nests as $nest)
-                                            <optgroup label="{{ $nest->name }}">
-                                                @foreach ($nest->eggs as $egg)
-                                                    <option value="{{ $egg->id }}">{{ $egg->name }}</option>
-                                                @endforeach
-                                            </optgroup>
-                                        @endforeach
-                                    </select>
+
+                            <div class="row">
+                                <div class="col-md-6">
+                                    <div class="form-group">
+                                        <label for="nest">{{__('Software / Games')}}</label>
+                                        <select class="custom-select"
+                                                required
+                                                name="nest"
+                                                id="nest"
+                                                x-model="selectedNest"
+                                                @change="setEggs();">
+                                            <option selected disabled hidden
+                                                    value="null">{{count($nests) > 0 ? __('Please select software ...') : __('---')}}</option>
+                                            @foreach ($nests as $nest)
+                                                <option value="{{ $nest->id }}">{{ $nest->name }}</option>
+                                            @endforeach
+                                        </select>
+
+                                    </div>
                                 </div>
 
-                                @error('egg_id')
-                                    <div class="invalid-feedback">
-                                        Please fill out this field.
+                                <div class="col-md-6">
+                                    <div class="form-group">
+                                        <label for="egg">{{__('Specification ')}}</label>
+                                        <div>
+                                            <select id="egg"
+                                                    required
+                                                    name="egg"
+                                                    :disabled="eggs.length == 0"
+                                                    x-model="selectedEgg"
+                                                    @change="fetchLocations();"
+                                                    required="required"
+                                                    class="custom-select">
+                                                <option x-text="getEggInputText()"
+                                                        selected disabled hidden value="null"></option>
+                                                <template x-for="egg in eggs" :key="egg.id">
+                                                    <option x-text="egg.name" :value="egg.id"></option>
+                                                </template>
+                                            </select>
+                                        </div>
                                     </div>
-                                @enderror
+                                </div>
                             </div>
+
                             <div class="form-group">
-                                <label for="product_id">* Resource Configuration</label>
-                                <div>
-                                    <select id="product_id" name="product_id" required="required"
-                                        class="custom-select @error('product_id') is-invalid @enderror">
-                                        <option selected disabled hidden value="">Please Select...</option>    
-                                        @foreach ($products as $product)
-                                            <option value="{{ $product->id }}" @if ($product->minimum_credits == -1 && Auth::user()->credits >= $minimum_credits)
-                                            @elseif ($product->minimum_credits != -1 && Auth::user()->credits >=
-                                                $product->minimum_credits)
-                                            @else
-                                                disabled
-                                        @endif
-                                        >{{ $product->name }}
-                                        ({{ $product->description }})
-                                        </option>
-                                        @endforeach
-                                    </select>
-                                </div>
+                                <label for="node">{{__('Node')}}</label>
+                                <select name="node"
+                                        required
+                                        id="node"
+                                        x-model="selectedNode"
+                                        :disabled="!fetchedLocations"
+                                        @change="fetchProducts();"
+                                        class="custom-select">
+                                    <option
+                                        x-text="getNodeInputText()"
+                                        disabled selected hidden value="null">
+                                    </option>
 
-                                @error('product_id')
-                                    <div class="invalid-feedback">
-                                        Please fill out this field.
-                                    </div>
-                                @enderror
+                                    <template x-for="location in locations" :key="location.id">
+                                        <optgroup :label="location.name">
+
+                                            <template x-for="node in location.nodes" :key="node.id">
+                                                <option x-text="node.name"
+                                                        :value="node.id">
+
+                                                </option>
+                                            </template>
+                                        </optgroup>
+                                    </template>
+
+                                </select>
                             </div>
-                            <div class="form-group text-right">
-                                <input type="submit" class="btn btn-primary mt-3" value="Submit"
-                                    onclick="this.disabled=true;this.value='Creating, please wait...';this.form.submit();">
+
+
+                            <div class="form-group">
+                                <label for="product">{{__('Resources')}}</label>
+                                <select name="product"
+                                        required
+                                        id="product"
+                                        :disabled="!fetchedProducts"
+                                        x-model="selectedProduct"
+                                        @change="updateSelectedObjects()"
+                                        class="custom-select">
+                                    <option
+                                        x-text="getProductInputText()"
+                                        disabled selected hidden value="null"></option>
+                                    <template x-for="product in products" :key="product.id">
+                                        <option :disabled="product.minimum_credits > user.credits"
+                                                x-text="getProductOptionText(product)"
+                                                :value="product.id">
+                                        </option>
+                                    </template>
+                                </select>
                             </div>
-                        </form>
 
+
+                        </div>
                     </div>
                 </div>
-            </div>
-            <!-- END CUSTOM CONTENT -->
+                <div class="col-md-4 mb-4">
+                    <div class="card">
+                        <div class="card-header">
+                            <div class="card-title">
+                                <i class="fas fa-list mr-2"></i>{{__('Server details')}}
+                            </div>
+                        </div>
+                        <div class="card-body">
+
+                            <ul class="list-group mb-3">
+                                <li x-show="selectedNestObject.name"
+                                    class="list-group-item d-flex justify-content-between lh-condensed">
+                                    <div>
+                                        <h6 class="my-0">{{__('Software / Games')}}</h6>
+                                        <small x-text="selectedNestObject?.name ?? '{{__('No selection')}}'"
+                                               class="text-muted"></small>
+                                    </div>
+                                </li>
+                                <li class="list-group-item d-flex justify-content-between lh-condensed">
+                                    <div>
+                                        <h6 class="my-0">{{__('Specification')}}</h6>
+                                        <small x-text="selectedEggObject?.name ?? '{{__('No selection')}}'"
+                                               class="text-muted"></small>
+                                    </div>
+                                </li>
+                                <li
+                                    class="list-group-item d-flex justify-content-between lh-condensed">
+                                    <div>
+                                        <h6 class="my-0">{{__('Node')}}</h6>
+                                        <small x-text="selectedNodeObject?.name ?? '{{__('No selection')}}'"
+                                               class="text-muted"></small>
+                                    </div>
+                                </li>
+                                <li class="list-group-item">
+                                    <div>
+                                        <h6 class="my-0">{{__('Resources')}}</h6>
+                                        <small x-text="selectedProductObject?.name ?? '{{__('No selection')}}'"
+                                               class="text-muted"></small>
+                                    </div>
 
+                                    <template x-if="selectedProductObject?.name">
+                                        <ul class="pl-0">
+                                            <li class="d-flex justify-content-between">
+                                                <small class="text-muted d-inline-block">{{__('CPU')}}</small>
+                                                <small class="text-muted d-inline-block"
+                                                       x-text="selectedProductObject.cpu + ' %'"></small>
+                                            </li>
+                                            <div class="d-flex justify-content-between">
+                                                <small class="text-muted d-inline-block">{{__('Memory')}}</small>
+                                                <small class="text-muted d-inline-block"
+                                                       x-text="selectedProductObject.memory + ' {{__('MB')}}'"></small>
+                                            </div>
+                                            <div class="d-flex justify-content-between">
+                                                <small class="text-muted d-inline-block">{{__('Disk')}}</small>
+                                                <small class="text-muted d-inline-block"
+                                                       x-text="selectedProductObject.disk + ' {{__('MB')}}'"></small>
+                                            </div>
+                                            <div class="d-flex justify-content-between">
+                                                <small class="text-muted d-inline-block">{{__('Databases')}}</small>
+                                                <small class="text-muted d-inline-block"
+                                                       x-text="selectedProductObject.databases + ' {{__('MySQL')}}'"></small>
+                                            </div>
+                                            <div class="d-flex justify-content-between">
+                                                <small class="text-muted d-inline-block">{{__('Backups')}}</small>
+                                                <small class="text-muted d-inline-block"
+                                                       x-text="selectedProductObject.backups"></small>
+                                            </div>
+                                            <div class="d-flex justify-content-between">
+                                               <small class="text-muted d-inline-block">{{__('Allocations')}} ({{__('ports')}})</small>
+                                               <small class="text-muted d-inline-block" x-text="selectedProductObject.allocations"></small>
+                                           </div>
+                                        </ul>
+                                    </template>
+
+                                </li>
+                            </ul>
+                            <ul class="list-group mb-3">
+                                <li class="list-group-item d-flex justify-content-between">
+                                    <span>{{CREDITS_DISPLAY_NAME}} {{__('per month')}}</span>
+                                    <strong>
+                                        <i x-show="selectedProductObject?.price" class="fas fa-coins"></i>
+                                        <span x-text="selectedProductObject?.price ?? ''"></span>
+                                    </strong>
+                                </li>
+                            </ul>
+                            <button :disabled="!isFormValid()" :class="isFormValid() ? '' : 'disabled'"
+                                    class="btn btn-primary btn-block">
+                                {{__('Create server')}}
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </form>
+            <!-- END FORM -->
 
         </div>
     </section>
     <!-- END CONTENT -->
 
+
+    <script>
+        function serverApp() {
+            return {
+                //loading
+                loading: false,
+                fetchedLocations: false,
+                fetchedProducts: false,
+
+                //input fields
+                name: null,
+                selectedNest: null,
+                selectedEgg: null,
+                selectedNode: null,
+                selectedProduct: null,
+
+                //selected objects based on input
+                selectedNestObject: {},
+                selectedEggObject: {},
+                selectedNodeObject: {},
+                selectedProductObject: {},
+
+                //values
+                user: {!! $user !!},
+                nests: {!! $nests !!},
+                eggsSave:{!! $eggs !!}, //store back-end eggs
+                eggs: [],
+                locations: [],
+                products: [],
+
+
+                /**
+                 * @description set available eggs based on the selected nest
+                 * @note called whenever a nest is selected
+                 * @see selectedNest
+                 */
+                async setEggs() {
+                    this.fetchedLocations = false;
+                    this.fetchedProducts = false;
+                    this.locations = [];
+                    this.products = [];
+                    this.selectedEgg = 'null';
+                    this.selectedNode = 'null';
+                    this.selectedProduct = 'null';
+
+                    this.eggs = this.eggsSave.filter(egg => egg.nest_id == this.selectedNest)
+
+                    //automatically select the first entry if there is only 1
+                    if (this.eggs.length === 1) {
+                        this.selectedEgg = this.eggs[0].id;
+                        await this.fetchLocations();
+                        return;
+                    }
+
+                    this.updateSelectedObjects()
+                },
+
+                /**
+                 * @description fetch all available locations based on the selected egg
+                 * @note called whenever a server configuration is selected
+                 * @see selectedEg
+                 */
+                async fetchLocations() {
+                    this.loading = true;
+                    this.fetchedLocations = false;
+                    this.fetchedProducts = false;
+                    this.locations = [];
+                    this.products = [];
+                    this.selectedNode = 'null';
+                    this.selectedProduct = 'null';
+
+                    let response = await axios.get(`{{route('products.locations.egg')}}/${this.selectedEgg}`)
+                        .catch(console.error)
+
+                    this.fetchedLocations = true;
+                    this.locations = response.data
+
+                    //automatically select the first entry if there is only 1
+                    if (this.locations.length === 1 && this.locations[0]?.nodes?.length === 1) {
+                        this.selectedNode = this.locations[0]?.nodes[0]?.id;
+                        await this.fetchProducts();
+                        return;
+                    }
+
+                    this.loading = false;
+                    this.updateSelectedObjects()
+                },
+
+                /**
+                 * @description fetch all available products based on the selected node
+                 * @note called whenever a node is selected
+                 * @see selectedNode
+                 */
+                async fetchProducts() {
+                    this.loading = true;
+                    this.fetchedProducts = false;
+                    this.products = [];
+                    this.selectedProduct = 'null';
+
+                    let response = await axios.get(`{{route('products.products.node')}}/${this.selectedEgg}/${this.selectedNode}`)
+                        .catch(console.error)
+
+                    this.fetchedProducts = true;
+                    this.products = response.data
+
+                    //automatically select the first entry if there is only 1
+                    if (this.products.length === 1) {
+                        this.selectedProduct = this.products[0].id;
+                    }
+
+                    this.loading = false;
+                    this.updateSelectedObjects()
+                },
+
+                /**
+                 * @description map selected id's to selected objects
+                 * @note being used in the server info box
+                 */
+                updateSelectedObjects() {
+                    this.selectedNestObject = this.nests.find(nest => nest.id == this.selectedNest) ?? {}
+                    this.selectedEggObject = this.eggs.find(egg => egg.id == this.selectedEgg) ?? {}
+
+                    this.locations.forEach(location => {
+                        this.selectedNodeObject = location.nodes.find(node => node.id == this.selectedNode) ?? {};
+                    })
+
+                    this.selectedProductObject = this.products.find(product => product.id == this.selectedProduct) ?? {}
+                },
+
+                /**
+                 * @description check if all options are selected
+                 * @return {boolean}
+                 */
+                isFormValid() {
+                    if (Object.keys(this.selectedNestObject).length === 0) return false;
+                    if (Object.keys(this.selectedEggObject).length === 0) return false;
+                    if (Object.keys(this.selectedNodeObject).length === 0) return false;
+                    if (Object.keys(this.selectedProductObject).length === 0) return false;
+                    return !!this.name;
+                },
+
+                getNodeInputText() {
+                    if (this.fetchedLocations) {
+                        if (this.locations.length > 0) {
+                            return '{{__('Please select a node ...')}}';
+                        }
+                        return '{{__('No nodes found matching current configuration')}}'
+                    }
+                    return '{{__('---')}}';
+                },
+
+                getProductInputText() {
+                    if (this.fetchedProducts) {
+                        if (this.products.length > 0) {
+                            return '{{__('Please select a resource ...')}}';
+                        }
+                        return '{{__('No resources found matching current configuration')}}'
+                    }
+                    return '{{__('---')}}';
+                },
+
+                getEggInputText() {
+                    if (this.selectedNest) {
+                        return '{{__('Please select a configuration ...')}}';
+                    }
+                    return '{{__('---')}}';
+                },
+
+                getProductOptionText(product) {
+                    let text = product.name + ' (' + product.description + ')';
+
+                    if (product.minimum_credits > this.user.credits) {
+                        return '{{__('Not enough credits!')}} | ' + text;
+                    }
+
+                    return text;
+                }
+            }
+        }
+    </script>
 @endsection

+ 5 - 5
resources/views/store/checkout.blade.php

@@ -83,7 +83,7 @@
                                         <td>1</td>
                                         <td><i class="fa fa-coins mr-2"></i>{{$product->quantity}} {{strtolower($product->type) == 'credits' ? CREDITS_DISPLAY_NAME : $product->type}}</td>
                                         <td>{{$product->description}}</td>
-                                        <td>{{$product->formatCurrency()}}</td>
+                                        <td>{{$product->formatToCurrency($product->price)}}</td>
                                     </tr>
                                     </tbody>
                                 </table>
@@ -111,11 +111,11 @@
                                     <table class="table">
                                         <tr>
                                             <th style="width:50%">Subtotal:</th>
-                                            <td>{{$product->formatCurrency()}}</td>
+                                            <td>{{$product->formatToCurrency($product->price)}}</td>
                                         </tr>
                                         <tr>
-                                            <th>Tax (0%)</th>
-                                            <td>0.00</td>
+                                            <th>Tax ({{$taxpercent}}%)</th>
+                                            <td>{{$product->formatToCurrency($taxvalue)}}</td>
                                         </tr>
                                         <tr>
                                             <th>Quantity:</th>
@@ -123,7 +123,7 @@
                                         </tr>
                                         <tr>
                                             <th>Total:</th>
-                                            <td>{{$product->formatCurrency()}}</td>
+                                            <td>{{$product->formatToCurrency($total)}}</td>
                                         </tr>
                                     </table>
                                 </div>

+ 1 - 1
resources/views/store/index.blade.php

@@ -50,7 +50,7 @@
                             <?php /** @var $product PaypalProduct */?>
                             @foreach($products as $product)
                                 <tr>
-                                    <td>{{$product->formatCurrency()}}</td>
+                                    <td>{{$product->formatToCurrency($product->price)}}</td>
                                     <td>{{strtolower($product->type) == 'credits' ? CREDITS_DISPLAY_NAME : $product->type}}</td>
                                     <td><i class="fa fa-coins mr-2"></i>{{$product->display}}</td>
                                     <td><a href="{{route('checkout' , $product->id)}}" class="btn btn-info">Purchase</a>

+ 19 - 9
routes/web.php

@@ -3,8 +3,7 @@
 use App\Http\Controllers\Admin\ActivityLogController;
 use App\Http\Controllers\Admin\ApplicationApiController;
 use App\Http\Controllers\Admin\ConfigurationController;
-use App\Http\Controllers\Admin\NestsController;
-use App\Http\Controllers\Admin\NodeController;
+use App\Http\Controllers\Admin\OverViewController;
 use App\Http\Controllers\Admin\PaymentController;
 use App\Http\Controllers\Admin\PaypalProductController;
 use App\Http\Controllers\Admin\ProductController;
@@ -16,6 +15,7 @@ use App\Http\Controllers\Admin\VoucherController;
 use App\Http\Controllers\Auth\SocialiteController;
 use App\Http\Controllers\HomeController;
 use App\Http\Controllers\NotificationController;
+use App\Http\Controllers\ProductController as FrontProductController;
 use App\Http\Controllers\ProfileController;
 use App\Http\Controllers\ServerController;
 use App\Http\Controllers\StoreController;
@@ -54,6 +54,12 @@ Route::middleware(['auth', 'checkSuspended'])->group(function () {
     Route::resource('profile', ProfileController::class);
     Route::resource('store', StoreController::class);
 
+    #server create utility routes (product)
+    #routes made for server create page to fetch product info
+    Route::get('/products/nodes/egg/{egg?}', [FrontProductController::class, 'getNodesBasedOnEgg'])->name('products.nodes.egg');
+    Route::get('/products/locations/egg/{egg?}', [FrontProductController::class, 'getLocationsBasedOnEgg'])->name('products.locations.egg');
+    Route::get('/products/products/{egg?}/{node?}', [FrontProductController::class, 'getProductsBasedOnNode'])->name('products.products.node');
+
     #payments
     Route::get('checkout/{paypalProduct}', [PaymentController::class, 'checkOut'])->name('checkout');
     Route::get('payment/success', [PaymentController::class, 'success'])->name('payment.success');
@@ -72,6 +78,9 @@ Route::middleware(['auth', 'checkSuspended'])->group(function () {
     #admin
     Route::prefix('admin')->name('admin.')->middleware('admin')->group(function () {
 
+        Route::get('overview', [OverViewController::class, 'index'])->name('overview.index');
+        Route::get('overview/sync', [OverViewController::class, 'syncPterodactyl'])->name('overview.sync');
+
         Route::resource('activitylogs', ActivityLogController::class);
 
         Route::get("users.json", [UserController::class, "json"])->name('users.json');
@@ -87,6 +96,7 @@ Route::middleware(['auth', 'checkSuspended'])->group(function () {
         Route::resource('servers', AdminServerController::class);
 
         Route::get('products/datatable', [ProductController::class, 'datatable'])->name('products.datatable');
+        Route::get('products/clone/{product}', [ProductController::class, 'clone'])->name('products.clone');
         Route::patch('products/disable/{product}', [ProductController::class, 'disable'])->name('products.disable');
         Route::resource('products', ProductController::class);
 
@@ -99,13 +109,13 @@ Route::middleware(['auth', 'checkSuspended'])->group(function () {
         Route::get('payments/datatable', [PaymentController::class, 'datatable'])->name('payments.datatable');
         Route::get('payments', [PaymentController::class, 'index'])->name('payments.index');
 
-        Route::get('nodes/datatable', [NodeController::class, 'datatable'])->name('nodes.datatable');
-        Route::get('nodes/sync', [NodeController::class, 'sync'])->name('nodes.sync');
-        Route::resource('nodes', NodeController::class);
-
-        Route::get('nests/datatable', [NestsController::class, 'datatable'])->name('nests.datatable');
-        Route::get('nests/sync', [NestsController::class, 'sync'])->name('nests.sync');
-        Route::resource('nests', NestsController::class);
+//        Route::get('nodes/datatable', [NodeController::class, 'datatable'])->name('nodes.datatable');
+//        Route::get('nodes/sync', [NodeController::class, 'sync'])->name('nodes.sync');
+//        Route::resource('nodes', NodeController::class);
+//
+//        Route::get('nests/datatable', [NestsController::class, 'datatable'])->name('nests.datatable');
+//        Route::get('nests/sync', [NestsController::class, 'sync'])->name('nests.sync');
+//        Route::resource('nests', NestsController::class);
 
         Route::get('configurations/datatable', [ConfigurationController::class, 'datatable'])->name('configurations.datatable');
         Route::patch('configurations/updatevalue', [ConfigurationController::class, 'updatevalue'])->name('configurations.updatevalue');

+ 2 - 0
storage/debugbar/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

Някои файлове не бяха показани, защото твърде много файлове са промени