From 4502cf2531d897cae807ad37fc0e0f6230d43871 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 8 Feb 2018 09:39:19 +0200 Subject: [PATCH] Proposals --- .../Controllers/ClientPortalController.php | 34 ++++++++++++++++- app/Http/Middleware/Authenticate.php | 17 ++++++--- app/Models/Invitation.php | 9 +++++ app/Models/Invoice.php | 8 ++++ app/Models/Traits/Inviteable.php | 9 ----- app/Ninja/Datatables/ProposalDatatable.php | 4 +- app/Ninja/Repositories/ProposalRepository.php | 35 +++++++++++++++++ ...8_01_10_073825_add_subscription_format.php | 3 ++ resources/lang/en/texts.php | 3 +- resources/views/invited/proposal.blade.php | 38 +++++++++++++++++++ .../views/invited/proposal_raw.blade.php | 10 +++++ routes/web.php | 3 +- 12 files changed, 153 insertions(+), 20 deletions(-) create mode 100644 resources/views/invited/proposal.blade.php create mode 100644 resources/views/invited/proposal_raw.blade.php diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index 353c2dcf05d0..3bfd6e01e13c 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -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(); diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index c233ac7e770d..d9412a90126c 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -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 { diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index d9fc6164b5c1..8fa8a23b73c0 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -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('

%s: %s', $this->signature_base64, trans('texts.signed'), Utils::fromSqlDateTime($this->signature_date)); + } } Invitation::creating(function ($invitation) diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 90d58f749b53..7856c8cc923c 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -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 */ diff --git a/app/Models/Traits/Inviteable.php b/app/Models/Traits/Inviteable.php index 825755a4936d..28344c4338da 100644 --- a/app/Models/Traits/Inviteable.php +++ b/app/Models/Traits/Inviteable.php @@ -106,13 +106,4 @@ trait Inviteable $client->markLoggedIn(); } } - - public function signatureDiv() - { - if (! $this->signature_base64) { - return false; - } - - return sprintf('

%s: %s', $this->signature_base64, trans('texts.signed'), Utils::fromSqlDateTime($this->signature_date)); - } } diff --git a/app/Ninja/Datatables/ProposalDatatable.php b/app/Ninja/Datatables/ProposalDatatable.php index 7a8b9f7fd651..00f2b18bacc1 100644 --- a/app/Ninja/Datatables/ProposalDatatable.php +++ b/app/Ninja/Datatables/ProposalDatatable.php @@ -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(); }, ], [ diff --git a/app/Ninja/Repositories/ProposalRepository.php b/app/Ninja/Repositories/ProposalRepository.php index 416a2f271c3a..3d3b18a6ab98 100644 --- a/app/Ninja/Repositories/ProposalRepository.php +++ b/app/Ninja/Repositories/ProposalRepository.php @@ -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; + } } diff --git a/database/migrations/2018_01_10_073825_add_subscription_format.php b/database/migrations/2018_01_10_073825_add_subscription_format.php index e873142d9cd3..7404894f9a3d 100644 --- a/database/migrations/2018_01_10_073825_add_subscription_format.php +++ b/database/migrations/2018_01_10_073825_add_subscription_format.php @@ -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'); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 0f7a3837a4d5..5ed1757e0f7d 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -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', ); diff --git a/resources/views/invited/proposal.blade.php b/resources/views/invited/proposal.blade.php new file mode 100644 index 000000000000..4bd3f1bc709a --- /dev/null +++ b/resources/views/invited/proposal.blade.php @@ -0,0 +1,38 @@ +@extends('public.header') + +@section('content') + +

+
+ @if (! $proposal->invoice->isApproved()) + {!! Button::success(trans('texts.approve'))->withAttributes(['id' => 'approveButton', 'onclick' => 'onApproveClick()'])->large() !!} + @endif +
+

+ +
+ + + +@stop diff --git a/resources/views/invited/proposal_raw.blade.php b/resources/views/invited/proposal_raw.blade.php new file mode 100644 index 000000000000..4010125338ac --- /dev/null +++ b/resources/views/invited/proposal_raw.blade.php @@ -0,0 +1,10 @@ + + + + + + + {!! $proposal->html !!} + diff --git a/routes/web.php b/routes/web.php index beb311ab6ea3..d05eb834d5ef 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');