1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-11 13:42:49 +01:00

Merge branch 'release-2.6.9'

Conflicts:
	app/Http/routes.php
	app/Ninja/PaymentDrivers/BasePaymentDriver.php
	composer.lock
	resources/lang/en/texts.php
	resources/views/reports/chart_builder.blade.php
This commit is contained in:
Hillel Coren 2016-08-14 09:07:44 +03:00
commit c7c253fbd8
275 changed files with 72150 additions and 21957 deletions

View File

@ -1,7 +1,7 @@
APP_ENV=production
APP_DEBUG=false
APP_URL=http://ninja.dev
APP_KEY=SomeRandomString
APP_KEY=SomeRandomStringSomeRandomString
APP_CIPHER=AES-256-CBC
DB_TYPE=mysql

5
.gitattributes vendored
View File

@ -1,3 +1,8 @@
* text=auto
*.css linguist-vendored
*.less linguist-vendored
.gitattributes export-ignore
.gitignore export-ignore
.codeclimate.yml export-ignore
.travis.yml export-ignore
.styleci.yml export-ignore

View File

@ -29,6 +29,7 @@ before_install:
- if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi;
- composer self-update && composer -V
# - export USE_ZEND_ALLOC=0
- rvm use 1.9.3 --install --fuzzy
install:
# install Composer dependencies

View File

@ -2,8 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Changed
- Auto billing uses credits if they exist
## [2.6.4] - 2016-07-19
### Added
- Added 'Buy Now' buttons

View File

