Compare commits

...

7 Commits

32 changed files with 382 additions and 130 deletions

View File

@ -10,32 +10,39 @@ use App\Models\{Site,Supplier,User};
class SupplierAccountSync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'supplier:account:sync'
.' {siteid : Site ID}'
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'supplier:account:sync'
.' {supplier : Supplier Name}'
.' {--f|forceprod : Force Prod API on dev environment}';
.' {--f|forceprod : Force Prod API on dev environment}'
.' {--s|site : Site ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync accounts with a supplier';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync accounts with a supplier';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
Config::set('site',Site::findOrFail($this->argument('siteid')));
$so = Supplier::where('name',$this->argument('supplier'))->singleOrFail();
/**
* Execute the console command.
*
* @return int
* @note Suppliers are now associated with accounts, so this needs to be updated
*/
public function handle()
{
Config::set(
'site',
$this->option('site')
? Site::findOrFail($this->option('site'))
: Site::where('url',config('app.url'))->sole()
);
$so = Supplier::where('name','ilike',strtolower($this->argument('supplier')))->sole();
foreach ($so->API($this->option('forceprod'))->getCustomers(['fetchall'=>true]) as $customer) {
// Check if we have this customer already (by ID)
@ -43,7 +50,6 @@ class SupplierAccountSync extends Command
$this->info(sprintf('User already linked (%s:%s)',$customer->id,$customer->email));
} elseif ($x=User::where('email',$customer->email)->single()) {
//dump($x->suppliers->first());
if ($x->suppliers->count()) {
$this->alert(sprintf('User [%d:%s] already linked to this supplier with ID (%s)',$customer->id,$customer->email,$x->suppliers->first()->pivot->id));
@ -63,5 +69,7 @@ class SupplierAccountSync extends Command
$this->error(sprintf('User doesnt exist with email (%s:%s)',$customer->id,$customer->email));
}
}
}
return self::SUCCESS;
}
}

View File

@ -3,9 +3,10 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\{Site,Supplier};
use App\Jobs\SupplierDomainSync as Job;
use App\Models\{Site,Supplier};
class SupplierDomainSync extends Command
{
@ -15,9 +16,9 @@ class SupplierDomainSync extends Command
* @var string
*/
protected $signature = 'supplier:domain:sync'
.' {siteid : Site ID}'
.' {supplier : Supplier Name}'
.' {--f|forceprod : Force Prod API on dev environment}';
.' {supplier : Supplier Name}'
.' {--f|forceprod : Force Prod API on dev environment}'
.' {--s|site : Site ID}';
/**
* The console command description.
@ -33,8 +34,15 @@ class SupplierDomainSync extends Command
*/
public function handle()
{
Config::set(
'site',
($x=$this->option('site')
? Site::findOrFail($this->option('site'))
: Site::where('url',config('app.url'))->sole())
);
$so = Supplier::where('name',$this->argument('supplier'))->singleOrFail();
Job::dispatchSync(Site::findOrFail($this->argument('siteid')),$so,$this->option('forceprod'));
return Job::dispatchSync($x,$so,$this->option('forceprod'));
}
}

View File

@ -27,7 +27,7 @@ class SocialLoginController extends Controller
$openiduser = Socialite::with($provider)->user();
if (! $openiduser)
return redirect('/home')
return redirect('home')
->with('error','No user details obtained.');
$oo = ProviderOauth::firstOrCreate(['name'=>$provider,'active'=>TRUE]);
@ -43,7 +43,7 @@ class SocialLoginController extends Controller
$user = User::where('email',$openiduser->email)->first();
if ((! $user) || (! $user->active))
return redirect('/login')
return redirect('login')
->with('error','Invalid account, or account inactive, please contact an admin.');
return $this->link($provider,$aoo,$user);
@ -54,7 +54,7 @@ class SocialLoginController extends Controller
// If there are too many users, then we have a problem
} elseif ($aoo->count() > 1) {
return redirect('/login')
return redirect('login')
->with('error','Seems you have multiple oauth IDs, please contact an admin.');
// User is using OAUTH for the first time.
@ -73,11 +73,11 @@ class SocialLoginController extends Controller
// If there are too many users, then we have a problem
} elseif ($uo->count() > 1) {
return redirect('/login')
return redirect('login')
->with('error','Seems you have multiple accounts, please contact an admin.');
} else {
return redirect('/login')
return redirect('login')
->with('error','Seems you dont have an account with that email, please contact an admin.');
}
}
@ -91,7 +91,7 @@ class SocialLoginController extends Controller
$openiduser = Socialite::with($provider)->user();
if (! $openiduser)
return redirect('/home')
return redirect('home')
->with('error','No user details obtained.');
$po = ProviderOauth::where('name',$provider)->singleOrFail();
@ -137,12 +137,12 @@ class SocialLoginController extends Controller
// Check our email matches
if (Arr::get($aoo->oauth_data,'email','invalid') !== $request->post('email'))
return redirect('/login')
return redirect('login')
->with('error','Account details didnt match to make link.');
// Check our token matches
if ($aoo->link_token !== $request->post('token'))
return redirect('/login')
return redirect('login')
->with('error','Token details didnt match to make link.');
// Load our email.

