Adds drawn signature to asset acceptance (#2846)

* Adds digital signature to asset acceptance

This is still a little broken - the history is displaying “Deleted user”, since there is no item type listed. Saving the item_type as App\Models\User tries to update accepted on the users table, which obviously doesn’t exist.

* Use asset facade for folks in subdirs

* Possible fix for weird accepted/declined display

* Display signature in modal popup if sigs are required

* Wrap that display file in auth middleware, just to be sure.

It shoudl fail if you’re not authorized since you’re not logged in, but better safe than sorry

* Fixed header section of layout

* Removed extra drop from migration rollback
This commit is contained in:
snipe 2016-10-31 21:00:30 -07:00 committed by GitHub
parent a182d8c924
commit a914dacf8e
18 changed files with 817 additions and 60 deletions

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\Helper;
use Response;
class ActionlogController extends Controller
{
public function displaySig($filename)
{
$file = config('app.private_uploads') . '/signatures/' . $filename;
$filetype = Helper::checkUploadIsImage($file);
$contents = file_get_contents($file);
return Response::make($contents)->header('Content-Type', $filetype);
}
}

View file

@ -356,8 +356,10 @@ class ReportsController extends Controller
$activity_item = '<a href="'.route('view/hardware', $activity->item_id).'">'.e($activity->item->asset_tag).' - '. e($activity->item->showAssetName()).'</a>'; $activity_item = '<a href="'.route('view/hardware', $activity->item_id).'">'.e($activity->item->asset_tag).' - '. e($activity->item->showAssetName()).'</a>';
$item_type = 'asset'; $item_type = 'asset';
} elseif ($activity->item) { } elseif ($activity->item) {
$activity_item = '<a href="'.route('view/'. $activity->itemType(), $activity->item_id).'">'.e($activity->item->name).'</a>'; $activity_item = '<a href="' . route('view/' . $activity->itemType(),
$activity->item_id) . '">' . e($activity->item->name) . '</a>';
$item_type = $activity->itemType(); $item_type = $activity->itemType();
} else { } else {
$activity_item = "unkonwn"; $activity_item = "unkonwn";
$item_type = "null"; $item_type = "null";
@ -378,6 +380,9 @@ class ReportsController extends Controller
} else { } else {
$activity_target = ''; $activity_target = '';
} }
} elseif (($activity->action_type=='accepted') || ($activity->action_type=='declined')) {
$activity_target = '<a href="' . route('view/user', $activity->item->assigneduser->id) . '">' . e($activity->item->assigneduser->fullName()) . '</a>';
} elseif ($activity->action_type=='requested') { } elseif ($activity->action_type=='requested') {
if ($activity->user) { if ($activity->user) {
$activity_target = '<a href="'.route('view/user', $activity->user_id).'">'.$activity->user->fullName().'</a>'; $activity_target = '<a href="'.route('view/user', $activity->user_id).'">'.$activity->user->fullName().'</a>';

View file

@ -349,6 +349,7 @@ class SettingsController extends Controller
$setting->email_domain = e(Input::get('email_domain')); $setting->email_domain = e(Input::get('email_domain'));
$setting->email_format = e(Input::get('email_format')); $setting->email_format = e(Input::get('email_format'));
$setting->username_format = e(Input::get('username_format')); $setting->username_format = e(Input::get('username_format'));
$setting->require_accept_signature = e(Input::get('require_accept_signature'));
$setting->labels_per_page = e(Input::get('labels_per_page')); $setting->labels_per_page = e(Input::get('labels_per_page'));

View file

@ -22,6 +22,7 @@ use Redirect;
use Slack; use Slack;
use Validator; use Validator;
use View; use View;
use Illuminate\Http\Request;
/** /**
* This controller handles all actions related to the ability for users * This controller handles all actions related to the ability for users
@ -294,10 +295,14 @@ class ViewAssetsController extends Controller
//return redirect()->to('account')->with('error', trans('admin/hardware/message.does_not_exist')); //return redirect()->to('account')->with('error', trans('admin/hardware/message.does_not_exist'));
} }
if ($findlog->accepted_id!='') {
return redirect()->to('account/view-assets')->with('error', trans('admin/users/message.error.asset_already_accepted'));
}
$user = Auth::user(); $user = Auth::user();
if ($user->id != $findlog->checkedout_to) {
if ($user->id != $findlog->item->assigned_to) {
return redirect()->to('account/view-assets')->with('error', trans('admin/users/message.error.incorrect_user_accepted')); return redirect()->to('account/view-assets')->with('error', trans('admin/users/message.error.incorrect_user_accepted'));
} }
@ -310,12 +315,12 @@ class ViewAssetsController extends Controller
} elseif (!Company::isCurrentUserHasAccess($item)) { } elseif (!Company::isCurrentUserHasAccess($item)) {
return redirect()->route('requestable-assets')->with('error', trans('general.insufficient_permissions')); return redirect()->route('requestable-assets')->with('error', trans('general.insufficient_permissions'));
} else { } else {
return View::make('account/accept-asset', compact('item'))->with('findlog', $findlog); return View::make('account/accept-asset', compact('item'))->with('findlog', $findlog)->with('item',$item);
} }
} }
// Save the acceptance // Save the acceptance
public function postAcceptAsset($logID = null) public function postAcceptAsset(Request $request, $logID = null)
{ {
// Check if the asset exists // Check if the asset exists
@ -331,15 +336,25 @@ class ViewAssetsController extends Controller
} }
if (!Input::has('asset_acceptance')) { if (!Input::has('asset_acceptance')) {
return redirect()->to('account/view-assets')->with('error', trans('admin/users/message.error.accept_or_decline')); return redirect()->back()->with('error', trans('admin/users/message.error.accept_or_decline'));
} }
$user = Auth::user(); $user = Auth::user();
if ($user->id != $findlog->checkedout_to) { if ($user->id != $findlog->item->assigned_to) {
return redirect()->to('account/view-assets')->with('error', trans('admin/users/message.error.incorrect_user_accepted')); return redirect()->to('account/view-assets')->with('error', trans('admin/users/message.error.incorrect_user_accepted'));
} }
if ($request->has('signature_output')) {
$path = config('app.private_uploads').'/signatures';
$sig_filename = "siglog-".$findlog->id.'-'.date('Y-m-d-his').".png";
$data_uri = e($request->get('signature_output'));
$encoded_image = explode(",", $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
file_put_contents($path."/".$sig_filename, $decoded_image);
}
$logaction = new Actionlog(); $logaction = new Actionlog();
if (Input::get('asset_acceptance')=='accepted') { if (Input::get('asset_acceptance')=='accepted') {
@ -353,6 +368,7 @@ class ViewAssetsController extends Controller
} }
$logaction->item_id = $findlog->item_id; $logaction->item_id = $findlog->item_id;
$logaction->item_type = $findlog->item_type; $logaction->item_type = $findlog->item_type;
// Asset // Asset
if (($findlog->item_id!='') && ($findlog->item_type==Asset::class)) { if (($findlog->item_id!='') && ($findlog->item_type==Asset::class)) {
if (Input::get('asset_acceptance')!='accepted') { if (Input::get('asset_acceptance')!='accepted') {
@ -361,19 +377,23 @@ class ViewAssetsController extends Controller
->update(array('assigned_to' => null)); ->update(array('assigned_to' => null));
} }
} }
$logaction->target_id = $findlog->target_id;
$logaction->target_id = $findlog->target_id;
$logaction->note = e(Input::get('note')); $logaction->note = e(Input::get('note'));
$logaction->user_id = $user->id; $logaction->updated_at = date("Y-m-d H:i:s");
$logaction->accepted_at = date("Y-m-d H:i:s");
if ($sig_filename) {
$logaction->accept_signature = $sig_filename;
}
$log = $logaction->logaction($logaction_msg); $log = $logaction->logaction($logaction_msg);
$update_checkout = DB::table('action_logs') $update_checkout = DB::table('action_logs')
->where('id', $findlog->id) ->where('id', $findlog->id)
->update(array('accepted_id' => $logaction->id)); ->update(array('accepted_id' => $logaction->id));
$affected_asset=$logaction->assetlog; $affected_asset = $logaction->item;
$affected_asset->accepted=$accepted; $affected_asset->accepted = $accepted;
$affected_asset->save(); $affected_asset->save();
if ($update_checkout) { if ($update_checkout) {

View file

@ -379,6 +379,30 @@ Route::group(
} }
); );
/*
|--------------------------------------------------------------------------
| Log Routes
|--------------------------------------------------------------------------
|
| Register all the admin routes.
|
*/
Route::group(['middleware' => 'auth'], function () {
Route::get(
'display-sig/{filename}',
[
'as' => 'log.signature.view',
'middleware' => 'authorize:assets.view',
'uses' => 'ActionlogController@displaySig' ]
);
});
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Admin Routes | Admin Routes

View file

@ -4,8 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Response;
use Illuminate\Support\Facades\Log;
/** /**
* Model for the Actionlog (the table that keeps a historical log of * Model for the Actionlog (the table that keeps a historical log of
@ -91,15 +90,14 @@ class Actionlog extends Model
/** /**
* Check if the file exists, and if it does, force a download * Check if the file exists, and if it does, force a download
**/ **/
public function get_src($type = 'assets') public function get_src($type = 'assets', $fieldname = 'filename')
{ {
$file = config('app.private_uploads') . '/' . $type . '/' . $this->{$fieldname};
$file = config('app.private_uploads') . '/' . $type . '/' . $this->filename;
return $file; return $file;
} }
/** /**
* Get the parent category name * Get the parent category name
*/ */

View file

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSignatureToAcceptance extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('settings', function ($table) {
$table->boolean('require_accept_signature')->default(0);
});
Schema::table('action_logs', function ($table) {
$table->string('accept_signature', 100)->nullable()->default(null);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('settings', function ($table) {
$table->dropColumn('require_accept_signature');
});
Schema::table('action_logs', function ($table) {
$table->dropColumn('accept_signature');
});
}
}

View file

@ -0,0 +1,136 @@
#signature-pad {
padding-top: 250px;
margin: auto;
}
.m-signature-pad {
position: relative;
font-size: 10px;
width: 100%;
height: 300px;
border: 1px solid #e8e8e8;
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.27), 0 0 40px rgba(0, 0, 0, 0.08) inset;
border-radius: 4px;
}
.m-signature-pad:before, .m-signature-pad:after {
position: absolute;
z-index: -1;
content: "";
width: 40%;
height: 10px;
left: 20px;
bottom: 10px;
background: transparent;
-webkit-transform: skew(-3deg) rotate(-3deg);
-moz-transform: skew(-3deg) rotate(-3deg);
-ms-transform: skew(-3deg) rotate(-3deg);
-o-transform: skew(-3deg) rotate(-3deg);
transform: skew(-3deg) rotate(-3deg);
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.4);
}
.m-signature-pad:after {
left: auto;
right: 20px;
-webkit-transform: skew(3deg) rotate(3deg);
-moz-transform: skew(3deg) rotate(3deg);
-ms-transform: skew(3deg) rotate(3deg);
-o-transform: skew(3deg) rotate(3deg);
transform: skew(3deg) rotate(3deg);
}
.m-signature-pad--body {
position: absolute;
left: 20px;
right: 20px;
top: 20px;
bottom: 60px;
border: 1px solid #f4f4f4;
}
.m-signature-pad--body
canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 4px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.02) inset;
}
.m-signature-pad--footer {
position: absolute;
left: 20px;
right: 20px;
bottom: 20px;
height: 40px;
}
.m-signature-pad--footer
.description {
color: #C3C3C3;
text-align: center;
font-size: 1.2em;
margin-top: 1.8em;
}
.m-signature-pad--footer
.button {
position: absolute;
bottom: 0;
}
.m-signature-pad--footer
.button.clear {
left: 0;
}
.m-signature-pad--footer
.button.save {
right: 0;
}
@media screen and (max-width: 1024px) {
.m-signature-pad {
top: 0;
left: 0;
right: 0;
bottom: 0;
width: auto;
height: auto;
min-width: 250px;
min-height: 140px;
margin: 5%;
}
}
@media screen and (min-device-width: 768px) and (max-device-width: 1024px) {
.m-signature-pad {
margin: 10%;
}
}
@media screen and (max-height: 320px) {
.m-signature-pad--body {
left: 0;
right: 0;
top: 0;
bottom: 32px;
}
.m-signature-pad--footer {
left: 20px;
right: 20px;
bottom: 4px;
height: 28px;
}
.m-signature-pad--footer
.description {
font-size: 1em;
margin-top: 1em;
}
}

