Compare commits

...

3 Commits

Author SHA1 Message Date
1a20b7a3f1 Added TxnTaxDetail to InvoiceAdd
Some checks failed
Create Docker Image / Build Docker Image (x86_64) (push) Failing after 30s
Create Docker Image / Final Docker Image Manifest (push) Has been skipped
2024-08-14 22:23:11 +10:00
9c4d446121 Framework updates 2024-08-14 22:21:32 +10:00
b87e9ff33a Rework products with components 2024-08-14 22:16:09 +10:00
13 changed files with 197 additions and 343 deletions

View File

@ -5,19 +5,20 @@ namespace App\Console\Commands\Intuit;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Intuit\Jobs\AccountingInvoiceUpdate; use Intuit\Jobs\AccountingInvoiceUpdate;
use Intuit\Models\Invoice as AccInvoice; use Intuit\Models\Invoice as AccInvoice;
use Intuit\Traits\ProviderTokenTrait;
use App\Models\{Invoice,ProviderOauth,User}; use App\Models\Invoice;
class InvoiceAdd extends Command class InvoiceAdd extends Command
{ {
private const provider = 'intuit'; use ProviderTokenTrait;
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'accounting:invoice:add' protected $signature = 'intuit:invoice:add'
.' {id : Invoice ID}' .' {id : Invoice ID}'
.' {user? : User Email}'; .' {user? : User Email}';
@ -36,44 +37,46 @@ class InvoiceAdd extends Command
*/ */
public function handle() public function handle()
{ {
$uo = User::where('email',$this->argument('user') ?: config('osb.admin'))->singleOrFail(); $to = $this->providerToken($this->argument('user'));
$so = ProviderOauth::where('name',self::provider)->singleOrFail(); $io = Invoice::findOrFail($this->argument('id'));
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
$o = Invoice::findOrFail($this->argument('id'));
// Check the customer exists // Check the customer exists
if ($o->account->providers->where('pivot.provider_oauth_id',$so->id)->count() !== 1) if ($io->account->providers->where('pivot.provider_oauth_id',$to->provider->id)->count() !== 1)
throw new \Exception(sprintf('Account [%d] for Invoice [%d] not defined',$o->account_id,$o->id)); throw new \Exception(sprintf('Account [%d] for Invoice [%d] not defined',$io->account_id,$io->id));
$ao = $o->account->providers->where('pivot.provider_oauth_id',$so->id)->pop(); $ao = $io->account->providers->where('pivot.provider_oauth_id',$to->provider->id)->pop();
// Some validation // Some validation
if (! $ao->pivot->ref) { if (! $ao->pivot->ref) {
$this->error(sprintf('Accounting not defined for account [%d]',$o->account_id)); $this->error(sprintf('Accounting not defined for account [%d]',$io->account_id));
exit(1);
return self::FAILURE;
} }
$acc = new AccInvoice; $acc = new AccInvoice;
$acc->CustomerRef = (object)['value'=>$ao->pivot->ref]; $acc->CustomerRef = (object)['value'=>$ao->pivot->ref];
$acc->DocNumber = $o->lid; $acc->DocNumber = $io->lid;
$acc->TxnDate = $o->created_at->format('Y-m-d'); $acc->TxnDate = $io->created_at->format('Y-m-d');
$acc->DueDate = $o->due_at->format('Y-m-d'); $acc->DueDate = $io->due_at->format('Y-m-d');
$lines = collect(); $lines = collect();
$c = 0; $c = 0;
$subtotal = 0;
// @todo Group these by ItemRef and the Same Unit Price and Description, so that we can then use quantity to represent the number of them. // @todo Group these by ItemRef and the Same Unit Price and Description, so that we can then use quantity to represent the number of them.
foreach ($o->items->groupBy(function($item) use ($so) { foreach ($io->items->groupBy(
return sprintf('%s.%s.%s.%s',$item->item_type_name,$item->price_base,$item->product->provider_ref($so),$item->taxes->pluck('description')->join('|')); fn($item)=>
}) as $os) sprintf('%s.%s.%s.%s',
$item->item_type_name,
$item->price_base,
$item->product->provider_ref($to->provider),
$item->taxes->pluck('description')->join('|'))) as $os)
{ {
$key = $os->first(); $key = $os->first();
// Some validation // Some validation
if (! ($ref=$key->product->provider_ref($so))) { if (! ($ref=$key->product->provider_ref($to->provider))) {
$this->error(sprintf('Accounting not defined in product [%d]',$key->product_id)); $this->error(sprintf('Accounting not defined in product [%d]',$key->product_id));
return self::FAILURE; return self::FAILURE;
@ -95,15 +98,36 @@ class InvoiceAdd extends Command
'UnitPrice' => $key->price_base, 'UnitPrice' => $key->price_base,
'ItemRef' => ['value'=>$ref], 'ItemRef' => ['value'=>$ref],
// @todo It is assumed there is only 1 tax category // @todo It is assumed there is only 1 tax category
'TaxCodeRef' => ['value'=>$key->taxes->first()->tax->provider_ref($so)], 'TaxCodeRef' => ['value'=>$key->taxes->first()->tax->provider_ref($to->provider)],
]; ];
$line->Amount = $os->sum('quantity')*$key->price_base; $line->Amount = round($os->sum('quantity')*$key->price_base,2);
$subtotal += $line->Amount;
$lines->push($line); $lines->push($line);
} }
$acc->Line = $lines; $acc->Line = $lines;
// If our subtotal doesnt match, we need to add a tax line
if ($io->subtotal !== $subtotal) {
$acc->TxnTaxDetail = (object)[
'TotalTax' => $x=$io->total-$subtotal,
'TaxLine' => [
(object) [
'Amount' => $x,
'DetailType' => 'TaxLineDetail',
'TaxLineDetail' => (object)[
'TaxRateRef' => (object)['value'=>23],
'NetAmountTaxable' => $io->subtotal,
]
]
]
];
}
dump($acc);
return AccountingInvoiceUpdate::dispatchSync($to,$acc); return AccountingInvoiceUpdate::dispatchSync($to,$acc);
} }
} }

