1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-18 23:42:25 +02:00

Proposals

This commit is contained in:
Hillel Coren 2018-02-08 09:39:19 +02:00
parent 36489b936b
commit 4502cf2531
12 changed files with 153 additions and 20 deletions

View File

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

View File

@ -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);
$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) {
return $invitation;
} else {

View File

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

View File

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

View File

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

View File

@ -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();
},
],
[

View File

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

View File

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

View File

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

View 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

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="{{App::getLocale()}}">
<head>
<style>
{!! $proposal->css !!}
</style>
</head>
<body>
{!! $proposal->html !!}
</body>

View File

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