Optimise charge table, implemented charge recording, optimised payment recording

This commit is contained in:
Deon George 2021-10-01 14:59:04 +10:00
parent c0ad46ba65
commit 7c5369203c
No known key found for this signature in database
GPG Key ID: 7670E8DC27415254
17 changed files with 731 additions and 48 deletions

View File

@ -4,8 +4,9 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use App\Models\{Account,Payment,PaymentItem,Service,SiteDetail}; use App\Models\{Account,Charge,InvoiceItem,Payment,PaymentItem,Service,SiteDetail};
class AdminController extends Controller class AdminController extends Controller
{ {
@ -14,6 +15,54 @@ class AdminController extends Controller
return View('a.service',['o'=>$o]); return View('a.service',['o'=>$o]);
} }
public function charge_addedit(Request $request,Charge $o)
{
if ($request->post()) {
$request->validate([
'account_id' => 'required|exists:accounts,id',
'charge_date' => 'required|date',
'service_id' => 'required|exists:ab_service,id',
'quantity' => 'required|numeric|not_in:0',
'amount' => 'required|numeric|min:0.01',
'sweep_type' => 'required|numeric|in:'.implode(',',array_keys(Charge::sweep)),
'type' => 'required|numeric|in:'.implode(',',array_keys(InvoiceItem::type)),
'taxable' => 'nullable|boolean',
'description' => 'nullable|string|max:128',
]);
if (! $o->exists) {
$o->site_id = config('SITE')->site_id;
$o->user_id = Auth::id();
$o->active = TRUE;
}
$o->forceFill($request->only(['account_id','charge_date','service_id','quantity','amount','sweep_type','type','taxable','description']));
$o->save();
return redirect()->back()
->with('success','Charge recorded: '.$o->id);
}
return view('a.charge.addedit')
->with('o',$o);
}
public function charge_pending_account(Request $request,Account $o)
{
return view('a.charge.widgets.pending')
->with('list',$o->charges->where('active',TRUE)->where('processed',NULL)->except($request->exclude));
}
/**
* List unprocessed charges
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function charge_unprocessed()
{
return view('a.charge.unprocessed');
}
/** /**
* Record payments on an account. * Record payments on an account.
* *
@ -64,13 +113,13 @@ class AdminController extends Controller
$oo->invoice_id = $id; $oo->invoice_id = $id;
} }
$oo->alloc_amt = $amount; $oo->alloc_amt = ($oo->invoice->due >= 0) && ($oo->invoice->due-$amount >= 0) ? $amount : 0;
$oo->site_id = config('SITE')->site_id; $oo->site_id = config('SITE')->site_id;
$o->items()->save($oo); $o->items()->save($oo);
} }
return redirect()->back() return redirect()->back()
->with('success','Payment recorded'); ->with('success','Payment recorded: '.$o->id);
} }
return view('a.payment.addedit') return view('a.payment.addedit')

View File

@ -2,7 +2,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Auth; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\Account;
class ResellerServicesController extends Controller class ResellerServicesController extends Controller
{ {
@ -21,6 +24,14 @@ class ResellerServicesController extends Controller
return ['data'=>Auth::user()->all_clients()->values()]; return ['data'=>Auth::user()->all_clients()->values()];
} }
public function services(Request $request,Account $o)
{
return $o->services
->filter(function($item) use ($request) {
return $item->active || ($item->id == $request->include);
});
}
public function service_inactive() public function service_inactive()
{ {
return ['data'=>Auth::user()->all_client_service_inactive()->values()]; return ['data'=>Auth::user()->all_client_service_inactive()->values()];

View File

@ -16,6 +16,7 @@ use App\Traits\NextKey;
* Attributes for accounts: * Attributes for accounts:
* + lid: : Local ID for account * + lid: : Local ID for account
* + sid: : System ID for account * + sid: : System ID for account
* + name: : Account Name
* *
* @package App\Models * @package App\Models
*/ */
@ -48,6 +49,11 @@ class Account extends Model implements IDs
/* RELATIONS */ /* RELATIONS */
public function charges()
{
return $this->hasMany(Charge::class);
}
/** /**
* Return the country the user belongs to * Return the country the user belongs to
*/ */

View File

