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\PaymentRepository;
|
||||
use App\Ninja\Repositories\TaskRepository;
|
||||
use App\Ninja\Repositories\ProposalRepository;
|
||||
use App\Services\PaymentService;
|
||||
use Auth;
|
||||
use Barracuda\ArchiveStream\ZipArchive;
|
||||
@ -36,6 +37,7 @@ class ClientPortalController extends BaseController
|
||||
private $invoiceRepo;
|
||||
private $paymentRepo;
|
||||
private $documentRepo;
|
||||
private $propoosalRepo;
|
||||
|
||||
public function __construct(
|
||||
InvoiceRepository $invoiceRepo,
|
||||
@ -44,7 +46,8 @@ class ClientPortalController extends BaseController
|
||||
DocumentRepository $documentRepo,
|
||||
PaymentService $paymentService,
|
||||
CreditRepository $creditRepo,
|
||||
TaskRepository $taskRepo)
|
||||
TaskRepository $taskRepo,
|
||||
ProposalRepository $propoosalRepo)
|
||||
{
|
||||
$this->invoiceRepo = $invoiceRepo;
|
||||
$this->paymentRepo = $paymentRepo;
|
||||
@ -53,9 +56,36 @@ class ClientPortalController extends BaseController
|
||||
$this->paymentService = $paymentService;
|
||||
$this->creditRepo = $creditRepo;
|
||||
$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)) {
|
||||
return $this->returnError();
|
||||
|
@ -4,6 +4,7 @@ namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Contact;
|
||||
use App\Models\Invitation;
|
||||
use App\Models\ProposalInvitation;
|
||||
use Auth;
|
||||
use Closure;
|
||||
use Session;
|
||||
@ -25,13 +26,14 @@ class Authenticate
|
||||
public function handle($request, Closure $next, $guard = 'user')
|
||||
{
|
||||
$authenticated = Auth::guard($guard)->check();
|
||||
$invitationKey = $request->invitation_key ?: $request->proposal_invitation_key;
|
||||
|
||||
if ($guard == 'client') {
|
||||
if (! empty($request->invitation_key)) {
|
||||
if (! empty($request->invitation_key) || ! empty($request->proposal_invitation_key)) {
|
||||
$contact_key = session('contact_key');
|
||||
if ($contact_key) {
|
||||
$contact = $this->getContact($contact_key);
|
||||
$invitation = $this->getInvitation($request->invitation_key);
|
||||
$invitation = $this->getInvitation($invitationKey, ! empty($request->proposal_invitation_key));
|
||||
|
||||
if (! $invitation) {
|
||||
return response()->view('error', [
|
||||
@ -59,7 +61,7 @@ class Authenticate
|
||||
$contact = false;
|
||||
if ($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;
|
||||
Session::put('contact_key', $contact->contact_key);
|
||||
}
|
||||
@ -108,7 +110,7 @@ class Authenticate
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Model|null|static
|
||||
*/
|
||||
protected function getInvitation($key)
|
||||
protected function getInvitation($key, $isProposal = false)
|
||||
{
|
||||
if (! $key) {
|
||||
return false;
|
||||
@ -118,7 +120,12 @@ class Authenticate
|
||||
list($key) = explode('&', $key);
|
||||
$key = substr($key, 0, RANDOM_KEY_LENGTH);
|
||||
|
||||
if ($isProposal) {
|
||||
$invitation = ProposalInvitation::withTrashed()->where('invitation_key', '=', $key)->first();
|
||||
} else {
|
||||
$invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first();
|
||||
}
|
||||
|
||||
if ($invitation && ! $invitation->is_deleted) {
|
||||
return $invitation;
|
||||
} else {
|
||||
|
@ -58,6 +58,15 @@ class Invitation extends EntityModel
|
||||
{
|
||||
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)
|
||||
|
@ -841,6 +841,14 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
return $this->invoice_status_id >= INVOICE_STATUS_VIEWED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isApproved()
|
||||
{
|
||||
return $this->invoice_status_id >= INVOICE_STATUS_APPROVED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
@ -106,13 +106,4 @@ trait Inviteable
|
||||
$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',
|
||||
function ($model) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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('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('contact_id')->references('id')->on('contacts')->onDelete('cascade');
|
||||
|
@ -794,7 +794,7 @@ $LANG = array(
|
||||
'default_invoice_footer' => 'Default Invoice Footer',
|
||||
'quote_footer' => 'Quote Footer',
|
||||
'free' => 'Free',
|
||||
'quote_is_approved' => 'The quote has been approved',
|
||||
'quote_is_approved' => 'Successfully approved',
|
||||
'apply_credit' => 'Apply Credit',
|
||||
'system_settings' => 'System Settings',
|
||||
'archive_token' => 'Archive Token',
|
||||
@ -2725,6 +2725,7 @@ $LANG = array(
|
||||
'delete_status' => 'Delete Status',
|
||||
'standard' => 'Standard',
|
||||
'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
|
||||
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::put('sign/{invitation_key}', 'ClientPortalController@sign');
|
||||
Route::get('view', 'HomeController@viewLogo');
|
||||
|
Loading…
Reference in New Issue
Block a user