1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-19 16:01:34 +02:00

Working on proposals

This commit is contained in:
Hillel Coren 2018-01-30 20:58:55 +02:00
parent 63fcac391f
commit 7c3cbc3a7e
39 changed files with 48903 additions and 2 deletions

View File

@ -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);

View 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');
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class ProposalRequest extends EntityRequest
{
protected $entityType = ENTITY_PROPOSAL;
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class ProposalSnippetRequest extends EntityRequest
{
protected $entityType = ENTITY_PROPOSAL_SNIPPET;
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class ProposalTemplateRequest extends EntityRequest
{
protected $entityType = ENTITY_PROPOSAL_TEMPLATE;
}

View File

@ -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
View 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();
});

View 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();
});
*/

View 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();
});
*/

View 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();
});
*/

View 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]);
},
],
];
}
}

View 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]);
},
],
];
}
}

View 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]);
},
],
];
}
}

View 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]);
},
],
];
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Policies;
class ProposalCategoryPolicy extends EntityPolicy
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Policies;
class ProposalPolicy extends EntityPolicy
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Policies;
class ProposalSnippetPolicy extends EntityPolicy
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Policies;
class ProposalTemplatePolicy extends EntityPolicy
{
}

View File

@ -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,
];
/**

View File

@ -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"

View File

@ -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');

View File

@ -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');
}
}

View File

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5
public/css/grapesjs.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

47711
public/js/grapes.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

29
public/js/grapesjs.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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); } }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2659,6 +2659,8 @@ $LANG = array(
'tax_amount' => 'Tax Amount',
'tax_paid' => 'Tax Paid',
'none' => 'None',
'proposals' => 'Proposals',
'new_proposal' => 'New Proposal',
);

View File

@ -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',

View 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

View File

@ -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');