@ -3,15 +3,67 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
/**
* CLEANUP NOTES:
* + Charge Date should not be null
* + Attributes should be a collection array
* + type should not be null
*/
class Charge extends Model class Charge extends Model
{ {
protected $table = 'ab_charge'; const CREATED_AT = 'date_orig';
protected $dates = ['date_charge']; const UPDATED_AT = 'date_last';
protected $dates = ['charge_date'];
public $dateFormat = 'U'; public $dateFormat = 'U';
public const sweep = [
// 0 => 'Daily',
// 1 => 'Weekly',
// 2 => 'Monthly',
// 3 => 'Quarterly',
// 4 => 'Semi-Annually',
// 5 => 'Annually',
6 => 'Service Rebill',
];
/* RELATIONS */
public function account()
{
return $this->belongsTo(Account::class);
}
public function service()
{
return $this->belongsTo(Service::class);
}
/* SCOPES */
public function scopeUnprocessed($query)
{
return $query
->where('active',TRUE)
->whereNotNull('charge_date')
->whereNotNull('type')
->where(function($q) {
return $q->where('processed',FALSE)
->orWhereNull('processed');
});
}
/* ATTRIBUTES */
public function getNameAttribute() public function getNameAttribute()
{ {
return sprintf('%s %s',$this->description,$this->getAttribute('attributes') ? join('|',unserialize($this->getAttribute('attributes'))) : ''); return sprintf('%s %s',$this->description,$this->getAttribute('attributes') ? join('|',unserialize($this->getAttribute('attributes'))) : '');
} }
public function getTypeAttribute($value)
{
return Arr::get(InvoiceItem::type,$value);
}
} }

View File

