diff --git a/.gitignore b/.gitignore index f808462..ad6c226 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /storage/*.key /vendor /.idea +.editorconfig /.vscode /.vagrant Homestead.json diff --git a/app/Http/Controllers/AdminHomeController.php b/app/Http/Controllers/AdminHomeController.php index 59344f5..f5ff194 100644 --- a/app/Http/Controllers/AdminHomeController.php +++ b/app/Http/Controllers/AdminHomeController.php @@ -17,55 +17,6 @@ class AdminHomeController extends Controller return View('a.service',['o'=>$o]); } - public function service_update(Request $request, Service $o) - { - if (! $o->validStatus(strtolower($request->input('action')))) - return $this->service($o); - - $action = strtolower($request->input('action')); - - switch ($action) - { - case 'approve': - // Send an email to the supplier. - // @todo Change to address to suppliers email address. - Mail::to('help@graytech.net.au') - ->queue((new OrderRequestApprove($o,$request->input('order_notes') ?: 'NONE'))->onQueue('high')); - - // Send an email to the client. - // @todo Your order has been submitted to supplier. - - // Update the service to "ORDER-SENT" - $o->nextStatus($action); - - break; - - case 'reject': - $o->order_info = array_merge($o->order_info ? $o->order_info : [],['reason'=>$request->input('notes')]); - - // Send mail to user - Mail::to($o->orderby->email)->queue((new OrderRequestReject($o,$request->input('notes')))->onQueue('email')); - - $o->nextStatus($action); - break; - - case 'hold': - case 'release': - $o->nextStatus($action); - break; - - case 'update_reference': - $o->order_info = array_merge($o->order_info ? $o->order_info : [],['order_reference'=>$request->input('notes')]); - $o->save(); - - // No action specified. - default: - return $this->service($o); - } - - return redirect(url('/a/service',[$o->id])); - } - public function setup() { return view('a.setup'); diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php new file mode 100644 index 0000000..4a6a87f --- /dev/null +++ b/app/Http/Controllers/ServiceController.php @@ -0,0 +1,50 @@ +order_status) { + case 'ORDER-SENT': + if ($request->post()) { + foreach (['reference','notes'] as $key) { + $o->setOrderInfo($key,$request->post($key)); + } + + $o->save(); + + foreach ($request->post($o->stype) as $k=>$v) { + $o->type->{$k} = $v; + } + + $o->type->save(); + + return redirect()->to(url('u/service',$o->id))->with('updated','Order sent notes updated.'); + } + + return $this->update_order_status($o); + + default: + abort(499,'Not yet implemented'); + } + } + + private function update_order_status(Service $o) + { + return View('r.service.order.sent',['o'=>$o]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/UserHomeController.php b/app/Http/Controllers/UserHomeController.php index 88d9604..2e7278b 100644 --- a/app/Http/Controllers/UserHomeController.php +++ b/app/Http/Controllers/UserHomeController.php @@ -87,6 +87,19 @@ class UserHomeController extends Controller */ public function service(Service $o): View { - return View('u.service',['o'=>$o]); + return View('u.service.home',['o'=>$o]); + } + + /** + * Progress the order to the next stage + * + * @note: Route Middleware protects this path + * @param Service $o + * @param string $status + * @return \Illuminate\Http\RedirectResponse + */ + public function service_progress(Service $o,string $status) + { + return redirect()->to($o->action($status) ?: url('u/service',$o->id)); } } \ No newline at end of file diff --git a/app/Models/Policies/ServicePolicy.php b/app/Models/Policies/ServicePolicy.php index 5ea586a..64c0596 100644 --- a/app/Models/Policies/ServicePolicy.php +++ b/app/Models/Policies/ServicePolicy.php @@ -23,10 +23,10 @@ class ServicePolicy // If this is a service for an account managed by a user. return ($user->services->pluck('id')->search($o->id)) - // The user is the wholesaler + // The user is the wholesaler OR $user->isWholesaler() - // The user is the reseller + // The user is the reseller OR $user->all_accounts()->pluck('id')->search($o->account_id); } @@ -41,6 +41,18 @@ class ServicePolicy return TRUE; } + /** + * Can the user progress an order status + * + * @param User $user + * @param Service $o + * @return bool + */ + public function progress(User $user, Service $o,string $next) + { + return $o->actions()->has($next); + } + /** * Determine whether the user can update the service. * diff --git a/app/Models/Service.php b/app/Models/Service.php index 09bc25d..14cfacc 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -11,9 +11,12 @@ use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; +use Symfony\Component\HttpKernel\Exception\HttpException; -use App\Traits\NextKey; use Leenooks\Carbon; +use App\User; +use App\Traits\NextKey; class Service extends Model { @@ -45,7 +48,7 @@ class Service extends Model ]; protected $casts = [ - 'order_info'=>'array', + 'order_info'=>'collection', ]; public $dateFormat = 'U'; @@ -92,9 +95,58 @@ class Service extends Model * * @var array */ - private $valid_status = [ + public static $action_progress = [ // Order Submitted - 'ORDER-SUBMIT' => ['approve'=>'ORDER-SENT','hold'=>'ORDER-HOLD','reject'=>'ORDER-REJECTED','cancel'=>'ORDER-CANCELLED'], + 'ORDER-SUBMIT' => [ + 'fail'=>FALSE, + // Progress to next stages by who + 'next'=>[ + 'ORDER-ACCEPT'=>['customer'], + 'SETUP-PAYMENT-WAIT'=>['reseller','wholesaler'] + ], + // Manual or System moves to the next stage + 'system'=>TRUE, + 'method'=>'action_order_submit', + 'title'=>'Order Submit', + ], + // Client accepts order, if performed by RW + 'ORDER-ACCEPT' => [ + 'fail'=>FALSE, + 'next'=>[ + 'SETUP-PAYMENT-WAIT'=>['customer'], + ], + 'system'=>FALSE, + 'method'=>'action_order_accept', + 'title'=>'Client Accept Order', + ], + // If the product has a setup, collect payment information + 'SETUP-PAYMENT-WAIT' => [ + 'fail'=>FALSE, + 'next'=>[ + 'PAYMENT-WAIT'=>['customer'], + ], + 'system'=>FALSE, + 'method'=>'action_setup_payment_wait', + 'title'=>'Setup Payment', + ], + 'PAYMENT-WAIT' => [ + 'fail'=>FALSE, + 'next'=>[ + 'PAYMENT-CHECK'=>['reseller','wholesaler'], + ], + 'system'=>FALSE, + 'method'=>'action_payment_wait', + 'title'=>'Service Payment', + ], + 'PAYMENT-CHECK' => [ + 'fail'=>'ORDER-HOLD', + 'next'=>[ + 'ORDER-SENT'=>[], + ], + 'system'=>TRUE, + 'method'=>'action_payment_check', + 'title'=>'Validate Payment Method', + ], // Order On Hold (Reason) 'ORDER-HOLD' => ['release'=>'ORDER-SUBMIT','update_reference'=>'ORDER-SENT'], // Order Rejected (Reason) @@ -102,7 +154,15 @@ class Service extends Model // Order Cancelled 'ORDER-CANCELLED' => [], // Order Sent to Supplier - 'ORDER-SENT' => ['update_reference'=>'ORDER-SENT','confirm'=>'ORDERED'], + 'ORDER-SENT' => [ + 'fail'=>'ORDER-HOLD', + 'next'=>[ + 'ORDERED'=>['wholesaler'], + ], + 'system'=>FALSE, + 'method'=>'action_order_sent', + 'title'=>'Send Order', + ], // Order Confirmed by Supplier 'ORDERED' => ['update_reference'=>'ORDER-SENT'], ]; @@ -493,7 +553,7 @@ class Service extends Model */ public function getNameShortAttribute() { - return $this->type ? $this->type->service_name : $this->id; + return $this->type ? $this->type->name : $this->id; } /** @@ -504,25 +564,14 @@ class Service extends Model return $this->getInvoiceNextAttribute(); } - /** - * This function will present the Order Info Details - */ - public function getOrderInfoDetailsAttribute(): string + public function getOrderInfoNotesAttribute(): ?string { - if (! $this->order_info) - return ''; + return $this->getOrderInfoValue('notes'); + } - $result = ''; - - foreach ($this->order_info as $k=>$v) - { - if (in_array($k,['order_reference'])) - continue; - - $result .= sprintf('%s: %s
',ucfirst($k),$v); - } - - return $result; + public function getOrderInfoReferenceAttribute(): ?string + { + return $this->getOrderInfoValue('reference'); } /** @@ -738,6 +787,218 @@ class Service extends Model /** FUNCTIONS **/ + // The action methods will return: NULL for no progress|FALSE for a failed status|next stage name. + + /** + * Process for an order when status ORDER-ACCEPT stage. + * This method should have the client confirm/accept the order, if it was placed by a reseller/wholesaler. + * + * @return bool + */ + private function action_order_accept(): ?bool + { + // @todo TO IMPLEMENT + return TRUE; + } + + /** + * Action method when status ORDER_SENT + * This method redirects to a form, where updating the form will progress to the next stage. + */ + private function action_order_sent() + { + throw new HttpException(301,url('r/service/update',$this->id)); + } + + /** + * Action method when status ORDER_SUBMIT + * + * @return bool + */ + private function action_order_submit(): ?bool + { + // @todo TO IMPLEMENT + return TRUE; + } + + /** + * Process for an order when status SETUP-PAYMENT-WAIT stage. + * This method should collect any setup fees payment. + * + * @return bool + */ + private function action_setup_payment_wait(): ?bool + { + // @todo TO IMPLEMENT + return TRUE; + } + + /** + * Process for an order when status PAYMENT-CHECK stage. + * This method should validate any payment details. + * + * @return bool + */ + private function action_payment_check(): ?bool + { + // @todo TO IMPLEMENT + return TRUE; + } + + /** + * Process for an order when status PAYMENT-WAIT stage. + * This method should collect any service payment details. + * + * @return bool + */ + private function action_payment_wait(): ?bool + { + // @todo TO IMPLEMENT + return TRUE; + } + + private function getOrderInfoValue(string $key): ?string + { + return $this->order_info ? $this->order_info->get($key) : NULL; + } + + /** + * Get the current stage parameters + * + * @param string $stage + * @return array + */ + private function getStageParameters(string $stage): Collection + { + $result = collect(Arr::get(self::$action_progress,$stage)); + $myrole = array_search(Auth::user()->role(),User::$role_order); + + // If we have no valid next stage, return an empty collection. + if (! $result->count() OR $myrole===FALSE) + return $result; + + // Filter the result based on who we are + $next = collect(); + + foreach ($result->get('next') as $action=>$roles) { + + // Can the current user do this role? + $cando = FALSE; + foreach ($roles as $role) { + if ($myrole < array_search($role,User::$role_order)) { + $cando = TRUE; + break; + } + } + + //dd($action,$roles,$result); + if ($cando OR $result->get('system')) { + $next->put($action,$roles); + } + } + + $result->put('next',$next); + + return $result; + } + + /** + * @notes + * + When progressing stages, we know who the user is that initiated the stage, + * + If no user, then we perform stages SYSTEM=TRUE + * + We need to validate that the current stage is complete, before progressing to the next stage + * + The current stage may require input from a user, or automation process to progress + * + Before leaving this method, we update the service with the stage that it is currently on. + * + * @param string $stage + * @return bool|int|string|null + */ + public function action(string $stage) + { + // While stage has a string value, that indicates the next stage we want to go to + // If stage is NULL, the current stage hasnt been completed + // If stage is FALSE, then the current stage failed, and may optionally be directed to another stage. + + while ($stage) { + // Check that stage is a valid next action for the user currently performing it + $current = $this->getStageParameters($this->order_status); + $next = $this->getStageParameters($stage); + + // If valid, call the method to confirm that the current stage is complete + if (method_exists($this,$current['method'])) { + try { + $result = $this->{$current['method']}(); + + // If we have a form to complete, we need to return with a URL, so we can catch that with an Exception + } catch (HttpException $e) { + if ($e->getStatusCode() == 301) + return ($e->getMessage()); + } + + // @todo Implement a status message + if (is_null($result)) { + $stage = NULL; + abort(500,'Current Method Cannot Proceed: '.$result); + + // @todo Implement a status message + } elseif (! $result) { + $stage = NULL; + abort(500,'Current Method FAILED: '.$result); + + } else { + $this->order_status = $stage; + $this->save(); + + // If we have more than 1 next step for the next stage, we'll have to end. + if ($this->actions()->count() > 1) { + $stage = NULL; + + } else { + $stage = $this->actions()->keys()->first(); + } + } + + // @todo Implement a status message + } else { + // Cant do anything, dont have a method to check if we can leave + $stage = NULL; + abort(500,'NO Method Cannot Proceed to leave this stage: '.$next['method']); + } + + // If valid, call the method to start the next stage + } + } + + /** + * Work out the next applicable actions for this service status + * + * @notes + * + Clients can only progress 1 step, if they are in the next step. + * + Resellers/Wholesales can progress to the next Reseller/Wholesaler and any steps in between. + * + * @param bool $next Only show next actions + * @return Collection + */ + public function actions(): Collection + { + $result = collect(); + $action = $this->getStageParameters($this->order_status); + + if (! $action->count()) + return $result; + + // Next Action + foreach ($this->getStageParameters($this->order_status)->get('next') as $k=>$v) { + $result->put($k,Arr::get(self::$action_progress,$k.'.title')); + } + + // No next actions, that will mean the current action hasnt completed. + if (! $result->count()) + $result->put($this->order_status,Arr::get($action,'title')); + + return $result; + } + /** * Add applicable tax to the cost * @@ -802,7 +1063,7 @@ class Service extends Model */ public function next_invoice_items(bool $future): Collection { - if ($this->wasCancelled() OR $this->suspend_billing OR ! $this->active) + if ($this->wasCancelled() OR $this->suspend_billing OR $this->external_billing OR (! $future AND ! $this->active)) return collect(); // If pending, add any connection charges @@ -879,23 +1140,6 @@ class Service extends Model return $this->invoice_items->filter(function($item) { return ! $item->exists; }); } - /** - * @todo - * @param string $status - * @return $this - */ - public function nextStatus(string $status) { - if ($x=$this->validStatus($status)) - { - $this->order_status = $x; - $this->save(); - - return $this; - } - - abort(500,'Next Status not set up for:'.$this->order_status); - } - /** * This function will return the associated service model for the product type * @deprecated use $this->type @@ -919,22 +1163,17 @@ class Service extends Model } /** - * Return if the proposed status is valid. + * Store order info details * - * @param string $status - * @return string | NULL + * @param string $key + * @param string $value */ - private function testNextStatusValid(string $status) + public function setOrderInfo(string $key,string $value): void { - return Arr::get(Arr::get($this->valid_status,$this->order_status,[]),$status,NULL); - } + $x = is_null($this->order_info) ? collect() : $this->order_info; + $x->put($key,$value); - /** - * @deprecated use testNextStatusValid() - */ - public function validStatus(string $status) - { - return $this->testNextStatusValid($status); + $this->order_info = $x; } /** diff --git a/app/User.php b/app/User.php index 06ee618..4d3fc55 100644 --- a/app/User.php +++ b/app/User.php @@ -64,6 +64,16 @@ class User extends Authenticatable protected $with = ['accounts']; + /** + * Role hierarchy order + * @var array + */ + public static $role_order = [ + 'wholesaler', + 'reseller', + 'customer', + ]; + /** * The accounts that this user manages * @@ -452,7 +462,7 @@ class User extends Authenticatable public function client_service_movements(): DatabaseCollection { return Service::active() - ->select(['id','account_id','product_id','order_status']) + ->select(['id','account_id','product_id','order_status','model']) ->where('order_status','!=','ACTIVE') ->whereIN('account_id',$this->all_accounts()->pluck('id')->unique()->toArray()) ->with(['account','product']) diff --git a/config/snappy.php b/config/snappy.php index a20201d..61cf8a0 100644 --- a/config/snappy.php +++ b/config/snappy.php @@ -3,7 +3,7 @@ return [ 'pdf' => array( 'enabled' => true, - 'binary' => '/usr/bin/wkhtmltopdf', + 'binary' => '/usr/local/bin/wkhtmltopdf', 'timeout' => false, 'options' => array('print-media-type' => true), 'env' => array(), @@ -11,7 +11,7 @@ return [ 'image' => array( 'enabled' => false, - 'binary' => '/var/www/html/vendor/bin/wkhtmltoimage', + 'binary' => '/usr/local/bin/wkhtmltoimage', 'timeout' => false, 'options' => array(), 'env' => array(), diff --git a/resources/views/theme/backend/adminlte/a/widgets/service/order/sent.blade.php b/resources/views/theme/backend/adminlte/a/widgets/service/order/sent.blade.php deleted file mode 100644 index 74b4b81..0000000 --- a/resources/views/theme/backend/adminlte/a/widgets/service/order/sent.blade.php +++ /dev/null @@ -1,52 +0,0 @@ -
-
-

