Updates to Product Model, product updates, enable pricing update, improved formating of product services
This commit is contained in:
parent
95bb55aad8
commit
0f91ce4940
@ -97,6 +97,18 @@ class ProductController extends Controller
|
|||||||
|
|
||||||
$o->active = (bool)$request->active;
|
$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 {
|
try {
|
||||||
$o->save();
|
$o->save();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
@ -35,6 +35,7 @@ class ProductAddEdit extends FormRequest
|
|||||||
'model' => 'sometimes|string', // @todo Check that it is a valid model type
|
'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
|
'model_id' => 'sometimes|int', // @todo Check that it is a valid model type
|
||||||
'accounting' => 'nullable|string',
|
'accounting' => 'nullable|string',
|
||||||
|
'pricing' => 'required|array', // @todo Validate the elements in the pricing
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -33,6 +33,7 @@ interface SupplierItem
|
|||||||
* Return the billing interval that the supplier charges
|
* Return the billing interval that the supplier charges
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
|
* @deprecated use Product::normalizeBillingInterval()
|
||||||
*/
|
*/
|
||||||
public function getBillingIntervalAttribute(): int;
|
public function getBillingIntervalAttribute(): int;
|
||||||
|
|
||||||
|
@ -4,8 +4,19 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Leenooks\Traits\ScopeActive;
|
||||||
|
|
||||||
class Group extends Model
|
class Group extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory, ScopeActive;
|
||||||
|
|
||||||
|
/* SCOPES */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function scopePricing()
|
||||||
|
{
|
||||||
|
return $this->where('pricing',TRUE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ use Illuminate\Support\Collection;
|
|||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Leenooks\Traits\ScopeActive;
|
use Leenooks\Traits\ScopeActive;
|
||||||
|
|
||||||
use App\Interfaces\{IDs,ProductItem};
|
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.
|
* 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.
|
* 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:
|
* Attributes for products:
|
||||||
* + lid : Local ID for product (part number)
|
* + lid : Local ID for product (part number)
|
||||||
* + sid : System ID for product (part number)
|
* + sid : System ID for product (part number)
|
||||||
* + category : Type of product supplied
|
* + category : Type of product supplied
|
||||||
* + category_name : Type of product supplied (Friendly Name for display, not for internal logic)
|
* + 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
|
* + supplier : Supplier for this offering
|
||||||
* + name : Brief Name for our product with name_detail
|
* + name : Brief Name for our product with name_detail
|
||||||
* + name_short : Product ID for our Product (description.name => name_short)
|
* + 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
|
* + setup_charge_taxable : Charge to setup this product including taxes
|
||||||
* + base_charge : Default billing amount
|
* + base_charge : Default billing amount
|
||||||
* + base_charge_taxable : Default billing amount including taxes
|
* + base_charge_taxable : Default billing amount including taxes
|
||||||
* + min_charge : Minimum cost taking into account billing interval and setup costs
|
* + min_charge : Minimum charge taking into account billing interval and setup charges
|
||||||
* + min_charge_taxable : Minimum cost taking into account billing interval and setup costs including taxes
|
* + min_charge_taxable : Minimum charge taking into account billing interval and setup charges including taxes
|
||||||
* + type : Returns the underlying product object, representing the type of product
|
|
||||||
*
|
*
|
||||||
* Attributes for product types (type - Product/*)
|
* Attributes for product types (type - Product/*)
|
||||||
* + name : Short Name for our Product
|
* + name : Short Name for our Product
|
||||||
@ -74,7 +75,37 @@ class Product extends Model implements IDs
|
|||||||
'pricing'=>'collection',
|
'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 */
|
/* RELATIONS */
|
||||||
|
|
||||||
@ -349,33 +380,38 @@ class Product extends Model implements IDs
|
|||||||
/* METHODS */
|
/* 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()))
|
return Arr::get($this->pricing,sprintf('%d.%d.%s',$timeperiod,$go->id,$type));
|
||||||
->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;
|
/**
|
||||||
});
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
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 int|NULL $timeperiod
|
||||||
* @param Group|NULL $go
|
* @param Group|NULL $go
|
||||||
* @return float
|
* @return float
|
||||||
|
* @todo use self::charge()
|
||||||
*/
|
*/
|
||||||
private function getCharge(string $type,int $timeperiod=NULL,Group $go=NULL): float
|
private function getCharge(string $type,int $timeperiod=NULL,Group $go=NULL): float
|
||||||
{
|
{
|
||||||
@ -407,17 +444,10 @@ class Product extends Model implements IDs
|
|||||||
$alt_tp--;
|
$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);
|
$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);
|
return round($price,2);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ use Illuminate\Support\Collection;
|
|||||||
use Leenooks\Traits\ScopeActive;
|
use Leenooks\Traits\ScopeActive;
|
||||||
|
|
||||||
use App\Interfaces\ProductItem;
|
use App\Interfaces\ProductItem;
|
||||||
|
use App\Models\Invoice;
|
||||||
use App\Models\Service\Broadband as ServiceBroadband;
|
use App\Models\Service\Broadband as ServiceBroadband;
|
||||||
use App\Models\Supplier\Broadband as SupplierBroadband;
|
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
|
// The model that is referenced when this product is ordered
|
||||||
protected string $order_model = ServiceBroadband::class;
|
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
|
// The model that the supplier supplies
|
||||||
const SupplierModel = SupplierBroadband::class;
|
const SupplierModel = SupplierBroadband::class;
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Models\Product;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
use App\Interfaces\ProductItem;
|
use App\Interfaces\ProductItem;
|
||||||
|
use App\Models\Invoice;
|
||||||
use App\Models\Service\Domain as ServiceDomain;
|
use App\Models\Service\Domain as ServiceDomain;
|
||||||
use App\Models\Supplier\Domain as SupplierDomain;
|
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
|
// The model that is referenced when this product is ordered
|
||||||
protected string $order_model = ServiceDomain::class;
|
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
|
// The model that the supplier supplies
|
||||||
const SupplierModel = SupplierDomain::class;
|
const SupplierModel = SupplierDomain::class;
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Models\Product;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
use App\Interfaces\ProductItem;
|
use App\Interfaces\ProductItem;
|
||||||
|
use App\Models\Invoice;
|
||||||
use App\Models\Service\Email as ServiceEmail;
|
use App\Models\Service\Email as ServiceEmail;
|
||||||
use App\Models\Supplier\Email as SupplierEmail;
|
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
|
// The model that is referenced when this product is ordered
|
||||||
protected string $order_model = ServiceEmail::class;
|
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
|
// The model that the supplier supplies
|
||||||
const SupplierModel = SupplierEmail::class;
|
const SupplierModel = SupplierEmail::class;
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Models\Product;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
use App\Interfaces\ProductItem;
|
use App\Interfaces\ProductItem;
|
||||||
|
use App\Models\Invoice;
|
||||||
use App\Models\Service\Generic as ServiceGeneric;
|
use App\Models\Service\Generic as ServiceGeneric;
|
||||||
use App\Models\Supplier\Generic as SupplierGeneric;
|
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
|
// The model that is referenced when this product is ordered
|
||||||
protected string $order_model = ServiceGeneric::class;
|
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
|
// The model that the supplier supplies
|
||||||
const SupplierModel = SupplierGeneric::class;
|
const SupplierModel = SupplierGeneric::class;
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Models\Product;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
use App\Interfaces\ProductItem;
|
use App\Interfaces\ProductItem;
|
||||||
|
use App\Models\Invoice;
|
||||||
use App\Models\Service\Host as ServiceHost;
|
use App\Models\Service\Host as ServiceHost;
|
||||||
use App\Models\Supplier\Host as SupplierHost;
|
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
|
// The model that is referenced when this product is ordered
|
||||||
protected string $order_model = ServiceHost::class;
|
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
|
// The model that the supplier supplies
|
||||||
const SupplierModel = SupplierHost::class;
|
const SupplierModel = SupplierHost::class;
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Models\Product;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
use App\Interfaces\ProductItem;
|
use App\Interfaces\ProductItem;
|
||||||
|
use App\Models\Invoice;
|
||||||
use App\Models\Service\Phone as ServicePhone;
|
use App\Models\Service\Phone as ServicePhone;
|
||||||
use App\Models\Supplier\Phone as SupplierPhone;
|
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
|
// The model that is referenced when this product is ordered
|
||||||
protected string $order_model = ServicePhone::class;
|
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
|
// The model that the supplier supplies
|
||||||
const SupplierModel = SupplierPhone::class;
|
const SupplierModel = SupplierPhone::class;
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Models\Product;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
use App\Interfaces\ProductItem;
|
use App\Interfaces\ProductItem;
|
||||||
|
use App\Models\Invoice;
|
||||||
use App\Models\Service\SSL as ServiceSSL;
|
use App\Models\Service\SSL as ServiceSSL;
|
||||||
use App\Models\Supplier\SSL as SupplierSSL;
|
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
|
// The model that is referenced when this product is ordered
|
||||||
protected string $order_model = ServiceSSL::class;
|
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
|
// The model that the supplier supplies
|
||||||
const SupplierModel = SupplierSSL::class;
|
const SupplierModel = SupplierSSL::class;
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ abstract class Type extends Model
|
|||||||
*
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||||
*/
|
*/
|
||||||
public function products()
|
final public function products()
|
||||||
{
|
{
|
||||||
return $this->morphMany(Product::class, null,'model','model_id');
|
return $this->morphMany(Product::class, null,'model','model_id');
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ abstract class Type extends Model
|
|||||||
*
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||||
*/
|
*/
|
||||||
public function supplied()
|
final public function supplied()
|
||||||
{
|
{
|
||||||
return $this->hasOne(static::SupplierModel,'id','supplier_item_id');
|
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');
|
abort(500,'use product->supplied->category_name');
|
||||||
return static::category_name;
|
return static::category_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* METHODs */
|
||||||
|
|
||||||
|
final function normalizeBillingInterval(): int
|
||||||
|
{
|
||||||
|
return static::DefaultBill;
|
||||||
|
}
|
||||||
}
|
}
|
@ -18,6 +18,7 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
use Leenooks\Carbon as LeenooksCarbon;
|
use Leenooks\Carbon as LeenooksCarbon;
|
||||||
|
|
||||||
|
use App\Models\Product\Type;
|
||||||
use App\Models\Scopes\SiteScope;
|
use App\Models\Scopes\SiteScope;
|
||||||
use App\Interfaces\IDs;
|
use App\Interfaces\IDs;
|
||||||
use App\Traits\ScopeServiceUserAuthorised;
|
use App\Traits\ScopeServiceUserAuthorised;
|
||||||
@ -530,6 +531,7 @@ class Service extends Model implements IDs
|
|||||||
*
|
*
|
||||||
* @return float
|
* @return float
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
|
* @deprecated use class::charge_normalized()
|
||||||
*/
|
*/
|
||||||
public function getBillingMonthlyPriceAttribute(): float
|
public function getBillingMonthlyPriceAttribute(): float
|
||||||
{
|
{
|
||||||
@ -809,7 +811,7 @@ class Service extends Model implements IDs
|
|||||||
*
|
*
|
||||||
* @return Model
|
* @return Model
|
||||||
*/
|
*/
|
||||||
public function getOfferingAttribute(): Model
|
public function getOfferingAttribute(): Type
|
||||||
{
|
{
|
||||||
return $this->product->type;
|
return $this->product->type;
|
||||||
}
|
}
|
||||||
@ -1068,6 +1070,16 @@ class Service extends Model implements IDs
|
|||||||
return round($value*1.1,2);
|
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
|
private function getOrderInfoValue(string $key): ?string
|
||||||
{
|
{
|
||||||
return $this->order_info ? $this->order_info->get($key) : NULL;
|
return $this->order_info ? $this->order_info->get($key) : NULL;
|
||||||
|
@ -37,6 +37,10 @@ class Broadband extends Type implements SupplierItem
|
|||||||
|
|
||||||
/* INTERFACES */
|
/* INTERFACES */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
* @deprecated use Product::normalizeBillingInterval()
|
||||||
|
*/
|
||||||
public function getBillingIntervalAttribute(): int
|
public function getBillingIntervalAttribute(): int
|
||||||
{
|
{
|
||||||
return 1; // Monthly
|
return 1; // Monthly
|
||||||
|
103
public/plugin/dataTables/leftSearchPanes.css
vendored
Normal file
103
public/plugin/dataTables/leftSearchPanes.css
vendored
Normal file
@ -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;
|
||||||
|
}
|
@ -14,6 +14,12 @@
|
|||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('main-content')
|
@section('main-content')
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
@include('product.widget.selector')
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -16,46 +16,7 @@
|
|||||||
@section('main-content')
|
@section('main-content')
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
@include('product.widget.selector')
|
||||||
<div class="card card-dark">
|
|
||||||
<div class="card-header">
|
|
||||||
<h1 class="card-title">Product Configuration</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="g-0 needs-validation" method="POST" enctype="multipart/form-data" role="form">
|
|
||||||
@csrf
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-sm-9 col-md-6 col-xl-5">
|
|
||||||
@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'=>'',
|
|
||||||
])
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('page-scripts')
|
|
||||||
@css(select2)
|
|
||||||
@js(select2,autofocus)
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(document).ready(function() {
|
|
||||||
$('#product_id').on('change',function(item) {
|
|
||||||
window.location.href = '{{ url('a/product/details') }}/'+item.target.value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@append
|
|
@ -52,9 +52,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Active -->
|
<!-- Active -->
|
||||||
<div class="col-3">
|
<div class="col-6">
|
||||||
@include('adminlte::widget.form_toggle',[
|
@include('adminlte::widget.form_toggle',[
|
||||||
'label'=>'Active',
|
'label'=>'Active',
|
||||||
'id'=>'active',
|
'id'=>'active',
|
||||||
@ -67,14 +69,14 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Product Type -->
|
<!-- Product Type -->
|
||||||
<div class="col-12 col-sm-9 col-md-12 col-xl-6">
|
<div class="col-12">
|
||||||
@include('adminlte::widget.form_select',[
|
@include('adminlte::widget.form_select',[
|
||||||
'label'=>'Product Type',
|
'label'=>'Product Type',
|
||||||
'icon'=>'fas fa-list',
|
'icon'=>'fas fa-list',
|
||||||
'id'=>'model',
|
'id'=>'model',
|
||||||
'old'=>'model',
|
'old'=>'model',
|
||||||
'name'=>'model',
|
'name'=>'model',
|
||||||
'options'=>$o->availableTypes()->transform(function($item) { return ['id'=>$item,'value'=>$item]; }),
|
'options'=>\App\Models\Product::availableTypes()->transform(function($item) { return ['id'=>$item,'value'=>$item]; }),
|
||||||
'value'=>get_class($o->type),
|
'value'=>get_class($o->type),
|
||||||
])
|
])
|
||||||
</div>
|
</div>
|
||||||
@ -82,7 +84,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Supplied Product -->
|
<!-- Supplied Product -->
|
||||||
<div class="col-12 col-sm-9 col-md-12 col-xl-6" id="supplier_product">
|
<div class="col-12" id="supplier_product">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="model_id">Supplied Product</label>
|
<label for="model_id">Supplied Product</label>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
@ -102,7 +104,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Accounting -->
|
<!-- Accounting -->
|
||||||
<div class="col-12 col-sm-9 col-md-12 col-xl-6">
|
<div class="col-12">
|
||||||
@include('adminlte::widget.form_text',[
|
@include('adminlte::widget.form_text',[
|
||||||
'label'=>'Accounting',
|
'label'=>'Accounting',
|
||||||
'icon'=>'fas fa-calculator',
|
'icon'=>'fas fa-calculator',
|
||||||
@ -113,6 +115,69 @@
|
|||||||
])
|
])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 offset-md-4 col-md-5">
|
||||||
|
<span class="h5">Pricing</span><small> Ex Taxes</small>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<ul class="nav nav-pills w-100 pl-0 pt-2 pb-2">
|
||||||
|
@foreach(\App\Models\Group::pricing()->active()->get() as $go)
|
||||||
|
<li class="nav-item"><a class="nav-link @if(! $loop->index)active @endif" href="#pg_{{ $go->id }}" data-toggle="tab">{{ $go->name }}</a></li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
@foreach(\App\Models\Group::pricing()->active()->get() as $go)
|
||||||
|
<div class="tab-pane fade @if(! $loop->index)show active @endif" id="pg_{{ $go->id }}" role="tabpanel">
|
||||||
|
|
||||||
|
@foreach(\App\Models\Invoice::billing_periods as $bp=>$detail)
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ $x=sprintf('base_%d_%d',$bp,$go->id) }}">{{ $detail['name'] }} Reoccurring</label>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa-fw fas fa-calculator"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control text-right @error($x) is-invalid @enderror" id="{{ $x }}" name="{{ sprintf('pricing[%d][%d][base]',$bp,$go->id) }}" {{ ($ca=$o->charge_available($bp,$go)) ? 'value' : 'placeholder' }}="{{ $c=$o->charge($bp,$go,'base') }}" @if(is_null($c) || ! $ca) disabled @endif>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<div class="input-group-text">
|
||||||
|
<input type="checkbox" name="{{ sprintf('pricing[%d][show]',$bp) }}" @if($c && $ca)checked @endif>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="invalid-feedback" role="alert">
|
||||||
|
@error($x)
|
||||||
|
{{ $message }}
|
||||||
|
@enderror
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ $x=sprintf('setup_%d_%d',$bp,$go->id) }}">{{ $detail['name'] }} Reoccurring</label>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa-fw fas fa-cog"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control text-right @error($x) is-invalid @enderror" id="{{ $x }}" name="{{ sprintf('pricing[%d][%d][setup]',$bp,$go->id) }}" {{ $ca ? 'value' : 'placeholder' }}="{{ $c=$o->charge($bp,$go,'setup') }}" @if(is_null($c) || ! $ca) disabled @endif>
|
||||||
|
<span class="invalid-feedback" role="alert">
|
||||||
|
@error($x)
|
||||||
|
{{ $message }}
|
||||||
|
@enderror
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
@ -183,6 +248,15 @@
|
|||||||
$('#supplier_product').hide();
|
$('#supplier_product').hide();
|
||||||
else
|
else
|
||||||
supplier_products($('#model').val(),$('#model_id'),{{ old('model_id',$o->model_id) }});
|
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);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@append
|
@append
|
@ -0,0 +1,40 @@
|
|||||||
|
<!-- $o = Product::class -->
|
||||||
|
<div class="card card-dark">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="card-title">Product Configuration</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="g-0 needs-validation" method="POST" enctype="multipart/form-data" role="form">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-sm-9 col-md-6 col-xl-5">
|
||||||
|
@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,
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section('page-scripts')
|
||||||
|
@css(select2)
|
||||||
|
@js(select2,autofocus)
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#product_id').on('change',function(item) {
|
||||||
|
window.location.href = '{{ url('a/product/details') }}/'+item.target.value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@append
|
@ -1,8 +1,8 @@
|
|||||||
<!-- $o = Product::class -->
|
<!-- $o = Product::class -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@if(count($o->services))
|
@if(count($o->services))
|
||||||
<div class="col-6">
|
<div class="col-7">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover w-100" id="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
@ -10,7 +10,9 @@
|
|||||||
<th>Date Stop</th>
|
<th>Date Stop</th>
|
||||||
<th>Data Invoiced</th>
|
<th>Data Invoiced</th>
|
||||||
<th>Active</th>
|
<th>Active</th>
|
||||||
<th class="text-right">Charge</th>
|
<th class="text-right"><small>Normalized</small><br>Charge</th>
|
||||||
|
<th class="text-right"><small>Normalized</small><br>Cost</th>
|
||||||
|
<th> </th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
@ -22,7 +24,9 @@
|
|||||||
<td>{{ $so->stop_at ? $so->stop_at->format('Y-m-d') : '-' }}</td>
|
<td>{{ $so->stop_at ? $so->stop_at->format('Y-m-d') : '-' }}</td>
|
||||||
<td>{{ $so->invoice_to ? $so->invoice_to->format('Y-m-d') : '-' }}</td>
|
<td>{{ $so->invoice_to ? $so->invoice_to->format('Y-m-d') : '-' }}</td>
|
||||||
<td>{{ $so->active ? 'YES' : 'NO' }}</td>
|
<td>{{ $so->active ? 'YES' : 'NO' }}</td>
|
||||||
<td class="text-right">{{ number_format($so->billing_charge,2) }}</td>
|
<td class="text-right">{{ $a=number_format($so->charge_normalized(),2) }}</td>
|
||||||
|
<td class="text-right">{{ $b=number_format($so->product->cost_normalized(),2) }}</td>
|
||||||
|
<td><button class="btn btn-sm @if($a<$b)btn-danger @else btn-success @endif"><small>@if($a<$b)<i class="fas fa-fw fa-exclamation"></i> @else <i class="fas fa-fw fa-check"></i> @endif</small></button></td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -33,3 +37,69 @@
|
|||||||
<p>No services use this product.</p>
|
<p>No services use this product.</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@section('page-scripts')
|
||||||
|
@css(datatables,bootstrap4|rowgroup|select|searchpanes|searchpanes-left)
|
||||||
|
@js(datatables,bootstrap4|rowgroup|select|searchpanes)
|
||||||
|
|
||||||
|
<style>
|
||||||
|
tr.odd td:first-child,
|
||||||
|
tr.even td:first-child {
|
||||||
|
padding-left: 3em;
|
||||||
|
}
|
||||||
|
table.dataTable tr.dtrg-group.dtrg-level-1 td {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
color: #4c110f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
.strike {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
@if(count($o->services))
|
||||||
|
$('#table').DataTable({
|
||||||
|
paging: true,
|
||||||
|
pageLength: 25,
|
||||||
|
lengthChange: true,
|
||||||
|
searching: true,
|
||||||
|
ordering: true,
|
||||||
|
info: true,
|
||||||
|
autoWidth: false,
|
||||||
|
fixedHeader: true,
|
||||||
|
order: [[4,'desc'],[0,'asc']],
|
||||||
|
rowGroup: {
|
||||||
|
dataSrc: 4,
|
||||||
|
},
|
||||||
|
columnDefs: [
|
||||||
|
{
|
||||||
|
targets: [4],
|
||||||
|
visible: false,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
language: {
|
||||||
|
searchPanes: {
|
||||||
|
clearMessage: 'Clear',
|
||||||
|
title: 'Filters: %d',
|
||||||
|
collapse: 'Filter',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchPanes: {
|
||||||
|
cascadePanes: true,
|
||||||
|
viewTotal: true,
|
||||||
|
layout: 'columns-1',
|
||||||
|
dataLength: 20,
|
||||||
|
controls: false,
|
||||||
|
},
|
||||||
|
dom: '<"dtsp-verticalContainer"<"dtsp-verticalPanes"P><"dtsp-dataTable"Bfrtip>>',
|
||||||
|
});
|
||||||
|
|
||||||
|
$('tbody').on('click','tr', function () {
|
||||||
|
$(this).toggleClass('selected');
|
||||||
|
});
|
||||||
|
@endif
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@append
|
Loading…
Reference in New Issue
Block a user