@ -37,6 +37,22 @@ class InvoiceItem extends Model
// Array of items that can be updated with PushNew // Array of items that can be updated with PushNew
protected $pushable = ['taxes']; protected $pushable = ['taxes'];
public const type = [
1 => 'Hardware', // *
2 => 'Service Relocation Fee', // * Must have corresponding SERVICE_ID
3 => 'Service Change', // * Must have corresponding SERVICE_ID
4 => 'Service Connection', // * Must have corresponding SERVICE_ID
6 => 'Service Cancellation', // * Must have corresponding SERVICE_ID
7 => 'Extra Product/Service Charge', // * Service Billing in advance, Must have corresponding SERVICE_ID
8 => 'Product Addition', // * Additional Product Customisation, Must have corresponding SERVICE_ID
120 => 'Credit/Debit Transfer', // * SERVICE_ID is NULL, MODULE_ID is NULL, MODULE_REF is NULL : INVOICE_ID is NOT NULL
123 => 'Shipping',
124 => 'Late Payment Fee', // * SERVICE_ID is NULL, MODULE_ID = CHECKOUT MODULE,
125 => 'Payment Fee', // * SERVICE_ID is NULL, MODULE_ID = CHECKOUT MODULE, MODULE_REF = CHECKOUT NAME
126 => 'Other', // * MODEL_ID should be a module
127 => 'Rounding', // * SERVICE_ID is NULL, MODULE_ID is NULL, MODULE_REF is NULL
];
/* RELATIONS */ /* RELATIONS */
public function invoice() public function invoice()
@ -98,6 +114,7 @@ class InvoiceItem extends Model
public function getItemTypeNameAttribute() public function getItemTypeNameAttribute()
{ {
// @todo use self::type
$types = [ $types = [
1=>'Hardware', // * 1=>'Hardware', // *
2=>'Service Relocation Fee', // * Must have corresponding SERVICE_ID 2=>'Service Relocation Fee', // * Must have corresponding SERVICE_ID
@ -118,8 +135,11 @@ class InvoiceItem extends Model
{ {
// * Line Charge Topic on Invoice. // * Line Charge Topic on Invoice.
case 0: case 0:
return sprintf('%s [%s]','Product/Service', if ($this->date_start)
$this->date_start == $this->date_stop ? $this->date_start->format('Y-m-d') : sprintf('%s -> %s',$this->date_start->format('Y-m-d'),$this->date_stop->format('Y-m-d'))); return sprintf('%s [%s]','Product/Service',
(($this->date_start == $this->date_stop) || (! $this->date_stop)) ? $this->date_start->format('Y-m-d') : sprintf('%s -> %s',$this->date_start->format('Y-m-d'),$this->date_stop->format('Y-m-d')));
else
return 'Product/Service';
// * Excess Service Item, of item 0, must have corresponding SERVICE_ID // * Excess Service Item, of item 0, must have corresponding SERVICE_ID
case 5: case 5:

View File

@ -3,11 +3,11 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Interfaces\IDs;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Leenooks\Traits\ScopeActive; use Leenooks\Traits\ScopeActive;
use App\Traits\{NextKey,PushNew};
use App\Interfaces\IDs;
use App\Traits\PushNew;
/** /**
* Class Payment * Class Payment
@ -74,7 +74,6 @@ class Payment extends Model implements IDs
public function scopeUnapplied($query) public function scopeUnapplied($query)
{ {
//DB::enableQueryLog();
return $query return $query
->select(['payments.id','payment_date','account_id','checkout_id','total_amt',DB::raw("SUM(alloc_amt) as allocated")]) ->select(['payments.id','payment_date','account_id','checkout_id','total_amt',DB::raw("SUM(alloc_amt) as allocated")])
->leftJoin('payment_items',['payment_items.payment_id'=>'payments.id']) ->leftJoin('payment_items',['payment_items.payment_id'=>'payments.id'])

View File

@ -17,6 +17,11 @@ class PaymentItem extends Model
/* RELATIONS */ /* RELATIONS */
public function invoice()
{
return $this->belongsTo(Invoice::class);
}
public function payment() { public function payment() {
return $this->belongsTo(Payment::class); return $this->belongsTo(Payment::class);
} }

View File

@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ReworkCharges extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('ab_charge', function (Blueprint $table) {
$table->dropForeign('fk_chg_acc');
$table->dropForeign('fk_chg_pdt');
$table->dropForeign('fk_chg_svc');
$table->dropIndex('fk_chg_acc_idx');
$table->dropIndex('fk_chg_svc_idx');
$table->dropIndex('fk_chg_pdt_idx');
$table->dropPrimary(['id','account_id','site_id']);
});
DB::statement('ALTER TABLE ab_charge RENAME TO charges');
DB::statement('ALTER TABLE charges RENAME COLUMN date_charge TO charge_date');
DB::statement('ALTER TABLE charges MODIFY COLUMN id INT auto_increment');
Schema::table('charges', function (Blueprint $table) {
$table->unique(['id','account_id','site_id']);
$table->foreign(['account_id','site_id'])->references(['id','site_id'])->on('accounts');
$table->foreign(['service_id','site_id'])->references(['id','site_id'])->on('ab_service');
$table->foreign(['product_id','site_id'])->references(['id','site_id'])->on('ab_product');
$table->integer('user_id')->unsigned()->nullable();
$table->foreign(['user_id','site_id'])->references(['id','site_id'])->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
abort(500,'cant go back');
}
}

View File

@ -0,0 +1,345 @@
@extends('adminlte::layouts.app')
@section('htmlheader_title')
Charge {{ $o->id ? '#'. $o->id : '' }}
@endsection
@section('page_title')
Charge
@endsection
@section('contentheader_title')
Record Charge
@endsection
@section('contentheader_description')
@endsection
@section('main-content')
<div class="row">
<div class="col-6">
<div class="card card-dark">
<div class="card-header">
<h1 class="card-title">Record Charge {{ $o->id ? '#'. $o->id : '' }}</h1>
@if(session()->has('success'))
<span class="ml-3 pt-0 pb-0 pr-1 pl-1 btn btn-outline-success"><small>{{ session()->get('success') }}</small></span>
@endif
</div>
<div class="card-body">
<form class="g-0 needs-validation" method="POST" role="form">
@csrf
<div class="row">
<!-- DATE CHARGE -->
<div class="col-4">
<div class="form-group has-validation">
<label for="charge_date">Date Charge</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-calendar"></i></span>
</div>
<input type="date" class="form-control @error('charge_date') is-invalid @enderror" id="charge_date" name="charge_date" value="{{ old('charge_date',($o->exists ? $o->charge_date : \Carbon\Carbon::now())->format('Y-m-d')) }}" required>
<span class="invalid-feedback" role="alert">
@error('charge_date')
{{ $message }}
@else
Charge Date is required.
@enderror
</span>
</div>
<span class="input-helper">Date Payment Received.</span>
</div>
</div>
<!-- QUANTITY -->
<div class="offset-6 col-2">
<div class="form-group has-validation">
<label class="float-right" for="quantity">Quantity</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-hashtag"></i></span>
</div>
<input type="text" class="text-right form-control @error('quantity') is-invalid @enderror" id="quantity" name="quantity" value="{{ old('quantity',$o->exists ? $o->quantity : 1) }}" required>
<span class="invalid-feedback" role="alert">
@error('quantity')
{{ $message }}
@else
Quantity is required.
@enderror
</span>
</div>
</div>
</div>
</div>
<div class="row">
<!-- ACCOUNTS -->
<div class="col-4">
<div class="form-group has-validation">
<label for="account_id">Account</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-user"></i></span>
</div>
<select class="form-control @error('account_id') is-invalid @enderror" id="account_id" name="account_id" required>
<option value=""></option>
@foreach (\App\Models\Account::active()->with(['user'])->get()->sortBy('name') as $ao)
<option value="{{ $ao->id }}" {{ $ao->id == old('account_id',$o->exists ? $o->account_id : NULL) ? 'selected' : '' }}>{{ $ao->name }}</option>
@endforeach
</select>
<span class="invalid-feedback" role="alert">
@error('account_id')
{{ $message }}
@else
Account is required.
@enderror
</span>
</div>
<span class="input-helper">Account to add charge to.</span>
</div>
</div>
<!-- SWEEP TYPE -->
<div class="offset-1 col-4">
<div class="form-group has-validation">
<label for="sweep_type">Sweep</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-dollar-sign"></i></span>
</div>
<select class="form-control @error('sweep_type') is-invalid @enderror" id="sweep_type" name="sweep_type" required>
@foreach (\App\Models\Charge::sweep as $k=>$v)
<option value="{{ $k }}" {{ $k == old('sweep_type',$o->exists ? $o->sweep_type : NULL) ? 'selected' : '' }}>{{ $v }}</option>
@endforeach
</select>
<span class="invalid-feedback" role="alert">
@error('sweep_type')
{{ $message }}
@else
Sweep Type is required.
@enderror
</span>
</div>
<span class="input-helper">When to add the charge to an invoice.</span>
</div>
</div>
<!-- TAXABLE -->
<div class="col-1">
<div class="form-check has-validation">
<label for="taxable">Taxable</label>
<div class="form-check text-right">
<input type="checkbox" class="form-check-input @error('taxable') is-invalid @enderror" id="taxable" name="taxable" value="1" {{ old('taxable',$o->exists ? $o->taxable : 1) ? 'checked' : '' }}>
</div>
</div>
</div>
<!-- AMOUNT -->
<div class="col-2">
<div class="form-group has-validation">
<label class="float-right" for="amount">Amount</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-dollar-sign"></i></span>
</div>
<input type="text" class="text-right form-control @error('amount') is-invalid @enderror" id="amount" name="amount" value="{{ number_format(old('amount',$o->exists ? $o->amount : 0),2) }}">
<span class="invalid-feedback" role="alert">
@error('amount')
{{ $message }}
@else
Amount is required.
@enderror
</span>
</div>
<span class="input-helper">Amount (ex Tax).</span>
</div>
</div>
</div>
<div class="row">
<!-- SERVICES -->
<div class="col-4">
<div class="form-group has-validation">
<label for="service_id">Services</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-bolt"></i></span>
</div>
<select class="form-control @error('service_id') is-invalid @enderror" id="service_id" name="service_id" required>
</select>
<span class="ml-2 pt-2"><i class="fas fa-spinner d-none"></i></span>
<span class="invalid-feedback" role="alert">
@error('service_id')
{{ $message }}
@else
Service is required.
@enderror
</span>
</div>
<span class="input-helper"><sup>**</sup>Service inactive.</span>
</div>
</div>
<!-- CHARGE TYPE -->
<div class="offset-1 col-4">
<div class="form-group has-validation">
<label for="type">Type</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-dollar-sign"></i></span>
</div>
<select class="form-control @error('type') is-invalid @enderror" id="type" name="type" required>
@foreach (collect(\App\Models\InvoiceItem::type)->sort() as $k=>$v)
<option value="{{ $k }}" {{ $k == old('type',$o->exists ? $o->type : NULL) ? 'selected' : '' }}>{{ $v }}</option>
@endforeach
</select>
<span class="invalid-feedback" role="alert">
@error('type')
{{ $message }}
@else
Type is required.
@enderror
</span>
</div>
<span class="input-helper">Charge type.</span>
</div>
</div>
<!-- TOTAL -->
<div class="offset-1 col-2">
<label class="float-right" for="fees_amt">Total</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-dollar-sign"></i></span>
</div>
<input type="text" class="text-right form-control" id="total" value="{{ number_format($o->exists ? $o->quantity*$o->amount : 0,2) }}" disabled>
</div>
<span class="input-helper">Total (ex Tax).</span>
</div>
</div>
<div class="row">
<!-- DESCRIPTION -->
<div class="col-12">
<div class="form-group has-validation">
<label for="description">Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-file-alt"></i></span>
</div>
<input type="text" class="form-control @error('description') is-invalid @enderror" id="description" name="description" value="{{ old('description',$o->exists ? $o->description : '') }}">
<span class="invalid-feedback" role="alert">
@error('description')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<a href="{{ url('/home') }}" class="btn btn-danger">Cancel</a>
@can('wholesaler')
<button type="submit" name="submit" class="btn btn-success mr-0 float-right">@if ($site->exists)Save @else Add @endif</button>
@endcan
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-5">
<div id="pending"></div>
</div>
</div>
@endsection
@section('page-scripts')
@css('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css','select-css')
@js('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js','select-js')
@js('/select2/fix-autofocus.js','select-fix-js','select-js')
@css('/select2/fix-select-height.css','select-fix-css','select-css')
<script type="text/javascript">
function populate(account,spinner) {
spinner.toggleClass('d-none').toggleClass('fa-spin');
$.ajax({
type: 'GET',
dataType: 'json',
cache: false,
url: '{{ url('api/r/services') }}'+'/'+account,
data: {include: {{ $o->service_id ?: 'null' }} },
timeout: 2000,
error: function(x) {
spinner.toggleClass('d-none').toggleClass('fa-spin');
alert('Failed to submit');
},
success: function(data) {
$("select[name=service_id]").empty();
$.each(data,function(i,j) {
var row = '<option value="' + j.id + '" '+(j.id == {{ $o->service_id ?: 'null' }} ? 'selected' : '')+'>' + j.id + ': ' + j.product_name + ' ' + j.name_short + ((! j.active) ? ' **' : '') +'</option>';
$(row).appendTo("select[name=service_id]");
});
spinner.toggleClass('d-none').toggleClass('fa-spin');
}
});
$.ajax({
type: 'GET',
dataType: 'html',
data: {exclude: {{ $o->id ?: 'null' }}},
cache: false,
url: '{{ url('r/charges') }}'+'/'+account,
timeout: 2000,
error: function(x) {
spinner.toggleClass('d-none').toggleClass('fa-spin');
alert('Failed to submit');
},
success: function(data) {
$("div[id=pending]").empty().append(data);
}
});
}
function total() {
$('#total').val(($('#quantity').val()*$('#amount').val()).toFixed(2));
}
$(document).ready(function() {
var spinner = $('#service_id').parent().find('i.fas.fa-spinner');
if ($('#account_id').val()) {
populate($('#account_id').val(),spinner);
}
$('#account_id').select2({
sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)),
})
.on('change',function(e) {
$("select[id=service_id]").empty();
if (! $(this).val()) {
return;
}
populate($(this).val(),spinner);
});
$('#service_id').select2();
if ($('#quantity').val() && $('#amount').val()) {
total();
}
$('#quantity').on('change',total);
$('#amount').on('change',total);
});
</script>
@append

View File

@ -0,0 +1,66 @@
@extends('adminlte::layouts.app')
@section('htmlheader_title')
Unprocessed Charges
@endsection
@section('page_title')
Unprocessed
@endsection
@section('contentheader_title')
Unprocessed Charges
@endsection
@section('contentheader_description')
@endsection
@section('main-content')
<div class="row">
<div class="col-9">
<div class="card">
<div class="card-body">
<table class="table table-striped table-hover" id="unprocessed_charges">
<thead>
<tr>
<th>ID</th>
<th>Date Created</th>
<th>Date Charge</th>
<th>Account</th>
<th>Service</th>
<th>Description</th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
@foreach(\App\Models\Charge::unprocessed()->with(['account.user','service'])->get() as $o)
<tr>
<td><a href="{{ url('a/charge/addedit',$o->id) }}">{{ $o->id }}</td>
<td>{{ $o->charge_date->format('Y-m-d') }}</td>
<td>{{ $o->date_orig->format('Y-m-d') }}</td>
<td>{{ $o->account->name }}</td>
<td>{{ $o->service->name_short }}</td>
<td>{{ $o->description }}</td>
<td class="text-right">{{ number_format($o->quantity*$o->amount,2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
@section('page-scripts')
@css('//cdn.datatables.net/1.10.25/css/dataTables.bootstrap4.min.css','jq-dt-css')
@js('//cdn.datatables.net/1.10.25/js/jquery.dataTables.min.js','jq-dt-js')
@js('//cdn.datatables.net/1.10.25/js/dataTables.bootstrap4.min.js','jq-dt-bs5-js','jq-dt-js')
<script type="text/javascript">
$(document).ready(function() {
$('#unprocessed_charges').DataTable( {
order: [1,'desc'],
});
});
</script>
@append

View File

@ -0,0 +1,37 @@
@if(($x=$list)->count())
<div class="card card-light">
<div class="card-header">
<h1 class="card-title">Pending Charges</h1>
</div>
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Date Created</th>
<th>Date Charge</th>
<th>Service</th>
<th>Type</th>
<th>Description</th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
@foreach ($x as $co)
<tr>
<td><a href="{{ url('a/charge/addedit',[$co->id]) }}">{{ $co->id }}</a></td>
<td>{{ $co->date_orig->format('Y-m-d') }}</td>
<td>{{ $co->charge_date->format('Y-m-d') }}</td>
<td>{{ $co->service->sid }}</td>
<td>{{ $co->type }}</td>
<td>{{ $co->description }}</td>
<td class="text-right">{{ number_format($co->quantity*$co->amount,2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif

View File

@ -30,6 +30,7 @@
@csrf @csrf
<div class="row"> <div class="row">
<!-- DATE RECEIVED -->
<div class="col-4"> <div class="col-4">
<div class="form-group has-validation"> <div class="form-group has-validation">
<label for="payment_date">Date Received</label> <label for="payment_date">Date Received</label>
@ -50,6 +51,7 @@
</div> </div>
</div> </div>
<!-- AMOUNT -->
<div class="offset-4 col-4"> <div class="offset-4 col-4">
<div class="form-group has-validation"> <div class="form-group has-validation">
<label for="total_amt">Amount</label> <label for="total_amt">Amount</label>
@ -72,6 +74,7 @@
</div> </div>
<div class="row"> <div class="row">
<!-- METHOD -->
<div class="col-4"> <div class="col-4">
<div class="form-group has-validation"> <div class="form-group has-validation">
<label for="checkout_id">Payment Method</label> <label for="checkout_id">Payment Method</label>
@ -97,6 +100,7 @@
</div> </div>
</div> </div>
<!-- PAYMENT FEE -->
<div class="offset-4 col-4"> <div class="offset-4 col-4">
<div class="form-group has-validation"> <div class="form-group has-validation">
<label for="fees_amt">Fee</label> <label for="fees_amt">Fee</label>
@ -119,6 +123,7 @@
</div> </div>
<div class="row"> <div class="row">
<!-- ACCOUNT -->
<div class="col-6"> <div class="col-6">
<div class="form-group has-validation"> <div class="form-group has-validation">
<label for="account_id">Account</label> <label for="account_id">Account</label>
@ -129,7 +134,7 @@
<!-- @todo Only show active accounts or accounts with outstanding invoices --> <!-- @todo Only show active accounts or accounts with outstanding invoices -->
<select class="form-control @error('account_id') is-invalid @enderror" id="account_id" name="account_id" required> <select class="form-control @error('account_id') is-invalid @enderror" id="account_id" name="account_id" required>
<option value=""></option> <option value=""></option>
@foreach (\App\Models\Account::active()->with(['user'])->get() as $ao) @foreach (\App\Models\Account::active()->with(['user'])->get()->sortBy('name') as $ao)
<option value="{{ $ao->id }}" {{ $ao->id == old('account_id',$o->exists ? $o->account_id : NULL) ? 'selected' : '' }}>{{ $ao->name }}</option> <option value="{{ $ao->id }}" {{ $ao->id == old('account_id',$o->exists ? $o->account_id : NULL) ? 'selected' : '' }}>{{ $ao->name }}</option>
@endforeach @endforeach
</select> </select>
@ -147,13 +152,14 @@
</div> </div>
</div> </div>
<!-- BALANCE -->
<div class="offset-2 col-4"> <div class="offset-2 col-4">
<label for="fees_amt">Balance</label> <label for="fees_amt">Balance</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-dollar-sign"></i></span> <span class="input-group-text"><i class="fas fa-fw fa-dollar-sign"></i></span>
</div> </div>
<input type="text" class="text-right form-control @error('fees_amt') is-invalid @enderror" value="{{ number_format($o->exists ? $o->balance : 0,2) }}" disabled> <input type="text" class="text-right form-control @error('fees_amt') is-invalid @enderror" id="balance" value="{{ number_format($o->exists ? $o->balance : 0,2) }}" disabled>
</div> </div>
</div> </div>
</div> </div>
@ -185,14 +191,10 @@
@endsection @endsection
@section('page-scripts') @section('page-scripts')
@css('//cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css','s2-css') @css('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css','select-css')
@js('//cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js','s2-js','jquery') @js('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js','select-js')
@js('/select2/fix-autofocus.js','select-fix-js','select-js')
<style> @css('/select2/fix-select-height.css','select-fix-css','select-css')
.select2-selection.select2-selection--single {
height: calc(2.25rem + 2px) !important;
}
</style>
<script type="text/javascript"> <script type="text/javascript">
function populate(account,spinner) { function populate(account,spinner) {
@ -203,7 +205,7 @@
dataType: 'html', dataType: 'html',
cache: false, cache: false,
url: '{{ url('api/r/invoices') }}'+'/'+account, url: '{{ url('api/r/invoices') }}'+'/'+account,
data: {pid:{{ $o->id }}}, data: {pid:{{ $o->id ?: 'null' }}},
timeout: 2000, timeout: 2000,
error: function(x) { error: function(x) {
spinner.toggleClass('d-none').toggleClass('fa-spin'); spinner.toggleClass('d-none').toggleClass('fa-spin');
@ -216,6 +218,16 @@
}); });
} }
function balance() {
var alloc = 0;
$('.invoice').each(function() {
alloc += $(this).val();
})
$('#balance').val(($('#total_amt').val()-alloc).toFixed(2))
}
$(document).ready(function() { $(document).ready(function() {
if ($('#account_id').val()) { if ($('#account_id').val()) {
var spinner = $('#account_id').parent().parent().find('i.fas.fa-spinner'); var spinner = $('#account_id').parent().parent().find('i.fas.fa-spinner');
@ -236,6 +248,10 @@
populate($(this).val(),spinner); populate($(this).val(),spinner);
}); });
$('#total_amt').on('change',balance);
}); });
$(document).on('change','.invoice',balance);
</script> </script>
@append @append

View File

@ -61,11 +61,6 @@
$(document).ready(function() { $(document).ready(function() {
$('#unapplied_payments').DataTable( { $('#unapplied_payments').DataTable( {
order: [1,'desc'], order: [1,'desc'],
orderFixed: [1,'desc']
});
$('#unapplied_payments tbody').on('click','tr', function () {
$(this).toggleClass('selected');
}); });
}); });
</script> </script>

View File

@ -27,7 +27,7 @@
<td>{{ number_format($io->total,2) }}</td> <td>{{ number_format($io->total,2) }}</td>
<td>{{ number_format($io->due,2) }}</td> <td>{{ number_format($io->due,2) }}</td>
<td class="text-right"> <td class="text-right">
<input type="text" class="text-right" name="invoices[{{ $io->id }}]" value="{{ number_format(($x=$io->paymentitems->filter(function($item) use ($pid) { return $item->payment_id == $pid; })) ? $x->sum('alloc_amt') : 0,2) }}"> <input type="text" class="text-right invoice" name="invoices[{{ $io->id }}]" value="{{ number_format(($x=$io->paymentitems->filter(function($item) use ($pid) { return $item->payment_id == $pid; })) ? $x->sum('alloc_amt') : 0,2) }}">
</td> </td>
</tr> </tr>
@endforeach @endforeach

View File

@ -23,27 +23,49 @@
</li> </li>
@can('wholesaler') @can('wholesaler')
<!-- PAYMENTS --> <!-- CHARGES -->
<li class="nav-item has-treeview @if(preg_match('#^payment/#',Route::current()->uri())) menu-open @endif"> <li class="nav-item has-treeview @if(preg_match('#^charge/#',Route::current()->uri())) menu-open @endif">
<a href="#" class="nav-link @if(preg_match('#^payment/#',Route::current()->uri())) active @endif"> <a href="#" class="nav-link @if(preg_match('#^charge/#',Route::current()->uri())) active @endif">
<i class="nav-icon fas fa-receipt"></i> <i class="nav-icon fas fa-plus"></i>
<p>Payments <i class="fas fa-angle-left right"></i></p> <p>Charges <i class="fas fa-angle-left right"></i></p>
</a> </a>
<ul class="nav nav-treeview"> <ul class="nav nav-treeview">
<li class="nav-item"> <li class="nav-item">
<a href="{{ url('a/payment/addedit') }}" class="nav-link @if(Route::current()->uri() == 'payment/addedit') active @endif"> <a href="{{ url('a/charge/addedit') }}" class="nav-link @if(Route::current()->uri() == 'charge/addedit') active @endif">
<i class="fas fa-money-bill nav-icon"></i> <p>New Payment</p> <i class="fas fa-cart-plus nav-icon"></i> <p>New Charge</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="{{ url('a/payment/unapplied') }}" class="nav-link @if(Route::current()->uri() == 'payment/unapplied') active @endif"> <a href="{{ url('a/charge/unprocessed') }}" class="nav-link @if(Route::current()->uri() == 'charge/unprocessed') active @endif">
<i class="fas fa-receipt nav-icon"></i> <p>Unapplied</p> <i class="fas fa-list nav-icon"></i> <p>Unprocessed</p>
</a> </a>
</li> </li>
</ul> </ul>
</li> </li>
<!-- PAYMENTS -->
<li class="nav-item has-treeview @if(preg_match('#^payment/#',Route::current()->uri())) menu-open @endif">
<a href="#" class="nav-link @if(preg_match('#^payment/#',Route::current()->uri())) active @endif">
<i class="nav-icon fas fa-receipt"></i>
<p>Payments <i class="fas fa-angle-left right"></i></p>
</a>
<ul class="nav nav-treeview">
<li class="nav-item">
<a href="{{ url('a/payment/addedit') }}" class="nav-link @if(Route::current()->uri() == 'payment/addedit') active @endif">
<i class="fas fa-money-bill nav-icon"></i> <p>New Payment</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url('a/payment/unapplied') }}" class="nav-link @if(Route::current()->uri() == 'payment/unapplied') active @endif">
<i class="fas fa-receipt nav-icon"></i> <p>Unapplied</p>
</a>
</li>
</ul>
</li>
@endcan @endcan
@can('wholesaler') @can('wholesaler')

View File

@ -18,6 +18,8 @@ use App\Http\Controllers\{AdminController,
Route::group(['middleware'=>['auth:api','role:reseller']], function() { Route::group(['middleware'=>['auth:api','role:reseller']], function() {
// Route::get('/r/agents','ResellerServicesController@agents'); // Route::get('/r/agents','ResellerServicesController@agents');
Route::get('/r/accounts',[ResellerServicesController::class,'accounts']); Route::get('/r/accounts',[ResellerServicesController::class,'accounts']);
Route::get('/r/services/{o}',[ResellerServicesController::class,'services'])
->where('o','[0-9]+');
// Route::get('/r/clients','ResellerServicesController@clients'); // Route::get('/r/clients','ResellerServicesController@clients');
// Route::get('/r/service_inactive','ResellerServicesController@service_inactive'); // Route::get('/r/service_inactive','ResellerServicesController@service_inactive');
Route::post('r/invoices/{o}',[AdminController::class,'pay_invoices']) Route::post('r/invoices/{o}',[AdminController::class,'pay_invoices'])

View File

@ -44,6 +44,11 @@ Route::group(['middleware'=>['theme:adminlte-be','auth','role:wholesaler'],'pref
// Route::post('service/{o}','AdminHomeController@service_update'); // Route::post('service/{o}','AdminHomeController@service_update');
// Route::get('report/products','Wholesale\ReportController@products'); // Route::get('report/products','Wholesale\ReportController@products');
// Charges
Route::match(['get','post'],'charge/addedit/{o?}',[AdminController::class,'charge_addedit']);
Route::get('charge/unprocessed',[AdminController::class,'charge_unprocessed']);
// Payments
Route::match(['get','post'],'payment/addedit/{o?}',[AdminController::class,'pay_addedit']); Route::match(['get','post'],'payment/addedit/{o?}',[AdminController::class,'pay_addedit']);
Route::get('payment/unapplied',[AdminController::class,'pay_unapplied']); Route::get('payment/unapplied',[AdminController::class,'pay_unapplied']);
@ -65,6 +70,10 @@ Route::group(['middleware'=>['theme:adminlte-be','auth','role:reseller'],'prefix
Route::group(['middleware'=>['theme:adminlte-be','auth','role:reseller'],'prefix'=>'report'],function() { Route::group(['middleware'=>['theme:adminlte-be','auth','role:reseller'],'prefix'=>'report'],function() {
Route::get('domain',[ServiceController::class,'domain_list']); Route::get('domain',[ServiceController::class,'domain_list']);
}); });
// Charges on an account
Route::get('charges/{o}',[AdminController::class,'charge_pending_account'])
->where('o','[0-9]+');
}); });
// Our User Routes // Our User Routes