389
public/assets/js/signature_pad.js Executable file
View file

@ -0,0 +1,389 @@
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module unless amdModuleId is set
define([], function () {
return (root['SignaturePad'] = factory());
});
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
root['SignaturePad'] = factory();
}
}(this, function () {
/*!
* Signature Pad v1.5.3
* https://github.com/szimek/signature_pad
*
* Copyright 2016 Szymon Nowak
* Released under the MIT license
*
* The main idea and some parts of the code (e.g. drawing variable width Bézier curve) are taken from:
* http://corner.squareup.com/2012/07/smoother-signatures.html
*
* Implementation of interpolation using cubic Bézier curves is taken from:
* http://benknowscode.wordpress.com/2012/09/14/path-interpolation-using-cubic-bezier-and-control-point-estimation-in-javascript
*
* Algorithm for approximated length of a Bézier curve is taken from:
* http://www.lemoda.net/maths/bezier-length/index.html
*
*/
var SignaturePad = (function (document) {
"use strict";
var SignaturePad = function (canvas, options) {
var self = this,
opts = options || {};
this.velocityFilterWeight = opts.velocityFilterWeight || 0.7;
this.minWidth = opts.minWidth || 0.5;
this.maxWidth = opts.maxWidth || 2.5;
this.dotSize = opts.dotSize || function () {
return (this.minWidth + this.maxWidth) / 2;
};
this.penColor = opts.penColor || "black";
this.backgroundColor = opts.backgroundColor || "rgba(0,0,0,0)";
this.onEnd = opts.onEnd;
this.onBegin = opts.onBegin;
this._canvas = canvas;
this._ctx = canvas.getContext("2d");
this.clear();
// we need add these inline so they are available to unbind while still having
// access to 'self' we could use _.bind but it's not worth adding a dependency
this._handleMouseDown = function (event) {
if (event.which === 1) {
self._mouseButtonDown = true;
self._strokeBegin(event);
}
};
this._handleMouseMove = function (event) {
if (self._mouseButtonDown) {
self._strokeUpdate(event);
}
};
this._handleMouseUp = function (event) {
if (event.which === 1 && self._mouseButtonDown) {
self._mouseButtonDown = false;
self._strokeEnd(event);
}
};
this._handleTouchStart = function (event) {
if (event.targetTouches.length == 1) {
var touch = event.changedTouches[0];
self._strokeBegin(touch);
}
};
this._handleTouchMove = function (event) {
// Prevent scrolling.
event.preventDefault();
var touch = event.targetTouches[0];
self._strokeUpdate(touch);
};
this._handleTouchEnd = function (event) {
var wasCanvasTouched = event.target === self._canvas;
if (wasCanvasTouched) {
event.preventDefault();
self._strokeEnd(event);
}
};
this._handleMouseEvents();
this._handleTouchEvents();
};
SignaturePad.prototype.clear = function () {
var ctx = this._ctx,
canvas = this._canvas;
ctx.fillStyle = this.backgroundColor;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
this._reset();
};
SignaturePad.prototype.toDataURL = function (imageType, quality) {
var canvas = this._canvas;
return canvas.toDataURL.apply(canvas, arguments);
};
SignaturePad.prototype.fromDataURL = function (dataUrl) {
var self = this,
image = new Image(),
ratio = window.devicePixelRatio || 1,
width = this._canvas.width / ratio,
height = this._canvas.height / ratio;
this._reset();
image.src = dataUrl;
image.onload = function () {
self._ctx.drawImage(image, 0, 0, width, height);
};
this._isEmpty = false;
};
SignaturePad.prototype._strokeUpdate = function (event) {
var point = this._createPoint(event);
this._addPoint(point);
};
SignaturePad.prototype._strokeBegin = function (event) {
this._reset();
this._strokeUpdate(event);
if (typeof this.onBegin === 'function') {
this.onBegin(event);
}
};
SignaturePad.prototype._strokeDraw = function (point) {
var ctx = this._ctx,
dotSize = typeof(this.dotSize) === 'function' ? this.dotSize() : this.dotSize;
ctx.beginPath();
this._drawPoint(point.x, point.y, dotSize);
ctx.closePath();
ctx.fill();
};
SignaturePad.prototype._strokeEnd = function (event) {
var canDrawCurve = this.points.length > 2,
point = this.points[0];
if (!canDrawCurve && point) {
this._strokeDraw(point);
}
if (typeof this.onEnd === 'function') {
this.onEnd(event);
}
};
SignaturePad.prototype._handleMouseEvents = function () {
this._mouseButtonDown = false;
this._canvas.addEventListener("mousedown", this._handleMouseDown);
this._canvas.addEventListener("mousemove", this._handleMouseMove);
document.addEventListener("mouseup", this._handleMouseUp);
};
SignaturePad.prototype._handleTouchEvents = function () {
// Pass touch events to canvas element on mobile IE11 and Edge.
this._canvas.style.msTouchAction = 'none';
this._canvas.style.touchAction = 'none';
this._canvas.addEventListener("touchstart", this._handleTouchStart);
this._canvas.addEventListener("touchmove", this._handleTouchMove);
this._canvas.addEventListener("touchend", this._handleTouchEnd);
};
SignaturePad.prototype.on = function () {
this._handleMouseEvents();
this._handleTouchEvents();
};
SignaturePad.prototype.off = function () {
this._canvas.removeEventListener("mousedown", this._handleMouseDown);
this._canvas.removeEventListener("mousemove", this._handleMouseMove);
document.removeEventListener("mouseup", this._handleMouseUp);
this._canvas.removeEventListener("touchstart", this._handleTouchStart);
this._canvas.removeEventListener("touchmove", this._handleTouchMove);
this._canvas.removeEventListener("touchend", this._handleTouchEnd);
};
SignaturePad.prototype.isEmpty = function () {
return this._isEmpty;
};
SignaturePad.prototype._reset = function () {
this.points = [];
this._lastVelocity = 0;
this._lastWidth = (this.minWidth + this.maxWidth) / 2;
this._isEmpty = true;
this._ctx.fillStyle = this.penColor;
};
SignaturePad.prototype._createPoint = function (event) {
var rect = this._canvas.getBoundingClientRect();
return new Point(
event.clientX - rect.left,
event.clientY - rect.top
);
};
SignaturePad.prototype._addPoint = function (point) {
var points = this.points,
c2, c3,
curve, tmp;
points.push(point);
if (points.length > 2) {
// To reduce the initial lag make it work with 3 points
// by copying the first point to the beginning.
if (points.length === 3) points.unshift(points[0]);
tmp = this._calculateCurveControlPoints(points[0], points[1], points[2]);
c2 = tmp.c2;
tmp = this._calculateCurveControlPoints(points[1], points[2], points[3]);
c3 = tmp.c1;
curve = new Bezier(points[1], c2, c3, points[2]);
this._addCurve(curve);
// Remove the first element from the list,
// so that we always have no more than 4 points in points array.
points.shift();
}
};
SignaturePad.prototype._calculateCurveControlPoints = function (s1, s2, s3) {
var dx1 = s1.x - s2.x, dy1 = s1.y - s2.y,
dx2 = s2.x - s3.x, dy2 = s2.y - s3.y,
m1 = {x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0},
m2 = {x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0},
l1 = Math.sqrt(dx1*dx1 + dy1*dy1),
l2 = Math.sqrt(dx2*dx2 + dy2*dy2),
dxm = (m1.x - m2.x),
dym = (m1.y - m2.y),
k = l2 / (l1 + l2),
cm = {x: m2.x + dxm*k, y: m2.y + dym*k},
tx = s2.x - cm.x,
ty = s2.y - cm.y;
return {
c1: new Point(m1.x + tx, m1.y + ty),
c2: new Point(m2.x + tx, m2.y + ty)
};
};
SignaturePad.prototype._addCurve = function (curve) {
var startPoint = curve.startPoint,
endPoint = curve.endPoint,
velocity, newWidth;
velocity = endPoint.velocityFrom(startPoint);
velocity = this.velocityFilterWeight * velocity
+ (1 - this.velocityFilterWeight) * this._lastVelocity;
newWidth = this._strokeWidth(velocity);
this._drawCurve(curve, this._lastWidth, newWidth);
this._lastVelocity = velocity;
this._lastWidth = newWidth;
};
SignaturePad.prototype._drawPoint = function (x, y, size) {
var ctx = this._ctx;
ctx.moveTo(x, y);
ctx.arc(x, y, size, 0, 2 * Math.PI, false);
this._isEmpty = false;
};
SignaturePad.prototype._drawCurve = function (curve, startWidth, endWidth) {
var ctx = this._ctx,
widthDelta = endWidth - startWidth,
drawSteps, width, i, t, tt, ttt, u, uu, uuu, x, y;
drawSteps = Math.floor(curve.length());
ctx.beginPath();
for (i = 0; i < drawSteps; i++) {
// Calculate the Bezier (x, y) coordinate for this step.
t = i / drawSteps;
tt = t * t;
ttt = tt * t;
u = 1 - t;
uu = u * u;
uuu = uu * u;
x = uuu * curve.startPoint.x;
x += 3 * uu * t * curve.control1.x;
x += 3 * u * tt * curve.control2.x;
x += ttt * curve.endPoint.x;
y = uuu * curve.startPoint.y;
y += 3 * uu * t * curve.control1.y;
y += 3 * u * tt * curve.control2.y;
y += ttt * curve.endPoint.y;
width = startWidth + ttt * widthDelta;
this._drawPoint(x, y, width);
}
ctx.closePath();
ctx.fill();
};
SignaturePad.prototype._strokeWidth = function (velocity) {
return Math.max(this.maxWidth / (velocity + 1), this.minWidth);
};
var Point = function (x, y, time) {
this.x = x;
this.y = y;
this.time = time || new Date().getTime();
};
Point.prototype.velocityFrom = function (start) {
return (this.time !== start.time) ? this.distanceTo(start) / (this.time - start.time) : 1;
};
Point.prototype.distanceTo = function (start) {
return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
};
var Bezier = function (startPoint, control1, control2, endPoint) {
this.startPoint = startPoint;
this.control1 = control1;
this.control2 = control2;
this.endPoint = endPoint;
};
// Returns approximated length.
Bezier.prototype.length = function () {
var steps = 10,
length = 0,
i, t, cx, cy, px, py, xdiff, ydiff;
for (i = 0; i <= steps; i++) {
t = i / steps;
cx = this._point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
cy = this._point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
if (i > 0) {
xdiff = cx - px;
ydiff = cy - py;
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
}
px = cx;
py = cy;
}
return length;
};
Bezier.prototype._point = function (t, start, c1, c2, end) {
return start * (1.0 - t) * (1.0 - t) * (1.0 - t)
+ 3.0 * c1 * (1.0 - t) * (1.0 - t) * t
+ 3.0 * c2 * (1.0 - t) * t * t
+ end * t * t * t;
};
return SignaturePad;
})(document);
return SignaturePad;
}));

