From 0f91ce49408e2e902644bc9dd91fbec91f448cda Mon Sep 17 00:00:00 2001 From: Deon George Date: Thu, 4 May 2023 22:17:42 +1000 Subject: [PATCH] Updates to Product Model, product updates, enable pricing update, improved formating of product services --- app/Http/Controllers/ProductController.php | 12 ++ app/Http/Requests/ProductAddEdit.php | 1 + app/Interfaces/SupplierItem.php | 1 + app/Models/Group.php | 13 +- app/Models/Product.php | 104 +++++++---- app/Models/Product/Broadband.php | 3 + app/Models/Product/Domain.php | 3 + app/Models/Product/Email.php | 3 + app/Models/Product/Generic.php | 3 + app/Models/Product/Host.php | 3 + app/Models/Product/Phone.php | 3 + app/Models/Product/SSL.php | 3 + app/Models/Product/Type.php | 11 +- app/Models/Service.php | 14 +- app/Models/Supplier/Broadband.php | 4 + public/plugin/dataTables/leftSearchPanes.css | 103 ++++++++++ .../adminlte/product/details.blade.php | 6 + .../backend/adminlte/product/home.blade.php | 43 +---- .../adminlte/product/widget/detail.blade.php | 176 +++++++++++++----- .../product/widget/selector.blade.php | 40 ++++ .../product/widget/services.blade.php | 80 +++++++- 21 files changed, 491 insertions(+), 138 deletions(-) create mode 100644 public/plugin/dataTables/leftSearchPanes.css create mode 100644 resources/views/theme/backend/adminlte/product/widget/selector.blade.php diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 7f9cc46..3fa53ef 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -97,6 +97,18 @@ class ProductController extends Controller $o->active = (bool)$request->active; + // Trim down the pricing array, remove null values + $o->pricing = $o->pricing->map(function($item) { + foreach ($item as $k=>$v) { + if (is_array($v)) { + $v = array_filter($v); + $item[$k] = $v; + } + } + + return $item; + }); + try { $o->save(); } catch (\Exception $e) { diff --git a/app/Http/Requests/ProductAddEdit.php b/app/Http/Requests/ProductAddEdit.php index c5ba9cd..da60a24 100644 --- a/app/Http/Requests/ProductAddEdit.php +++ b/app/Http/Requests/ProductAddEdit.php @@ -35,6 +35,7 @@ class ProductAddEdit extends FormRequest 'model' => 'sometimes|string', // @todo Check that it is a valid model type 'model_id' => 'sometimes|int', // @todo Check that it is a valid model type 'accounting' => 'nullable|string', + 'pricing' => 'required|array', // @todo Validate the elements in the pricing ]; } } \ No newline at end of file diff --git a/app/Interfaces/SupplierItem.php b/app/Interfaces/SupplierItem.php index 8c1b651..46081fe 100644 --- a/app/Interfaces/SupplierItem.php +++ b/app/Interfaces/SupplierItem.php @@ -33,6 +33,7 @@ interface SupplierItem * Return the billing interval that the supplier charges * * @return string + * @deprecated use Product::normalizeBillingInterval() */ public function getBillingIntervalAttribute(): int; diff --git a/app/Models/Group.php b/app/Models/Group.php index 0e17d02..ffc6ae5 100644 --- a/app/Models/Group.php +++ b/app/Models/Group.php @@ -4,8 +4,19 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Leenooks\Traits\ScopeActive; class Group extends Model { - use HasFactory; + use HasFactory, ScopeActive; + + /* SCOPES */ + + /** + * @return void + */ + public function scopePricing() + { + return $this->where('pricing',TRUE); + } } diff --git a/app/Models/Product.php b/app/Models/Product.php index 22fcb62..f04bc35 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -11,7 +11,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Http\Request; use Illuminate\Support\Facades\File; -use Illuminate\Support\Facades\Log; use Leenooks\Traits\ScopeActive; use App\Interfaces\{IDs,ProductItem}; @@ -24,12 +23,15 @@ use App\Traits\{ProductDetails,SiteID}; * Products have one Type (Product/*), made of an Offering (Supplier/*) from a Supplier. * Conversely, Suppliers provide Offerings (Supplier/*) which belong to a Type (Product/*) of a Product. * + * So each product attribute has: + * + supplied : Supplier product provided for this offering (Supplier/*) + * + type : Returns the underlying product object, representing the type of product (Product/*) + * * Attributes for products: * + lid : Local ID for product (part number) * + sid : System ID for product (part number) * + category : Type of product supplied * + category_name : Type of product supplied (Friendly Name for display, not for internal logic) - * + supplied : Supplier product provided for this offering * + supplier : Supplier for this offering * + name : Brief Name for our product with name_detail * + name_short : Product ID for our Product (description.name => name_short) @@ -41,9 +43,8 @@ use App\Traits\{ProductDetails,SiteID}; * + setup_charge_taxable : Charge to setup this product including taxes * + base_charge : Default billing amount * + base_charge_taxable : Default billing amount including taxes - * + min_charge : Minimum cost taking into account billing interval and setup costs - * + min_charge_taxable : Minimum cost taking into account billing interval and setup costs including taxes - * + type : Returns the underlying product object, representing the type of product + * + min_charge : Minimum charge taking into account billing interval and setup charges + * + min_charge_taxable : Minimum charge taking into account billing interval and setup charges including taxes * * Attributes for product types (type - Product/*) * + name : Short Name for our Product @@ -74,7 +75,37 @@ class Product extends Model implements IDs 'pricing'=>'collection', ]; - protected $with = ['translate']; + /* STATIC */ + + /** + * Return a list of available product types + * + * @return Collection + */ + public static function availableTypes(): Collection + { + $models = collect(File::allFiles(app_path())) + ->map(function ($item) { + $path = $item->getRelativePathName(); + $class = sprintf('%s%s', + Container::getInstance()->getNamespace(), + strtr(substr($path, 0, strrpos($path, '.')), '/', '\\')); + + return $class; + }) + ->filter(function ($class) { + $valid = FALSE; + + if (class_exists($class)) { + $reflection = new \ReflectionClass($class); + $valid = $reflection->isSubclassOf(ProductItem::class) && (! $reflection->isAbstract()); + } + + return $valid; + }); + + return $models->values(); + } /* RELATIONS */ @@ -349,33 +380,38 @@ class Product extends Model implements IDs /* METHODS */ /** - * Return a list of available product types + * Return the charge from the pricing table for the specific time period and group * - * @return Collection + * @param int $timeperiod + * @param Group $go + * @param string $type + * @return float|null */ - function availableTypes(): Collection + public function charge(int $timeperiod,Group $go,string $type): ?float { - $models = collect(File::allFiles(app_path())) - ->map(function ($item) { - $path = $item->getRelativePathName(); - $class = sprintf('%s%s', - Container::getInstance()->getNamespace(), - strtr(substr($path, 0, strrpos($path, '.')), '/', '\\')); + return Arr::get($this->pricing,sprintf('%d.%d.%s',$timeperiod,$go->id,$type)); + } - return $class; - }) - ->filter(function ($class) { - $valid = FALSE; + /** + * Do we have a charge for specific group/period + * + * @param int $timeperiod + * @return bool + */ + public function charge_available(int $timeperiod): bool + { + return Arr::get($this->pricing,sprintf('%d.show',$timeperiod),FALSE); + } - if (class_exists($class)) { - $reflection = new \ReflectionClass($class); - $valid = $reflection->isSubclassOf(ProductItem::class) && (! $reflection->isAbstract()); - } - - return $valid; - }); - - return $models->values(); + /** + * Return a normalize price dependent on the product, ie: Broadband = Monthly, Domain = Yearly, etc + * + * @note: By definition products are normalised, as their cost price is based on the default billing interval + * @return float + */ + public function cost_normalized(): float + { + return number_format(Tax::tax_calc($this->supplied->base_cost,config('site')->taxes),2); } /** @@ -385,6 +421,7 @@ class Product extends Model implements IDs * @param int|NULL $timeperiod * @param Group|NULL $go * @return float + * @todo use self::charge() */ private function getCharge(string $type,int $timeperiod=NULL,Group $go=NULL): float { @@ -407,16 +444,9 @@ class Product extends Model implements IDs $alt_tp--; } - if (! is_null($price) && $alt_tp !== $timeperiod) { + // If we havent got a price, we'll extrapolate one, except for setup charges + if (! is_null($price) && ($alt_tp !== $timeperiod) && ($type !== 'setup')) $price = $price*Invoice::billing_change($alt_tp,$timeperiod); - } - } - - // @todo - if price doesnt exist for the time period, reduce down to timeperiod 1 and multiply appropriately. - if (is_null($price)) { - Log::error(sprintf('Price is still null for [%d] timeperiod [%d] group [%d]',$this->id,$timeperiod,$go->id)); - - $price = 0; } return round($price,2); diff --git a/app/Models/Product/Broadband.php b/app/Models/Product/Broadband.php index d26b013..cbd898b 100644 --- a/app/Models/Product/Broadband.php +++ b/app/Models/Product/Broadband.php @@ -6,6 +6,7 @@ use Illuminate\Support\Collection; use Leenooks\Traits\ScopeActive; use App\Interfaces\ProductItem; +use App\Models\Invoice; use App\Models\Service\Broadband as ServiceBroadband; use App\Models\Supplier\Broadband as SupplierBroadband; @@ -34,6 +35,8 @@ final class Broadband extends Type implements ProductItem // The model that is referenced when this product is ordered protected string $order_model = ServiceBroadband::class; + // When comparing billing/pricing/charging, what metric to normalise to + const DefaultBill = Invoice::BILL_MONTHLY; // The model that the supplier supplies const SupplierModel = SupplierBroadband::class; diff --git a/app/Models/Product/Domain.php b/app/Models/Product/Domain.php index 90181ee..4bbd27d 100644 --- a/app/Models/Product/Domain.php +++ b/app/Models/Product/Domain.php @@ -5,6 +5,7 @@ namespace App\Models\Product; use Illuminate\Support\Collection; use App\Interfaces\ProductItem; +use App\Models\Invoice; use App\Models\Service\Domain as ServiceDomain; use App\Models\Supplier\Domain as SupplierDomain; @@ -31,6 +32,8 @@ final class Domain extends Type implements ProductItem // The model that is referenced when this product is ordered protected string $order_model = ServiceDomain::class; + // When comparing billing/pricing/charging, what metric to normalise to + const DefaultBill = Invoice::BILL_YEARLY; // The model that the supplier supplies const SupplierModel = SupplierDomain::class; diff --git a/app/Models/Product/Email.php b/app/Models/Product/Email.php index 018332d..b0cb63e 100644 --- a/app/Models/Product/Email.php +++ b/app/Models/Product/Email.php @@ -5,6 +5,7 @@ namespace App\Models\Product; use Illuminate\Support\Collection; use App\Interfaces\ProductItem; +use App\Models\Invoice; use App\Models\Service\Email as ServiceEmail; use App\Models\Supplier\Email as SupplierEmail; @@ -15,6 +16,8 @@ final class Email extends Type implements ProductItem // The model that is referenced when this product is ordered protected string $order_model = ServiceEmail::class; + // When comparing billing/pricing/charging, what metric to normalise to + const DefaultBill = Invoice::BILL_YEARLY; // The model that the supplier supplies const SupplierModel = SupplierEmail::class; diff --git a/app/Models/Product/Generic.php b/app/Models/Product/Generic.php index e2a1984..0e290c1 100644 --- a/app/Models/Product/Generic.php +++ b/app/Models/Product/Generic.php @@ -5,6 +5,7 @@ namespace App\Models\Product; use Illuminate\Support\Collection; use App\Interfaces\ProductItem; +use App\Models\Invoice; use App\Models\Service\Generic as ServiceGeneric; use App\Models\Supplier\Generic as SupplierGeneric; @@ -15,6 +16,8 @@ final class Generic extends Type implements ProductItem // The model that is referenced when this product is ordered protected string $order_model = ServiceGeneric::class; + // When comparing billing/pricing/charging, what metric to normalise to + const DefaultBill = Invoice::BILL_MONTHLY; // The model that the supplier supplies const SupplierModel = SupplierGeneric::class; diff --git a/app/Models/Product/Host.php b/app/Models/Product/Host.php index 8f8ece6..df05a5a 100644 --- a/app/Models/Product/Host.php +++ b/app/Models/Product/Host.php @@ -5,6 +5,7 @@ namespace App\Models\Product; use Illuminate\Support\Collection; use App\Interfaces\ProductItem; +use App\Models\Invoice; use App\Models\Service\Host as ServiceHost; use App\Models\Supplier\Host as SupplierHost; @@ -15,6 +16,8 @@ final class Host extends Type implements ProductItem // The model that is referenced when this product is ordered protected string $order_model = ServiceHost::class; + // When comparing billing/pricing/charging, what metric to normalise to + const DefaultBill = Invoice::BILL_MONTHLY; // The model that the supplier supplies const SupplierModel = SupplierHost::class; diff --git a/app/Models/Product/Phone.php b/app/Models/Product/Phone.php index 6daa9ef..21b901a 100644 --- a/app/Models/Product/Phone.php +++ b/app/Models/Product/Phone.php @@ -5,6 +5,7 @@ namespace App\Models\Product; use Illuminate\Support\Collection; use App\Interfaces\ProductItem; +use App\Models\Invoice; use App\Models\Service\Phone as ServicePhone; use App\Models\Supplier\Phone as SupplierPhone; @@ -42,6 +43,8 @@ final class Phone extends Type implements ProductItem // The model that is referenced when this product is ordered protected string $order_model = ServicePhone::class; + // When comparing billing/pricing/charging, what metric to normalise to + const DefaultBill = Invoice::BILL_MONTHLY; // The model that the supplier supplies const SupplierModel = SupplierPhone::class; diff --git a/app/Models/Product/SSL.php b/app/Models/Product/SSL.php index 1e6f37d..d9c5d7f 100644 --- a/app/Models/Product/SSL.php +++ b/app/Models/Product/SSL.php @@ -5,6 +5,7 @@ namespace App\Models\Product; use Illuminate\Support\Collection; use App\Interfaces\ProductItem; +use App\Models\Invoice; use App\Models\Service\SSL as ServiceSSL; use App\Models\Supplier\SSL as SupplierSSL; @@ -15,6 +16,8 @@ final class SSL extends Type implements ProductItem // The model that is referenced when this product is ordered protected string $order_model = ServiceSSL::class; + // When comparing billing/pricing/charging, what metric to normalise to + const DefaultBill = Invoice::BILL_MONTHLY; // The model that the supplier supplies const SupplierModel = SupplierSSL::class; diff --git a/app/Models/Product/Type.php b/app/Models/Product/Type.php index 589a9ed..17a7d38 100644 --- a/app/Models/Product/Type.php +++ b/app/Models/Product/Type.php @@ -22,7 +22,7 @@ abstract class Type extends Model * * @return \Illuminate\Database\Eloquent\Relations\MorphMany */ - public function products() + final public function products() { return $this->morphMany(Product::class, null,'model','model_id'); } @@ -32,7 +32,7 @@ abstract class Type extends Model * * @return \Illuminate\Database\Eloquent\Relations\HasOne */ - public function supplied() + final public function supplied() { return $this->hasOne(static::SupplierModel,'id','supplier_item_id'); } @@ -61,4 +61,11 @@ abstract class Type extends Model abort(500,'use product->supplied->category_name'); return static::category_name; } + + /* METHODs */ + + final function normalizeBillingInterval(): int + { + return static::DefaultBill; + } } \ No newline at end of file diff --git a/app/Models/Service.php b/app/Models/Service.php index f7d3e14..25370ee 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -18,6 +18,7 @@ use Illuminate\Support\Facades\Auth; use Symfony\Component\HttpKernel\Exception\HttpException; use Leenooks\Carbon as LeenooksCarbon; +use App\Models\Product\Type; use App\Models\Scopes\SiteScope; use App\Interfaces\IDs; use App\Traits\ScopeServiceUserAuthorised; @@ -530,6 +531,7 @@ class Service extends Model implements IDs * * @return float * @throws Exception + * @deprecated use class::charge_normalized() */ public function getBillingMonthlyPriceAttribute(): float { @@ -809,7 +811,7 @@ class Service extends Model implements IDs * * @return Model */ - public function getOfferingAttribute(): Model + public function getOfferingAttribute(): Type { return $this->product->type; } @@ -1068,6 +1070,16 @@ class Service extends Model implements IDs return round($value*1.1,2); } + /** + * Return a normalize price dependent on the product, ie: Broadband = Monthly, Domain = Yearly, etc + * + * @return float + */ + public function charge_normalized(): float + { + return number_format($this->getBillingChargeAttribute()*Invoice::billing_change($this->recur_schedule,$this->offering->normalizeBillingInterval()),2); + } + private function getOrderInfoValue(string $key): ?string { return $this->order_info ? $this->order_info->get($key) : NULL; diff --git a/app/Models/Supplier/Broadband.php b/app/Models/Supplier/Broadband.php index 511527d..2b1fe2b 100644 --- a/app/Models/Supplier/Broadband.php +++ b/app/Models/Supplier/Broadband.php @@ -37,6 +37,10 @@ class Broadband extends Type implements SupplierItem /* INTERFACES */ + /** + * @return int + * @deprecated use Product::normalizeBillingInterval() + */ public function getBillingIntervalAttribute(): int { return 1; // Monthly diff --git a/public/plugin/dataTables/leftSearchPanes.css b/public/plugin/dataTables/leftSearchPanes.css new file mode 100644 index 0000000..52ae593 --- /dev/null +++ b/public/plugin/dataTables/leftSearchPanes.css @@ -0,0 +1,103 @@ +table.dataTable tr.dtrg-group.dtrg-level-1 td { + background-color: #e0e0e0; + color: #4c110f; +} + +/* RENDERING */ +div.dtsp-verticalPanes { + margin-right: 10px; +} + +div.dtsp-panesContainer { + margin-top: 0px; + margin-bottom: 0px; + width: 15em; +} + +div.dtsp-subRow1 { + width: 100%; +} + +div.dtsp-searchCont input.dtsp-search.dtsp-disabledButton { + background: #eaeaea; + font-size: larger; + border-radius: 3px; +} + +div.dtsp-searchCont input.dtsp-search.dtsp-disabledButton::placeholder, +div.dtsp-searchCont input.dtsp-search.dtsp-disabledButton:-moz-placeholder, +div.dtsp-searchCont input.dtsp-search.dtsp-disabledButton::-moz-placeholder, +div.dtsp-searchCont input.dtsp-search.dtsp-disabledButton::-webkit-input-placeholder { + color: #000000; + font-weight: bold; +} + +div.dtsp-titleRow { + margin-top: 13px; + padding: 5px; +} + +div.dtsp-titleRow button { + padding: 0 0 0 5px !important; + font-size: 90%; +} + +div.dtsp-titleRow div.dtsp-title { + padding: 1px; + margin: 0 !important; + font-weight: bolder; +} + +div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody div.dtsp-nameCont span.dtsp-pill { + min-width: 4em; +} + +div.dtsp-verticalContainer{ + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + align-content: flex-start; + align-items: flex-start; +} + +div.dtsp-verticalContainer div.dtsp-verticalPanes, +div.dtsp-verticalContainer div.dtsp-dataTable{ + width: 50%; + flex-grow: 0; + flex-shrink: 0; + flex-basis: 0; +} + +div.dtsp-verticalContainer div.dtsp-verticalPanes{ + background: rgba(33, 39, 45, 0.1); + border-radius: 6px; + border: 1px solid #ccc; +} + +div.dtsp-title { + margin-right: 0px !important; + margin-top: 13px !important; + margin-left: 5px !important; +} + +input.dtsp-search { + min-width: 0px !important; + padding-left: 0px !important; + margin: 0px !important; +} + +div.dtsp-verticalContainer div.dtsp-verticalPanes div.dtsp-searchPanes{ + flex-direction: column; + flex-basis: 0px; +} + +div.dtsp-verticalContainer div.dtsp-verticalPanes div.dtsp-searchPanes div.dtsp-searchPane{ + flex-basis: 0px; +} + +div.dtsp-verticalContainer div.dtsp-dataTable{ + flex-grow: 1; + flex-shrink: 0; + flex-basis: auto; +} diff --git a/resources/views/theme/backend/adminlte/product/details.blade.php b/resources/views/theme/backend/adminlte/product/details.blade.php index 6206f73..9e7351e 100644 --- a/resources/views/theme/backend/adminlte/product/details.blade.php +++ b/resources/views/theme/backend/adminlte/product/details.blade.php @@ -14,6 +14,12 @@ @endsection @section('main-content') +
+
+ @include('product.widget.selector') +
+
+
diff --git a/resources/views/theme/backend/adminlte/product/home.blade.php b/resources/views/theme/backend/adminlte/product/home.blade.php index 7d8f325..3fdff61 100644 --- a/resources/views/theme/backend/adminlte/product/home.blade.php +++ b/resources/views/theme/backend/adminlte/product/home.blade.php @@ -16,46 +16,7 @@ @section('main-content')
- -
-
-