New Order Sent to Supplier

-
- - -
-
- -
- - - - - - - - @if($o->date_last_invoice) - - - - - - - - - - @endif - - - - @if ($o->date_last) - - - - @endif - - - - - - -
Account{{ $o->account->company }}
Product{{ $o->product->name }}: {{ $o->name }}
Last Invoice{{ $o->date_last_invoice }}
Paid Until{{ 'TBA' }}
Next Invoice{{ $o->date_next_invoice }}
Ordered{{ $o->date_orig->format('Y-m-d') }}
Update{{ $o->date_last->format('Y-m-d') }}
Order Details{!! $o->order_info_details !!}
Reference:
-
- - {{-- - - --}} -
\ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/a/widgets/service/order/submit.blade.php b/resources/views/theme/backend/adminlte/a/widgets/service/order/submit.blade.php deleted file mode 100644 index 865d2ee..0000000 --- a/resources/views/theme/backend/adminlte/a/widgets/service/order/submit.blade.php +++ /dev/null @@ -1,52 +0,0 @@ -
-
-

New Order Submitted

-
- - -
-
- -
- - - - - - - - @if($o->date_last_invoice) - - - - - - - - - - @endif - - - - @if ($o->date_last) - - - - @endif - - - - - - -
Account{{ $o->account->company }}
Product{{ $o->product->name }}: {{ $o->name }}
Last Invoice{{ $o->date_last_invoice }}
Paid Until{{ 'TBA' }}
Next Invoice{{ $o->date_next_invoice }}
Ordered{{ $o->date_orig->format('Y-m-d') }}
Update{{ $o->date_last->format('Y-m-d') }}
Order Details{!! $o->order_info_details !!}
Save/Reject Note:
-
- - {{-- - - --}} -
\ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/r/service/order/sent.blade.php b/resources/views/theme/backend/adminlte/r/service/order/sent.blade.php new file mode 100644 index 0000000..3eea5aa --- /dev/null +++ b/resources/views/theme/backend/adminlte/r/service/order/sent.blade.php @@ -0,0 +1,81 @@ +@extends('adminlte::layouts.app') + +@section('htmlheader_title') + {{ $o->sid }} +@endsection +@section('page_title') + {{ $o->sid }} +@endsection + +@section('contentheader_title') + Service: {{ $o->sid }} {{ $o->product->name }} +@endsection +@section('contentheader_description') + {{ $o->sname }}: {{ $o->sdesc }} +@endsection + +@section('main-content') +
+
+
+
+
Update Order Status: {{ $o->order_status }}
+
+ +
+ {{ csrf_field() }} + +
+
+ +
+
+
+ +
+ +
+
+
+ + @includeIf('u.service.widgets.'.$o->stype.'.order',['o'=>$o->type]) + +
+ +
+ +
+
+
+ + + +
+
+
+
+@endsection + +@section('page-scripts') + @css('//cdnjs.cloudflare.com/ajax/libs/summernote/0.8.12/summernote-bs4.css','summernote-css') + @js('//cdnjs.cloudflare.com/ajax/libs/summernote/0.8.12/summernote-bs4.js','summernote-js') + + +@append \ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/r/service/widget/movement.blade.php b/resources/views/theme/backend/adminlte/r/service/widget/movement.blade.php index 75dff1b..80f3575 100644 --- a/resources/views/theme/backend/adminlte/r/service/widget/movement.blade.php +++ b/resources/views/theme/backend/adminlte/r/service/widget/movement.blade.php @@ -26,7 +26,7 @@ {{ $o->id }} {{ $o->account->name }} - {{ $o->name }} + {{ $o->name_short }} {{ $o->status }} {{ $o->product->name }} diff --git a/resources/views/theme/backend/adminlte/u/service.blade.php b/resources/views/theme/backend/adminlte/u/service/home.blade.php similarity index 80% rename from resources/views/theme/backend/adminlte/u/service.blade.php rename to resources/views/theme/backend/adminlte/u/service/home.blade.php index 52367ca..c20bb60 100644 --- a/resources/views/theme/backend/adminlte/u/service.blade.php +++ b/resources/views/theme/backend/adminlte/u/service/home.blade.php @@ -31,7 +31,9 @@ --}} - + @if (! $o->suspend_billing AND ! $o->external_billing) + + @endif {{-- @@ -48,11 +50,10 @@ ACTION @@ -67,9 +68,11 @@
Product.
-
- @include('common.service.widget.invoice') -
+ @if (! $o->suspend_billing AND ! $o->external_billing) +
+ @include('common.service.widget.invoice') +
+ @endif
Invoices.
diff --git a/resources/views/theme/backend/adminlte/u/service/widgets/broadband/order.blade.php b/resources/views/theme/backend/adminlte/u/service/widgets/broadband/order.blade.php new file mode 100644 index 0000000..2ea73f9 --- /dev/null +++ b/resources/views/theme/backend/adminlte/u/service/widgets/broadband/order.blade.php @@ -0,0 +1,11 @@ +
+ +
+
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/u/service/widgets/information.blade.php b/resources/views/theme/backend/adminlte/u/service/widgets/information.blade.php index d0a93d6..5d0827f 100644 --- a/resources/views/theme/backend/adminlte/u/service/widgets/information.blade.php +++ b/resources/views/theme/backend/adminlte/u/service/widgets/information.blade.php @@ -1,4 +1,12 @@
+ @if($o->external_billing) +
+
+ EXTERNAL BILLING +
+
+ @endif +

Service Information

@@ -13,7 +21,7 @@ Status {!! $o->status_html !!} - @if ($o->active or $o->isPending()) + @if (($o->active OR $o->isPending()) AND ! $o->external_billing) Billed {{ $o->billing_period }} @@ -43,7 +51,7 @@ @if ($o->autopay)Direct Debit @else Invoice @endif - @elseif(! $o->wasCancelled()) + @elseif($o->wasCancelled()) Cancelled {!! $o->date_end ? $o->date_end->format('Y-m-d') : $o->paid_to->format('Y-m-d').'*' !!} diff --git a/routes/web.php b/routes/web.php index 9179a8d..2daa03e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -43,6 +43,9 @@ Route::group(['middleware'=>['theme:adminlte-be','auth','role:reseller'],'prefix Route::get('supplier/create','SuppliersController@create'); Route::post('supplier/store','SuppliersController@store'); Route::get('switch/start/{id}','\Leenooks\Controllers\AdminController@user_switch_start')->name('switch.user.stop'); + Route::match(['get','post'],'service/update/{o}','ServiceController@update') + ->where('o','[0-9]+') + ->middleware('can:update,o'); }); // Our User Routes @@ -63,6 +66,9 @@ Route::group(['middleware'=>['theme:adminlte-be','auth'],'prefix'=>'u'],function Route::get('service/{o}','UserHomeController@service') ->where('o','[0-9]+') ->middleware('can:view,o'); + Route::get('service/progress/{o}/{status}','UserHomeController@service_progress') + ->where('o','[0-9]+') + ->middleware('can:progress,o,status'); }); // Frontend Routes (Non-Authed Users)