mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Working on proposals
This commit is contained in:
parent
63fcac391f
commit
7c3cbc3a7e
@ -42,6 +42,10 @@ if (! defined('APP_NAME')) {
|
||||
define('ENTITY_RECURRING_EXPENSE', 'recurring_expense');
|
||||
define('ENTITY_CUSTOMER', 'customer');
|
||||
define('ENTITY_SUBSCRIPTION', 'subscription');
|
||||
define('ENTITY_PROPOSAL', 'proposal');
|
||||
define('ENTITY_PROPOSAL_TEMPLATE', 'proposal_template');
|
||||
define('ENTITY_PROPOSAL_SNIPPET', 'proposal_snippet');
|
||||
define('ENTITY_PROPOSAL_CATEGORY', 'proposal_category');
|
||||
|
||||
define('INVOICE_TYPE_STANDARD', 1);
|
||||
define('INVOICE_TYPE_QUOTE', 2);
|
||||
|
144
app/Http/Controllers/ProposalController.php
Normal file
144
app/Http/Controllers/ProposalController.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\GenerateProposalChartData;
|
||||
use App\Http\Requests\CreateProposalRequest;
|
||||
use App\Http\Requests\ProposalRequest;
|
||||
use App\Http\Requests\UpdateProposalRequest;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Proposal;
|
||||
use App\Models\ProposalTemplate;
|
||||
use App\Ninja\Datatables\ProposalDatatable;
|
||||
use App\Ninja\Repositories\ProposalRepository;
|
||||
use App\Services\ProposalService;
|
||||
use Auth;
|
||||
use Input;
|
||||
use Session;
|
||||
use View;
|
||||
|
||||
class ProposalController extends BaseController
|
||||
{
|
||||
protected $proposalRepo;
|
||||
protected $proposalService;
|
||||
protected $entityType = ENTITY_PROPOSAL;
|
||||
|
||||
/*
|
||||
public function __construct(ProposalRepository $proposalRepo, ProposalService $proposalService)
|
||||
{
|
||||
$this->proposalRepo = $proposalRepo;
|
||||
$this->proposalService = $proposalService;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return View::make('list_wrapper', [
|
||||
'entityType' => ENTITY_PROPOSAL,
|
||||
'datatable' => new ProposalDatatable(),
|
||||
'title' => trans('texts.proposals'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getDatatable($expensePublicId = null)
|
||||
{
|
||||
$search = Input::get('sSearch');
|
||||
$userId = Auth::user()->filterId();
|
||||
|
||||
return $this->proposalService->getDatatable($search, $userId);
|
||||
}
|
||||
|
||||
/*
|
||||
public function show(ProposalRequest $request)
|
||||
{
|
||||
$account = auth()->user()->account;
|
||||
$proposal = $request->entity();
|
||||
|
||||
$data = [
|
||||
'account' => auth()->user()->account,
|
||||
'proposal' => $proposal,
|
||||
'title' => trans('texts.view_proposal'),
|
||||
'showBreadcrumbs' => false,
|
||||
];
|
||||
|
||||
return View::make('proposals.show', $data);
|
||||
}
|
||||
*/
|
||||
|
||||
public function create(ProposalRequest $request)
|
||||
{
|
||||
$data = [
|
||||
'account' => auth()->user()->account,
|
||||
'proposal' => null,
|
||||
'method' => 'POST',
|
||||
'url' => 'proposals',
|
||||
'title' => trans('texts.new_proposal'),
|
||||
'quotes' => Invoice::scope()->with('client.contacts')->quotes()->orderBy('id')->get(),
|
||||
'templates' => ProposalTemplate::scope()->orderBy('name')->get(),
|
||||
'quotePublicId' => $request->quote_id,
|
||||
];
|
||||
|
||||
return View::make('proposals.edit', $data);
|
||||
}
|
||||
|
||||
public function edit(ProposalRequest $request)
|
||||
{
|
||||
$proposal = $request->entity();
|
||||
|
||||
$data = [
|
||||
'account' => auth()->user()->account,
|
||||
'proposal' => $proposal,
|
||||
'method' => 'PUT',
|
||||
'url' => 'proposals/' . $proposal->public_id,
|
||||
'title' => trans('texts.edit_proposal'),
|
||||
'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
|
||||
'clientPublicId' => $proposal->client ? $proposal->client->public_id : null,
|
||||
];
|
||||
|
||||
return View::make('proposals.edit', $data);
|
||||
}
|
||||
|
||||
public function store(CreateProposalRequest $request)
|
||||
{
|
||||
$proposal = $this->proposalService->save($request->input());
|
||||
|
||||
Session::flash('message', trans('texts.created_proposal'));
|
||||
|
||||
return redirect()->to($proposal->getRoute());
|
||||
}
|
||||
|
||||
public function update(UpdateProposalRequest $request)
|
||||
{
|
||||
$proposal = $this->proposalService->save($request->input(), $request->entity());
|
||||
|
||||
Session::flash('message', trans('texts.updated_proposal'));
|
||||
|
||||
$action = Input::get('action');
|
||||
if (in_array($action, ['archive', 'delete', 'restore'])) {
|
||||
return self::bulk();
|
||||
}
|
||||
|
||||
return redirect()->to($proposal->getRoute());
|
||||
}
|
||||
|
||||
public function bulk()
|
||||
{
|
||||
$action = Input::get('action');
|
||||
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
|
||||
|
||||
$count = $this->proposalService->bulk($ids, $action);
|
||||
|
||||
if ($count > 0) {
|
||||
$field = $count == 1 ? "{$action}d_proposal" : "{$action}d_proposals";
|
||||
$message = trans("texts.$field", ['count' => $count]);
|
||||
Session::flash('message', $message);
|
||||
}
|
||||
|
||||
return redirect()->to('/proposals');
|
||||
}
|
||||
}
|
8
app/Http/Requests/ProposalRequest.php
Normal file
8
app/Http/Requests/ProposalRequest.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class ProposalRequest extends EntityRequest
|
||||
{
|
||||
protected $entityType = ENTITY_PROPOSAL;
|
||||
}
|
8
app/Http/Requests/ProposalSnippetRequest.php
Normal file
8
app/Http/Requests/ProposalSnippetRequest.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class ProposalSnippetRequest extends EntityRequest
|
||||
{
|
||||
protected $entityType = ENTITY_PROPOSAL_SNIPPET;
|
||||
}
|
8
app/Http/Requests/ProposalTemplateRequest.php
Normal file
8
app/Http/Requests/ProposalTemplateRequest.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class ProposalTemplateRequest extends EntityRequest
|
||||
{
|
||||
protected $entityType = ENTITY_PROPOSAL_TEMPLATE;
|
||||
}
|
@ -321,6 +321,7 @@ class EntityModel extends Eloquent
|
||||
'recurring_expenses' => 'files-o',
|
||||
'credits' => 'credit-card',
|
||||
'quotes' => 'file-text-o',
|
||||
'proposals' => 'tasks',
|
||||
'tasks' => 'clock-o',
|
||||
'expenses' => 'file-image-o',
|
||||
'vendors' => 'building',
|
||||
|
76
app/Models/Proposal.php
Normal file
76
app/Models/Proposal.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
|
||||
/**
|
||||
* Class ExpenseCategory.
|
||||
*/
|
||||
class Proposal extends EntityModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
use PresentableTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
//protected $presenter = 'App\Ninja\Presenters\ProjectPresenter';
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEntityType()
|
||||
{
|
||||
return ENTITY_PROPOSAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRoute()
|
||||
{
|
||||
return "/proposals/{$this->public_id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Account');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function quote()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Invoice')->withTrashed();
|
||||
}
|
||||
|
||||
public function getDisplayName()
|
||||
{
|
||||
return 'TODO';
|
||||
}
|
||||
}
|
||||
|
||||
Proposal::creating(function ($project) {
|
||||
$project->setNullValues();
|
||||
});
|
||||
|
||||
Proposal::updating(function ($project) {
|
||||
$project->setNullValues();
|
||||
});
|
70
app/Models/ProposalCategory.php
Normal file
70
app/Models/ProposalCategory.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
|
||||
/**
|
||||
* Class ExpenseCategory.
|
||||
*/
|
||||
class ProposalCategory extends EntityModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
use PresentableTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
//protected $presenter = 'App\Ninja\Presenters\ProjectPresenter';
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEntityType()
|
||||
{
|
||||
return ENTITY_PROPOSAL_CATEGORY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRoute()
|
||||
{
|
||||
return "/proposal_categories/{$this->public_id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Account');
|
||||
}
|
||||
|
||||
public function getDisplayName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Proposal::creating(function ($project) {
|
||||
$project->setNullValues();
|
||||
});
|
||||
|
||||
Proposal::updating(function ($project) {
|
||||
$project->setNullValues();
|
||||
});
|
||||
*/
|
70
app/Models/ProposalSnippet.php
Normal file
70
app/Models/ProposalSnippet.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
|
||||
/**
|
||||
* Class ExpenseCategory.
|
||||
*/
|
||||
class ProposalSnippet extends EntityModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
use PresentableTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
//protected $presenter = 'App\Ninja\Presenters\ProjectPresenter';
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEntityType()
|
||||
{
|
||||
return ENTITY_PROPOSAL_SNIPPET;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRoute()
|
||||
{
|
||||
return "/proposal_snippets/{$this->public_id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Account');
|
||||
}
|
||||
|
||||
public function getDisplayName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Proposal::creating(function ($project) {
|
||||
$project->setNullValues();
|
||||
});
|
||||
|
||||
Proposal::updating(function ($project) {
|
||||
$project->setNullValues();
|
||||
});
|
||||
*/
|
70
app/Models/ProposalTemplate.php
Normal file
70
app/Models/ProposalTemplate.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
|
||||
/**
|
||||
* Class ExpenseCategory.
|
||||
*/
|
||||
class ProposalTemplate extends EntityModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
use PresentableTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
//protected $presenter = 'App\Ninja\Presenters\ProjectPresenter';
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEntityType()
|
||||
{
|
||||
return ENTITY_PROPOSAL_TEMPLATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRoute()
|
||||
{
|
||||
return "/proposal_templates/{$this->public_id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Account');
|
||||
}
|
||||
|
||||
public function getDisplayName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Proposal::creating(function ($project) {
|
||||
$project->setNullValues();
|
||||
});
|
||||
|
||||
Proposal::updating(function ($project) {
|
||||
$project->setNullValues();
|
||||
});
|
||||
*/
|
40
app/Ninja/Datatables/ProposalCategoryDatatable.php
Normal file
40
app/Ninja/Datatables/ProposalCategoryDatatable.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Datatables;
|
||||
|
||||
use Auth;
|
||||
use URL;
|
||||
use Utils;
|
||||
|
||||
class ProposalCategoryDatatable extends EntityDatatable
|
||||
{
|
||||
public $entityType = ENTITY_PROPOSAL_CATEGORY;
|
||||
public $sortCol = 1;
|
||||
|
||||
public function columns()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name',
|
||||
function ($model) {
|
||||
return $model->name;
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function actions()
|
||||
{
|
||||
return [
|
||||
[
|
||||
trans('texts.edit_category'),
|
||||
function ($model) {
|
||||
return URL::to("proposal_categories/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_CATEGORY, $model->user_id]);
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
64
app/Ninja/Datatables/ProposalDatatable.php
Normal file
64
app/Ninja/Datatables/ProposalDatatable.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Datatables;
|
||||
|
||||
use Auth;
|
||||
use URL;
|
||||
use Utils;
|
||||
|
||||
class ProposalDatatable extends EntityDatatable
|
||||
{
|
||||
public $entityType = ENTITY_PROPOSAL;
|
||||
public $sortCol = 1;
|
||||
|
||||
public function columns()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'quote',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_QUOTE, $model->user_id])) {
|
||||
return $model->quote_number;
|
||||
}
|
||||
|
||||
return link_to("quotes/{$model->quote_public_id}", $model->quote_number)->toHtml();
|
||||
//$str = link_to("quotes/{$model->quote_public_id}", $model->quote_number)->toHtml();
|
||||
//return $this->addNote($str, $model->private_notes);
|
||||
},
|
||||
],
|
||||
[
|
||||
'template',
|
||||
function ($model) {
|
||||
return $model->template_name;
|
||||
},
|
||||
],
|
||||
[
|
||||
'created',
|
||||
function ($model) {
|
||||
return Utils::fromSqlDate($model->created_at);
|
||||
},
|
||||
],
|
||||
[
|
||||
'valid_until',
|
||||
function ($model) {
|
||||
return Utils::fromSqlDate($model->due_date);
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function actions()
|
||||
{
|
||||
return [
|
||||
[
|
||||
trans('texts.edit_proposal'),
|
||||
function ($model) {
|
||||
return URL::to("proposals/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PROPOSAL, $model->user_id]);
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
46
app/Ninja/Datatables/ProposalSnippetDatatable.php
Normal file
46
app/Ninja/Datatables/ProposalSnippetDatatable.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Datatables;
|
||||
|
||||
use Auth;
|
||||
use URL;
|
||||
use Utils;
|
||||
|
||||
class ProposalSnippetDatatable extends EntityDatatable
|
||||
{
|
||||
public $entityType = ENTITY_PROPOSAL_SNIPPET;
|
||||
public $sortCol = 1;
|
||||
|
||||
public function columns()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_SNIPPET, $model->user_id])) {
|
||||
return $model->name;
|
||||
}
|
||||
|
||||
return link_to("proposal_snippets/{$model->public_id}", $model->name)->toHtml();
|
||||
//$str = link_to("quotes/{$model->quote_public_id}", $model->quote_number)->toHtml();
|
||||
//return $this->addNote($str, $model->private_notes);
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function actions()
|
||||
{
|
||||
return [
|
||||
[
|
||||
trans('texts.edit_snippet'),
|
||||
function ($model) {
|
||||
return URL::to("proposals_snippets/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_SNIPPET, $model->user_id]);
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
46
app/Ninja/Datatables/ProposalTemplateDatatable.php
Normal file
46
app/Ninja/Datatables/ProposalTemplateDatatable.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Datatables;
|
||||
|
||||
use Auth;
|
||||
use URL;
|
||||
use Utils;
|
||||
|
||||
class ProposalTemplateDatatable extends EntityDatatable
|
||||
{
|
||||
public $entityType = ENTITY_PROPOSAL_TEMPLATE;
|
||||
public $sortCol = 1;
|
||||
|
||||
public function columns()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'quote',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_TEMPLATE, $model->user_id])) {
|
||||
return $model->name;
|
||||
}
|
||||
|
||||
return link_to("proposal_templates/{$model->public_id}", $model->name)->toHtml();
|
||||
//$str = link_to("quotes/{$model->quote_public_id}", $model->quote_number)->toHtml();
|
||||
//return $this->addNote($str, $model->private_notes);
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function actions()
|
||||
{
|
||||
return [
|
||||
[
|
||||
trans('texts.edit_template'),
|
||||
function ($model) {
|
||||
return URL::to("proposal_templates/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_TEMPLATE, $model->user_id]);
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
7
app/Policies/ProposalCategoryPolicy.php
Normal file
7
app/Policies/ProposalCategoryPolicy.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class ProposalCategoryPolicy extends EntityPolicy
|
||||
{
|
||||
}
|
7
app/Policies/ProposalPolicy.php
Normal file
7
app/Policies/ProposalPolicy.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class ProposalPolicy extends EntityPolicy
|
||||
{
|
||||
}
|
7
app/Policies/ProposalSnippetPolicy.php
Normal file
7
app/Policies/ProposalSnippetPolicy.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class ProposalSnippetPolicy extends EntityPolicy
|
||||
{
|
||||
}
|
7
app/Policies/ProposalTemplatePolicy.php
Normal file
7
app/Policies/ProposalTemplatePolicy.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class ProposalTemplatePolicy extends EntityPolicy
|
||||
{
|
||||
}
|
@ -34,6 +34,10 @@ class AuthServiceProvider extends ServiceProvider
|
||||
\App\Models\PaymentTerm::class => \App\Policies\PaymentTermPolicy::class,
|
||||
\App\Models\Project::class => \App\Policies\ProjectPolicy::class,
|
||||
\App\Models\AccountGatewayToken::class => \App\Policies\CustomerPolicy::class,
|
||||
\App\Models\Proposal::class => \App\Policies\Proposal::class,
|
||||
\App\Models\ProposalSnippet::class => \App\Policies\ProposalSnippet::class,
|
||||
\App\Models\ProposalTemplate::class => \App\Policies\ProposalTemplate::class,
|
||||
\App\Models\ProposalCategory::class => \App\Policies\ProposalCategory::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -40,7 +40,8 @@
|
||||
"toastr": "^2.1.3",
|
||||
"jt.timepicker": "jquery-timepicker-jt#^1.11.12",
|
||||
"qrcode.js": "qrcode-js#*",
|
||||
"money.js": "^0.1.3"
|
||||
"money.js": "^0.1.3",
|
||||
"grapesjs": "^0.13.8"
|
||||
},
|
||||
"resolutions": {
|
||||
"jquery": "~1.11"
|
||||
|
@ -338,7 +338,6 @@ class ConfideSetupUsersTable extends Migration
|
||||
$t->timestamp('viewed_date')->nullable();
|
||||
|
||||
$t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
;
|
||||
$t->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade');
|
||||
$t->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
||||
|
||||
|
@ -20,6 +20,83 @@ class AddSubscriptionFormat extends Migration
|
||||
Schema::table('accounts', function ($table) {
|
||||
$table->boolean('ubl_email_attachment')->default(false);
|
||||
});
|
||||
|
||||
Schema::create('proposal_categories', function ($table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('account_id');
|
||||
$table->unsignedInteger('user_id');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->string('name');
|
||||
|
||||
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
|
||||
$table->unsignedInteger('public_id')->index();
|
||||
$table->unique(['account_id', 'public_id']);
|
||||
});
|
||||
|
||||
Schema::create('proposal_snippets', function ($table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('account_id');
|
||||
$table->unsignedInteger('user_id');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unsignedInteger('proposal_category_id');
|
||||
$table->string('name');
|
||||
|
||||
$table->mediumText('html');
|
||||
$table->mediumText('css');
|
||||
|
||||
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
|
||||
$table->unsignedInteger('public_id')->index();
|
||||
$table->unique(['account_id', 'public_id']);
|
||||
});
|
||||
|
||||
Schema::create('proposal_templates', function ($table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('account_id');
|
||||
$table->unsignedInteger('user_id');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->string('name');
|
||||
$table->text('tags');
|
||||
$table->mediumText('html');
|
||||
$table->mediumText('css');
|
||||
|
||||
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
|
||||
$table->unsignedInteger('public_id')->index();
|
||||
$table->unique(['account_id', 'public_id']);
|
||||
});
|
||||
|
||||
Schema::create('proposals', function ($table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('account_id');
|
||||
$table->unsignedInteger('user_id');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unsignedInteger('quote_id')->index();
|
||||
$table->unsignedInteger('temlate_id')->index();
|
||||
$table->mediumText('html');
|
||||
$table->mediumText('css');
|
||||
|
||||
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
$table->foreign('quote_id')->references('id')->on('invoices')->onDelete('cascade');
|
||||
$table->foreign('temlate_id')->references('id')->on('proposal_templates')->onDelete('cascade');
|
||||
|
||||
$table->unsignedInteger('public_id')->index();
|
||||
$table->unique(['account_id', 'public_id']);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -36,5 +113,10 @@ class AddSubscriptionFormat extends Migration
|
||||
Schema::table('accounts', function ($table) {
|
||||
$table->dropColumn('ubl_email_attachment');
|
||||
});
|
||||
|
||||
Schema::dropIfExists('proposals');
|
||||
Schema::dropIfExists('proposal_templates');
|
||||
Schema::dropIfExists('proposal_snippets');
|
||||
Schema::dropIfExists('proposal_categories');
|
||||
}
|
||||
}
|
||||
|
11
gulpfile.js
11
gulpfile.js
@ -67,6 +67,11 @@ elixir(function(mix) {
|
||||
bowerDir + '/bootstrap-daterangepicker/daterangepicker.css'
|
||||
], 'public/css/daterangepicker.css');
|
||||
|
||||
mix.styles([
|
||||
bowerDir + '/grapesjs/dist/css/grapes.min.css',
|
||||
//'grapesjs-preset-newsletter.css',
|
||||
], 'public/css/grapesjs.css');
|
||||
|
||||
mix.styles([
|
||||
bowerDir + '/jt.timepicker/jquery.timepicker.css'
|
||||
], 'public/css/jquery.timepicker.css');
|
||||
@ -103,6 +108,12 @@ elixir(function(mix) {
|
||||
bowerDir + '/bootstrap-daterangepicker/daterangepicker.js'
|
||||
], 'public/js/daterangepicker.min.js');
|
||||
|
||||
mix.scripts([
|
||||
bowerDir + '/grapesjs/dist/grapes.js',
|
||||
'grapesjs-blocks-basic.min.js',
|
||||
'grapesjs-preset-newsletter.min.js',
|
||||
], 'public/js/grapesjs.min.js');
|
||||
|
||||
mix.scripts([
|
||||
bowerDir + '/jt.timepicker/jquery.timepicker.js'
|
||||
], 'public/js/jquery.timepicker.js');
|
||||
|
6
public/css/grapes.css
vendored
Normal file
6
public/css/grapes.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/css/grapes.css.map
Normal file
1
public/css/grapes.css.map
Normal file
File diff suppressed because one or more lines are too long
5
public/css/grapesjs.css
vendored
Normal file
5
public/css/grapesjs.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/css/grapesjs.css.map
Normal file
1
public/css/grapesjs.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
public/fonts/main-fonts.woff
Normal file
BIN
public/fonts/main-fonts.woff
Normal file
Binary file not shown.
47711
public/js/grapes.min.js
vendored
Normal file
47711
public/js/grapes.min.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
public/js/grapes.min.js.map
Normal file
1
public/js/grapes.min.js.map
Normal file
File diff suppressed because one or more lines are too long
29
public/js/grapesjs.min.js
vendored
Normal file
29
public/js/grapesjs.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/js/grapesjs.min.js.map
Normal file
1
public/js/grapesjs.min.js.map
Normal file
File diff suppressed because one or more lines are too long
211
resources/assets/css/grapesjs-preset-newsletter.css
vendored
Normal file
211
resources/assets/css/grapesjs-preset-newsletter.css
vendored
Normal file
@ -0,0 +1,211 @@
|
||||
/* Class names prefixes */
|
||||
/* Colors / Theme */
|
||||
.gjs-clm-tags .gjs-sm-title,
|
||||
.gjs-sm-sector .gjs-sm-title {
|
||||
border-top: none; }
|
||||
|
||||
.gjs-clm-tags .gjs-clm-tag {
|
||||
background-color: #4c9790;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 5px 8px;
|
||||
text-shadow: none; }
|
||||
|
||||
.gjs-field {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
box-shadow: none; }
|
||||
|
||||
.gjs-btnt.gjs-pn-active,
|
||||
.gjs-pn-btn.gjs-pn-active {
|
||||
box-shadow: none; }
|
||||
|
||||
.gjs-pn-btn:hover {
|
||||
color: rgba(255, 255, 255, 0.75); }
|
||||
|
||||
.gjs-btnt.gjs-pn-active,
|
||||
.gjs-color-active,
|
||||
.gjs-pn-btn.gjs-pn-active,
|
||||
.gjs-pn-btn:active,
|
||||
.gjs-block:hover {
|
||||
color: #35d7bb; }
|
||||
|
||||
#gjs-rte-toolbar .gjs-rte-btn,
|
||||
.gjs-btn-prim,
|
||||
.gjs-btnt,
|
||||
.gjs-clm-tags .gjs-sm-composite.gjs-clm-field,
|
||||
.gjs-clm-tags .gjs-sm-field.gjs-sm-composite,
|
||||
.gjs-clm-tags .gjs-sm-stack #gjs-sm-add,
|
||||
.gjs-color-main,
|
||||
.gjs-mdl-dialog,
|
||||
.gjs-off-prv,
|
||||
.gjs-pn-btn,
|
||||
.gjs-pn-panel,
|
||||
.gjs-sm-sector .gjs-sm-composite.gjs-clm-field,
|
||||
.gjs-sm-sector .gjs-sm-field.gjs-sm-composite,
|
||||
.gjs-sm-sector .gjs-sm-stack #gjs-sm-add {
|
||||
color: #a0aabf; }
|
||||
|
||||
#gjs-rte-toolbar,
|
||||
.gjs-bg-main,
|
||||
.gjs-clm-select option,
|
||||
.gjs-clm-tags .gjs-sm-colorp-c,
|
||||
.gjs-editor,
|
||||
.gjs-mdl-dialog,
|
||||
.gjs-nv-item .gjs-nv-title-c,
|
||||
.gjs-off-prv,
|
||||
.gjs-pn-panel,
|
||||
.gjs-block,
|
||||
.gjs-select option,
|
||||
.gjs-sm-sector .gjs-sm-colorp-c,
|
||||
.gjs-sm-select option,
|
||||
.gjs-sm-unit option,
|
||||
.sp-container {
|
||||
background-color: #373d49; }
|
||||
|
||||
.gjs-import-label,
|
||||
.gjs-export-label {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px; }
|
||||
|
||||
.gjs-mdl-dialog .gjs-btn-import {
|
||||
margin-top: 10px; }
|
||||
|
||||
.CodeMirror {
|
||||
border-radius: 3px;
|
||||
height: 450px;
|
||||
font-family: sans-serif, monospace;
|
||||
letter-spacing: 0.3px;
|
||||
font-size: 12px; }
|
||||
|
||||
/* Extra */
|
||||
.gjs-block {
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
margin: 10px 2.5% 5px;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
|
||||
transition: box-shadow, color 0.2s ease 0s; }
|
||||
|
||||
.gjs-block:hover {
|
||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15); }
|
||||
|
||||
#gjs-pn-views-container.gjs-pn-panel {
|
||||
padding: 39px 0 0; }
|
||||
|
||||
#gjs-pn-views.gjs-pn-panel {
|
||||
padding: 0;
|
||||
border: none; }
|
||||
|
||||
#gjs-pn-views .gjs-pn-btn {
|
||||
margin: 0;
|
||||
height: 40px;
|
||||
padding: 10px;
|
||||
width: 25%;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.3); }
|
||||
|
||||
#gjs-pn-views .gjs-pn-active {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border-bottom: 2px solid #35d7bb;
|
||||
border-radius: 0; }
|
||||
|
||||
#gjs-pn-devices-c {
|
||||
padding-left: 30px; }
|
||||
|
||||
#gjs-pn-options {
|
||||
padding-right: 30px; }
|
||||
|
||||
.gjs-sm-composite .gjs-sm-properties {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between; }
|
||||
|
||||
#gjs-sm-border-top-left-radius,
|
||||
#gjs-sm-border-top-right-radius,
|
||||
#gjs-sm-border-bottom-left-radius,
|
||||
#gjs-sm-border-bottom-right-radius,
|
||||
#gjs-sm-margin-top,
|
||||
#gjs-sm-margin-bottom,
|
||||
#gjs-sm-margin-right,
|
||||
#gjs-sm-margin-left,
|
||||
#gjs-sm-padding-top,
|
||||
#gjs-sm-padding-bottom,
|
||||
#gjs-sm-padding-right,
|
||||
#gjs-sm-padding-left {
|
||||
flex: 999 1 60px; }
|
||||
|
||||
#gjs-sm-border-width,
|
||||
#gjs-sm-border-style,
|
||||
#gjs-sm-border-color {
|
||||
flex: 999 1 80px; }
|
||||
|
||||
#gjs-sm-margin-left,
|
||||
#gjs-sm-padding-left {
|
||||
order: 2; }
|
||||
|
||||
#gjs-sm-margin-right,
|
||||
#gjs-sm-padding-right {
|
||||
order: 3; }
|
||||
|
||||
#gjs-sm-margin-bottom,
|
||||
#gjs-sm-padding-bottom {
|
||||
order: 4; }
|
||||
|
||||
.gjs-field-radio {
|
||||
width: 100%; }
|
||||
|
||||
.gjs-field-radio #gjs-sm-input-holder {
|
||||
display: flex; }
|
||||
|
||||
.gjs-radio-item {
|
||||
flex: 1 0 auto;
|
||||
text-align: center; }
|
||||
|
||||
.gjs-sm-sector .gjs-sm-property.gjs-sm-list {
|
||||
width: 50%; }
|
||||
|
||||
.gjs-mdl-content {
|
||||
border-top: none; }
|
||||
|
||||
.gjs-sm-sector .gjs-sm-property .gjs-sm-layer.gjs-sm-active {
|
||||
background-color: rgba(255, 255, 255, 0.09); }
|
||||
|
||||
/*
|
||||
|
||||
#gjs-pn-views-container,
|
||||
#gjs-pn-views{
|
||||
min-width: 270px;
|
||||
}
|
||||
*/
|
||||
.gjs-f-button::before {
|
||||
content: 'B'; }
|
||||
|
||||
.gjs-f-divider::before {
|
||||
content: 'D'; }
|
||||
|
||||
.gjs-mdl-dialog-sm {
|
||||
width: 300px; }
|
||||
|
||||
.gjs-mdl-dialog form .gjs-sm-property {
|
||||
font-size: 12px;
|
||||
margin-bottom: 15px; }
|
||||
|
||||
.gjs-mdl-dialog form .gjs-sm-label {
|
||||
margin-bottom: 5px; }
|
||||
|
||||
#gjs-clm-status-c {
|
||||
display: none; }
|
||||
|
||||
.anim-spin {
|
||||
animation: 0.5s linear 0s normal none infinite running spin; }
|
||||
|
||||
.form-status {
|
||||
float: right;
|
||||
font-size: 14px; }
|
||||
|
||||
.text-danger {
|
||||
color: #f92929; }
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
transform: rotate(360deg); } }
|
2
resources/assets/js/grapesjs-blocks-basic.min.js
vendored
Normal file
2
resources/assets/js/grapesjs-blocks-basic.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
15
resources/assets/js/grapesjs-preset-newsletter.min.js
vendored
Normal file
15
resources/assets/js/grapesjs-preset-newsletter.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -2659,6 +2659,8 @@ $LANG = array(
|
||||
'tax_amount' => 'Tax Amount',
|
||||
'tax_paid' => 'Tax Paid',
|
||||
'none' => 'None',
|
||||
'proposals' => 'Proposals',
|
||||
'new_proposal' => 'New Proposal',
|
||||
|
||||
);
|
||||
|
||||
|
@ -348,6 +348,7 @@
|
||||
'recurring_invoices' => 'recurring',
|
||||
'credits' => false,
|
||||
'quotes' => false,
|
||||
'proposals' => false,
|
||||
'projects' => false,
|
||||
'tasks' => false,
|
||||
'expenses' => false,
|
||||
@ -376,6 +377,7 @@
|
||||
'recurring_invoices',
|
||||
'credits',
|
||||
'quotes',
|
||||
'proposals',
|
||||
'projects',
|
||||
'tasks',
|
||||
'expenses',
|
||||
|
122
resources/views/proposals/edit.blade.php
Normal file
122
resources/views/proposals/edit.blade.php
Normal file
@ -0,0 +1,122 @@
|
||||
@extends('header')
|
||||
|
||||
@section('head')
|
||||
@parent
|
||||
|
||||
<script src="{{ asset('js/grapesjs.min.js') }}?no_cache={{ NINJA_VERSION }}" type="text/javascript"></script>
|
||||
<link href="{{ asset('css/grapesjs.css') }}?no_cache={{ NINJA_VERSION }}" rel="stylesheet" type="text/css"/>
|
||||
|
||||
<style>
|
||||
.gjs-four-color {
|
||||
color: white !important;
|
||||
}
|
||||
.gjs-block.fa {
|
||||
font-size: 4em !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@stop
|
||||
|
||||
@section('content')
|
||||
|
||||
{!! Former::open() !!}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{!! Former::select('quote_id')->addOption('', '')
|
||||
->label(trans('texts.quote'))
|
||||
->addGroupClass('quote-select') !!}
|
||||
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{!! Former::select('template_id')->addOption('', '')
|
||||
->label(trans('texts.template'))
|
||||
->addGroupClass('template-select') !!}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<center class="buttons">
|
||||
{!! Button::normal(trans('texts.cancel'))
|
||||
->appendIcon(Icon::create('remove-circle'))
|
||||
->asLinkTo(HTMLUtils::previousUrl('/proposals')) !!}
|
||||
|
||||
{!! Button::success(trans("texts.save"))
|
||||
->withAttributes(array('id' => 'saveButton', 'onclick' => 'onSaveClick()'))
|
||||
->appendIcon(Icon::create('floppy-disk')) !!}
|
||||
</center>
|
||||
|
||||
{!! Former::close() !!}
|
||||
|
||||
<div id="gjs"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var quotes = {!! $quotes !!};
|
||||
var quoteMap = {};
|
||||
|
||||
var templates = {!! $templates !!};
|
||||
var templateMap = {};
|
||||
|
||||
$(function() {
|
||||
var quoteId = {{ $quotePublicId ?: 0 }};
|
||||
var $quoteSelect = $('select#quote_id');
|
||||
for (var i = 0; i < quotes.length; i++) {
|
||||
var quote = quotes[i];
|
||||
quoteMap[quote.public_id] = quote;
|
||||
$quoteSelect.append(new Option(quote.invoice_number + ' - ' + getClientDisplayName(quote.client), quote.public_id));
|
||||
}
|
||||
@include('partials/entity_combobox', ['entityType' => ENTITY_QUOTE])
|
||||
|
||||
var $proposal_templateSelect = $('select#template_id');
|
||||
for (var i = 0; i < templates.length; i++) {
|
||||
var template = templates[i];
|
||||
templateMap[template.public_id] = template;
|
||||
$templateSelect.append(new Option(template.name, template.public_id));
|
||||
}
|
||||
@include('partials/entity_combobox', ['entityType' => ENTITY_PROPOSAL_TEMPLATE])
|
||||
|
||||
var editor = grapesjs.init({
|
||||
container : '#gjs',
|
||||
components: '',
|
||||
style: '',
|
||||
showDevices: false,
|
||||
plugins: ['gjs-preset-newsletter'],
|
||||
//plugins: ['gjs-blocks-basic'],
|
||||
storageManager: {type: 'none'},
|
||||
panels: {
|
||||
Xdefaults : [{
|
||||
id : 'commands',
|
||||
buttons : [{
|
||||
id : 'smile',
|
||||
className : 'fa fa-smile-o',
|
||||
attributes : { title: 'Smile' }
|
||||
}],
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
var blockManager = editor.BlockManager;
|
||||
blockManager.add('h1-block', {
|
||||
label: 'Heading',
|
||||
category: 'Basic',
|
||||
content: '<h1>Put your title here</h1>',
|
||||
attributes: {
|
||||
title: 'Insert h1 block',
|
||||
class:'fa fa-smile-o'
|
||||
}
|
||||
});
|
||||
*/
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@stop
|
@ -205,6 +205,18 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
|
||||
Route::get('api/quotes/{client_id?}', 'QuoteController@getDatatable');
|
||||
Route::post('quotes/bulk', 'QuoteController@bulk');
|
||||
|
||||
Route::get('proposals/create/{quote_id?}', 'ProposalController@create');
|
||||
Route::resource('proposals', 'ProposalController');
|
||||
|
||||
Route::get('proposal_templates/create', 'ProposalTemplateController@create');
|
||||
Route::resource('proposal_templates', 'ProposalTemplateController');
|
||||
|
||||
Route::get('proposal_snippets/create', 'ProposalSnippetController@create');
|
||||
Route::resource('proposal_snippets', 'ProposalSnippetController');
|
||||
|
||||
Route::get('proposal_categories/create', 'ProposalCategoryController@create');
|
||||
Route::resource('proposal_categories', 'ProposalController');
|
||||
|
||||
Route::resource('payments', 'PaymentController');
|
||||
Route::get('payments/create/{client_id?}/{invoice_id?}', 'PaymentController@create');
|
||||
Route::get('api/payments/{client_id?}', 'PaymentController@getDatatable');
|
||||
|
Loading…
x
Reference in New Issue
Block a user