5
public/assets/js/signature_pad.min.js vendored Executable file

File diff suppressed because one or more lines are too long

View file

@ -121,6 +121,8 @@ return array(
'two_factor_config_complete' => 'Submit Code', 'two_factor_config_complete' => 'Submit Code',
'two_factor_enabled_edit_not_allowed' => 'Your administrator does not permit you to edit this setting.', 'two_factor_enabled_edit_not_allowed' => 'Your administrator does not permit you to edit this setting.',
'two_factor_enrollment_text' => "Two factor authentication is required, however your device has not been enrolled yet. Open your Google Authenticator app and scan the QR code below to enroll your device. Once you've enrolled your device, enter the code below", 'two_factor_enrollment_text' => "Two factor authentication is required, however your device has not been enrolled yet. Open your Google Authenticator app and scan the QR code below to enroll your device. Once you've enrolled your device, enter the code below",
'require_accept_signature' => 'Require Signature',
'require_accept_signature_help_text' => 'Enabling this feature will require users to physically sign off on accepting an asset.',
'left' => 'left', 'left' => 'left',
'right' => 'right', 'right' => 'right',
'top' => 'top', 'top' => 'top',

View file

@ -145,6 +145,7 @@
'select_asset' => 'Select Asset', 'select_asset' => 'Select Asset',
'settings' => 'Settings', 'settings' => 'Settings',
'sign_in' => 'Sign in', 'sign_in' => 'Sign in',
'signature' => 'Signature',
'some_features_disabled' => 'DEMO MODE: Some features are disabled for this installation.', 'some_features_disabled' => 'DEMO MODE: Some features are disabled for this installation.',
'site_name' => 'Site Name', 'site_name' => 'Site Name',
'state' => 'State', 'state' => 'State',

View file

@ -2,59 +2,142 @@
{{-- Page title --}} {{-- Page title --}}
@section('title') @section('title')
Accept {{ $item->showAssetName() }}
@parent @parent
@stop @stop
{{-- Page content --}} {{-- Page content --}}
@section('content') @section('content')
<link rel="stylesheet" href="{{ asset('assets/css/signature-pad.css') }}">
<style> <style>
.form-horizontal .control-label, .form-horizontal .radio, .form-horizontal .checkbox, .form-horizontal .radio-inline, .form-horizontal .checkbox-inline { .form-horizontal .control-label, .form-horizontal .radio, .form-horizontal .checkbox, .form-horizontal .radio-inline, .form-horizontal .checkbox-inline {
padding-top: 17px; padding-top: 17px;
padding-right: 10px; padding-right: 10px;
} }
.radio input[type="radio"], .radio-inline input[type="radio"], .checkbox input[type="checkbox"], .checkbox-inline input[type="checkbox"] { #eula_div {
margin-left: -40px; width: 100%;
height: 200px;
overflow: scroll;
} }
</style> </style>
<div class="row header">
<div class="col-md-12">
<h3>
Accept {{ $item->showAssetName() }}</h3>
</div>
</div>
<div class="col-md-12"> <form class="form-horizontal" method="post" action="" autocomplete="off">
<form class="form-horizontal" method="post" action="" autocomplete="off"> <!-- CSRF Token -->
<!-- CSRF Token --> <input type="hidden" name="_token" value="{{ csrf_token() }}" />
<input type="hidden" name="_token" value="{{ csrf_token() }}" /> <input type="hidden" name="logId" value="{{ $findlog->id }}" />
<input type="hidden" name="logId" value="{{ $findlog->id }}" />
<div class="radio">
<label>
<input type="radio" name="asset_acceptance" id="accepted" value="accepted"> I accept
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="asset_acceptance" id="declined" value="declined"> I decline
</label>
</div>
<!-- Form actions --> <div class="row">
<div class="form-group"> <div class="col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2">
<div class="col-md-7 col-md-offset-3">
<button type="submit" class="btn btn-success">Submit </button> <div class="panel box box-default">
</div>
</div> <div class="box-body">
</form> <div class="col-md-12">
</div>
</div>
<div class="radio">
<label>
<input type="radio" name="asset_acceptance" id="accepted" value="accepted">
I accept
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="asset_acceptance" id="declined" value="declined">
I decline
</label>
</div>
@if ($item->getEula())
<div class="col-md-12" style="padding-top: 20px">
<div id="eula_div">
{!! $item->getEula() !!}
</div>
</div>
@endif
@if (\App\Models\Setting::getSettings()->require_accept_signature=='1')
<div class="col-md-12 col-sm-12 text-center" style="padding-top: 20px">
<h3>Sign below to indicate that you agree to the terms of service:</h3>
<div id="signature-pad" class="m-signature-pad col-md-12 col-sm-12">
<div class="m-signature-pad--body col-md-12 col-sm-12">
<canvas></canvas>
<input type="hidden" name="signature_output" id="signature_output">
</div>
<div class="col-md-12 col-sm-12 text-center">
<button type="button" class="btn btn-sm btn-default clear" data-action="clear" id="clear_button">Clear</button>
</div>
</div>
</div>
@endif
</div><!-- / col-md-7 col-sm-12 -->
</div> <!-- / box-body -->
<div class="box-footer text-right">
<button type="submit" class="btn btn-success" id="submit-button"><i class="fa fa-check icon-white"></i> {{ trans('general.submit') }}</button>
</div><!-- /.box-footer -->
</div> <!-- / box-default -->
</div> <!-- / col -->
</div> <!-- / row -->
</form>
@section('moar_scripts')
<script src="{{ asset('assets/js/signature_pad.min.js') }}"></script>
<script>
var wrapper = document.getElementById("signature-pad"),
clearButton = wrapper.querySelector("[data-action=clear]"),
saveButton = wrapper.querySelector("[data-action=save]"),
canvas = wrapper.querySelector("canvas"),
signaturePad;
// Adjust canvas coordinate space taking into account pixel ratio,
// to make it look crisp on mobile devices.
// This also causes canvas to be cleared.
function resizeCanvas() {
// When zoomed out to less than 100%, for some very strange reason,
// some browsers report devicePixelRatio as less than 1
// and only part of the canvas is cleared then.
var ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
}
window.onresize = resizeCanvas;
resizeCanvas();
signaturePad = new SignaturePad(canvas);
$('#clear_button').on("click", function (event) {
signaturePad.clear();
});
$('#submit-button').on("click", function (event) {
if (signaturePad.isEmpty()) {
alert("Please provide signature first.");
return false;
} else {
$('#signature_output').val(signaturePad.toDataURL());
}
});
</script>
@stop
@stop @stop