@ -11,7 +11,7 @@ Thanks for your contributions!
* Make your changes and commit
* Check if your branch is still in sync with the repositorys **`develop`** branch
* _Read:_ [Syncing a fork](https://help.github.com/articles/syncing-a-fork/)
* _Also read:_ [How to rebase a pull request](https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request)
* _Also read:_ [How to rebase a pull request](https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request)
* Push your branch and create a PR against the Invoice Ninja **`develop`** branch
* Update the [Changelog](CHANGELOG.md)
@ -21,7 +21,7 @@ To make the contribution process nice and easy for anyone, please follow some ru
to give a more detailed explanation.
* Only one feature/bugfix per issue. If you want to submit more, create multiple issues.
* Only one feature/bugfix per PR(pull request). Split more changes into multiple PRs.
#### Coding Style
Try to follow the [PSR-2 guidlines](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
@ -29,7 +29,7 @@ _Example styling:_
```php
/**
* Gets a preview of the email
*
*
* @param TemplateService $templateService
*
* @return \Illuminate\Http\Response

View File

@ -4,12 +4,12 @@ module.exports = function(grunt) {
pkg: grunt.file.readJSON('package.json'),
dump_dir: (function() {
var out = {};
grunt.file.expand({ filter: 'isDirectory'}, 'public/fonts/invoice-fonts/*').forEach(function(path) {
var fontName = /[^/]*$/.exec(path)[0],
files = {},
license='';
// Add license text
grunt.file.expand({ filter: 'isFile'}, path+'/*.txt').forEach(function(path) {
var licenseText = grunt.file.read(path);
@ -19,10 +19,10 @@ module.exports = function(grunt) {
license += "/*\n"+licenseText+"\n*/";
});
// Create files list
files['public/js/vfs_fonts/'+fontName+'.js'] = [path+'/*.ttf'];
out[fontName] = {
options: {
pre: license+'window.ninjaFontVfs=window.ninjaFontVfs||{};window.ninjaFontVfs.'+fontName+'=',
@ -30,8 +30,8 @@ module.exports = function(grunt) {
},
files: files
};
});
});
// Return the computed object
return out;
}()),
@ -104,8 +104,8 @@ module.exports = function(grunt) {
'public/vendor/moment-timezone/builds/moment-timezone-with-data.min.js',
'public/vendor/stacktrace-js/dist/stacktrace-with-polyfills.min.js',
'public/vendor/fuse.js/src/fuse.min.js',
'public/vendor/sweetalert/dist/sweetalert.min.js',
//'public/vendor/moment-duration-format/lib/moment-duration-format.js',
//'public/vendor/handsontable/dist/jquery.handsontable.full.min.js',
//'public/vendor/pdfmake/build/pdfmake.min.js',
//'public/vendor/pdfmake/build/vfs_fonts.js',
//'public/js/vfs_fonts.js',
@ -116,14 +116,12 @@ module.exports = function(grunt) {
dest: 'public/built.js',
nonull: true
},
js_public: {
/*js_public: {
src: [
/*
'public/js/simpleexpand.js',
'public/js/valign.js',
'public/js/bootstrap.min.js',
'public/js/simpleexpand.js',
*/
'public/vendor/bootstrap/dist/js/bootstrap.min.js',
'public/js/bootstrap-combobox.js',
@ -142,7 +140,7 @@ module.exports = function(grunt) {
'public/vendor/spectrum/spectrum.css',
'public/css/bootstrap-combobox.css',
'public/css/typeahead.js-bootstrap.css',
//'public/vendor/handsontable/dist/jquery.handsontable.full.css',
'public/vendor/sweetalert/dist/sweetalert.css',
'public/css/style.css',
],
dest: 'public/css/built.css',
@ -150,8 +148,8 @@ module.exports = function(grunt) {
options: {
process: false
}
},
css_public: {
},*/
/*css_public: {
src: [
'public/vendor/bootstrap/dist/css/bootstrap.min.css',
'public/vendor/font-awesome/css/font-awesome.min.css',
@ -165,8 +163,8 @@ module.exports = function(grunt) {
options: {
process: false
}
},
js_pdf: {
},*/
/*js_pdf: {
src: [
'public/js/pdf_viewer.js',
'public/js/compatibility.js',
@ -175,7 +173,7 @@ module.exports = function(grunt) {
],
dest: 'public/pdf.built.js',
nonull: true
}
}*/
}
});

View File

@ -4,7 +4,7 @@
# Invoice Ninja
[![Build Status](https://travis-ci.org/invoiceninja/invoiceninja.svg?branch=master)](https://travis-ci.org/invoiceninja/invoiceninja)
[![Build Status](https://travis-ci.org/invoiceninja/invoiceninja.svg?branch=develop)](https://travis-ci.org/invoiceninja/invoiceninja)
[![Join the chat at https://gitter.im/hillelcoren/invoice-ninja](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hillelcoren/invoice-ninja?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## [Hosted](https://www.invoiceninja.com) | [Self-hosted](https://invoiceninja.org)

View File

@ -0,0 +1,75 @@
<?php
namespace App\Console\Commands;
use App\Models\Account;
use App\Models\Invoice;
use Carbon\Carbon;
use Illuminate\Console\Command;
class ResetInvoiceSchemaCounter extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:reset-invoice-schema-counter
{account? : The ID of the account}
{--force : Force setting the counter back to "1", regardless if the year changed}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset the invoice schema counter at the turn of the year.';
/**
* @var Invoice
*/
protected $invoice;
/**
* Create a new command instance.
*
* @param Invoice $invoice
*/
public function __construct(Invoice $invoice)
{
parent::__construct();
$this->invoice = $invoice;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$force = $this->option('force');
$account = $this->argument('account');
$accounts = null;
if ($account) {
$accounts = Account::find($account)->get();
} else {
$accounts = Account::all();
}
$latestInvoice = $this->invoice->latest()->first();
$invoiceYear = Carbon::parse($latestInvoice->created_at)->year;
if(Carbon::now()->year > $invoiceYear || $force) {
$accounts->transform(function ($a) {
/** @var Account $a */
$a->invoice_number_counter = 1;
$a->update();
});
$this->info('The counter has been resetted successfully for '.$accounts->count().' account(s).');
}
}
}

View File

@ -21,8 +21,8 @@ class Kernel extends ConsoleKernel
'App\Console\Commands\SendRenewalInvoices',
'App\Console\Commands\ChargeRenewalInvoices',
'App\Console\Commands\SendReminders',
'App\Console\Commands\TestOFX',
'App\Console\Commands\GenerateResources',
'App\Console\Commands\TestOFX',
];
/**

View File

@ -1461,6 +1461,7 @@ class AccountController extends BaseController
return trans('texts.create_invoice_for_sample');
}
/** @var \App\Models\Account $account */
$account = Auth::user()->account;
$invitation = $invoice->invitations->first();

View File

@ -60,6 +60,8 @@ class AccountGatewayController extends BaseController
$data['hiddenFields'] = Gateway::$hiddenFields;
$data['selectGateways'] = Gateway::where('id', '=', $accountGateway->gateway_id)->get();
$this->testGateway($accountGateway);
return View::make('accounts.account_gateway', $data);
}
@ -307,7 +309,7 @@ class AccountGatewayController extends BaseController
$account->account_gateways()->save($accountGateway);
}
if(isset($wepayResponse)) {
if (isset($wepayResponse)) {
return $wepayResponse;
} else {
if ($accountGatewayPublicId) {
@ -322,6 +324,16 @@ class AccountGatewayController extends BaseController
}
}
private function testGateway($accountGateway)
{
$paymentDriver = $accountGateway->paymentDriver();
$result = $paymentDriver->isValid();
if ($result !== true) {
Session::flash('error', $result . ' - ' . trans('texts.gateway_config_error'));
}
}
protected function getWePayUpdateUri($accountGateway)
{
if ($accountGateway->gateway_id != GATEWAY_WEPAY) {

View File

@ -0,0 +1,323 @@
<?php
namespace App\Http\Controllers;
use Auth;
use DB;
use Utils;
use Cache;
use Input;
use Exception;
use App\Libraries\Skype\SkypeResponse;
use App\Libraries\CurlUtils;
use App\Models\User;
use App\Models\SecurityCode;
use App\Ninja\Intents\BaseIntent;
use App\Ninja\Mailers\UserMailer;
class BotController extends Controller
{
protected $userMailer;
public function __construct(UserMailer $userMailer)
{
$this->userMailer = $userMailer;
}
public function handleMessage($platform)
{
$input = Input::all();
$botUserId = $input['from']['id'];
if ( ! $token = $this->authenticate($input)) {
return SkypeResponse::message(trans('texts.not_authorized'));
}
try {
if ($input['type'] === 'contactRelationUpdate') {
// brand new user, ask for their email
if ($input['action'] === 'add') {
$response = SkypeResponse::message(trans('texts.bot_get_email'));
$state = BOT_STATE_GET_EMAIL;
} elseif ($input['action'] === 'remove') {
$this->removeBot($botUserId);
$this->saveState($token, false);
return RESULT_SUCCESS;
}
} else {
$state = $this->loadState($token);
$text = strip_tags($input['text']);
// user gaves us their email
if ($state === BOT_STATE_GET_EMAIL) {
if ($this->validateEmail($text, $botUserId)) {
$response = SkypeResponse::message(trans('texts.bot_get_code'));
$state = BOT_STATE_GET_CODE;
} else {
$response = SkypeResponse::message(trans('texts.email_not_found', ['email' => $text]));
}
// user sent the scurity code
} elseif ($state === BOT_STATE_GET_CODE) {
if ($this->validateCode($text, $botUserId)) {
$response = SkypeResponse::message(trans('texts.bot_welcome') . trans('texts.bot_help_message'));
$state = BOT_STATE_READY;
} else {
$response = SkypeResponse::message(trans('texts.invalid_code'));
}
// regular chat message
} else {
if ($message === 'help') {
$response = SkypeResponse::message(trans('texts.bot_help_message'));
} elseif ($message == 'status') {
$response = SkypeResponse::message(trans('texts.intent_not_supported'));
} else {
if ( ! $user = User::whereBotUserId($botUserId)->with('account')->first()) {
return SkypeResponse::message(trans('texts.not_authorized'));
}
Auth::onceUsingId($user->id);
$user->account->loadLocalizationSettings();
$data = $this->parseMessage($text);
$intent = BaseIntent::createIntent($state, $data);
$response = $intent->process();
$state = $intent->getState();
}
}
}
$this->saveState($token, $state);
} catch (Exception $exception) {
$response = SkypeResponse::message($exception->getMessage());
}
$this->sendResponse($token, $botUserId, $response);
return RESULT_SUCCESS;
}
private function authenticate($input)
{
$headers = getallheaders();
$token = isset($headers['Authorization']) ? $headers['Authorization'] : false;
if (Utils::isNinjaDev()) {
// skip validation for testing
} elseif ( ! $this->validateToken($token)) {
return false;
}
if ($token = Cache::get('msbot_token')) {
return $token;
}
$clientId = env('MSBOT_CLIENT_ID');
$clientSecret = env('MSBOT_CLIENT_SECRET');
$scope = 'https://graph.microsoft.com/.default';
$data = sprintf('grant_type=client_credentials&client_id=%s&client_secret=%s&scope=%s', $clientId, $clientSecret, $scope);
$response = CurlUtils::post(MSBOT_LOGIN_URL, $data);
$response = json_decode($response);
$expires = ($response->expires_in / 60) - 5;
Cache::put('msbot_token', $response->access_token, $expires);
return $response->access_token;
}
private function loadState($token)
{
$url = sprintf('%s/botstate/skype/conversations/%s', MSBOT_STATE_URL, '29:1C-OsU7OWBEDOYJhQUsDkYHmycOwOq9QOg5FVTwRX9ts');
$headers = [
'Authorization: Bearer ' . $token
];
$response = CurlUtils::get($url, $headers);
$data = json_decode($response);
return json_decode($data->data);
}
private function parseMessage($message)
{
$appId = env('MSBOT_LUIS_APP_ID');
$subKey = env('MSBOT_LUIS_SUBSCRIPTION_KEY');
$message = rawurlencode($message);
$url = sprintf('%s?id=%s&subscription-key=%s&q=%s', MSBOT_LUIS_URL, $appId, $subKey, $message);
$data = file_get_contents($url);
$data = json_decode($data);
return $data;
}
private function saveState($token, $data)
{
$url = sprintf('%s/botstate/skype/conversations/%s', MSBOT_STATE_URL, '29:1C-OsU7OWBEDOYJhQUsDkYHmycOwOq9QOg5FVTwRX9ts');
$headers = [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
];
//echo "STATE<pre>" . htmlentities(json_encode($data), JSON_PRETTY_PRINT) . "</pre>";
$data = '{ eTag: "*", data: "' . addslashes(json_encode($data)) . '" }';
CurlUtils::post($url, $data, $headers);
}
private function sendResponse($token, $to, $message)
{
$url = sprintf('%s/conversations/%s/activities/', SKYPE_API_URL, $to);
$headers = [
'Authorization: Bearer ' . $token,
];
//echo "<pre>" . htmlentities(json_encode(json_decode($message), JSON_PRETTY_PRINT)) . "</pre>";
$response = CurlUtils::post($url, $message, $headers);
//var_dump($response);
}
private function validateEmail($email, $botUserId)
{
if ( ! $email || ! $botUserId) {
return false;
}
// delete any expired codes
SecurityCode::whereBotUserId($botUserId)
->where('created_at', '<', DB::raw('now() - INTERVAL 10 MINUTE'))
->delete();
if (SecurityCode::whereBotUserId($botUserId)->first()) {
return false;
}
$user = User::whereEmail($email)
->whereNull('bot_user_id')
->first();
if ( ! $user) {
return false;
}
$code = new SecurityCode();
$code->user_id = $user->id;
$code->account_id = $user->account_id;
$code->code = mt_rand(100000, 999999);
$code->bot_user_id = $botUserId;
$code->save();
$this->userMailer->sendSecurityCode($user, $code->code);
return $code->code;
}
private function validateCode($input, $botUserId)
{
if ( ! $input || ! $botUserId) {
return false;
}
$code = SecurityCode::whereBotUserId($botUserId)
->where('created_at', '>', DB::raw('now() - INTERVAL 10 MINUTE'))
->where('attempts', '<', 5)
->first();
if ( ! $code) {
return false;
}
if ( ! hash_equals($code->code, $input)) {
$code->attempts += 1;
$code->save();
return false;
}
$user = User::find($code->user_id);
$user->bot_user_id = $code->bot_user_id;
$user->save();
return true;
}
private function removeBot($botUserId)
{
$user = User::whereBotUserId($botUserId)->first();
$user->bot_user_id = null;
$user->save();
}
private function validateToken($token)
{
if ( ! $token) {
return false;
}
// https://blogs.msdn.microsoft.com/tsmatsuz/2016/07/12/developing-skype-bot/
// 0:Invalid, 1:Valid
$token_valid = 0;
// 1 separate token by dot (.)
$token_arr = explode('.', $token);
$headers_enc = $token_arr[0];
$claims_enc = $token_arr[1];
$sig_enc = $token_arr[2];
// 2 base 64 url decoding
$headers_arr = json_decode($this->base64_url_decode($headers_enc), TRUE);
$claims_arr = json_decode($this->base64_url_decode($claims_enc), TRUE);
$sig = $this->base64_url_decode($sig_enc);
// 3 get key list
$keylist = file_get_contents('https://api.aps.skype.com/v1/keys');
$keylist_arr = json_decode($keylist, TRUE);
foreach($keylist_arr['keys'] as $key => $value) {
// 4 select one key (which matches)
if($value['kid'] == $headers_arr['kid']) {
// 5 get public key from key info
$cert_txt = '-----BEGIN CERTIFICATE-----' . "\n" . chunk_split($value['x5c'][0], 64) . '-----END CERTIFICATE-----';
$cert_obj = openssl_x509_read($cert_txt);
$pkey_obj = openssl_pkey_get_public($cert_obj);
$pkey_arr = openssl_pkey_get_details($pkey_obj);
$pkey_txt = $pkey_arr['key'];
// 6 verify signature
$token_valid = openssl_verify($headers_enc . '.' . $claims_enc, $sig, $pkey_txt, OPENSSL_ALGO_SHA256);
}
}
// 7 show result
return ($token_valid == 1);
}
private function base64_url_decode($arg) {
$res = $arg;
$res = str_replace('-', '+', $res);
$res = str_replace('_', '/', $res);
switch (strlen($res) % 4) {
case 0:
break;
case 2:
$res .= "==";
break;
case 3:
$res .= "=";
break;
default:
break;
}
$res = base64_decode($res);
return $res;
}
}

View File

@ -62,7 +62,7 @@ class CreditController extends BaseController
'method' => 'POST',
'url' => 'credits',
'title' => trans('texts.new_credit'),
'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
];
return View::make('credits.edit', $data);

View File

@ -52,7 +52,10 @@ class DocumentAPIController extends BaseAPIController
{
$document = $request->entity();
return DocumentController::getDownloadResponse($document);
if(array_key_exists($document->type, Document::$types))
return DocumentController::getDownloadResponse($document);
else
return $this->errorResponse(['error'=>'Invalid mime type'],400);
}
/**

View File

@ -611,4 +611,15 @@ class InvoiceController extends BaseController
return View::make('invoices.history', $data);
}
public function checkInvoiceNumber($invoiceNumber)
{
$count = Invoice::scope()
->whereInvoiceNumber($invoiceNumber)
->withTrashed()
->count();
return $count ? RESULT_FAILURE : RESULT_SUCCESS;
}
}

View File

@ -130,8 +130,9 @@ class OnlinePaymentController extends BaseController
}
try {
$paymentDriver->completeOffsitePurchase(Input::all());
Session::flash('message', trans('texts.applied_payment'));
if ($paymentDriver->completeOffsitePurchase(Input::all())) {
Session::flash('message', trans('texts.applied_payment'));
}
return redirect()->to('view/' . $invitation->invitation_key);
} catch (Exception $exception) {
return $this->error($paymentDriver, $exception);

View File

@ -25,6 +25,10 @@ class CreateInvoiceAPIRequest extends InvoiceRequest
'invoice_items' => 'valid_invoice_items',
'invoice_number' => 'unique:invoices,invoice_number,,id,account_id,' . $this->user()->account_id,
'discount' => 'positive',
'invoice_date' => 'date',
'due_date' => 'date',
'start_date' => 'date',
'end_date' => 'date',
];
return $rules;

View File

@ -24,6 +24,10 @@ class CreateInvoiceRequest extends InvoiceRequest
'invoice_items' => 'valid_invoice_items',
'invoice_number' => 'required|unique:invoices,invoice_number,,id,account_id,' . $this->user()->account_id,
'discount' => 'positive',
'invoice_date' => 'date',
'due_date' => 'date',
'start_date' => 'date',
'end_date' => 'date',
];
/* There's a problem parsing the dates

View File

@ -44,6 +44,11 @@ class EntityRequest extends Request {
return $this->entity;
}
public function setEntity($entity)
{
$this->entity = $entity;
}
public function authorize()
{
if ($this->entity()) {

View File

@ -29,6 +29,10 @@ class UpdateInvoiceAPIRequest extends InvoiceRequest
'invoice_items' => 'valid_invoice_items',
'invoice_number' => 'unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id,
'discount' => 'positive',
'invoice_date' => 'date',
'due_date' => 'date',
'start_date' => 'date',
'end_date' => 'date',
];
return $rules;

View File

@ -20,12 +20,16 @@ class UpdateInvoiceRequest extends InvoiceRequest
public function rules()
{
$invoiceId = $this->entity()->id;
$rules = [
'client.contacts' => 'valid_contacts',
'invoice_items' => 'valid_invoice_items',
'invoice_number' => 'required|unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id,
'discount' => 'positive',
'invoice_date' => 'date',
'due_date' => 'date',
'start_date' => 'date',
'end_date' => 'date',
];
/* There's a problem parsing the dates

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\ViewComposers;
use Illuminate\View\View;
class AppLanguageComposer
{
/**
* Bind data to the view.
*
* @param View $view
*
* @return void
*/
public function compose(View $view)
{
$view->with('appLanguage', $this->getLanguage());
}
/**
* Get the language from the current locale
*
* @return string
*/
private function getLanguage()
{
$code = app()->getLocale();
if(preg_match('/_/', $code)) {
$codes = explode('_', $code);
$code = $codes[0];
}
return $code;
}
}

View File

@ -85,6 +85,7 @@ Route::match(['GET', 'POST'], '/buy_now/{gateway_type?}', 'OnlinePaymentControll
Route::post('/hook/email_bounced', 'AppController@emailBounced');
Route::post('/hook/email_opened', 'AppController@emailOpened');
Route::post('/hook/bot/{platform?}', 'BotController@handleMessage');
Route::post('/payment_hook/{accountKey}/{gatewayId}', 'OnlinePaymentController@handlePaymentWebhook');
// Laravel auth routes
@ -125,7 +126,8 @@ Route::group(['middleware' => 'auth:user'], function() {
Route::get('view_archive/{entity_type}/{visible}', 'AccountController@setTrashVisible');
Route::get('hide_message', 'HomeController@hideMessage');
Route::get('force_inline_pdf', 'UserController@forcePDFJS');
Route::get('account/getSearchData', ['as' => 'getSearchData', 'uses' => 'AccountController@getSearchData']);
Route::get('account/get_search_data', ['as' => 'get_search_data', 'uses' => 'AccountController@getSearchData']);
Route::get('check_invoice_number/{invoice_number}', 'InvoiceController@checkInvoiceNumber');
Route::get('settings/user_details', 'AccountController@showUserDetails');
Route::post('settings/user_details', 'AccountController@saveUserDetails');
@ -351,7 +353,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ENTITY_CONTACT', 'contact');
define('ENTITY_INVOICE', 'invoice');
define('ENTITY_DOCUMENT', 'document');
define('ENTITY_INVOICE_ITEMS', 'invoice_items');
define('ENTITY_INVOICE_ITEM', 'invoice_item');
define('ENTITY_INVITATION', 'invitation');
define('ENTITY_RECURRING_INVOICE', 'recurring_invoice');
define('ENTITY_PAYMENT', 'payment');
@ -608,7 +610,7 @@ if (!defined('CONTACT_EMAIL')) {
define('NINJA_WEB_URL', env('NINJA_WEB_URL', 'https://www.invoiceninja.com'));
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
define('NINJA_DATE', '2000-01-01');
define('NINJA_VERSION', '2.6.8' . env('NINJA_VERSION_SUFFIX'));
define('NINJA_VERSION', '2.6.9' . env('NINJA_VERSION_SUFFIX'));
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'));
@ -626,6 +628,11 @@ if (!defined('CONTACT_EMAIL')) {
define('OFX_HOME_URL', env('OFX_HOME_URL', 'http://www.ofxhome.com/index.php/home/directory/all'));
define('GOOGLE_ANALYITCS_URL', env('GOOGLE_ANALYITCS_URL', 'https://www.google-analytics.com/collect'));
define('MSBOT_LOGIN_URL', 'https://login.microsoftonline.com/common/oauth2/v2.0/token');
define('MSBOT_LUIS_URL', 'https://api.projectoxford.ai/luis/v1/application');
define('SKYPE_API_URL', 'https://apis.skype.com/v3');
define('MSBOT_STATE_URL', 'https://state.botframework.com/v3');
define('BLANK_IMAGE', 'data:image/png;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=');
define('COUNT_FREE_DESIGNS', 4);
@ -790,6 +797,25 @@ if (!defined('CONTACT_EMAIL')) {
define('WEPAY_APP_FEE_MULTIPLIER', env('WEPAY_APP_FEE_MULTIPLIER', 0.002));
define('WEPAY_APP_FEE_FIXED', env('WEPAY_APP_FEE_MULTIPLIER', 0.00));
define('SKYPE_CARD_RECEIPT', 'message/card.receipt');
define('SKYPE_CARD_CAROUSEL', 'message/card.carousel');
define('SKYPE_CARD_HERO', '');
define('BOT_STATE_GET_EMAIL', 'get_email');
define('BOT_STATE_GET_CODE', 'get_code');
define('BOT_STATE_READY', 'ready');
define('SIMILAR_MIN_THRESHOLD', 50);
// https://docs.botframework.com/en-us/csharp/builder/sdkreference/attachments.html
define('SKYPE_BUTTON_OPEN_URL', 'openUrl');
define('SKYPE_BUTTON_IM_BACK', 'imBack');
define('SKYPE_BUTTON_POST_BACK', 'postBack');
define('SKYPE_BUTTON_CALL', 'call'); // "tel:123123123123"
define('SKYPE_BUTTON_PLAY_AUDIO', 'playAudio');
define('SKYPE_BUTTON_PLAY_VIDEO', 'playVideo');
define('SKYPE_BUTTON_SHOW_IMAGE', 'showImage');
define('SKYPE_BUTTON_DOWNLOAD_FILE', 'downloadFile');
$creditCards = [
1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'],
2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'],

View File

@ -0,0 +1,36 @@
<?php namespace App\Libraries;
class CurlUtils
{
public static function post($url, $data, $headers = false)
{
return self::exec('POST', $url, $data, $headers);
}
public static function get($url, $headers = false)
{
return self::exec('GET', $url, null, $headers);
}
public static function exec($method, $url, $data, $headers = false)
{
$curl = curl_init();
$opts = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => $method,
CURLOPT_HTTPHEADER => $headers ?: [],
];
if ($data) {
$opts[CURLOPT_POSTFIELDS] = $data;
}
curl_setopt_array($curl, $opts);
$response = curl_exec($curl);
curl_close($curl);
return $response;
}
}

View File

@ -0,0 +1,12 @@
<?php namespace App\Libraries\Skype;
class ButtonCard
{
public function __construct($type, $title, $value, $url = false)
{
$this->type = $type;
$this->title = $title;
$this->value = $value;
$this->image = $url;
}
}

View File

@ -0,0 +1,15 @@
<?php namespace App\Libraries\Skype;
class CarouselCard
{
public function __construct()
{
$this->contentType = 'application/vnd.microsoft.card.carousel';
$this->attachments = [];
}
public function addAttachment($attachment)
{
$this->attachments[] = $attachment;
}
}

View File

@ -0,0 +1,33 @@
<?php namespace App\Libraries\Skype;
use stdClass;
class HeroCard
{
public function __construct()
{
$this->contentType = 'application/vnd.microsoft.card.hero';
$this->content = new stdClass;
$this->content->buttons = [];
}
public function setTitle($title)
{
$this->content->title = $title;
}
public function setSubitle($subtitle)
{
$this->content->subtitle = $subtitle;
}
public function setText($text)
{
$this->content->text = $text;
}
public function addButton($type, $title, $value, $url = false)
{
$this->content->buttons[] = new ButtonCard($type, $title, $value, $url);
}
}

View File

@ -0,0 +1,79 @@
<?php namespace App\Libraries\Skype;
use HTML;
use stdClass;
class InvoiceCard
{
public function __construct($invoice)
{
$this->contentType = 'application/vnd.microsoft.card.receipt';
$this->content = new stdClass;
$this->content->facts = [];
$this->content->items = [];
$this->content->buttons = [];
$this->setTitle('test');
$this->setTitle(trans('texts.invoice_for_client', [
'invoice' => link_to($invoice->getRoute(), $invoice->invoice_number),
'client' => link_to($invoice->client->getRoute(), $invoice->client->getDisplayName())
]));
$this->addFact(trans('texts.email'), HTML::mailto($invoice->client->contacts[0]->email)->toHtml());
if ($invoice->due_date) {
$this->addFact($invoice->present()->dueDateLabel, $invoice->present()->due_date);
}
if ($invoice->po_number) {
$this->addFact(trans('texts.po_number'), $invoice->po_number);
}
if ($invoice->discount) {
$this->addFact(trans('texts.discount'), $invoice->present()->discount);
}
foreach ($invoice->invoice_items as $item) {
$this->addItem($item, $invoice->account);
}
$this->setTotal($invoice->present()->requestedAmount);
if (floatval($invoice->amount)) {
$this->addButton(SKYPE_BUTTON_OPEN_URL, trans('texts.download_pdf'), $invoice->getInvitationLink('view', true));
$this->addButton(SKYPE_BUTTON_IM_BACK, trans('texts.email_invoice'), trans('texts.email_invoice'));
} else {
$this->addButton(SKYPE_BUTTON_IM_BACK, trans('texts.list_products'), trans('texts.list_products'));
}
}
public function setTitle($title)
{
$this->content->title = $title;
}
public function setTotal($value)
{
$this->content->total = $value;
}
public function addFact($key, $value)
{
$fact = new stdClass;
$fact->key = $key;
$fact->value = $value;
$this->content->facts[] = $fact;
}
public function addItem($item, $account)
{
$this->content->items[] = new InvoiceItemCard($item, $account);
}
public function addButton($type, $title, $value, $url = false)
{
$this->content->buttons[] = new ButtonCard($type, $title, $value, $url);
}
}

View File

@ -0,0 +1,12 @@
<?php namespace App\Libraries\Skype;
class InvoiceItemCard
{
public function __construct($invoiceItem, $account)
{
$this->title = intval($invoiceItem->qty) . ' ' . $invoiceItem->product_key;
$this->subtitle = $invoiceItem->notes;
$this->quantity = $invoiceItem->qty;
$this->price = $account->formatMoney($invoiceItem->cost);
}
}

View File

@ -0,0 +1,28 @@
<?php namespace App\Libraries\Skype;
class SkypeResponse
{
public function __construct($type)
{
$this->type = $type;
$this->attachments = [];
}
public static function message($message)
{
$instance = new self('message/text');
$instance->setText($message);
return json_encode($instance);
}
public function setText($text)
{
$this->text = $text;
}
public function addAttachment($attachment)
{
$this->attachments[] = $attachment;
}
}

View File

@ -3,6 +3,7 @@
use Auth;
use Eloquent;
use Utils;
use Validator;
/**
* Class EntityModel
@ -86,6 +87,11 @@ class EntityModel extends Eloquent
return '[' . $this->getEntityType().':'.$this->public_id.':'.$this->getDisplayName() . ']';
}
public function entityKey()
{
return $this->public_id . ':' . $this->getEntityType();
}
/*
public function getEntityType()
{
@ -190,4 +196,37 @@ class EntityModel extends Eloquent
$name = $parts[count($parts)-1];
return strtolower($name) . '_id';
}
/**
* @param $data
* @param $entityType
* @return bool|string
*/
public static function validate($data, $entityType, $entity = false)
{
// Use the API request if it exists
$action = $entity ? 'update' : 'create';
$requestClass = sprintf('App\\Http\\Requests\\%s%sAPIRequest', ucwords($action), ucwords($entityType));
if ( ! class_exists($requestClass)) {
$requestClass = sprintf('App\\Http\\Requests\\%s%sRequest', ucwords($action), ucwords($entityType));
}
$request = new $requestClass();
$request->setUserResolver(function() { return Auth::user(); });
$request->setEntity($entity);
$request->replace($data);
if ( ! $request->authorize()) {
return trans('texts.not_allowed');
}
$validator = Validator::make($data, $request->rules());
if ($validator->fails()) {
return $validator->messages()->first();
} else {
return true;
}
}
}

View File

@ -516,6 +516,15 @@ class Invoice extends EntityModel implements BalanceAffecting
return self::calcLink($this);
}
public function getInvitationLink($type = 'view', $forceOnsite = false)
{
if ( ! $this->relationLoaded('invitations')) {
$this->load('invitations');
}
return $this->invitations[0]->getLink($type, $forceOnsite);
}
/**
* @return mixed
*/

View File

@ -7,6 +7,14 @@ use Illuminate\Database\Eloquent\SoftDeletes;
*/
class InvoiceItem extends EntityModel
{
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_INVOICE_ITEM;
}
use SoftDeletes;
/**
* @var array

View File

@ -1,5 +1,6 @@
<?php namespace App\Models;
use Laracasts\Presenter\PresentableTrait;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@ -7,12 +8,18 @@ use Illuminate\Database\Eloquent\SoftDeletes;
*/
class Product extends EntityModel
{
use PresentableTrait;
use SoftDeletes;
/**
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @var string
*/
protected $presenter = 'App\Ninja\Presenters\ProductPresenter';
/**
* @var array
*/

View File

@ -0,0 +1,15 @@
<?php namespace App\Models;
use Eloquent;
/**
* Class DatetimeFormat
*/
class SecurityCode extends Eloquent
{
/**
* @var bool
*/
public $timestamps = false;
}

View File

@ -0,0 +1,227 @@
<?php namespace App\Ninja\Intents;
use stdClass;
use Exception;
use App\Libraries\CurlUtils;
use App\Libraries\Skype\SkypeResponse;
class BaseIntent
{
protected $state;
protected $parameters;
protected $fieldMap = [];
public function __construct($state, $data)
{
//if (true) {
if ( ! $state || is_string($state)) {
$state = new stdClass;
foreach (['current', 'previous'] as $reference) {
$state->$reference = new stdClass;
$state->$reference->entityType = false;
foreach ([ENTITY_INVOICE, ENTITY_CLIENT, ENTITY_INVOICE_ITEM] as $entityType) {
$state->$reference->$entityType = [];
}
}
}
$this->state = $state;
$this->data = $data;
//var_dump($state);
}
public static function createIntent($state, $data)
{
if ( ! count($data->intents)) {
throw new Exception(trans('texts.intent_not_found'));
}
$intent = $data->intents[0]->intent;
$entityType = false;
foreach ($data->entities as $entity) {
if ($entity->type === 'EntityType') {
$entityType = $entity->entity;
break;
}
}
if ( ! $entityType) {
$entityType = $state->current->entityType;
}
$entityType = ucwords(strtolower($entityType));
$intent = str_replace('Entity', $entityType, $intent);
$className = "App\\Ninja\\Intents\\{$intent}Intent";
//echo "Intent: $intent<p>";
if ( ! class_exists($className)) {
throw new Exception(trans('texts.intent_not_supported'));
}
return (new $className($state, $data));
}
public function process()
{
// do nothing by default
}
public function setStateEntities($entityType, $entities)
{
if ( ! is_array($entities)) {
$entities = [$entities];
}
$state = $this->state;
$state->previous->$entityType = $state->current->$entityType;
$state->current->$entityType = $entities;
}
public function setStateEntityType($entityType)
{
$state = $this->state;
if ($state->current->entityType == $entityType) {
return;
}
$state->previous->entityType = $state->current->entityType;
$state->current->entityType = $entityType;
}
public function stateEntities($entityType)
{
return $this->state->current->$entityType;
}
public function stateEntity($entityType)
{
$entities = $this->state->current->$entityType;
return count($entities) ? $entities[0] : false;
}
public function previousStateEntities($entityType)
{
return $this->state->previous->$entityType;
}
public function stateEntityType()
{
return $this->state->current->entityType;
}
public function getState()
{
return $this->state;
}
protected function requestClient()
{
$clientRepo = app('App\Ninja\Repositories\ClientRepository');
$client = false;
foreach ($this->data->entities as $param) {
if ($param->type == 'Name') {
$client = $clientRepo->findPhonetically($param->entity);
}
}
return $client;
}
protected function requestFields()
{
$data = [];
if ( ! isset($this->data->compositeEntities)) {
return [];
}
foreach ($this->data->compositeEntities as $compositeEntity) {
if ($compositeEntity->parentType != 'FieldValuePair') {
continue;
}
$field = false;
$value = false;
foreach ($compositeEntity->children as $child) {
if ($child->type == 'Field') {
$field = $child->value;;
} elseif ($child->type == 'Value') {
$value = $child->value;
}
}
if ($field && $value) {
$field = $this->processField($field);
$value = $this->processValue($value);
$data[$field] = $value;
}
}
foreach ($this->fieldMap as $key => $value) {
if (isset($data[$key])) {
$data[$value] = $data[$key];
unset($data[$key]);
}
}
return $data;
}
protected function processField($field)
{
$field = str_replace(' ', '_', $field);
if (strpos($field, 'date') !== false) {
$field .= '_sql';
}
return $field;
}
protected function processValue($value)
{
// look for LUIS pre-built entity matches
foreach ($this->data->entities as $entity) {
if ($entity->entity === $value) {
if ($entity->type == 'builtin.datetime.date') {
$value = $entity->resolution->date;
$value = str_replace('XXXX', date('Y'), $value);
}
}
}
return $value;
}
protected function createResponse($type, $content)
{
$response = new SkypeResponse($type);
if (is_string($content)) {
$response->setText($content);
} else {
if ($content instanceof \Illuminate\Database\Eloquent\Collection) {
// do nothing
} elseif ( ! is_array($content)) {
$content = [$content];
}
foreach ($content as $item) {
$response->addAttachment($item);
}
}
return json_encode($response);
}
}

View File

@ -0,0 +1,44 @@
<?php namespace App\Ninja\Intents;
use Exception;
use App\Models\EntityModel;
class CreateInvoiceIntent extends InvoiceIntent
{
public function process()
{
$client = $this->requestClient();
$invoiceItems = $this->requestInvoiceItems();
if ( ! $client) {
throw new Exception(trans('texts.client_not_found'));
}
$data = array_merge($this->requestFields(), [
'client_id' => $client->id,
'invoice_items' => $invoiceItems,
]);
//var_dump($data);
$valid = EntityModel::validate($data, ENTITY_INVOICE);
if ($valid !== true) {
throw new Exception($valid);
}
$invoiceService = app('App\Services\InvoiceService');
$invoice = $invoiceService->save($data);
$invoiceItemIds = array_map(function($item) {
return $item['public_id'];
}, $invoice->invoice_items->toArray());
$this->setStateEntityType(ENTITY_INVOICE);
$this->setStateEntities(ENTITY_CLIENT, $client->public_id);
$this->setStateEntities(ENTITY_INVOICE, $invoice->public_id);
$this->setStateEntities(ENTITY_INVOICE_ITEM, $invoiceItemIds);
return $this->createResponse(SKYPE_CARD_RECEIPT, $invoice->present()->skypeBot);
}
}

View File

@ -0,0 +1,19 @@
<?php namespace App\Ninja\Intents;
use Auth;
use App\Models\EntityModel;
use App\Models\Invoice;
use App\Libraries\Skype\SkypeResponse;
class DownloadInvoiceIntent extends InvoiceIntent
{
public function process()
{
$invoice = $this->invoice();
$message = trans('texts.' . $invoice->getEntityType()) . ' ' . $invoice->invoice_number;
$message = link_to('/download/' . $invoice->invitations[0]->invitation_key, $message);
return SkypeResponse::message($message);
}
}

View File

@ -0,0 +1,32 @@
<?php namespace App\Ninja\Intents;
use Auth;
use Exception;
use App\Models\EntityModel;
use App\Models\Invoice;
use App\Libraries\Skype\SkypeResponse;
class EmailInvoiceIntent extends InvoiceIntent
{
public function process()
{
$invoice = $this->stateInvoice();
if ( ! Auth::user()->can('edit', $invoice)) {
throw new Exception(trans('texts.not_allowed'));
}
$contactMailer = app('App\Ninja\Mailers\ContactMailer');
$contactMailer->sendInvoice($invoice);
$message = trans('texts.bot_emailed_' . $invoice->getEntityType());
if (Auth::user()->notify_viewed) {
$message .= '<br/>' . trans('texts.bot_emailed_notify_viewed');
} elseif (Auth::user()->notify_paid) {
$message .= '<br/>' . trans('texts.bot_emailed_notify_paid');
}
return SkypeResponse::message($message);
}
}

View File

@ -0,0 +1,93 @@
<?php namespace App\Ninja\Intents;
use Auth;
use Exception;
use App\Models\Invoice;
class InvoiceIntent extends BaseIntent
{
protected $fieldMap = [
'deposit' => 'partial',
'due' => 'due_date',
];
public function __construct($state, $data)
{
$this->invoiceRepo = app('App\Ninja\Repositories\InvoiceRepository');
parent::__construct($state, $data);
}
protected function stateInvoice()
{
$invoiceId = $this->stateEntity(ENTITY_INVOICE);
if ( ! $invoiceId) {
throw new Exception(trans('texts.intent_not_supported'));
}
$invoice = Invoice::scope($invoiceId)->first();
if ( ! $invoice) {
throw new Exception(trans('texts.intent_not_supported'));
}
if ( ! Auth::user()->can('view', $invoice)) {
throw new Exception(trans('texts.not_allowed'));
}
return $invoice;
}
protected function requestInvoiceItems()
{
$productRepo = app('App\Ninja\Repositories\ProductRepository');
$invoiceItems = [];
if ( ! isset($this->data->compositeEntities) || ! count($this->data->compositeEntities)) {
return [];
}
foreach ($this->data->compositeEntities as $entity) {
if ($entity->parentType == 'InvoiceItem') {
$product = false;
$qty = 1;
foreach ($entity->children as $child) {
if ($child->type == 'Product') {
$product = $productRepo->findPhonetically($child->value);
} else {
$qty = $child->value;
}
}
if ($product) {
$item['qty'] = $qty;
$item['product_key'] = $product->product_key;
$item['cost'] = $product->cost;
$item['notes'] = $product->notes;
if ($taxRate = $product->default_tax_rate) {
$item['tax_name1'] = $taxRate->name;
$item['tax_rate1'] = $taxRate->rate;
}
$invoiceItems[] = $item;
}
}
}
/*
if ( ! count($invoiceItems)) {
foreach ($this->data->entities as $param) {
if ($param->type == 'Product') {
$product = $productRepo->findPhonetically($param->entity);
}
}
}
*/
return $invoiceItems;
}
}

View File

@ -0,0 +1,27 @@
<?php namespace App\Ninja\Intents;
use Auth;
use Exception;
use App\Models\Product;
class ListProductsIntent extends ProductIntent
{
public function process()
{
$account = Auth::user()->account;
$products = Product::scope()
->orderBy('product_key')
->limit(10)
->get()
->transform(function($item, $key) use ($account) {
$card = $item->present()->skypeBot($account);
if ($this->stateEntity(ENTITY_INVOICE)) {
$card->addButton('imBack', trans('texts.add_to_invoice'), trans('texts.add_product_to_invoice', ['product' => $item->product_key]));
}
return $card;
});
return $this->createResponse(SKYPE_CARD_CAROUSEL, $products);
}
}

View File

@ -0,0 +1,15 @@
<?php namespace App\Ninja\Intents;
use Auth;
use Exception;
class ProductIntent extends BaseIntent
{
public function __construct($state, $data)
{
$this->productRepo = app('App\Ninja\Repositories\ProductRepository');
parent::__construct($state, $data);
}
}

View File

@ -0,0 +1,54 @@
<?php namespace App\Ninja\Intents;
use Exception;
use App\Models\EntityModel;
use App\Models\Invoice;
class UpdateInvoiceIntent extends InvoiceIntent
{
public function process()
{
$invoice = $this->stateInvoice();
$invoiceItems = $this->requestInvoiceItems();
$data = array_merge($this->requestFields(), [
'public_id' => $invoice->public_id,
'invoice_items' => array_merge($invoice->invoice_items->toArray(), $invoiceItems),
]);
// map the cost and qty fields to the invoice items
if (isset($data['cost']) || isset($data['quantity'])) {
foreach ($data['invoice_items'] as $key => $item) {
// if it's new or we recently created it
if (empty($item['public_id']) || in_array($item['public_id'], $this->entities(ENTITY_INVOICE_ITEM))) {
$data['invoice_items'][$key]['cost'] = isset($data['cost']) ? $data['cost'] : $item['cost'];
$data['invoice_items'][$key]['qty'] = isset($data['quantity']) ? $data['quantity'] : $item['qty'];
}
}
}
//var_dump($data);
$valid = EntityModel::validate($data, ENTITY_INVOICE, $invoice);
if ($valid !== true) {
throw new Exception($valid);
}
$invoice = $this->invoiceRepo->save($data, $invoice);
$invoiceItems = array_slice($invoice->invoice_items->toArray(), count($invoiceItems) * -1);
$invoiceItemIds = array_map(function($item) {
return $item['public_id'];
}, $invoiceItems);
$this->setStateEntities(ENTITY_INVOICE_ITEM, $invoiceItemIds);
$response = $invoice
->load('invoice_items')
->present()
->skypeBot;
return $this->createResponse(SKYPE_CARD_RECEIPT, $response);
}
}

View File

@ -109,4 +109,20 @@ class UserMailer extends Mailer
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
public function sendSecurityCode($user, $code)
{
if (!$user->email) {
return;
}
$subject = trans('texts.security_code_email_subject');
$view = 'security_code';
$data = [
'userName' => $user->getDisplayName(),
'code' => $code,
];
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
}

View File

@ -44,6 +44,11 @@ class BasePaymentDriver
return $this->accountGateway->gateway_id == $gatewayId;
}
public function isValid()
{
return true;
}
// optionally pass a paymentMethod to determine the type from the token
protected function isGatewayType($gatewayType, $paymentMethod = false)
{
@ -535,6 +540,15 @@ class BasePaymentDriver
$paymentMethod->setRelation('account_gateway_token', $customer);
$paymentMethod = $this->creatingPaymentMethod($paymentMethod);
// archive the old payment method
$oldPaymentMethod = PaymentMethod::clientId($this->client()->id)
->wherePaymentTypeId($paymentMethod->payment_type_id)
->first();
if ($oldPaymentMethod) {
$oldPaymentMethod->delete();
}
if ($paymentMethod) {
$paymentMethod->save();
}
@ -753,7 +767,7 @@ class BasePaymentDriver
} elseif ($paymentMethod->payment_type_id == PAYMENT_TYPE_PAYPAL) {
$label = 'PayPal: ' . $paymentMethod->email;
} else {
$label = trans('texts.use_card_on_file');
$label = trans('texts.payment_type_on_file', ['type' => $paymentMethod->payment_type->name]);
}
$links[] = [

View File

@ -1,5 +1,7 @@
<?php namespace App\Ninja\PaymentDrivers;
use Exception;
class MolliePaymentDriver extends BasePaymentDriver
{
public function completeOffsitePurchase($input)
@ -10,6 +12,12 @@ class MolliePaymentDriver extends BasePaymentDriver
$response = $this->gateway()->fetchTransaction($details)->send();
if ($response->isCancelled()) {
return false;
} elseif ( ! $response->isSuccessful()) {
throw new Exception($response->getMessage());
}
return $this->createPayment($response->getTransactionReference());
}

View File

@ -39,6 +39,21 @@ class StripePaymentDriver extends BasePaymentDriver
return $rules;
}
public function isValid()
{
$result = $this->makeStripeCall(
'GET',
'charges',
'limit=1'
);
if (array_get($result, 'object') == 'list') {
return true;
} else {
return $result;
}
}
protected function checkCustomerExists($customer)
{
$response = $this->gateway()
@ -347,15 +362,15 @@ class StripePaymentDriver extends BasePaymentDriver
$eventDetails = $this->makeStripeCall('GET', 'events/'.$eventId);
if (is_string($eventDetails) || !$eventDetails) {
throw new Exception('Could not get event details');
return false;
}
if ($eventType != $eventDetails['type']) {
throw new Exception('Event type mismatch');
return false;
}
if (!$eventDetails['pending_webhooks']) {
throw new Exception('This is not a pending event');
return false;
}
if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded' || $eventType == 'charge.refunded') {
@ -365,7 +380,7 @@ class StripePaymentDriver extends BasePaymentDriver
$payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $transactionRef)->first();
if (!$payment) {
throw new Exception('Unknown payment');
return false;
}
if ($eventType == 'charge.failed') {

View File

@ -8,6 +8,22 @@ class ClientPresenter extends EntityPresenter {
return $this->entity->country ? $this->entity->country->name : '';
}
public function balance()
{
$client = $this->entity;
$account = $client->account;
return $account->formatMoney($client->balance, $client);
}
public function paid_to_date()
{
$client = $this->entity;
$account = $client->account;
return $account->formatMoney($client->paid_to_date, $client);
}
public function status()
{
$class = $text = '';

View File

@ -5,7 +5,6 @@ use Laracasts\Presenter\Presenter;
class EntityPresenter extends Presenter
{
/**
* @return string
*/

View File

@ -1,6 +1,7 @@
<?php namespace App\Ninja\Presenters;
use Utils;
use App\Libraries\Skype\InvoiceCard;
class InvoicePresenter extends EntityPresenter {
@ -14,6 +15,22 @@ class InvoicePresenter extends EntityPresenter {
return $this->entity->user->getDisplayName();
}
public function amount()
{
$invoice = $this->entity;
$account = $invoice->account;
return $account->formatMoney($invoice->amount, $invoice->client);
}
public function requestedAmount()
{
$invoice = $this->entity;
$account = $invoice->account;
return $account->formatMoney($invoice->getRequestedAmount(), $invoice->client);
}
public function balanceDueLabel()
{
if ($this->entity->partial > 0) {
@ -25,6 +42,26 @@ class InvoicePresenter extends EntityPresenter {
}
}
public function dueDateLabel()
{
if ($this->entity->isType(INVOICE_TYPE_STANDARD)) {
return trans('texts.due_date');
} else {
return trans('texts.valid_until');
}
}
public function discount()
{
$invoice = $this->entity;
if ($invoice->is_amount_discount) {
return $invoice->account->formatMoney($invoice->discount);
} else {
return $invoice->discount . '%';
}
}
// https://schema.org/PaymentStatusType
public function paymentStatus()
{
@ -99,4 +136,9 @@ class InvoicePresenter extends EntityPresenter {
return trans('texts.auto_bill_notification', $data);
}
public function skypeBot()
{
return new InvoiceCard($this->entity);
}
}

View File

@ -0,0 +1,20 @@
<?php namespace App\Ninja\Presenters;
use App\Libraries\Skype\HeroCard;
class ProductPresenter extends EntityPresenter
{
public function skypeBot($account)
{
$product = $this->entity;
$card = new HeroCard();
$card->setTitle($product->product_key);
$card->setSubitle($account->formatMoney($product->cost));
$card->setText($product->notes);
return $card;
}
}

View File

@ -132,4 +132,47 @@ class ClientRepository extends BaseRepository
return $client;
}
public function findPhonetically($clientName)
{
$clientNameMeta = metaphone($clientName);
$map = [];
$max = SIMILAR_MIN_THRESHOLD;
$clientId = 0;
$clients = Client::scope()->get(['id', 'name', 'public_id']);
foreach ($clients as $client) {
if ( ! $client->name) {
continue;
}
$map[$client->id] = $client;
$similar = similar_text($clientNameMeta, metaphone($client->name), $percent);
if ($percent > $max) {
$clientId = $client->id;
$max = $percent;
}
}
$contacts = Contact::scope()->get(['client_id', 'first_name', 'last_name', 'public_id']);
foreach ($contacts as $contact) {
if ( ! $contact->getFullName() || ! isset($map[$contact->client_id])) {
continue;
}
$similar = similar_text($clientNameMeta, metaphone($contact->getFullName()), $percent);
if ($percent > $max) {
$clientId = $contact->client_id;
$max = $percent;
}
}
return ($clientId && isset($map[$clientId])) ? $map[$clientId] : null;
}
}

View File

@ -262,6 +262,7 @@ class InvoiceRepository extends BaseRepository
if ($invoice) {
// do nothing
$entityType = $invoice->getEntityType();
} elseif ($isNew) {
$entityType = ENTITY_INVOICE;
if (isset($data['is_recurring']) && filter_var($data['is_recurring'], FILTER_VALIDATE_BOOLEAN)) {
@ -270,6 +271,7 @@ class InvoiceRepository extends BaseRepository
$entityType = ENTITY_QUOTE;
}
$invoice = $account->createInvoice($entityType, $data['client_id']);
$invoice->invoice_date = date_create()->format('Y-m-d');
if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) {
$invoice->has_tasks = true;
}

View File

@ -56,4 +56,34 @@ class ProductRepository extends BaseRepository
return $product;
}
public function findPhonetically($productName)
{
$productNameMeta = metaphone($productName);
$map = [];
$max = SIMILAR_MIN_THRESHOLD;
$productId = 0;
$products = Product::scope()
->with('default_tax_rate')
->get();
foreach ($products as $product) {
if ( ! $product->product_key) {
continue;
}
$map[$product->id] = $product;
$similar = similar_text($productNameMeta, metaphone($product->product_key), $percent);
if ($percent > $max) {
$productId = $product->id;
$max = $percent;
}
}
return ($productId && isset($map[$productId])) ? $map[$productId] : null;
}
}

View File

@ -40,7 +40,7 @@ class InvoiceTransformer extends EntityTransformer
public function includeInvoiceItems(Invoice $invoice)
{
$transformer = new InvoiceItemTransformer($this->account, $this->serializer);
return $this->includeCollection($invoice->invoice_items, $transformer, ENTITY_INVOICE_ITEMS);
return $this->includeCollection($invoice->invoice_items, $transformer, ENTITY_INVOICE_ITEM);
}
public function includeInvitations(Invoice $invoice)

View File

@ -29,4 +29,4 @@ class UserTransformer extends EntityTransformer
'permissions' => (int) $user->getOriginal('permissions'),
];
}
}
}

View File

@ -45,7 +45,7 @@ class EntityPolicy
* @param $ownerUserId
* @return bool
*/
public static function viewByOwner(User$user, $ownerUserId) {
public static function viewByOwner(User $user, $ownerUserId) {
return $user->hasPermission('view_all') || $user->id == $ownerUserId;
}

View File

@ -17,6 +17,11 @@ class ComposerServiceProvider extends ServiceProvider
['accounts.details', 'clients.edit', 'payments.edit', 'invoices.edit', 'accounts.localization'],
'App\Http\ViewComposers\TranslationComposer'
);
view()->composer(
['header', 'tasks.edit'],
'App\Http\ViewComposers\AppLanguageComposer'
);
}
/**
@ -28,4 +33,4 @@ class ComposerServiceProvider extends ServiceProvider
{
}
}
}

View File

@ -32,7 +32,7 @@ class BaseService
$entities = $this->getRepo()->findByPublicIdsWithTrashed($ids);
foreach ($entities as $entity) {
if(Auth::user()->can('edit', $entity)){
if (Auth::user()->can('edit', $entity)) {
$this->getRepo()->$action($entity);
}
}

View File

@ -9,7 +9,6 @@ use Auth;
use Utils;
use parsecsv;
use Session;
use Validator;
use League\Fractal\Manager;
use App\Ninja\Repositories\ContactRepository;
use App\Ninja\Repositories\ClientRepository;
@ -141,7 +140,7 @@ class ImportService
foreach ($json['clients'] as $jsonClient) {
if ($this->validate($jsonClient, ENTITY_CLIENT) === true) {
if (EntityModel::validate($jsonClient, ENTITY_CLIENT) === true) {
$client = $this->clientRepo->save($jsonClient);
$this->addSuccess($client);
} else {
@ -151,7 +150,7 @@ class ImportService
foreach ($jsonClient['invoices'] as $jsonInvoice) {
$jsonInvoice['client_id'] = $client->id;
if ($this->validate($jsonInvoice, ENTITY_INVOICE) === true) {
if (EntityModel::validate($jsonInvoice, ENTITY_INVOICE) === true) {
$invoice = $this->invoiceRepo->save($jsonInvoice);
$this->addSuccess($invoice);
} else {
@ -162,7 +161,7 @@ class ImportService
foreach ($jsonInvoice['payments'] as $jsonPayment) {
$jsonPayment['client_id'] = $jsonPayment['client'] = $client->id; // TODO: change to client_id once views are updated
$jsonPayment['invoice_id'] = $jsonPayment['invoice'] = $invoice->id; // TODO: change to invoice_id once views are updated
if ($this->validate($jsonPayment, ENTITY_PAYMENT) === true) {
if (EntityModel::validate($jsonPayment, ENTITY_PAYMENT) === true) {
$payment = $this->paymentRepo->save($jsonPayment);
$this->addSuccess($payment);
} else {
@ -280,7 +279,7 @@ class ImportService
$data['invoice_number'] = $account->getNextInvoiceNumber($invoice);
}
if ($this->validate($data, $entityType) !== true) {
if (EntityModel::validate($data, $entityType) !== true) {
return false;
}
@ -396,26 +395,6 @@ class ImportService
}
}
/**
* @param $data
* @param $entityType
* @return bool|string
*/
private function validate($data, $entityType)
{
$requestClass = 'App\\Http\\Requests\\Create' . ucwords($entityType) . 'Request';
$request = new $requestClass();
$request->setUserResolver(function() { return Auth::user(); });
$request->replace($data);
$validator = Validator::make($data, $request->rules());
if ($validator->fails()) {
return $validator->messages()->first();
} else {
return true;
}
}
/**
* @param array $files

View File

@ -57,6 +57,21 @@ class PaymentService extends BaseService
return false;
}
if ($credits = $client->credits->sum('balance')) {
$balance = $invoice->balance;
$amount = min($credits, $balance);
$data = [
'payment_type_id' => PAYMENT_TYPE_CREDIT,
'invoice_id' => $invoice->id,
'client_id' => $client->id,
'amount' => $amount,
];
$payment = $this->paymentRepo->save($data);
if ($amount == $balance) {
return $payment;
}
}
$paymentDriver = $account->paymentDriver($invitation, GATEWAY_TYPE_TOKEN);
if ( ! $paymentDriver) {

View File

@ -154,4 +154,4 @@ class PushService
else
return trans('texts.notification_invoice_viewed_subject', ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]);
}
}
}

View File

@ -4,6 +4,7 @@
"dependencies": {
"jquery": "1.11.3",
"bootstrap": "3.3.1",
"bootstrap-combobox": "1.1.5",
"jquery-ui": "1.11.2",
"datatables": "1.10.4",
"datatables-bootstrap3": "*",
@ -27,7 +28,8 @@
"datetimepicker": "~2.4.5",
"stacktrace-js": "~1.0.1",
"fuse.js": "~2.0.2",
"dropzone": "~4.3.0"
"dropzone": "~4.3.0",
"sweetalert": "~1.1.3"
},
"resolutions": {
"jquery": "~1.11"

View File

@ -13,6 +13,10 @@
}
],
"require": {
"php": ">=5.5.9",
"ext-mcrypt": "*",
"ext-gmp": "*",
"ext-gd": "*",
"turbo124/laravel-push-notification": "dev-laravel5",
"omnipay/mollie": "dev-master#22956c1a62a9662afa5f5d119723b413770ac525",
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",

View File

@ -17,16 +17,16 @@ class AddPageSize extends Migration
$table->boolean('live_preview')->default(true);
$table->smallInteger('invoice_number_padding')->default(4);
});
Schema::table('fonts', function ($table) {
$table->dropColumn('is_early_access');
});
Schema::create('expense_categories', function($table)
{
$table->increments('id');
$table->unsignedInteger('user_id');
$table->unsignedInteger('account_id')->index();
$table->unsignedInteger('account_id')->index();
$table->timestamps();
$table->softDeletes();
@ -34,15 +34,14 @@ class AddPageSize extends Migration
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->unsignedInteger('public_id')->index();
$table->unique( array('account_id','public_id') );
});
Schema::table('expenses', function ($table) {
$table->unsignedInteger('expense_category_id')->nullable()->index();
$table->foreign('expense_category_id')->references('id')->on('expense_categories')->onDelete('cascade');
//$table->foreign('expense_category_id')->references('id')->on('expense_categories')->onDelete('cascade');
});
}
@ -62,12 +61,12 @@ class AddPageSize extends Migration
Schema::table('fonts', function ($table) {
$table->boolean('is_early_access');
});
Schema::table('expenses', function ($table) {
$table->dropForeign('expenses_expense_category_id_foreign');
$table->dropColumn('expense_category_id');
});
Schema::dropIfExists('expense_categories');
}
}

View File

@ -88,7 +88,7 @@ class PaymentsChanges extends Migration
$table->string('email')->nullable();
$table->unsignedInteger('payment_method_id')->nullable();
$table->foreign('payment_method_id')->references('id')->on('payment_methods');
//$table->foreign('payment_method_id')->references('id')->on('payment_methods');
});
Schema::table('invoices', function($table)

View File

@ -0,0 +1,72 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSupportForBots extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('security_codes', function($table)
{
$table->increments('id');
$table->unsignedInteger('account_id')->index();
$table->unsignedInteger('user_id')->nullable();
$table->unsignedInteger('contact_id')->nullable();
$table->smallInteger('attempts');
$table->string('code')->nullable();
$table->string('bot_user_id')->unique();
$table->timestamp('created_at')->useCurrent();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade');
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
});
Schema::table('users', function($table)
{
$table->string('bot_user_id')->nullable();
});
Schema::table('contacts', function($table)
{
$table->string('bot_user_id')->nullable();
});
Schema::table('accounts', function($table)
{
$table->boolean('include_item_taxes_inline')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('security_codes');
Schema::table('users', function($table)
{
$table->dropColumn('bot_user_id');
});
Schema::table('contacts', function($table)
{
$table->dropColumn('bot_user_id');
});
Schema::table('accounts', function($table)
{
$table->dropColumn('include_item_taxes_inline');
});
}
}

View File

@ -55,7 +55,7 @@ class DateFormatsSeeder extends Seeder
];
foreach ($formats as $format) {
$record = DatetimeFormat::whereFormat($format['format'])->first();
$record = DatetimeFormat::whereRaw("BINARY `format`= ?", array($format['format']))->first();
if ($record) {
$record->format_moment = $format['format_moment'];
$record->save();

View File

@ -33,5 +33,5 @@ class PaymentStatusSeeder extends Seeder
PaymentStatus::create($status);
}
}
}
}
}

View File

@ -1,16 +1,96 @@
var elixir = require('laravel-elixir');
/*
|--------------------------------------------------------------------------
| Elixir Asset Management
|--------------------------------------------------------------------------
|
| Elixir provides a clean, fluent API for defining some basic Gulp tasks
| for your Laravel application. By default, we are compiling the Less
| file for our application, as well as publishing vendor resources.
|
/**
* Set Elixir Source Maps
*
* @type {boolean}
*/
elixir.config.sourcemaps = true;
/**
* Configuring assets path.
* Explicitly setting it to empty, as we're not using Laravels resources/assets folder
*
* @type {string}
*/
elixir.config.assetsPath = '';
/**
* Configuring Javascript assets path.
* Explicitly setting it to empty, as we're not using Laravels resources/assets/js folder
*
* @type {string}
*/
elixir.config.js.folder = '';
/**
* Configuring CSS assets path.
* Explicitly setting it to empty, as we're not using Laravels resources/assets/css folder
*
* @type {string}
*/
elixir.config.css.folder = '';
/**
* Remove all CSS comments
*
* @type {{discardComments: {removeAll: boolean}}}
*/
elixir.config.css.minifier.pluginOptions = {
discardComments: {
removeAll: true
}
};
/**
* Directory for bower source files.
* If changing this, please also see .bowerrc
*
* @type {string}
*/
var bowerDir = 'public/vendor';
elixir(function(mix) {
mix.less('app.less');
/**
* CSS configuration
*/
mix.styles([
bowerDir + '/bootstrap/dist/css/bootstrap.css',
bowerDir + '/font-awesome/css/font-awesome.css',
bowerDir + '/datatables/media/css/jquery.dataTables.css',
bowerDir + '/datatables-bootstrap3/BS3/assets/css/datatables.css',
'public/css/bootstrap-combobox.css',
'public/css/public.style.css'
], 'public/css/built.public.css');
mix.styles([
bowerDir + '/bootstrap/dist/css/bootstrap.css',
bowerDir + '/bootstrap-datepicker/dist/css/bootstrap-datepicker3.css',
bowerDir + '/datatables/media/css/jquery.dataTables.css',
bowerDir + '/datatables-bootstrap3/BS3/assets/css/datatables.css',
bowerDir + '/font-awesome/css/font-awesome.css',
bowerDir + '/dropzone/dist/dropzone.css',
bowerDir + '/spectrum/spectrum.css',
bowerDir + '/sweetalert/dist/sweetalert.css',
'public/css/bootstrap-combobox.css',
'public/css/typeahead.js-bootstrap.css',
'public/css/style.css'
], 'public/css/built.css');
/**
* JS configuration
*/
mix.scripts(['resources/assets/js/Chart.js'], 'public/js/Chart.min.js')
.scripts(['resources/assets/js/d3.js'], 'public/js/d3.min.js');
mix.scripts([
'public/js/pdf_viewer.js',
'public/js/compatibility.js',
//'public/js/pdfmake.min.js',
'public/js/pdfmake.js',
'public/js/vfs.js'
], 'public/pdf.built.js');
});

View File

@ -8,7 +8,7 @@
"grunt-contrib-uglify": "~0.2.2",
"grunt-dump-dir": "^0.1.2",
"gulp": "^3.8.8",
"laravel-elixir": "*"
"laravel-elixir": "^6.0.0"
},
"dependencies": {
"grunt-dump-dir": "^0.1.2"

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

1
public/built.js.map Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
public/css/app.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3235
public/css/built.css vendored

File diff suppressed because one or more lines are too long

1
public/css/built.css.map Normal file

File diff suppressed because one or more lines are too long

21
public/css/built.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

14
public/css/built.public.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/css/customCss.min.css vendored Normal file
View File

@ -0,0 +1,2 @@
.customContainer{padding:40px 0;margin:0!important;background:-webkit-linear-gradient(#f5f5f5,#fff)}.customFontHead{font-size:20px;text-align:center}.customTextBorder{border-bottom:1px solid #c3c1c1;padding-bottom:5%;margin-bottom:5%}.customSubMenu{margin-left:2%}.customMenuOne{padding-left:5px;padding-right:5px}.shiftLeft{float:left}.customMenuDiv{padding-bottom:30px;float:left;width:100%}
/*# sourceMappingURL=customCss.min.css.map */

View File

@ -0,0 +1 @@
{"version":3,"sources":["customCss.css"],"names":[],"mappings":"AAAA,iBACA,eAAA,AACA,mBAAA,AACA,gDAAA,CACA,AACA,gBACA,eAAA,AACA,iBAAA,CACA,AACA,kBACA,gCAAA,AACA,kBAAA,AACA,gBAAA,CACA,AACA,eACA,cAAA,CACA,AACA,eACA,iBAAA,AACA,iBAAA,CACA,AACA,WACA,UAAA,CACA,AACA,eACA,oBAAA,AACA,WAAA,AACA,UAAA,CACA","file":"customCss.min.css","sourcesContent":[".customContainer{\n\tpadding: 40px 0;\n\tmargin: 0px 0 !important;\n\tbackground: -webkit-linear-gradient(rgb(245, 245, 245), white);\n}\t\n.customFontHead{\n\tfont-size: 20px;\n\ttext-align: center;\n}\t\n.customTextBorder{\n\tborder-bottom: 1px solid rgb(195, 193, 193);\n\tpadding-bottom: 5%;\n\tmargin-bottom: 5%;\n}\n.customSubMenu{\n\tmargin-left: 2%;\n}\n.customMenuOne{\n\tpadding-left: 5px;\n\tpadding-right: 5px;\n}\n.shiftLeft{\n\tfloat: left;\n}\n.customMenuDiv{\n\tpadding-bottom: 30px;\n\tfloat: left;\n\twidth: 100%;\n}"],"sourceRoot":"/source/"}

View File

@ -0,0 +1,893 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="216"
height="144"
id="svg4136"
version="1.1"
inkscape:version="0.91 r"
sodipodi:docname="jsoneditor-icons.svg">
<title
id="title6512">JSON Editor Icons</title>
<metadata
id="metadata4148">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>JSON Editor Icons</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4146" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1028"
id="namedview4144"
showgrid="true"
inkscape:zoom="4"
inkscape:cx="97.217248"
inkscape:cy="59.950227"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4136"
showguides="false"
borderlayer="false"
inkscape:showpageshadow="true"
showborder="true">
<inkscape:grid
type="xygrid"
id="grid4640"
empspacing="24" />
</sodipodi:namedview>
<!-- Created with SVG-edit - http://svg-edit.googlecode.com/ -->
<g
id="g4394">
<rect
x="4"
y="4"
width="16"
height="16"
id="svg_1"
style="fill:#1aae1c;fill-opacity:1;stroke:none;stroke-width:0" />
<rect
style="fill:#ec3f29;fill-opacity:0.94117647;stroke:none;stroke-width:0"
x="28.000006"
y="3.999995"
width="16"
height="16"
id="svg_1-7" />
<rect
id="rect4165"
height="16"
width="16"
y="3.999995"
x="52.000004"
style="fill:#4c4c4c;fill-opacity:1;stroke:none;stroke-width:0" />
<rect
style="fill:#4c4c4c;fill-opacity:1;stroke:none;stroke-width:0"
x="172.00002"
y="3.9999852"
width="16"
height="16"
id="rect4175" />
<rect
style="fill:#4c4c4c;fill-opacity:1;stroke:none;stroke-width:0"
x="196"
y="3.999995"
width="16"
height="16"
id="rect4175-3" />
<g
style="stroke:none"
id="g4299">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1-1"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<g
style="stroke:none"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,19.029435,12.000001)"
id="g4299-3">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1-0"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1-1-9"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="55.000004"
y="7.0000048"
width="6.9999909"
height="6.9999905"
id="svg_1-7-5" />
<rect
id="rect4354"
height="6.9999905"
width="6.9999909"
y="10.00001"
x="58"
style="fill:#ffffff;fill-opacity:1;stroke:#4c4c4c;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#3c80df;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.94117647"
x="58.000004"
y="10.000005"
width="6.9999909"
height="6.9999905"
id="svg_1-7-5-7" />
<g
id="g4378">
<rect
id="svg_1-7-5-3"
height="1.9999965"
width="7.9999909"
y="10.999999"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="7.0000005"
width="11.999995"
height="1.9999946"
id="rect4374" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="14.999996"
width="3.9999928"
height="1.9999995"
id="rect4376" />
</g>
<g
id="g4383"
transform="matrix(1,0,0,-1,-23.999995,23.999995)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="10.999999"
width="7.9999909"
height="1.9999965"
id="rect4385" />
<rect
id="rect4387"
height="1.9999946"
width="11.999995"
y="7.0000005"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
id="rect4389"
height="1.9999995"
width="3.9999928"
y="14.999996"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
</g>
<rect
y="3.9999199"
x="76"
height="16"
width="16"
id="rect3754-4"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="cccccccc"
inkscape:connector-curvature="0"
id="path4351"
d="m 85.10447,6.0157384 -0.0156,1.4063 c 3.02669,-0.2402 0.33008,3.6507996 2.48438,4.5780996 -2.18694,1.0938 0.49191,4.9069 -2.45313,4.5781 l -0.0156,1.4219 c 5.70828,0.559 1.03264,-5.1005 4.70313,-5.2656 l 0,-1.4063 c -3.61303,-0.027 1.11893,-5.7069996 -4.70313,-5.3124996 z"
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccccccc"
inkscape:connector-curvature="0"
id="path4351-9"
d="m 82.78125,5.9984384 0.0156,1.4063 c -3.02668,-0.2402 -0.33007,3.6506996 -2.48437,4.5780996 2.18694,1.0938 -0.49192,4.9069 2.45312,4.5781 l 0.0156,1.4219 c -5.70827,0.559 -1.03263,-5.1004 -4.70312,-5.2656 l 0,-1.4063 c 3.61303,-0.027 -1.11894,-5.7070996 4.70312,-5.3124996 z"
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="3.9999199"
x="100"
height="16"
width="16"
id="rect3754-25"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path2987"
d="m 103.719,5.6719384 0,12.7187996 3.03125,0 0,-1.5313 -1.34375,0 0,-9.6249996 1.375,0 0,-1.5625 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path2987-1"
d="m 112.2185,5.6721984 0,12.7187996 -3.03125,0 0,-1.5313 1.34375,0 0,-9.6249996 -1.375,0 0,-1.5625 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<rect
y="3.9999199"
x="124"
height="16"
width="16"
id="rect3754-73"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="ccccccccc"
inkscape:connector-curvature="0"
id="path3780"
d="m 126.2824,17.602938 1.78957,0 1.14143,-2.8641 5.65364,0 1.14856,2.8641 1.76565,0 -4.78687,-11.1610996 -1.91903,0 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path3782"
d="m 129.72704,13.478838 4.60852,0.01 -2.30426,-5.5497996 z"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<rect
y="3.9999199"
x="148"
height="16"
width="16"
id="rect3754-35"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="ccccccc"
inkscape:connector-curvature="0"
id="path5008-2"
d="m 156.47655,5.8917384 0,2.1797 0.46093,2.3983996 1.82813,0 0.39844,-2.3983996 0,-2.1797 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="ccccccc"
inkscape:connector-curvature="0"
id="path5008-2-8"
d="m 152.51561,5.8906384 0,2.1797 0.46094,2.3983996 1.82812,0 0.39844,-2.3983996 0,-2.1797 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
</g>
<rect
x="4"
y="27.999994"
width="16"
height="16"
id="rect4432"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0"
x="28.000006"
y="27.99999"
width="16"
height="16"
id="rect4434" />
<rect
id="rect4436"
height="16"
width="16"
y="27.99999"
x="52.000004"
style="fill:#d3d3d3;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
style="fill:#d3d3d3;stroke:#000000;stroke-width:0"
x="172.00002"
y="27.999981"
width="16"
height="16"
id="rect4446" />
<rect
style="fill:#d3d3d3;stroke:#000000;stroke-width:0"
x="196"
y="27.99999"
width="16"
height="16"
id="rect4448" />
<g
id="g4466"
style="stroke:none"
transform="translate(0,23.999995)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4468"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4470"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<g
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,19.029435,35.999996)"
id="g4472"
style="stroke:none">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4474"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4476"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="55.000004"
y="31"
width="6.9999909"
height="6.9999905"
id="rect4478" />
<rect
id="rect4480"
height="6.9999905"
width="6.9999909"
y="34.000008"
x="58"
style="fill:#ffffff;fill-opacity:1;stroke:#d3d3d3;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#d3d3d3;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none"
x="58.000004"
y="34.000004"
width="6.9999909"
height="6.9999905"
id="rect4482" />
<g
id="g4484"
transform="translate(0,23.999995)">
<rect
id="rect4486"
height="1.9999965"
width="7.9999909"
y="10.999999"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="7.0000005"
width="11.999995"
height="1.9999946"
id="rect4488" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="14.999996"
width="3.9999928"
height="1.9999995"
id="rect4490" />
</g>
<g
id="g4492"
transform="matrix(1,0,0,-1,-23.999995,47.99999)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="10.999999"
width="7.9999909"
height="1.9999965"
id="rect4494" />
<rect
id="rect4496"
height="1.9999946"
width="11.999995"
y="7.0000005"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
id="rect4498"
height="1.9999995"
width="3.9999928"
y="14.999996"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
</g>
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-8"
width="16"
height="16"
x="76"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 85.10448,30.015537 -0.0156,1.4063 c 3.02668,-0.2402 0.33007,3.6508 2.48438,4.5781 -2.18695,1.0938 0.49191,4.90688 -2.45313,4.57808 l -0.0156,1.4219 c 5.70827,0.559 1.03263,-5.10048 4.70313,-5.26558 l 0,-1.4063 c -3.61304,-0.027 1.11893,-5.707 -4.70313,-5.3125 z"
id="path4351-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 82.78126,29.998237 0.0156,1.4063 c -3.02668,-0.2402 -0.33008,3.6507 -2.48438,4.5781 2.18694,1.0938 -0.49191,4.90688 2.45313,4.57808 l 0.0156,1.4219 c -5.70828,0.559 -1.03264,-5.10038 -4.70313,-5.26558 l 0,-1.4063 c 3.61303,-0.027 -1.11893,-5.7071 4.70313,-5.3125 z"
id="path4351-9-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-65"
width="16"
height="16"
x="100"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 103.719,29.671937 0,12.71878 3.03125,0 0,-1.5313 -1.34375,0 0,-9.62498 1.375,0 0,-1.5625 z"
id="path2987-8"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 112.2185,29.671937 0,12.71878 -3.03125,0 0,-1.5313 1.34375,0 0,-9.62498 -1.375,0 0,-1.5625 z"
id="path2987-1-9"
inkscape:connector-curvature="0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-92"
width="16"
height="16"
x="124"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 126.2824,41.602917 1.78957,0 1.14143,-2.86408 5.65364,0 1.14856,2.86408 1.76565,0 -4.78687,-11.16108 -1.91902,0 z"
id="path3780-9"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
d="m 129.72704,37.478837 4.60852,0.01 -2.30426,-5.5498 z"
id="path3782-2"
inkscape:connector-curvature="0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-47"
width="16"
height="16"
x="148"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 156.47656,29.891737 0,2.1797 0.46093,2.3984 1.82813,0 0.39844,-2.3984 0,-2.1797 z"
id="path5008-2-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 152.51562,29.890637 0,2.1797 0.46094,2.3984 1.82812,0 0.39844,-2.3984 0,-2.1797 z"
id="path5008-2-8-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<rect
id="svg_1-7-2"
height="1.9999961"
width="11.999996"
y="64"
x="54"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
id="svg_1-7-2-2"
height="2.9999905"
width="2.9999907"
y="52"
x="80.000008"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="85.000008"
y="52"
width="2.9999907"
height="2.9999905"
id="rect4561" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="80.000008"
y="58"
width="2.9999907"
height="2.9999905"
id="rect4563" />
<rect
id="rect4565"
height="2.9999905"
width="2.9999907"
y="58"
x="85.000008"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
id="rect4567"
height="2.9999905"
width="2.9999907"
y="64"
x="80.000008"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="85.000008"
y="64"
width="2.9999907"
height="2.9999905"
id="rect4569" />
<circle
style="opacity:1;fill:none;fill-opacity:1;stroke:#4c4c4c;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="path4571"
cx="110.06081"
cy="57.939209"
r="4.7438836" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="116.64566"
y="-31.79752"
width="4.229713"
height="6.4053884"
id="rect4563-2"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<path
style="fill:#4c4c4c;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 125,56 138.77027,56.095 132,64 Z"
id="path4613"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path4615"
d="M 149,64 162.77027,63.905 156,56 Z"
style="fill:#4c4c4c;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="54"
y="53"
width="11.999996"
height="1.9999961"
id="rect4638" />
<rect
id="svg_1-7-2-24"
height="1.9999957"
width="12.99999"
y="-56"
x="53"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
transform="matrix(0,1,-1,0,0,0)" />
<rect
transform="matrix(0,1,-1,0,0,0)"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="53"
y="-66"
width="12.99999"
height="1.9999957"
id="rect4657" />
<rect
id="rect4659"
height="0.99999291"
width="11.999999"
y="57"
x="54"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="54"
y="88.000122"
width="11.999996"
height="1.9999961"
id="rect4661" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="80.000008"
y="76.000122"
width="2.9999907"
height="2.9999905"
id="rect4663" />
<rect
id="rect4665"
height="2.9999905"
width="2.9999907"
y="76.000122"
x="85.000008"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<rect
id="rect4667"
height="2.9999905"
width="2.9999907"
y="82.000122"
x="80.000008"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="85.000008"
y="82.000122"
width="2.9999907"
height="2.9999905"
id="rect4669" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="80.000008"
y="88.000122"
width="2.9999907"
height="2.9999905"
id="rect4671" />
<rect
id="rect4673"
height="2.9999905"
width="2.9999907"
y="88.000122"
x="85.000008"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<circle
r="4.7438836"
cy="81.939331"
cx="110.06081"
id="circle4675"
style="opacity:1;fill:none;fill-opacity:1;stroke:#d3d3d3;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
id="rect4677"
height="6.4053884"
width="4.229713"
y="-14.826816"
x="133.6163"
style="fill:#d3d3d3;fill-opacity:1;stroke:#d3d3d3;stroke-width:0;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path4679"
d="m 125,80.000005 13.77027,0.09499 L 132,87.999992 Z"
style="fill:#d3d3d3;fill-opacity:1;fill-rule:evenodd;stroke:#d3d3d3;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:#d3d3d3;fill-opacity:1;fill-rule:evenodd;stroke:#d3d3d3;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 149,88.0002 162.77027,87.9052 156,80.0002 Z"
id="path4681"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<rect
id="rect4683"
height="1.9999961"
width="11.999996"
y="77.000122"
x="54"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<rect
transform="matrix(0,1,-1,0,0,0)"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="77.000122"
y="-56"
width="12.99999"
height="1.9999957"
id="rect4685" />
<rect
id="rect4687"
height="1.9999957"
width="12.99999"
y="-66"
x="77.000122"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
transform="matrix(0,1,-1,0,0,0)" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="54"
y="81.000122"
width="11.999999"
height="0.99999291"
id="rect4689" />
<rect
id="rect4761-1"
height="1.9999945"
width="15.99999"
y="101"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-0"
height="1.9999945"
width="15.99999"
y="105"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-7"
height="1.9999945"
width="9"
y="109"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1"
height="1.9999945"
width="12"
y="125"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1-4"
height="1.9999945"
width="10"
y="137"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1-4-4"
height="1.9999945"
width="10"
y="129"
x="82"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1-4-4-3"
height="1.9999945"
width="9"
y="133"
x="82"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.8;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 36.398438,100.0254 c -0.423362,-0.013 -0.846847,0.01 -1.265626,0.062 -1.656562,0.2196 -3.244567,0.9739 -4.507812,2.2266 L 29,100.5991 l -2.324219,7.7129 7.826172,-1.9062 -1.804687,-1.9063 c 1.597702,-1.5308 4.048706,-1.8453 5.984375,-0.7207 1.971162,1.1452 2.881954,3.3975 2.308593,5.5508 -0.573361,2.1533 -2.533865,3.6953 -4.830078,3.6953 l 0,3.0742 c 3.550756,0 6.710442,-2.4113 7.650391,-5.9414 0.939949,-3.5301 -0.618463,-7.2736 -3.710938,-9.0703 -1.159678,-0.6738 -2.431087,-1.0231 -3.701171,-1.0625 z"
id="path4138" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.8;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 59.722656,99.9629 c -1.270084,0.039 -2.541493,0.3887 -3.701172,1.0625 -3.092475,1.7967 -4.650886,5.5402 -3.710937,9.0703 0.939949,3.5301 4.09768,5.9414 7.648437,5.9414 l 0,-3.0742 c -2.296214,0 -4.256717,-1.542 -4.830078,-3.6953 -0.573361,-2.1533 0.337432,-4.4056 2.308594,-5.5508 1.935731,-1.1246 4.38863,-0.8102 5.986326,0.7207 l -1.806638,1.9063 7.828128,1.9062 -2.32422,-7.7129 -1.62696,1.7168 c -1.26338,-1.2531 -2.848917,-2.0088 -4.505855,-2.2285 -0.418778,-0.055 -0.842263,-0.076 -1.265625,-0.062 z"
id="path4138-1" />
<path
inkscape:connector-curvature="0"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
d="m 10.5,100 0,2 -2.4999996,0 L 12,107 l 4,-5 -2.5,0 0,-2 -3,0 z"
id="path3055-0-77" />
<path
style="opacity:0.8;fill:none;stroke:#ffffff;stroke-width:1.966;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 4.9850574,108.015 14.0298856,-0.03"
id="path5244-5-0-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="opacity:0.8;fill:none;stroke:#ffffff;stroke-width:1.966;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 4.9849874,132.015 14.0298866,-0.03"
id="path5244-5-0-5-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.4;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#4d4d4d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 36.398438,123.9629 c -0.423362,-0.013 -0.846847,0.01 -1.265626,0.062 -1.656562,0.2196 -3.244567,0.9739 -4.507812,2.2266 L 29,124.5366 l -2.324219,7.7129 7.826172,-1.9062 -1.804687,-1.9063 c 1.597702,-1.5308 4.048706,-1.8453 5.984375,-0.7207 1.971162,1.1453 2.881954,3.3975 2.308593,5.5508 -0.573361,2.1533 -2.533864,3.6953 -4.830078,3.6953 l 0,3.0742 c 3.550757,0 6.710442,-2.4093 7.650391,-5.9394 0.939949,-3.5301 -0.618463,-7.2756 -3.710938,-9.0723 -1.159678,-0.6737 -2.431087,-1.0231 -3.701171,-1.0625 z"
id="path4138-12" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.4;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#4d4d4d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 59.722656,123.9629 c -1.270084,0.039 -2.541493,0.3888 -3.701172,1.0625 -3.092475,1.7967 -4.650886,5.5422 -3.710937,9.0723 0.939949,3.5301 4.09768,5.9394 7.648437,5.9394 l 0,-3.0742 c -2.296214,0 -4.256717,-1.542 -4.830078,-3.6953 -0.573361,-2.1533 0.337432,-4.4055 2.308594,-5.5508 1.935731,-1.1246 4.38863,-0.8102 5.986326,0.7207 l -1.806638,1.9063 7.828128,1.9062 -2.32422,-7.7129 -1.62696,1.7168 c -1.26338,-1.2531 -2.848917,-2.0088 -4.505855,-2.2285 -0.418778,-0.055 -0.842263,-0.076 -1.265625,-0.062 z"
id="path4138-1-3" />
<path
id="path6191"
d="m 10.5,116 0,-2 -2.4999996,0 L 12,109 l 4,5 -2.5,0 0,2 -3,0 z"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
d="m 10.5,129 0,-2 -2.4999996,0 L 12,122 l 4,5 -2.5,0 0,2 -3,0 z"
id="path6193" />
<path
id="path6195"
d="m 10.5,135 0,2 -2.4999996,0 L 12,142 l 4,-5 -2.5,0 0,-2 -3,0 z"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0" />
<path
sodipodi:type="star"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="path4500"
sodipodi:sides="3"
sodipodi:cx="11.55581"
sodipodi:cy="60.073242"
sodipodi:r1="5.1116104"
sodipodi:r2="2.5558052"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 16.66742,60.073242 -3.833708,2.213392 -3.8337072,2.213393 0,-4.426785 0,-4.426784 3.8337082,2.213392 z"
inkscape:transform-center-x="-1.2779026" />
<path
inkscape:transform-center-x="1.277902"
d="m -31.500004,60.073242 -3.833708,2.213392 -3.833707,2.213393 0,-4.426785 0,-4.426784 3.833707,2.213392 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="false"
sodipodi:arg2="1.0471976"
sodipodi:arg1="0"
sodipodi:r2="2.5558052"
sodipodi:r1="5.1116104"
sodipodi:cy="60.073242"
sodipodi:cx="-36.611614"
sodipodi:sides="3"
id="path4502"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="star"
transform="scale(-1,1)" />
<path
d="m 16.66742,60.073212 -3.833708,2.213392 -3.8337072,2.213392 0,-4.426784 0,-4.426785 3.8337082,2.213392 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="false"
sodipodi:arg2="1.0471976"
sodipodi:arg1="0"
sodipodi:r2="2.5558052"
sodipodi:r1="5.1116104"
sodipodi:cy="60.073212"
sodipodi:cx="11.55581"
sodipodi:sides="3"
id="path4504"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="star"
transform="matrix(0,1,-1,0,72.0074,71.7877)"
inkscape:transform-center-y="1.2779029" />
<path
inkscape:transform-center-y="-1.2779026"
transform="matrix(0,-1,-1,0,96,96)"
sodipodi:type="star"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="path4506"
sodipodi:sides="3"
sodipodi:cx="11.55581"
sodipodi:cy="60.073212"
sodipodi:r1="5.1116104"
sodipodi:r2="2.5558052"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 16.66742,60.073212 -3.833708,2.213392 -3.8337072,2.213392 0,-4.426784 0,-4.426785 3.8337082,2.213392 z" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path4615-5"
d="m 171.82574,65.174193 16.34854,0 -8.17427,-13.348454 z"
style="fill:#fbb917;fill-opacity:1;fill-rule:evenodd;stroke:#fbb917;stroke-width:1.65161395;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 179,55 0,6 2,0 0,-6"
id="path4300"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 179,62 0,2 2,0 0,-2"
id="path4300-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
</svg>

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/css/lightbox.min.css vendored Normal file
View File

@ -0,0 +1,2 @@
body:after{content:url(../images/close.png) url(../images/loading.gif) url(../images/prev.png) url(../images/next.png);display:none}body.lb-disable-scrolling{overflow:hidden}.lightboxOverlay{position:absolute;top:0;left:0;z-index:9999;background-color:#000;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);opacity:.8;display:none}.lightbox{position:absolute;left:0;width:100%;z-index:10000;text-align:center;line-height:0;font-weight:400}.lightbox .lb-image{display:block;height:auto;max-width:inherit;border-radius:3px}.lightbox a img{border:none}.lb-outerContainer{position:relative;background-color:#fff;*zoom:1;width:250px;height:250px;margin:0 auto;border-radius:4px}.lb-outerContainer:after{content:"";display:table;clear:both}.lb-container{padding:4px}.lb-loader{position:absolute;top:43%;left:0;height:25%;width:100%;text-align:center;line-height:0}.lb-cancel{display:block;width:32px;height:32px;margin:0 auto;background:url(../images/loading.gif) no-repeat}.lb-nav{position:absolute;top:0;left:0;height:100%;width:100%;z-index:10}.lb-container>.nav{left:0}.lb-nav a{outline:none;background-image:url('data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==')}.lb-next,.lb-prev{height:100%;cursor:pointer;display:block}.lb-nav a.lb-prev{width:34%;left:0;float:left;background:url(../images/prev.png) left 48% no-repeat;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=0);opacity:0;-webkit-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-prev:hover{filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);opacity:1}.lb-nav a.lb-next{width:64%;right:0;float:right;background:url(../images/next.png) right 48% no-repeat;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=0);opacity:0;-webkit-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-next:hover{filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);opacity:1}.lb-dataContainer{margin:0 auto;padding-top:5px;*zoom:1;width:100%;border-bottom-left-radius:4px;border-bottom-right-radius:4px}.lb-dataContainer:after{content:"";display:table;clear:both}.lb-data{padding:0 4px;color:#ccc}.lb-data .lb-details{width:85%;float:left;text-align:left;line-height:1.1em}.lb-data .lb-caption{font-size:13px;font-weight:700;line-height:1em}.lb-data .lb-number{display:block;clear:left;padding-bottom:1em;font-size:12px;color:#999}.lb-data .lb-close{display:block;float:right;width:30px;height:30px;background:url(../images/close.png) 100% 0 no-repeat;text-align:right;outline:none;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=70);opacity:.7;-webkit-transition:opacity .2s;transition:opacity .2s}.lb-data .lb-close:hover{cursor:pointer;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);opacity:1}
/*# sourceMappingURL=lightbox.min.css.map */

File diff suppressed because one or more lines are too long

6
public/css/quill.snow.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/css/style.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More