Product Configuration

-
- -
-
- @csrf - -
-
- @include('adminlte::widget.form_select',[ - 'label'=>'Product', - 'icon'=>'fas fa-list', - 'id'=>'product_id', - 'old'=>'product_id', - 'name'=>'product_id', - 'groupby'=>'active', - 'options'=>\App\Models\Product::get()->sortBy(function($item) { return ($item->active ? '0' : '1').$item->name; })->transform(function($item) { return ['id'=>$item->id,'value'=>$item->name,'active'=>$item->active]; }), - 'value'=>'', - ]) -
-
-
-
-
+ @include('product.widget.selector')
-@endsection - -@section('page-scripts') - @css(select2) - @js(select2,autofocus) - - -@append \ No newline at end of file +@endsection \ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/product/widget/detail.blade.php b/resources/views/theme/backend/adminlte/product/widget/detail.blade.php index 772bbb6..d975205 100644 --- a/resources/views/theme/backend/adminlte/product/widget/detail.blade.php +++ b/resources/views/theme/backend/adminlte/product/widget/detail.blade.php @@ -53,64 +53,129 @@
- -
- @include('adminlte::widget.form_toggle',[ - 'label'=>'Active', - 'id'=>'active', - 'old'=>'active', - 'name'=>'active', - 'value'=>$o->active ?? '', - ]) -
-
+
+
+ +
+ @include('adminlte::widget.form_toggle',[ + 'label'=>'Active', + 'id'=>'active', + 'old'=>'active', + 'name'=>'active', + 'value'=>$o->active ?? '', + ]) +
+
-
- -
- @include('adminlte::widget.form_select',[ - 'label'=>'Product Type', - 'icon'=>'fas fa-list', - 'id'=>'model', - 'old'=>'model', - 'name'=>'model', - 'options'=>$o->availableTypes()->transform(function($item) { return ['id'=>$item,'value'=>$item]; }), - 'value'=>get_class($o->type), - ]) -
-
+
+ +
+ @include('adminlte::widget.form_select',[ + 'label'=>'Product Type', + 'icon'=>'fas fa-list', + 'id'=>'model', + 'old'=>'model', + 'name'=>'model', + 'options'=>\App\Models\Product::availableTypes()->transform(function($item) { return ['id'=>$item,'value'=>$item]; }), + 'value'=>get_class($o->type), + ]) +
+
-
- -
-
- -
-
- +
+ +
+
+ +
+
+ +
+ + + @error('model_id') + {{ $message }} + @enderror + +
- - - @error('model_id') - {{ $message }} - @enderror - +
+
+ +
+ +
+ @include('adminlte::widget.form_text',[ + 'label'=>'Accounting', + 'icon'=>'fas fa-calculator', + 'id'=>'accounting', + 'old'=>'accounting', + 'name'=>'accounting', + 'value'=>$o->accounting ?? '', + ])
-
-
- -
- @include('adminlte::widget.form_text',[ - 'label'=>'Accounting', - 'icon'=>'fas fa-calculator', - 'id'=>'accounting', - 'old'=>'accounting', - 'name'=>'accounting', - 'value'=>$o->accounting ?? '', - ]) +
+ Pricing Ex Taxes +
+ + + +
+ @foreach(\App\Models\Group::pricing()->active()->get() as $go) +
+ + @foreach(\App\Models\Invoice::billing_periods as $bp=>$detail) +
+
+
+ +
+
+ +
+ charge_available($bp,$go)) ? 'value' : 'placeholder' }}="{{ $c=$o->charge($bp,$go,'base') }}" @if(is_null($c) || ! $ca) disabled @endif> +
+
+ +
+
+ + @error($x) + {{ $message }} + @enderror + +
+
+
+ +
+
+ +
+
+ +
+ + + @error($x) + {{ $message }} + @enderror + +
+
+
+
+ @endforeach +
+ @endforeach +
@@ -183,6 +248,15 @@ $('#supplier_product').hide(); else supplier_products($('#model').val(),$('#model_id'),{{ old('model_id',$o->model_id) }}); + + $('input[type=checkbox]').on('click',function(item) { + var input = $(this).parent().parent().parent().find('input[type="text"]'); + input.prop('disabled',(i,v)=>!v); + + // Find the setup input and toggle it + input = $('#'+input.attr('id').replace('base','setup')+''); + input.prop('disabled',(i,v)=>!v); + }) }); @append \ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/product/widget/selector.blade.php b/resources/views/theme/backend/adminlte/product/widget/selector.blade.php new file mode 100644 index 0000000..dfed821 --- /dev/null +++ b/resources/views/theme/backend/adminlte/product/widget/selector.blade.php @@ -0,0 +1,40 @@ + +
+
+

Product Configuration

+
+ +
+
+ @csrf + +
+
+ @include('adminlte::widget.form_select',[ + 'label'=>'Product', + 'icon'=>'fas fa-list', + 'id'=>'product_id', + 'old'=>'product_id', + 'name'=>'product_id', + 'groupby'=>'active', + 'options'=>\App\Models\Product::get()->sortBy(function($item) { return ($item->active ? '0' : '1').$item->name; })->transform(function($item) { return ['id'=>$item->id,'value'=>$item->name,'active'=>$item->active]; }), + 'value'=>isset($o) ? $o->id : NULL, + ]) +
+
+
+
+
+ +@section('page-scripts') + @css(select2) + @js(select2,autofocus) + + +@append \ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/product/widget/services.blade.php b/resources/views/theme/backend/adminlte/product/widget/services.blade.php index 7adb7c7..d495161 100644 --- a/resources/views/theme/backend/adminlte/product/widget/services.blade.php +++ b/resources/views/theme/backend/adminlte/product/widget/services.blade.php @@ -1,8 +1,8 @@
@if(count($o->services)) -
- +
+
@@ -10,7 +10,9 @@ - + + + @@ -22,7 +24,9 @@ - + + + @endforeach @@ -32,4 +36,70 @@ @else

No services use this product.

@endif - \ No newline at end of file + + +@section('page-scripts') + @css(datatables,bootstrap4|rowgroup|select|searchpanes|searchpanes-left) + @js(datatables,bootstrap4|rowgroup|select|searchpanes) + + + + +@append \ No newline at end of file
IDDate Stop Data Invoiced ActiveChargeNormalized
Charge
Normalized
Cost
 
{{ $so->stop_at ? $so->stop_at->format('Y-m-d') : '-' }} {{ $so->invoice_to ? $so->invoice_to->format('Y-m-d') : '-' }} {{ $so->active ? 'YES' : 'NO' }}{{ number_format($so->billing_charge,2) }}{{ $a=number_format($so->charge_normalized(),2) }}{{ $b=number_format($so->product->cost_normalized(),2) }}