View file

@ -572,6 +572,9 @@
<th class="col-md-2"><span class="line"></span>{{ trans('table.actions') }}</th> <th class="col-md-2"><span class="line"></span>{{ trans('table.actions') }}</th>
<th class="col-md-2"><span class="line"></span>{{ trans('general.user') }}</th> <th class="col-md-2"><span class="line"></span>{{ trans('general.user') }}</th>
<th class="col-md-3"><span class="line"></span>{{ trans('general.notes') }}</th> <th class="col-md-3"><span class="line"></span>{{ trans('general.notes') }}</th>
@if (App\Models\Setting::getSettings()->require_accept_signature=='1')
<th class="col-md-3"><span class="line"></span>{{ trans('general.signature') }}</th>
@endif
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -583,8 +586,6 @@
@if ($log->action_type != 'requested') @if ($log->action_type != 'requested')
@if (isset($log->user)) @if (isset($log->user))
{{ $log->user->fullName() }} {{ $log->user->fullName() }}
@else
Deleted Admin
@endif @endif
@endif @endif
</td> </td>
@ -602,7 +603,6 @@
<a href="{{ route('view/user', $log->target_id) }}"> <a href="{{ route('view/user', $log->target_id) }}">
{{ $log->target->fullName() }} {{ $log->target->fullName() }}
</a> </a>
@else @else
<del>{{ $log->target->fullName() }}</del> <del>{{ $log->target->fullName() }}</del>
@endif @endif
@ -611,11 +611,13 @@
<a href="{{ route('view/hardware', $log->target_id) }}"> <a href="{{ route('view/hardware', $log->target_id) }}">
{{ $log->target->showAssetName() }} {{ $log->target->showAssetName() }}
</a> </a>
@else @else
<del>{{ $log->target->showAssetName() }}</del> <del>{{ $log->target->showAssetName() }}</del>
@endif @endif
@elseif (($log->action_type=='accepted') || ($log->action_type=='declined'))
{{ $log->item->assigneduser->fullName() }}
@else @else
Deleted User Deleted User
@endif @endif
@endif @endif
@ -624,6 +626,13 @@
@if ($log->note) {{ $log->note }} @if ($log->note) {{ $log->note }}
@endif @endif
</td> </td>
@if (App\Models\Setting::getSettings()->require_accept_signature=='1')
<td>
@if (($log->accept_signature!='') && (($log->action_type=='accepted') || ($log->action_type=='declined')))
<a href="{{ route('log.signature.view', ['filename' => $log->accept_signature ]) }}" data-toggle="lightbox" data-type="image"><img src="{{ route('log.signature.view', ['filename' => $log->accept_signature ]) }}" class="img-responsive"></a>
@endif
</td>
@endif
</tr> </tr>
@endforeach @endforeach

