mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-09 12:42:36 +01:00
Proposals
This commit is contained in:
parent
36489b936b
commit
4502cf2531
@ -15,6 +15,7 @@ use App\Ninja\Repositories\DocumentRepository;
|
|||||||
use App\Ninja\Repositories\InvoiceRepository;
|
use App\Ninja\Repositories\InvoiceRepository;
|
||||||
use App\Ninja\Repositories\PaymentRepository;
|
use App\Ninja\Repositories\PaymentRepository;
|
||||||
use App\Ninja\Repositories\TaskRepository;
|
use App\Ninja\Repositories\TaskRepository;
|
||||||
|
use App\Ninja\Repositories\ProposalRepository;
|
||||||
use App\Services\PaymentService;
|
use App\Services\PaymentService;
|
||||||
use Auth;
|
use Auth;
|
||||||
use Barracuda\ArchiveStream\ZipArchive;
|
use Barracuda\ArchiveStream\ZipArchive;
|
||||||
@ -36,6 +37,7 @@ class ClientPortalController extends BaseController
|
|||||||
private $invoiceRepo;
|
private $invoiceRepo;
|
||||||
private $paymentRepo;
|
private $paymentRepo;
|
||||||
private $documentRepo;
|
private $documentRepo;
|
||||||
|
private $propoosalRepo;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
InvoiceRepository $invoiceRepo,
|
InvoiceRepository $invoiceRepo,
|
||||||
@ -44,7 +46,8 @@ class ClientPortalController extends BaseController
|
|||||||
DocumentRepository $documentRepo,
|
DocumentRepository $documentRepo,
|
||||||
PaymentService $paymentService,
|
PaymentService $paymentService,
|
||||||
CreditRepository $creditRepo,
|
CreditRepository $creditRepo,
|
||||||
TaskRepository $taskRepo)
|
TaskRepository $taskRepo,
|
||||||
|
ProposalRepository $propoosalRepo)
|
||||||
{
|
{
|
||||||
$this->invoiceRepo = $invoiceRepo;
|
$this->invoiceRepo = $invoiceRepo;
|
||||||
$this->paymentRepo = $paymentRepo;
|
$this->paymentRepo = $paymentRepo;
|
||||||
@ -53,9 +56,36 @@ class ClientPortalController extends BaseController
|
|||||||
$this->paymentService = $paymentService;
|
$this->paymentService = $paymentService;
|
||||||
$this->creditRepo = $creditRepo;
|
$this->creditRepo = $creditRepo;
|
||||||
$this->taskRepo = $taskRepo;
|
$this->taskRepo = $taskRepo;
|
||||||
|
$this->propoosalRepo = $propoosalRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function view($invitationKey)
|
public function viewProposal($invitationKey)
|
||||||
|
{
|
||||||
|
if (! $invitation = $this->propoosalRepo->findInvitationByKey($invitationKey)) {
|
||||||
|
return $this->returnError(trans('texts.proposal_not_found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $invitation->account;
|
||||||
|
$proposal = $invitation->proposal;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'proposalInvitation' => $invitation,
|
||||||
|
'proposal' => $proposal,
|
||||||
|
'account' => $account,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (request()->raw) {
|
||||||
|
return view('invited.proposal_raw', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['invitation'] = Invitation::whereContactId($invitation->contact_id)
|
||||||
|
->whereInvoiceId($proposal->invoice_id)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
return view('invited.proposal', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewInvoice($invitationKey)
|
||||||
{
|
{
|
||||||
if (! $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
|
if (! $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
|
||||||
return $this->returnError();
|
return $this->returnError();
|
||||||
|
@ -4,6 +4,7 @@ namespace App\Http\Middleware;
|
|||||||
|
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
use App\Models\Invitation;
|
use App\Models\Invitation;
|
||||||
|
use App\Models\ProposalInvitation;
|
||||||
use Auth;
|
use Auth;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Session;
|
use Session;
|
||||||
@ -25,13 +26,14 @@ class Authenticate
|
|||||||
public function handle($request, Closure $next, $guard = 'user')
|
public function handle($request, Closure $next, $guard = 'user')
|
||||||
{
|
{
|
||||||
$authenticated = Auth::guard($guard)->check();
|
$authenticated = Auth::guard($guard)->check();
|
||||||
|
$invitationKey = $request->invitation_key ?: $request->proposal_invitation_key;
|
||||||
|
|
||||||
if ($guard == 'client') {
|
if ($guard == 'client') {
|
||||||
if (! empty($request->invitation_key)) {
|
if (! empty($request->invitation_key) || ! empty($request->proposal_invitation_key)) {
|
||||||
$contact_key = session('contact_key');
|
$contact_key = session('contact_key');
|
||||||
if ($contact_key) {
|
if ($contact_key) {
|
||||||
$contact = $this->getContact($contact_key);
|
$contact = $this->getContact($contact_key);
|
||||||
$invitation = $this->getInvitation($request->invitation_key);
|
$invitation = $this->getInvitation($invitationKey, ! empty($request->proposal_invitation_key));
|
||||||
|
|
||||||
if (! $invitation) {
|
if (! $invitation) {
|
||||||
return response()->view('error', [
|
return response()->view('error', [
|
||||||
@ -59,7 +61,7 @@ class Authenticate
|
|||||||
$contact = false;
|
$contact = false;
|
||||||
if ($contact_key) {
|
if ($contact_key) {
|
||||||
$contact = $this->getContact($contact_key);
|
$contact = $this->getContact($contact_key);
|
||||||
} elseif ($invitation = $this->getInvitation($request->invitation_key)) {
|
} elseif ($invitation = $this->getInvitation($invitationKey, ! empty($request->proposal_invitation_key))) {
|
||||||
$contact = $invitation->contact;
|
$contact = $invitation->contact;
|
||||||
Session::put('contact_key', $contact->contact_key);
|
Session::put('contact_key', $contact->contact_key);
|
||||||
}
|
}
|
||||||
@ -108,7 +110,7 @@ class Authenticate
|
|||||||
*
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Model|null|static
|
* @return \Illuminate\Database\Eloquent\Model|null|static
|
||||||
*/
|
*/
|
||||||
protected function getInvitation($key)
|
protected function getInvitation($key, $isProposal = false)
|
||||||
{
|
{
|
||||||
if (! $key) {
|
if (! $key) {
|
||||||
return false;
|
return false;
|
||||||
@ -118,7 +120,12 @@ class Authenticate
|
|||||||
list($key) = explode('&', $key);
|
list($key) = explode('&', $key);
|
||||||
$key = substr($key, 0, RANDOM_KEY_LENGTH);
|
$key = substr($key, 0, RANDOM_KEY_LENGTH);
|
||||||
|
|
||||||
$invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first();
|
if ($isProposal) {
|
||||||
|
$invitation = ProposalInvitation::withTrashed()->where('invitation_key', '=', $key)->first();
|
||||||
|
} else {
|
||||||
|
$invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first();
|
||||||
|
}
|
||||||
|
|
||||||
if ($invitation && ! $invitation->is_deleted) {
|
if ($invitation && ! $invitation->is_deleted) {
|
||||||
return $invitation;
|
return $invitation;
|
||||||
} else {
|
} else {
|
||||||
|
@ -58,6 +58,15 @@ class Invitation extends EntityModel
|
|||||||
{
|
{
|
||||||
return $this->belongsTo('App\Models\Account');
|
return $this->belongsTo('App\Models\Account');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function signatureDiv()
|
||||||
|
{
|
||||||
|
if (! $this->signature_base64) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('<img src="data:image/svg+xml;base64,%s"></img><p/>%s: %s', $this->signature_base64, trans('texts.signed'), Utils::fromSqlDateTime($this->signature_date));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Invitation::creating(function ($invitation)
|
Invitation::creating(function ($invitation)
|
||||||
|
@ -841,6 +841,14 @@ class Invoice extends EntityModel implements BalanceAffecting
|
|||||||
return $this->invoice_status_id >= INVOICE_STATUS_VIEWED;
|
return $this->invoice_status_id >= INVOICE_STATUS_VIEWED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isApproved()
|
||||||
|
{
|
||||||
|
return $this->invoice_status_id >= INVOICE_STATUS_APPROVED;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
|
@ -106,13 +106,4 @@ trait Inviteable
|
|||||||
$client->markLoggedIn();
|
$client->markLoggedIn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function signatureDiv()
|
|
||||||
{
|
|
||||||
if (! $this->signature_base64) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf('<img src="data:image/svg+xml;base64,%s"></img><p/>%s: %s', $this->signature_base64, trans('texts.signed'), Utils::fromSqlDateTime($this->signature_date));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -28,10 +28,10 @@ class ProposalDatatable extends EntityDatatable
|
|||||||
'template',
|
'template',
|
||||||
function ($model) {
|
function ($model) {
|
||||||
if (! Auth::user()->can('viewByOwner', [ENTITY_PROPOSAL_TEMPLATE, $model->template_user_id])) {
|
if (! Auth::user()->can('viewByOwner', [ENTITY_PROPOSAL_TEMPLATE, $model->template_user_id])) {
|
||||||
return $model->template;
|
return $model->template ?: ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
return link_to("proposals/templates/{$model->template_public_id}/edit", $model->template)->toHtml();
|
return link_to("proposals/templates/{$model->template_public_id}/edit", $model->template ?: ' ')->toHtml();
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
@ -120,4 +120,39 @@ class ProposalRepository extends BaseRepository
|
|||||||
|
|
||||||
return $proposal;
|
return $proposal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $invitationKey
|
||||||
|
*
|
||||||
|
* @return Invitation|bool
|
||||||
|
*/
|
||||||
|
public function findInvitationByKey($invitationKey)
|
||||||
|
{
|
||||||
|
// check for extra params at end of value (from website feature)
|
||||||
|
list($invitationKey) = explode('&', $invitationKey);
|
||||||
|
$invitationKey = substr($invitationKey, 0, RANDOM_KEY_LENGTH);
|
||||||
|
|
||||||
|
/** @var \App\Models\Invitation $invitation */
|
||||||
|
$invitation = ProposalInvitation::where('invitation_key', '=', $invitationKey)->first();
|
||||||
|
if (! $invitation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$proposal = $invitation->proposal;
|
||||||
|
if (! $proposal || $proposal->is_deleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice = $proposal->invoice;
|
||||||
|
if (! $invoice || $invoice->is_deleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = $invoice->client;
|
||||||
|
if (! $client || $client->is_deleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $invitation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,6 +116,9 @@ class AddSubscriptionFormat extends Migration
|
|||||||
|
|
||||||
$table->timestamp('sent_date')->nullable();
|
$table->timestamp('sent_date')->nullable();
|
||||||
$table->timestamp('viewed_date')->nullable();
|
$table->timestamp('viewed_date')->nullable();
|
||||||
|
$table->timestamp('opened_date')->nullable();
|
||||||
|
$table->string('message_id')->nullable();
|
||||||
|
$table->text('email_error')->nullable();
|
||||||
|
|
||||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||||
$table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade');
|
$table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade');
|
||||||
|
@ -794,7 +794,7 @@ $LANG = array(
|
|||||||
'default_invoice_footer' => 'Default Invoice Footer',
|
'default_invoice_footer' => 'Default Invoice Footer',
|
||||||
'quote_footer' => 'Quote Footer',
|
'quote_footer' => 'Quote Footer',
|
||||||
'free' => 'Free',
|
'free' => 'Free',
|
||||||
'quote_is_approved' => 'The quote has been approved',
|
'quote_is_approved' => 'Successfully approved',
|
||||||
'apply_credit' => 'Apply Credit',
|
'apply_credit' => 'Apply Credit',
|
||||||
'system_settings' => 'System Settings',
|
'system_settings' => 'System Settings',
|
||||||
'archive_token' => 'Archive Token',
|
'archive_token' => 'Archive Token',
|
||||||
@ -2725,6 +2725,7 @@ $LANG = array(
|
|||||||
'delete_status' => 'Delete Status',
|
'delete_status' => 'Delete Status',
|
||||||
'standard' => 'Standard',
|
'standard' => 'Standard',
|
||||||
'icon' => 'Icon',
|
'icon' => 'Icon',
|
||||||
|
'proposal_not_found' => 'The requested proposal is not available',
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
38
resources/views/invited/proposal.blade.php
Normal file
38
resources/views/invited/proposal.blade.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
@extends('public.header')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="container" style="padding: 20px;">
|
||||||
|
<div class="pull-right">
|
||||||
|
@if (! $proposal->invoice->isApproved())
|
||||||
|
{!! Button::success(trans('texts.approve'))->withAttributes(['id' => 'approveButton', 'onclick' => 'onApproveClick()'])->large() !!}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="clearfix"></div><br/>
|
||||||
|
<iframe src="{{ url('/proposal/' . $proposalInvitation->invitation_key . '?raw=true') }}" scrolling="no" onload="resizeIframe(this)"
|
||||||
|
frameborder="0" width="100%" height="1000px" style="background-color:white; border: solid 1px #DDD;"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function onApproveClick() {
|
||||||
|
@if ($account->requiresAuthorization($proposal->invoice))
|
||||||
|
window.pendingPaymentFunction = approveQuote;
|
||||||
|
showAuthorizationModal();
|
||||||
|
@else
|
||||||
|
approveQuote();
|
||||||
|
@endif
|
||||||
|
}
|
||||||
|
|
||||||
|
function approveQuote() {
|
||||||
|
$('#approveButton').prop('disabled', true);
|
||||||
|
location.href = "{{ url('/approve/' . $invitation->invitation_key) }}";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeIframe(obj) {
|
||||||
|
obj.style.height = obj.contentWindow.document.body.scrollHeight + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@stop
|
10
resources/views/invited/proposal_raw.blade.php
Normal file
10
resources/views/invited/proposal_raw.blade.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{App::getLocale()}}">
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
{!! $proposal->css !!}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{!! $proposal->html !!}
|
||||||
|
</body>
|
@ -15,7 +15,8 @@ Route::post('/get_started', 'AccountController@getStarted');
|
|||||||
|
|
||||||
// Client visible pages
|
// Client visible pages
|
||||||
Route::group(['middleware' => ['lookup:contact', 'auth:client']], function () {
|
Route::group(['middleware' => ['lookup:contact', 'auth:client']], function () {
|
||||||
Route::get('view/{invitation_key}', 'ClientPortalController@view');
|
Route::get('view/{invitation_key}', 'ClientPortalController@viewInvoice');
|
||||||
|
Route::get('proposal/{proposal_invitation_key}', 'ClientPortalController@viewProposal');
|
||||||
Route::get('download/{invitation_key}', 'ClientPortalController@download');
|
Route::get('download/{invitation_key}', 'ClientPortalController@download');
|
||||||
Route::put('sign/{invitation_key}', 'ClientPortalController@sign');
|
Route::put('sign/{invitation_key}', 'ClientPortalController@sign');
|
||||||
Route::get('view', 'HomeController@viewLogo');
|
Route::get('view', 'HomeController@viewLogo');
|
||||||
|
Loading…
Reference in New Issue
Block a user