View File

@ -34,8 +34,7 @@ class CheckoutController extends Controller
}
return $o->wasRecentlyCreated
? redirect()
->to('a/checkout/'.$o->id)
? redirect('a/checkout/'.$o->id)
->with('success','Checkout added')
: redirect()
->back()

View File

@ -50,7 +50,7 @@ class HomeController extends Controller
} catch (ExpiredInviteCode $e) {
Log::alert(sprintf('User is using an expired token for invoice [%s] using [%s]',$o->id,$code));
return redirect()->to('/login');
return redirect('login');
} catch (DoormanException $e) {
Log::alert(sprintf('An attempt to read invoice id [%s] using [%s]',$o->id,$code));
@ -72,6 +72,6 @@ class HomeController extends Controller
*/
public function service_progress(Service $o,string $status)
{
return redirect()->to($o->action($status) ?: url('u/service',$o->id));
return redirect($o->action($status) ?: url('u/service',$o->id));
}
}

View File

@ -63,8 +63,7 @@ class PaymentController extends Controller
}
return $o->wasRecentlyCreated
? redirect()
->to('r/payment/'.$o->id)
? redirect('r/payment/'.$o->id)
->with('success','Payment added')
: redirect()
->back()

View File

@ -34,8 +34,7 @@ class PaypalController extends Controller
public function cancel()
{
return redirect()
->to(self::cart_url);
return redirect(self::cart_url);
}
/**
@ -52,8 +51,7 @@ class PaypalController extends Controller
$cart = request()->session()->get('invoice.cart');
if (! $cart)
return redirect()
->to('u/home');
return redirect('home');
$invoices = Invoice::find($cart);
@ -113,15 +111,13 @@ class PaypalController extends Controller
} catch (HttpException $e) {
Log::error('Paypal Exception',['request'=>$paypal,'response'=>$e->getMessage()]);
return redirect()
->to(self::cart_url)
return redirect(self::cart_url)
->withErrors('Paypal Exception: '.$e->getCode());
} catch (\HttpException $e) {
Log::error('HTTP Exception',['request'=>$this->client,'response'=>$e->getMessage()]);
return redirect()
->to(self::cart_url)
return redirect(self::cart_url)
->withErrors('HTTP Exception: '.$e->getCode());
}
@ -138,8 +134,7 @@ class PaypalController extends Controller
return redirect()
->away($redirect_url);
return redirect()
->to(self::cart_url)
return redirect(self::cart_url)
->withErrors('An error occurred with Paypal?');
}
@ -192,23 +187,20 @@ class PaypalController extends Controller
->away($redirect_url);
}
return redirect()
->to(self::cart_url)
return redirect(self::cart_url)
->withErrors('An error occurred with Paypal?');
} catch (\HttpException $e) {
Log::error('HTTP Exception',['request'=>$paypal,'response'=>$e->getMessage()]);
return redirect()
->to(self::cart_url)
return redirect(self::cart_url)
->withErrors('HTTP Exception: '.$e->getCode());
}
if ((! $response) || (! $response->result->purchase_units)) {
Log::error('Paypal Capture: No Purchase Units?');
return redirect()
->to(self::cart_url)
return redirect(self::cart_url)
->withErrors('Paypal Exception: NPU');
}
@ -267,8 +259,7 @@ class PaypalController extends Controller
Log::info('Paypal Payment Recorded',['po'=>$po->id]);
return redirect()
->to('u/home')
return redirect('home')
->with('success','Payment recorded thank you.');
}
}

View File

@ -113,7 +113,7 @@ class ServiceController extends Controller
$np->pivot->effective_at = Carbon::now();
$np->pivot->save();
return redirect()->to(url('u/service',[$o->id]));
return redirect(url('u/service',[$o->id]));
}
return view('theme.backend.adminlte.service.change_pending')
@ -191,7 +191,7 @@ class ServiceController extends Controller
// An Error Condition
if (is_null($result))
return redirect()->to('u/service/'.$o->id);
return redirect('u/service/'.$o->id);
elseif ($result instanceof RedirectResponse)
return $result;
@ -213,7 +213,7 @@ class ServiceController extends Controller
$stage = ''; // @todo this is temporary, we havent written the code to automatically jump to the next stage if wecan
}
return redirect()->to('u/service/'.$o->id);
return redirect('u/service/'.$o->id);
}
/**
@ -412,7 +412,10 @@ class ServiceController extends Controller
'suspend_billing' => 'nullable|in:on',
'recur_schedule' => ['required',Rule::in(collect(Invoice::billing_periods)->keys())],
'invoice_next_at' => 'nullable|date',
'price' => 'nullable|numeric',
'product_id'=>'required|exists:products,id',
'price' => 'nullable|numeric|min:0', // Price we charge the client, if we dont charge supplied/price
'cost' => 'nullable|numeric|min:0', // Price we are charged by supplier, if we arent charged supplier/price
'supplierid' => 'nullable|string|min:1', // As used on invoices
$o->product->category => 'array|min:1',
]
)
@ -456,6 +459,9 @@ class ServiceController extends Controller
$o->suspend_billing = ($validated->get('suspend_billing') == 'on');
$o->external_billing = ($validated->get('external_billing') == 'on');
$o->price = $validated->get('price');
$o->cost = $validated->get('cost');
$o->supplierid = $validated->get('supplierid');
$o->product_id = $validated->get('product_id');
// Also update our service start_at date.
// @todo We may want to make start_at/stop_at dynamic values calculated by the type records

View File

@ -30,7 +30,9 @@ class SupplierController extends Controller
$o->save();
} catch (\Exception $e) {
return redirect()->back()->withErrors($e->getMessage())->withInput();
return redirect()
->back()
->withErrors($e->getMessage())->withInput();
}
$o->load(['detail']);

View File

@ -45,6 +45,7 @@ use App\Traits\{ScopeAccountUserAuthorised,ScopeServiceActive,SiteID};
*
* Methods:
* + isChargeOverridden : Has the price been overridden?
* + isCostOverridden : Has the cost been overridden?
* + isPending : Is this a pending active service
*
* @package App\Models
@ -535,6 +536,11 @@ class Service extends Model implements IDs
return $this->account->taxed($this->billing_charge());
}
public function getBillingCostAttribute(): float
{
return $this->account->taxed($this->billing_cost());
}
/**
* Determine a monthly price for a service, even if it is billed at a different frequency
*
@ -543,7 +549,16 @@ class Service extends Model implements IDs
*/
public function getBillingChargeNormalisedAttribute(): float
{
return number_format($this->getBillingChargeAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),2);
return number_format(
$this->getBillingChargeAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),
2);
}
public function getBillingCostNormalisedAttribute(): float
{
return number_format(
$this->getBillingCostAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),
2);
}
/**
@ -567,7 +582,7 @@ class Service extends Model implements IDs
}
/**
* Return the earliest date that the service can be cancelled
* Return the earliest date that the service can be canceled as per contract/billing intervals
*
* @return Carbon
*/
@ -916,17 +931,21 @@ class Service extends Model implements IDs
/**
* Provide billing charge to a future date
*
* If $date is earlier than our contract end date, then our charge is to the contract end date.
* If $date is after our contract end date:
* + and we are still in contract, then our charge is to contract end date plus additional time, else
* + then our charge is the months to the $date
*
* @param Carbon $date
* @return float
* @throws Exception
*/
public function billing_charge_to(Carbon $date): float
{
// if the date is less than the paid to, but less than the cancel date to, return cancel-paid to charge
// If the date is greater than the paid to, and less than the cancel date to, return cancel-paid to charge
if ($this->getPaidToAttribute()->lessThan($this->getCancelDateAttribute())) {
$max = max($date,$this->getPaidToAttribute())->clone();
$d = $max->diffInDays($this->getCancelDateAttribute());
$max = max($date,$this->getCancelDateAttribute())->clone();
if ($this->getPaidToAttribute()->lessThan($max)) {
$d = $this->getPaidToAttribute()->diffInDays($max);
return $this->account->taxed($d/30*$this->getBillingChargeNormalisedAttribute());
}
@ -934,6 +953,43 @@ class Service extends Model implements IDs
return 0;
}
/**
* The amount we are charged for the client to have this service
*
* @return float
*/
public function billing_cost(): float
{
return is_null($this->cost)
? $this->product->getBaseCostAttribute()
: $this->cost*Invoice::billing_change($this->product->type->billing_interval,$this->product->billing_interval);
}
/**
* Calculate our costs to keep this service to a future date
*
* If $date is earlier than the contract end date, then our cost is to the contract end date.
* If $date is after the contract end date:
* + and we are still in contract, then our cost is to contract end date plus additional time, else
* + then our cost is the months to the $date
*
* @param Carbon $date
* @return float
* @throws Exception
*/
public function billing_cost_to(Carbon $date): float
{
$max = max($date,$this->getCancelDateAttribute())->clone();
if ($this->getInvoicedToAttribute()->lessThan($max)) {
$d = $this->getInvoicedToAttribute()->diffInDays($max);
return $this->account->taxed($d/30*$this->getBillingCostNormalisedAttribute());
}
return 0;
}
/**
* Get the stage parameters
*
@ -1065,9 +1121,14 @@ class Service extends Model implements IDs
return ! is_null($this->price);
}
public function isCostOverridden(): bool
{
return ! is_null($this->cost);
}
public function isContract(): bool
{
return $this->getContractEndAttribute()->greaterThan(Carbon::now());
return $this->getContractEndAttribute() && $this->getContractEndAttribute()->greaterThan(Carbon::now());
}
/**

View File

@ -57,16 +57,6 @@ class Broadband extends Type implements ServiceUsage
/* ATTRIBUTES */
/**
* @deprecated use $o->service_name;
* @return mixed|string
*/
public function getNameAttribute()
{
abort(500,'deprecated - use $o->service_name');
return $this->service_number ?: $this->service_address;
}
/**
* The type of technology used to provide this Internet Service
*
@ -104,15 +94,14 @@ class Broadband extends Type implements ServiceUsage
/* METHODS */
/**
* Return the suppliers offering that this service is providing
* Return the supplier's offering that this service is providing
*
* @return SupplierType
* @todo This column provided_adsl_plan_id should either be deprecated or renamed.
*/
public function supplied(): SupplierType
{
return $this->provided_adsl_plan_id
? SupplierBroadband::findOrFail($this->provided_adsl_plan_id)
return $this->provided_supplier_broadband_id
? SupplierBroadband::findOrFail($this->provided_supplier_broadband_id)
: $this->service->offering->supplied;
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('services', function (Blueprint $table) {
$table->float('cost')->nullable();
$table->string('supplierid')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('service_broadband', function (Blueprint $table) {
$table->dropColumn('cost');
$table->dropColumn('supplierid');
});
}
};

View File

@ -19,7 +19,7 @@
$ao->load(['services:id,active,account_id']);
@endphp
<tr>
<td><a href="{{ url('r/switch/start',$ao->user_id) }}"><i class="fas fa-external-link-alt"></i></a></td>
<td><a href="{{ url('switch/start',$ao->user_id) }}"><i class="fas fa-external-link-alt"></i></a></td>
<td><a href="{{ url('u/home',$ao->user_id) }}">{{ $ao->name }}</a></td>
<td class="text-right">{{ $ao->services->where('active',TRUE)->count() }} <small>/{{ $ao->services->count() }}</small></td>
</tr>

View File

@ -1,4 +1,5 @@
<!-- $co=Checkout::class -->
@extends('adminlte::layouts.app')
@section('htmlheader_title')

View File

@ -1,4 +1,5 @@
<!-- $o=Invoice::class -->
@use(App\Models\Checkout)
@use(App\Models\Service)

View File

@ -1,4 +1,5 @@
<!-- po=Payment::class -->
<!-- $po=Payment::class -->
@use(Carbon\Carbon)
@use(App\Models\Account)
@use(App\Models\Checkout)

View File

@ -1,3 +1,5 @@
<!-- $o=Service::class,$np=Product::class -->
@use(App\Models\Product)
@extends('adminlte::layouts.app')
@ -16,7 +18,6 @@
{{ $o->sid }}
@endsection
<!-- $o=Service::class, $np=Product::class -->
@section('main-content')
<div class="row">
<div class="col-12 col-lg-4">

View File

@ -25,6 +25,7 @@
<th>ID</th>
<th>Service</th>
<th>Product</th>
<th>Supplier ID</th>
<th class="text-right">Monthly</th>
<th class="text-right">Cost</th>
<th class="text-right">Usage</th>
@ -38,13 +39,26 @@
<td><a href="{{ url('u/service',[$o->id]) }}">{{ $o->id }}</a></td>
<td>{{ $o->name }}</td>
<td>{{ $o->product->name }}</td>
<td>{{ $o->supplierid }}</td>
<td class="text-right">{{ number_format($o->billing_charge_normalised,2) }}</td>
<td class="text-right">{{ number_format($o->product->cost_normalized(),2) }}</td>
<td class="text-right">{{ number_format($o->billing_cost_normalised,2) }}</td>
<td class="text-right">{{ $o->product->hasUsage() ? number_format($o->type->usage_summary(0)->sum()/1000,1) : '-' }}</td>
<td>{{ $o->product->supplier->name }}</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td></td>
<th>TOTAL:</th>
<td></td>
<td class="text-right"></td>
<td class="text-right"></td>
<td></td>
<td></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
@ -77,6 +91,41 @@
],
rowGroup: {
dataSrc: [2],
endRender: function (rows, group) {
// Remove the formatting to get integer data for summation
let intVal = function (i) {
return typeof i === 'string'
? i.replace(/[\$,]/g, '') * 1
: typeof i === 'number'
? i
: 0;
};
// Total over all pages
let month = rows
.data()
.pluck(4)
.reduce((a, b) => intVal(a) + intVal(b), 0);
let cost = rows
.data()
.pluck(5)
.reduce((a, b) => intVal(a) + intVal(b), 0);
let usage = rows
.data()
.pluck(6)
.reduce((a, b) => intVal(a) + intVal(b), 0);
return $('<tr/>')
.append('<td/>')
.append('<td><strong>SUB-TOTAL:</strong></td>')
.append('<td/>')
.append('<td class="text-right"><strong>'+month.toFixed(2)+'</strong></td>')
.append('<td class="text-right"><strong>'+cost.toFixed(2)+'</strong></td>')
.append('<td class="text-right"><strong>'+usage.toFixed(2)+'</strong></td>')
.append('<td/>');
}
},
columnDefs: [
{
@ -84,7 +133,7 @@
visible: false,
},
{
targets: [0,1,3,4,5],
targets: [0,1,4,5,6],
searchPanes: {
show: false,
}
@ -104,6 +153,46 @@
controls: false,
},
dom: '<"dtsp-verticalContainer"<"dtsp-verticalPanes"P><"dtsp-dataTable"Bfrtip>>',
footerCallback: function (row, data, start, end, display) {
let api = this.api();
// Remove the formatting to get integer data for summation
let intVal = function (i) {
return typeof i === 'string'
? i.replace(/[\$,]/g, '') * 1
: typeof i === 'number'
? i
: 0;
};
// Total over all pages
month = api
.column(4,{ search: 'applied' })
.data()
.reduce((a, b) => intVal(a) + intVal(b), 0);
// Total over this page
monthTotal = api
.column(4, { page: 'current' })
.data()
.reduce((a, b) => intVal(a) + intVal(b), 0);
// Total over all pages
cost = api
.column(5,{ search: 'applied' })
.data()
.reduce((a, b) => intVal(a) + intVal(b), 0);
// Total over this page
costTotal = api
.column(5, { page: 'current' })
.data()
.reduce((a, b) => intVal(a) + intVal(b), 0);
// Update footer
api.column(4).footer().innerHTML = monthTotal.toFixed(2)+'<br><small>('+month.toFixed(2)+')</small>';
api.column(5).footer().innerHTML = costTotal.toFixed(2)+'<br><small>('+cost.toFixed(2)+')</small>';
}
});
});
</script>