View File

@ -1,38 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Collection;
use App\Models\{ProviderOauth,User};
class AccountingController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
/**
* Query the accounting system and get a valid list of accounting codes
*
* @param string $provider
* @return Collection
*/
public static function list(string $provider): Collection
{
// @todo This should be a variable
$uo = User::findOrFail(1);
$so = ProviderOauth::where('name',$provider)->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
$api = $to->API();
return $api->getItems()
->pluck('pid','Id')
->transform(function($item,$value) { return ['id'=>$value,'value'=>$item]; })
->values();
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Http\Requests\ProductAddEdit; use App\Http\Requests\ProductAddEdit;
@ -17,14 +18,14 @@ class ProductController extends Controller
* @return Collection * @return Collection
* @throws \Exception * @throws \Exception
*/ */
public function api_supplier_products(Request $request): Collection public function api_supplied_products(Request $request): Collection
{ {
switch ($request->type) { switch ($request->type) {
case Product\Broadband::class: case Product\Broadband::class:
return Product\Broadband::select(['id','supplier_item_id']) return Product\Broadband::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier']) ->with(['supplied.supplier_detail.supplier'])
->get() ->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; }) ->map(fn($item)=>['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)])
->sortBy('name') ->sortBy('name')
->values(); ->values();
@ -32,7 +33,7 @@ class ProductController extends Controller
return Product\Domain::select(['id','supplier_item_id']) return Product\Domain::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier']) ->with(['supplied.supplier_detail.supplier'])
->get() ->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; }) ->map(fn($item)=>['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)])
->sortBy('name') ->sortBy('name')
->values(); ->values();
@ -40,7 +41,7 @@ class ProductController extends Controller
return Product\Email::select(['id','supplier_item_id']) return Product\Email::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier']) ->with(['supplied.supplier_detail.supplier'])
->get() ->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; }) ->map(fn($item)=>['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)])
->sortBy('name') ->sortBy('name')
->values(); ->values();
@ -48,7 +49,7 @@ class ProductController extends Controller
return Product\Generic::select(['id','supplier_item_id']) return Product\Generic::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier']) ->with(['supplied.supplier_detail.supplier'])
->get() ->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; }) ->map(fn($item)=>['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)])
->sortBy('name') ->sortBy('name')
->values(); ->values();
@ -56,7 +57,7 @@ class ProductController extends Controller
return Product\Host::select(['id','supplier_item_id']) return Product\Host::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier']) ->with(['supplied.supplier_detail.supplier'])
->get() ->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; }) ->map(fn($item)=>['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)])
->sortBy('name') ->sortBy('name')
->values(); ->values();
@ -64,7 +65,7 @@ class ProductController extends Controller
return Product\Phone::select(['id','supplier_item_id']) return Product\Phone::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier']) ->with(['supplied.supplier_detail.supplier'])
->get() ->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; }) ->map(fn($item)=>['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)])
->sortBy('name') ->sortBy('name')
->values(); ->values();
@ -73,32 +74,16 @@ class ProductController extends Controller
} }
} }
/** public function addedit(ProductAddEdit $request,Product $o)
* Update a suppliers details
*
* @param Request $request
* @param Product $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
*/
public function details(Request $request,Product $o)
{ {
if (! $o->exists && $request->name) foreach (Arr::except($request->validated(),['translate','accounting','pricing','active']) as $key => $item)
$o = Product::where('name',$request->name)->firstOrNew();
return view('theme.backend.adminlte.product.details')
->with('breadcrumb',collect(['Products'=>url('a/product')]))
->with('o',$o);
}
public function details_addedit(ProductAddEdit $request,Product $o)
{
foreach ($request->except(['_token','submit','translate','accounting']) as $key => $item)
$o->{$key} = $item; $o->{$key} = $item;
$o->active = (bool)$request->active; $o->active = (bool)$request->active;
// Trim down the pricing array, remove null values // Trim down the pricing array, remove null values
$o->pricing = $o->pricing->map(function($item) { $o->pricing = collect($request->validated('pricing'))
->map(function($item) {
foreach ($item as $k=>$v) { foreach ($item as $k=>$v) {
if (is_array($v)) { if (is_array($v)) {
$v = array_filter($v); $v = array_filter($v);
@ -111,39 +96,32 @@ class ProductController extends Controller
try { try {
$o->save(); $o->save();
} catch (\Exception $e) { } catch (\Exception $e) {
return redirect()->back()->withErrors($e->getMessage())->withInput(); return redirect()
->back()
->withErrors($e->getMessage())
->withInput();
} }
$o->load(['translate']); $o->load(['translate']);
$oo = $o->translate ?: new ProductTranslate; $oo = $o->translate ?: new ProductTranslate;
foreach ($request->get('translate',[]) as $key => $item) foreach ($request->validated('translate',[]) as $key => $item)
$oo->{$key} = $item; $oo->{$key} = $item;
$o->translate()->save($oo); $o->translate()->save($oo);
if ($request->accounting) foreach ($request->validated('accounting',[]) as $k=>$v) {
foreach ($request->accounting as $k=>$v)
$o->providers()->syncWithoutDetaching([ $o->providers()->syncWithoutDetaching([
$k => [ $k => [
'ref' => $v, 'ref' => $v,
'site_id'=>$o->site_id, 'site_id'=>$o->site_id,
], ],
]); ]);
}
return redirect() return redirect()
->back() ->back()
->with('success','Product saved'); ->with('success','Product saved');
} }
/**
* Manage products for a site
*
* @note This method is protected by the routes
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function home()
{
return view('theme.backend.adminlte.product.home');
}
} }

View File

@ -3,7 +3,9 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate;
use App\Models\ProviderOauth;
/** /**
* Editing Suppliers * Editing Suppliers
@ -17,7 +19,7 @@ class ProductAddEdit extends FormRequest
*/ */
public function authorize() public function authorize()
{ {
return Auth::user()->isWholesaler(); return Gate::allows('wholesaler');
} }
/** /**
@ -34,7 +36,21 @@ class ProductAddEdit extends FormRequest
'active' => 'sometimes|accepted', 'active' => 'sometimes|accepted',
'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|array', // @todo Validate that the value is in the accounting system 'accounting' => [
'nullable',
'array',
function (string $attribute,mixed $value,\Closure $fail) {
if (! is_array($value))
$fail("Invalid format for {$attribute}");
foreach ($value as $k=>$v) {
if (! ProviderOauth::where('id',$k)->exists())
$fail("Provider doesnt exist [$k]");
// @todo Validate that the value is in the accounting system
}
},
],
'pricing' => 'required|array', // @todo Validate the elements in the pricing 'pricing' => 'required|array', // @todo Validate the elements in the pricing
]; ];
} }

View File

@ -11,10 +11,11 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Intuit\Exceptions\NotTokenException;
use Intuit\Traits\ProviderTokenTrait;
use Leenooks\Traits\ScopeActive; use Leenooks\Traits\ScopeActive;
use App\Casts\CollectionOrNull; use App\Casts\CollectionOrNull;
use App\Http\Controllers\AccountingController;
use App\Interfaces\{IDs,ProductItem}; use App\Interfaces\{IDs,ProductItem};
use App\Traits\{ProductDetails,ProviderRef}; use App\Traits\{ProductDetails,ProviderRef};
@ -66,7 +67,7 @@ use App\Traits\{ProductDetails,ProviderRef};
*/ */
class Product extends Model implements IDs class Product extends Model implements IDs
{ {
use HasFactory,ProductDetails,ScopeActive,ProviderRef; use HasFactory,ProductDetails,ScopeActive,ProviderRef,ProviderTokenTrait;
protected $casts = [ protected $casts = [
'pricing' => CollectionOrNull::class, 'pricing' => CollectionOrNull::class,
@ -368,7 +369,18 @@ class Product extends Model implements IDs
public function accounting(string $provider): Collection public function accounting(string $provider): Collection
{ {
return AccountingController::list($provider); $so = ProviderOauth::where('name',self::provider)
->sole();
if (! ($to=$so->token(Auth::user())))
throw new NotTokenException(sprintf('Unknown Tokens for [%s]',Auth::user()->email));
return $to
->API()
->getItems()
->pluck('pid','Id')
->transform(fn($item,$value)=>['id'=>$value,'value'=>$item])
->values();
} }
/** /**

View File

@ -35,5 +35,6 @@ class AppServiceProvider extends ServiceProvider
Route::model('co',\App\Models\Checkout::class); Route::model('co',\App\Models\Checkout::class);
Route::model('po',\App\Models\Payment::class); Route::model('po',\App\Models\Payment::class);
Route::model('pdo',\App\Models\Product::class);
} }
} }

36
composer.lock generated
View File

@ -2222,16 +2222,16 @@
}, },
{ {
"name": "league/commonmark", "name": "league/commonmark",
"version": "2.5.1", "version": "2.5.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/commonmark.git", "url": "https://github.com/thephpleague/commonmark.git",
"reference": "ac815920de0eff6de947eac0a6a94e5ed0fb147c" "reference": "df09d5b6a4188f8f3c3ab2e43a109076a5eeb767"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/ac815920de0eff6de947eac0a6a94e5ed0fb147c", "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/df09d5b6a4188f8f3c3ab2e43a109076a5eeb767",
"reference": "ac815920de0eff6de947eac0a6a94e5ed0fb147c", "reference": "df09d5b6a4188f8f3c3ab2e43a109076a5eeb767",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2324,7 +2324,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-07-24T12:52:09+00:00" "time": "2024-08-14T10:56:57+00:00"
}, },
{ {
"name": "league/config", "name": "league/config",
@ -3021,11 +3021,11 @@
}, },
{ {
"name": "leenooks/intuit", "name": "leenooks/intuit",
"version": "0.2.0", "version": "0.2.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://gitea.dege.au/laravel/intuit.git", "url": "https://gitea.dege.au/laravel/intuit.git",
"reference": "0cc4a217bdeabf0ad7973d9d159c823125492174" "reference": "9d81824b52dadc7b5fc0db9912373ecfb0942da7"
}, },
"require": { "require": {
"jenssegers/model": "^1.5" "jenssegers/model": "^1.5"
@ -3055,15 +3055,15 @@
"laravel", "laravel",
"leenooks" "leenooks"
], ],
"time": "2024-08-12T10:55:41+00:00" "time": "2024-08-14T12:18:57+00:00"
}, },
{ {
"name": "leenooks/laravel", "name": "leenooks/laravel",
"version": "11.1.10", "version": "11.1.11",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://gitea.dege.au/laravel/leenooks.git", "url": "https://gitea.dege.au/laravel/leenooks.git",
"reference": "92dd7ac3cb6598e55af3cbc95d9cf7af44318a30" "reference": "83470c3ff575e62a3e7b66f07177ef9c95c8a54d"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -3096,7 +3096,7 @@
"laravel", "laravel",
"leenooks" "leenooks"
], ],
"time": "2024-08-10T12:25:03+00:00" "time": "2024-08-14T12:19:53+00:00"
}, },
{ {
"name": "leenooks/passkey", "name": "leenooks/passkey",
@ -8912,16 +8912,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "11.3.0", "version": "11.3.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "a8dce73a8938dfec7ac0daa91bdbcaae7d7188a3" "reference": "fe179875ef0c14e90b75617002767eae0a742641"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a8dce73a8938dfec7ac0daa91bdbcaae7d7188a3", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fe179875ef0c14e90b75617002767eae0a742641",
"reference": "a8dce73a8938dfec7ac0daa91bdbcaae7d7188a3", "reference": "fe179875ef0c14e90b75617002767eae0a742641",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8942,7 +8942,7 @@
"phpunit/php-timer": "^7.0.1", "phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2", "sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.1", "sebastian/code-unit": "^3.0.1",
"sebastian/comparator": "^6.0.1", "sebastian/comparator": "^6.0.2",
"sebastian/diff": "^6.0.2", "sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.0", "sebastian/environment": "^7.2.0",
"sebastian/exporter": "^6.1.3", "sebastian/exporter": "^6.1.3",
@ -8992,7 +8992,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.0" "source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.1"
}, },
"funding": [ "funding": [
{ {
@ -9008,7 +9008,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-08-02T03:56:43+00:00" "time": "2024-08-13T06:14:23+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",

View File

@ -1,14 +1,14 @@
@extends('adminlte::layouts.app') @extends('adminlte::layouts.app')
@section('htmlheader_title') @section('htmlheader_title')
{{ $o->name ?: 'New Product' }} {{ $pdo->name ?: 'New Product' }}
@endsection @endsection
@section('page_title') @section('page_title')
{{ $o->name ?: 'New Product' }} {{ $pdo->name ?: 'New Product' }}
@endsection @endsection
@section('contentheader_title') @section('contentheader_title')
{{ $o->name ?: 'New Product' }} {{ $pdo->name ?: 'New Product' }}
@endsection @endsection
@section('contentheader_description') @section('contentheader_description')
@endsection @endsection
@ -23,8 +23,8 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header bg-dark d-flex p-0"> <div class="card-header bg-dark p-2">
<ul class="nav nav-pills w-100 p-2"> <ul class="nav nav-pills">
<li class="nav-item"><a class="nav-link active" href="#details" data-toggle="tab">Detail</a></li> <li class="nav-item"><a class="nav-link active" href="#details" data-toggle="tab">Detail</a></li>
<li class="nav-item"><a class="nav-link" href="#services" data-toggle="tab">Services</a></li> <li class="nav-item"><a class="nav-link" href="#services" data-toggle="tab">Services</a></li>
</ul> </ul>

View File

@ -1,54 +1,32 @@
<!-- $o = Product::class --> <!-- $pdo=Product::class -->
@use(App\Models\Group)
@use(App\Models\Invoice)
@use(App\Models\Product)
@use(App\Models\ProviderOauth)
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h3>Product Details @include('adminlte::widget.success_button')</h3> <h3>Product Details <x-leenooks::button.success class="float-right"/></h3>
<hr> <hr>
<form class="g-0 needs-validation" method="POST" enctype="multipart/form-data" role="form"> <form method="POST">
@csrf @csrf
<div class="row"> <div class="row">
<!-- Product ID --> <!-- Product ID -->
<div class="col-12 col-sm-9 col-md-4 col-xl-3"> <div class="col-12 col-sm-9 col-md-4 col-xl-3">
@include('adminlte::widget.form_text',[ <x-leenooks::form.text id="translate.name_short" name="translate[name_short]" icon="fa-atom" label="Product ID" old="translate.name_short" :value="$pdo->pid"/>
'label'=>'Product ID',
'icon'=>'fas fa-atom',
'id'=>'translate.name_short',
'old'=>'translate.name_short',
'name'=>'translate[name_short]',
'value'=>$o->pid,
])
</div> </div>
<!-- Product Name --> <!-- Product Name -->
<div class="col-12 col-sm-9 col-md-8 col-xl-9"> <div class="col-12 col-sm-9 col-md-8 col-xl-9">
@include('adminlte::widget.form_text',[ <x-leenooks::form.text id="translate.name_detail" name="translate[name_detail]" icon="fa-atom" label="Product Name" old="translate.name_detail" :value="$pdo->name"/>
'label'=>'Product Name',
'icon'=>'fas fa-atom',
'id'=>'translate.name_detail',
'old'=>'translate.name_detail',
'name'=>'translate[name_detail]',
'value'=>$o->name,
])
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="form-group @error('description') is-invalid @enderror"> <x-leenooks::form.textarea id="translate.description" name="translate[description]" label="Product Description" placeholder="Full Description..." old="translate.description" :value="$pdo->description"/>
<!-- Product Description -->
<label for="description_full" class="col-form-label">Product Description</label>
<div class="input-group">
<div class="w-100">
<textarea class="textarea" id="description" name="translate[description]" placeholder="Full Description">{!! old('description',$o->description) ?? '' !!}</textarea>
<span class="invalid-feedback" role="alert">
@error('description')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -57,48 +35,21 @@
<div class="row"> <div class="row">
<!-- Active --> <!-- Active -->
<div class="col-6"> <div class="col-6">
@include('adminlte::widget.form_toggle',[ <x-leenooks::form.toggle name="active" label="Active" :value="$pdo->active"/>
'label'=>'Active',
'id'=>'active',
'old'=>'active',
'name'=>'active',
'value'=>$o->active ?? '',
])
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<!-- Product Type --> <!-- Product Type -->
<div class="col-12"> <div class="col-12">
@include('adminlte::widget.form_select',[ <x-leenooks::form.select name="model" icon="fa-list" label="Product" choose="true" groupby="active" :value="get_class($pdo->type)" :options="Product::availableTypes()->transform(fn($item)=>['id'=>$item,'value'=>$item])"/>
'label'=>'Product Type',
'icon'=>'fas fa-list',
'id'=>'model',
'old'=>'model',
'name'=>'model',
'options'=>\App\Models\Product::availableTypes()->transform(function($item) { return ['id'=>$item,'value'=>$item]; }),
'value'=>get_class($o->type),
])
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<!-- Supplied Product --> <!-- Supplied Product -->
<div class="col-12" id="supplier_product"> <div class="col-12" id="supplied_product">
<div class="form-group"> <x-leenooks::form.select name="model_id" icon="fa-shopping-cart" label="Supplied Product"/>
<label for="model_id">Supplied Product</label>
<div class="input-group has-validation">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa-fw fas fa-shopping-cart"></i></span>
</div>
<select class="form-control @error('model_id') is-invalid @enderror" id="model_id" name="model_id"></select>
<span class="invalid-feedback" role="alert">
@error('model_id')
{{ $message }}
@enderror
</span>
</div>
</div>
</div> </div>
</div> </div>
@ -107,26 +58,10 @@
<div class="col-12"> <div class="col-12">
<span class="h5">Accounting</span> <span class="h5">Accounting</span>
<hr> <hr>
@foreach (\App\Models\ProviderOauth::providers() as $apo)
<div class="form-group"> <!-- @todo When returning with a bad value old() is not selecting the previous value, may need to have the full html here instead of a component -->
<label for="{{ $x=sprintf('acc_%d',$apo->id) }}">{{ ucfirst($apo->name) }}</label> @foreach (ProviderOauth::providers() as $apo)
<div class="input-group has-validation"> <x-leenooks::form.select :id="sprintf('acc_%d',$apo->id)" :name="sprintf('accounting[%d]',$apo->id)" icon="fa-calculator" :label="ucfirst($apo->name)" :choose="true" :value="$pdo->provider_ref($apo)" old="accounting" :options="$pdo->accounting($apo->name)"/>
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa-fw fas fa-calculator"></i></span>
</div>
<select class="form-control @error($x) is-invalid @enderror select" id="{{ $x }}" name="{{ sprintf('accounting[%d]',$apo->id) }}">
<option></option>
@foreach ($o->accounting($apo->name) as $v)
<option value="{{ $v['id'] }}" @if($o->provider_ref($apo) === (string)$v['id'])selected @endif>{{ $v['value'] }}</option>
@endforeach
</select>
<span class="invalid-feedback" role="alert">
@error($x)
{{ $message }}
@enderror
</span>
</div>
</div>
@endforeach @endforeach
</div> </div>
</div> </div>
@ -136,20 +71,20 @@
<span class="h5">Pricing</span><small> Ex Taxes</small> <span class="h5">Pricing</span><small> Ex Taxes</small>
<hr> <hr>
@error('pricing') @error('pricing')
@include('adminlte::widget.errors') <x-leenooks::errors/>
@enderror @enderror
<ul class="nav nav-pills w-100 pl-0 pt-2 pb-2"> <ul class="nav nav-pills w-100 pl-0 pt-2 pb-2">
@foreach(\App\Models\Group::pricing()->active()->get() as $go) @foreach($g=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> <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 @endforeach
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
@foreach(\App\Models\Group::pricing()->active()->get() as $go) @foreach($g as $go)
<div class="tab-pane fade @if(! $loop->index)show active @endif" id="pg_{{ $go->id }}" role="tabpanel"> <div class="tab-pane fade @if(! $loop->index)show active @endif" id="pg_{{ $go->id }}">
@foreach(\App\Models\Invoice::billing_periods as $bp=>$detail) @foreach(Invoice::billing_periods as $bp=>$detail)
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<div class="form-group"> <div class="form-group">
@ -158,13 +93,13 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa-fw fas fa-calculator"></i></span> <span class="input-group-text"><i class="fa-fw fas fa-calculator"></i></span>
</div> </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> <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=$pdo->charge_available($bp,$go)) ? 'value' : 'placeholder' }}="{{ $c=$pdo->charge($bp,$go,'base') }}" @if(is_null($c) || ! $ca) disabled @endif>
<div class="input-group-append"> <div class="input-group-append">
<div class="input-group-text"> <div class="input-group-text">
<input type="checkbox" name="{{ sprintf('pricing[%d][show]',$bp) }}" @if($c && $ca)checked @endif> <input type="checkbox" name="{{ sprintf('pricing[%d][show]',$bp) }}" @if($c && $ca)checked @endif>
</div> </div>
</div> </div>
<span class="invalid-feedback" role="alert"> <span class="invalid-feedback">
@error($x) @error($x)
{{ $message }} {{ $message }}
@enderror @enderror
@ -180,8 +115,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa-fw fas fa-cog"></i></span> <span class="input-group-text"><i class="fa-fw fas fa-cog"></i></span>
</div> </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> <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=$pdo->charge($bp,$go,'setup') }}" @if(is_null($c) || ! $ca) disabled @endif>
<span class="invalid-feedback" role="alert"> <span class="invalid-feedback">
@error($x) @error($x)
{{ $message }} {{ $message }}
@enderror @enderror
@ -198,12 +133,9 @@
</div> </div>
<div class="row"> <div class="row">
<!-- Buttons --> <div class="col">
<div class="col-12"> <x-leenooks::button.reset/>
<a href="{{ url('/home') }}" class="btn btn-danger">Cancel</a> <x-leenooks::button.submit class="float-right">Save</x-leenooks::button.submit>
@can('wholesaler')
<button type="submit" name="submit" class="btn btn-success mr-0 float-right">@if ($o->exists)Save @else Add @endif</button>
@endcan
</div> </div>
</div> </div>
</form> </form>
@ -211,14 +143,9 @@
</div> </div>
@section('page-scripts') @section('page-scripts')
@css(select2)
@css(simplemde)
@js(select2,autofocus)
@js(simplemde)
<script type="text/javascript"> <script type="text/javascript">
// Get a list of supplier items matching this type to populate model_id // Get a list of supplier items matching this type to populate model_id
function supplier_products(type,destination,selected) { function supplied_products(type,destination,selected) {
destination.prop('disabled',true); destination.prop('disabled',true);
$.ajax({ $.ajax({
@ -226,11 +153,10 @@
dataType: 'json', dataType: 'json',
data: {type: type}, data: {type: type},
cache: false, cache: false,
url: '{{ url('api/a/supplier_products') }}', url: '{{ url('a/supplied_products') }}',
timeout: 2000, timeout: 2000,
error: function(x) { error: function(x) {
// @todo add a spinner // @todo add a spinner
//spinner.toggleClass('d-none').toggleClass('fa-spin');
alert('Failed to submit'); alert('Failed to submit');
}, },
success: function(data) { success: function(data) {
@ -246,28 +172,24 @@
} }
$(document).ready(function() { $(document).ready(function() {
new SimpleMDE({ element: $('.textarea')[0], forceSync: true });
$('#model').on('change',function(item) { $('#model').on('change',function(item) {
if ($(this).val()) { if ($(this).val()) {
$('#supplier_product').show(); $('#supplied_product').show();
supplier_products($(this).val(),$('#model_id')); supplied_products($(this).val(),$('#model_id'));
} else { } else {
$('#supplier_product').hide(); $('#supplied_product').hide();
} }
}); });
$('#model_id').select2(); // After we render the page, hide the supplied_product if this product has no model.
$('.select').select2();
// After we render the page, hide the supplier_product if this product has no model.
// We do this here, because adding d-none to the div results in the select2 input not presenting correctly // We do this here, because adding d-none to the div results in the select2 input not presenting correctly
if (! $('#model').val()) if (! $('#model').val())
$('#supplier_product').hide(); $('#supplied_product').hide();
else else
supplier_products($('#model').val(),$('#model_id'),{{ old('model_id',$o->model_id) }}); supplied_products($('#model').val(),$('#model_id'),{{ old('model_id',$pdo->model_id) }});
// @todo when there is a value in 1 of the two boxes in the row, the toggle incorrectly disables one and enables the other
$('input[type=checkbox]').on('click',function(item) { $('input[type=checkbox]').on('click',function(item) {
var input = $(this).parent().parent().parent().find('input[type="text"]'); var input = $(this).parent().parent().parent().find('input[type="text"]');
input.prop('disabled',(i,v)=>!v); input.prop('disabled',(i,v)=>!v);

View File

@ -1,25 +1,18 @@
<!-- $o=Product::class --> <!-- $o=Product::class -->
@use(App\Models\Product)
<div class="card card-dark"> <div class="card card-dark">
<div class="card-header"> <div class="card-header">
<h1 class="card-title">Product Configuration</h1> <h1 class="card-title">Product Configuration</h1>
</div> </div>
<div class="card-body"> <div class="card-body">
<form class="g-0 needs-validation" method="POST" enctype="multipart/form-data" role="form"> <form method="POST">
@csrf @csrf
<div class="row"> <div class="row">
<div class="col-12 col-sm-9 col-md-6 col-xl-5"> <div class="col-12 col-sm-9 col-md-6 col-xl-5">
@include('adminlte::widget.form_select',[ <x-leenooks::form.select name="product_id" icon="fa-list" label="Product" choose="true" groupby="active" :value="$po?->id ?? ''" :options="Product::get()->sortBy(fn($item)=>($item->active ? '0' : '1').$item->name)->transform(fn($item)=>['id'=>$item->id,'value'=>$item->name,'active'=>$item->active])"/>
'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>
</div> </div>
</form> </form>
@ -27,13 +20,10 @@
</div> </div>
@section('page-scripts') @section('page-scripts')
@css(select2)
@js(select2,autofocus)
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
$('#product_id').on('change',function(item) { $('#product_id').on('change',function(item) {
window.location.href = '{{ url('a/product/details') }}/'+item.target.value; window.location.href = '{{ url('a/product') }}/'+item.target.value;
}); });
}); });
</script> </script>

View File

@ -1,8 +1,8 @@
<!-- $o = Product::class --> <!-- $pdo=Product::class -->
<div class="row"> <div class="row">
@if(count($o->services)) @if(count($pdo->services))
<div class="col-7"> <div class="col-7">
<table class="table table-hover w-100" id="table"> <table class="table table-hover" id="table">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
@ -17,7 +17,7 @@
</thead> </thead>
<tbody> <tbody>
@foreach ($o->services as $so) @foreach ($pdo->services as $so)
<tr> <tr>
<td><a href="{{ url('u/service',[$so->id]) }}">{{ $so->sid }}</a></td> <td><a href="{{ url('u/service',[$so->id]) }}">{{ $so->sid }}</a></td>
<td>{{ $so->start_at ? $so->start_at->format('Y-m-d') : '-' }}</td> <td>{{ $so->start_at ? $so->start_at->format('Y-m-d') : '-' }}</td>
@ -38,24 +38,12 @@
@endif @endif
</div> </div>
@pa(datatables,select)
@section('page-scripts') @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>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
@if(count($o->services)) @if(count($pdo->services))
$('#table').DataTable({ $('#table').DataTable({
paging: true, paging: true,
pageLength: 25, pageLength: 25,
@ -66,34 +54,12 @@
autoWidth: false, autoWidth: false,
fixedHeader: true, fixedHeader: true,
order: [[4,'desc'],[0,'asc']], order: [[4,'desc'],[0,'asc']],
rowGroup: {
dataSrc: 4,
},
columnDefs: [ columnDefs: [
{ {
targets: [4], targets: [4],
visible: false, 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 @endif
}); });

View File

@ -3,11 +3,6 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Intuit\Controllers\Webhook; use Intuit\Controllers\Webhook;
use App\Http\Controllers\{AccountingController,
AdminController,
CheckoutController,
ProductController};
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| API Routes | API Routes
@ -19,13 +14,4 @@ use App\Http\Controllers\{AccountingController,
| |
*/ */
// Wholesaler API calls
Route::group(['middleware'=>['auth:api','role:wholesaler']], function() {
Route::get('a/supplier_products',[ProductController::class,'api_supplier_products']);
});
Route::group(['middleware'=>'auth:api'], function() {
Route::any('/intuit/accounting/list',[AccountingController::class,'list']);
});
Route::any('/intuit/webhook',[Webhook::class,'webhook']); Route::any('/intuit/webhook',[Webhook::class,'webhook']);

View File

@ -90,12 +90,14 @@ Route::group(['middleware'=>['auth','role:wholesaler'],'prefix'=>'a'],function()
->where('o','[0-9]+'); ->where('o','[0-9]+');
// Product Setup // Product Setup
Route::match(['get'],'product',[ProductController::class,'home']); Route::view('product','theme.backend.adminlte.product.home',['breadcrumb'=>collect(['x'=>'foobar'])]);
Route::get('product/details/{o?}',[ProductController::class,'details']) Route::view('product/{pdo}','theme.backend.adminlte.product.details',['breadcrumb'=>collect(['Products'=>url('a/product')])])
->where('o','[0-9]+'); ->where('pdo','[0-9]+');
Route::post('product/details/{o?}',[ProductController::class,'details_addedit']) Route::post('product/{o?}',[ProductController::class,'addedit'])
->where('o','[0-9]+'); ->where('o','[0-9]+');
Route::get('supplied_products',[ProductController::class,'api_supplied_products']);
// Supplier Setup // Supplier Setup
Route::get('supplier',[SupplierController::class,'admin_home']); Route::get('supplier',[SupplierController::class,'admin_home']);
Route::get('supplier/cost/new/{o}',[SupplierController::class,'cost_add']); Route::get('supplier/cost/new/{o}',[SupplierController::class,'cost_add']);
@ -127,11 +129,6 @@ Route::group(['middleware'=>['auth','role:wholesaler'],'prefix'=>'a'],function()
// @todo This should probably go to resellers - implement a change audit log first // @todo This should probably go to resellers - implement a change audit log first
Route::post('service/update/{o}',[ServiceController::class,'update']) Route::post('service/update/{o}',[ServiceController::class,'update'])
->where('o','[0-9]+'); ->where('o','[0-9]+');
//@deprecated
// Route::get('service/{o}','AdminHomeController@service');
// Route::post('service/{o}','AdminHomeController@service_update');
// Route::get('accounting/connect','AccountingController@connect');
}); });
// Our Reseller Routes // Our Reseller Routes