View file

@ -116,6 +116,21 @@
</div> </div>
<!-- /.form-group --> <!-- /.form-group -->
<!-- Require signature for acceptance -->
<div class="form-group {{ $errors->has('require_accept_signature') ? 'error' : '' }}">
<div class="col-md-3">
{{ Form::label('full_multiple_companies_support',
trans('admin/settings/general.require_accept_signature')) }}
</div>
<div class="col-md-9">
{{ Form::checkbox('require_accept_signature', '1', Input::old('require_accept_signature', $setting->require_accept_signature),array('class' => 'minimal')) }}
{{ trans('general.yes') }}
{!! $errors->first('require_accept_signature', '<span class="alert-msg">:message</span>') !!}
<p class="help-block">{{ trans('admin/settings/general.require_accept_signature_help_text') }}</p>
</div>
</div>
<!-- /.form-group -->
<!-- Logo --> <!-- Logo -->
<div class="form-group {{ $errors->has('logo') ? 'has-error' : '' }}"> <div class="form-group {{ $errors->has('logo') ? 'has-error' : '' }}">
<div class="col-md-3"> <div class="col-md-3">

View file

@ -101,6 +101,16 @@
@endif @endif
</tr> </tr>
<tr>
<td>{{ trans('admin/settings/general.require_accept_signature') }}</td>
@if ($setting->require_accept_signature == 1)
<td>{{ trans('general.yes') }}</td>
@else
<td>{{ trans('general.no') }}</td>
@endif
</tr>
<tr> <tr>
<td>{{ trans('admin/settings/general.load_remote_text') }}</td> <td>{{ trans('admin/settings/general.load_remote_text') }}</td>

View file

@ -0,0 +1,2 @@
*
!.gitignore