View File

@ -1,4 +1,5 @@
<!-- $o = App\Models\Service\Broadband::class -->
<!-- $o=Service\Broadband::class -->
<div class="card">
@if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg">

View File

@ -10,7 +10,7 @@
</div>
<div class="row">
<div class="col">
<div class="col-12">
Connection Type
<x-leenooks::form.toggle id="pppoe" name="broadband[pppoe]" label="PPPoE" old="broadband.pppoe" :value="$o->pppoe"/>
</div>

View File

@ -1,4 +1,5 @@
<!-- $o = App\Models\Service\Domain::class -->
<!-- $o=Service\Domain::class -->
<div class="card">
@if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg">

View File

@ -1,4 +1,5 @@
<!-- $o = App\Models\Service\Email::class -->
<!-- $o=Service\Email::class -->
<div class="card">
@if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg">

View File

@ -1,4 +1,5 @@
<!-- $o = App\Models\Service\Host::class -->
<!-- $o=Service\Host::class -->
<div class="card">
@if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg">

View File

@ -1,7 +1,10 @@
<!-- $o=Service::class,$p=Product::class -->
@use(Carbon\CarbonInterface)
@use(App\Models\Invoice)
@php($c=$o->product)
<!-- $o=Service::class, $p=Product::class -->
<table class="table table-sm">
<thead>
<tr>
@ -29,7 +32,8 @@
<tbody>
<tr>
<th>Product</th>
<td class="text-center" colspan="2"><a href="{{ url('a/product/details',$c->id) }}">#{{ $c->supplied->id }}: {{ $c->supplied->name_long }}</a></td>
<td><a href="{{ url('a/product/details',$o->product_id) }}">#{{ $o->product_id }}: {{ $o->product->name }}</a></td>
<td><a href="{{ url('a/product/details',$c->id) }}">#{{ $c->supplied->id }}: {{ $c->supplied->name_long }}</a></td>
@if($p->exists)
<th>&nbsp;</th>
<td class="text-center" colspan="2">#{{ $p->supplied->id }}: {{ $p->supplied->name_long }}</td>
@ -64,7 +68,7 @@
<tr>
<th>Billing Price</th>
<td @if($o->isChargeOverridden())class="text-danger"@endif>${{ number_format($b=$o->billing_charge,2) }}</td>
<td>${{ number_format($a=$o->account->taxed($c->base_cost),2) }}</td>
<td @if($o->isCostOverridden())class="text-danger"@endif>${{ number_format($a=$o->billing_cost,2) }}</td>
<td>{!! markup($a,$b) !!}</td>
@if($p->exists)
<td @if($o->isChargeOverridden())class="text-danger"@endif>${{ number_format($b=$o->account->taxed($p->base_charge),2) }}</td>
@ -79,10 +83,16 @@
@if($x)
<abbr title="${{ number_format($b=$o->account->taxed($c->base_charge)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}">${{ number_format($b=$o->billing_charge_normalised,2) }}
@else
{{ number_format($b=$o->billing_charge_normalised,2) }}
${{ number_format($b=$o->billing_charge_normalised,2) }}
@endif
</td>
<td @if($x=$o->isCostOverridden()) class="text-danger" @endif>
@if($x)
<abbr title="${{ number_format($a=$o->account->taxed($c->base_cost)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}">${{ number_format($b=$o->billing_cost_normalised,2) }}
@else
${{ number_format($a=$o->billing_cost_normalised,2) }}
@endif
</td>
<td>${{ number_format($a=$o->account->taxed($c->base_cost)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}</td>
<td>{!! markup($a,$b) !!}</td>
@if($p->exists)
<td @if($x=$o->isChargeOverridden()) class="text-danger" @endif>${{ number_format($b=$o->account->taxed($p->base_charge)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}</td>
@ -109,11 +119,25 @@
<td>${{ number_format($b=$o->account->taxed($o->product->min_charge),2) }}</td>
<td>${{ number_format($a=$o->account->taxed($c->supplied->min_cost),2) }}</td>
<td>{!! markup($a,$b) !!}</td>
@if($p->exists)
<td>${{ number_format($a=$o->account->taxed($p->min_charge),2) }}</td>
<td>${{ number_format($a=$o->account->taxed($p->supplied->min_cost ?? 0),2) }}</td>
<td>{!! markup($a,$b) !!}</td>
@endif
</tr>
<tr>
<th>Contract Left</th>
@if($o->isContract())
<td>${{ number_format($o->billing_charge_to($o->contract_end),2) }} (<small>{{ $o->paid_to->format('Y-m-d') }}</small>)</td>
<td>${{ number_format($o->billing_cost_to($o->contract_end),2) }} (<small>{{ $o->invoiced_to->format('Y-m-d') }}</small>)</td>
<td>{{ $o->contract_end->format('Y-m-d') }}<br><small>({{ $o->contract_end->diffForHumans(now(),CarbonInterface::DIFF_RELATIVE_TO_OTHER,FALSE,2) }} today)</small></td>
@else
<td colspan="2" class="text-center">Not on contract</td>
<td>&nbsp;</td>
@endif
</tr>
</tbody>
</table>

View File

@ -1,4 +1,5 @@
<!-- $o = App\Models\Service\Phone::class -->
<!-- $o=Service\Phone::class -->
<div class="card">
@if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg">

View File

@ -1,9 +1,10 @@
<!-- $o=Service::class -->
@use(App\Models\Invoice)
@use(App\Models\Product)
<div class="row">
<div class="col-12">
<h4>Update Service details <x-leenooks::button.success class="float-right"/></h4>
<h4>Update Service Details <x-leenooks::button.success class="float-right"/></h4>
<hr>
<form method="POST" action="{{ url('a/service/update',[$o->id]) }}">
@ -29,13 +30,36 @@
</div>
<div class="row">
<div class="col-3"></div>
<div class="col-12 col-sm-3">
<x-leenooks::form.text name="supplierid" icon="fa-hashtag" label="Supplier ID" :value="$o->supplierid"/>
</div>
<div class="col-12 col-sm-9 col-md-6 col-xl-5">
<x-leenooks::form.select id="recur_schedule" name="recur_schedule" icon="fa-redo" label="Renew Term" :value="$o->recur_schedule" :options="collect(Invoice::billing_periods)->map(fn($item,$key)=>['id'=>$key,'value'=>$item['name']])"/>
</div>
<!-- Cost -->
<div class="col-12 col-sm-4 col-xl-3">
<x-leenooks::form.text name="cost" icon="fa-dollar-sign" label="Cost (Monthly)" :value="$o->cost"/>
</div>
</div>
<div class="row">
<div class="col-12">
<!-- PRODUCT -->
<x-leenooks::form.select name="product_id" icon="fa-list" label="Product" :helper="$o->product->category_name"
:value="$o->product_id"
:options="Product::get()
->filter(fn($item)=>$item->active && ($item->category === $o->product->category))
->sortBy('name')
->map(fn($item)=>[
'id'=>$item->id,
'value'=>sprintf('%s (%3.2f/%3.2f)',
$item->name,
$o->account->taxed($item->base_charge)*Invoice::billing_change($item->billing_interval,Invoice::BILL_MONTHLY),
$o->account->taxed($item->base_cost)*Invoice::billing_change($item->billing_interval,Invoice::BILL_MONTHLY))])" :required="true"/>
</div>
</div>
<hr>
@includeIf('theme.backend.adminlte.service.widget.'.$o->product->category.'.update',['o'=>$o->type])

View File

@ -1,6 +1,7 @@
<!-- $spo=Supplier::class -->
@use(Carbon\Carbon)
<!-- $spo=Supplier::class -->
@extends('adminlte::layouts.app')
@section('htmlheader_title')

View File

@ -1,6 +1,8 @@
<!-- $spo=Supplier::class -->
<!-- $oo=Supplier/{type}::class -->
@use(App\Models\Supplier)
@php
if(isset($spo)) {
$oo = $spo->detail?->find(request()->route()->parameter('type'),request()->route()->parameter('id'));

View File

@ -1,4 +1,5 @@
<!-- $o = Supplier{type}::class -->
<!-- $o=Supplier/Broadband::class -->
<div class="row">
<!-- Supplier Name -->
<div class="col-4">

View File

@ -1,4 +1,5 @@
<!-- ($o??$user)=User::class -->
@use(App\Models\Country)
@extends('adminlte::layouts.app')

View File

@ -46,7 +46,7 @@
<div class="dropdown-divider"></div>
@if($user->switched)
<a href="{{ url('/admin/switch/stop') }}" class="dropdown-item">
<a href="{{ url('switch/stop') }}" class="dropdown-item">
<i class="fas fa-sign-out-alt mr-2"></i> {{ trans('adminlte_lang::message.switchoff') }}
</a>
@else

View File

@ -18,8 +18,7 @@ use App\Http\Controllers\{AdminController,
SearchController,
ServiceController,
SupplierController,
UserController,
Wholesale\ReportController};
UserController};
use App\Models\Supplier;
/*
@ -42,8 +41,8 @@ Auth::routes([
'confirm' => false, // for additional password confirmations
'verify' => false, // for email verification
]);
Route::get('logout',[LoginController::class,'logout'])
->name('logout-get');
// Auth::routes doesnt provide a get /logout path, so we'll specify it here
Route::get('logout',[LoginController::class,'logout']);
// Frontend Routes (Non-Authed Users)
Route::view('/','theme.frontend.metronic.welcome.home');
@ -54,21 +53,35 @@ Route::redirect('passkey/loggedin','/u/home');
Route::get('search',[SearchController::class,'search']);
Route::get('pay/paypal/authorise',[PaypalController::class,'authorise']);
Route::get('pay/paypal/cancel',[PaypalController::class,'cancel']);
Route::get('pay/paypal/capture',[PaypalController::class,'capture']);
// Paypal paths
Route::controller(PaypalController::class)
->prefix('pay/paypal')
->group(function() {
Route::get('authorise','authorise');
Route::get('cancel','cancel');
Route::get('capture','capture');
});
// Account linking to OPENID host
Route::get('auth/{socialProvider}',[SocialLoginController::class,'redirectToProvider']);
Route::get('auth/{socialProvider}/callback',[SocialLoginController::class,'handleProviderCallback']);
Route::get('auth/{socialProvider}/token',[SocialLoginController::class,'handleBearerTokenCallback']);
Route::get('auth/{socialProvider}/link',[SocialLoginController::class,'link']);
Route::post('auth/{socialProvider}/linkcomplete',[SocialLoginController::class,'linkcomplete']);
Route::controller(PaypalController::class)
->prefix('auth')
->group(function() {
Route::get('{socialProvider}','redirectToProvider');
Route::get('{socialProvider}/callback','handleProviderCallback');
Route::get('{socialProvider}/token','handleBearerTokenCallback');
Route::get('{socialProvider}/link','link');
Route::post('{socialProvider}/linkcomplete','linkcomplete');
});
// Return from user switch
Route::get('admin/switch/stop',[SwitchUserController::class,'switch_stop'])
// User Switch
Route::controller(SwitchUserController::class)
->prefix('switch')
->middleware('auth')
->name('switch.stop');
->group(function() {
Route::get('stop','switch_stop');
Route::get('start/{user}','switch_start')
->middleware(['role:reseller','can:assume,user']);
});
// Our Admin Routes - for wholesalers
Route::group(['middleware'=>['auth','role:wholesaler'],'prefix'=>'a'],function() {
@ -137,11 +150,6 @@ Route::group(['middleware'=>['auth','role:wholesaler'],'prefix'=>'a'],function()
// Our Reseller Routes
Route::group(['middleware'=>['auth','role:reseller'],'prefix'=>'r'],function() {
// Enable user switch
Route::get('switch/start/{user}',[SwitchUserController::class,'switch_start'])
->middleware('can:assume,user')
->name('switch.start');
// Reseller Reports
Route::group(['prefix'=>'report'],function() {
Route::view('charge/pending','theme.backend.adminlte.charge.pending');