diff --git a/.gitignore b/.gitignore index f3b06ceeb9..eba40da49d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ Thumbs.db /.project tests/_output/ tests/_bootstrap.php +tests/_support/_generated/ # composer stuff /c3.php diff --git a/.travis.yml b/.travis.yml index 6bd3c485a8..727f351d7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,6 +65,7 @@ before_script: - sleep 5 # Make sure the app is up-to-date - curl -L http://ninja.dev:8000/update + #- php artisan ninja:create-test-data 25 script: - php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 6100d090e1..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,36 +0,0 @@ -# Changelog -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 - -### Fixed -- Setting default tax rate breaks invoice creation #974 - - -## [2.6] - 2016-07-12 - -### Added -- Configuration for first day of the week #950 -- StyleCI configuration #929 -- Added expense category - -### Changed -- Removed `invoiceninja.komodoproject` from Git #932 -- `APP_CIPHER` changed from `rinjdael-128` to `AES-256-CBC` #898 -- Improved options when exporting data - -### Fixed -- "Manual entry" untranslatable #562 -- Using a database table prefix breaks the dashboard #203 -- Request statically called in StartupCheck.php #977 diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index d81bf5ff6e..ae37a429f4 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -1,6 +1,7 @@ info(date('Y-m-d') . ' Running CheckData...'); + $this->logMessage(date('Y-m-d') . ' Running CheckData...'); if (!$this->option('client_id')) { $this->checkPaidToDate(); @@ -66,7 +70,21 @@ class CheckData extends Command { $this->checkAccountData(); } - $this->info('Done'); + $this->logMessage('Done'); + $errorEmail = env('ERROR_EMAIL'); + + if ( ! $this->isValid && $errorEmail) { + Mail::raw($this->log, function ($message) use ($errorEmail) { + $message->to($errorEmail) + ->from(CONTACT_EMAIL) + ->subject('Check-Data'); + }); + } + } + + private function logMessage($str) + { + $this->log .= $str . "\n"; } private function checkBlankInvoiceHistory() @@ -75,9 +93,14 @@ class CheckData extends Command { ->where('activity_type_id', '=', 5) ->where('json_backup', '=', '') ->whereNotIn('id', [634386, 756352, 756353, 756356]) + ->whereNotIn('id', [634386, 756352, 756353, 756356, 820872]) ->count(); - $this->info($count . ' activities with blank invoice backup'); + if ($count > 0) { + $this->isValid = false; + } + + $this->logMessage($count . ' activities with blank invoice backup'); } private function checkAccountData() @@ -132,7 +155,8 @@ class CheckData extends Command { ->get(["{$table}.id", 'clients.account_id', 'clients.user_id']); if (count($records)) { - $this->info(count($records) . " {$table} records with incorrect {$entityType} account id"); + $this->isValid = false; + $this->logMessage(count($records) . " {$table} records with incorrect {$entityType} account id"); if ($this->option('fix') == 'true') { foreach ($records as $record) { @@ -162,7 +186,11 @@ class CheckData extends Command { ->groupBy('clients.id') ->havingRaw('clients.paid_to_date != sum(payments.amount - payments.refunded) and clients.paid_to_date != 999999999.9999') ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); - $this->info(count($clients) . ' clients with incorrect paid to date'); + $this->logMessage(count($clients) . ' clients with incorrect paid to date'); + + if (count($clients) > 0) { + $this->isValid = false; + } if ($this->option('fix') == 'true') { foreach ($clients as $client) { @@ -179,6 +207,7 @@ class CheckData extends Command { $clients = DB::table('clients') ->join('invoices', 'invoices.client_id', '=', 'clients.id') ->join('accounts', 'accounts.id', '=', 'clients.account_id') + ->where('accounts.id', '!=', 20432) ->where('clients.is_deleted', '=', 0) ->where('invoices.is_deleted', '=', 0) ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD) @@ -192,10 +221,14 @@ class CheckData extends Command { $clients = $clients->groupBy('clients.id', 'clients.balance', 'clients.created_at') ->orderBy('accounts.company_id', 'DESC') ->get(['accounts.company_id', 'clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]); - $this->info(count($clients) . ' clients with incorrect balance/activities'); + $this->logMessage(count($clients) . ' clients with incorrect balance/activities'); + + if (count($clients) > 0) { + $this->isValid = false; + } foreach ($clients as $client) { - $this->info("=== Company: {$client->company_id} Account:{$client->account_id} Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ==="); + $this->logMessage("=== Company: {$client->company_id} Account:{$client->account_id} Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ==="); $foundProblem = false; $lastBalance = 0; $lastAdjustment = 0; @@ -205,7 +238,7 @@ class CheckData extends Command { ->where('client_id', '=', $client->id) ->orderBy('activities.id') ->get(['activities.id', 'activities.created_at', 'activities.activity_type_id', 'activities.adjustment', 'activities.balance', 'activities.invoice_id']); - //$this->info(var_dump($activities)); + //$this->logMessage(var_dump($activities)); foreach ($activities as $activity) { @@ -252,19 +285,19 @@ class CheckData extends Command { // **Fix for ninja invoices which didn't have the invoice_type_id value set if ($noAdjustment && $client->account_id == 20432) { - $this->info("No adjustment for ninja invoice"); + $this->logMessage("No adjustment for ninja invoice"); $foundProblem = true; $clientFix += $invoice->amount; $activityFix = $invoice->amount; // **Fix for allowing converting a recurring invoice to a normal one without updating the balance** } elseif ($noAdjustment && $invoice->invoice_type_id == INVOICE_TYPE_STANDARD && !$invoice->is_recurring) { - $this->info("No adjustment for new invoice:{$activity->invoice_id} amount:{$invoice->amount} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}"); + $this->logMessage("No adjustment for new invoice:{$activity->invoice_id} amount:{$invoice->amount} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}"); $foundProblem = true; $clientFix += $invoice->amount; $activityFix = $invoice->amount; // **Fix for updating balance when creating a quote or recurring invoice** } elseif ($activity->adjustment != 0 && ($invoice->invoice_type_id == INVOICE_TYPE_QUOTE || $invoice->is_recurring)) { - $this->info("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}"); + $this->logMessage("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}"); $foundProblem = true; $clientFix -= $activity->adjustment; $activityFix = 0; @@ -272,7 +305,7 @@ class CheckData extends Command { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_INVOICE) { // **Fix for updating balance when deleting a recurring invoice** if ($activity->adjustment != 0 && $invoice->is_recurring) { - $this->info("Incorrect adjustment for deleted invoice adjustment:{$activity->adjustment}"); + $this->logMessage("Incorrect adjustment for deleted invoice adjustment:{$activity->adjustment}"); $foundProblem = true; if ($activity->balance != $lastBalance) { $clientFix -= $activity->adjustment; @@ -282,7 +315,7 @@ class CheckData extends Command { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_ARCHIVE_INVOICE) { // **Fix for updating balance when archiving an invoice** if ($activity->adjustment != 0 && !$invoice->is_recurring) { - $this->info("Incorrect adjustment for archiving invoice adjustment:{$activity->adjustment}"); + $this->logMessage("Incorrect adjustment for archiving invoice adjustment:{$activity->adjustment}"); $foundProblem = true; $activityFix = 0; $clientFix += $activity->adjustment; @@ -290,12 +323,12 @@ class CheckData extends Command { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_INVOICE) { // **Fix for updating balance when updating recurring invoice** if ($activity->adjustment != 0 && $invoice->is_recurring) { - $this->info("Incorrect adjustment for updated recurring invoice adjustment:{$activity->adjustment}"); + $this->logMessage("Incorrect adjustment for updated recurring invoice adjustment:{$activity->adjustment}"); $foundProblem = true; $clientFix -= $activity->adjustment; $activityFix = 0; } else if ((strtotime($activity->created_at) - strtotime($lastCreatedAt) <= 1) && $activity->adjustment > 0 && $activity->adjustment == $lastAdjustment) { - $this->info("Duplicate adjustment for updated invoice adjustment:{$activity->adjustment}"); + $this->logMessage("Duplicate adjustment for updated invoice adjustment:{$activity->adjustment}"); $foundProblem = true; $clientFix -= $activity->adjustment; $activityFix = 0; @@ -303,7 +336,7 @@ class CheckData extends Command { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_QUOTE) { // **Fix for updating balance when updating a quote** if ($activity->balance != $lastBalance) { - $this->info("Incorrect adjustment for updated quote adjustment:{$activity->adjustment}"); + $this->logMessage("Incorrect adjustment for updated quote adjustment:{$activity->adjustment}"); $foundProblem = true; $clientFix += $lastBalance - $activity->balance; $activityFix = 0; @@ -311,7 +344,7 @@ class CheckData extends Command { } else if ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_PAYMENT) { // **Fix for deleting payment after deleting invoice** if ($activity->adjustment != 0 && $invoice->is_deleted && $activity->created_at > $invoice->deleted_at) { - $this->info("Incorrect adjustment for deleted payment adjustment:{$activity->adjustment}"); + $this->logMessage("Incorrect adjustment for deleted payment adjustment:{$activity->adjustment}"); $foundProblem = true; $activityFix = 0; $clientFix -= $activity->adjustment; @@ -340,7 +373,7 @@ class CheckData extends Command { } if ($activity->balance + $clientFix != $client->actual_balance) { - $this->info("** Creating 'recovered update' activity **"); + $this->logMessage("** Creating 'recovered update' activity **"); if ($this->option('fix') == 'true') { DB::table('activities')->insert([ 'created_at' => new Carbon, @@ -354,7 +387,7 @@ class CheckData extends Command { } $data = ['balance' => $client->actual_balance]; - $this->info("Corrected balance:{$client->actual_balance}"); + $this->logMessage("Corrected balance:{$client->actual_balance}"); if ($this->option('fix') == 'true') { DB::table('clients') ->where('id', $client->id) diff --git a/app/Events/TaskWasArchived.php b/app/Events/TaskWasArchived.php new file mode 100644 index 0000000000..74a7d1e4f6 --- /dev/null +++ b/app/Events/TaskWasArchived.php @@ -0,0 +1,28 @@ +task = $task; + } + +} diff --git a/app/Events/TaskWasDeleted.php b/app/Events/TaskWasDeleted.php new file mode 100644 index 0000000000..3e41c77ff6 --- /dev/null +++ b/app/Events/TaskWasDeleted.php @@ -0,0 +1,28 @@ +task = $task; + } + +} diff --git a/app/Events/TaskWasRestored.php b/app/Events/TaskWasRestored.php new file mode 100644 index 0000000000..58891f6e3d --- /dev/null +++ b/app/Events/TaskWasRestored.php @@ -0,0 +1,29 @@ +task = $task; + } + +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 9dfadde1b7..1935b0df7a 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -1,5 +1,7 @@ path() != 'get_started') { // https://gist.github.com/jrmadsen67/bd0f9ad0ef1ed6bb594e @@ -82,6 +84,40 @@ class Handler extends ExceptionHandler } } + if($this->isHttpException($e)) + { + switch ($e->getStatusCode()) + { + // not found + case 404: + if($request->header('X-Ninja-Token') != '') { + //API request which has hit a route which does not exist + + $error['error'] = ['message'=>'Route does not exist']; + $error = json_encode($error, JSON_PRETTY_PRINT); + $headers = Utils::getApiHeaders(); + + return response()->make($error, 404, $headers); + + } + break; + + // internal error + case '500': + if($request->header('X-Ninja-Token') != '') { + //API request which produces 500 error + + $error['error'] = ['message'=>'Internal Server Error']; + $error = json_encode($error, JSON_PRETTY_PRINT); + $headers = Utils::getApiHeaders(); + + return response()->make($error, 500, $headers); + } + break; + + } + } + // In production, except for maintenance mode, we'll show a custom error screen if (Utils::isNinjaProd() && !Utils::isDownForMaintenance() diff --git a/app/Http/Controllers/AccountApiController.php b/app/Http/Controllers/AccountApiController.php index a74536a3c1..05039ca99a 100644 --- a/app/Http/Controllers/AccountApiController.php +++ b/app/Http/Controllers/AccountApiController.php @@ -4,6 +4,9 @@ use Auth; use Utils; use Response; use Cache; +use Socialite; +use Exception; +use App\Services\AuthService; use App\Models\Account; use App\Ninja\Repositories\AccountRepository; use Illuminate\Http\Request; @@ -181,4 +184,30 @@ class AccountApiController extends BaseAPIController } } + + public function oauthLogin(Request $request) + { + $user = false; + $token = $request->input('token'); + $provider = $request->input('provider'); + + try { + $user = Socialite::driver($provider)->userFromToken($token); + } catch (Exception $exception) { + return $this->errorResponse(['message' => $exception->getMessage()], 401); + } + + if ($user) { + $providerId = AuthService::getProviderId($provider); + $user = $this->accountRepo->findUserByOauth($providerId, $user->id); + } + + if ($user) { + Auth::login($user); + return $this->processLogin($request); + } else { + sleep(ERROR_DELAY); + return $this->errorResponse(['message' => 'Invalid credentials'], 401); + } + } } diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 434cffe5e9..6f73cb19c3 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -1,6 +1,8 @@ getPlanDetails(false, false); $newPlan = [ @@ -193,7 +199,9 @@ class AccountController extends BaseController } } + $hasPaid = false; if (!empty($planDetails['paid']) && $plan != PLAN_FREE) { + $hasPaid = true; $time_used = $planDetails['paid']->diff(date_create()); $days_used = $time_used->days; @@ -209,7 +217,11 @@ class AccountController extends BaseController if ($newPlan['price'] > $credit) { $invitation = $this->accountRepo->enablePlan($newPlan, $credit); - return Redirect::to('view/' . $invitation->invitation_key); + if ($hasPaid) { + return Redirect::to('view/' . $invitation->invitation_key); + } else { + return Redirect::to('payment/' . $invitation->invitation_key); + } } else { if ($plan != PLAN_FREE) { @@ -417,6 +429,7 @@ class AccountController extends BaseController 'currencies' => Cache::get('currencies'), 'title' => trans('texts.localization'), 'weekdays' => Utils::getTranslatedWeekdayNames(), + 'months' => Utils::getMonthOptions(), ]; return View::make('accounts.localization', $data); @@ -458,10 +471,12 @@ class AccountController extends BaseController } return View::make('accounts.payments', [ - 'showAdd' => $count < count(Gateway::$alternate) + 1, - 'title' => trans('texts.online_payments'), + 'showAdd' => $count < count(Gateway::$alternate) + 1, + 'title' => trans('texts.online_payments'), 'tokenBillingOptions' => $tokenBillingOptions, - 'account' => $account, + 'currency' => Utils::getFromCache(Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY), + 'currencies'), + 'account' => $account, ]); } } @@ -471,16 +486,9 @@ class AccountController extends BaseController */ private function showProducts() { - $columns = ['product', 'description', 'unit_cost']; - if (Auth::user()->account->invoice_item_taxes) { - $columns[] = 'tax_rate'; - } - $columns[] = 'action'; - $data = [ 'account' => Auth::user()->account, 'title' => trans('texts.product_library'), - 'columns' => Utils::trans($columns), ]; return View::make('accounts.products', $data); @@ -667,11 +675,9 @@ class AccountController extends BaseController * @param $section * @return \Illuminate\Http\RedirectResponse */ - public function doSection($section = ACCOUNT_COMPANY_DETAILS) + public function doSection($section) { - if ($section === ACCOUNT_COMPANY_DETAILS) { - return AccountController::saveDetails(); - } elseif ($section === ACCOUNT_LOCALIZATION) { + if ($section === ACCOUNT_LOCALIZATION) { return AccountController::saveLocalization(); } elseif ($section == ACCOUNT_PAYMENTS) { return self::saveOnlinePayments(); @@ -697,9 +703,27 @@ class AccountController extends BaseController return AccountController::saveTaxRates(); } elseif ($section === ACCOUNT_PAYMENT_TERMS) { return AccountController::savePaymetTerms(); + } elseif ($section === ACCOUNT_MANAGEMENT) { + return AccountController::saveAccountManagement(); } } + /** + * @return \Illuminate\Http\RedirectResponse + */ + private function saveAccountManagement() + { + $account = Auth::user()->account; + $modules = Input::get('modules'); + + $account->enabled_modules = $modules ? array_sum($modules) : 0; + $account->save(); + + Session::flash('message', trans('texts.updated_settings')); + + return Redirect::to('settings/'.ACCOUNT_MANAGEMENT); + } + /** * @return \Illuminate\Http\RedirectResponse */ @@ -723,12 +747,7 @@ class AccountController extends BaseController private function saveClientPortal() { $account = Auth::user()->account; - - $account->enable_client_portal = !!Input::get('enable_client_portal'); - $account->enable_client_portal_dashboard = !!Input::get('enable_client_portal_dashboard'); - $account->enable_portal_password = !!Input::get('enable_portal_password'); - $account->send_portal_password = !!Input::get('send_portal_password'); - $account->enable_buy_now_buttons = !!Input::get('enable_buy_now_buttons'); + $account->fill(Input::all()); // Only allowed for pro Invoice Ninja users or white labeled self-hosted users if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { @@ -1200,6 +1219,7 @@ class AccountController extends BaseController $account->military_time = Input::get('military_time') ? true : false; $account->show_currency_code = Input::get('show_currency_code') ? true : false; $account->start_of_week = Input::get('start_of_week') ? Input::get('start_of_week') : 0; + $account->financial_year_start = Input::get('financial_year_start') ? Input::get('financial_year_start') : null; $account->save(); event(new UserSettingsChanged()); @@ -1226,6 +1246,35 @@ class AccountController extends BaseController return Redirect::to('settings/'.ACCOUNT_PAYMENTS); } + /** + * @return \Illuminate\Http\RedirectResponse + */ + public function savePaymentGatewayLimits() + { + $gateway_type_id = intval(Input::get('gateway_type_id')); + $gateway_settings = AccountGatewaySettings::scope()->where('gateway_type_id', '=', $gateway_type_id)->first(); + + if ( ! $gateway_settings) { + $gateway_settings = AccountGatewaySettings::createNew(); + $gateway_settings->gateway_type_id = $gateway_type_id; + } + + $gateway_settings->min_limit = Input::get('limit_min_enable') ? intval(Input::get('limit_min')) : null; + $gateway_settings->max_limit = Input::get('limit_max_enable') ? intval(Input::get('limit_max')) : null; + + if ($gateway_settings->max_limit !== null && $gateway_settings->min_limit > $gateway_settings->max_limit) { + $gateway_settings->max_limit = $gateway_settings->min_limit; + } + + $gateway_settings->save(); + + event(new UserSettingsChanged()); + + Session::flash('message', trans('texts.updated_settings')); + + return Redirect::to('settings/' . ACCOUNT_PAYMENTS); + } + /** * @return \Illuminate\Http\RedirectResponse */ diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index d0ce1b6f62..844cd21754 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -48,8 +48,10 @@ class AccountGatewayController extends BaseController $accountGateway = AccountGateway::scope($publicId)->firstOrFail(); $config = $accountGateway->getConfig(); - foreach ($config as $field => $value) { - $config->$field = str_repeat('*', strlen($value)); + if ($accountGateway->gateway_id != GATEWAY_CUSTOM) { + foreach ($config as $field => $value) { + $config->$field = str_repeat('*', strlen($value)); + } } $data = self::getViewModel($accountGateway); @@ -60,8 +62,6 @@ 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); } @@ -100,7 +100,7 @@ class AccountGatewayController extends BaseController if ($otherProviders) { $availableGatewaysIds = $account->availableGatewaysIds(); - $data['primaryGateways'] = Gateway::primary($availableGatewaysIds)->orderBy('name', 'desc')->get(); + $data['primaryGateways'] = Gateway::primary($availableGatewaysIds)->orderBy('sort_order')->get(); $data['secondaryGateways'] = Gateway::secondary($availableGatewaysIds)->orderBy('name')->get(); $data['hiddenFields'] = Gateway::$hiddenFields; @@ -132,7 +132,9 @@ class AccountGatewayController extends BaseController foreach ($gateways as $gateway) { $fields = $gateway->getFields(); - asort($fields); + if ( ! $gateway->isCustom()) { + asort($fields); + } $gateway->fields = $gateway->id == GATEWAY_WEPAY ? [] : $fields; if ($accountGateway && $accountGateway->gateway_id == $gateway->id) { $accountGateway->fields = $gateway->fields; @@ -247,6 +249,8 @@ class AccountGatewayController extends BaseController } if (!$value && ($field == 'testMode' || $field == 'developerMode')) { // do nothing + } elseif ($gatewayId == GATEWAY_CUSTOM) { + $config->$field = strip_tags($value); } else { $config->$field = $value; } @@ -312,14 +316,17 @@ class AccountGatewayController extends BaseController if (isset($wepayResponse)) { return $wepayResponse; } else { + $this->testGateway($accountGateway); + if ($accountGatewayPublicId) { $message = trans('texts.updated_gateway'); + Session::flash('message', $message); + return Redirect::to("gateways/{$accountGateway->public_id}/edit"); } else { $message = trans('texts.created_gateway'); + Session::flash('message', $message); + return Redirect::to("/settings/online_payments"); } - - Session::flash('message', $message); - return Redirect::to("gateways/{$accountGateway->public_id}/edit"); } } } diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index c120661db7..c583f919b8 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -1,5 +1,6 @@ layout = View::make($this->layout); } } + + protected function returnBulk($entityType, $action, $ids) + { + if ( ! is_array($ids)) { + $ids = [$ids]; + } + + $isDatatable = filter_var(request()->datatable, FILTER_VALIDATE_BOOLEAN); + $entityTypes = Utils::pluralizeEntityType($entityType); + + if ($action == 'restore' && count($ids) == 1) { + return redirect("{$entityTypes}/" . $ids[0]); + } elseif ($isDatatable || ($action == 'archive' || $action == 'delete')) { + return redirect("{$entityTypes}"); + } elseif (count($ids)) { + return redirect("{$entityTypes}/" . $ids[0]); + } else { + return redirect("{$entityTypes}"); + } + } } diff --git a/app/Http/Controllers/ClientApiController.php b/app/Http/Controllers/ClientApiController.php index 918b9c701e..4f6ca6521c 100644 --- a/app/Http/Controllers/ClientApiController.php +++ b/app/Http/Controllers/ClientApiController.php @@ -1,5 +1,6 @@ listResponse($clients); } + /** + * @SWG\Get( + * path="/clients/{client_id}", + * summary="Individual Client", + * tags={"client"}, + * @SWG\Response( + * response=200, + * description="A single client", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Client")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + + public function show(ClientRequest $request) + { + return $this->itemResponse($request->entity()); + } + + + + /** * @SWG\Post( * path="/clients", diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index b964f49206..e7bcd6e82e 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -95,7 +95,7 @@ class ClientController extends BaseController if($user->can('create', ENTITY_TASK)){ $actionLinks[] = ['label' => trans('texts.new_task'), 'url' => URL::to('/tasks/create/'.$client->public_id)]; } - if (Utils::hasFeature(FEATURE_QUOTES) && $user->can('create', ENTITY_INVOICE)) { + if (Utils::hasFeature(FEATURE_QUOTES) && $user->can('create', ENTITY_QUOTE)) { $actionLinks[] = ['label' => trans('texts.new_quote'), 'url' => URL::to('/quotes/create/'.$client->public_id)]; } @@ -221,10 +221,6 @@ class ClientController extends BaseController $message = Utils::pluralize($action.'d_client', $count); Session::flash('message', $message); - if ($action == 'restore' && $count == 1) { - return Redirect::to('clients/'.Utils::getFirst($ids)); - } else { - return Redirect::to('clients'); - } + return $this->returnBulk(ENTITY_CLIENT, $action, $ids); } } diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index fa396a6824..c2d850f0a1 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -22,6 +22,7 @@ use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\ActivityRepository; use App\Ninja\Repositories\DocumentRepository; +use App\Ninja\Repositories\CreditRepository; use App\Events\InvoiceInvitationWasViewed; use App\Events\QuoteInvitationWasViewed; use App\Services\PaymentService; @@ -33,13 +34,14 @@ class ClientPortalController extends BaseController private $paymentRepo; private $documentRepo; - public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo, DocumentRepository $documentRepo, PaymentService $paymentService) + public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo, DocumentRepository $documentRepo, PaymentService $paymentService, CreditRepository $creditRepo) { $this->invoiceRepo = $invoiceRepo; $this->paymentRepo = $paymentRepo; $this->activityRepo = $activityRepo; $this->documentRepo = $documentRepo; $this->paymentService = $paymentService; + $this->creditRepo = $creditRepo; } public function view($invitationKey) @@ -102,7 +104,9 @@ class ClientPortalController extends BaseController $paymentURL = ''; if (count($paymentTypes) == 1) { $paymentURL = $paymentTypes[0]['url']; - if (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) { + if ($paymentTypes[0]['gatewayTypeId'] == GATEWAY_TYPE_CUSTOM) { + // do nothing + } elseif (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) { $paymentURL = URL::to($paymentURL); } } @@ -141,7 +145,12 @@ class ClientPortalController extends BaseController ]; } - + if ($accountGateway = $account->getGatewayByType(GATEWAY_TYPE_CUSTOM)) { + $data += [ + 'customGatewayName' => $accountGateway->getConfigField('name'), + 'customGatewayText' => $accountGateway->getConfigField('text'), + ]; + } if($account->hasFeature(FEATURE_DOCUMENTS) && $this->canCreateZip()){ $zipDocs = $this->getInvoiceZipDocuments($invoice, $size); @@ -155,18 +164,6 @@ class ClientPortalController extends BaseController return View::make('invoices.view', $data); } - public function contactIndex($contactKey) { - if (!$contact = Contact::where('contact_key', '=', $contactKey)->first()) { - return $this->returnError(); - } - - $client = $contact->client; - - Session::put('contact_key', $contactKey);// track current contact - - return redirect()->to($client->account->enable_client_portal_dashboard?'/client/dashboard':'/client/invoices/'); - } - private function getPaymentTypes($account, $client, $invitation) { $links = []; @@ -201,19 +198,41 @@ class ClientPortalController extends BaseController return $pdfString; } - public function dashboard() + public function sign($invitationKey) { - if (!$contact = $this->getContact()) { + if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { + return RESULT_FAILURE; + } + + $invitation->signature_base64 = Input::get('signature'); + $invitation->signature_date = date_create(); + $invitation->save(); + + return RESULT_SUCCESS; + } + + public function dashboard($contactKey = false) + { + if ($contactKey) { + if (!$contact = Contact::where('contact_key', '=', $contactKey)->first()) { + return $this->returnError(); + } + Session::put('contact_key', $contactKey);// track current contact + } else if (!$contact = $this->getContact()) { return $this->returnError(); } $client = $contact->client; $account = $client->account; + $account->loadLocalizationSettings($client); + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $customer = false; - if (!$account->enable_client_portal || !$account->enable_client_portal_dashboard) { + if (!$account->enable_client_portal) { return $this->returnError(); + } elseif (!$account->enable_client_portal_dashboard) { + return redirect()->to('/client/invoices/'); } if ($paymentDriver = $account->paymentDriver(false, GATEWAY_TYPE_TOKEN)) { @@ -273,6 +292,7 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); @@ -300,6 +320,7 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); @@ -346,12 +367,14 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ 'color' => $color, 'account' => $account, @@ -416,12 +439,14 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ 'color' => $color, 'account' => $account, @@ -444,6 +469,42 @@ class ClientPortalController extends BaseController return $this->invoiceRepo->getClientDatatable($contact->id, ENTITY_QUOTE, Input::get('sSearch')); } + public function creditIndex() + { + if (!$contact = $this->getContact()) { + return $this->returnError(); + } + + $account = $contact->account; + $account->loadLocalizationSettings($contact->client); + + if (!$account->enable_client_portal) { + return $this->returnError(); + } + + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + + $data = [ + 'color' => $color, + 'account' => $account, + 'clientFontUrl' => $account->getFontsUrl(), + 'title' => trans('texts.credits'), + 'entityType' => ENTITY_CREDIT, + 'columns' => Utils::trans(['credit_date', 'credit_amount', 'credit_balance']), + ]; + + return response()->view('public_list', $data); + } + + public function creditDatatable() + { + if (!$contact = $this->getContact()) { + return false; + } + + return $this->creditRepo->getClientDatatable($contact->client_id); + } + public function documentIndex() { if (!$contact = $this->getContact()) { @@ -451,12 +512,14 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ 'color' => $color, 'account' => $account, diff --git a/app/Http/Controllers/DashboardApiController.php b/app/Http/Controllers/DashboardApiController.php index d5ff416e7e..0db08e5550 100644 --- a/app/Http/Controllers/DashboardApiController.php +++ b/app/Http/Controllers/DashboardApiController.php @@ -23,8 +23,8 @@ class DashboardApiController extends BaseAPIController $dashboardRepo = $this->dashboardRepo; $metrics = $dashboardRepo->totals($accountId, $userId, $viewAll); - $paidToDate = $dashboardRepo->paidToDate($accountId, $userId, $viewAll); - $averageInvoice = $dashboardRepo->averages($accountId, $userId, $viewAll); + $paidToDate = $dashboardRepo->paidToDate($user->account, $userId, $viewAll); + $averageInvoice = $dashboardRepo->averages($user->account, $userId, $viewAll); $balances = $dashboardRepo->balances($accountId, $userId, $viewAll); $activities = $dashboardRepo->activities($accountId, $userId, $viewAll); $pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll); diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 50c5d3d394..14cb4c638b 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -33,9 +33,9 @@ class DashboardController extends BaseController $dashboardRepo = $this->dashboardRepo; $metrics = $dashboardRepo->totals($accountId, $userId, $viewAll); - $paidToDate = $dashboardRepo->paidToDate($accountId, $userId, $viewAll); - $averageInvoice = $dashboardRepo->averages($accountId, $userId, $viewAll); - $balances = $dashboardRepo->balances($accountId, $userId, $viewAll); + $paidToDate = $dashboardRepo->paidToDate($account, $userId, $viewAll); + $averageInvoice = $dashboardRepo->averages($account, $userId, $viewAll); + $balances = $dashboardRepo->balances($accountId, $userId, $viewAll); $activities = $dashboardRepo->activities($accountId, $userId, $viewAll); $pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll); $upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll); diff --git a/app/Http/Controllers/ExpenseApiController.php b/app/Http/Controllers/ExpenseApiController.php index ea4425df86..465edcc852 100644 --- a/app/Http/Controllers/ExpenseApiController.php +++ b/app/Http/Controllers/ExpenseApiController.php @@ -1,8 +1,12 @@ expenseService = $expenseService; } + /** + * @SWG\Get( + * path="/expenses", + * summary="List of expenses", + * tags={"expense"}, + * @SWG\Response( + * response=200, + * description="A list with expenses", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Expense")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function index() { $expenses = Expense::scope() @@ -30,23 +50,103 @@ class ExpenseApiController extends BaseAPIController return $this->listResponse($expenses); } - public function update() + /** + * @SWG\Post( + * path="/expenses", + * tags={"expense"}, + * summary="Create a expense", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Expense") + * ), + * @SWG\Response( + * response=200, + * description="New expense", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function store(CreateExpenseRequest $request) { - //stub + $expense = $this->expenseRepo->save($request->input()); + $expense = Expense::scope($expense->public_id) + ->with('client', 'invoice', 'vendor') + ->first(); + + return $this->itemResponse($expense); } - public function store() + /** + * @SWG\Put( + * path="/expenses/{expense_id}", + * tags={"expense"}, + * summary="Update a expense", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Expense") + * ), + * @SWG\Response( + * response=200, + * description="Update expense", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function update(UpdateExpenseRequest $request, $publicId) { - //stub + if ($request->action) { + return $this->handleAction($request); + } + $data = $request->input(); + $data['public_id'] = $publicId; + $expense = $this->expenseRepo->save($data, $request->entity()); + + return $this->itemResponse($expense); } - public function destroy() + /** + * @SWG\Delete( + * path="/expenses/{expense_id}", + * tags={"expense"}, + * summary="Delete a expense", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Expense") + * ), + * @SWG\Response( + * response=200, + * description="Delete expense", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function destroy(ExpenseRequest $request) { - //stub + $expense = $request->entity(); + $this->expenseRepo->delete($expense); + + return $this->itemResponse($expense); } -} \ No newline at end of file + + +} diff --git a/app/Http/Controllers/ExpenseCategoryApiController.php b/app/Http/Controllers/ExpenseCategoryApiController.php new file mode 100644 index 0000000000..5a18735dee --- /dev/null +++ b/app/Http/Controllers/ExpenseCategoryApiController.php @@ -0,0 +1,41 @@ +categoryRepo = $categoryRepo; + $this->categoryService = $categoryService; + } + + public function update(UpdateExpenseCategoryRequest $request) + { + $category = $this->categoryRepo->save($request->input(), $request->entity()); + + return $this->itemResponse($category); + } + + public function store(CreateExpenseCategoryRequest $request) + { + $category = $this->categoryRepo->save($request->input()); + + return $this->itemResponse($category); + + } +} diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index 97ab3adb7b..1130b6b103 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -134,6 +134,7 @@ class ExpenseController extends BaseController $data = [ 'vendor' => null, 'expense' => $expense, + 'entity' => $expense, 'method' => 'PUT', 'url' => 'expenses/'.$expense->public_id, 'title' => 'Edit Expense', @@ -245,7 +246,7 @@ class ExpenseController extends BaseController Session::flash('message', $message); } - return Redirect::to('expenses'); + return $this->returnBulk($this->entityType, $action, $ids); } private static function getViewModel() diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 395d4a0f24..4f153f78ef 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -38,7 +38,7 @@ class HomeController extends BaseController public function showIndex() { Session::reflash(); - + if (!Utils::isNinja() && (!Utils::isDatabaseSetup() || Account::count() == 0)) { return Redirect::to('/setup'); } elseif (Auth::check()) { @@ -76,10 +76,8 @@ class HomeController extends BaseController } // Track the referral/campaign code - foreach (['rc', 'utm_campaign'] as $code) { - if (Input::has($code)) { - Session::set(SESSION_REFERRAL_CODE, Input::get($code)); - } + if (Input::has('rc')) { + Session::set(SESSION_REFERRAL_CODE, Input::get('rc')); } if (Auth::check()) { @@ -115,7 +113,7 @@ class HomeController extends BaseController $user->save(); } } - + Session::forget('news_feed_message'); return 'success'; diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 3e893d57b5..e5b74453e4 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -31,6 +31,11 @@ class ImportController extends BaseController } } + if ( ! count($files)) { + Session::flash('error', trans('texts.select_file')); + return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT); + } + try { if ($source === IMPORT_CSV) { $data = $this->importService->mapCSV($files); diff --git a/app/Http/Controllers/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php index 4eec4346ec..3e2341b0e4 100644 --- a/app/Http/Controllers/InvoiceApiController.php +++ b/app/Http/Controllers/InvoiceApiController.php @@ -176,7 +176,7 @@ class InvoiceApiController extends BaseAPIController if (isset($data['email_invoice']) && $data['email_invoice']) { if ($payment) { $this->mailer->sendPaymentConfirmation($payment); - } else { + } elseif ( ! $invoice->is_recurring) { $this->mailer->sendInvoice($invoice); } } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index f41fd49f15..843a83777d 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -218,6 +218,7 @@ class InvoiceController extends BaseController $contact->invitation_viewed = $invitation->viewed_date && $invitation->viewed_date != '0000-00-00 00:00:00' ? $invitation->viewed_date : false; $contact->invitation_openend = $invitation->opened_date && $invitation->opened_date != '0000-00-00 00:00:00' ? $invitation->opened_date : false; $contact->invitation_status = $contact->email_error ? false : $invitation->getStatus(); + $contact->invitation_signature_svg = $invitation->signatureDiv(); } } } @@ -269,6 +270,9 @@ class InvoiceController extends BaseController private static function getViewModel($invoice) { $recurringHelp = ''; + $recurringDueDateHelp = ''; + $recurringDueDates = []; + foreach (preg_split("/((\r?\n)|(\r\n?))/", trans('texts.recurring_help')) as $line) { $parts = explode('=>', $line); if (count($parts) > 1) { @@ -279,7 +283,6 @@ class InvoiceController extends BaseController } } - $recurringDueDateHelp = ''; foreach (preg_split("/((\r?\n)|(\r\n?))/", trans('texts.recurring_due_date_help')) as $line) { $parts = explode('=>', $line); if (count($parts) > 1) { @@ -409,10 +412,10 @@ class InvoiceController extends BaseController Session::flash('message', $message); if ($action == 'email') { - return $this->emailInvoice($invoice, Input::get('pdfupload')); + $this->emailInvoice($invoice, Input::get('pdfupload')); } - return redirect()->to($invoice->getRoute()); + return url($invoice->getRoute()); } /** @@ -435,14 +438,14 @@ class InvoiceController extends BaseController Session::flash('message', $message); if ($action == 'clone') { - return $this->cloneInvoice($request, $invoice->public_id); + return url(sprintf('%ss/%s/clone', $entityType, $invoice->public_id)); } elseif ($action == 'convert') { return $this->convertQuote($request, $invoice->public_id); } elseif ($action == 'email') { - return $this->emailInvoice($invoice, Input::get('pdfupload')); + $this->emailInvoice($invoice, Input::get('pdfupload')); } - return redirect()->to($invoice->getRoute()); + return url($invoice->getRoute()); } @@ -469,8 +472,6 @@ class InvoiceController extends BaseController } else { Session::flash('error', $response); } - - return Redirect::to("{$entityType}s/{$invoice->public_id}/edit"); } private function emailRecurringInvoice(&$invoice) @@ -527,11 +528,7 @@ class InvoiceController extends BaseController Session::flash('message', $message); } - if ($action == 'restore' && $count == 1) { - return Redirect::to("{$entityType}s/".Utils::getFirst($ids)); - } else { - return Redirect::to("{$entityType}s"); - } + return $this->returnBulk($entityType, $action, $ids); } public function convertQuote(InvoiceRequest $request) @@ -540,7 +537,7 @@ class InvoiceController extends BaseController Session::flash('message', trans('texts.converted_to_invoice')); - return Redirect::to('invoices/' . $clone->public_id); + return url('invoices/' . $clone->public_id); } public function cloneInvoice(InvoiceRequest $request, $publicId) @@ -608,12 +605,19 @@ class InvoiceController extends BaseController return View::make('invoices.history', $data); } - public function checkInvoiceNumber($invoiceNumber) + public function checkInvoiceNumber($invoicePublicId = false) { - $count = Invoice::scope() + $invoiceNumber = request()->invoice_number; + + $query = Invoice::scope() ->whereInvoiceNumber($invoiceNumber) - ->withTrashed() - ->count(); + ->withTrashed(); + + if ($invoicePublicId) { + $query->where('public_id', '!=', $invoicePublicId); + } + + $count = $query->count(); return $count ? RESULT_FAILURE : RESULT_SUCCESS; } diff --git a/app/Http/Controllers/OnlinePaymentController.php b/app/Http/Controllers/OnlinePaymentController.php index bd621e82d9..e7d709096e 100644 --- a/app/Http/Controllers/OnlinePaymentController.php +++ b/app/Http/Controllers/OnlinePaymentController.php @@ -20,6 +20,7 @@ use App\Http\Requests\CreateOnlinePaymentRequest; use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\InvoiceRepository; use App\Services\InvoiceService; +use App\Models\GatewayType; /** * Class OnlinePaymentController @@ -60,7 +61,7 @@ class OnlinePaymentController extends BaseController * @param bool $sourceId * @return \Illuminate\Http\RedirectResponse */ - public function showPayment($invitationKey, $gatewayType = false, $sourceId = false) + public function showPayment($invitationKey, $gatewayTypeAlias = false, $sourceId = false) { if ( ! $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { return response()->view('error', [ @@ -69,17 +70,23 @@ class OnlinePaymentController extends BaseController ]); } - if ( ! floatval($invitation->invoice->balance)) { + if ( ! $invitation->invoice->canBePaid()) { return redirect()->to('view/' . $invitation->invitation_key); } $invitation = $invitation->load('invoice.client.account.account_gateways.gateway'); + $account = $invitation->account; + $account->loadLocalizationSettings($invitation->invoice->client); - if ( ! $gatewayType) { - $gatewayType = Session::get($invitation->id . 'gateway_type'); + if ( ! $gatewayTypeAlias) { + $gatewayTypeId = Session::get($invitation->id . 'gateway_type'); + } elseif ($gatewayTypeAlias != GATEWAY_TYPE_TOKEN) { + $gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias); + } else { + $gatewayTypeId = $gatewayTypeAlias; } - $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); + $paymentDriver = $account->paymentDriver($invitation, $gatewayTypeId); try { return $paymentDriver->startPurchase(Input::all(), $sourceId); @@ -95,8 +102,12 @@ class OnlinePaymentController extends BaseController public function doPayment(CreateOnlinePaymentRequest $request) { $invitation = $request->invitation; - $gatewayType = Session::get($invitation->id . 'gateway_type'); - $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); + $gatewayTypeId = Session::get($invitation->id . 'gateway_type'); + $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId); + + if ( ! $invitation->invoice->canBePaid()) { + return redirect()->to('view/' . $invitation->invitation_key); + } try { $paymentDriver->completeOnsitePurchase($request->all()); @@ -114,17 +125,24 @@ class OnlinePaymentController extends BaseController /** * @param bool $invitationKey - * @param bool $gatewayType + * @param mixed $gatewayTypeAlias * @return \Illuminate\Http\RedirectResponse */ - public function offsitePayment($invitationKey = false, $gatewayType = false) + public function offsitePayment($invitationKey = false, $gatewayTypeAlias = false) { $invitationKey = $invitationKey ?: Session::get('invitation_key'); $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway') ->where('invitation_key', '=', $invitationKey)->firstOrFail(); - $gatewayType = $gatewayType ?: Session::get($invitation->id . 'gateway_type'); - $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); + if ( ! $gatewayTypeAlias) { + $gatewayTypeId = Session::get($invitation->id . 'gateway_type'); + } elseif ($gatewayTypeAlias != GATEWAY_TYPE_TOKEN) { + $gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias); + } else { + $gatewayTypeId = $gatewayTypeAlias; + } + + $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId); if ($error = Input::get('error_description') ?: Input::get('error')) { return $this->error($paymentDriver, $error); @@ -228,7 +246,7 @@ class OnlinePaymentController extends BaseController } } - public function handleBuyNow(ClientRepository $clientRepo, InvoiceService $invoiceService, $gatewayType = false) + public function handleBuyNow(ClientRepository $clientRepo, InvoiceService $invoiceService, $gatewayTypeAlias = false) { if (Crawler::isCrawler()) { return redirect()->to(NINJA_WEB_URL, 301); @@ -267,6 +285,8 @@ class OnlinePaymentController extends BaseController $data = [ 'client_id' => $client->id, + 'tax_rate1' => $account->default_tax_rate ? $account->default_tax_rate->rate : 0, + 'tax_name1' => $account->default_tax_rate ? $account->default_tax_rate->name : '', 'invoice_items' => [[ 'product_key' => $product->product_key, 'notes' => $product->notes, @@ -280,8 +300,8 @@ class OnlinePaymentController extends BaseController $invitation = $invoice->invitations[0]; $link = $invitation->getLink(); - if ($gatewayType) { - return redirect()->to($invitation->getLink('payment') . "/{$gatewayType}"); + if ($gatewayTypeAlias) { + return redirect()->to($invitation->getLink('payment') . "/{$gatewayTypeAlias}"); } else { return redirect()->to($invitation->getLink()); } diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 59f79661c2..62a1183278 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -142,11 +142,13 @@ class PaymentController extends BaseController 'invoices' => Invoice::scope()->invoiceType(INVOICE_TYPE_STANDARD)->where('is_recurring', '=', false) ->with('client', 'invoice_status')->orderBy('invoice_number')->get(), 'payment' => $payment, + 'entity' => $payment, 'method' => 'PUT', 'url' => 'payments/'.$payment->public_id, 'title' => trans('texts.edit_payment'), 'paymentTypes' => Cache::get('paymentTypes'), - 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), ]; + 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), + ]; return View::make('payments.edit', $data); } diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 2d21dbd3cb..9c72694949 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -3,6 +3,7 @@ use Auth; use URL; use View; +use Utils; use Input; use Session; use Redirect; @@ -37,15 +38,40 @@ class ProductController extends BaseController */ public function index() { - return Redirect::to('settings/' . ACCOUNT_PRODUCTS); + $columns = [ + 'checkbox', + 'product', + 'description', + 'unit_cost' + ]; + + if (Auth::user()->account->invoice_item_taxes) { + $columns[] = 'tax_rate'; + } + $columns[] = 'action'; + + return View::make('list', [ + 'entityType' => ENTITY_PRODUCT, + 'title' => trans('texts.products'), + 'sortCol' => '4', + 'columns' => Utils::trans($columns), + ]); } + public function show($publicId) + { + Session::reflash(); + + return Redirect::to("products/$publicId/edit"); + } + + /** * @return \Illuminate\Http\JsonResponse */ public function getDatatable() { - return $this->productService->getDatatable(Auth::user()->account_id); + return $this->productService->getDatatable(Auth::user()->account_id, Input::get('sSearch')); } /** @@ -55,11 +81,13 @@ class ProductController extends BaseController public function edit($publicId) { $account = Auth::user()->account; + $product = Product::scope($publicId)->withTrashed()->firstOrFail(); $data = [ 'account' => $account, 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null, - 'product' => Product::scope($publicId)->firstOrFail(), + 'product' => $product, + 'entity' => $product, 'method' => 'PUT', 'url' => 'products/'.$publicId, 'title' => trans('texts.edit_product'), @@ -111,7 +139,7 @@ class ProductController extends BaseController private function save($productPublicId = false) { if ($productPublicId) { - $product = Product::scope($productPublicId)->firstOrFail(); + $product = Product::scope($productPublicId)->withTrashed()->firstOrFail(); } else { $product = Product::createNew(); } @@ -134,12 +162,12 @@ class ProductController extends BaseController */ public function bulk() { - $action = Input::get('bulk_action'); - $ids = Input::get('bulk_public_id'); + $action = Input::get('action'); + $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); $count = $this->productService->bulk($ids, $action); Session::flash('message', trans('texts.archived_product')); - return Redirect::to('settings/' . ACCOUNT_PRODUCTS); + return $this->returnBulk(ENTITY_PRODUCT, $action, $ids); } } diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index a9719798a1..c8c952debe 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -154,11 +154,7 @@ class QuoteController extends BaseController Session::flash('message', $message); } - if ($action == 'restore' && $count == 1) { - return Redirect::to('quotes/'.Utils::getFirst($ids)); - } else { - return Redirect::to('quotes'); - } + return $this->returnBulk(ENTITY_QUOTE, $action, $ids); } public function approve($invitationKey) diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 3f954da853..3431d5c183 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -11,6 +11,7 @@ use App\Models\Account; use App\Models\Client; use App\Models\Payment; use App\Models\Expense; +use App\Models\Task; /** * Class ReportController @@ -56,8 +57,8 @@ class ReportController extends BaseController if (Input::get('report_type')) { $reportType = Input::get('report_type'); $dateField = Input::get('date_field'); - $startDate = Utils::toSqlDate(Input::get('start_date'), false); - $endDate = Utils::toSqlDate(Input::get('end_date'), false); + $startDate = date_create(Input::get('start_date')); + $endDate = date_create(Input::get('end_date')); } else { $reportType = ENTITY_INVOICE; $dateField = FILTER_INVOICE_DATE; @@ -71,15 +72,17 @@ class ReportController extends BaseController ENTITY_PRODUCT => trans('texts.product'), ENTITY_PAYMENT => trans('texts.payment'), ENTITY_EXPENSE => trans('texts.expense'), + ENTITY_TASK => trans('texts.task'), ENTITY_TAX_RATE => trans('texts.tax'), ]; $params = [ - 'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)), - 'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)), + 'startDate' => $startDate->format('Y-m-d'), + 'endDate' => $endDate->format('Y-m-d'), 'reportTypes' => $reportTypes, 'reportType' => $reportType, 'title' => trans('texts.charts_and_reports'), + 'account' => Auth::user()->account, ]; if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { @@ -120,9 +123,37 @@ class ReportController extends BaseController return $this->generateTaxRateReport($startDate, $endDate, $dateField, $isExport); } elseif ($reportType == ENTITY_EXPENSE) { return $this->generateExpenseReport($startDate, $endDate, $isExport); + } elseif ($reportType == ENTITY_TASK) { + return $this->generateTaskReport($startDate, $endDate, $isExport); } } + private function generateTaskReport($startDate, $endDate, $isExport) + { + $columns = ['client', 'date', 'description', 'duration']; + $displayData = []; + + $tasks = Task::scope() + ->with('client.contacts') + ->withArchived() + ->dateRange($startDate, $endDate); + + foreach ($tasks->get() as $task) { + $displayData[] = [ + $task->client ? ($isExport ? $task->client->getDisplayName() : $task->client->present()->link) : trans('texts.unassigned'), + link_to($task->present()->url, $task->getStartTime()), + $task->present()->description, + Utils::formatTime($task->getDuration()), + ]; + } + + return [ + 'columns' => $columns, + 'displayData' => $displayData, + 'reportTotals' => [], + ]; + } + /** * @param $startDate * @param $endDate @@ -354,7 +385,7 @@ class ReportController extends BaseController $isExport ? $client->getDisplayName() : $client->present()->link, $isExport ? $invoice->invoice_number : $invoice->present()->link, $invoice->present()->invoice_date, - $invoiceItem->qty, + round($invoiceItem->qty, 2), $invoiceItem->product_key, ]; //$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment ? $payment->amount : 0); @@ -433,36 +464,31 @@ class ReportController extends BaseController */ private function generateExpenseReport($startDate, $endDate, $isExport) { - $columns = ['vendor', 'client', 'date', 'expense_amount', 'invoiced_amount']; + $columns = ['vendor', 'client', 'date', 'expense_amount']; $account = Auth::user()->account; $displayData = []; $reportTotals = []; $expenses = Expense::scope() - ->withTrashed() + ->withArchived() ->with('client.contacts', 'vendor') ->where('expense_date', '>=', $startDate) ->where('expense_date', '<=', $endDate); foreach ($expenses->get() as $expense) { - $amount = $expense->amount; - $invoiced = $expense->present()->invoiced_amount; + $amount = $expense->amountWithTax(); $displayData[] = [ $expense->vendor ? ($isExport ? $expense->vendor->name : $expense->vendor->present()->link) : '', $expense->client ? ($isExport ? $expense->client->getDisplayName() : $expense->client->present()->link) : '', $expense->present()->expense_date, Utils::formatMoney($amount, $expense->currency_id), - Utils::formatMoney($invoiced, $expense->invoice_currency_id), ]; $reportTotals = $this->addToTotals($reportTotals, $expense->expense_currency_id, 'amount', $amount); $reportTotals = $this->addToTotals($reportTotals, $expense->invoice_currency_id, 'amount', 0); - - $reportTotals = $this->addToTotals($reportTotals, $expense->invoice_currency_id, 'invoiced', $invoiced); - $reportTotals = $this->addToTotals($reportTotals, $expense->expense_currency_id, 'invoiced', 0); } return [ diff --git a/app/Http/Controllers/SelfUpdateController.php b/app/Http/Controllers/SelfUpdateController.php index 1f362da3eb..a58bb1c93c 100644 --- a/app/Http/Controllers/SelfUpdateController.php +++ b/app/Http/Controllers/SelfUpdateController.php @@ -2,10 +2,9 @@ namespace App\Http\Controllers; -use Codedge\Updater\UpdaterManager; - -use App\Http\Requests; +use Utils; use Redirect; +use Codedge\Updater\UpdaterManager; class SelfUpdateController extends BaseController { @@ -21,6 +20,10 @@ class SelfUpdateController extends BaseController */ public function __construct(UpdaterManager $updater) { + if (Utils::isNinjaProd()) { + exit; + } + $this->updater = $updater; } diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index 42099026eb..bcc803243f 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -177,15 +177,14 @@ class TaskController extends BaseController $data = [ 'task' => $task, + 'entity' => $task, 'clientPublicId' => $task->client ? $task->client->public_id : 0, 'method' => 'PUT', 'url' => 'tasks/'.$task->public_id, 'title' => trans('texts.edit_task'), - 'duration' => $task->is_running ? $task->getCurrentDuration() : $task->getDuration(), 'actions' => $actions, 'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE, 'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(), - //'entityStatus' => $task->present()->status, ]; $data = array_merge($data, self::getViewModel()); @@ -295,16 +294,12 @@ class TaskController extends BaseController return Redirect::to("invoices/{$invoiceId}/edit")->with('tasks', $data); } } else { - $count = $this->taskRepo->bulk($ids, $action); + $count = $this->taskService->bulk($ids, $action); $message = Utils::pluralize($action.'d_task', $count); Session::flash('message', $message); - if ($action == 'restore' && $count == 1) { - return Redirect::to('tasks/'.$ids[0].'/edit'); - } else { - return Redirect::to('tasks'); - } + return $this->returnBulk($this->entityType, $action, $ids); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 1d504978c6..b2d587c1a8 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -66,7 +66,9 @@ class UserController extends BaseController public function edit($publicId) { $user = User::where('account_id', '=', Auth::user()->account_id) - ->where('public_id', '=', $publicId)->firstOrFail(); + ->where('public_id', '=', $publicId) + ->withTrashed() + ->firstOrFail(); $data = [ 'user' => $user, @@ -157,7 +159,9 @@ class UserController extends BaseController if ($userPublicId) { $user = User::where('account_id', '=', Auth::user()->account_id) - ->where('public_id', '=', $userPublicId)->firstOrFail(); + ->where('public_id', '=', $userPublicId) + ->withTrashed() + ->firstOrFail(); $rules['email'] = 'required|email|unique:users,email,'.$user->id.',id'; } else { @@ -334,7 +338,13 @@ class UserController extends BaseController } } - return Redirect::to($referer); + // If the user is looking at an entity redirect to the dashboard + preg_match('/\/[0-9*][\/edit]*$/', $referer, $matches); + if (count($matches)) { + return Redirect::to('/dashboard'); + } else { + return Redirect::to($referer); + } } public function unlinkAccount($userAccountId, $userId) diff --git a/app/Http/Controllers/VendorApiController.php b/app/Http/Controllers/VendorApiController.php index 3b0c2dadd7..c38e5867e2 100644 --- a/app/Http/Controllers/VendorApiController.php +++ b/app/Http/Controllers/VendorApiController.php @@ -1,5 +1,7 @@ itemResponse($vendor); } + + /** + * @SWG\Put( + * path="/vendors/{vendor_id}", + * tags={"vendor"}, + * summary="Update a vendor", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Vendor") + * ), + * @SWG\Response( + * response=200, + * description="Update vendor", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Vendor")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + + public function update(UpdateVendorRequest $request, $publicId) + { + if ($request->action) { + return $this->handleAction($request); + } + + $data = $request->input(); + $data['public_id'] = $publicId; + $vendor = $this->vendorRepo->save($data, $request->entity()); + + $vendor->load(['vendor_contacts']); + + return $this->itemResponse($vendor); + } + + + /** + * @SWG\Delete( + * path="/vendors/{vendor_id}", + * tags={"vendor"}, + * summary="Delete a vendor", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Vendor") + * ), + * @SWG\Response( + * response=200, + * description="Delete vendor", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Vendor")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + + public function destroy(VendorRequest $request) + { + $vendor = $request->entity(); + + $this->vendorRepo->delete($vendor); + + return $this->itemResponse($vendor); + } } diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php index 280f788b52..f1bd21e984 100644 --- a/app/Http/Controllers/VendorController.php +++ b/app/Http/Controllers/VendorController.php @@ -81,7 +81,7 @@ class VendorController extends BaseController public function show(VendorRequest $request) { $vendor = $request->entity(); - + $actionLinks = [ ['label' => trans('texts.new_vendor'), 'url' => URL::to('/vendors/create/' . $vendor->public_id)] ]; @@ -185,10 +185,6 @@ class VendorController extends BaseController $message = Utils::pluralize($action.'d_vendor', $count); Session::flash('message', $message); - if ($action == 'restore' && $count == 1) { - return Redirect::to('vendors/' . Utils::getFirst($ids)); - } else { - return Redirect::to('vendors'); - } + return $this->returnBulk($this->entityType, $action, $ids); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 23ed4f62ca..b20e0b7544 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -18,7 +18,6 @@ class Kernel extends HttpKernel { 'App\Http\Middleware\VerifyCsrfToken', 'App\Http\Middleware\DuplicateSubmissionCheck', 'App\Http\Middleware\QueryLogging', - 'App\Http\Middleware\SessionDataCheckMiddleware', 'App\Http\Middleware\StartupCheck', ]; diff --git a/app/Http/Middleware/ApiCheck.php b/app/Http/Middleware/ApiCheck.php index 3561695e5c..6e7e73223d 100644 --- a/app/Http/Middleware/ApiCheck.php +++ b/app/Http/Middleware/ApiCheck.php @@ -23,18 +23,22 @@ class ApiCheck { */ public function handle($request, Closure $next) { - $loggingIn = $request->is('api/v1/login') || $request->is('api/v1/register'); + $loggingIn = $request->is('api/v1/login') + || $request->is('api/v1/register') + || $request->is('api/v1/oauth_login'); $headers = Utils::getApiHeaders(); + $hasApiSecret = false; if ($secret = env(API_SECRET)) { - $hasApiSecret = hash_equals($request->api_secret ?: '', $secret); + $requestSecret = Request::header('X-Ninja-Secret') ?: ($request->api_secret ?: ''); + $hasApiSecret = hash_equals($requestSecret, $secret); } if ($loggingIn) { // check API secret if ( ! $hasApiSecret) { sleep(ERROR_DELAY); - return Response::json('Invalid secret', 403, $headers); + return Response::json('Invalid value for API_SECRET', 403, $headers); } } else { // check for a valid token diff --git a/app/Http/Middleware/DuplicateSubmissionCheck.php b/app/Http/Middleware/DuplicateSubmissionCheck.php index aa128bbc43..f92a31a483 100644 --- a/app/Http/Middleware/DuplicateSubmissionCheck.php +++ b/app/Http/Middleware/DuplicateSubmissionCheck.php @@ -15,8 +15,7 @@ class DuplicateSubmissionCheck */ public function handle(Request $request, Closure $next) { - - if ($request->is('api/v1/*')) { + if ($request->is('api/v1/*') || $request->is('documents')) { return $next($request); } diff --git a/app/Http/Middleware/QueryLogging.php b/app/Http/Middleware/QueryLogging.php index 57a26166a1..1bc8160dff 100644 --- a/app/Http/Middleware/QueryLogging.php +++ b/app/Http/Middleware/QueryLogging.php @@ -23,6 +23,7 @@ class QueryLogging // Enable query logging for development if (Utils::isNinjaDev()) { DB::enableQueryLog(); + $timeStart = microtime(true); } $response = $next($request); @@ -32,7 +33,9 @@ class QueryLogging if (strstr($request->url(), '_debugbar') === false) { $queries = DB::getQueryLog(); $count = count($queries); - Log::info($request->method() . ' - ' . $request->url() . ": $count queries"); + $timeEnd = microtime(true); + $time = $timeEnd - $timeStart; + Log::info($request->method() . ' - ' . $request->url() . ": $count queries - " . $time); //Log::info($queries); } } diff --git a/app/Http/Middleware/SessionDataCheckMiddleware.php b/app/Http/Middleware/SessionDataCheckMiddleware.php deleted file mode 100644 index c626fa6ee8..0000000000 --- a/app/Http/Middleware/SessionDataCheckMiddleware.php +++ /dev/null @@ -1,31 +0,0 @@ -getLastUsed(); - - if ( ! $bag || $elapsed > $max) { - $request->session()->flush(); - Auth::logout(); - $request->session()->flash('warning', trans('texts.inactive_logout')); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index 1aee1c59b8..776974a894 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -13,7 +13,7 @@ use Event; use Schema; use App\Models\Language; use App\Models\InvoiceDesign; -use App\Events\UserSettingsChanged; +use App\Events\UserLoggedIn; use App\Libraries\CurlUtils; /** @@ -118,13 +118,13 @@ class StartupCheck // Make sure the account/user localization settings are in the session if (Auth::check() && !Session::has(SESSION_TIMEZONE)) { - Event::fire(new UserSettingsChanged()); + Event::fire(new UserLoggedIn()); } // Check if the user is claiming a license (ie, additional invoices, white label, etc.) - if (isset($_SERVER['REQUEST_URI'])) { + if ( ! Utils::isNinjaProd() && isset($_SERVER['REQUEST_URI'])) { $claimingLicense = Utils::startsWith($_SERVER['REQUEST_URI'], '/claim_license'); - if (!$claimingLicense && Input::has('license_key') && Input::has('product_id')) { + if ( ! $claimingLicense && Input::has('license_key') && Input::has('product_id')) { $licenseKey = Input::get('license_key'); $productId = Input::get('product_id'); @@ -154,6 +154,8 @@ class StartupCheck $company->save(); Session::flash('message', trans('texts.bought_white_label')); + } else { + Session::flash('error', trans('texts.invalid_white_label_license')); } } } diff --git a/app/Http/Requests/EntityRequest.php b/app/Http/Requests/EntityRequest.php index 94dae7a443..88ba623d03 100644 --- a/app/Http/Requests/EntityRequest.php +++ b/app/Http/Requests/EntityRequest.php @@ -36,11 +36,11 @@ class EntityRequest extends Request { $class = Utils::getEntityClass($this->entityType); - if (method_exists($class, 'trashed')) { - $this->entity = $class::scope($publicId)->withTrashed()->firstOrFail(); - } else { - $this->entity = $class::scope($publicId)->firstOrFail(); - } + if (method_exists($class, 'trashed')) { + $this->entity = $class::scope($publicId)->withTrashed()->firstOrFail(); + } else { + $this->entity = $class::scope($publicId)->firstOrFail(); + } return $this->entity; } diff --git a/app/Http/Requests/UpdateTaxRateRequest.php b/app/Http/Requests/UpdateTaxRateRequest.php index 381990f32b..e0a741ee96 100644 --- a/app/Http/Requests/UpdateTaxRateRequest.php +++ b/app/Http/Requests/UpdateTaxRateRequest.php @@ -4,7 +4,6 @@ class UpdateTaxRateRequest extends TaxRateRequest { - // Expenses /** * Determine if the user is authorized to make this request. * diff --git a/app/Http/ViewComposers/ClientPortalHeaderComposer.php b/app/Http/ViewComposers/ClientPortalHeaderComposer.php new file mode 100644 index 0000000000..ac0d6ca686 --- /dev/null +++ b/app/Http/ViewComposers/ClientPortalHeaderComposer.php @@ -0,0 +1,52 @@ +with('client') + ->first(); + + if ( ! $contact || $contact->is_deleted) { + return false; + } + + $client = $contact->client; + + $hasDocuments = DB::table('invoices') + ->where('invoices.client_id', '=', $client->id) + ->whereNull('invoices.deleted_at') + ->join('documents', 'documents.invoice_id', '=', 'invoices.id') + ->count(); + + $view->with('hasQuotes', $client->quotes->count()); + $view->with('hasCredits', $client->creditsWithBalance->count()); + $view->with('hasDocuments', $hasDocuments); + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index bdaa3837f3..a40d1e0656 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -1,6 +1,5 @@ 'auth:client'], function() { Route::get('view/{invitation_key}', 'ClientPortalController@view'); Route::get('download/{invitation_key}', 'ClientPortalController@download'); + Route::put('sign/{invitation_key}', 'ClientPortalController@sign'); Route::get('view', 'HomeController@viewLogo'); Route::get('approve/{invitation_key}', 'QuoteController@approve'); Route::get('payment/{invitation_key}/{gateway_type?}/{source_id?}', 'OnlinePaymentController@showPayment'); @@ -52,18 +52,19 @@ Route::group(['middleware' => 'auth:client'], function() { Route::post('client/payment_methods/default', 'ClientPortalController@setDefaultPaymentMethod'); Route::post('client/payment_methods/{source_id}/remove', 'ClientPortalController@removePaymentMethod'); Route::get('client/quotes', 'ClientPortalController@quoteIndex'); + Route::get('client/credits', 'ClientPortalController@creditIndex'); Route::get('client/invoices', 'ClientPortalController@invoiceIndex'); Route::get('client/invoices/recurring', 'ClientPortalController@recurringInvoiceIndex'); Route::post('client/invoices/auto_bill', 'ClientPortalController@setAutoBill'); Route::get('client/documents', 'ClientPortalController@documentIndex'); Route::get('client/payments', 'ClientPortalController@paymentIndex'); - Route::get('client/dashboard', 'ClientPortalController@dashboard'); - Route::get('client/dashboard/{contact_key}', 'ClientPortalController@contactIndex'); + Route::get('client/dashboard/{contact_key?}', 'ClientPortalController@dashboard'); Route::get('client/documents/js/{documents}/{filename}', 'ClientPortalController@getDocumentVFSJS'); Route::get('client/documents/{invitation_key}/{documents}/{filename?}', 'ClientPortalController@getDocument'); Route::get('client/documents/{invitation_key}/{filename?}', 'ClientPortalController@getInvoiceDocumentsZip'); Route::get('api/client.quotes', ['as'=>'api.client.quotes', 'uses'=>'ClientPortalController@quoteDatatable']); + Route::get('api/client.credits', ['as'=>'api.client.credits', 'uses'=>'ClientPortalController@creditDatatable']); Route::get('api/client.invoices', ['as'=>'api.client.invoices', 'uses'=>'ClientPortalController@invoiceDatatable']); Route::get('api/client.recurring_invoices', ['as'=>'api.client.recurring_invoices', 'uses'=>'ClientPortalController@recurringInvoiceDatatable']); Route::get('api/client.documents', ['as'=>'api.client.documents', 'uses'=>'ClientPortalController@documentDatatable']); @@ -128,11 +129,12 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('hide_message', 'HomeController@hideMessage'); Route::get('force_inline_pdf', 'UserController@forcePDFJS'); Route::get('account/get_search_data', ['as' => 'get_search_data', 'uses' => 'AccountController@getSearchData']); - Route::get('check_invoice_number/{invoice_number}', 'InvoiceController@checkInvoiceNumber'); - Route::get('save_sidebar_state', 'UserController@saveSidebarState'); + Route::get('check_invoice_number/{invoice_id?}', 'InvoiceController@checkInvoiceNumber'); + Route::post('save_sidebar_state', 'UserController@saveSidebarState'); Route::get('settings/user_details', 'AccountController@showUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails'); + Route::post('settings/payment_gateway_limits', 'AccountController@savePaymentGatewayLimits'); Route::post('users/change_password', 'UserController@changePassword'); Route::resource('clients', 'ClientController'); @@ -186,6 +188,11 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('api/credits/{client_id?}', ['as'=>'api.credits', 'uses'=>'CreditController@getDatatable']); Route::post('credits/bulk', 'CreditController@bulk'); + Route::get('api/products', ['as'=>'api.products', 'uses'=>'ProductController@getDatatable']); + Route::resource('products', 'ProductController'); + Route::post('products/bulk', 'ProductController@bulk'); + + Route::get('/resend_confirmation', 'AccountController@resendConfirmation'); Route::post('/update_setup', 'AppController@updateSetup'); @@ -228,10 +235,6 @@ Route::group([ Route::resource('tokens', 'TokenController'); Route::post('tokens/bulk', 'TokenController@bulk'); - Route::get('api/products', ['as'=>'api.products', 'uses'=>'ProductController@getDatatable']); - Route::resource('products', 'ProductController'); - Route::post('products/bulk', 'ProductController@bulk'); - Route::get('api/tax_rates', ['as'=>'api.tax_rates', 'uses'=>'TaxRateController@getDatatable']); Route::resource('tax_rates', 'TaxRateController'); Route::post('tax_rates/bulk', 'TaxRateController@bulk'); @@ -281,6 +284,7 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() { Route::get('ping', 'AccountApiController@ping'); Route::post('login', 'AccountApiController@login'); + Route::post('oauth_login', 'AccountApiController@oauthLogin'); Route::post('register', 'AccountApiController@register'); Route::get('static', 'AccountApiController@getStaticData'); Route::get('accounts', 'AccountApiController@show'); @@ -305,12 +309,8 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() Route::post('update_notifications', 'AccountApiController@updatePushNotifications'); Route::get('dashboard', 'DashboardApiController@index'); Route::resource('documents', 'DocumentAPIController'); - - // Vendor Route::resource('vendors', 'VendorApiController'); - - //Expense - Route::resource('expenses', 'ExpenseApiController'); + Route::resource('expense_categories', 'ExpenseCategoryApiController'); }); // Redirects for legacy links @@ -430,56 +430,50 @@ if (!defined('CONTACT_EMAIL')) { define('ACTIVITY_TYPE_CREATE_CLIENT', 1); define('ACTIVITY_TYPE_ARCHIVE_CLIENT', 2); define('ACTIVITY_TYPE_DELETE_CLIENT', 3); - define('ACTIVITY_TYPE_CREATE_INVOICE', 4); define('ACTIVITY_TYPE_UPDATE_INVOICE', 5); define('ACTIVITY_TYPE_EMAIL_INVOICE', 6); define('ACTIVITY_TYPE_VIEW_INVOICE', 7); define('ACTIVITY_TYPE_ARCHIVE_INVOICE', 8); define('ACTIVITY_TYPE_DELETE_INVOICE', 9); - define('ACTIVITY_TYPE_CREATE_PAYMENT', 10); //define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); - define('ACTIVITY_TYPE_VOIDED_PAYMENT', 39); - define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 40); - define('ACTIVITY_TYPE_FAILED_PAYMENT', 41); - define('ACTIVITY_TYPE_CREATE_CREDIT', 14); //define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); define('ACTIVITY_TYPE_ARCHIVE_CREDIT', 16); define('ACTIVITY_TYPE_DELETE_CREDIT', 17); - define('ACTIVITY_TYPE_CREATE_QUOTE', 18); define('ACTIVITY_TYPE_UPDATE_QUOTE', 19); define('ACTIVITY_TYPE_EMAIL_QUOTE', 20); define('ACTIVITY_TYPE_VIEW_QUOTE', 21); define('ACTIVITY_TYPE_ARCHIVE_QUOTE', 22); define('ACTIVITY_TYPE_DELETE_QUOTE', 23); - define('ACTIVITY_TYPE_RESTORE_QUOTE', 24); define('ACTIVITY_TYPE_RESTORE_INVOICE', 25); define('ACTIVITY_TYPE_RESTORE_CLIENT', 26); define('ACTIVITY_TYPE_RESTORE_PAYMENT', 27); define('ACTIVITY_TYPE_RESTORE_CREDIT', 28); define('ACTIVITY_TYPE_APPROVE_QUOTE', 29); - - // Vendors define('ACTIVITY_TYPE_CREATE_VENDOR', 30); define('ACTIVITY_TYPE_ARCHIVE_VENDOR', 31); define('ACTIVITY_TYPE_DELETE_VENDOR', 32); define('ACTIVITY_TYPE_RESTORE_VENDOR', 33); - - // expenses define('ACTIVITY_TYPE_CREATE_EXPENSE', 34); define('ACTIVITY_TYPE_ARCHIVE_EXPENSE', 35); define('ACTIVITY_TYPE_DELETE_EXPENSE', 36); define('ACTIVITY_TYPE_RESTORE_EXPENSE', 37); - - // tasks + define('ACTIVITY_TYPE_VOIDED_PAYMENT', 39); + define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 40); + define('ACTIVITY_TYPE_FAILED_PAYMENT', 41); define('ACTIVITY_TYPE_CREATE_TASK', 42); define('ACTIVITY_TYPE_UPDATE_TASK', 43); + define('ACTIVITY_TYPE_ARCHIVE_TASK', 44); + define('ACTIVITY_TYPE_DELETE_TASK', 45); + define('ACTIVITY_TYPE_RESTORE_TASK', 46); + define('ACTIVITY_TYPE_UPDATE_EXPENSE', 47); + define('DEFAULT_INVOICE_NUMBER', '0001'); define('RECENTLY_VIEWED_LIMIT', 20); @@ -491,6 +485,7 @@ if (!defined('CONTACT_EMAIL')) { define('MAX_IFRAME_URL_LENGTH', 250); define('MAX_LOGO_FILE_SIZE', 200); // KB define('MAX_FAILED_LOGINS', 10); + define('MAX_INVOICE_ITEMS', env('MAX_INVOICE_ITEMS', 100)); define('MAX_DOCUMENT_SIZE', env('MAX_DOCUMENT_SIZE', 10000));// KB define('MAX_EMAIL_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 10000));// Total KB define('MAX_ZIP_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 30000));// Total KB (uncompressed) @@ -547,6 +542,7 @@ if (!defined('CONTACT_EMAIL')) { define('SESSION_TIMEZONE', 'timezone'); define('SESSION_CURRENCY', 'currency'); + define('SESSION_CURRENCY_DECORATOR', 'currency_decorator'); define('SESSION_DATE_FORMAT', 'dateFormat'); define('SESSION_DATE_PICKER_FORMAT', 'datePickerFormat'); define('SESSION_DATETIME_FORMAT', 'datetimeFormat'); @@ -601,6 +597,7 @@ if (!defined('CONTACT_EMAIL')) { define('GATEWAY_CYBERSOURCE', 49); define('GATEWAY_WEPAY', 60); define('GATEWAY_BRAINTREE', 61); + define('GATEWAY_CUSTOM', 62); // The customer exists, but only as a local concept // The remote gateway doesn't understand the concept of customers @@ -623,6 +620,7 @@ if (!defined('CONTACT_EMAIL')) { define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest')); define('NINJA_DATE', '2000-01-01'); define('NINJA_VERSION', '2.7.2' . env('NINJA_VERSION_SUFFIX')); + define('NINJA_VERSION', '2.8.0' . 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')); @@ -639,6 +637,7 @@ if (!defined('CONTACT_EMAIL')) { define('EMAIL_MARKUP_URL', env('EMAIL_MARKUP_URL', 'https://developers.google.com/gmail/markup')); 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('TRANSIFEX_URL', env('TRANSIFEX_URL', 'https://www.transifex.com/invoice-ninja/invoice-ninja')); define('MSBOT_LOGIN_URL', 'https://login.microsoftonline.com/common/oauth2/v2.0/token'); define('MSBOT_LUIS_URL', 'https://api.projectoxford.ai/luis/v1/application'); @@ -704,11 +703,12 @@ if (!defined('CONTACT_EMAIL')) { define('PAYMENT_METHOD_STATUS_VERIFICATION_FAILED', 'verification_failed'); define('PAYMENT_METHOD_STATUS_VERIFIED', 'verified'); - define('GATEWAY_TYPE_CREDIT_CARD', 'credit_card'); - define('GATEWAY_TYPE_BANK_TRANSFER', 'bank_transfer'); - define('GATEWAY_TYPE_PAYPAL', 'paypal'); - define('GATEWAY_TYPE_BITCOIN', 'bitcoin'); - define('GATEWAY_TYPE_DWOLLA', 'dwolla'); + define('GATEWAY_TYPE_CREDIT_CARD', 1); + define('GATEWAY_TYPE_BANK_TRANSFER', 2); + define('GATEWAY_TYPE_PAYPAL', 3); + define('GATEWAY_TYPE_BITCOIN', 4); + define('GATEWAY_TYPE_DWOLLA', 5); + define('GATEWAY_TYPE_CUSTOM', 6); define('GATEWAY_TYPE_TOKEN', 'token'); define('REMINDER1', 'reminder1'); @@ -744,6 +744,10 @@ if (!defined('CONTACT_EMAIL')) { define('BANK_LIBRARY_OFX', 1); + define('CURRENCY_DECORATOR_CODE', 'code'); + define('CURRENCY_DECORATOR_SYMBOL', 'symbol'); + define('CURRENCY_DECORATOR_NONE', 'none'); + define('RESELLER_REVENUE_SHARE', 'A'); define('RESELLER_LIMITED_USERS', 'B'); @@ -852,6 +856,7 @@ if (!defined('CONTACT_EMAIL')) { 'invoiceStatus' => 'App\Models\InvoiceStatus', 'frequencies' => 'App\Models\Frequency', 'gateways' => 'App\Models\Gateway', + 'gatewayTypes' => 'App\Models\GatewayType', 'fonts' => 'App\Models\Font', 'banks' => 'App\Models\Bank', ]; diff --git a/app/Includes/parsecsv.lib.php b/app/Includes/parsecsv.lib.php index 7fe04984db..797beef303 100644 --- a/app/Includes/parsecsv.lib.php +++ b/app/Includes/parsecsv.lib.php @@ -1,21 +1,21 @@ output (true, 'movies.csv', $array); ---------------- - + */ @@ -83,87 +83,87 @@ class parseCSV { * Configuration * - set these options with $object->var_name = 'value'; */ - + # use first line/entry as field names var $heading = true; - + # override field names var $fields = []; - + # sort entries by this field var $sort_by = null; var $sort_reverse = false; - + # delimiter (comma) and enclosure (double quote) var $delimiter = ','; var $enclosure = '"'; - + # basic SQL-like conditions for row matching var $conditions = null; - + # number of rows to ignore from beginning of data var $offset = null; - + # limits the number of returned rows to specified amount var $limit = null; - + # number of rows to analyze when attempting to auto-detect delimiter var $auto_depth = 15; - + # characters to ignore when attempting to auto-detect delimiter var $auto_non_chars = "a-zA-Z0-9\n\r"; - + # preferred delimiter characters, only used when all filtering method # returns multiple possible delimiters (happens very rarely) var $auto_preferred = ",;\t.:|"; - + # character encoding options var $convert_encoding = false; var $input_encoding = 'ISO-8859-1'; var $output_encoding = 'ISO-8859-1'; - + # used by unparse(), save(), and output() functions var $linefeed = "\r\n"; - + # only used by output() function var $output_delimiter = ','; var $output_filename = 'data.csv'; - - + + /** * Internal variables */ - + # current file var $file; - + # loaded file contents var $file_data; - + # array of field values in data parsed var $titles = []; - + # two dimentional array of CSV data var $data = []; - - + + /** * Constructor * @param input CSV file or string * @return nothing */ - function parseCSV ($input = null, $offset = null, $limit = null, $conditions = null) { + function __construct ($input = null, $offset = null, $limit = null, $conditions = null) { if ( $offset !== null ) $this->offset = $offset; if ( $limit !== null ) $this->limit = $limit; if ( count($conditions) > 0 ) $this->conditions = $conditions; if ( !empty($input) ) $this->parse($input); } - - + + // ============================================== // ----- [ Main Functions ] --------------------- // ============================================== - + /** * Parse CSV file or string * @param input CSV file or string @@ -184,7 +184,7 @@ class parseCSV { } return true; } - + /** * Save changes, or new file and/or data * @param file file to save to @@ -199,7 +199,7 @@ class parseCSV { $is_php = ( preg_match('/\.php$/i', $file) ) ? true : false ; return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode); } - + /** * Generate CSV based string for output * @param output if true, prints headers and strings to browser @@ -220,7 +220,7 @@ class parseCSV { } return $data; } - + /** * Convert character encoding * @param input input character encoding, uses default if left blank @@ -232,7 +232,7 @@ class parseCSV { if ( $input !== null ) $this->input_encoding = $input; if ( $output !== null ) $this->output_encoding = $output; } - + /** * Auto-Detect Delimiter: Find delimiter by analyzing a specific number of * rows to determine most probable delimiter character @@ -244,13 +244,13 @@ class parseCSV { * @return delimiter character */ function auto ($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) { - + if ( $file === null ) $file = $this->file; if ( empty($search_depth) ) $search_depth = $this->auto_depth; if ( $enclosure === null ) $enclosure = $this->enclosure; - + if ( $preferred === null ) $preferred = $this->auto_preferred; - + if ( empty($this->file_data) ) { if ( $this->_check_data($file) ) { $data = &$this->file_data; @@ -258,24 +258,24 @@ class parseCSV { } else { $data = &$this->file_data; } - + $chars = []; $strlen = strlen($data); $enclosed = false; $n = 1; $to_end = true; - + // walk specific depth finding posssible delimiter characters for ( $i=0; $i < $strlen; $i++ ) { $ch = $data{$i}; $nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ; $pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ; - + // open and closing quotes if ( $ch == $enclosure && (!$enclosed || $nch != $enclosure) ) { $enclosed = ( $enclosed ) ? false : true ; - - // inline quotes + + // inline quotes } elseif ( $ch == $enclosure && $enclosed ) { $i++; @@ -287,7 +287,7 @@ class parseCSV { } else { $n++; } - + // count character } elseif (!$enclosed) { if ( !preg_match('/['.preg_quote($this->auto_non_chars, '/').']/i', $ch) ) { @@ -299,7 +299,7 @@ class parseCSV { } } } - + // filtering $depth = ( $to_end ) ? $n-1 : $n ; $filtered = []; @@ -308,24 +308,24 @@ class parseCSV { $filtered[$match] = $char; } } - + // capture most probable delimiter ksort($filtered); $delimiter = reset($filtered); $this->delimiter = $delimiter; - + // parse data if ( $parse ) $this->data = $this->parse_string(); - + return $delimiter; - + } - - + + // ============================================== // ----- [ Core Functions ] --------------------- // ============================================== - + /** * Read file to string and call parse_string() * @param file local CSV file @@ -336,7 +336,7 @@ class parseCSV { if ( empty($this->file_data) ) $this->load_data($file); return ( !empty($this->file_data) ) ? $this->parse_string() : false ; } - + /** * Parse CSV strings to arrays * @param data CSV string @@ -348,7 +348,7 @@ class parseCSV { $data = &$this->file_data; } else return false; } - + $rows = []; $row = []; $row_count = 0; @@ -358,19 +358,19 @@ class parseCSV { $enclosed = false; $was_enclosed = false; $strlen = strlen($data); - + // walk through each character for ( $i=0; $i < $strlen; $i++ ) { $ch = $data{$i}; $nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ; $pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ; - + // open and closing quotes if ( $ch == $this->enclosure && (!$enclosed || $nch != $this->enclosure) ) { $enclosed = ( $enclosed ) ? false : true ; if ( $enclosed ) $was_enclosed = true; - - // inline quotes + + // inline quotes } elseif ( $ch == $this->enclosure && $enclosed ) { $current .= $ch; $i++; @@ -382,7 +382,7 @@ class parseCSV { $row[$key] = $current; $current = ''; $col++; - + // end of row if ( $ch == "\n" || $ch == "\r" ) { if ( $this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions) ) { @@ -406,7 +406,7 @@ class parseCSV { $i = $strlen; } } - + // append character to current field } else { $current .= $ch; @@ -421,7 +421,7 @@ class parseCSV { } return $rows; } - + /** * Create CSV data from array * @param data 2D array with data @@ -436,10 +436,10 @@ class parseCSV { if ( !is_array($data) || empty($data) ) $data = &$this->data; if ( !is_array($fields) || empty($fields) ) $fields = &$this->titles; if ( $delimiter === null ) $delimiter = $this->delimiter; - + $string = ( $is_php ) ? "".$this->linefeed : '' ; $entry = []; - + // create heading if ( $this->heading && !$append ) { foreach( $fields as $key => $value ) { @@ -448,7 +448,7 @@ class parseCSV { $string .= implode($delimiter, $entry).$this->linefeed; $entry = []; } - + // create data foreach( $data as $key => $row ) { foreach( $row as $field => $value ) { @@ -457,10 +457,10 @@ class parseCSV { $string .= implode($delimiter, $entry).$this->linefeed; $entry = []; } - + return $string; } - + /** * Load local file or string * @param input local CSV file @@ -488,16 +488,16 @@ class parseCSV { } return false; } - - + + // ============================================== // ----- [ Internal Functions ] ----------------- // ============================================== - + /** * Validate a row against specified conditions * @param row array with values from a row - * @param conditions specified conditions that the row must match + * @param conditions specified conditions that the row must match * @return true of false */ function _validate_row_conditions ($row = [], $conditions = null) { @@ -523,11 +523,11 @@ class parseCSV { } return false; } - + /** * Validate a row against a single condition * @param row array with values from a row - * @param condition specified condition that the row must match + * @param condition specified condition that the row must match * @return true of false */ function _validate_row_condition ($row, $condition) { @@ -583,7 +583,7 @@ class parseCSV { } return '1'; } - + /** * Validates if the row is within the offset or not if sorting is disabled * @param current_row the current row number being processed @@ -593,7 +593,7 @@ class parseCSV { if ( $this->sort_by === null && $this->offset !== null && $current_row < $this->offset ) return false; return true; } - + /** * Enclose values if needed * - only used by unparse() @@ -611,7 +611,7 @@ class parseCSV { } return $value; } - + /** * Check file data * @param file local filename @@ -624,8 +624,8 @@ class parseCSV { } return true; } - - + + /** * Check if passed info might be delimiter * - only used by find_delimiter() @@ -656,7 +656,7 @@ class parseCSV { } else return false; } } - + /** * Read local file * @param file local filename @@ -689,7 +689,7 @@ class parseCSV { } return false; } - + } -?> \ No newline at end of file +?> diff --git a/app/Libraries/HistoryUtils.php b/app/Libraries/HistoryUtils.php index feb5d331d4..8d34c2bed6 100644 --- a/app/Libraries/HistoryUtils.php +++ b/app/Libraries/HistoryUtils.php @@ -24,6 +24,8 @@ class HistoryUtils ACTIVITY_TYPE_CREATE_CLIENT, ACTIVITY_TYPE_CREATE_TASK, ACTIVITY_TYPE_UPDATE_TASK, + ACTIVITY_TYPE_CREATE_EXPENSE, + ACTIVITY_TYPE_UPDATE_EXPENSE, ACTIVITY_TYPE_CREATE_INVOICE, ACTIVITY_TYPE_UPDATE_INVOICE, ACTIVITY_TYPE_EMAIL_INVOICE, @@ -35,7 +37,7 @@ class HistoryUtils ]; $activities = Activity::scope() - ->with(['client.contacts', 'invoice', 'task']) + ->with(['client.contacts', 'invoice', 'task', 'expense']) ->whereIn('user_id', $userIds) ->whereIn('activity_type_id', $activityTypes) ->orderBy('id', 'asc') @@ -52,6 +54,12 @@ class HistoryUtils continue; } $entity->setRelation('client', $activity->client); + } else if ($activity->activity_type_id == ACTIVITY_TYPE_CREATE_EXPENSE || $activity->activity_type_id == ACTIVITY_TYPE_UPDATE_EXPENSE) { + $entity = $activity->expense; + if ( ! $entity) { + continue; + } + $entity->setRelation('client', $activity->client); } else { $entity = $activity->invoice; if ( ! $entity) { diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 448ca17bb0..2dede0e95d 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -22,6 +22,9 @@ class Utils "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", ]; + public static $months = [ + 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december', + ]; public static function isRegistered() { @@ -92,6 +95,17 @@ class Utils return Utils::getResllerType() ? true : false; } + public static function isWhiteLabel() + { + if (Utils::isNinjaProd()) { + return false; + } + + $account = \App\Models\Account::first(); + + return $account && $account->hasFeature(FEATURE_WHITE_LABEL); + } + public static function getResllerType() { return isset($_ENV['RESELLER_TYPE']) ? $_ENV['RESELLER_TYPE'] : false; @@ -151,6 +165,11 @@ class Utils return Auth::check() && Auth::user()->isTrial(); } + public static function isPaidPro() + { + return static::isPro() && ! static::isTrial(); + } + public static function isEnglish() { return App::getLocale() == 'en'; @@ -186,7 +205,7 @@ class Utils $response = new stdClass(); $response->message = isset($_ENV["{$userType}_MESSAGE"]) ? $_ENV["{$userType}_MESSAGE"] : ''; $response->id = isset($_ENV["{$userType}_ID"]) ? $_ENV["{$userType}_ID"] : ''; - $response->version = env('NINJA_SELF_HOST_VERSION', NINJA_VERSION); + $response->version = NINJA_VERSION; return $response; } @@ -354,7 +373,7 @@ class Utils return $data->first(); } - public static function formatMoney($value, $currencyId = false, $countryId = false, $showCode = false) + public static function formatMoney($value, $currencyId = false, $countryId = false, $decorator = false) { $value = floatval($value); @@ -362,6 +381,10 @@ class Utils $currencyId = Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY); } + if (!$decorator) { + $decorator = Session::get(SESSION_CURRENCY_DECORATOR, CURRENCY_DECORATOR_SYMBOL); + } + if (!$countryId && Auth::check()) { $countryId = Auth::user()->account->country_id; } @@ -387,7 +410,9 @@ class Utils $value = number_format($value, $precision, $decimal, $thousand); $symbol = $currency->symbol; - if ($showCode || !$symbol) { + if ($decorator == CURRENCY_DECORATOR_NONE) { + return $value; + } elseif ($decorator == CURRENCY_DECORATOR_CODE || ! $symbol) { return "{$value} {$code}"; } elseif ($swapSymbol) { return "{$value} " . trim($symbol); @@ -635,11 +660,22 @@ class Utils } } + public static function getMonthOptions() + { + $months = []; + + for ($i=1; $i<=count(static::$months); $i++) { + $month = static::$months[$i-1]; + $number = $i < 10 ? '0' . $i : $i; + $months["2000-{$number}-01"] = trans("texts.{$month}"); + } + + return $months; + } + private static function getMonth($offset) { - $months = ['january', 'february', 'march', 'april', 'may', 'june', - 'july', 'august', 'september', 'october', 'november', 'december', ]; - + $months = static::$months; $month = intval(date('n')) - 1; $month += $offset; @@ -1029,4 +1065,66 @@ class Utils return trans('texts.'.strtolower($day)); }); } + + public static function getDocsUrl($path) + { + $page = ''; + $parts = explode('/', $path); + $first = count($parts) ? $parts[0] : false; + $second = count($parts) > 1 ? $parts[1] : false; + + $entityTypes = [ + 'clients', + 'invoices', + 'payments', + 'recurring_invoices', + 'credits', + 'quotes', + 'tasks', + 'expenses', + 'vendors', + ]; + + if ($path == 'dashboard') { + $page = '/introduction.html#dashboard'; + } elseif (in_array($path, $entityTypes)) { + $page = "/{$path}.html#list-" . str_replace('_', '-', $path); + } elseif (in_array($first, $entityTypes)) { + $action = ($first == 'payments' || $first == 'credits') ? 'enter' : 'create'; + $page = "/{$first}.html#{$action}-" . substr(str_replace('_', '-', $first), 0, -1); + } elseif ($first == 'expense_categories') { + $page = '/expenses.html#expense-categories'; + } elseif ($first == 'settings') { + if ($second == 'bank_accounts') { + $page = ''; // TODO write docs + } elseif (in_array($second, \App\Models\Account::$basicSettings)) { + if ($second == 'products') { + $second = 'product_library'; + } elseif ($second == 'notifications') { + $second = 'email_notifications'; + } + $page = '/settings.html#' . str_replace('_', '-', $second); + } elseif (in_array($second, \App\Models\Account::$advancedSettings)) { + $page = "/{$second}.html"; + } elseif ($second == 'customize_design') { + $page = '/invoice_design.html#customize'; + } + } elseif ($first == 'tax_rates') { + $page = '/settings.html#tax-rates'; + } elseif ($first == 'products') { + $page = '/settings.html#product-library'; + } elseif ($first == 'users') { + $page = '/user_management.html#create-user'; + } + + return url(NINJA_DOCS_URL . $page); + } + + public static function calculateTaxes($amount, $taxRate1, $taxRate2) + { + $tax1 = round($amount * $taxRate1 / 100, 2); + $tax2 = round($amount * $taxRate2 / 100, 2); + + return round($amount + $tax1 + $tax2, 2); + } } diff --git a/app/Listeners/ActivityListener.php b/app/Listeners/ActivityListener.php index 91e327454d..ee016cbff3 100644 --- a/app/Listeners/ActivityListener.php +++ b/app/Listeners/ActivityListener.php @@ -1,7 +1,5 @@ find($event->invoice->id); + $backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts') + ->withArchived() + ->find($event->invoice->id); $activity = $this->activityRepo->create( $event->invoice, @@ -489,9 +499,92 @@ class ActivityListener */ public function updatedTask(TaskWasUpdated $event) { + if ( ! $event->task->isChanged()) { + return; + } + $this->activityRepo->create( $event->task, ACTIVITY_TYPE_UPDATE_TASK ); } + + public function archivedTask(TaskWasArchived $event) + { + if ($event->task->is_deleted) { + return; + } + + $this->activityRepo->create( + $event->task, + ACTIVITY_TYPE_ARCHIVE_TASK + ); + } + + public function deletedTask(TaskWasDeleted $event) + { + $this->activityRepo->create( + $event->task, + ACTIVITY_TYPE_DELETE_TASK + ); + } + + public function restoredTask(TaskWasRestored $event) + { + $this->activityRepo->create( + $event->task, + ACTIVITY_TYPE_RESTORE_TASK + ); + } + + + public function createdExpense(ExpenseWasCreated $event) + { + $this->activityRepo->create( + $event->expense, + ACTIVITY_TYPE_CREATE_EXPENSE + ); + } + + public function updatedExpense(ExpenseWasUpdated $event) + { + if ( ! $event->expense->isChanged()) { + return; + } + + $this->activityRepo->create( + $event->expense, + ACTIVITY_TYPE_UPDATE_EXPENSE + ); + } + + public function archivedExpense(ExpenseWasArchived $event) + { + if ($event->expense->is_deleted) { + return; + } + + $this->activityRepo->create( + $event->expense, + ACTIVITY_TYPE_ARCHIVE_EXPENSE + ); + } + + public function deletedExpense(ExpenseWasDeleted $event) + { + $this->activityRepo->create( + $event->expense, + ACTIVITY_TYPE_DELETE_EXPENSE + ); + } + + public function restoredExpense(ExpenseWasRestored $event) + { + $this->activityRepo->create( + $event->expense, + ACTIVITY_TYPE_RESTORE_EXPENSE + ); + } + + } diff --git a/app/Models/Account.php b/app/Models/Account.php index d06763ab01..8a81fa24f1 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -69,6 +69,16 @@ class Account extends Eloquent 'enable_second_tax_rate', 'include_item_taxes_inline', 'start_of_week', + 'financial_year_start', + 'enable_client_portal', + 'enable_client_portal_dashboard', + 'enable_portal_password', + 'send_portal_password', + 'enable_buy_now_buttons', + 'show_accept_invoice_terms', + 'show_accept_quote_terms', + 'require_invoice_signature', + 'require_quote_signature', ]; /** @@ -102,6 +112,21 @@ class Account extends Eloquent ACCOUNT_USER_MANAGEMENT, ]; + public static $modules = [ + ENTITY_RECURRING_INVOICE => 1, + ENTITY_CREDIT => 2, + ENTITY_QUOTE => 4, + ENTITY_TASK => 8, + ENTITY_EXPENSE => 16, + ENTITY_VENDOR => 32, + ]; + + public static $dashboardSections = [ + 'total_revenue' => 1, + 'average_invoice' => 2, + 'outstanding' => 4, + ]; + /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ @@ -438,7 +463,7 @@ class Account extends Eloquent * @param bool $hideSymbol * @return string */ - public function formatMoney($amount, $client = null, $hideSymbol = false) + public function formatMoney($amount, $client = null, $decorator = false) { if ($client && $client->currency_id) { $currencyId = $client->currency_id; @@ -456,9 +481,11 @@ class Account extends Eloquent $countryId = false; } - $hideSymbol = $this->show_currency_code || $hideSymbol; + if ( ! $decorator) { + $decorator = $this->show_currency_code ? CURRENCY_DECORATOR_CODE : CURRENCY_DECORATOR_SYMBOL; + } - return Utils::formatMoney($amount, $currencyId, $countryId, $hideSymbol); + return Utils::formatMoney($amount, $currencyId, $countryId, $decorator); } /** @@ -610,14 +637,14 @@ class Account extends Eloquent /** * @param bool $invitation - * @param bool $gatewayType + * @param mixed $gatewayTypeId * @return bool */ - public function paymentDriver($invitation = false, $gatewayType = false) + public function paymentDriver($invitation = false, $gatewayTypeId = false) { /** @var AccountGateway $accountGateway */ - if ($accountGateway = $this->getGatewayByType($gatewayType)) { - return $accountGateway->paymentDriver($invitation, $gatewayType); + if ($accountGateway = $this->getGatewayByType($gatewayTypeId)) { + return $accountGateway->paymentDriver($invitation, $gatewayTypeId); } return false; @@ -735,6 +762,22 @@ class Account extends Eloquent return Document::getDirectFileUrl($this->logo, $this->getLogoDisk()); } + public function getLogoPath() + { + if ( ! $this->hasLogo()){ + return null; + } + + $disk = $this->getLogoDisk(); + $adapter = $disk->getAdapter(); + + if ($adapter instanceof \League\Flysystem\Adapter\Local) { + return $adapter->applyPathPrefix($this->logo); + } else { + return Document::getDirectFileUrl($this->logo, $this->getLogoDisk()); + } + } + /** * @return mixed */ @@ -1024,6 +1067,7 @@ class Account extends Eloquent $locale = ($client && $client->language_id) ? $client->language->locale : ($this->language_id ? $this->Language->locale : DEFAULT_LOCALE); Session::put(SESSION_CURRENCY, $currencyId); + Session::put(SESSION_CURRENCY_DECORATOR, $this->show_currency_code ? CURRENCY_DECORATOR_CODE : CURRENCY_DECORATOR_SYMBOL); Session::put(SESSION_LOCALE, $locale); App::setLocale($locale); @@ -1812,6 +1856,43 @@ class Account extends Eloquent public function getFontFolders(){ return array_map(function($item){return $item['folder'];}, $this->getFontsData()); } + + public function isModuleEnabled($entityType) + { + if (in_array($entityType, [ + ENTITY_CLIENT, + ENTITY_INVOICE, + ENTITY_PRODUCT, + ENTITY_PAYMENT, + ])) { + return true; + } + + return $this->enabled_modules & static::$modules[$entityType]; + } + + public function showAuthenticatePanel($invoice) + { + return $this->showAcceptTerms($invoice) || $this->showSignature($invoice); + } + + public function showAcceptTerms($invoice) + { + if ( ! $this->isPro() || ! $invoice->terms) { + return false; + } + + return $invoice->is_quote ? $this->show_accept_quote_terms : $this->show_accept_invoice_terms; + } + + public function showSignature($invoice) + { + if ( ! $this->isPro()) { + return false; + } + + return $invoice->is_quote ? $this->require_quote_signature : $this->require_invoice_signature; + } } Account::updated(function ($account) diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php index a2250b9bfc..241d1013f8 100644 --- a/app/Models/AccountGateway.php +++ b/app/Models/AccountGateway.php @@ -73,14 +73,14 @@ class AccountGateway extends EntityModel /** * @param bool $invitation - * @param bool $gatewayType + * @param mixed $gatewayTypeId * @return mixed */ - public function paymentDriver($invitation = false, $gatewayType = false) + public function paymentDriver($invitation = false, $gatewayTypeId = false) { $class = static::paymentDriverClass($this->gateway->provider); - return new $class($this, $invitation, $gatewayType); + return new $class($this, $invitation, $gatewayTypeId); } /** diff --git a/app/Models/AccountGatewaySettings.php b/app/Models/AccountGatewaySettings.php new file mode 100644 index 0000000000..6b95c585c0 --- /dev/null +++ b/app/Models/AccountGatewaySettings.php @@ -0,0 +1,32 @@ +belongsTo('App\Models\GatewayType'); + } + + public function setCreatedAtAttribute($value) + { + // to Disable created_at + } +} diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 5641967634..88a3aa1a9d 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -83,6 +83,11 @@ class Activity extends Eloquent return $this->belongsTo('App\Models\Task')->withTrashed(); } + public function expense() + { + return $this->belongsTo('App\Models\Expense')->withTrashed(); + } + public function key() { return sprintf('%s-%s-%s', $this->activity_type_id, $this->client_id, $this->created_at->timestamp); @@ -101,9 +106,8 @@ class Activity extends Eloquent $contactId = $this->contact_id; $payment = $this->payment; $credit = $this->credit; + $expense = $this->expense; $isSystem = $this->is_system; - - /** @var Task $task */ $task = $this->task; $data = [ @@ -117,6 +121,7 @@ class Activity extends Eloquent 'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : null, 'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null, 'task' => $task ? link_to($task->getRoute(), substr($task->description, 0, 30).'...') : null, + 'expense' => $expense ? link_to($expense->getRoute(), substr($expense->public_notes, 0, 30).'...') : null, ]; return trans("texts.activity_{$activityTypeId}", $data); diff --git a/app/Models/Client.php b/app/Models/Client.php index feef118779..a1db398ef5 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -89,6 +89,10 @@ class Client extends EntityModel * @var string */ public static $fieldWebsite = 'website'; + /** + * @var string + */ + public static $fieldVatNumber = 'vat_number'; /** * @return array @@ -106,6 +110,7 @@ class Client extends EntityModel Client::$fieldCountry, Client::$fieldNotes, Client::$fieldWebsite, + Client::$fieldVatNumber, Contact::$fieldFirstName, Contact::$fieldLastName, Contact::$fieldPhone, @@ -132,6 +137,7 @@ class Client extends EntityModel 'country' => 'country', 'note' => 'notes', 'site|website' => 'website', + 'vat' => 'vat_number', ]; } @@ -159,6 +165,14 @@ class Client extends EntityModel return $this->hasMany('App\Models\Invoice'); } + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function quotes() + { + return $this->hasMany('App\Models\Invoice')->where('invoice_type_id', '=', INVOICE_TYPE_QUOTE); + } + /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ @@ -223,6 +237,14 @@ class Client extends EntityModel return $this->hasMany('App\Models\Credit'); } + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function creditsWithBalance() + { + return $this->hasMany('App\Models\Credit')->where('balance', '>', 0); + } + /** * @return mixed */ @@ -329,7 +351,7 @@ class Client extends EntityModel $contact = $this->contacts[0]; - return $contact->getDisplayName() ?: trans('texts.unnamed_client'); + return $contact->getDisplayName(); } /** diff --git a/app/Models/EntityModel.php b/app/Models/EntityModel.php index b0933ea159..51ec29da1a 100644 --- a/app/Models/EntityModel.php +++ b/app/Models/EntityModel.php @@ -2,6 +2,7 @@ use Auth; use Eloquent; +use Illuminate\Database\QueryException; use Utils; use Validator; @@ -14,6 +15,12 @@ class EntityModel extends Eloquent * @var bool */ public $timestamps = true; + + /** + * @var bool + */ + protected static $hasPublicId = true; + /** * @var array */ @@ -56,13 +63,16 @@ class EntityModel extends Eloquent $lastEntity = $className::whereAccountId($entity->account_id); } - $lastEntity = $lastEntity->orderBy('public_id', 'DESC') - ->first(); - if ($lastEntity) { - $entity->public_id = $lastEntity->public_id + 1; - } else { - $entity->public_id = 1; + if (static::$hasPublicId) { + $lastEntity = $lastEntity->orderBy('public_id', 'DESC') + ->first(); + + if ($lastEntity) { + $entity->public_id = $lastEntity->public_id + 1; + } else { + $entity->public_id = 1; + } } return $entity; @@ -244,6 +254,7 @@ class EntityModel extends Eloquent $icons = [ 'dashboard' => 'tachometer', 'clients' => 'users', + 'products' => 'cube', 'invoices' => 'file-pdf-o', 'payments' => 'credit-card', 'recurring_invoices' => 'files-o', @@ -253,9 +264,21 @@ class EntityModel extends Eloquent 'expenses' => 'file-image-o', 'vendors' => 'building', 'settings' => 'cog', + 'self-update' => 'download', ]; return array_get($icons, $entityType); } + // isDirty return true if the field's new value is the same as the old one + public function isChanged() + { + foreach ($this->fillable as $field) { + if ($this->$field != $this->getOriginal($field)) { + return true; + } + } + + return false; + } } diff --git a/app/Models/Expense.php b/app/Models/Expense.php index f5cb229fea..b19b7e2637 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -1,5 +1,6 @@ 'amount', + 'category' => 'expense_category', + 'client' => 'client', + 'vendor' => 'vendor', + 'notes|details' => 'public_notes', + 'date' => 'expense_date', + ]; + } /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ @@ -107,7 +132,13 @@ class Expense extends EntityModel */ public function getName() { - return $this->transaction_id ?: '#' . $this->public_id; + if ($this->transaction_id) { + return $this->transaction_id; + } elseif ($this->public_notes) { + return mb_strimwidth($this->public_notes, 0, 16, "..."); + } else { + return '#' . $this->public_id; + } } /** @@ -175,6 +206,11 @@ class Expense extends EntityModel return $query; } + + public function amountWithTax() + { + return Utils::calculateTaxes($this->amount, $this->tax_rate1, $this->tax_rate2); + } } Expense::creating(function ($expense) { @@ -196,7 +232,3 @@ Expense::updated(function ($expense) { Expense::deleting(function ($expense) { $expense->setNullValues(); }); - -Expense::deleted(function ($expense) { - event(new ExpenseWasDeleted($expense)); -}); diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index c13065c73e..6f7c42d90c 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -14,6 +14,12 @@ class Gateway extends Eloquent */ public $timestamps = true; + protected $fillable = [ + 'provider', + 'is_offsite', + 'sort_order', + ]; + /** * @var array */ @@ -39,6 +45,7 @@ class Gateway extends Eloquent GATEWAY_BRAINTREE, GATEWAY_AUTHORIZE_NET, GATEWAY_MOLLIE, + GATEWAY_CUSTOM, ]; // allow adding these gateway if another gateway @@ -174,6 +181,18 @@ class Gateway extends Eloquent */ public function getFields() { - return Omnipay::create($this->provider)->getDefaultParameters(); + if ($this->isCustom()) { + return [ + 'name' => '', + 'text' => '', + ]; + } else { + return Omnipay::create($this->provider)->getDefaultParameters(); + } + } + + public function isCustom() + { + return $this->id === GATEWAY_CUSTOM; } } diff --git a/app/Models/GatewayType.php b/app/Models/GatewayType.php new file mode 100644 index 0000000000..b695dcf3df --- /dev/null +++ b/app/Models/GatewayType.php @@ -0,0 +1,34 @@ +name; + } + + public static function getAliasFromId($id) + { + return Utils::getFromCache($id, 'gatewayTypes')->alias; + } + + public static function getIdFromAlias($alias) + { + return Cache::get('gatewayTypes')->where('alias', $alias)->first()->id; + } +} diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index a86cc72173..fd0fc7d720 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -134,4 +134,13 @@ class Invitation extends EntityModel $invoice->markViewed(); $client->markLoggedIn(); } + + public function signatureDiv() + { + if ( ! $this->signature_base64) { + return false; + } + + return sprintf('

%s: %s', $this->signature_base64, trans('texts.signed'), Utils::fromSqlDateTime($this->signature_date)); + } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 14b3d68a90..ad20fc1830 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -514,6 +514,11 @@ class Invoice extends EntityModel implements BalanceAffecting return storage_path() . '/pdfcache/cache-' . $this->id . '.pdf'; } + public function canBePaid() + { + return floatval($this->balance) > 0 && ! $this->is_deleted; + } + /** * @param $invoice * @return string diff --git a/app/Models/PaymentType.php b/app/Models/PaymentType.php index 01e96239fd..811ed0a486 100644 --- a/app/Models/PaymentType.php +++ b/app/Models/PaymentType.php @@ -11,4 +11,12 @@ class PaymentType extends Eloquent * @var bool */ public $timestamps = false; + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function gatewayType() + { + return $this->belongsTo('App\Models\GatewayType'); + } } diff --git a/app/Models/Task.php b/app/Models/Task.php index c1534d8898..5b4ca6b6eb 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -14,6 +14,16 @@ class Task extends EntityModel use SoftDeletes; use PresentableTrait; + /** + * @var array + */ + protected $fillable = [ + 'client_id', + 'description', + 'time_log', + 'is_running', + ]; + /** * @return mixed */ @@ -82,6 +92,18 @@ class Task extends EntityModel return self::calcStartTime($this); } + public function getLastStartTime() + { + $parts = json_decode($this->time_log) ?: []; + + if (count($parts)) { + $index = count($parts) - 1; + return $parts[$index][0]; + } else { + return ''; + } + } + /** * @param $task * @return int @@ -165,6 +187,14 @@ class Task extends EntityModel return '#' . $this->public_id; } + + public function scopeDateRange($query, $startDate, $endDate) + { + $query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) >= ' . $startDate->format('U')); + $query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) <= ' . $endDate->format('U')); + + return $query; + } } diff --git a/app/Models/Traits/PresentsInvoice.php b/app/Models/Traits/PresentsInvoice.php index 0249368368..493a4aecdb 100644 --- a/app/Models/Traits/PresentsInvoice.php +++ b/app/Models/Traits/PresentsInvoice.php @@ -87,6 +87,7 @@ trait PresentsInvoice 'invoice.partial_due', 'invoice.custom_text_value1', 'invoice.custom_text_value2', + '.blank', ], INVOICE_FIELDS_CLIENT => [ 'client.client_name', @@ -97,9 +98,11 @@ trait PresentsInvoice 'client.city_state_postal', 'client.country', 'client.email', + 'client.phone', 'client.contact_name', 'client.custom_value1', 'client.custom_value2', + '.blank', ], INVOICE_FIELDS_ACCOUNT => [ 'account.company_name', @@ -114,6 +117,7 @@ trait PresentsInvoice 'account.country', 'account.custom_value1', 'account.custom_value2', + '.blank', ] ]; @@ -198,6 +202,7 @@ trait PresentsInvoice 'company_name', 'website', 'phone', + 'blank', ]; foreach ($fields as $field) { diff --git a/app/Models/User.php b/app/Models/User.php index 6d98a49c01..f4e49a259a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,19 +5,14 @@ use Event; use App\Libraries\Utils; use App\Events\UserSettingsChanged; use App\Events\UserSignedUp; -use Illuminate\Auth\Authenticatable; -use Illuminate\Foundation\Auth\Access\Authorizable; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Auth\Passwords\CanResetPassword; -use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; -use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; -use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; +use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Database\Eloquent\SoftDeletes; /** * Class User */ -class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract { +class User extends Authenticatable +{ /** * @var array */ @@ -27,8 +22,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'edit_all' => 0b0100, ]; - use Authenticatable, Authorizable, CanResetPassword; - /** * The database table used by the model. * @@ -102,26 +95,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return PERSON_USER; } - /** - * Get the unique identifier for the user. - * - * @return mixed - */ - public function getAuthIdentifier() - { - return $this->getKey(); - } - - /** - * Get the password for the user. - * - * @return string - */ - public function getAuthPassword() - { - return $this->password; - } - /** * Get the e-mail address where password reminders are sent. * @@ -258,31 +231,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return MAX_NUM_VENDORS; } - - /** - * @return mixed - */ - public function getRememberToken() - { - return $this->remember_token; - } - - /** - * @param string $value - */ - public function setRememberToken($value) - { - $this->remember_token = $value; - } - - /** - * @return string - */ - public function getRememberTokenName() - { - return 'remember_token'; - } - public function clearSession() { $keys = [ @@ -425,7 +373,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac public function caddAddUsers() { - if ( ! Utils::isNinja()) { + if ( ! Utils::isNinjaProd()) { return true; } elseif ( ! $this->hasFeature(FEATURE_USERS)) { return false; @@ -441,6 +389,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return $numUsers < $company->num_users; } + + public function canCreateOrEdit($entityType, $entity = false) + { + return (($entity && $this->can('edit', $entity)) + || (!$entity && $this->can('create', $entityType))); + } } User::updating(function ($user) { diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 6a387e9246..a68cf42e2f 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -211,7 +211,8 @@ class Vendor extends EntityModel */ public function addVendorContact($data, $isPrimary = false) { - $publicId = isset($data['public_id']) ? $data['public_id'] : false; + //$publicId = isset($data['public_id']) ? $data['public_id'] : false; + $publicId = isset($data['public_id']) ? $data['public_id'] : (isset($data['id']) ? $data['id'] : false); if ($publicId && $publicId != '-1') { $contact = VendorContact::scope($publicId)->firstOrFail(); diff --git a/app/Ninja/Datatables/AccountGatewayDatatable.php b/app/Ninja/Datatables/AccountGatewayDatatable.php index 09c7018734..e7a4be2558 100644 --- a/app/Ninja/Datatables/AccountGatewayDatatable.php +++ b/app/Ninja/Datatables/AccountGatewayDatatable.php @@ -1,10 +1,17 @@ deleted_at) { return $model->name; + } elseif ($model->gateway_id == GATEWAY_CUSTOM) { + $accountGateway = $this->getAccountGateway($model->id); + $name = $accountGateway->getConfigField('name') . ' [' . trans('texts.custom') . ']'; + return link_to("gateways/{$model->public_id}/edit", $name)->toHtml(); } elseif ($model->gateway_id != GATEWAY_WEPAY) { return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); } else { - $accountGateway = AccountGateway::find($model->id); + $accountGateway = $this->getAccountGateway($model->id); $config = $accountGateway->getConfig(); $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; $wepayAccountId = $config->accountId; @@ -45,12 +56,56 @@ class AccountGatewayDatatable extends EntityDatatable } } ], + [ + 'limit', + function ($model) { + if ($model->gateway_id == GATEWAY_CUSTOM) { + $gatewayTypes = [GATEWAY_TYPE_CUSTOM]; + } else { + $accountGateway = $this->getAccountGateway($model->id); + $paymentDriver = $accountGateway->paymentDriver(); + $gatewayTypes = $paymentDriver->gatewayTypes(); + $gatewayTypes = array_diff($gatewayTypes, array(GATEWAY_TYPE_TOKEN)); + } + + $html = ''; + foreach ($gatewayTypes as $gatewayTypeId) { + $accountGatewaySettings = AccountGatewaySettings::scope()->where('account_gateway_settings.gateway_type_id', + '=', $gatewayTypeId)->first(); + $gatewayType = GatewayType::find($gatewayTypeId); + + if (count($gatewayTypes) > 1) { + if ($html) { + $html .= '
'; + } + + $html .= $gatewayType->name . ' — '; + } + + if ($accountGatewaySettings && $accountGatewaySettings->min_limit !== null && $accountGatewaySettings->max_limit !== null) { + $html .= Utils::formatMoney($accountGatewaySettings->min_limit) . ' - ' . Utils::formatMoney($accountGatewaySettings->max_limit); + } elseif ($accountGatewaySettings && $accountGatewaySettings->min_limit !== null) { + $html .= trans('texts.min_limit', + array('min' => Utils::formatMoney($accountGatewaySettings->min_limit)) + ); + } elseif ($accountGatewaySettings && $accountGatewaySettings->max_limit !== null) { + $html .= trans('texts.max_limit', + array('max' => Utils::formatMoney($accountGatewaySettings->max_limit)) + ); + } else { + $html .= trans('texts.no_limit'); + } + } + + return $html; + } + ], ]; } public function actions() { - return [ + $actions = [ [ uctrans('texts.resend_confirmation_email'), function ($model) { @@ -78,7 +133,7 @@ class AccountGatewayDatatable extends EntityDatatable ], [ uctrans('texts.manage_account'), function ($model) { - $accountGateway = AccountGateway::find($model->id); + $accountGateway = $this->getAccountGateway($model->id); $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; return [ 'url' => $endpoint.'account/'.$accountGateway->getConfig()->accountId, @@ -98,6 +153,46 @@ class AccountGatewayDatatable extends EntityDatatable } ] ]; + + foreach (Cache::get('gatewayTypes') as $gatewayType) { + $actions[] = [ + trans('texts.set_limits', ['gateway_type' => $gatewayType->name]), + function () use ($gatewayType) { + $accountGatewaySettings = AccountGatewaySettings::scope() + ->where('account_gateway_settings.gateway_type_id', '=', $gatewayType->id) + ->first(); + $min = $accountGatewaySettings && $accountGatewaySettings->min_limit !== null ? $accountGatewaySettings->min_limit : 'null'; + $max = $accountGatewaySettings && $accountGatewaySettings->max_limit !== null ? $accountGatewaySettings->max_limit : 'null'; + + return "javascript:showLimitsModal('{$gatewayType->name}', {$gatewayType->id}, $min, $max)"; + }, + function ($model) use ($gatewayType) { + // Only show this action if the given gateway supports this gateway type + if ($model->gateway_id == GATEWAY_CUSTOM) { + return $gatewayType->id == GATEWAY_TYPE_CUSTOM; + } else { + $accountGateway = $this->getAccountGateway($model->id); + $paymentDriver = $accountGateway->paymentDriver(); + $gatewayTypes = $paymentDriver->gatewayTypes(); + + return in_array($gatewayType->id, $gatewayTypes); + } + } + ]; + } + + return $actions; + } + + private function getAccountGateway($id) + { + if (isset(static::$accountGateways[$id])) { + return static::$accountGateways[$id]; + } + + static::$accountGateways[$id] = AccountGateway::find($id); + + return static::$accountGateways[$id]; } } diff --git a/app/Ninja/Datatables/ActivityDatatable.php b/app/Ninja/Datatables/ActivityDatatable.php index cb87c26ae6..62fdc646d8 100644 --- a/app/Ninja/Datatables/ActivityDatatable.php +++ b/app/Ninja/Datatables/ActivityDatatable.php @@ -27,7 +27,9 @@ class ActivityDatatable extends EntityDatatable 'payment' => $model->payment ?: '', 'credit' => $model->payment_amount ? Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) : '', 'payment_amount' => $model->payment_amount ? Utils::formatMoney($model->payment_amount, $model->currency_id, $model->country_id) : null, - 'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null + 'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null, + 'task' => $model->task_public_id ? link_to('/tasks/' . $model->task_public_id, substr($model->task_description, 0, 30).'...') : null, + 'expense' => $model->expense_public_id ? link_to('/expenses/' . $model->expense_public_id, substr($model->expense_public_notes, 0, 30).'...') : null, ]; return trans("texts.activity_{$model->activity_type_id}", $data); diff --git a/app/Ninja/Datatables/ClientDatatable.php b/app/Ninja/Datatables/ClientDatatable.php index 4b0ca68b05..400e51f08c 100644 --- a/app/Ninja/Datatables/ClientDatatable.php +++ b/app/Ninja/Datatables/ClientDatatable.php @@ -93,7 +93,7 @@ class ClientDatatable extends EntityDatatable return URL::to("quotes/create/{$model->public_id}"); }, function ($model) { - return Auth::user()->hasFeature(FEATURE_QUOTES) && Auth::user()->can('create', ENTITY_INVOICE); + return Auth::user()->hasFeature(FEATURE_QUOTES) && Auth::user()->can('create', ENTITY_QUOTE); } ], [ diff --git a/app/Ninja/Datatables/ExpenseDatatable.php b/app/Ninja/Datatables/ExpenseDatatable.php index 3b398a5184..5832fd8e63 100644 --- a/app/Ninja/Datatables/ExpenseDatatable.php +++ b/app/Ninja/Datatables/ExpenseDatatable.php @@ -46,7 +46,7 @@ class ExpenseDatatable extends EntityDatatable [ 'expense_date', function ($model) { - if(!Auth::user()->can('editByOwner', [ENTITY_EXPENSE, $model->user_id])){ + if(!Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id])){ return Utils::fromSqlDate($model->expense_date); } @@ -56,14 +56,16 @@ class ExpenseDatatable extends EntityDatatable [ 'amount', function ($model) { + $amount = Utils::calculateTaxes($model->amount, $model->tax_rate1, $model->tax_rate2); + $str = Utils::formatMoney($amount, $model->expense_currency_id); + // show both the amount and the converted amount if ($model->exchange_rate != 1) { - $converted = round($model->amount * $model->exchange_rate, 2); - return Utils::formatMoney($model->amount, $model->expense_currency_id) . ' | ' . - Utils::formatMoney($converted, $model->invoice_currency_id); - } else { - return Utils::formatMoney($model->amount, $model->expense_currency_id); + $converted = round($amount * $model->exchange_rate, 2); + $str .= ' | ' . Utils::formatMoney($converted, $model->invoice_currency_id); } + + return $str; } ], [ diff --git a/app/Ninja/Datatables/InvoiceDatatable.php b/app/Ninja/Datatables/InvoiceDatatable.php index b9fa70aa0a..646ebff05a 100644 --- a/app/Ninja/Datatables/InvoiceDatatable.php +++ b/app/Ninja/Datatables/InvoiceDatatable.php @@ -16,7 +16,7 @@ class InvoiceDatatable extends EntityDatatable [ 'invoice_number', function ($model) use ($entityType) { - if(!Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id])){ + if(!Auth::user()->can('viewByOwner', [ENTITY_INVOICE, $model->user_id])){ return $model->invoice_number; } @@ -186,7 +186,7 @@ class InvoiceDatatable extends EntityDatatable $class = 'success'; break; } - + return "

$label

"; } diff --git a/app/Ninja/Datatables/PaymentDatatable.php b/app/Ninja/Datatables/PaymentDatatable.php index ce79ad96fd..faa9733ca9 100644 --- a/app/Ninja/Datatables/PaymentDatatable.php +++ b/app/Ninja/Datatables/PaymentDatatable.php @@ -21,7 +21,7 @@ class PaymentDatatable extends EntityDatatable [ 'invoice_number', function ($model) { - if(!Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id])){ + if(!Auth::user()->can('viewByOwner', [ENTITY_INVOICE, $model->invoice_user_id])){ return $model->invoice_number; } @@ -89,7 +89,11 @@ class PaymentDatatable extends EntityDatatable [ 'payment_date', function ($model) { - return Utils::dateToString($model->payment_date); + if ($model->is_deleted) { + return Utils::dateToString($model->payment_date); + } else { + return link_to("payments/{$model->public_id}/edit", Utils::dateToString($model->payment_date))->toHtml(); + } } ], [ @@ -123,12 +127,11 @@ class PaymentDatatable extends EntityDatatable return "javascript:showRefundModal({$model->public_id}, '{$max_refund}', '{$formatted}', '{$symbol}')"; }, function ($model) { - return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]) && $model->payment_status_id >= PAYMENT_STATUS_COMPLETED && - $model->refunded < $model->amount && - ( - ($model->transaction_reference && in_array($model->gateway_id , static::$refundableGateways)) - || $model->payment_type_id == PAYMENT_TYPE_CREDIT - ); + return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]) + && $model->payment_status_id >= PAYMENT_STATUS_COMPLETED + && $model->refunded < $model->amount + && $model->transaction_reference + && in_array($model->gateway_id , static::$refundableGateways); } ] ]; diff --git a/app/Ninja/Datatables/RecurringInvoiceDatatable.php b/app/Ninja/Datatables/RecurringInvoiceDatatable.php index 64a5bc3307..a04bcfe591 100644 --- a/app/Ninja/Datatables/RecurringInvoiceDatatable.php +++ b/app/Ninja/Datatables/RecurringInvoiceDatatable.php @@ -58,7 +58,17 @@ class RecurringInvoiceDatatable extends EntityDatatable function ($model) { return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); } - ] + ], + [ + trans("texts.clone_invoice"), + function ($model) { + return URL::to("invoices/{$model->public_id}/clone"); + }, + function ($model) { + return Auth::user()->can('create', ENTITY_INVOICE); + } + ], + ]; } diff --git a/app/Ninja/Datatables/TaskDatatable.php b/app/Ninja/Datatables/TaskDatatable.php index 2184d9c359..0f0a1cc09c 100644 --- a/app/Ninja/Datatables/TaskDatatable.php +++ b/app/Ninja/Datatables/TaskDatatable.php @@ -26,6 +26,9 @@ class TaskDatatable extends EntityDatatable [ 'created_at', function ($model) { + if(!Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id])){ + return Task::calcStartTime($model); + } return link_to("tasks/{$model->public_id}/edit", Task::calcStartTime($model))->toHtml(); } ], diff --git a/app/Ninja/Import/BaseTransformer.php b/app/Ninja/Import/BaseTransformer.php index c1ad30d34f..9c0c53ace1 100644 --- a/app/Ninja/Import/BaseTransformer.php +++ b/app/Ninja/Import/BaseTransformer.php @@ -182,10 +182,21 @@ class BaseTransformer extends TransformerAbstract * @param $name * @return null */ - protected function getVendorId($name) + public function getVendorId($name) { $name = strtolower($name); return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null; } + + /** + * @param $name + * @return null + */ + public function getExpenseCategoryId($name) + { + $name = strtolower($name); + return isset($this->maps[ENTITY_EXPENSE_CATEGORY][$name]) ? $this->maps[ENTITY_EXPENSE_CATEGORY][$name] : null; + } + } diff --git a/app/Ninja/Import/CSV/ClientTransformer.php b/app/Ninja/Import/CSV/ClientTransformer.php index b7707081e6..b8d509ad74 100644 --- a/app/Ninja/Import/CSV/ClientTransformer.php +++ b/app/Ninja/Import/CSV/ClientTransformer.php @@ -28,6 +28,7 @@ class ClientTransformer extends BaseTransformer 'postal_code' => $this->getString($data, 'postal_code'), 'private_notes' => $this->getString($data, 'notes'), 'website' => $this->getString($data, 'website'), + 'vat_number' => $this->getString($data, 'vat_number'), 'contacts' => [ [ 'first_name' => $this->getString($data, 'first_name'), diff --git a/app/Ninja/Import/CSV/ExpenseTransformer.php b/app/Ninja/Import/CSV/ExpenseTransformer.php new file mode 100644 index 0000000000..cd566c0f1a --- /dev/null +++ b/app/Ninja/Import/CSV/ExpenseTransformer.php @@ -0,0 +1,28 @@ + isset($data->amount) ? (float) $data->amount : null, + 'vendor_id' => isset($data->vendor) ? $this->getVendorId($data->vendor) : null, + 'client_id' => isset($data->client) ? $this->getClientId($data->client) : null, + 'expense_date' => isset($data->expense_date) ? date('Y-m-d', strtotime($data->expense_date)) : null, + 'public_notes' => $this->getString($data, 'public_notes'), + 'expense_category_id' => isset($data->expense_category) ? $this->getExpenseCategoryId($data->expense_category) : null, + ]; + }); + } +} diff --git a/app/Ninja/Mailers/Mailer.php b/app/Ninja/Mailers/Mailer.php index 8e5b213367..c1b127c106 100644 --- a/app/Ninja/Mailers/Mailer.php +++ b/app/Ninja/Mailers/Mailer.php @@ -1,5 +1,6 @@ attachData($data['pdfString'], $data['pdfFileName']); } - + // Attach documents to the email if(!empty($data['documents'])){ foreach($data['documents'] as $document){ @@ -90,7 +91,7 @@ class Mailer $invoice->markInvitationSent($invitation, $messageId); } - + return true; } @@ -107,11 +108,13 @@ class Mailer } else { $emailError = $exception->getMessage(); } - + if (isset($data['invitation'])) { $invitation = $data['invitation']; $invitation->email_error = $emailError; $invitation->save(); + } elseif ( ! Utils::isNinja()) { + Utils::logError(Utils::getErrorString($exception)); } return $emailError; diff --git a/app/Ninja/PaymentDrivers/BasePaymentDriver.php b/app/Ninja/PaymentDrivers/BasePaymentDriver.php index db95d31f8a..4db33e1989 100644 --- a/app/Ninja/PaymentDrivers/BasePaymentDriver.php +++ b/app/Ninja/PaymentDrivers/BasePaymentDriver.php @@ -2,16 +2,19 @@ use URL; use Session; +use Utils; use Request; use Omnipay; use Exception; use CreditCard; use DateTime; use App\Models\AccountGatewayToken; +use App\Models\AccountGatewaySettings; use App\Models\Account; use App\Models\Payment; use App\Models\PaymentMethod; use App\Models\Country; +use App\Models\GatewayType; class BasePaymentDriver { @@ -119,6 +122,12 @@ class BasePaymentDriver $gateway = $this->accountGateway->gateway; + if ( ! $this->meetsGatewayTypeLimits($this->gatewayType)) { + // The customer must have hacked the URL + Session::flash('error', trans('texts.limits_not_met')); + return redirect()->to('view/' . $this->invitation->invitation_key); + } + if ($this->isGatewayType(GATEWAY_TYPE_TOKEN) || $gateway->is_offsite) { if (Session::has('error')) { Session::reflash(); @@ -158,12 +167,14 @@ class BasePaymentDriver // check if a custom view exists for this provider protected function paymentView() { - $file = sprintf('%s/views/payments/%s/%s.blade.php', resource_path(), $this->providerName(), $this->gatewayType); + $gatewayTypeAlias = GatewayType::getAliasFromId($this->gatewayType); + + $file = sprintf('%s/views/payments/%s/%s.blade.php', resource_path(), $this->providerName(), $gatewayTypeAlias); if (file_exists($file)) { - return sprintf('payments.%s/%s', $this->providerName(), $this->gatewayType); + return sprintf('payments.%s/%s', $this->providerName(), $gatewayTypeAlias); } else { - return sprintf('payments.%s', $this->gatewayType); + return sprintf('payments.%s', $gatewayTypeAlias); } } @@ -242,8 +253,22 @@ class BasePaymentDriver ->wherePublicId($this->sourceId) ->firstOrFail(); } - } elseif ($this->shouldCreateToken()) { - $paymentMethod = $this->createToken(); + + if ( ! $this->meetsGatewayTypeLimits($paymentMethod->payment_type->gateway_type_id)) { + // The customer must have hacked the URL + Session::flash('error', trans('texts.limits_not_met')); + return redirect()->to('view/' . $this->invitation->invitation_key); + } + } else { + if ($this->shouldCreateToken()) { + $paymentMethod = $this->createToken(); + } + + if ( ! $this->meetsGatewayTypeLimits($this->gatewayType)) { + // The customer must have hacked the URL + Session::flash('error', trans('texts.limits_not_met')); + return redirect()->to('view/' . $this->invitation->invitation_key); + } } if ($this->isTwoStep()) { @@ -323,7 +348,8 @@ class BasePaymentDriver protected function paymentDetails($paymentMethod = false) { $invoice = $this->invoice(); - $completeUrl = url('complete/' . $this->invitation->invitation_key . '/' . $this->gatewayType); + $gatewayTypeAlias = $this->gatewayType == GATEWAY_TYPE_TOKEN ? $this->gatewayType : GatewayType::getAliasFromId($this->gatewayType); + $completeUrl = url('complete/' . $this->invitation->invitation_key . '/' . $gatewayTypeAlias); $data = [ 'amount' => $invoice->getRequestedAmount(), @@ -760,6 +786,10 @@ class BasePaymentDriver continue; } + if ( ! $this->meetsGatewayTypeLimits($paymentMethod->payment_type->gateway_type_id)) { + continue; + } + $url = URL::to("/payment/{$this->invitation->invitation_key}/token/".$paymentMethod->public_id); if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) { @@ -787,27 +817,68 @@ class BasePaymentDriver { $links = []; - foreach ($this->gatewayTypes() as $gatewayType) { - if ($gatewayType === GATEWAY_TYPE_TOKEN) { + foreach ($this->gatewayTypes() as $gatewayTypeId) { + if ($gatewayTypeId === GATEWAY_TYPE_TOKEN) { continue; } + if ( ! $this->meetsGatewayTypeLimits($gatewayTypeId)) { + continue; + } + + $gatewayTypeAlias = GatewayType::getAliasFromId($gatewayTypeId); + + if ($gatewayTypeId == GATEWAY_TYPE_CUSTOM) { + $url = "javascript:showCustomModal();"; + $label = e($this->accountGateway->getConfigField('name')); + } else { + $url = $this->paymentUrl($gatewayTypeAlias); + $label = trans("texts.{$gatewayTypeAlias}"); + } + $links[] = [ - 'url' => $this->paymentUrl($gatewayType), - 'label' => trans("texts.{$gatewayType}") + 'gatewayTypeId' => $gatewayTypeId, + 'url' => $url, + 'label' => $label, ]; } return $links; } - protected function paymentUrl($gatewayType) + protected function meetsGatewayTypeLimits($gatewayTypeId) + { + if ( !$gatewayTypeId ) { + return true; + } + + $accountGatewaySettings = AccountGatewaySettings::scope(false, $this->invitation->account_id) + ->where('account_gateway_settings.gateway_type_id', '=', $gatewayTypeId)->first(); + + if ($accountGatewaySettings) { + $invoice = $this->invoice(); + + if ($accountGatewaySettings->min_limit !== null && $invoice->balance < $accountGatewaySettings->min_limit) { + return false; + } + + if ($accountGatewaySettings->max_limit !== null && $invoice->balance > $accountGatewaySettings->max_limit) { + return false; + } + } + + return true; + } + + protected function paymentUrl($gatewayTypeAlias) { $account = $this->account(); - $url = URL::to("/payment/{$this->invitation->invitation_key}/{$gatewayType}"); + $url = URL::to("/payment/{$this->invitation->invitation_key}/{$gatewayTypeAlias}"); + + $gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias); // PayPal doesn't allow being run in an iframe so we need to open in new tab - if ($gatewayType === GATEWAY_TYPE_PAYPAL) { + if ($gatewayTypeId === GATEWAY_TYPE_PAYPAL) { $url .= '#braintree_paypal'; if ($account->iframe_url) { diff --git a/app/Ninja/PaymentDrivers/CustomPaymentDriver.php b/app/Ninja/PaymentDrivers/CustomPaymentDriver.php new file mode 100644 index 0000000000..1dcbc7095b --- /dev/null +++ b/app/Ninja/PaymentDrivers/CustomPaymentDriver.php @@ -0,0 +1,12 @@ +tokenResponse; + $source = false; if (!empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) { $source = $data; } elseif (!empty($data['object']) && $data['object'] == 'customer') { $sources = !empty($data['sources']) ? $data['sources'] : $data['cards']; $source = reset($sources['data']); - } else { - $source = !empty($data['source']) ? $data['source'] : $data['card']; + } elseif (!empty($data['source'])) { + $source = $data['source']; + } elseif (!empty($data['card'])) { + $source = $data['card']; } if ( ! $source) { diff --git a/app/Ninja/Presenters/ClientPresenter.php b/app/Ninja/Presenters/ClientPresenter.php index 43aca8ba53..f4c8adeb05 100644 --- a/app/Ninja/Presenters/ClientPresenter.php +++ b/app/Ninja/Presenters/ClientPresenter.php @@ -23,22 +23,4 @@ class ClientPresenter extends EntityPresenter { return $account->formatMoney($client->paid_to_date, $client); } - - public function status() - { - $class = $text = ''; - - if ($this->entity->is_deleted) { - $class = 'danger'; - $text = trans('texts.deleted'); - } elseif ($this->entity->trashed()) { - $class = 'warning'; - $text = trans('texts.archived'); - } else { - $class = 'success'; - $text = trans('texts.active'); - } - - return "{$text}"; - } } diff --git a/app/Ninja/Presenters/EntityPresenter.php b/app/Ninja/Presenters/EntityPresenter.php index c30bb84cb0..66e6518c87 100644 --- a/app/Ninja/Presenters/EntityPresenter.php +++ b/app/Ninja/Presenters/EntityPresenter.php @@ -17,6 +17,24 @@ class EntityPresenter extends Presenter return URL::to($link); } + public function statusLabel() + { + $class = $text = ''; + + if ($this->entity->is_deleted) { + $class = 'danger'; + $text = trans('texts.deleted'); + } elseif ($this->entity->trashed()) { + $class = 'warning'; + $text = trans('texts.archived'); + } else { + //$class = 'success'; + //$text = trans('texts.active'); + } + + return "{$text}"; + } + /** * @return mixed */ diff --git a/app/Ninja/Presenters/ExpensePresenter.php b/app/Ninja/Presenters/ExpensePresenter.php index 3de0773625..4760515720 100644 --- a/app/Ninja/Presenters/ExpensePresenter.php +++ b/app/Ninja/Presenters/ExpensePresenter.php @@ -24,12 +24,4 @@ class ExpensePresenter extends EntityPresenter return Utils::fromSqlDate($this->entity->expense_date); } - /** - * @return int - */ - public function invoiced_amount() - { - return $this->entity->invoice_id ? $this->entity->convertedAmount() : 0; - } - } diff --git a/app/Ninja/Presenters/TaskPresenter.php b/app/Ninja/Presenters/TaskPresenter.php index d9b96c1e9a..28dd05d818 100644 --- a/app/Ninja/Presenters/TaskPresenter.php +++ b/app/Ninja/Presenters/TaskPresenter.php @@ -21,6 +21,11 @@ class TaskPresenter extends EntityPresenter return $this->entity->user->getDisplayName(); } + public function description() + { + return substr($this->entity->description, 0, 40) . (strlen($this->entity->description) > 40 ? '...' : ''); + } + /** * @param $account * @return mixed diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index 01eb49e70f..b80bd1321e 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -2,6 +2,7 @@ use Auth; use Request; +use Input; use Session; use Utils; use URL; @@ -27,6 +28,11 @@ class AccountRepository public function create($firstName = '', $lastName = '', $email = '', $password = '') { $company = new Company(); + $company->utm_source = Input::get('utm_source'); + $company->utm_medium = Input::get('utm_medium'); + $company->utm_campaign = Input::get('utm_campaign'); + $company->utm_term = Input::get('utm_term'); + $company->utm_content = Input::get('utm_content'); $company->save(); $account = new Account(); diff --git a/app/Ninja/Repositories/ActivityRepository.php b/app/Ninja/Repositories/ActivityRepository.php index 7ea2616b62..2a910707e4 100644 --- a/app/Ninja/Repositories/ActivityRepository.php +++ b/app/Ninja/Repositories/ActivityRepository.php @@ -74,6 +74,8 @@ class ActivityRepository ->leftJoin('invoices', 'invoices.id', '=', 'activities.invoice_id') ->leftJoin('payments', 'payments.id', '=', 'activities.payment_id') ->leftJoin('credits', 'credits.id', '=', 'activities.credit_id') + ->leftJoin('tasks', 'tasks.id', '=', 'activities.task_id') + ->leftJoin('expenses', 'expenses.id', '=', 'activities.expense_id') ->where('clients.id', '=', $clientId) ->where('contacts.is_primary', '=', 1) ->whereNull('contacts.deleted_at') @@ -102,7 +104,11 @@ class ActivityRepository 'contacts.email as email', 'payments.transaction_reference as payment', 'payments.amount as payment_amount', - 'credits.amount as credit' + 'credits.amount as credit', + 'tasks.description as task_description', + 'tasks.public_id as task_public_id', + 'expenses.public_notes as expense_public_notes', + 'expenses.public_id as expense_public_id' ); } diff --git a/app/Ninja/Repositories/BaseRepository.php b/app/Ninja/Repositories/BaseRepository.php index 49f296ef1b..6b213a25e2 100644 --- a/app/Ninja/Repositories/BaseRepository.php +++ b/app/Ninja/Repositories/BaseRepository.php @@ -8,7 +8,7 @@ class BaseRepository /** * @return null */ - public function getClassName() + public function getClassName() { return null; } @@ -40,7 +40,7 @@ class BaseRepository if ($entity->trashed()) { return; } - + $entity->delete(); $className = $this->getEventClass($entity, 'Archived'); @@ -83,7 +83,7 @@ class BaseRepository if ($entity->is_deleted) { return; } - + $entity->is_deleted = true; $entity->save(); diff --git a/app/Ninja/Repositories/ClientRepository.php b/app/Ninja/Repositories/ClientRepository.php index ae61f37492..b5bcd55196 100644 --- a/app/Ninja/Repositories/ClientRepository.php +++ b/app/Ninja/Repositories/ClientRepository.php @@ -31,6 +31,7 @@ class ClientRepository extends BaseRepository ->where('clients.account_id', '=', \Auth::user()->account_id) ->where('contacts.is_primary', '=', true) ->where('contacts.deleted_at', '=', null) + //->whereRaw('(clients.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices ->select( DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), @@ -78,7 +79,10 @@ class ClientRepository extends BaseRepository $client = Client::createNew(); } else { $client = Client::scope($publicId)->with('contacts')->firstOrFail(); - \Log::warning('Entity not set in client repo save'); + } + + if ($client->is_deleted) { + return $client; } // convert currency code to id @@ -107,7 +111,11 @@ class ClientRepository extends BaseRepository // If the primary is set ensure it's listed first usort($contacts, function ($left, $right) { - return (isset($right['is_primary']) ? $right['is_primary'] : 1) - (isset($left['is_primary']) ? $left['is_primary'] : 0); + if (isset($right['is_primary']) && isset($left['is_primary'])) { + return $right['is_primary'] - $left['is_primary']; + } else { + return 0; + } }); foreach ($contacts as $contact) { diff --git a/app/Ninja/Repositories/CreditRepository.php b/app/Ninja/Repositories/CreditRepository.php index c295f56d8c..e20be664ea 100644 --- a/app/Ninja/Repositories/CreditRepository.php +++ b/app/Ninja/Repositories/CreditRepository.php @@ -58,10 +58,36 @@ class CreditRepository extends BaseRepository return $query; } + public function getClientDatatable($clientId) + { + $query = DB::table('credits') + ->join('accounts', 'accounts.id', '=', 'credits.account_id') + ->join('clients', 'clients.id', '=', 'credits.client_id') + ->where('credits.client_id', '=', $clientId) + ->where('clients.deleted_at', '=', null) + ->where('credits.deleted_at', '=', null) + ->where('credits.balance', '>', 0) + ->select( + DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), + DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), + 'credits.amount', + 'credits.balance', + 'credits.credit_date' + ); + + $table = \Datatable::query($query) + ->addColumn('credit_date', function ($model) { return Utils::fromSqlDate($model->credit_date); }) + ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); }) + ->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); }) + ->make(); + + return $table; + } + public function save($input, $credit = null) { $publicId = isset($data['public_id']) ? $data['public_id'] : false; - + if ($credit) { // do nothing } elseif ($publicId) { diff --git a/app/Ninja/Repositories/DashboardRepository.php b/app/Ninja/Repositories/DashboardRepository.php index 5b04fbc039..2f89e71749 100644 --- a/app/Ninja/Repositories/DashboardRepository.php +++ b/app/Ninja/Repositories/DashboardRepository.php @@ -39,13 +39,15 @@ class DashboardRepository $data = []; $count = 0; + $balance = 0; $records = $this->rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId); - array_map(function ($item) use (&$data, &$count, $groupBy) { + array_map(function ($item) use (&$data, &$count, &$balance, $groupBy) { $data[$item->$groupBy] = $item->total; $count += $item->count; - }, $records->get()); - + $balance += isset($item->balance) ? $item->balance : 0; + }, $records); + $padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month'); $endDate->modify('+1 '.$padding); $interval = new DateInterval('P1'.substr($groupBy, 0, 1)); @@ -84,9 +86,9 @@ class DashboardRepository if ($entityType == ENTITY_INVOICE) { $totals->invoices = array_sum($data); $totals->average = $count ? round($totals->invoices / $count, 2) : 0; + $totals->balance = $balance; } elseif ($entityType == ENTITY_PAYMENT) { $totals->revenue = array_sum($data); - $totals->balance = $totals->invoices - $totals->revenue; } elseif ($entityType == ENTITY_EXPENSE) { //$totals->profit = $totals->revenue - array_sum($data); $totals->expenses = array_sum($data); @@ -106,6 +108,10 @@ class DashboardRepository private function rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId) { + if ( ! in_array($groupBy, ['DAYOFYEAR', 'WEEK', 'MONTH'])) { + return []; + } + $accountId = $account->id; $currencyId = intval($currencyId); $timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))'; @@ -128,7 +134,7 @@ class DashboardRepository } if ($entityType == ENTITY_INVOICE) { - $records->select(DB::raw('sum(invoices.amount) as total, count(invoices.id) as count, '.$timeframe.' as '.$groupBy)) + $records->select(DB::raw('sum(invoices.amount) as total, sum(invoices.balance) as balance, count(invoices.id) as count, '.$timeframe.' as '.$groupBy)) ->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->where('is_recurring', '=', false); } elseif ($entityType == ENTITY_PAYMENT) { @@ -137,10 +143,10 @@ class DashboardRepository ->where('invoices.is_deleted', '=', false) ->whereNotIn('payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]); } elseif ($entityType == ENTITY_EXPENSE) { - $records->select(DB::raw('sum(expenses.amount) as total, count(expenses.id) as count, '.$timeframe.' as '.$groupBy)); + $records->select(DB::raw('sum(expenses.amount + (expenses.amount * expenses.tax_rate1 / 100) + (expenses.amount * expenses.tax_rate2 / 100)) as total, count(expenses.id) as count, '.$timeframe.' as '.$groupBy)); } - return $records; + return $records->get(); } public function totals($accountId, $userId, $viewAll) @@ -175,29 +181,39 @@ class DashboardRepository return $metrics->groupBy('accounts.id')->first(); } - public function paidToDate($accountId, $userId, $viewAll) + public function paidToDate($account, $userId, $viewAll) { + $accountId = $account->id; $select = DB::raw( - 'SUM('.DB::getQueryGrammar()->wrap('clients.paid_to_date', true).') as value,' + 'SUM('.DB::getQueryGrammar()->wrap('payments.amount', true).' - '.DB::getQueryGrammar()->wrap('payments.refunded', true).') as value,' .DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id' ); - $paidToDate = DB::table('accounts') + $paidToDate = DB::table('payments') ->select($select) - ->leftJoin('clients', 'accounts.id', '=', 'clients.account_id') - ->where('accounts.id', '=', $accountId) - ->where('clients.is_deleted', '=', false); + ->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id') + ->leftJoin('clients', 'clients.id', '=', 'invoices.client_id') + ->where('payments.account_id', '=', $accountId) + ->where('clients.is_deleted', '=', false) + ->where('invoices.is_deleted', '=', false) + ->whereNotIn('payments.payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]); if (!$viewAll){ - $paidToDate = $paidToDate->where('clients.user_id', '=', $userId); + $paidToDate->where('invoices.user_id', '=', $userId); } - return $paidToDate->groupBy('accounts.id') - ->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN CASE WHEN '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' IS NULL THEN 1 ELSE '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' END ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END')) + if ($account->financial_year_start) { + $yearStart = str_replace('2000', date('Y'), $account->financial_year_start); + $paidToDate->where('payments.payment_date', '>=', $yearStart); + } + + return $paidToDate->groupBy('payments.account_id') + ->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN '.($account->currency_id ?: DEFAULT_CURRENCY).' ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END')) ->get(); } - public function averages($accountId, $userId, $viewAll) + public function averages($account, $userId, $viewAll) { + $accountId = $account->id; $select = DB::raw( 'AVG('.DB::getQueryGrammar()->wrap('invoices.amount', true).') as invoice_avg, ' .DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id' @@ -213,7 +229,12 @@ class DashboardRepository ->where('invoices.is_recurring', '=', false); if (!$viewAll){ - $averageInvoice = $averageInvoice->where('invoices.user_id', '=', $userId); + $averageInvoice->where('invoices.user_id', '=', $userId); + } + + if ($account->financial_year_start) { + $yearStart = str_replace('2000', date('Y'), $account->financial_year_start); + $averageInvoice->where('invoices.invoice_date', '>=', $yearStart); } return $averageInvoice->groupBy('accounts.id') @@ -252,7 +273,7 @@ class DashboardRepository } return $activities->orderBy('activities.created_at', 'desc') - ->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account', 'task') + ->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account', 'task', 'expense', 'contact') ->take(50) ->get(); } @@ -335,8 +356,12 @@ class DashboardRepository public function expenses($accountId, $userId, $viewAll) { + $amountField = DB::getQueryGrammar()->wrap('expenses.amount', true); + $taxRate1Field = DB::getQueryGrammar()->wrap('expenses.tax_rate1', true); + $taxRate2Field = DB::getQueryGrammar()->wrap('expenses.tax_rate2', true); + $select = DB::raw( - 'SUM('.DB::getQueryGrammar()->wrap('expenses.amount', true).') as value,' + "SUM({$amountField} + ({$amountField} * {$taxRate1Field} / 100) + ({$amountField} * {$taxRate2Field} / 100)) as value," .DB::getQueryGrammar()->wrap('expenses.expense_currency_id', true).' as currency_id' ); $paidToDate = DB::table('accounts') diff --git a/app/Ninja/Repositories/ExpenseCategoryRepository.php b/app/Ninja/Repositories/ExpenseCategoryRepository.php index 90edbd2d74..9622a30ddb 100644 --- a/app/Ninja/Repositories/ExpenseCategoryRepository.php +++ b/app/Ninja/Repositories/ExpenseCategoryRepository.php @@ -12,6 +12,11 @@ class ExpenseCategoryRepository extends BaseRepository return 'App\Models\ExpenseCategory'; } + public function all() + { + return ExpenseCategory::scope()->get(); + } + public function find($filter = null) { $query = DB::table('expense_categories') diff --git a/app/Ninja/Repositories/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php index b692e03c0e..de36b38c87 100644 --- a/app/Ninja/Repositories/ExpenseRepository.php +++ b/app/Ninja/Repositories/ExpenseRepository.php @@ -76,6 +76,8 @@ class ExpenseRepository extends BaseRepository 'expenses.expense_currency_id', 'expenses.invoice_currency_id', 'expenses.user_id', + 'expenses.tax_rate1', + 'expenses.tax_rate2', 'expense_categories.name as category', 'invoices.public_id as invoice_public_id', 'invoices.user_id as invoice_user_id', @@ -118,20 +120,24 @@ class ExpenseRepository extends BaseRepository // do nothing } elseif ($publicId) { $expense = Expense::scope($publicId)->firstOrFail(); - \Log::warning('Entity not set in expense repo save'); + if (Utils::isNinjaDev()) { + \Log::warning('Entity not set in expense repo save'); + } } else { $expense = Expense::createNew(); } + if ($expense->is_deleted) { + return $expense; + } + // First auto fill $expense->fill($input); - $expense->expense_date = Utils::toSqlDate($input['expense_date']); - - if (isset($input['private_notes'])) { - $expense->private_notes = trim($input['private_notes']); + if (isset($input['expense_date'])) { + $expense->expense_date = Utils::toSqlDate($input['expense_date']); } - $expense->public_notes = trim($input['public_notes']); + $expense->should_be_invoiced = isset($input['should_be_invoiced']) && floatval($input['should_be_invoiced']) || $expense->client_id ? true : false; if ( ! $expense->expense_currency_id) { @@ -143,7 +149,9 @@ class ExpenseRepository extends BaseRepository $rate = isset($input['exchange_rate']) ? Utils::parseFloat($input['exchange_rate']) : 1; $expense->exchange_rate = round($rate, 4); - $expense->amount = round(Utils::parseFloat($input['amount']), 2); + if (isset($input['amount'])) { + $expense->amount = round(Utils::parseFloat($input['amount']), 2); + } $expense->save(); @@ -173,27 +181,4 @@ class ExpenseRepository extends BaseRepository return $expense; } - - public function bulk($ids, $action) - { - $expenses = Expense::withTrashed()->scope($ids)->get(); - - foreach ($expenses as $expense) { - if ($action == 'restore') { - $expense->restore(); - - $expense->is_deleted = false; - $expense->save(); - } else { - if ($action == 'delete') { - $expense->is_deleted = true; - $expense->save(); - } - - $expense->delete(); - } - } - - return count($tasks); - } } diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index e4ba36b0e6..d020fd6a4d 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -50,6 +50,7 @@ class InvoiceRepository extends BaseRepository ->where('contacts.deleted_at', '=', null) ->where('invoices.is_recurring', '=', false) ->where('contacts.is_primary', '=', true) + //->whereRaw('(clients.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices ->select( DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), @@ -280,7 +281,13 @@ class InvoiceRepository extends BaseRepository } } else { $invoice = Invoice::scope($publicId)->firstOrFail(); - \Log::warning('Entity not set in invoice repo save'); + if (Utils::isNinjaDev()) { + \Log::warning('Entity not set in invoice repo save'); + } + } + + if ($invoice->is_deleted) { + return $invoice; } $invoice->fill($data); @@ -306,9 +313,6 @@ class InvoiceRepository extends BaseRepository if (isset($data['is_amount_discount'])) { $invoice->is_amount_discount = $data['is_amount_discount'] ? true : false; } - if (isset($data['partial'])) { - $invoice->partial = round(Utils::parseFloat($data['partial']), 2); - } if (isset($data['invoice_date_sql'])) { $invoice->invoice_date = $data['invoice_date_sql']; } elseif (isset($data['invoice_date'])) { @@ -477,6 +481,10 @@ class InvoiceRepository extends BaseRepository $invoice->balance = $total; } + if (isset($data['partial'])) { + $invoice->partial = max(0,min(round(Utils::parseFloat($data['partial']), 2), $invoice->balance)); + } + $invoice->amount = $total; $invoice->save(); @@ -653,6 +661,9 @@ class InvoiceRepository extends BaseRepository if ($quotePublicId) { $clone->invoice_type_id = INVOICE_TYPE_STANDARD; $clone->quote_id = $quotePublicId; + if ($account->invoice_terms) { + $clone->terms = $account->invoice_terms; + } } $clone->save(); diff --git a/app/Ninja/Repositories/PaymentRepository.php b/app/Ninja/Repositories/PaymentRepository.php index 1e3ac8a930..f13419f302 100644 --- a/app/Ninja/Repositories/PaymentRepository.php +++ b/app/Ninja/Repositories/PaymentRepository.php @@ -150,11 +150,17 @@ class PaymentRepository extends BaseRepository // do nothing } elseif ($publicId) { $payment = Payment::scope($publicId)->firstOrFail(); - \Log::warning('Entity not set in payment repo save'); + if (Utils::isNinjaDev()) { + \Log::warning('Entity not set in payment repo save'); + } } else { $payment = Payment::createNew(); } + if ($payment->is_deleted) { + return $payment; + } + $paymentTypeId = false; if (isset($input['payment_type_id'])) { $paymentTypeId = $input['payment_type_id'] ? $input['payment_type_id'] : null; diff --git a/app/Ninja/Repositories/ProductRepository.php b/app/Ninja/Repositories/ProductRepository.php index bf20e03171..7371581df2 100644 --- a/app/Ninja/Repositories/ProductRepository.php +++ b/app/Ninja/Repositories/ProductRepository.php @@ -17,15 +17,14 @@ class ProductRepository extends BaseRepository ->get(); } - public function find($accountId) + public function find($accountId, $filter = null) { - return DB::table('products') + $query = DB::table('products') ->leftJoin('tax_rates', function($join) { $join->on('tax_rates.id', '=', 'products.default_tax_rate_id') ->whereNull('tax_rates.deleted_at'); }) ->where('products.account_id', '=', $accountId) - ->where('products.deleted_at', '=', null) ->select( 'products.public_id', 'products.product_key', @@ -35,6 +34,19 @@ class ProductRepository extends BaseRepository 'tax_rates.rate as tax_rate', 'products.deleted_at' ); + + if ($filter) { + $query->where(function ($query) use ($filter) { + $query->where('products.product_key', 'like', '%'.$filter.'%') + ->orWhere('products.notes', 'like', '%'.$filter.'%'); + }); + } + + if (!\Session::get('show_trash:product')) { + $query->where('products.deleted_at', '=', null); + } + + return $query; } public function save($data, $product = null) diff --git a/app/Ninja/Repositories/TaskRepository.php b/app/Ninja/Repositories/TaskRepository.php index db78cba3bf..879cc5c106 100644 --- a/app/Ninja/Repositories/TaskRepository.php +++ b/app/Ninja/Repositories/TaskRepository.php @@ -5,8 +5,13 @@ use Session; use App\Models\Client; use App\Models\Task; -class TaskRepository +class TaskRepository extends BaseRepository { + public function getClassName() + { + return 'App\Models\Task'; + } + public function find($clientPublicId = null, $filter = null) { $query = \DB::table('tasks') @@ -67,12 +72,15 @@ class TaskRepository if ($task) { // do nothing } elseif ($publicId) { - $task = Task::scope($publicId)->firstOrFail(); - \Log::warning('Entity not set in task repo save'); + $task = Task::scope($publicId)->withTrashed()->firstOrFail(); } else { $task = Task::createNew(); } + if ($task->is_deleted) { + return $task; + } + if (isset($data['client']) && $data['client']) { $task->client_id = Client::getPrivateId($data['client']); } @@ -109,26 +117,4 @@ class TaskRepository return $task; } - public function bulk($ids, $action) - { - $tasks = Task::withTrashed()->scope($ids)->get(); - - foreach ($tasks as $task) { - if ($action == 'restore') { - $task->restore(); - - $task->is_deleted = false; - $task->save(); - } else { - if ($action == 'delete') { - $task->is_deleted = true; - $task->save(); - } - - $task->delete(); - } - } - - return count($tasks); - } } diff --git a/app/Ninja/Repositories/VendorRepository.php b/app/Ninja/Repositories/VendorRepository.php index 1f848e0a0b..43500c0f1a 100644 --- a/app/Ninja/Repositories/VendorRepository.php +++ b/app/Ninja/Repositories/VendorRepository.php @@ -1,5 +1,6 @@ with('vendor_contacts')->firstOrFail(); - \Log::warning('Entity not set in vendor repo save'); + if (Utils::isNinjaDev()) { + \Log::warning('Entity not set in vendor repo save'); + } + } + + if ($vendor->is_deleted) { + return $vendor; } $vendor->fill($data); @@ -78,12 +85,22 @@ class VendorRepository extends BaseRepository $first = true; $vendorcontacts = isset($data['vendor_contact']) ? [$data['vendor_contact']] : $data['vendor_contacts']; + $vendorcontactIds = []; foreach ($vendorcontacts as $vendorcontact) { $vendorcontact = $vendor->addVendorContact($vendorcontact, $first); + $vendorcontactIds[] = $vendorcontact->public_id; $first = false; } + if ( ! $vendor->wasRecentlyCreated) { + foreach ($vendor->vendor_contacts as $contact) { + if (!in_array($contact->public_id, $vendorcontactIds)) { + $contact->delete(); + } + } + } + return $vendor; } } diff --git a/app/Ninja/Transformers/ActivityTransformer.php b/app/Ninja/Transformers/ActivityTransformer.php index 5b25f31598..892868bd08 100644 --- a/app/Ninja/Transformers/ActivityTransformer.php +++ b/app/Ninja/Transformers/ActivityTransformer.php @@ -16,7 +16,7 @@ class ActivityTransformer extends EntityTransformer protected $availableIncludes = [ ]; /** - * @param Client $client + * @param Activity $activity * @return array */ public function transform(Activity $activity) @@ -29,7 +29,10 @@ class ActivityTransformer extends EntityTransformer 'invoice_id' => $activity->invoice ? $activity->invoice->public_id : null, 'payment_id' => $activity->payment ? $activity->payment->public_id : null, 'credit_id' => $activity->credit ? $activity->credit->public_id : null, - 'updated_at' => $this->getTimestamp($activity->updated_at) + 'updated_at' => $this->getTimestamp($activity->updated_at), + 'expense_id' => $activity->expense_id ? $activity->expense->public_id : null, + 'is_system' => (bool) $activity->is_system ? $activity->is_system : null, + 'contact_id' => $activity->contact_id ? $activity->contact->public_id : null ]; } } diff --git a/app/Ninja/Transformers/ContactTransformer.php b/app/Ninja/Transformers/ContactTransformer.php index fa01260116..279658979e 100644 --- a/app/Ninja/Transformers/ContactTransformer.php +++ b/app/Ninja/Transformers/ContactTransformer.php @@ -26,4 +26,4 @@ class ContactTransformer extends EntityTransformer 'send_invoice' => (bool) $contact->send_invoice, ]); } -} \ No newline at end of file +} diff --git a/app/Ninja/Transformers/ExpenseTransformer.php b/app/Ninja/Transformers/ExpenseTransformer.php index 5a07d962a8..31248deaa1 100644 --- a/app/Ninja/Transformers/ExpenseTransformer.php +++ b/app/Ninja/Transformers/ExpenseTransformer.php @@ -23,11 +23,16 @@ class ExpenseTransformer extends EntityTransformer 'transaction_id' => $expense->transaction_id, 'bank_id' => $expense->bank_id, 'expense_currency_id' => (int) $expense->expense_currency_id, + 'expense_category_id' => (int) $expense->expense_category_id, 'amount' => (float) $expense->amount, 'expense_date' => $expense->expense_date, 'exchange_rate' => (float) $expense->exchange_rate, 'invoice_currency_id' => (int) $expense->invoice_currency_id, 'is_deleted' => (bool) $expense->is_deleted, + 'tax_name1' => $expense->tax_name1, + 'tax_name2' => $expense->tax_name2, + 'tax_rate1' => $expense->tax_rate1, + 'tax_rate2' => $expense->tax_rate2, 'client_id' => $this->client ? $this->client->public_id : (isset($expense->client->public_id) ? (int) $expense->client->public_id : null), 'invoice_id' => isset($expense->invoice->public_id) ? (int) $expense->invoice->public_id : null, 'vendor_id' => isset($expense->vendor->public_id) ? (int) $expense->vendor->public_id : null, diff --git a/app/Ninja/Transformers/UserAccountTransformer.php b/app/Ninja/Transformers/UserAccountTransformer.php index 32fb1b30af..0009b250e4 100644 --- a/app/Ninja/Transformers/UserAccountTransformer.php +++ b/app/Ninja/Transformers/UserAccountTransformer.php @@ -10,7 +10,7 @@ class UserAccountTransformer extends EntityTransformer ]; protected $tokenName; - + public function __construct(Account $account, $serializer, $tokenName) { parent::__construct($account, $serializer); @@ -31,8 +31,9 @@ class UserAccountTransformer extends EntityTransformer 'name' => $user->account->present()->name, 'token' => $user->account->getToken($user->id, $this->tokenName), 'default_url' => SITE_URL, + 'plan' => $user->account->company->plan, 'logo' => $user->account->logo, 'logo_url' => $user->account->getLogoURL(), ]; } -} \ No newline at end of file +} diff --git a/app/Policies/AccountGatewayPolicy.php b/app/Policies/AccountGatewayPolicy.php index 5fc2d43b10..0cfd295136 100644 --- a/app/Policies/AccountGatewayPolicy.php +++ b/app/Policies/AccountGatewayPolicy.php @@ -23,7 +23,7 @@ class AccountGatewayPolicy extends EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) { + public static function create(User $user, $item) { return $user->hasPermission('admin'); } -} \ No newline at end of file +} diff --git a/app/Policies/BankAccountPolicy.php b/app/Policies/BankAccountPolicy.php index 33ca0a22fe..9c8e6794dc 100644 --- a/app/Policies/BankAccountPolicy.php +++ b/app/Policies/BankAccountPolicy.php @@ -22,7 +22,7 @@ class BankAccountPolicy extends EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) { + public static function create(User $user, $item) { return $user->hasPermission('admin'); } -} \ No newline at end of file +} diff --git a/app/Policies/DocumentPolicy.php b/app/Policies/DocumentPolicy.php index 0af6e80a2a..abb4408004 100644 --- a/app/Policies/DocumentPolicy.php +++ b/app/Policies/DocumentPolicy.php @@ -13,7 +13,7 @@ class DocumentPolicy extends EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) + public static function create(User $user, $item) { return !empty($user); } diff --git a/app/Policies/EntityPolicy.php b/app/Policies/EntityPolicy.php index 04951f35e2..9a5216ec87 100644 --- a/app/Policies/EntityPolicy.php +++ b/app/Policies/EntityPolicy.php @@ -16,7 +16,11 @@ class EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) { + public static function create(User $user, $item) { + if ( ! static::checkModuleEnabled($user, $item)) { + return false; + } + return $user->hasPermission('create_all'); } @@ -27,6 +31,10 @@ class EntityPolicy * @return bool */ public static function edit(User $user, $item) { + if ( ! static::checkModuleEnabled($user, $item)) { + return false; + } + return $user->hasPermission('edit_all') || $user->owns($item); } @@ -37,6 +45,10 @@ class EntityPolicy * @return bool */ public static function view(User $user, $item) { + if ( ! static::checkModuleEnabled($user, $item)) { + return false; + } + return $user->hasPermission('view_all') || $user->owns($item); } @@ -57,4 +69,10 @@ class EntityPolicy public static function editByOwner(User $user, $ownerUserId) { return $user->hasPermission('edit_all') || $user->id == $ownerUserId; } + + private static function checkModuleEnabled(User $user, $item) + { + $entityType = is_string($item) ? $item : $item->getEntityType(); + return $user->account->isModuleEnabled($entityType); + } } diff --git a/app/Policies/ExpenseCategoryPolicy.php b/app/Policies/ExpenseCategoryPolicy.php index bac89b183c..2eeb3d7e96 100644 --- a/app/Policies/ExpenseCategoryPolicy.php +++ b/app/Policies/ExpenseCategoryPolicy.php @@ -11,7 +11,7 @@ class ExpenseCategoryPolicy extends EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) { + public static function create(User $user, $item) { return $user->is_admin; } diff --git a/app/Policies/ExpensePolicy.php b/app/Policies/ExpensePolicy.php index d281d2f407..92a22ac580 100644 --- a/app/Policies/ExpensePolicy.php +++ b/app/Policies/ExpensePolicy.php @@ -10,8 +10,8 @@ class ExpensePolicy extends EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) { - if ( ! parent::create($user)) { + public static function create(User $user, $item) { + if ( ! parent::create($user, $item)) { return false; } diff --git a/app/Policies/GenericEntityPolicy.php b/app/Policies/GenericEntityPolicy.php index 264867a262..fc18c7c550 100644 --- a/app/Policies/GenericEntityPolicy.php +++ b/app/Policies/GenericEntityPolicy.php @@ -25,7 +25,7 @@ class GenericEntityPolicy if (method_exists("App\\Policies\\{$itemType}Policy", 'editByOwner')) { return call_user_func(["App\\Policies\\{$itemType}Policy", 'editByOwner'], $user, $ownerUserId); } - + return false; } @@ -40,7 +40,7 @@ class GenericEntityPolicy if (method_exists("App\\Policies\\{$itemType}Policy", 'viewByOwner')) { return call_user_func(["App\\Policies\\{$itemType}Policy", 'viewByOwner'], $user, $ownerUserId); } - + return false; } @@ -50,11 +50,26 @@ class GenericEntityPolicy * @return bool|mixed */ public static function create(User $user, $itemType) { - $itemType = Utils::getEntityName($itemType); - if (method_exists("App\\Policies\\{$itemType}Policy", 'create')) { - return call_user_func(["App\\Policies\\{$itemType}Policy", 'create'], $user); + $entityName = Utils::getEntityName($itemType); + if (method_exists("App\\Policies\\{$entityName}Policy", 'create')) { + return call_user_func(["App\\Policies\\{$entityName}Policy", 'create'], $user, $itemType); } - + return false; } -} \ No newline at end of file + + /** + * @param User $user + * @param $itemType + * @return bool|mixed + */ + public static function view(User $user, $itemType) { + $entityName = Utils::getEntityName($itemType); + if (method_exists("App\\Policies\\{$entityName}Policy", 'view')) { + return call_user_func(["App\\Policies\\{$entityName}Policy", 'view'], $user, $itemType); + } + + return false; + } + +} diff --git a/app/Policies/PaymentTermPolicy.php b/app/Policies/PaymentTermPolicy.php index 36d1ae210a..e1ee2e494b 100644 --- a/app/Policies/PaymentTermPolicy.php +++ b/app/Policies/PaymentTermPolicy.php @@ -23,7 +23,7 @@ class PaymentTermPolicy extends EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) { + public static function create(User $user, $item) { return $user->hasPermission('admin'); } -} \ No newline at end of file +} diff --git a/app/Policies/ProductPolicy.php b/app/Policies/ProductPolicy.php index 10f8b13eee..23ce11fc75 100644 --- a/app/Policies/ProductPolicy.php +++ b/app/Policies/ProductPolicy.php @@ -10,20 +10,4 @@ use App\Models\User; class ProductPolicy extends EntityPolicy { - /** - * @param User $user - * @param $item - * @return mixed - */ - public static function edit(User $user, $item) { - return $user->hasPermission('admin'); - } - - /** - * @param User $user - * @return mixed - */ - public static function create(User $user) { - return $user->hasPermission('admin'); - } -} \ No newline at end of file +} diff --git a/app/Policies/QuotePolicy.php b/app/Policies/QuotePolicy.php index 8d5f60ea7f..6d1920d16c 100644 --- a/app/Policies/QuotePolicy.php +++ b/app/Policies/QuotePolicy.php @@ -10,8 +10,8 @@ class QuotePolicy extends EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) { - if ( ! parent::create($user)) { + public static function create(User $user, $item) { + if ( ! parent::create($user, $item)) { return false; } diff --git a/app/Policies/TaskPolicy.php b/app/Policies/TaskPolicy.php index 6e7391e031..aa0a2ef762 100644 --- a/app/Policies/TaskPolicy.php +++ b/app/Policies/TaskPolicy.php @@ -10,8 +10,8 @@ class TaskPolicy extends EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) { - if ( ! parent::create($user)) { + public static function create(User $user, $item) { + if ( ! parent::create($user, $item)) { return false; } diff --git a/app/Policies/TaxRatePolicy.php b/app/Policies/TaxRatePolicy.php index 0ce64eca79..5f70694890 100644 --- a/app/Policies/TaxRatePolicy.php +++ b/app/Policies/TaxRatePolicy.php @@ -19,7 +19,7 @@ class TaxRatePolicy extends EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) { + public static function create(User $user, $item) { return $user->hasPermission('admin'); } -} \ No newline at end of file +} diff --git a/app/Policies/TokenPolicy.php b/app/Policies/TokenPolicy.php index fff0ffc1f6..419a288262 100644 --- a/app/Policies/TokenPolicy.php +++ b/app/Policies/TokenPolicy.php @@ -9,7 +9,7 @@ class TokenPolicy extends EntityPolicy { return $user->hasPermission('admin'); } - public static function create(User $user) { + public static function create(User $user, $item) { return $user->hasPermission('admin'); } } diff --git a/app/Policies/VendorPolicy.php b/app/Policies/VendorPolicy.php index 8c38819325..f806ae8260 100644 --- a/app/Policies/VendorPolicy.php +++ b/app/Policies/VendorPolicy.php @@ -10,12 +10,12 @@ class VendorPolicy extends EntityPolicy * @param User $user * @return bool */ - public static function create(User $user) { - if ( ! parent::create($user)) { + public static function create(User $user, $item) { + if ( ! parent::create($user, $item)) { return false; } return $user->hasFeature(FEATURE_EXPENSES); } - + } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ceaa9e89ce..c0c3492a95 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -117,7 +117,7 @@ class AppServiceProvider extends ServiceProvider } if ($status) { - $str .= '    ' . $status; + $str .= $status; } return $str . ''; diff --git a/app/Providers/ComposerServiceProvider.php b/app/Providers/ComposerServiceProvider.php index 40f1a12219..17aeef6caf 100644 --- a/app/Providers/ComposerServiceProvider.php +++ b/app/Providers/ComposerServiceProvider.php @@ -14,14 +14,30 @@ class ComposerServiceProvider extends ServiceProvider public function boot() { view()->composer( - ['accounts.details', 'clients.edit', 'payments.edit', 'invoices.edit', 'accounts.localization'], + [ + 'accounts.details', + 'clients.edit', + 'payments.edit', + 'invoices.edit', + 'accounts.localization' + ], 'App\Http\ViewComposers\TranslationComposer' ); view()->composer( - ['header', 'tasks.edit'], + [ + 'header', + 'tasks.edit' + ], 'App\Http\ViewComposers\AppLanguageComposer' ); + + view()->composer( + [ + 'public.header' + ], + 'App\Http\ViewComposers\ClientPortalHeaderComposer' + ); } /** diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index a383a3ad5e..0561a1a1f6 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -157,9 +157,35 @@ class EventServiceProvider extends ServiceProvider { 'App\Events\TaskWasCreated' => [ 'App\Listeners\ActivityListener@createdTask', ], - 'App\Events\TaskWasUpdated' => [ + 'App\Events\TaskWasUpdated' => [ 'App\Listeners\ActivityListener@updatedTask', ], + 'App\Events\TaskWasRestored' => [ + 'App\Listeners\ActivityListener@restoredTask', + ], + 'App\Events\TaskWasArchived' => [ + 'App\Listeners\ActivityListener@archivedTask', + ], + 'App\Events\TaskWasDeleted' => [ + 'App\Listeners\ActivityListener@deletedTask', + ], + + // Expense events + 'App\Events\ExpenseWasCreated' => [ + 'App\Listeners\ActivityListener@createdExpense', + ], + 'App\Events\ExpenseWasUpdated' => [ + 'App\Listeners\ActivityListener@updatedExpense', + ], + 'App\Events\ExpenseWasRestored' => [ + 'App\Listeners\ActivityListener@restoredExpense', + ], + 'App\Events\ExpenseWasArchived' => [ + 'App\Listeners\ActivityListener@archivedExpense', + ], + 'App\Events\ExpenseWasDeleted' => [ + 'App\Listeners\ActivityListener@deletedExpense', + ], // Update events \Codedge\Updater\Events\UpdateAvailable::class => [ diff --git a/app/Services/AuthService.php b/app/Services/AuthService.php index d3a5502080..58bd185636 100644 --- a/app/Services/AuthService.php +++ b/app/Services/AuthService.php @@ -85,7 +85,7 @@ class AuthService return redirect()->to('login'); } } - + $redirectTo = Input::get('redirect_to') ?: 'dashboard'; return redirect()->to($redirectTo); } diff --git a/app/Services/DatatableService.php b/app/Services/DatatableService.php index 977ca0c7d0..82223ffc32 100644 --- a/app/Services/DatatableService.php +++ b/app/Services/DatatableService.php @@ -77,6 +77,7 @@ class DatatableService if (!$model->deleted_at || $model->deleted_at == '0000-00-00') { foreach ($datatable->actions() as $action) { if (count($action)) { + // if show function isn't set default to true if (count($action) == 2) { $action[] = function() { return true; @@ -84,11 +85,10 @@ class DatatableService } list($value, $url, $visible) = $action; if ($visible($model)) { - if($value == '--divider--'){ + if ($value == '--divider--') { $dropdown_contents .= '
  • '; $lastIsDivider = true; - } - else { + } else { $urlVal = $url($model); $urlStr = is_string($urlVal) ? $urlVal : $urlVal['url']; $attributes = ''; diff --git a/app/Services/ImportService.php b/app/Services/ImportService.php index 67ec500d66..eb03ea768d 100644 --- a/app/Services/ImportService.php +++ b/app/Services/ImportService.php @@ -15,9 +15,15 @@ use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\ProductRepository; +use App\Ninja\Repositories\ExpenseRepository; +use App\Ninja\Repositories\VendorRepository; +use App\Ninja\Repositories\ExpenseCategoryRepository; use App\Ninja\Serializers\ArraySerializer; use App\Models\Client; use App\Models\Invoice; +use App\Models\Vendor; +use App\Models\Expense; +use App\Models\ExpenseCategory; use App\Models\EntityModel; /** @@ -110,7 +116,10 @@ class ImportService InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ContactRepository $contactRepo, - ProductRepository $productRepo + ProductRepository $productRepo, + ExpenseRepository $expenseRepo, + VendorRepository $vendorRepo, + ExpenseCategoryRepository $expenseCategoryRepo ) { $this->fractal = $manager; @@ -121,6 +130,9 @@ class ImportService $this->paymentRepo = $paymentRepo; $this->contactRepo = $contactRepo; $this->productRepo = $productRepo; + $this->expenseRepo = $expenseRepo; + $this->vendorRepo = $vendorRepo; + $this->expenseCategoryRepo = $expenseCategoryRepo; } /** @@ -264,6 +276,25 @@ class ImportService private function transformRow($source, $entityType, $row) { $transformer = $this->getTransformer($source, $entityType, $this->maps); + + // Create expesnse category + if ($entityType == ENTITY_EXPENSE) { + if ( ! empty($row->expense_category)) { + $categoryId = $transformer->getExpenseCategoryId($row->expense_category); + if ( ! $categoryId) { + $category = $this->expenseCategoryRepo->save(['name' => $row->expense_category]); + $this->addExpenseCategoryToMaps($category); + } + } + if ( ! empty($row->vendor)) { + $vendorId = $transformer->getVendorId($row->vendor); + if ( ! $vendorId) { + $vendor = $this->vendorRepo->save(['name' => $row->vendor, 'vendor_contact' => []]); + $this->addVendorToMaps($vendor); + } + } + } + $resource = $transformer->transform($row); if (!$resource) { @@ -462,12 +493,11 @@ class ImportService $title = strtolower($headers[$i]); $mapped[$i] = ''; - if ($hasHeaders) { - foreach ($map as $search => $column) { - if ($this->checkForMatch($title, $search)) { - $mapped[$i] = $column; - break; - } + foreach ($map as $search => $column) { + if ($this->checkForMatch($title, $search)) { + $hasHeaders = true; + $mapped[$i] = $column; + break; } } } @@ -668,6 +698,8 @@ class ImportService 'currencies' => [], 'client_ids' => [], 'invoice_ids' => [], + 'vendors' => [], + 'expense_categories' => [], ]; $clients = $this->clientRepo->all(); @@ -695,6 +727,16 @@ class ImportService foreach ($currencies as $currency) { $this->maps['currencies'][strtolower($currency->code)] = $currency->id; } + + $vendors = $this->vendorRepo->all(); + foreach ($vendors as $vendor) { + $this->addVendorToMaps($vendor); + } + + $expenseCaegories = $this->expenseCategoryRepo->all(); + foreach ($expenseCaegories as $category) { + $this->addExpenseCategoryToMaps($category); + } } /** @@ -729,4 +771,21 @@ class ImportService $this->maps['product'][$key] = $product->id; } } + + private function addExpenseToMaps(Expense $expense) + { + // do nothing + } + + private function addVendorToMaps(Vendor $vendor) + { + $this->maps['vendor'][strtolower($vendor->name)] = $vendor->id; + } + + private function addExpenseCategoryToMaps(ExpenseCategory $category) + { + if ($name = strtolower($category->name)) { + $this->maps['expense_category'][$name] = $category->id; + } + } } diff --git a/app/Services/InvoiceService.php b/app/Services/InvoiceService.php index 0d6bb9822c..82bbfe35d6 100644 --- a/app/Services/InvoiceService.php +++ b/app/Services/InvoiceService.php @@ -86,11 +86,16 @@ class InvoiceService extends BaseService $sendInvoiceIds = []; foreach ($client->contacts as $contact) { - if ($contact->send_invoice || count($client->contacts) == 1) { + if ($contact->send_invoice) { $sendInvoiceIds[] = $contact->id; } } + // if no contacts are selected auto-select the first to enusre there's an invitation + if ( ! count($sendInvoiceIds)) { + $sendInvoiceIds[] = $client->contacts[0]->id; + } + foreach ($client->contacts as $contact) { $invitation = Invitation::scope()->whereContactId($contact->id)->whereInvoiceId($invoice->id)->first(); diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 53d6828688..c1a7c36f04 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -149,7 +149,6 @@ class PaymentService extends BaseService foreach ($payments as $payment) { if (Auth::user()->can('edit', $payment)) { $amount = !empty($params['amount']) ? floatval($params['amount']) : null; - $accountGateway = $payment->account_gateway; if ($accountGateway = $payment->account_gateway) { $paymentDriver = $accountGateway->paymentDriver(); if ($paymentDriver->refundPayment($payment, $amount)) { diff --git a/app/Services/ProductService.php b/app/Services/ProductService.php index 220ac08a60..3fcab2fb35 100644 --- a/app/Services/ProductService.php +++ b/app/Services/ProductService.php @@ -1,5 +1,7 @@ productRepo->find($accountId); + $datatable = new ProductDatatable(true); + $query = $this->productRepo->find($accountId, $search); + + if(!Utils::hasPermission('view_all')){ + $query->where('products.user_id', '=', Auth::user()->id); + } return $this->datatableService->createDatatable($datatable, $query); } diff --git a/bower.json b/bower.json index 3bd9b33598..24abc56e68 100644 --- a/bower.json +++ b/bower.json @@ -29,8 +29,10 @@ "stacktrace-js": "~1.0.1", "fuse.js": "~2.0.2", "dropzone": "~4.3.0", - "sweetalert": "~1.1.3", - "bootstrap-daterangepicker": "~2.1.24" + "nouislider": "~8.5.1", + "bootstrap-daterangepicker": "~2.1.24", + "sweetalert2": "^5.3.8", + "jSignature": "brinley/jSignature#^2.1.0" }, "resolutions": { "jquery": "~1.11" diff --git a/composer.json b/composer.json index 7d95797828..15ff96fa34 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "omnipay/omnipay": "~2.3", "intervention/image": "dev-master", "webpatser/laravel-countries": "dev-master", - "lokielse/omnipay-alipay": "dev-master", + "lokielse/omnipay-alipay": "~1.4", "coatesap/omnipay-datacash": "~2.0", "mfauveau/omnipay-pacnet": "~2.0", "coatesap/omnipay-paymentsense": "2.0.0", @@ -77,7 +77,7 @@ "gatepay/FedACHdir": "dev-master@dev", "websight/l5-google-cloud-storage": "^1.0", "wepay/php-sdk": "^0.2", - "collizo4sky/omnipay-wepay": "dev-additional-calls", + "collizo4sky/omnipay-wepay": "dev-address-fix", "barryvdh/laravel-ide-helper": "~2.2", "barryvdh/laravel-debugbar": "~2.2", "fzaninotto/faker": "^1.5", diff --git a/composer.lock b/composer.lock index 495098c46c..ac97f1ba05 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "cef2393922d37e3206e2feb5674b50eb", - "content-hash": "6577f7bf72c505d13e807f16762a2e10", + "hash": "cf642e3384eec7504bcdace749d2bb88", + "content-hash": "c0a5b571bc2305c4b0d9eae18bf5011b", "packages": [ { "name": "agmscode/omnipay-agms", @@ -1029,12 +1029,12 @@ "source": { "type": "git", "url": "https://github.com/codedge/laravel-selfupdater.git", - "reference": "ef8f30b8084d6b0c2d2587c0b621011a1515d250" + "reference": "b24b155fe0fcccf0ecfbc926a6c3043911418906" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codedge/laravel-selfupdater/zipball/ef8f30b8084d6b0c2d2587c0b621011a1515d250", - "reference": "ef8f30b8084d6b0c2d2587c0b621011a1515d250", + "url": "https://api.github.com/repos/codedge/laravel-selfupdater/zipball/b24b155fe0fcccf0ecfbc926a6c3043911418906", + "reference": "b24b155fe0fcccf0ecfbc926a6c3043911418906", "shasum": "" }, "require": { @@ -1077,20 +1077,20 @@ "self-update", "update" ], - "time": "2016-08-21 16:46:12" + "time": "2016-09-21 12:43:00" }, { "name": "collizo4sky/omnipay-wepay", - "version": "dev-additional-calls", + "version": "dev-address-fix", "source": { "type": "git", "url": "https://github.com/hillelcoren/omnipay-wepay.git", - "reference": "a341b9997d71803d0f774d86908cb49a8bc4c405" + "reference": "916785146c5433e9216f295d09d1cbcec2fdf33a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hillelcoren/omnipay-wepay/zipball/a341b9997d71803d0f774d86908cb49a8bc4c405", - "reference": "a341b9997d71803d0f774d86908cb49a8bc4c405", + "url": "https://api.github.com/repos/hillelcoren/omnipay-wepay/zipball/916785146c5433e9216f295d09d1cbcec2fdf33a", + "reference": "916785146c5433e9216f295d09d1cbcec2fdf33a", "shasum": "" }, "require": { @@ -1120,9 +1120,9 @@ "wepay" ], "support": { - "source": "https://github.com/sometechie/omnipay-wepay/tree/additional-calls" + "source": "https://github.com/hillelcoren/omnipay-wepay/tree/address-fix" }, - "time": "2016-05-25 19:18:42" + "time": "2016-11-01 10:54:54" }, { "name": "container-interop/container-interop", @@ -2184,7 +2184,7 @@ "shasum": null }, "type": "library", - "time": "2016-06-03 12:00:26" + "time": "2016-10-12 12:00:38" }, { "name": "google/apiclient", @@ -3091,6 +3091,7 @@ "purchase", "wechat" ], + "abandoned": "lokielse/omnipay-wechatpay", "time": "2016-05-10 08:43:41" }, { @@ -3782,16 +3783,16 @@ }, { "name": "lokielse/omnipay-alipay", - "version": "dev-master", + "version": "v1.4.13", "source": { "type": "git", "url": "https://github.com/lokielse/omnipay-alipay.git", - "reference": "1769e8747f856cd6f52242dfe22254fc5d7eb68e" + "reference": "294449c11dcba9f2669666f2a3dc44cd07f7597f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lokielse/omnipay-alipay/zipball/1769e8747f856cd6f52242dfe22254fc5d7eb68e", - "reference": "1769e8747f856cd6f52242dfe22254fc5d7eb68e", + "url": "https://api.github.com/repos/lokielse/omnipay-alipay/zipball/294449c11dcba9f2669666f2a3dc44cd07f7597f", + "reference": "294449c11dcba9f2669666f2a3dc44cd07f7597f", "shasum": "" }, "require": { @@ -3831,7 +3832,7 @@ "payment", "purchase" ], - "time": "2016-09-26 06:23:26" + "time": "2016-09-22 10:40:06" }, { "name": "maatwebsite/excel", @@ -7215,16 +7216,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.9", + "version": "v2.8.13", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8" + "reference": "25c576abd4e0f212e678fe8b2bd9a9a98c7ea934" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/889983a79a043dfda68f38c38b6dba092dd49cd8", - "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/25c576abd4e0f212e678fe8b2bd9a9a98c7ea934", + "reference": "25c576abd4e0f212e678fe8b2bd9a9a98c7ea934", "shasum": "" }, "require": { @@ -7271,7 +7272,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-07-28 16:56:28" + "time": "2016-10-13 01:43:15" }, { "name": "symfony/finder", @@ -7324,16 +7325,16 @@ }, { "name": "symfony/http-foundation", - "version": "v2.8.9", + "version": "v2.8.13", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f20bea598906c990eebe3c70a63ca5ed18cdbc11" + "reference": "a6e6c34d337f3c74c39b29c5f54d33023de8897c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f20bea598906c990eebe3c70a63ca5ed18cdbc11", - "reference": "f20bea598906c990eebe3c70a63ca5ed18cdbc11", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a6e6c34d337f3c74c39b29c5f54d33023de8897c", + "reference": "a6e6c34d337f3c74c39b29c5f54d33023de8897c", "shasum": "" }, "require": { @@ -7375,7 +7376,7 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2016-07-30 07:20:35" + "time": "2016-10-24 15:52:36" }, { "name": "symfony/http-kernel", @@ -10638,7 +10639,6 @@ "chumper/datatable": 20, "intervention/image": 20, "webpatser/laravel-countries": 20, - "lokielse/omnipay-alipay": 20, "alfaproject/omnipay-skrill": 20, "omnipay/bitpay": 20, "dwolla/omnipay-dwolla": 20, diff --git a/config/database.php b/config/database.php index 082117ea76..a184796c2b 100644 --- a/config/database.php +++ b/config/database.php @@ -115,7 +115,7 @@ return [ 'cluster' => false, 'default' => [ - 'host' => '127.0.0.1', + 'host' => env('REDIS_HOST', '127.0.0.1'), 'port' => 6379, 'database' => 0, ], diff --git a/config/excel.php b/config/excel.php index 7acea7bc07..3151039dee 100644 --- a/config/excel.php +++ b/config/excel.php @@ -148,7 +148,7 @@ return array( | an array of columns ( array('A', 'B') ) | */ - 'autosize' => true, + 'autosize' => false, /* |-------------------------------------------------------------------------- diff --git a/config/self-update.php b/config/self-update.php index 53d4ac8f0f..89bc029347 100644 --- a/config/self-update.php +++ b/config/self-update.php @@ -45,6 +45,29 @@ return [ ], ], + /* + |-------------------------------------------------------------------------- + | Exclude folders from update + |-------------------------------------------------------------------------- + | + | Specifiy folders which should not be updated and will be skipped during the + | update process. + | + | Here's already a list of good examples to skip. You may want to keep those. + | + */ + + 'exclude_folders' => [ + 'node_modules', + 'bootstrap/cache', + 'bower', + 'storage/app', + 'storage/framework', + 'storage/logs', + 'storage/self-update', + 'vendor', + ], + /* |-------------------------------------------------------------------------- | Event Logging diff --git a/database/migrations/2016_09_05_150625_create_gateway_types.php b/database/migrations/2016_09_05_150625_create_gateway_types.php new file mode 100644 index 0000000000..8010fb9165 --- /dev/null +++ b/database/migrations/2016_09_05_150625_create_gateway_types.php @@ -0,0 +1,70 @@ +increments('id'); + $table->string('alias'); + $table->string('name'); + }); + + Schema::dropIfExists('account_gateway_settings'); + Schema::create('account_gateway_settings', function($table) + { + $table->increments('id'); + + $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('gateway_type_id')->nullable(); + + $table->timestamp('updated_at')->nullable(); + + + $table->unsignedInteger('min_limit')->nullable(); + $table->unsignedInteger('max_limit')->nullable(); + + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('gateway_type_id')->references('id')->on('gateway_types')->onDelete('cascade'); + + }); + + Schema::table('payment_types', function($table) + { + $table->unsignedInteger('gateway_type_id')->nullable(); + }); + + Schema::table('payment_types', function($table) + { + $table->foreign('gateway_type_id')->references('id')->on('gateway_types')->onDelete('cascade'); + }); + } + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('payment_types', function($table) + { + $table->dropForeign('payment_types_gateway_type_id_foreign'); + $table->dropColumn('gateway_type_id'); + }); + + Schema::dropIfExists('account_gateway_settings'); + Schema::dropIfExists('gateway_types'); + } +} diff --git a/database/migrations/2016_10_20_191150_add_expense_to_activities.php b/database/migrations/2016_10_20_191150_add_expense_to_activities.php new file mode 100644 index 0000000000..6b95da88e5 --- /dev/null +++ b/database/migrations/2016_10_20_191150_add_expense_to_activities.php @@ -0,0 +1,65 @@ +unsignedInteger('expense_id')->nullable(); + }); + + Schema::table('accounts', function($table) + { + $table->date('financial_year_start')->nullable(); + $table->smallInteger('enabled_modules')->default(63); + $table->smallInteger('enabled_dashboard_sections')->default(7); + $table->boolean('show_accept_invoice_terms')->default(false); + $table->boolean('show_accept_quote_terms')->default(false); + $table->boolean('require_invoice_signature')->default(false); + $table->boolean('require_quote_signature')->default(false); + }); + + Schema::table('payments', function($table) + { + $table->text('credit_ids')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('activities', function($table) + { + $table->dropColumn('expense_id'); + }); + + Schema::table('accounts', function($table) + { + $table->dropColumn('financial_year_start'); + $table->dropColumn('enabled_modules'); + $table->dropColumn('enabled_dashboard_sections'); + $table->dropColumn('show_accept_invoice_terms'); + $table->dropColumn('show_accept_quote_terms'); + $table->dropColumn('require_invoice_signature'); + $table->dropColumn('require_quote_signature'); + }); + + Schema::table('payments', function($table) + { + $table->dropColumn('credit_ids'); + }); + } +} diff --git a/database/migrations/2016_11_03_113316_add_invoice_signature.php b/database/migrations/2016_11_03_113316_add_invoice_signature.php new file mode 100644 index 0000000000..5a567dcf11 --- /dev/null +++ b/database/migrations/2016_11_03_113316_add_invoice_signature.php @@ -0,0 +1,73 @@ +text('signature_base64')->nullable(); + $table->timestamp('signature_date')->nullable(); + }); + + Schema::table('companies', function($table) + { + $table->string('utm_source')->nullable(); + $table->string('utm_medium')->nullable(); + $table->string('utm_campaign')->nullable(); + $table->string('utm_term')->nullable(); + $table->string('utm_content')->nullable(); + }); + + Schema::table('payment_methods', function($table) + { + $table->dropForeign('payment_methods_account_gateway_token_id_foreign'); + }); + + Schema::table('payment_methods', function($table) + { + $table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens')->onDelete('cascade'); + }); + + Schema::table('payments', function($table) + { + $table->dropForeign('payments_payment_method_id_foreign'); + }); + + Schema::table('payments', function($table) + { + $table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');; + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('invitations', function($table) + { + $table->dropColumn('signature_base64'); + $table->dropColumn('signature_date'); + }); + + Schema::table('companies', function($table) + { + $table->dropColumn('utm_source'); + $table->dropColumn('utm_medium'); + $table->dropColumn('utm_campaign'); + $table->dropColumn('utm_term'); + $table->dropColumn('utm_content'); + }); + } +} diff --git a/database/seeds/CurrenciesSeeder.php b/database/seeds/CurrenciesSeeder.php index b9f7aa801c..cf0b61f618 100644 --- a/database/seeds/CurrenciesSeeder.php +++ b/database/seeds/CurrenciesSeeder.php @@ -60,6 +60,7 @@ class CurrenciesSeeder extends Seeder ['name' => 'Pakistani Rupee', 'code' => 'PKR', 'symbol' => 'Rs ', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['name' => 'Polish Zloty', 'code' => 'PLN', 'symbol' => 'zł', 'precision' => '2', 'thousand_separator' => ' ', 'decimal_separator' => ',', 'swap_currency_symbol' => true], ['name' => 'Sri Lankan Rupee', 'code' => 'LKR', 'symbol' => 'LKR', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.', 'swap_currency_symbol' => true], + //['name' => 'Bahraini Dinar', 'code' => 'BHD', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ]; foreach ($currencies as $currency) { diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php index 1a08e1cc69..e6c3f63204 100644 --- a/database/seeds/DatabaseSeeder.php +++ b/database/seeds/DatabaseSeeder.php @@ -17,6 +17,7 @@ class DatabaseSeeder extends Seeder $this->call('CountriesSeeder'); $this->call('PaymentLibrariesSeeder'); $this->call('FontsSeeder'); + $this->call('GatewayTypesSeeder'); $this->call('BanksSeeder'); $this->call('InvoiceStatusSeeder'); $this->call('PaymentStatusSeeder'); diff --git a/database/seeds/GatewayTypesSeeder.php b/database/seeds/GatewayTypesSeeder.php new file mode 100644 index 0000000000..dc556e89fd --- /dev/null +++ b/database/seeds/GatewayTypesSeeder.php @@ -0,0 +1,29 @@ + 'credit_card', 'name' => 'Credit Card'], + ['alias' => 'bank_transfer', 'name' => 'Bank Transfer'], + ['alias' => 'paypal', 'name' => 'PayPal'], + ['alias' => 'bitcoin', 'name' => 'Bitcoin'], + ['alias' => 'dwolla', 'name' => 'Dwolla'], + ['alias' => 'custom', 'name' => 'Custom'], + ]; + + foreach ($gateway_types as $gateway_type) { + $record = GatewayType::where('name', '=', $gateway_type['name'])->first(); + if (!$record) { + GatewayType::create($gateway_type); + } + } + + } +} diff --git a/database/seeds/PaymentLibrariesSeeder.php b/database/seeds/PaymentLibrariesSeeder.php index a626b446c1..f8a4528ef0 100644 --- a/database/seeds/PaymentLibrariesSeeder.php +++ b/database/seeds/PaymentLibrariesSeeder.php @@ -15,34 +15,34 @@ class PaymentLibrariesSeeder extends Seeder Eloquent::unguard(); $gateways = [ - ['name' => 'Authorize.Net AIM', 'provider' => 'AuthorizeNet_AIM'], - ['name' => 'Authorize.Net SIM', 'provider' => 'AuthorizeNet_SIM', 'payment_library_id' => 2], - ['name' => 'CardSave', 'provider' => 'CardSave'], - ['name' => 'Eway Rapid', 'provider' => 'Eway_RapidShared', 'is_offsite' => true], - ['name' => 'FirstData Connect', 'provider' => 'FirstData_Connect'], - ['name' => 'GoCardless', 'provider' => 'GoCardless', 'is_offsite' => true], - ['name' => 'Migs ThreeParty', 'provider' => 'Migs_ThreeParty'], - ['name' => 'Migs TwoParty', 'provider' => 'Migs_TwoParty'], - ['name' => 'Mollie', 'provider' => 'Mollie', 'is_offsite' => true], - ['name' => 'MultiSafepay', 'provider' => 'MultiSafepay'], - ['name' => 'Netaxept', 'provider' => 'Netaxept'], - ['name' => 'NetBanx', 'provider' => 'NetBanx'], - ['name' => 'PayFast', 'provider' => 'PayFast', 'is_offsite' => true], - ['name' => 'Payflow Pro', 'provider' => 'Payflow_Pro'], - ['name' => 'PaymentExpress PxPay', 'provider' => 'PaymentExpress_PxPay'], - ['name' => 'PaymentExpress PxPost', 'provider' => 'PaymentExpress_PxPost'], - ['name' => 'PayPal Express', 'provider' => 'PayPal_Express', 'is_offsite' => true], - ['name' => 'PayPal Pro', 'provider' => 'PayPal_Pro'], - ['name' => 'Pin', 'provider' => 'Pin'], - ['name' => 'SagePay Direct', 'provider' => 'SagePay_Direct'], - ['name' => 'SagePay Server', 'provider' => 'SagePay_Server'], - ['name' => 'SecurePay DirectPost', 'provider' => 'SecurePay_DirectPost'], - ['name' => 'Stripe', 'provider' => 'Stripe'], - ['name' => 'TargetPay Direct eBanking', 'provider' => 'TargetPay_Directebanking'], - ['name' => 'TargetPay Ideal', 'provider' => 'TargetPay_Ideal'], - ['name' => 'TargetPay Mr Cash', 'provider' => 'TargetPay_Mrcash'], - ['name' => 'TwoCheckout', 'provider' => 'TwoCheckout', 'is_offsite' => true], - ['name' => 'WorldPay', 'provider' => 'WorldPay'], + ['name' => 'Authorize.Net AIM', 'provider' => 'AuthorizeNet_AIM', 'sort_order' => 4], + ['name' => 'Authorize.Net SIM', 'provider' => 'AuthorizeNet_SIM', 'payment_library_id' => 2], + ['name' => 'CardSave', 'provider' => 'CardSave'], + ['name' => 'Eway Rapid', 'provider' => 'Eway_RapidShared', 'is_offsite' => true], + ['name' => 'FirstData Connect', 'provider' => 'FirstData_Connect'], + ['name' => 'GoCardless', 'provider' => 'GoCardless', 'is_offsite' => true], + ['name' => 'Migs ThreeParty', 'provider' => 'Migs_ThreeParty'], + ['name' => 'Migs TwoParty', 'provider' => 'Migs_TwoParty'], + ['name' => 'Mollie', 'provider' => 'Mollie', 'is_offsite' => true, 'sort_order' => 7], + ['name' => 'MultiSafepay', 'provider' => 'MultiSafepay'], + ['name' => 'Netaxept', 'provider' => 'Netaxept'], + ['name' => 'NetBanx', 'provider' => 'NetBanx'], + ['name' => 'PayFast', 'provider' => 'PayFast', 'is_offsite' => true], + ['name' => 'Payflow Pro', 'provider' => 'Payflow_Pro'], + ['name' => 'PaymentExpress PxPay', 'provider' => 'PaymentExpress_PxPay'], + ['name' => 'PaymentExpress PxPost', 'provider' => 'PaymentExpress_PxPost'], + ['name' => 'PayPal Express', 'provider' => 'PayPal_Express', 'is_offsite' => true, 'sort_order' => 3], + ['name' => 'PayPal Pro', 'provider' => 'PayPal_Pro'], + ['name' => 'Pin', 'provider' => 'Pin'], + ['name' => 'SagePay Direct', 'provider' => 'SagePay_Direct'], + ['name' => 'SagePay Server', 'provider' => 'SagePay_Server'], + ['name' => 'SecurePay DirectPost', 'provider' => 'SecurePay_DirectPost'], + ['name' => 'Stripe', 'provider' => 'Stripe', 'sort_order' => 1], + ['name' => 'TargetPay Direct eBanking', 'provider' => 'TargetPay_Directebanking'], + ['name' => 'TargetPay Ideal', 'provider' => 'TargetPay_Ideal'], + ['name' => 'TargetPay Mr Cash', 'provider' => 'TargetPay_Mrcash'], + ['name' => 'TwoCheckout', 'provider' => 'TwoCheckout', 'is_offsite' => true], + ['name' => 'WorldPay', 'provider' => 'WorldPay'], ['name' => 'BeanStream', 'provider' => 'BeanStream', 'payment_library_id' => 2], ['name' => 'Psigate', 'provider' => 'Psigate', 'payment_library_id' => 2], ['name' => 'moolah', 'provider' => 'AuthorizeNet_AIM'], @@ -56,8 +56,8 @@ class PaymentLibrariesSeeder extends Seeder ['name' => 'Realex', 'provider' => 'Realex_Remote'], ['name' => 'Sisow', 'provider' => 'Sisow'], ['name' => 'Skrill', 'provider' => 'Skrill'], - ['name' => 'BitPay', 'provider' => 'BitPay', 'is_offsite' => true], - ['name' => 'Dwolla', 'provider' => 'Dwolla', 'is_offsite' => true], + ['name' => 'BitPay', 'provider' => 'BitPay', 'is_offsite' => true, 'sort_order' => 6], + ['name' => 'Dwolla', 'provider' => 'Dwolla', 'is_offsite' => true, 'sort_order' => 5], ['name' => 'AGMS', 'provider' => 'Agms'], ['name' => 'Barclays', 'provider' => 'BarclaysEpdq\Essential'], ['name' => 'Cardgate', 'provider' => 'Cardgate'], @@ -75,14 +75,14 @@ class PaymentLibrariesSeeder extends Seeder ['name' => 'SecPay', 'provider' => 'SecPay'], ['name' => 'WeChat Express', 'provider' => 'WeChat_Express'], ['name' => 'WePay', 'provider' => 'WePay', 'is_offsite' => false], - ['name' => 'Braintree', 'provider' => 'Braintree'], + ['name' => 'Braintree', 'provider' => 'Braintree', 'sort_order' => 2], + ['name' => 'Custom', 'provider' => 'Custom', 'is_offsite' => true, 'sort_order' => 8], ]; foreach ($gateways as $gateway) { $record = Gateway::where('name', '=', $gateway['name'])->first(); if ($record) { - $record->provider = $gateway['provider']; - $record->is_offsite = isset($gateway['is_offsite']) ? boolval($gateway['is_offsite']) : false; + $record->fill($gateway); $record->save(); } else { Gateway::create($gateway); diff --git a/database/seeds/PaymentTypesSeeder.php b/database/seeds/PaymentTypesSeeder.php index 6ded622845..66addcc8b9 100644 --- a/database/seeds/PaymentTypesSeeder.php +++ b/database/seeds/PaymentTypesSeeder.php @@ -10,32 +10,39 @@ class PaymentTypesSeeder extends Seeder $paymentTypes = [ array('name' => 'Apply Credit'), - array('name' => 'Bank Transfer'), + array('name' => 'Bank Transfer', 'gateway_type_id' => GATEWAY_TYPE_BANK_TRANSFER), array('name' => 'Cash'), - array('name' => 'Debit'), - array('name' => 'ACH'), - array('name' => 'Visa Card'), - array('name' => 'MasterCard'), - array('name' => 'American Express'), - array('name' => 'Discover Card'), - array('name' => 'Diners Card'), - array('name' => 'EuroCard'), - array('name' => 'Nova'), - array('name' => 'Credit Card Other'), - array('name' => 'PayPal'), + array('name' => 'Debit', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'ACH', 'gateway_type_id' => GATEWAY_TYPE_BANK_TRANSFER), + array('name' => 'Visa Card', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'MasterCard', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'American Express', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'Discover Card', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'Diners Card', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'EuroCard', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'Nova', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'Credit Card Other', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'PayPal', 'gateway_type_id' => GATEWAY_TYPE_PAYPAL), array('name' => 'Google Wallet'), array('name' => 'Check'), - array('name' => 'Carte Blanche'), - array('name' => 'UnionPay'), - array('name' => 'JCB'), - array('name' => 'Laser'), - array('name' => 'Maestro'), - array('name' => 'Solo'), - array('name' => 'Switch'), + array('name' => 'Carte Blanche', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'UnionPay', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'JCB', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'Laser', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'Maestro', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'Solo', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), + array('name' => 'Switch', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD), ]; foreach ($paymentTypes as $paymentType) { - if (!DB::table('payment_types')->where('name', '=', $paymentType['name'])->get()) { + $record = PaymentType::where('name', '=', $paymentType['name'])->first(); + + if ( $record) { + $record->name = $paymentType['name']; + $record->gateway_type_id = ! empty($paymentType['gateway_type_id']) ? $paymentType['gateway_type_id'] : null; + + $record->save(); + } else { PaymentType::create($paymentType); } } diff --git a/database/seeds/UpdateSeeder.php b/database/seeds/UpdateSeeder.php index a4dcc2e4f3..eb99890c94 100644 --- a/database/seeds/UpdateSeeder.php +++ b/database/seeds/UpdateSeeder.php @@ -13,6 +13,7 @@ class UpdateSeeder extends Seeder $this->call('PaymentLibrariesSeeder'); $this->call('FontsSeeder'); + $this->call('GatewayTypesSeeder'); $this->call('BanksSeeder'); $this->call('InvoiceStatusSeeder'); $this->call('PaymentStatusSeeder'); diff --git a/docs/images/env_file_api_secret.png b/docs/images/env_file_api_secret.png new file mode 100644 index 0000000000..ca64703059 Binary files /dev/null and b/docs/images/env_file_api_secret.png differ diff --git a/docs/images/iphone_self_hosted.png b/docs/images/iphone_self_hosted.png new file mode 100644 index 0000000000..503e2cc1aa Binary files /dev/null and b/docs/images/iphone_self_hosted.png differ diff --git a/docs/index.rst b/docs/index.rst index aa61e20225..411e67bf98 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Want to find out everything there is to know about how to use your Invoice Ninja .. _basic-features: .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Basic Features introduction @@ -18,12 +18,13 @@ Want to find out everything there is to know about how to use your Invoice Ninja quotes tasks expenses + vendors settings .. _advanced-settings: .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Advanced Settings invoice_settings @@ -35,3 +36,11 @@ Want to find out everything there is to know about how to use your Invoice Ninja data_visualizations api_tokens user_management + +.. _self_host: + +.. toctree:: + :maxdepth: 1 + :caption: Self Host + + iphone_app diff --git a/docs/iphone_app.rst b/docs/iphone_app.rst new file mode 100644 index 0000000000..5cb6dcce34 --- /dev/null +++ b/docs/iphone_app.rst @@ -0,0 +1,49 @@ +iPhone Application +================== + +The Invoice Ninja iPhone application allows a user to connect to their self-hosted Invoice Ninja web application. + +Connecting your iPhone to your self-hosted invoice ninja installation requires a couple of easy steps. + +Web app configuration +""""""""""""""""""""" + +Firstly you'll need to add an additional field to your .env file which is located in the root directory of your self-hosted Invoice Ninja installation. + +The additional field to add is API_SECRET, set this to your own defined alphanumeric string. + +.. image:: images/env_file_api_secret.png + +Save your .env file and now open Invoice Ninja on your iPhone. + + +iPhone configuration +"""""""""""""""""""" + +Once you have completed the in-app purchase to unlock the iPhone to connect to your own server, you'll be presented with two fields. + +The first is the Base URL of your self-hosted installation, ie http://ninja.yourapp.com + +The second field is the API_SECRET, enter in the API_SECRET you used in your .env file. + +.. image:: images/iphone_self_hosted.png + +Click SAVE. + +You should be able to login now from your iPhone! + + +FAQ: +"""" + +Q: I get a HTTP 500 error. + +A: Most likely you have not entered your API_SECRET in your .env file + +Q: I get a HTTP 403 error when i attempt to login with the iPhone. + +A: Most likely your API_SECRET on the iPhone does not match that on your self-hosted installation. + +Q: Do I need to create a token on the server? + +A: No, this is not required. The server will automagically create a token if one does not exist on first login. diff --git a/docs/payments.rst b/docs/payments.rst index 89070a90e1..c1c3ca5183 100644 --- a/docs/payments.rst +++ b/docs/payments.rst @@ -47,9 +47,6 @@ Want to view archived or deleted payments? Check the box marked Show archived/de - **Deleted payments** are displayed with a red Deleted button. To restore deleted payments, hover on the red Deleted button. A gray Select button will appear. Click on the Select arrow, and select Restore payment in the drop-down list. - **Archived payments** are displayed with an orange Archived button. To restore the archived payment, hover on the orange Archived button. A gray Select button will appear. Click on the Select arrow, and choose Restore payment from the drop-down list. To delete an archived payment, select Delete payment from the drop-down list of the Select button. -Enter Payment -^^^^^^^^^^^^^ - You can enter a new payment directly from the Payments list page by clicking on the blue Enter Payment + button located at the top right side of the page. The Payments / Create page will open. Filter diff --git a/docs/recurring_invoices.rst b/docs/recurring_invoices.rst index bfd245b614..a97d0ec12f 100644 --- a/docs/recurring_invoices.rst +++ b/docs/recurring_invoices.rst @@ -26,9 +26,6 @@ Here is a description of the columns in the recurring invoices list, as displaye - **Archive Recurring**: Invoice Click here to archive the recurring invoice. It will be archived and removed from the Recurring Invoices list page. - **Delete Recurring**: Invoice Click here to delete the recurring invoice. It will be deleted and removed from the Recurring Invoices list page. -New Recurring Invoice -^^^^^^^^^^^^^^^^^^^^^ - You can create a new recurring invoice directly from the Recurring Invoices list page by clicking on the blue New Recurring Invoice + button located at the top right side of the page. The Recurring Invoices / Create page will open. Filter diff --git a/docs/reports.rst b/docs/reports.rst index f151bbad1d..130cb4174e 100644 --- a/docs/reports.rst +++ b/docs/reports.rst @@ -1,9 +1,9 @@ -Charts & Reports -================ +Reports +======= It's easy to get caught up in the job you are currently working on, sending invoices and chasing payments. But it's just as important to take a step back from time to time, and to look at the bigger picture of how your freelance business is doing. -The Charts and Reports function helps you do just that. You can define your parameters and extract the exact information you need to generate a range of reports. And the graphical chart display gives you an instant visual understanding of your important business data. +The Reports function helps you do just that. You can define your parameters and extract the exact information you need to generate a range of reports. Report Settings """"""""""""""" @@ -12,13 +12,7 @@ The Report Settings section enables you to set parameters and filter the data to - **Start Date**: Click on the calendar button and select the Start Date for the report. - **End Date**: Click on the calendar button and select the End Date for the report. -- **Report**: To generate a report, check the Enable box. - **Type**: To select the report type, click on the Type field and a drop down menu will open. Select the type of report you want from the available list. -- **Chart**: To generate a chart, check the Enable box. -- **Group by**: To select the required timeframe for the data segmentation (Monthly, Weekly or Daily), click on the Group by field and a drop down menu will open. Select the timeframe you want from the list. -- **Chart type**: To select the chart type, click on the Chart type field and a drop down menu will open. Select the type of chart you want from the available list. - **Run**: Once you've selected all the parameters, click the green Run> button. The extracted data will show in the report display section below. TIP: When you click Run>, the report will generate automatically, and includes only the relevant columns and data, based on the type of reports and dates selected. To view another report, simply change your selections and click Run>. -If you enabled the Chart function, a graphical chart will display below the Report data. - Export To export the report data to Excel or other spreadsheet software, click the blue Export button. The report will automatically download as a .csv file. diff --git a/docs/settings.rst b/docs/settings.rst index 2076824b8b..e45467b890 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -1,27 +1,279 @@ Settings ======== +Intro +""""" + +Welcome to the Basic Settings section, where you can create the foundations of your Invoice Ninja account. All your company information, user details, product entries, notification preferences, tax rates, payment settings, location and currency selections, and so much more, can be found here. This is your first stop to set up and customize your account to your business. And any time you need to modify your settings, edit your details or change your preferences, this is where you'll return. We've made it simple and user friendly to manage your settings, so the Basic Settings section is a place that's fun to come back to. We promise. + +To open the Basic Settings sections, click on the Settings tab, which is the last tab on the Invoice Ninja sidebar. The Basic Settings section will automatically open to the first settings page, Company Details. To move between settings pages, click on the relevant tab on the list situated at the left side of the Basic Settings page. + Company Details """"""""""""""" -In this section, you’ll set up your personal profile’s basic settings including your Users, Personal Logo, Location Details, Currency, and Time Zone. +The company details page is divided into three sections: Details, Address and Email Signature. This covers all the key information about your business. + +Enter the following company details in the relevant field: + +Details +^^^^^^^ + +- **Name**: The company name +- **ID Number**: The business number or personal ID number of the business owner +- **VAT Number**: The company's VAT number (if applicable) +- **Website**: Your company web address +- **Email**: Your company email address +- **Phone**: Your company phone number +- **Logo**: Upload your company logo by clicking on the Choose File button. Browse your computer for the appropriate image file and upload it. Remember, the Invoice Ninja system supports JPEG, GIF and PNG file types. TIP: Once, you've uploaded your logo, the graphic image will appear on the page. Uploaded the wrong image? Got yourself a new logo? Click Remove logo, and upload a new one. +- **Company Size**: Select the company size from the options in the drop down list +- **Industry Select**: the industry your company belongs to from the options in the drop down list + +Address +^^^^^^^ + +Complete the relevant address detail in each field. For the Country field, start typing the name of your country, and the autocomplete function will offer you options to select from in a drop down list. Select the relevant country. To remove the country entry, click the X at the right hand side of the field. + +Email Signature +^^^^^^^^^^^^^^^ + +Create your unique company signature, which will appear on every email that is sent on your behalf from the Invoice Ninja system. First, enter free text in the text box. This may include your company name, address, phone number, website, email address or anything else you can think of. After you've entered the text, use the formatting toolbar to create the look you want. You can select the font, font size, font design (bold, italic, underline, strikethrough), font color, fill, numbering, bullets and alignment. You can also insert hyperlinks, which means you can create clickable links to your website or email address within your email signature. + +When you've completed your Company Details, click the green Save button at the bottom of the page. You can edit these details at any time by returning to the page, changing the details and clicking Save again. + +User Details +"""""""""""" + +As the business owner, you are the primary user of your Invoice Ninja account, so you'll need to define your user details. (Alternatively, if you are setting up the account on behalf of someone else, you'll need to have their details handy.) + +Enter your first name, last name, email address and phone number in the fields provided. + +One Click Log In +^^^^^^^^^^^^^^^^ + +If you want to login to your Invoice Ninja account without a password, you can do so via your other online accounts. By linking your Google, Facebook, GitHub or LinkedIn account to your Invoice Ninja account, you enable one-click login that truly makes life simpler. + +To enable one click login, click the blue Enable button. A drop down menu will open, giving you the option to select an online account. Choose the account you want, and you'll be redirected to the third party website to link your Invoice Ninja account. Follow the prompts to complete the process. + +Referral URL +^^^^^^^^^^^^ + +Want to join our referral program and save money? Get your friends to sign up and receive a whopping discount on your Invoice Ninja fees. To join the referral program, check the Enable box. To find out more about our referral program, go here. + +Change Password +^^^^^^^^^^^^^^^ + +If you ever need to change your password, click on the blue Change Password button. You'll need to fill in your current password, your new password, and you'll need to confirm the password by typing it in a second time. Then click Save and your password is changed. + +When you've finished completing your user details, click the green Save button to save all your information. + +Localization +"""""""""""" + +The Localization page is the place to customize a bunch of settings that make your Invoice Ninja account feel like home. + +- **Currency**: Select your default currency from the list of approximately 50 available currencies. +- **Language**: Select your default language from the list of approximately 20 available languages. +- **Timezone**: Select your time zone from the drop down menu. +- **Date Format**: Select the date format you want from a list of some 13 formatting options. +- **Date/Time Format**: Select the date/time format you want from a list of some 13 formatting options. +- **First Day of the Week**: Select the day that your business week begins. (Different cultures and countries have varied work weeks.) +- **24 Hour Time**: If you prefer to use the 24 hour clock time formatting, check the enable box. + +To save your Localization settings, make sure to click the green Save button. Online Payments """"""""""""""" -When you open an account, this is one of the most important steps you’ll need to take towards getting paid. Here you can choose your payment type, payment gateway, and accepted credit cards. Please note that you must first set up an account with a payment gateway to integrate. We integrate with over 30+ payment gateways: https://www.invoiceninja.com/partners/ +The Online Payments page is where you go to set up and manage your payment gateways. + +If you are using WePay, Stripe or Braintree, you'll need to complete the Payment Settings. (TIP: If you're not using any of these three gateways, you can skip the Payment Settings box.) + +- **Token Billing**: Select your preferred option from the drop down list. + - **Disabled**: Never auto bill + - **Opt In (checkbox is shown but not selected) In the client portal**: The client has the option to check the auto billing box to securely store their credit card/bank account on file. + - **Opt Out (checkbox is shown and selected) In the client portal**: The auto bill option is selected automatically. The client can still choose to uncheck the box and disable storing their credit card/bank account. + - **Always**: Always auto bill + +- **Auto Bill**: Choose the date you prefer to auto bill for the invoice: on the send date OR on the due date. Check the relevant box to make your selection. + +.. TIP:: For bank transfer payments, auto billing occurs on the due date only. + +To save your token billing/auto billing payment settings, click the green Save button. + +Payment Gateways list +^^^^^^^^^^^^^^^^^^^^^ + +To view a current list of your payment gateways, scroll below the Payment Settings box. The Payment Gateways list has two columns: + +- **Name**: The payment gateway name (this is a clickable link that takes you to the Payment Gateway/ Edit page) +- **Action**: You can edit or archive the payment gateway by clicking on the Select button. Choose the desired action from the drop down menu. + +Add Gateway +^^^^^^^^^^^ + +To add a new payment gateway, click on the blue Add Gateway + button. You'll be automatically redirected to the Payment Gateways/ Create page. + +.. TIP:: The Payment Gateways/ Create page is a default page for the WePay gateway. With a fantastic range of payment features, we like to give you the option to use WePay upfront. Of course, we offer more than 50 other payment gateway options. If you prefer to add one of those, scroll to the bottom of the page and click on the gray Use another provider button. You'll be redirected to the general Payment Gateways/ Create page. + +Payment Gateways/ Create – WePay +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Let's take a look at the default Payment Gateways/ Create screen for the WePay system. + +- **First Name**: Enter your first name +- **Last Name**: Enter your last name +- **Email**: Enter your email address +- **Company Name**: Enter the company name. This will appear on the client's credit card bill. +- **Country**: Select United States or Canada +- **Billing address**: Check the preferred option + - **Require client to provide their billing address**: Show the client's address on the payment page + - **Update client's address with the provided details**: The client's address will be updated on the payment page with the details they provide +- **Accepted Credit Cards**: Check the box for the credit cards you accept +- **ACH**: To allow bank transfer payments, check the Enable ACH box +- **I agree**: Check the box to accept WePay terms and conditions. TIP: To continue using WePay, you must agree to the terms and conditions. + +To sign up to WePay or to link an existing WePay account, click the green Sign Up with WePay button. Follow the prompts to complete the process. + +Payment Gateways/ Create – Other Payment Gateways +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To create a new payment gateway besides WePay, scroll to the bottom of the default Payment Gateways/ Create page and click the gray Use another provider button. Complete the details in the Add Gateway box by first selecting the desired payment gateway from the list of more than 50 available options. To do this, click on the arrow in the Gateway field and a drop down list will open. + +After you've finished creating a payment gateway, it will automatically appear on the Payment Gateways list on the Online Payments page. + +.. TIP:: Each payment gateway has different requirements for information. When you select a payment gateway, the box will refresh with the relevant requirements. Follow the prompts and use the Help function on the payment gateway website to complete the process correctly. + +Tax Rates +""""""""" + +Set and apply your tax rates and preferences on the Tax Rates page. + +You can create your tax settings with a few simple checkboxes. Select the relevant preference by checking the applicable boxes from the list: + +- **Enable specifying an invoice tax**: Check this box to apply a tax rate to the whole invoice. +- **Enable specifying line item taxes**: Check this box to apply a tax rate for individual line items. +- **Display line item taxes inline**: Check this box to show the line item tax on the separate item line on the invoice. +- **Include line item taxes in line total**: Check this box to show the item tax included in the total cost of the line item. +- **Enable specifying a second tax rate**: Check this box if you need to apply another tax rate to the invoice. +- **Default tax rate**: Select a default tax rate from the drop down list of pre-defined tax rates. + +Once you've completed your preferences, click the green Save button. + +Tax Rates List +^^^^^^^^^^^^^^ + +Your pre-defined tax rates are displayed in the list below the Tax Settings section. Scroll down to view. The Tax Rates list has two columns: + +- **Name**: The tax rate name (this is a clickable link that takes you to the Tax Rate/ Edit page) +- **Action**: You can edit or archive the tax rate by clicking on the Select button. Choose the desired action from the drop down menu. + +Add Tax Rate +^^^^^^^^^^^^ + +To add a new tax rate, click on the blue Add Tax Rate + button. You'll be automatically redirected to the Tax Rates/ Create page. + +To create a tax rate, complete the two fields: + +- **Name**: Enter the name of the tax rate (Examples: VAT, NY state tax) +- **Rate**: Enter the percentage value of the tax rate + +Click the green Save button to create the tax rate. It will now appear on the tax rates list. Product Library """"""""""""""" -The product library has the ability to store all off the various products you offer to clients, along with cost and a notes section per product. You can add as many products as you’d like, and they will all appear in the ‘item’ dropdown when you create an invoice, saving you time and hassle. Auto-fill and Auto-update settings are available for further ease of use when working with products frequently. +Add products to your product library to make your invoice creation process faster. -Notifications -""""""""""""" +Product Settings +^^^^^^^^^^^^^^^^ -We offer a variety of e-mail notification settings which include whether or not you’d like a notification when an invoice is sent, paid, viewed, or approved. +Set your preferences for your product library with the following options: -Import/Export -""""""""""""" +- **Autofill products**: Check this box to enable autofill of product description and cost when you select the product. +- **Auto-update products**: Check this box to enable automatic updating of the product library entry when you update the product in the invoice. -If you have client data to import or export, this is the place to upload or download. +Product Library List +^^^^^^^^^^^^^^^^^^^^ + +Your pre-defined products are displayed in the list below the Product Settings section. Scroll down to view. The Product Library list has four columns: + +- **Product**: The title of the product (ie. Logo design, 500 words translation, 500g cookies). This is a clickable link that takes you to the Product Library/ Edit page. +- **Description**: A description of the product +- **Unit Cost**: The cost for one unit of the product +- **Action**: You can edit or archive the product by clicking on the Select button. Choose the desired action from the drop down menu. + +Add Product +^^^^^^^^^^^ + +To add a new product, click on the blue Add Product + button. You'll be automatically redirected to the Product Library/ Create page. + +To create a product, complete the fields: + +- **Product**: Enter the product title +- **Notes**: Enter the product description +- **Cost**: Enter the cost per unit + +Click the green Save button to create the product. It will now appear on the product library list. + +Email Notifications +""""""""""""""""""" + +The Invoice Ninja system sends automated emails notifying you about the status of invoices and quotes sent to clients. Set your notification preferences on the Email Notifications page. + +To create your preferences for email notifications, check any of the following options: + +- Email me when an invoice is sent +- Email me when an invoice is viewed +- Email me when an invoice is paid +- Email me when a quote is approved (Pro Plan users only) + +Facebook and Twitter +^^^^^^^^^^^^^^^^^^^^ + +Want to keep up to date with Invoice Ninja updates, features and news? Follow our feeds on Facebook and Twitter by clicking on the Follow buttons. + +To save your email notification preferences, click the green Save button. + +Import / Export +""""""""""""""" + +If you need to import data to your Invoice Ninja account, or, alternatively, if you need to export your Invoice Ninja invoicing data, you can do both from the Import / Export page. + +Import Data +^^^^^^^^^^^ + +To import data, select a source from the Source drop down menu. To upload a client file, invoice file or product file, click the browse button and select the relevant file from your computer. Click the orange Upload button to import the file. + +.. TIP:: In addition to CSV and JSON file types, you can import files from a range of software programs, including Freshbooks, Hiveage, Invoiceable and more. + +Export Data +^^^^^^^^^^^ + +To export data, select a source from the Source drop down menu. TIP: Export file types include CSV, XLS and JSON. Then, select the data you wish to export from the list. Click the blue Download button to export the data. + +Account Management +"""""""""""""""""" + +Whether upgrading, downgrading or cancelling (we hope not!), manage your Invoice Ninja account via this page. +Plan Status + +View your current plan level (Free, Pro or Enterprise) and Renews status here. + +To change your plan status, click the orange Change Plan button. + +Change Plan +^^^^^^^^^^^ + +To change your plan status, click the drop down menu and select your new plan (Free, Pro or Enterprise). Click the blue Change Plan button, and the change will take effect immediately. Changed your mind? Click the gray Go Back button. + +Delete Account +^^^^^^^^^^^^^^ + +In the event that you wish to delete your account, click the red Delete Account button. + +.. NOTE:: If you delete your Invoice Ninja account, all data is permanently erased. There is no undo option. + +You'll be prompted to confirm the delete action, and to provide us with a reason to help us improve the Invoice Ninja experience. + +If you're sure you want to delete, click the red Delete Account button. +Changed your mind? Click the gray Go Back button. diff --git a/docs/tasks.rst b/docs/tasks.rst index 0cf1fa75a1..a13af4cd21 100644 --- a/docs/tasks.rst +++ b/docs/tasks.rst @@ -42,9 +42,6 @@ To view the actions, hover your mouse over the Action area of the relevant task .. TIP:: To sort the tasks list according to any of the columns, click on the orange column tab of your choice. A small arrow will appear. If the arrow is pointing up, data is sorted from lowest to highest value. If the arrow is pointing down, data is sorted from highest to lowest value. Click to change the arrow direction. -Create Task -^^^^^^^^^^^ - You can create a new task directly from the Tasks list page by clicking on the blue New Task + button located at the top right side of the page. The Tasks / Create page will open. Filter diff --git a/docs/vendors.rst b/docs/vendors.rst new file mode 100644 index 0000000000..1ed3285224 --- /dev/null +++ b/docs/vendors.rst @@ -0,0 +1,97 @@ +Vendors +======= + +Running a business is a two way street. Sure, you serve clients and charge them for it. But you also use suppliers, companies and service providers who charge you for all the things you need to get your job done. Office supplies? Check. Coffee machine? Check. Gas for the car? Check. And a hundred-and-one other supplies and services you can name. + +Your expenses and vendors are managed with the same ease and one-click functionality as every other part of your business, right here in your Invoice Ninja account. With Invoice Ninja, managing your vendors and expenses is simple – and similar – to managing your clients and invoices. It's only a matter of which way the money's going... + +List Vendors +"""""""""""" + +To view your Vendors list page, go to the main taskbar and click the Expenses tab. Select Vendors from the drop down menu. + +Overview +^^^^^^^^ + +The Vendors page shows a list in table format of all companies, service providers or suppliers that you have entered to the Invoice Ninja system as part of your business activities. The table features the following data columns: + +- **Vendor:** The company name of the vendor +- **City:** The city where the vendor is located +- **Phone:** The phone number of the vendor +- **Email:** The vendor's email address +- **Date Created:** The date the vendor was created in the system +- **Action column:** The final column to the right features a drop-down menu with a range of actions you can take to manage the selected vendor + +Actions +^^^^^^^ + +To select an action for a particular vendor, hover with your mouse anywhere in the row entry of the vendor. A gray Select button will appear in the far right column. Click on the Select arrow and a drop-down list will open. +When you click on an action, you will be automatically redirected to the relevant action page for the selected vendor. + +Here are the available actions in the drop-down list of the Action button, and the corresponding action pages that will open: + +- **Edit Vendor**: Edit the vendor's details on the Vendors / Edit page +- **Enter Expense**: Enter a new expense for this vendor on the Expenses / Create page TIP: The vendor's name will automatically feature in the new expense. You won't need to enter the vendor's information. +- **Archive Vendor**: Click to archive the vendor +- **Delete Vendor**: Click to delete the vendor + +.. TIP:: Each vendor you create has its own summary page, where you can view and manage the vendor details. To view a vendor's summary page, click on the vendor name in the Vendors list. You'll be automatically redirected to the vendor's summary page. + +You can add a new vendor directly from the Vendors list page by clicking on the blue New Vendor + button located at the top right side of the page. The Vendors / Create page will open. + +Sorting & Filtering Vendors +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The sort and filter functions make it easy for you to manage and view your vendor information. +Sort the vendors table by clicking on any of the tabs in the list header. A small arrow will appear. If the arrow is pointing up, data is sorted from lowest to highest value. If the arrow is pointing down, data is sorted from highest to lowest value. Click to change the arrow direction. + +Filter the vendors list by completing the Filter field, situated at the top right of the page, to the left of the blue New Vendor + button. + +Archiving/Deleting Vendors +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To archive or delete a specific vendor, hover over the vendor entry row, and open the Action drop-down list. Select Archive Vendor or Delete Vendor. The Vendors table will automatically refresh, and the vendor will no longer appear in the vendors list. + +You can also archive or delete one or more vendors via the gray Archive button that appears at the top left side of the Vendors table. To archive or delete vendors, check the relevant vendors in the check boxes that appear in the far left column next to the vendor name. Then click on the Archive button, open the drop-down list and select the desired action. +Want to view archived or deleted vendors? Check the box marked Show archived/deleted, situated to the right of the Archive button. The table will automatically refresh, and will now feature the full list of vendors, including current, archived and deleted vendors. + +- **Deleted vendors** are displayed with a strikethrough and a red Deleted button in the far right column. You can choose to restore a deleted vendor. To restore a deleted vendor, hover with your mouse over the red Deleted button of the relevant deleted vendor. A gray Select button will appear. Click on the Select arrow, and choose Restore vendor from the drop-down list. + +- **Archived vendors** are displayed with an orange Archived button in the far right column. + +You can choose to restore or delete the archived vendor. To restore an archived vendor, hover with your mouse over the orange Archived button of the relevant archived vendor. A gray Select button will appear. Click on the Select arrow, and choose Restore vendor from the drop-down list. To delete an archived vendor, select Delete vendor from the drop-down list of the Select button. + +Create Vendor +""""""""""""" + +To manage your earnings and expenses properly, you need a simple, effective system for every aspect of your accounts. Keeping an updated list and history of your vendors is another part of the puzzle, and, just like your clients, tasks and invoices, Invoice Ninja delivers the goods here too. Create and manage your vendor details with the familiar, fast functionality you've come to expect from Invoice Ninja. + +You can create a new vendor either by going directly to the Vendors/ Create page from the main taskbar, or by clicking New Vendor + on the Vendors list page. Here, we’re going to focus on entering a new vendor via the Vendors/ Create page. + +**Let’s Begin** + +To enter a new vendor, go to the Expenses tab, open the drop-down menu, and click on New Vendor. This will open the Vendors/Create page. + +The Vendors/ Create page is divided into four sections. Enter the information in the relevant fields. + +.. NOTE:: You don’t have to complete every field. Enter the information that is important or necessary for your needs. + +Let’s take a closer look at each section: + +- **Organization**: Enter details about the vendor's company, including the company name, ID number, VAT number, website address and telephone number. +- **Contacts**: Enter the name, email address and phone number of your contact person for this vendor. You can enter as many contact people as you like. To add more contact people, click +Add Contact. +- **Address**: Enter the vendor's street address. +- **Additional Info**: Enter the vendor's currency, and any other private notes or reminders you wish to add (don’t worry - no one can see them but you.) + +Once you have filled in the page, click Save to save the new vendor information. The vendor will now appear as an entry in the Vendors list page. + +Vendor Summary Page +^^^^^^^^^^^^^^^^^^^ + +Each vendor you create has its own summary page that displays the vendor details and related expenses. To view a vendor's summary page, go the Vendors list page and click on the name of the vendor you wish to view. You'll be automatically redirected to the vendor's summary page. + +There are a number of actions you can take from the vendor summary page. Let's check them out: + +- **Edit Vendor**: Edit the vendor's details by clicking on the gray Edit Vendor button. +- **Enter Expense**: Create a new expense for the vendor by clicking on the blue New Expense + button at the top right of the page. +- **Archive or Delete Vendor**: Archive or delete the vendor by clicking on the arrow on the right hand side of the Edit Vendor button. A drop down menu will open. Select the desired action from the menu. diff --git a/gulpfile.js b/gulpfile.js index 79c76add01..02abd7f27c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -49,7 +49,7 @@ elixir(function(mix) { bowerDir + '/font-awesome/css/font-awesome.css', bowerDir + '/dropzone/dist/dropzone.css', bowerDir + '/spectrum/spectrum.css', - bowerDir + '/sweetalert/dist/sweetalert.css', + bowerDir + '/sweetalert2/dist/sweetalert2.css', 'bootstrap-combobox.css', 'typeahead.js-bootstrap.css', 'style.css', @@ -80,6 +80,10 @@ elixir(function(mix) { bowerDir + '/bootstrap-daterangepicker/daterangepicker.js' ], 'public/js/daterangepicker.min.js'); + mix.scripts([ + bowerDir + '/jSignature/libs/jSignature.min.js' + ], 'public/js/jSignature.min.js'); + mix.scripts([ bowerDir + '/jquery/dist/jquery.js', bowerDir + '/jquery-ui/jquery-ui.js', @@ -109,8 +113,11 @@ elixir(function(mix) { bowerDir + '/moment/moment.js', bowerDir + '/moment-timezone/builds/moment-timezone-with-data.js', //bowerDir + '/stacktrace-js/dist/stacktrace-with-polyfills.min.js', + bowerDir + '/es6-promise/es6-promise.auto.js', + bowerDir + '/sweetalert2/dist/sweetalert2.js', + //bowerDir + '/sweetalert/dist/sweetalert-dev.js', + bowerDir + '/nouislider/distribute/nouislider.js', bowerDir + '/fuse.js/src/fuse.js', - bowerDir + '/sweetalert/dist/sweetalert-dev.js', 'bootstrap-combobox.js', 'script.js', 'pdf.pdfmake.js', diff --git a/public/browserconfig.xml b/public/browserconfig.xml index 774ed327e5..74bb89ac30 100644 --- a/public/browserconfig.xml +++ b/public/browserconfig.xml @@ -1,5 +1,9 @@ - - + + + + #da532c + + diff --git a/public/built.js b/public/built.js index f3bb80cbb8..615988d0b3 100644 --- a/public/built.js +++ b/public/built.js @@ -1,29 +1,31 @@ -function generatePDF(t,e,n,i){if(t&&e){if(!n)return refreshTimer&&clearTimeout(refreshTimer),void(refreshTimer=setTimeout(function(){generatePDF(t,e,!0,i)},500));refreshTimer=null,t=calculateAmounts(t);var o=GetPdfMake(t,e,i);return i&&o.getDataUrl(i),o}}function copyObject(t){return!!t&&JSON.parse(JSON.stringify(t))}function processVariables(t){if(!t)return"";for(var e=["MONTH","QUARTER","YEAR"],n=0;n1?c=r.split("+")[1]:r.split("-").length>1&&(c=parseInt(r.split("-")[1])*-1),t=t.replace(r,getDatePart(i,c))}}return t}function getDatePart(t,e){return e=parseInt(e),e||(e=0),"MONTH"==t?getMonth(e):"QUARTER"==t?getQuarter(e):"YEAR"==t?getYear(e):void 0}function getMonth(t){var e=new Date,n=["January","February","March","April","May","June","July","August","September","October","November","December"],i=e.getMonth();return i=parseInt(i)+t,i%=12,i<0&&(i+=12),n[i]}function getYear(t){var e=new Date,n=e.getFullYear();return parseInt(n)+t}function getQuarter(t){var e=new Date,n=Math.floor((e.getMonth()+3)/3);return n+=t,n%=4,0==n&&(n=4),"Q"+n}function isStorageSupported(){try{return"localStorage"in window&&null!==window.localStorage}catch(t){return!1}}function isValidEmailAddress(t){var e=new RegExp(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i);return e.test(t)}function enableHoverClick(t,e,n){}function setAsLink(t,e){e?(t.css("text-decoration","underline"),t.css("cursor","pointer")):(t.css("text-decoration","none"),t.css("cursor","text"))}function setComboboxValue(t,e,n){t.find("input").val(e),t.find("input.form-control").val(n),e&&n?(t.find("select").combobox("setSelected"),t.find(".combobox-container").addClass("combobox-selected")):t.find(".combobox-container").removeClass("combobox-selected")}function convertDataURIToBinary(t){var e=t.indexOf(BASE64_MARKER)+BASE64_MARKER.length,n=t.substring(e);return base64DecToArr(n)}function getContactDisplayName(t){return t.first_name||t.last_name?(t.first_name||"")+" "+(t.last_name||""):t.email}function getClientDisplayName(t){var e=!!t.contacts&&t.contacts[0];return t.name?t.name:e?getContactDisplayName(e):""}function populateInvoiceComboboxes(t,e){for(var n={},i={},o={},a=$("select#client"),s=0;s1?t+=", ":n64&&t<91?t-65:t>96&&t<123?t-71:t>47&&t<58?t+4:43===t?62:47===t?63:0}function base64DecToArr(t,e){for(var n,i,o=t.replace(/[^A-Za-z0-9\+\/]/g,""),a=o.length,s=e?Math.ceil((3*a+1>>2)/e)*e:3*a+1>>2,r=new Uint8Array(s),c=0,l=0,u=0;u>>(16>>>n&24)&255;c=0}return r}function uint6ToB64(t){return t<26?t+65:t<52?t+71:t<62?t-4:62===t?43:63===t?47:65}function base64EncArr(t){for(var e=2,n="",i=t.length,o=0,a=0;a0&&4*a/3%76===0&&(n+="\r\n"),o|=t[a]<<(16>>>e&24),2!==e&&t.length-a!==1||(n+=String.fromCharCode(uint6ToB64(o>>>18&63),uint6ToB64(o>>>12&63),uint6ToB64(o>>>6&63),uint6ToB64(63&o)),o=0);return n.substr(0,n.length-2+e)+(2===e?"":1===e?"=":"==")}function UTF8ArrToStr(t){for(var e,n="",i=t.length,o=0;o251&&e<254&&o+5247&&e<252&&o+4239&&e<248&&o+3223&&e<240&&o+2191&&e<224&&o+1>>6),e[s++]=128+(63&n)):n<65536?(e[s++]=224+(n>>>12),e[s++]=128+(n>>>6&63),e[s++]=128+(63&n)):n<2097152?(e[s++]=240+(n>>>18),e[s++]=128+(n>>>12&63),e[s++]=128+(n>>>6&63),e[s++]=128+(63&n)):n<67108864?(e[s++]=248+(n>>>24),e[s++]=128+(n>>>18&63),e[s++]=128+(n>>>12&63),e[s++]=128+(n>>>6&63),e[s++]=128+(63&n)):(e[s++]=252+n/1073741824,e[s++]=128+(n>>>24&63),e[s++]=128+(n>>>18&63),e[s++]=128+(n>>>12&63),e[s++]=128+(n>>>6&63),e[s++]=128+(63&n));return e}function hexToR(t){return parseInt(cutHex(t).substring(0,2),16)}function hexToG(t){return parseInt(cutHex(t).substring(2,4),16)}function hexToB(t){return parseInt(cutHex(t).substring(4,6),16)}function cutHex(t){return"#"==t.charAt(0)?t.substring(1,7):t}function setDocHexColor(t,e){var n=hexToR(e),i=hexToG(e),o=hexToB(e);return t.setTextColor(n,i,o)}function setDocHexFill(t,e){var n=hexToR(e),i=hexToG(e),o=hexToB(e);return t.setFillColor(n,i,o)}function setDocHexDraw(t,e){var n=hexToR(e),i=hexToG(e),o=hexToB(e);return t.setDrawColor(n,i,o)}function toggleDatePicker(t){$("#"+t).datepicker("show")}function roundToTwo(t,e){var n=+(Math.round(t+"e+2")+"e-2");return e?n.toFixed(2):n||0}function roundToFour(t,e){var n=+(Math.round(t+"e+4")+"e-4");return e?n.toFixed(4):n||0}function truncate(t,e){return t&&t.length>e?t.substr(0,e-1)+"...":t}function endsWith(t,e){return t.indexOf(e,t.length-e.length)!==-1}function secondsToTime(t){t=Math.round(t);var e=Math.floor(t/3600),n=t%3600,i=Math.floor(n/60),o=n%60,a=Math.ceil(o),s={h:e,m:i,s:a};return s}function twoDigits(t){return t<10?"0"+t:t}function toSnakeCase(t){return t?t.replace(/([A-Z])/g,function(t){return"_"+t.toLowerCase()}):""}function snakeToCamel(t){return t.replace(/_([a-z])/g,function(t){return t[1].toUpperCase()})}function getDescendantProp(t,e){for(var n=e.split(".");n.length&&(t=t[n.shift()]););return t}function doubleDollarSign(t){return t?t.replace?t.replace(/\$/g,"$$$"):t:""}function truncate(t,e){return t.length>e?t.substring(0,e)+"...":t}function actionListHandler(){$("tbody tr .tr-action").closest("tr").mouseover(function(){$(this).closest("tr").find(".tr-action").show(),$(this).closest("tr").find(".tr-status").hide()}).mouseout(function(){$dropdown=$(this).closest("tr").find(".tr-action"),$dropdown.hasClass("open")||($dropdown.hide(),$(this).closest("tr").find(".tr-status").show())})}function loadImages(t){$(t+" img").each(function(t,e){var n=$(e).attr("data-src");$(e).attr("src",n),$(e).attr("data-src",n)})}function prettyJson(t){return"string"!=typeof t&&(t=JSON.stringify(t,void 0,2)),t=t.replace(/&/g,"&").replace(//g,">"),t.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,function(t){var e="number";return/^"/.test(t)?e=/:$/.test(t)?"key":"string":/true|false/.test(t)?e="boolean":/null/.test(t)&&(e="null"),t=snakeToCamel(t),''+t+""})}function searchData(t,e,n){return function(i,o){var a;if(n){var s={keys:[e]},r=new Fuse(t,s);a=r.search(i)}else a=[],substrRegex=new RegExp(escapeRegExp(i),"i"),$.each(t,function(t,n){substrRegex.test(n[e])&&a.push(n)});o(a)}}function escapeRegExp(t){return t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}function GetPdfMake(t,e,n){function i(e,n){if("string"==typeof n){if(0===n.indexOf("$firstAndLast")){var i=n.split(":");return function(t,e){return 0===t||t===e.table.body.length?parseFloat(i[1]):0}}if(0===n.indexOf("$none"))return function(t,e){return 0};if(0===n.indexOf("$notFirstAndLastColumn")){var i=n.split(":");return function(t,e){return 0===t||t===e.table.widths.length?0:parseFloat(i[1])}}if(0===n.indexOf("$notFirst")){var i=n.split(":");return function(t,e){return 0===t?0:parseFloat(i[1])}}if(0===n.indexOf("$amount")){var i=n.split(":");return function(t,e){return parseFloat(i[1])}}if(0===n.indexOf("$primaryColor")){var i=n.split(":");return NINJA.primaryColor||i[1]}if(0===n.indexOf("$secondaryColor")){var i=n.split(":");return NINJA.secondaryColor||i[1]}}if(t.features.customize_invoice_design){if("header"===e)return function(e,i){return 1===e||"1"==t.account.all_pages_header?n:""};if("footer"===e)return function(e,i){return e===i||"1"==t.account.all_pages_footer?n:""}}return"text"===e&&(n=NINJA.parseMarkdownText(n,!0)),n}function o(t){window.ninjaFontVfs[t.folder]&&(folder="fonts/"+t.folder,pdfMake.fonts[t.name]={normal:folder+"/"+t.normal,italics:folder+"/"+t.italics,bold:folder+"/"+t.bold,bolditalics:folder+"/"+t.bolditalics})}e=NINJA.decodeJavascript(t,e);var a=JSON.parse(e,i);t.invoice_design_id;if(!t.features.remove_created_by&&!isEdge){var s="function"==typeof a.footer?a.footer():a.footer;if(s)if(s.hasOwnProperty("columns"))s.columns.push({image:logoImages.imageLogo1,alignment:"right",width:130,margin:[0,0,0,0]});else{for(var r,c=0;c0&&e-1 in t))}function i(t,e,n){if(ot.isFunction(e))return ot.grep(t,function(t,i){return!!e.call(t,i,t)!==n});if(e.nodeType)return ot.grep(t,function(t){return t===e!==n});if("string"==typeof e){if(dt.test(e))return ot.filter(e,t,n);e=ot.filter(e,t)}return ot.grep(t,function(t){return ot.inArray(t,e)>=0!==n})}function o(t,e){do t=t[e];while(t&&1!==t.nodeType);return t}function a(t){var e=yt[t]={};return ot.each(t.match(Mt)||[],function(t,n){e[n]=!0}),e}function s(){ft.addEventListener?(ft.removeEventListener("DOMContentLoaded",r,!1),t.removeEventListener("load",r,!1)):(ft.detachEvent("onreadystatechange",r),t.detachEvent("onload",r))}function r(){(ft.addEventListener||"load"===event.type||"complete"===ft.readyState)&&(s(),ot.ready())}function c(t,e,n){if(void 0===n&&1===t.nodeType){var i="data-"+e.replace(Tt,"-$1").toLowerCase();if(n=t.getAttribute(i),"string"==typeof n){try{n="true"===n||"false"!==n&&("null"===n?null:+n+""===n?+n:_t.test(n)?ot.parseJSON(n):n)}catch(o){}ot.data(t,e,n)}else n=void 0}return n}function l(t){var e;for(e in t)if(("data"!==e||!ot.isEmptyObject(t[e]))&&"toJSON"!==e)return!1;return!0}function u(t,e,n,i){if(ot.acceptData(t)){var o,a,s=ot.expando,r=t.nodeType,c=r?ot.cache:t,l=r?t[s]:t[s]&&s;if(l&&c[l]&&(i||c[l].data)||void 0!==n||"string"!=typeof e)return l||(l=r?t[s]=V.pop()||ot.guid++:s),c[l]||(c[l]=r?{}:{toJSON:ot.noop}),"object"!=typeof e&&"function"!=typeof e||(i?c[l]=ot.extend(c[l],e):c[l].data=ot.extend(c[l].data,e)),a=c[l],i||(a.data||(a.data={}),a=a.data),void 0!==n&&(a[ot.camelCase(e)]=n),"string"==typeof e?(o=a[e],null==o&&(o=a[ot.camelCase(e)])):o=a,o}}function h(t,e,n){if(ot.acceptData(t)){var i,o,a=t.nodeType,s=a?ot.cache:t,r=a?t[ot.expando]:ot.expando;if(s[r]){if(e&&(i=n?s[r]:s[r].data)){ot.isArray(e)?e=e.concat(ot.map(e,ot.camelCase)):e in i?e=[e]:(e=ot.camelCase(e),e=e in i?[e]:e.split(" ")),o=e.length;for(;o--;)delete i[e[o]];if(n?!l(i):!ot.isEmptyObject(i))return}(n||(delete s[r].data,l(s[r])))&&(a?ot.cleanData([t],!0):nt.deleteExpando||s!=s.window?delete s[r]:s[r]=null)}}}function d(){return!0}function p(){return!1}function f(){try{return ft.activeElement}catch(t){}}function m(t){var e=Et.split("|"),n=t.createDocumentFragment();if(n.createElement)for(;e.length;)n.createElement(e.pop());return n}function g(t,e){var n,i,o=0,a=typeof t.getElementsByTagName!==zt?t.getElementsByTagName(e||"*"):typeof t.querySelectorAll!==zt?t.querySelectorAll(e||"*"):void 0;if(!a)for(a=[],n=t.childNodes||t;null!=(i=n[o]);o++)!e||ot.nodeName(i,e)?a.push(i):ot.merge(a,g(i,e));return void 0===e||e&&ot.nodeName(t,e)?ot.merge([t],a):a}function b(t){Nt.test(t.type)&&(t.defaultChecked=t.checked)}function v(t,e){return ot.nodeName(t,"table")&&ot.nodeName(11!==e.nodeType?e:e.firstChild,"tr")?t.getElementsByTagName("tbody")[0]||t.appendChild(t.ownerDocument.createElement("tbody")):t}function M(t){return t.type=(null!==ot.find.attr(t,"type"))+"/"+t.type,t}function y(t){var e=Yt.exec(t.type);return e?t.type=e[1]:t.removeAttribute("type"),t}function A(t,e){for(var n,i=0;null!=(n=t[i]);i++)ot._data(n,"globalEval",!e||ot._data(e[i],"globalEval"))}function w(t,e){if(1===e.nodeType&&ot.hasData(t)){var n,i,o,a=ot._data(t),s=ot._data(e,a),r=a.events;if(r){delete s.handle,s.events={};for(n in r)for(i=0,o=r[n].length;i")).appendTo(e.documentElement),e=(Qt[0].contentWindow||Qt[0].contentDocument).document,e.write(),e.close(),n=_(t,e),Qt.detach()),Zt[t]=n),n}function x(t,e){return{get:function(){var n=t();if(null!=n)return n?void delete this.get:(this.get=e).apply(this,arguments)}}}function C(t,e){if(e in t)return e;for(var n=e.charAt(0).toUpperCase()+e.slice(1),i=e,o=de.length;o--;)if(e=de[o]+n,e in t)return e;return i}function O(t,e){for(var n,i,o,a=[],s=0,r=t.length;s=0&&n=0},isEmptyObject:function(t){var e;for(e in t)return!1;return!0},isPlainObject:function(t){var e;if(!t||"object"!==ot.type(t)||t.nodeType||ot.isWindow(t))return!1;try{if(t.constructor&&!et.call(t,"constructor")&&!et.call(t.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}if(nt.ownLast)for(e in t)return et.call(t,e);for(e in t);return void 0===e||et.call(t,e)},type:function(t){return null==t?t+"":"object"==typeof t||"function"==typeof t?Z[tt.call(t)]||"object":typeof t},globalEval:function(e){e&&ot.trim(e)&&(t.execScript||function(e){t.eval.call(t,e)})(e)},camelCase:function(t){return t.replace(st,"ms-").replace(rt,ct)},nodeName:function(t,e){return t.nodeName&&t.nodeName.toLowerCase()===e.toLowerCase()},each:function(t,e,i){var o,a=0,s=t.length,r=n(t);if(i){if(r)for(;aw.cacheLength&&delete t[e.shift()],t[n+" "]=i}var e=[];return t}function i(t){return t[R]=!0,t}function o(t){var e=D.createElement("div");try{return!!t(e)}catch(n){return!1}finally{e.parentNode&&e.parentNode.removeChild(e),e=null}}function a(t,e){for(var n=t.split("|"),i=t.length;i--;)w.attrHandle[n[i]]=e}function s(t,e){var n=e&&t,i=n&&1===t.nodeType&&1===e.nodeType&&(~e.sourceIndex||Y)-(~t.sourceIndex||Y);if(i)return i;if(n)for(;n=n.nextSibling;)if(n===e)return-1;return t?1:-1}function r(t){return function(e){var n=e.nodeName.toLowerCase();return"input"===n&&e.type===t}}function c(t){return function(e){var n=e.nodeName.toLowerCase();return("input"===n||"button"===n)&&e.type===t; -}}function l(t){return i(function(e){return e=+e,i(function(n,i){for(var o,a=t([],n.length,e),s=a.length;s--;)n[o=a[s]]&&(n[o]=!(i[o]=n[o]))})})}function u(t){return t&&"undefined"!=typeof t.getElementsByTagName&&t}function h(){}function d(t){for(var e=0,n=t.length,i="";e1?function(e,n,i){for(var o=t.length;o--;)if(!t[o](e,n,i))return!1;return!0}:t[0]}function m(t,n,i){for(var o=0,a=n.length;o-1&&(i[l]=!(s[l]=h))}}else M=g(M===s?M.splice(f,M.length):M),a?a(null,s,M,c):Q.apply(s,M)})}function v(t){for(var e,n,i,o=t.length,a=w.relative[t[0].type],s=a||w.relative[" "],r=a?1:0,c=p(function(t){return t===e},s,!0),l=p(function(t){return tt(e,t)>-1},s,!0),u=[function(t,n,i){var o=!a&&(i||n!==O)||((e=n).nodeType?c(t,n,i):l(t,n,i));return e=null,o}];r1&&f(u),r>1&&d(t.slice(0,r-1).concat({value:" "===t[r-2].type?"*":""})).replace(ct,"$1"),n,r0,a=t.length>0,s=function(i,s,r,c,l){var u,h,d,p=0,f="0",m=i&&[],b=[],v=O,M=i||a&&w.find.TAG("*",l),y=X+=null==v?1:Math.random()||.1,A=M.length;for(l&&(O=s!==D&&s);f!==A&&null!=(u=M[f]);f++){if(a&&u){for(h=0;d=t[h++];)if(d(u,s,r)){c.push(u);break}l&&(X=y)}o&&((u=!d&&u)&&p--,i&&m.push(u))}if(p+=f,o&&f!==p){for(h=0;d=n[h++];)d(m,b,s,r);if(i){if(p>0)for(;f--;)m[f]||b[f]||(b[f]=G.call(c));b=g(b)}Q.apply(c,b),l&&!i&&b.length>0&&p+n.length>1&&e.uniqueSort(c)}return l&&(X=y,O=v),m};return o?i(s):s}var y,A,w,z,_,T,x,C,O,S,N,L,D,q,k,W,E,B,I,R="sizzle"+1*new Date,P=t.document,X=0,F=0,H=n(),j=n(),U=n(),$=function(t,e){return t===e&&(N=!0),0},Y=1<<31,V={}.hasOwnProperty,J=[],G=J.pop,K=J.push,Q=J.push,Z=J.slice,tt=function(t,e){for(var n=0,i=t.length;n+~]|"+nt+")"+nt+"*"),ht=new RegExp("="+nt+"*([^\\]'\"]*?)"+nt+"*\\]","g"),dt=new RegExp(st),pt=new RegExp("^"+ot+"$"),ft={ID:new RegExp("^#("+it+")"),CLASS:new RegExp("^\\.("+it+")"),TAG:new RegExp("^("+it.replace("w","w*")+")"),ATTR:new RegExp("^"+at),PSEUDO:new RegExp("^"+st),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+nt+"*(even|odd|(([+-]|)(\\d*)n|)"+nt+"*(?:([+-]|)"+nt+"*(\\d+)|))"+nt+"*\\)|)","i"),bool:new RegExp("^(?:"+et+")$","i"),needsContext:new RegExp("^"+nt+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+nt+"*((?:-\\d)?\\d*)"+nt+"*\\)|)(?=[^-]|$)","i")},mt=/^(?:input|select|textarea|button)$/i,gt=/^h\d$/i,bt=/^[^{]+\{\s*\[native \w/,vt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Mt=/[+~]/,yt=/'|\\/g,At=new RegExp("\\\\([\\da-f]{1,6}"+nt+"?|("+nt+")|.)","ig"),wt=function(t,e,n){var i="0x"+e-65536;return i!==i||n?e:i<0?String.fromCharCode(i+65536):String.fromCharCode(i>>10|55296,1023&i|56320)},zt=function(){L()};try{Q.apply(J=Z.call(P.childNodes),P.childNodes),J[P.childNodes.length].nodeType}catch(_t){Q={apply:J.length?function(t,e){K.apply(t,Z.call(e))}:function(t,e){for(var n=t.length,i=0;t[n++]=e[i++];);t.length=n-1}}}A=e.support={},_=e.isXML=function(t){var e=t&&(t.ownerDocument||t).documentElement;return!!e&&"HTML"!==e.nodeName},L=e.setDocument=function(t){var e,n,i=t?t.ownerDocument||t:P;return i!==D&&9===i.nodeType&&i.documentElement?(D=i,q=i.documentElement,n=i.defaultView,n&&n!==n.top&&(n.addEventListener?n.addEventListener("unload",zt,!1):n.attachEvent&&n.attachEvent("onunload",zt)),k=!_(i),A.attributes=o(function(t){return t.className="i",!t.getAttribute("className")}),A.getElementsByTagName=o(function(t){return t.appendChild(i.createComment("")),!t.getElementsByTagName("*").length}),A.getElementsByClassName=bt.test(i.getElementsByClassName),A.getById=o(function(t){return q.appendChild(t).id=R,!i.getElementsByName||!i.getElementsByName(R).length}),A.getById?(w.find.ID=function(t,e){if("undefined"!=typeof e.getElementById&&k){var n=e.getElementById(t);return n&&n.parentNode?[n]:[]}},w.filter.ID=function(t){var e=t.replace(At,wt);return function(t){return t.getAttribute("id")===e}}):(delete w.find.ID,w.filter.ID=function(t){var e=t.replace(At,wt);return function(t){var n="undefined"!=typeof t.getAttributeNode&&t.getAttributeNode("id");return n&&n.value===e}}),w.find.TAG=A.getElementsByTagName?function(t,e){return"undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t):A.qsa?e.querySelectorAll(t):void 0}:function(t,e){var n,i=[],o=0,a=e.getElementsByTagName(t);if("*"===t){for(;n=a[o++];)1===n.nodeType&&i.push(n);return i}return a},w.find.CLASS=A.getElementsByClassName&&function(t,e){if(k)return e.getElementsByClassName(t)},E=[],W=[],(A.qsa=bt.test(i.querySelectorAll))&&(o(function(t){q.appendChild(t).innerHTML="",t.querySelectorAll("[msallowcapture^='']").length&&W.push("[*^$]="+nt+"*(?:''|\"\")"),t.querySelectorAll("[selected]").length||W.push("\\["+nt+"*(?:value|"+et+")"),t.querySelectorAll("[id~="+R+"-]").length||W.push("~="),t.querySelectorAll(":checked").length||W.push(":checked"),t.querySelectorAll("a#"+R+"+*").length||W.push(".#.+[+~]")}),o(function(t){var e=i.createElement("input");e.setAttribute("type","hidden"),t.appendChild(e).setAttribute("name","D"),t.querySelectorAll("[name=d]").length&&W.push("name"+nt+"*[*^$|!~]?="),t.querySelectorAll(":enabled").length||W.push(":enabled",":disabled"),t.querySelectorAll("*,:x"),W.push(",.*:")})),(A.matchesSelector=bt.test(B=q.matches||q.webkitMatchesSelector||q.mozMatchesSelector||q.oMatchesSelector||q.msMatchesSelector))&&o(function(t){A.disconnectedMatch=B.call(t,"div"),B.call(t,"[s!='']:x"),E.push("!=",st)}),W=W.length&&new RegExp(W.join("|")),E=E.length&&new RegExp(E.join("|")),e=bt.test(q.compareDocumentPosition),I=e||bt.test(q.contains)?function(t,e){var n=9===t.nodeType?t.documentElement:t,i=e&&e.parentNode;return t===i||!(!i||1!==i.nodeType||!(n.contains?n.contains(i):t.compareDocumentPosition&&16&t.compareDocumentPosition(i)))}:function(t,e){if(e)for(;e=e.parentNode;)if(e===t)return!0;return!1},$=e?function(t,e){if(t===e)return N=!0,0;var n=!t.compareDocumentPosition-!e.compareDocumentPosition;return n?n:(n=(t.ownerDocument||t)===(e.ownerDocument||e)?t.compareDocumentPosition(e):1,1&n||!A.sortDetached&&e.compareDocumentPosition(t)===n?t===i||t.ownerDocument===P&&I(P,t)?-1:e===i||e.ownerDocument===P&&I(P,e)?1:S?tt(S,t)-tt(S,e):0:4&n?-1:1)}:function(t,e){if(t===e)return N=!0,0;var n,o=0,a=t.parentNode,r=e.parentNode,c=[t],l=[e];if(!a||!r)return t===i?-1:e===i?1:a?-1:r?1:S?tt(S,t)-tt(S,e):0;if(a===r)return s(t,e);for(n=t;n=n.parentNode;)c.unshift(n);for(n=e;n=n.parentNode;)l.unshift(n);for(;c[o]===l[o];)o++;return o?s(c[o],l[o]):c[o]===P?-1:l[o]===P?1:0},i):D},e.matches=function(t,n){return e(t,null,null,n)},e.matchesSelector=function(t,n){if((t.ownerDocument||t)!==D&&L(t),n=n.replace(ht,"='$1']"),A.matchesSelector&&k&&(!E||!E.test(n))&&(!W||!W.test(n)))try{var i=B.call(t,n);if(i||A.disconnectedMatch||t.document&&11!==t.document.nodeType)return i}catch(o){}return e(n,D,null,[t]).length>0},e.contains=function(t,e){return(t.ownerDocument||t)!==D&&L(t),I(t,e)},e.attr=function(t,e){(t.ownerDocument||t)!==D&&L(t);var n=w.attrHandle[e.toLowerCase()],i=n&&V.call(w.attrHandle,e.toLowerCase())?n(t,e,!k):void 0;return void 0!==i?i:A.attributes||!k?t.getAttribute(e):(i=t.getAttributeNode(e))&&i.specified?i.value:null},e.error=function(t){throw new Error("Syntax error, unrecognized expression: "+t)},e.uniqueSort=function(t){var e,n=[],i=0,o=0;if(N=!A.detectDuplicates,S=!A.sortStable&&t.slice(0),t.sort($),N){for(;e=t[o++];)e===t[o]&&(i=n.push(o));for(;i--;)t.splice(n[i],1)}return S=null,t},z=e.getText=function(t){var e,n="",i=0,o=t.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof t.textContent)return t.textContent;for(t=t.firstChild;t;t=t.nextSibling)n+=z(t)}else if(3===o||4===o)return t.nodeValue}else for(;e=t[i++];)n+=z(e);return n},w=e.selectors={cacheLength:50,createPseudo:i,match:ft,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(t){return t[1]=t[1].replace(At,wt),t[3]=(t[3]||t[4]||t[5]||"").replace(At,wt),"~="===t[2]&&(t[3]=" "+t[3]+" "),t.slice(0,4)},CHILD:function(t){return t[1]=t[1].toLowerCase(),"nth"===t[1].slice(0,3)?(t[3]||e.error(t[0]),t[4]=+(t[4]?t[5]+(t[6]||1):2*("even"===t[3]||"odd"===t[3])),t[5]=+(t[7]+t[8]||"odd"===t[3])):t[3]&&e.error(t[0]),t},PSEUDO:function(t){var e,n=!t[6]&&t[2];return ft.CHILD.test(t[0])?null:(t[3]?t[2]=t[4]||t[5]||"":n&&dt.test(n)&&(e=T(n,!0))&&(e=n.indexOf(")",n.length-e)-n.length)&&(t[0]=t[0].slice(0,e),t[2]=n.slice(0,e)),t.slice(0,3))}},filter:{TAG:function(t){var e=t.replace(At,wt).toLowerCase();return"*"===t?function(){return!0}:function(t){return t.nodeName&&t.nodeName.toLowerCase()===e}},CLASS:function(t){var e=H[t+" "];return e||(e=new RegExp("(^|"+nt+")"+t+"("+nt+"|$)"))&&H(t,function(t){return e.test("string"==typeof t.className&&t.className||"undefined"!=typeof t.getAttribute&&t.getAttribute("class")||"")})},ATTR:function(t,n,i){return function(o){var a=e.attr(o,t);return null==a?"!="===n:!n||(a+="","="===n?a===i:"!="===n?a!==i:"^="===n?i&&0===a.indexOf(i):"*="===n?i&&a.indexOf(i)>-1:"$="===n?i&&a.slice(-i.length)===i:"~="===n?(" "+a.replace(rt," ")+" ").indexOf(i)>-1:"|="===n&&(a===i||a.slice(0,i.length+1)===i+"-"))}},CHILD:function(t,e,n,i,o){var a="nth"!==t.slice(0,3),s="last"!==t.slice(-4),r="of-type"===e;return 1===i&&0===o?function(t){return!!t.parentNode}:function(e,n,c){var l,u,h,d,p,f,m=a!==s?"nextSibling":"previousSibling",g=e.parentNode,b=r&&e.nodeName.toLowerCase(),v=!c&&!r;if(g){if(a){for(;m;){for(h=e;h=h[m];)if(r?h.nodeName.toLowerCase()===b:1===h.nodeType)return!1;f=m="only"===t&&!f&&"nextSibling"}return!0}if(f=[s?g.firstChild:g.lastChild],s&&v){for(u=g[R]||(g[R]={}),l=u[t]||[],p=l[0]===X&&l[1],d=l[0]===X&&l[2],h=p&&g.childNodes[p];h=++p&&h&&h[m]||(d=p=0)||f.pop();)if(1===h.nodeType&&++d&&h===e){u[t]=[X,p,d];break}}else if(v&&(l=(e[R]||(e[R]={}))[t])&&l[0]===X)d=l[1];else for(;(h=++p&&h&&h[m]||(d=p=0)||f.pop())&&((r?h.nodeName.toLowerCase()!==b:1!==h.nodeType)||!++d||(v&&((h[R]||(h[R]={}))[t]=[X,d]),h!==e)););return d-=o,d===i||d%i===0&&d/i>=0}}},PSEUDO:function(t,n){var o,a=w.pseudos[t]||w.setFilters[t.toLowerCase()]||e.error("unsupported pseudo: "+t);return a[R]?a(n):a.length>1?(o=[t,t,"",n],w.setFilters.hasOwnProperty(t.toLowerCase())?i(function(t,e){for(var i,o=a(t,n),s=o.length;s--;)i=tt(t,o[s]),t[i]=!(e[i]=o[s])}):function(t){return a(t,0,o)}):a}},pseudos:{not:i(function(t){var e=[],n=[],o=x(t.replace(ct,"$1"));return o[R]?i(function(t,e,n,i){for(var a,s=o(t,null,i,[]),r=t.length;r--;)(a=s[r])&&(t[r]=!(e[r]=a))}):function(t,i,a){return e[0]=t,o(e,null,a,n),e[0]=null,!n.pop()}}),has:i(function(t){return function(n){return e(t,n).length>0}}),contains:i(function(t){return t=t.replace(At,wt),function(e){return(e.textContent||e.innerText||z(e)).indexOf(t)>-1}}),lang:i(function(t){return pt.test(t||"")||e.error("unsupported lang: "+t),t=t.replace(At,wt).toLowerCase(),function(e){var n;do if(n=k?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return n=n.toLowerCase(),n===t||0===n.indexOf(t+"-");while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var n=t.location&&t.location.hash;return n&&n.slice(1)===e.id},root:function(t){return t===q},focus:function(t){return t===D.activeElement&&(!D.hasFocus||D.hasFocus())&&!!(t.type||t.href||~t.tabIndex)},enabled:function(t){return t.disabled===!1},disabled:function(t){return t.disabled===!0},checked:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&!!t.checked||"option"===e&&!!t.selected},selected:function(t){return t.parentNode&&t.parentNode.selectedIndex,t.selected===!0},empty:function(t){for(t=t.firstChild;t;t=t.nextSibling)if(t.nodeType<6)return!1;return!0},parent:function(t){return!w.pseudos.empty(t)},header:function(t){return gt.test(t.nodeName)},input:function(t){return mt.test(t.nodeName)},button:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&"button"===t.type||"button"===e},text:function(t){var e;return"input"===t.nodeName.toLowerCase()&&"text"===t.type&&(null==(e=t.getAttribute("type"))||"text"===e.toLowerCase())},first:l(function(){return[0]}),last:l(function(t,e){return[e-1]}),eq:l(function(t,e,n){return[n<0?n+e:n]}),even:l(function(t,e){for(var n=0;n=0;)t.push(i);return t}),gt:l(function(t,e,n){for(var i=n<0?n+e:n;++i2&&"ID"===(s=a[0]).type&&A.getById&&9===e.nodeType&&k&&w.relative[a[1].type]){if(e=(w.find.ID(s.matches[0].replace(At,wt),e)||[])[0],!e)return n;l&&(e=e.parentNode),t=t.slice(a.shift().value.length)}for(o=ft.needsContext.test(t)?0:a.length;o--&&(s=a[o],!w.relative[r=s.type]);)if((c=w.find[r])&&(i=c(s.matches[0].replace(At,wt),Mt.test(a[0].type)&&u(e.parentNode)||e))){if(a.splice(o,1),t=i.length&&d(a),!t)return Q.apply(n,i),n;break}}return(l||x(t,h))(i,e,!k,n,Mt.test(t)&&u(e.parentNode)||e),n},A.sortStable=R.split("").sort($).join("")===R,A.detectDuplicates=!!N,L(),A.sortDetached=o(function(t){return 1&t.compareDocumentPosition(D.createElement("div"))}),o(function(t){return t.innerHTML="","#"===t.firstChild.getAttribute("href")})||a("type|href|height|width",function(t,e,n){if(!n)return t.getAttribute(e,"type"===e.toLowerCase()?1:2)}),A.attributes&&o(function(t){return t.innerHTML="",t.firstChild.setAttribute("value",""),""===t.firstChild.getAttribute("value")})||a("value",function(t,e,n){if(!n&&"input"===t.nodeName.toLowerCase())return t.defaultValue}),o(function(t){return null==t.getAttribute("disabled")})||a(et,function(t,e,n){var i;if(!n)return t[e]===!0?e.toLowerCase():(i=t.getAttributeNode(e))&&i.specified?i.value:null}),e}(t);ot.find=lt,ot.expr=lt.selectors,ot.expr[":"]=ot.expr.pseudos,ot.unique=lt.uniqueSort,ot.text=lt.getText,ot.isXMLDoc=lt.isXML,ot.contains=lt.contains;var ut=ot.expr.match.needsContext,ht=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,dt=/^.[^:#\[\.,]*$/;ot.filter=function(t,e,n){var i=e[0];return n&&(t=":not("+t+")"),1===e.length&&1===i.nodeType?ot.find.matchesSelector(i,t)?[i]:[]:ot.find.matches(t,ot.grep(e,function(t){return 1===t.nodeType}))},ot.fn.extend({find:function(t){var e,n=[],i=this,o=i.length;if("string"!=typeof t)return this.pushStack(ot(t).filter(function(){for(e=0;e1?ot.unique(n):n),n.selector=this.selector?this.selector+" "+t:t,n},filter:function(t){return this.pushStack(i(this,t||[],!1))},not:function(t){return this.pushStack(i(this,t||[],!0))},is:function(t){return!!i(this,"string"==typeof t&&ut.test(t)?ot(t):t||[],!1).length}});var pt,ft=t.document,mt=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,gt=ot.fn.init=function(t,e){var n,i;if(!t)return this;if("string"==typeof t){if(n="<"===t.charAt(0)&&">"===t.charAt(t.length-1)&&t.length>=3?[null,t,null]:mt.exec(t),!n||!n[1]&&e)return!e||e.jquery?(e||pt).find(t):this.constructor(e).find(t);if(n[1]){if(e=e instanceof ot?e[0]:e,ot.merge(this,ot.parseHTML(n[1],e&&e.nodeType?e.ownerDocument||e:ft,!0)),ht.test(n[1])&&ot.isPlainObject(e))for(n in e)ot.isFunction(this[n])?this[n](e[n]):this.attr(n,e[n]);return this}if(i=ft.getElementById(n[2]),i&&i.parentNode){if(i.id!==n[2])return pt.find(t);this.length=1,this[0]=i}return this.context=ft,this.selector=t,this}return t.nodeType?(this.context=this[0]=t,this.length=1,this):ot.isFunction(t)?"undefined"!=typeof pt.ready?pt.ready(t):t(ot):(void 0!==t.selector&&(this.selector=t.selector,this.context=t.context),ot.makeArray(t,this))};gt.prototype=ot.fn,pt=ot(ft);var bt=/^(?:parents|prev(?:Until|All))/,vt={children:!0,contents:!0,next:!0,prev:!0};ot.extend({dir:function(t,e,n){for(var i=[],o=t[e];o&&9!==o.nodeType&&(void 0===n||1!==o.nodeType||!ot(o).is(n));)1===o.nodeType&&i.push(o),o=o[e];return i},sibling:function(t,e){for(var n=[];t;t=t.nextSibling)1===t.nodeType&&t!==e&&n.push(t);return n}}),ot.fn.extend({has:function(t){var e,n=ot(t,this),i=n.length;return this.filter(function(){for(e=0;e-1:1===n.nodeType&&ot.find.matchesSelector(n,t))){a.push(n);break}return this.pushStack(a.length>1?ot.unique(a):a)},index:function(t){return t?"string"==typeof t?ot.inArray(this[0],ot(t)):ot.inArray(t.jquery?t[0]:t,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,e){return this.pushStack(ot.unique(ot.merge(this.get(),ot(t,e))))},addBack:function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}}),ot.each({parent:function(t){var e=t.parentNode;return e&&11!==e.nodeType?e:null},parents:function(t){return ot.dir(t,"parentNode")},parentsUntil:function(t,e,n){return ot.dir(t,"parentNode",n)},next:function(t){return o(t,"nextSibling")},prev:function(t){return o(t,"previousSibling")},nextAll:function(t){return ot.dir(t,"nextSibling")},prevAll:function(t){return ot.dir(t,"previousSibling")},nextUntil:function(t,e,n){return ot.dir(t,"nextSibling",n)},prevUntil:function(t,e,n){return ot.dir(t,"previousSibling",n)},siblings:function(t){return ot.sibling((t.parentNode||{}).firstChild,t)},children:function(t){return ot.sibling(t.firstChild)},contents:function(t){return ot.nodeName(t,"iframe")?t.contentDocument||t.contentWindow.document:ot.merge([],t.childNodes)}},function(t,e){ot.fn[t]=function(n,i){var o=ot.map(this,e,n);return"Until"!==t.slice(-5)&&(i=n),i&&"string"==typeof i&&(o=ot.filter(i,o)),this.length>1&&(vt[t]||(o=ot.unique(o)),bt.test(t)&&(o=o.reverse())),this.pushStack(o)}});var Mt=/\S+/g,yt={};ot.Callbacks=function(t){t="string"==typeof t?yt[t]||a(t):ot.extend({},t);var e,n,i,o,s,r,c=[],l=!t.once&&[],u=function(a){for(n=t.memory&&a,i=!0,s=r||0,r=0,o=c.length,e=!0;c&&s-1;)c.splice(i,1),e&&(i<=o&&o--,i<=s&&s--)}),this},has:function(t){return t?ot.inArray(t,c)>-1:!(!c||!c.length)},empty:function(){return c=[],o=0,this},disable:function(){return c=l=n=void 0,this},disabled:function(){return!c},lock:function(){return l=void 0,n||h.disable(),this},locked:function(){return!l},fireWith:function(t,n){return!c||i&&!l||(n=n||[],n=[t,n.slice?n.slice():n],e?l.push(n):u(n)),this},fire:function(){return h.fireWith(this,arguments),this},fired:function(){return!!i}};return h},ot.extend({Deferred:function(t){var e=[["resolve","done",ot.Callbacks("once memory"),"resolved"],["reject","fail",ot.Callbacks("once memory"),"rejected"],["notify","progress",ot.Callbacks("memory")]],n="pending",i={state:function(){return n},always:function(){return o.done(arguments).fail(arguments),this},then:function(){var t=arguments;return ot.Deferred(function(n){ot.each(e,function(e,a){var s=ot.isFunction(t[e])&&t[e];o[a[1]](function(){var t=s&&s.apply(this,arguments);t&&ot.isFunction(t.promise)?t.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a[0]+"With"](this===i?n.promise():this,s?[t]:arguments)})}),t=null}).promise()},promise:function(t){return null!=t?ot.extend(t,i):i}},o={};return i.pipe=i.then,ot.each(e,function(t,a){var s=a[2],r=a[3];i[a[1]]=s.add,r&&s.add(function(){n=r},e[1^t][2].disable,e[2][2].lock),o[a[0]]=function(){return o[a[0]+"With"](this===o?i:this,arguments),this},o[a[0]+"With"]=s.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(t){var e,n,i,o=0,a=J.call(arguments),s=a.length,r=1!==s||t&&ot.isFunction(t.promise)?s:0,c=1===r?t:ot.Deferred(),l=function(t,n,i){return function(o){n[t]=this,i[t]=arguments.length>1?J.call(arguments):o,i===e?c.notifyWith(n,i):--r||c.resolveWith(n,i)}};if(s>1)for(e=new Array(s),n=new Array(s),i=new Array(s);o0||(At.resolveWith(ft,[ot]),ot.fn.triggerHandler&&(ot(ft).triggerHandler("ready"),ot(ft).off("ready")))}}}),ot.ready.promise=function(e){if(!At)if(At=ot.Deferred(),"complete"===ft.readyState)setTimeout(ot.ready);else if(ft.addEventListener)ft.addEventListener("DOMContentLoaded",r,!1),t.addEventListener("load",r,!1);else{ft.attachEvent("onreadystatechange",r),t.attachEvent("onload",r);var n=!1;try{n=null==t.frameElement&&ft.documentElement}catch(i){}n&&n.doScroll&&!function o(){if(!ot.isReady){try{n.doScroll("left")}catch(t){return setTimeout(o,50)}s(),ot.ready()}}()}return At.promise(e)};var wt,zt="undefined";for(wt in ot(nt))break;nt.ownLast="0"!==wt,nt.inlineBlockNeedsLayout=!1,ot(function(){var t,e,n,i;n=ft.getElementsByTagName("body")[0],n&&n.style&&(e=ft.createElement("div"),i=ft.createElement("div"),i.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(i).appendChild(e),typeof e.style.zoom!==zt&&(e.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",nt.inlineBlockNeedsLayout=t=3===e.offsetWidth,t&&(n.style.zoom=1)),n.removeChild(i))}),function(){var t=ft.createElement("div");if(null==nt.deleteExpando){nt.deleteExpando=!0;try{delete t.test}catch(e){nt.deleteExpando=!1}}t=null}(),ot.acceptData=function(t){var e=ot.noData[(t.nodeName+" ").toLowerCase()],n=+t.nodeType||1;return(1===n||9===n)&&(!e||e!==!0&&t.getAttribute("classid")===e)};var _t=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Tt=/([A-Z])/g;ot.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(t){return t=t.nodeType?ot.cache[t[ot.expando]]:t[ot.expando],!!t&&!l(t)},data:function(t,e,n){return u(t,e,n)},removeData:function(t,e){return h(t,e)},_data:function(t,e,n){return u(t,e,n,!0)},_removeData:function(t,e){return h(t,e,!0)}}),ot.fn.extend({data:function(t,e){var n,i,o,a=this[0],s=a&&a.attributes;if(void 0===t){if(this.length&&(o=ot.data(a),1===a.nodeType&&!ot._data(a,"parsedAttrs"))){for(n=s.length;n--;)s[n]&&(i=s[n].name,0===i.indexOf("data-")&&(i=ot.camelCase(i.slice(5)),c(a,i,o[i])));ot._data(a,"parsedAttrs",!0)}return o}return"object"==typeof t?this.each(function(){ot.data(this,t)}):arguments.length>1?this.each(function(){ot.data(this,t,e)}):a?c(a,t,ot.data(a,t)):void 0},removeData:function(t){return this.each(function(){ot.removeData(this,t)})}}),ot.extend({queue:function(t,e,n){var i;if(t)return e=(e||"fx")+"queue",i=ot._data(t,e),n&&(!i||ot.isArray(n)?i=ot._data(t,e,ot.makeArray(n)):i.push(n)),i||[]},dequeue:function(t,e){e=e||"fx";var n=ot.queue(t,e),i=n.length,o=n.shift(),a=ot._queueHooks(t,e),s=function(){ot.dequeue(t,e)};"inprogress"===o&&(o=n.shift(),i--),o&&("fx"===e&&n.unshift("inprogress"),delete a.stop,o.call(t,s,a)),!i&&a&&a.empty.fire()},_queueHooks:function(t,e){var n=e+"queueHooks";return ot._data(t,n)||ot._data(t,n,{empty:ot.Callbacks("once memory").add(function(){ot._removeData(t,e+"queue"),ot._removeData(t,n)})})}}),ot.fn.extend({queue:function(t,e){var n=2;return"string"!=typeof t&&(e=t,t="fx",n--),arguments.length
    a",nt.leadingWhitespace=3===e.firstChild.nodeType,nt.tbody=!e.getElementsByTagName("tbody").length,nt.htmlSerialize=!!e.getElementsByTagName("link").length,nt.html5Clone="<:nav>"!==ft.createElement("nav").cloneNode(!0).outerHTML,t.type="checkbox",t.checked=!0,n.appendChild(t),nt.appendChecked=t.checked,e.innerHTML="",nt.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue,n.appendChild(e),e.innerHTML="",nt.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,nt.noCloneEvent=!0,e.attachEvent&&(e.attachEvent("onclick",function(){nt.noCloneEvent=!1}),e.cloneNode(!0).click()),null==nt.deleteExpando){nt.deleteExpando=!0;try{delete e.test}catch(i){nt.deleteExpando=!1}}}(),function(){var e,n,i=ft.createElement("div");for(e in{submit:!0,change:!0,focusin:!0})n="on"+e,(nt[e+"Bubbles"]=n in t)||(i.setAttribute(n,"t"),nt[e+"Bubbles"]=i.attributes[n].expando===!1);i=null}();var Lt=/^(?:input|select|textarea)$/i,Dt=/^key/,qt=/^(?:mouse|pointer|contextmenu)|click/,kt=/^(?:focusinfocus|focusoutblur)$/,Wt=/^([^.]*)(?:\.(.+)|)$/;ot.event={global:{},add:function(t,e,n,i,o){var a,s,r,c,l,u,h,d,p,f,m,g=ot._data(t);if(g){for(n.handler&&(c=n,n=c.handler,o=c.selector),n.guid||(n.guid=ot.guid++),(s=g.events)||(s=g.events={}),(u=g.handle)||(u=g.handle=function(t){return typeof ot===zt||t&&ot.event.triggered===t.type?void 0:ot.event.dispatch.apply(u.elem,arguments)},u.elem=t),e=(e||"").match(Mt)||[""],r=e.length;r--;)a=Wt.exec(e[r])||[],p=m=a[1],f=(a[2]||"").split(".").sort(),p&&(l=ot.event.special[p]||{},p=(o?l.delegateType:l.bindType)||p,l=ot.event.special[p]||{},h=ot.extend({type:p,origType:m,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&ot.expr.match.needsContext.test(o),namespace:f.join(".")},c),(d=s[p])||(d=s[p]=[],d.delegateCount=0,l.setup&&l.setup.call(t,i,f,u)!==!1||(t.addEventListener?t.addEventListener(p,u,!1):t.attachEvent&&t.attachEvent("on"+p,u))),l.add&&(l.add.call(t,h),h.handler.guid||(h.handler.guid=n.guid)),o?d.splice(d.delegateCount++,0,h):d.push(h),ot.event.global[p]=!0);t=null}},remove:function(t,e,n,i,o){var a,s,r,c,l,u,h,d,p,f,m,g=ot.hasData(t)&&ot._data(t);if(g&&(u=g.events)){for(e=(e||"").match(Mt)||[""],l=e.length;l--;)if(r=Wt.exec(e[l])||[],p=m=r[1],f=(r[2]||"").split(".").sort(),p){for(h=ot.event.special[p]||{},p=(i?h.delegateType:h.bindType)||p,d=u[p]||[],r=r[2]&&new RegExp("(^|\\.)"+f.join("\\.(?:.*\\.|)")+"(\\.|$)"),c=a=d.length;a--;)s=d[a],!o&&m!==s.origType||n&&n.guid!==s.guid||r&&!r.test(s.namespace)||i&&i!==s.selector&&("**"!==i||!s.selector)||(d.splice(a,1),s.selector&&d.delegateCount--,h.remove&&h.remove.call(t,s));c&&!d.length&&(h.teardown&&h.teardown.call(t,f,g.handle)!==!1||ot.removeEvent(t,p,g.handle),delete u[p])}else for(p in u)ot.event.remove(t,p+e[l],n,i,!0);ot.isEmptyObject(u)&&(delete g.handle,ot._removeData(t,"events"))}},trigger:function(e,n,i,o){var a,s,r,c,l,u,h,d=[i||ft],p=et.call(e,"type")?e.type:e,f=et.call(e,"namespace")?e.namespace.split("."):[];if(r=u=i=i||ft,3!==i.nodeType&&8!==i.nodeType&&!kt.test(p+ot.event.triggered)&&(p.indexOf(".")>=0&&(f=p.split("."),p=f.shift(),f.sort()),s=p.indexOf(":")<0&&"on"+p,e=e[ot.expando]?e:new ot.Event(p,"object"==typeof e&&e),e.isTrigger=o?2:3,e.namespace=f.join("."),e.namespace_re=e.namespace?new RegExp("(^|\\.)"+f.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=i),n=null==n?[e]:ot.makeArray(n,[e]),l=ot.event.special[p]||{},o||!l.trigger||l.trigger.apply(i,n)!==!1)){if(!o&&!l.noBubble&&!ot.isWindow(i)){for(c=l.delegateType||p,kt.test(c+p)||(r=r.parentNode);r;r=r.parentNode)d.push(r),u=r;u===(i.ownerDocument||ft)&&d.push(u.defaultView||u.parentWindow||t)}for(h=0;(r=d[h++])&&!e.isPropagationStopped();)e.type=h>1?c:l.bindType||p,a=(ot._data(r,"events")||{})[e.type]&&ot._data(r,"handle"),a&&a.apply(r,n),a=s&&r[s],a&&a.apply&&ot.acceptData(r)&&(e.result=a.apply(r,n),e.result===!1&&e.preventDefault());if(e.type=p,!o&&!e.isDefaultPrevented()&&(!l._default||l._default.apply(d.pop(),n)===!1)&&ot.acceptData(i)&&s&&i[p]&&!ot.isWindow(i)){u=i[s],u&&(i[s]=null),ot.event.triggered=p;try{i[p]()}catch(m){}ot.event.triggered=void 0, -u&&(i[s]=u)}return e.result}},dispatch:function(t){t=ot.event.fix(t);var e,n,i,o,a,s=[],r=J.call(arguments),c=(ot._data(this,"events")||{})[t.type]||[],l=ot.event.special[t.type]||{};if(r[0]=t,t.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,t)!==!1){for(s=ot.event.handlers.call(this,t,c),e=0;(o=s[e++])&&!t.isPropagationStopped();)for(t.currentTarget=o.elem,a=0;(i=o.handlers[a++])&&!t.isImmediatePropagationStopped();)t.namespace_re&&!t.namespace_re.test(i.namespace)||(t.handleObj=i,t.data=i.data,n=((ot.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,r),void 0!==n&&(t.result=n)===!1&&(t.preventDefault(),t.stopPropagation()));return l.postDispatch&&l.postDispatch.call(this,t),t.result}},handlers:function(t,e){var n,i,o,a,s=[],r=e.delegateCount,c=t.target;if(r&&c.nodeType&&(!t.button||"click"!==t.type))for(;c!=this;c=c.parentNode||this)if(1===c.nodeType&&(c.disabled!==!0||"click"!==t.type)){for(o=[],a=0;a=0:ot.find(n,this,null,[c]).length),o[n]&&o.push(i);o.length&&s.push({elem:c,handlers:o})}return r]","i"),Rt=/^\s+/,Pt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,Xt=/<([\w:]+)/,Ft=/\s*$/g,Jt={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:nt.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]},Gt=m(ft),Kt=Gt.appendChild(ft.createElement("div"));Jt.optgroup=Jt.option,Jt.tbody=Jt.tfoot=Jt.colgroup=Jt.caption=Jt.thead,Jt.th=Jt.td,ot.extend({clone:function(t,e,n){var i,o,a,s,r,c=ot.contains(t.ownerDocument,t);if(nt.html5Clone||ot.isXMLDoc(t)||!It.test("<"+t.nodeName+">")?a=t.cloneNode(!0):(Kt.innerHTML=t.outerHTML,Kt.removeChild(a=Kt.firstChild)),!(nt.noCloneEvent&&nt.noCloneChecked||1!==t.nodeType&&11!==t.nodeType||ot.isXMLDoc(t)))for(i=g(a),r=g(t),s=0;null!=(o=r[s]);++s)i[s]&&z(o,i[s]);if(e)if(n)for(r=r||g(t),i=i||g(a),s=0;null!=(o=r[s]);s++)w(o,i[s]);else w(t,a);return i=g(a,"script"),i.length>0&&A(i,!c&&g(t,"script")),i=r=o=null,a},buildFragment:function(t,e,n,i){for(var o,a,s,r,c,l,u,h=t.length,d=m(e),p=[],f=0;f")+u[2],o=u[0];o--;)r=r.lastChild;if(!nt.leadingWhitespace&&Rt.test(a)&&p.push(e.createTextNode(Rt.exec(a)[0])),!nt.tbody)for(a="table"!==c||Ft.test(a)?""!==u[1]||Ft.test(a)?0:r:r.firstChild,o=a&&a.childNodes.length;o--;)ot.nodeName(l=a.childNodes[o],"tbody")&&!l.childNodes.length&&a.removeChild(l);for(ot.merge(p,r.childNodes),r.textContent="";r.firstChild;)r.removeChild(r.firstChild);r=d.lastChild}else p.push(e.createTextNode(a));for(r&&d.removeChild(r),nt.appendChecked||ot.grep(g(p,"input"),b),f=0;a=p[f++];)if((!i||ot.inArray(a,i)===-1)&&(s=ot.contains(a.ownerDocument,a),r=g(d.appendChild(a),"script"),s&&A(r),n))for(o=0;a=r[o++];)$t.test(a.type||"")&&n.push(a);return r=null,d},cleanData:function(t,e){for(var n,i,o,a,s=0,r=ot.expando,c=ot.cache,l=nt.deleteExpando,u=ot.event.special;null!=(n=t[s]);s++)if((e||ot.acceptData(n))&&(o=n[r],a=o&&c[o])){if(a.events)for(i in a.events)u[i]?ot.event.remove(n,i):ot.removeEvent(n,i,a.handle);c[o]&&(delete c[o],l?delete n[r]:typeof n.removeAttribute!==zt?n.removeAttribute(r):n[r]=null,V.push(o))}}}),ot.fn.extend({text:function(t){return St(this,function(t){return void 0===t?ot.text(this):this.empty().append((this[0]&&this[0].ownerDocument||ft).createTextNode(t))},null,t,arguments.length)},append:function(){return this.domManip(arguments,function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=v(this,t);e.appendChild(t)}})},prepend:function(){return this.domManip(arguments,function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=v(this,t);e.insertBefore(t,e.firstChild)}})},before:function(){return this.domManip(arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this)})},after:function(){return this.domManip(arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)})},remove:function(t,e){for(var n,i=t?ot.filter(t,this):this,o=0;null!=(n=i[o]);o++)e||1!==n.nodeType||ot.cleanData(g(n)),n.parentNode&&(e&&ot.contains(n.ownerDocument,n)&&A(g(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){for(var t,e=0;null!=(t=this[e]);e++){for(1===t.nodeType&&ot.cleanData(g(t,!1));t.firstChild;)t.removeChild(t.firstChild);t.options&&ot.nodeName(t,"select")&&(t.options.length=0)}return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map(function(){return ot.clone(this,t,e)})},html:function(t){return St(this,function(t){var e=this[0]||{},n=0,i=this.length;if(void 0===t)return 1===e.nodeType?e.innerHTML.replace(Bt,""):void 0;if("string"==typeof t&&!jt.test(t)&&(nt.htmlSerialize||!It.test(t))&&(nt.leadingWhitespace||!Rt.test(t))&&!Jt[(Xt.exec(t)||["",""])[1].toLowerCase()]){t=t.replace(Pt,"<$1>");try{for(;n1&&"string"==typeof d&&!nt.checkClone&&Ut.test(d))return this.each(function(n){var i=u.eq(n);p&&(t[0]=d.call(this,n,i.html())),i.domManip(t,e)});if(l&&(r=ot.buildFragment(t,this[0].ownerDocument,!1,this),n=r.firstChild,1===r.childNodes.length&&(r=n),n)){for(a=ot.map(g(r,"script"),M),o=a.length;c
    t
    ",o=e.getElementsByTagName("td"),o[0].style.cssText="margin:0;border:0;padding:0;display:none",r=0===o[0].offsetHeight,r&&(o[0].style.display="",o[1].style.display="none",r=0===o[0].offsetHeight),n.removeChild(i))}var n,i,o,a,s,r,c;n=ft.createElement("div"),n.innerHTML="
    a",o=n.getElementsByTagName("a")[0],i=o&&o.style,i&&(i.cssText="float:left;opacity:.5",nt.opacity="0.5"===i.opacity,nt.cssFloat=!!i.cssFloat,n.style.backgroundClip="content-box",n.cloneNode(!0).style.backgroundClip="",nt.clearCloneStyle="content-box"===n.style.backgroundClip,nt.boxSizing=""===i.boxSizing||""===i.MozBoxSizing||""===i.WebkitBoxSizing,ot.extend(nt,{reliableHiddenOffsets:function(){return null==r&&e(),r},boxSizingReliable:function(){return null==s&&e(),s},pixelPosition:function(){return null==a&&e(),a},reliableMarginRight:function(){return null==c&&e(),c}}))}(),ot.swap=function(t,e,n,i){var o,a,s={};for(a in e)s[a]=t.style[a],t.style[a]=e[a];o=n.apply(t,i||[]);for(a in e)t.style[a]=s[a];return o};var ae=/alpha\([^)]*\)/i,se=/opacity\s*=\s*([^)]*)/,re=/^(none|table(?!-c[ea]).+)/,ce=new RegExp("^("+xt+")(.*)$","i"),le=new RegExp("^([+-])=("+xt+")","i"),ue={position:"absolute",visibility:"hidden",display:"block"},he={letterSpacing:"0",fontWeight:"400"},de=["Webkit","O","Moz","ms"];ot.extend({cssHooks:{opacity:{get:function(t,e){if(e){var n=ee(t,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":nt.cssFloat?"cssFloat":"styleFloat"},style:function(t,e,n,i){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var o,a,s,r=ot.camelCase(e),c=t.style;if(e=ot.cssProps[r]||(ot.cssProps[r]=C(c,r)),s=ot.cssHooks[e]||ot.cssHooks[r],void 0===n)return s&&"get"in s&&void 0!==(o=s.get(t,!1,i))?o:c[e];if(a=typeof n,"string"===a&&(o=le.exec(n))&&(n=(o[1]+1)*o[2]+parseFloat(ot.css(t,e)),a="number"),null!=n&&n===n&&("number"!==a||ot.cssNumber[r]||(n+="px"),nt.clearCloneStyle||""!==n||0!==e.indexOf("background")||(c[e]="inherit"),!(s&&"set"in s&&void 0===(n=s.set(t,n,i)))))try{c[e]=n}catch(l){}}},css:function(t,e,n,i){var o,a,s,r=ot.camelCase(e);return e=ot.cssProps[r]||(ot.cssProps[r]=C(t.style,r)),s=ot.cssHooks[e]||ot.cssHooks[r],s&&"get"in s&&(a=s.get(t,!0,n)),void 0===a&&(a=ee(t,e,i)),"normal"===a&&e in he&&(a=he[e]),""===n||n?(o=parseFloat(a),n===!0||ot.isNumeric(o)?o||0:a):a}}),ot.each(["height","width"],function(t,e){ot.cssHooks[e]={get:function(t,n,i){if(n)return re.test(ot.css(t,"display"))&&0===t.offsetWidth?ot.swap(t,ue,function(){return L(t,e,i)}):L(t,e,i)},set:function(t,n,i){var o=i&&te(t);return S(t,n,i?N(t,e,i,nt.boxSizing&&"border-box"===ot.css(t,"boxSizing",!1,o),o):0)}}}),nt.opacity||(ot.cssHooks.opacity={get:function(t,e){return se.test((e&&t.currentStyle?t.currentStyle.filter:t.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":e?"1":""},set:function(t,e){var n=t.style,i=t.currentStyle,o=ot.isNumeric(e)?"alpha(opacity="+100*e+")":"",a=i&&i.filter||n.filter||"";n.zoom=1,(e>=1||""===e)&&""===ot.trim(a.replace(ae,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===e||i&&!i.filter)||(n.filter=ae.test(a)?a.replace(ae,o):a+" "+o)}}),ot.cssHooks.marginRight=x(nt.reliableMarginRight,function(t,e){if(e)return ot.swap(t,{display:"inline-block"},ee,[t,"marginRight"])}),ot.each({margin:"",padding:"",border:"Width"},function(t,e){ot.cssHooks[t+e]={expand:function(n){for(var i=0,o={},a="string"==typeof n?n.split(" "):[n];i<4;i++)o[t+Ct[i]+e]=a[i]||a[i-2]||a[0];return o}},ne.test(t)||(ot.cssHooks[t+e].set=S)}),ot.fn.extend({css:function(t,e){return St(this,function(t,e,n){var i,o,a={},s=0;if(ot.isArray(e)){for(i=te(t),o=e.length;s1)},show:function(){return O(this,!0)},hide:function(){return O(this)},toggle:function(t){return"boolean"==typeof t?t?this.show():this.hide():this.each(function(){Ot(this)?ot(this).show():ot(this).hide()})}}),ot.Tween=D,D.prototype={constructor:D,init:function(t,e,n,i,o,a){this.elem=t,this.prop=n,this.easing=o||"swing",this.options=e,this.start=this.now=this.cur(),this.end=i,this.unit=a||(ot.cssNumber[n]?"":"px")},cur:function(){var t=D.propHooks[this.prop];return t&&t.get?t.get(this):D.propHooks._default.get(this)},run:function(t){var e,n=D.propHooks[this.prop];return this.options.duration?this.pos=e=ot.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):D.propHooks._default.set(this),this}},D.prototype.init.prototype=D.prototype,D.propHooks={_default:{get:function(t){var e;return null==t.elem[t.prop]||t.elem.style&&null!=t.elem.style[t.prop]?(e=ot.css(t.elem,t.prop,""),e&&"auto"!==e?e:0):t.elem[t.prop]},set:function(t){ot.fx.step[t.prop]?ot.fx.step[t.prop](t):t.elem.style&&(null!=t.elem.style[ot.cssProps[t.prop]]||ot.cssHooks[t.prop])?ot.style(t.elem,t.prop,t.now+t.unit):t.elem[t.prop]=t.now}}},D.propHooks.scrollTop=D.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},ot.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2}},ot.fx=D.prototype.init,ot.fx.step={};var pe,fe,me=/^(?:toggle|show|hide)$/,ge=new RegExp("^(?:([+-])=|)("+xt+")([a-z%]*)$","i"),be=/queueHooks$/,ve=[E],Me={"*":[function(t,e){var n=this.createTween(t,e),i=n.cur(),o=ge.exec(e),a=o&&o[3]||(ot.cssNumber[t]?"":"px"),s=(ot.cssNumber[t]||"px"!==a&&+i)&&ge.exec(ot.css(n.elem,t)),r=1,c=20;if(s&&s[3]!==a){a=a||s[3],o=o||[],s=+i||1;do r=r||".5",s/=r,ot.style(n.elem,t,s+a);while(r!==(r=n.cur()/i)&&1!==r&&--c)}return o&&(s=n.start=+s||+i||0,n.unit=a,n.end=o[1]?s+(o[1]+1)*o[2]:+o[2]),n}]};ot.Animation=ot.extend(I,{tweener:function(t,e){ot.isFunction(t)?(e=t,t=["*"]):t=t.split(" ");for(var n,i=0,o=t.length;i
    a",i=e.getElementsByTagName("a")[0],n=ft.createElement("select"),o=n.appendChild(ft.createElement("option")),t=e.getElementsByTagName("input")[0],i.style.cssText="top:1px",nt.getSetAttribute="t"!==e.className,nt.style=/top/.test(i.getAttribute("style")),nt.hrefNormalized="/a"===i.getAttribute("href"),nt.checkOn=!!t.value,nt.optSelected=o.selected,nt.enctype=!!ft.createElement("form").enctype,n.disabled=!0,nt.optDisabled=!o.disabled,t=ft.createElement("input"),t.setAttribute("value",""),nt.input=""===t.getAttribute("value"),t.value="t",t.setAttribute("type","radio"),nt.radioValue="t"===t.value}();var ye=/\r/g;ot.fn.extend({val:function(t){var e,n,i,o=this[0];{if(arguments.length)return i=ot.isFunction(t),this.each(function(n){var o;1===this.nodeType&&(o=i?t.call(this,n,ot(this).val()):t,null==o?o="":"number"==typeof o?o+="":ot.isArray(o)&&(o=ot.map(o,function(t){return null==t?"":t+""})),e=ot.valHooks[this.type]||ot.valHooks[this.nodeName.toLowerCase()],e&&"set"in e&&void 0!==e.set(this,o,"value")||(this.value=o))});if(o)return e=ot.valHooks[o.type]||ot.valHooks[o.nodeName.toLowerCase()],e&&"get"in e&&void 0!==(n=e.get(o,"value"))?n:(n=o.value,"string"==typeof n?n.replace(ye,""):null==n?"":n)}}}),ot.extend({valHooks:{option:{get:function(t){var e=ot.find.attr(t,"value");return null!=e?e:ot.trim(ot.text(t))}},select:{get:function(t){for(var e,n,i=t.options,o=t.selectedIndex,a="select-one"===t.type||o<0,s=a?null:[],r=a?o+1:i.length,c=o<0?r:a?o:0;c=0)try{i.selected=n=!0}catch(r){i.scrollHeight}else i.selected=!1;return n||(t.selectedIndex=-1),o}}}}),ot.each(["radio","checkbox"],function(){ot.valHooks[this]={set:function(t,e){if(ot.isArray(e))return t.checked=ot.inArray(ot(t).val(),e)>=0}},nt.checkOn||(ot.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})});var Ae,we,ze=ot.expr.attrHandle,_e=/^(?:checked|selected)$/i,Te=nt.getSetAttribute,xe=nt.input;ot.fn.extend({attr:function(t,e){return St(this,ot.attr,t,e,arguments.length>1)},removeAttr:function(t){return this.each(function(){ot.removeAttr(this,t)})}}),ot.extend({attr:function(t,e,n){var i,o,a=t.nodeType;if(t&&3!==a&&8!==a&&2!==a)return typeof t.getAttribute===zt?ot.prop(t,e,n):(1===a&&ot.isXMLDoc(t)||(e=e.toLowerCase(),i=ot.attrHooks[e]||(ot.expr.match.bool.test(e)?we:Ae)),void 0===n?i&&"get"in i&&null!==(o=i.get(t,e))?o:(o=ot.find.attr(t,e),null==o?void 0:o):null!==n?i&&"set"in i&&void 0!==(o=i.set(t,n,e))?o:(t.setAttribute(e,n+""),n):void ot.removeAttr(t,e))},removeAttr:function(t,e){var n,i,o=0,a=e&&e.match(Mt);if(a&&1===t.nodeType)for(;n=a[o++];)i=ot.propFix[n]||n,ot.expr.match.bool.test(n)?xe&&Te||!_e.test(n)?t[i]=!1:t[ot.camelCase("default-"+n)]=t[i]=!1:ot.attr(t,n,""),t.removeAttribute(Te?n:i)},attrHooks:{type:{set:function(t,e){if(!nt.radioValue&&"radio"===e&&ot.nodeName(t,"input")){var n=t.value;return t.setAttribute("type",e),n&&(t.value=n),e}}}}}),we={set:function(t,e,n){return e===!1?ot.removeAttr(t,n):xe&&Te||!_e.test(n)?t.setAttribute(!Te&&ot.propFix[n]||n,n):t[ot.camelCase("default-"+n)]=t[n]=!0,n}},ot.each(ot.expr.match.bool.source.match(/\w+/g),function(t,e){var n=ze[e]||ot.find.attr;ze[e]=xe&&Te||!_e.test(e)?function(t,e,i){var o,a;return i||(a=ze[e],ze[e]=o,o=null!=n(t,e,i)?e.toLowerCase():null,ze[e]=a),o}:function(t,e,n){if(!n)return t[ot.camelCase("default-"+e)]?e.toLowerCase():null}}),xe&&Te||(ot.attrHooks.value={set:function(t,e,n){return ot.nodeName(t,"input")?void(t.defaultValue=e):Ae&&Ae.set(t,e,n)}}),Te||(Ae={set:function(t,e,n){var i=t.getAttributeNode(n);if(i||t.setAttributeNode(i=t.ownerDocument.createAttribute(n)),i.value=e+="","value"===n||e===t.getAttribute(n))return e}},ze.id=ze.name=ze.coords=function(t,e,n){var i;if(!n)return(i=t.getAttributeNode(e))&&""!==i.value?i.value:null},ot.valHooks.button={get:function(t,e){var n=t.getAttributeNode(e);if(n&&n.specified)return n.value},set:Ae.set},ot.attrHooks.contenteditable={set:function(t,e,n){Ae.set(t,""!==e&&e,n)}},ot.each(["width","height"],function(t,e){ot.attrHooks[e]={set:function(t,n){if(""===n)return t.setAttribute(e,"auto"),n}}})),nt.style||(ot.attrHooks.style={get:function(t){return t.style.cssText||void 0},set:function(t,e){return t.style.cssText=e+""}});var Ce=/^(?:input|select|textarea|button|object)$/i,Oe=/^(?:a|area)$/i;ot.fn.extend({prop:function(t,e){return St(this,ot.prop,t,e,arguments.length>1)},removeProp:function(t){return t=ot.propFix[t]||t,this.each(function(){try{this[t]=void 0,delete this[t]}catch(e){}})}}),ot.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(t,e,n){var i,o,a,s=t.nodeType;if(t&&3!==s&&8!==s&&2!==s)return a=1!==s||!ot.isXMLDoc(t),a&&(e=ot.propFix[e]||e,o=ot.propHooks[e]),void 0!==n?o&&"set"in o&&void 0!==(i=o.set(t,n,e))?i:t[e]=n:o&&"get"in o&&null!==(i=o.get(t,e))?i:t[e]},propHooks:{tabIndex:{get:function(t){var e=ot.find.attr(t,"tabindex");return e?parseInt(e,10):Ce.test(t.nodeName)||Oe.test(t.nodeName)&&t.href?0:-1}}}}),nt.hrefNormalized||ot.each(["href","src"],function(t,e){ot.propHooks[e]={get:function(t){return t.getAttribute(e,4)}}}),nt.optSelected||(ot.propHooks.selected={get:function(t){var e=t.parentNode;return e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex),null}}),ot.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){ot.propFix[this.toLowerCase()]=this}),nt.enctype||(ot.propFix.enctype="encoding");var Se=/[\t\r\n\f]/g;ot.fn.extend({addClass:function(t){var e,n,i,o,a,s,r=0,c=this.length,l="string"==typeof t&&t;if(ot.isFunction(t))return this.each(function(e){ot(this).addClass(t.call(this,e,this.className))});if(l)for(e=(t||"").match(Mt)||[];r=0;)i=i.replace(" "+o+" "," ");s=t?ot.trim(i):"",n.className!==s&&(n.className=s)}return this},toggleClass:function(t,e){var n=typeof t;return"boolean"==typeof e&&"string"===n?e?this.addClass(t):this.removeClass(t):ot.isFunction(t)?this.each(function(n){ -ot(this).toggleClass(t.call(this,n,this.className,e),e)}):this.each(function(){if("string"===n)for(var e,i=0,o=ot(this),a=t.match(Mt)||[];e=a[i++];)o.hasClass(e)?o.removeClass(e):o.addClass(e);else n!==zt&&"boolean"!==n||(this.className&&ot._data(this,"__className__",this.className),this.className=this.className||t===!1?"":ot._data(this,"__className__")||"")})},hasClass:function(t){for(var e=" "+t+" ",n=0,i=this.length;n=0)return!0;return!1}}),ot.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(t,e){ot.fn[e]=function(t,n){return arguments.length>0?this.on(e,null,t,n):this.trigger(e)}}),ot.fn.extend({hover:function(t,e){return this.mouseenter(t).mouseleave(e||t)},bind:function(t,e,n){return this.on(t,null,e,n)},unbind:function(t,e){return this.off(t,null,e)},delegate:function(t,e,n,i){return this.on(e,t,n,i)},undelegate:function(t,e,n){return 1===arguments.length?this.off(t,"**"):this.off(e,t||"**",n)}});var Ne=ot.now(),Le=/\?/,De=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;ot.parseJSON=function(e){if(t.JSON&&t.JSON.parse)return t.JSON.parse(e+"");var n,i=null,o=ot.trim(e+"");return o&&!ot.trim(o.replace(De,function(t,e,o,a){return n&&e&&(i=0),0===i?t:(n=o||e,i+=!a-!o,"")}))?Function("return "+o)():ot.error("Invalid JSON: "+e)},ot.parseXML=function(e){var n,i;if(!e||"string"!=typeof e)return null;try{t.DOMParser?(i=new DOMParser,n=i.parseFromString(e,"text/xml")):(n=new ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(e))}catch(o){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||ot.error("Invalid XML: "+e),n};var qe,ke,We=/#.*$/,Ee=/([?&])_=[^&]*/,Be=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Ie=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Re=/^(?:GET|HEAD)$/,Pe=/^\/\//,Xe=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Fe={},He={},je="*/".concat("*");try{ke=location.href}catch(Ue){ke=ft.createElement("a"),ke.href="",ke=ke.href}qe=Xe.exec(ke.toLowerCase())||[],ot.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:ke,type:"GET",isLocal:Ie.test(qe[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":je,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":ot.parseJSON,"text xml":ot.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?X(X(t,ot.ajaxSettings),e):X(ot.ajaxSettings,t)},ajaxPrefilter:R(Fe),ajaxTransport:R(He),ajax:function(t,e){function n(t,e,n,i){var o,u,b,v,y,w=e;2!==M&&(M=2,r&&clearTimeout(r),l=void 0,s=i||"",A.readyState=t>0?4:0,o=t>=200&&t<300||304===t,n&&(v=F(h,A,n)),v=H(h,v,A,o),o?(h.ifModified&&(y=A.getResponseHeader("Last-Modified"),y&&(ot.lastModified[a]=y),y=A.getResponseHeader("etag"),y&&(ot.etag[a]=y)),204===t||"HEAD"===h.type?w="nocontent":304===t?w="notmodified":(w=v.state,u=v.data,b=v.error,o=!b)):(b=w,!t&&w||(w="error",t<0&&(t=0))),A.status=t,A.statusText=(e||w)+"",o?f.resolveWith(d,[u,w,A]):f.rejectWith(d,[A,w,b]),A.statusCode(g),g=void 0,c&&p.trigger(o?"ajaxSuccess":"ajaxError",[A,h,o?u:b]),m.fireWith(d,[A,w]),c&&(p.trigger("ajaxComplete",[A,h]),--ot.active||ot.event.trigger("ajaxStop")))}"object"==typeof t&&(e=t,t=void 0),e=e||{};var i,o,a,s,r,c,l,u,h=ot.ajaxSetup({},e),d=h.context||h,p=h.context&&(d.nodeType||d.jquery)?ot(d):ot.event,f=ot.Deferred(),m=ot.Callbacks("once memory"),g=h.statusCode||{},b={},v={},M=0,y="canceled",A={readyState:0,getResponseHeader:function(t){var e;if(2===M){if(!u)for(u={};e=Be.exec(s);)u[e[1].toLowerCase()]=e[2];e=u[t.toLowerCase()]}return null==e?null:e},getAllResponseHeaders:function(){return 2===M?s:null},setRequestHeader:function(t,e){var n=t.toLowerCase();return M||(t=v[n]=v[n]||t,b[t]=e),this},overrideMimeType:function(t){return M||(h.mimeType=t),this},statusCode:function(t){var e;if(t)if(M<2)for(e in t)g[e]=[g[e],t[e]];else A.always(t[A.status]);return this},abort:function(t){var e=t||y;return l&&l.abort(e),n(0,e),this}};if(f.promise(A).complete=m.add,A.success=A.done,A.error=A.fail,h.url=((t||h.url||ke)+"").replace(We,"").replace(Pe,qe[1]+"//"),h.type=e.method||e.type||h.method||h.type,h.dataTypes=ot.trim(h.dataType||"*").toLowerCase().match(Mt)||[""],null==h.crossDomain&&(i=Xe.exec(h.url.toLowerCase()),h.crossDomain=!(!i||i[1]===qe[1]&&i[2]===qe[2]&&(i[3]||("http:"===i[1]?"80":"443"))===(qe[3]||("http:"===qe[1]?"80":"443")))),h.data&&h.processData&&"string"!=typeof h.data&&(h.data=ot.param(h.data,h.traditional)),P(Fe,h,e,A),2===M)return A;c=ot.event&&h.global,c&&0===ot.active++&&ot.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Re.test(h.type),a=h.url,h.hasContent||(h.data&&(a=h.url+=(Le.test(a)?"&":"?")+h.data,delete h.data),h.cache===!1&&(h.url=Ee.test(a)?a.replace(Ee,"$1_="+Ne++):a+(Le.test(a)?"&":"?")+"_="+Ne++)),h.ifModified&&(ot.lastModified[a]&&A.setRequestHeader("If-Modified-Since",ot.lastModified[a]),ot.etag[a]&&A.setRequestHeader("If-None-Match",ot.etag[a])),(h.data&&h.hasContent&&h.contentType!==!1||e.contentType)&&A.setRequestHeader("Content-Type",h.contentType),A.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+je+"; q=0.01":""):h.accepts["*"]);for(o in h.headers)A.setRequestHeader(o,h.headers[o]);if(h.beforeSend&&(h.beforeSend.call(d,A,h)===!1||2===M))return A.abort();y="abort";for(o in{success:1,error:1,complete:1})A[o](h[o]);if(l=P(He,h,e,A)){A.readyState=1,c&&p.trigger("ajaxSend",[A,h]),h.async&&h.timeout>0&&(r=setTimeout(function(){A.abort("timeout")},h.timeout));try{M=1,l.send(b,n)}catch(w){if(!(M<2))throw w;n(-1,w)}}else n(-1,"No Transport");return A},getJSON:function(t,e,n){return ot.get(t,e,n,"json")},getScript:function(t,e){return ot.get(t,void 0,e,"script")}}),ot.each(["get","post"],function(t,e){ot[e]=function(t,n,i,o){return ot.isFunction(n)&&(o=o||i,i=n,n=void 0),ot.ajax({url:t,type:e,dataType:o,data:n,success:i})}}),ot._evalUrl=function(t){return ot.ajax({url:t,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},ot.fn.extend({wrapAll:function(t){if(ot.isFunction(t))return this.each(function(e){ot(this).wrapAll(t.call(this,e))});if(this[0]){var e=ot(t,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&e.insertBefore(this[0]),e.map(function(){for(var t=this;t.firstChild&&1===t.firstChild.nodeType;)t=t.firstChild;return t}).append(this)}return this},wrapInner:function(t){return ot.isFunction(t)?this.each(function(e){ot(this).wrapInner(t.call(this,e))}):this.each(function(){var e=ot(this),n=e.contents();n.length?n.wrapAll(t):e.append(t)})},wrap:function(t){var e=ot.isFunction(t);return this.each(function(n){ot(this).wrapAll(e?t.call(this,n):t)})},unwrap:function(){return this.parent().each(function(){ot.nodeName(this,"body")||ot(this).replaceWith(this.childNodes)}).end()}}),ot.expr.filters.hidden=function(t){return t.offsetWidth<=0&&t.offsetHeight<=0||!nt.reliableHiddenOffsets()&&"none"===(t.style&&t.style.display||ot.css(t,"display"))},ot.expr.filters.visible=function(t){return!ot.expr.filters.hidden(t)};var $e=/%20/g,Ye=/\[\]$/,Ve=/\r?\n/g,Je=/^(?:submit|button|image|reset|file)$/i,Ge=/^(?:input|select|textarea|keygen)/i;ot.param=function(t,e){var n,i=[],o=function(t,e){e=ot.isFunction(e)?e():null==e?"":e,i[i.length]=encodeURIComponent(t)+"="+encodeURIComponent(e)};if(void 0===e&&(e=ot.ajaxSettings&&ot.ajaxSettings.traditional),ot.isArray(t)||t.jquery&&!ot.isPlainObject(t))ot.each(t,function(){o(this.name,this.value)});else for(n in t)j(n,t[n],e,o);return i.join("&").replace($e,"+")},ot.fn.extend({serialize:function(){return ot.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var t=ot.prop(this,"elements");return t?ot.makeArray(t):this}).filter(function(){var t=this.type;return this.name&&!ot(this).is(":disabled")&&Ge.test(this.nodeName)&&!Je.test(t)&&(this.checked||!Nt.test(t))}).map(function(t,e){var n=ot(this).val();return null==n?null:ot.isArray(n)?ot.map(n,function(t){return{name:e.name,value:t.replace(Ve,"\r\n")}}):{name:e.name,value:n.replace(Ve,"\r\n")}}).get()}}),ot.ajaxSettings.xhr=void 0!==t.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&U()||$()}:U;var Ke=0,Qe={},Ze=ot.ajaxSettings.xhr();t.attachEvent&&t.attachEvent("onunload",function(){for(var t in Qe)Qe[t](void 0,!0)}),nt.cors=!!Ze&&"withCredentials"in Ze,Ze=nt.ajax=!!Ze,Ze&&ot.ajaxTransport(function(t){if(!t.crossDomain||nt.cors){var e;return{send:function(n,i){var o,a=t.xhr(),s=++Ke;if(a.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(o in t.xhrFields)a[o]=t.xhrFields[o];t.mimeType&&a.overrideMimeType&&a.overrideMimeType(t.mimeType),t.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest");for(o in n)void 0!==n[o]&&a.setRequestHeader(o,n[o]+"");a.send(t.hasContent&&t.data||null),e=function(n,o){var r,c,l;if(e&&(o||4===a.readyState))if(delete Qe[s],e=void 0,a.onreadystatechange=ot.noop,o)4!==a.readyState&&a.abort();else{l={},r=a.status,"string"==typeof a.responseText&&(l.text=a.responseText);try{c=a.statusText}catch(u){c=""}r||!t.isLocal||t.crossDomain?1223===r&&(r=204):r=l.text?200:404}l&&i(r,c,l,a.getAllResponseHeaders())},t.async?4===a.readyState?setTimeout(e):a.onreadystatechange=Qe[s]=e:e()},abort:function(){e&&e(void 0,!0)}}}}),ot.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(t){return ot.globalEval(t),t}}}),ot.ajaxPrefilter("script",function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET",t.global=!1)}),ot.ajaxTransport("script",function(t){if(t.crossDomain){var e,n=ft.head||ot("head")[0]||ft.documentElement;return{send:function(i,o){e=ft.createElement("script"),e.async=!0,t.scriptCharset&&(e.charset=t.scriptCharset),e.src=t.url,e.onload=e.onreadystatechange=function(t,n){(n||!e.readyState||/loaded|complete/.test(e.readyState))&&(e.onload=e.onreadystatechange=null,e.parentNode&&e.parentNode.removeChild(e),e=null,n||o(200,"success"))},n.insertBefore(e,n.firstChild)},abort:function(){e&&e.onload(void 0,!0)}}}});var tn=[],en=/(=)\?(?=&|$)|\?\?/;ot.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var t=tn.pop()||ot.expando+"_"+Ne++;return this[t]=!0,t}}),ot.ajaxPrefilter("json jsonp",function(e,n,i){var o,a,s,r=e.jsonp!==!1&&(en.test(e.url)?"url":"string"==typeof e.data&&!(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&en.test(e.data)&&"data");if(r||"jsonp"===e.dataTypes[0])return o=e.jsonpCallback=ot.isFunction(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,r?e[r]=e[r].replace(en,"$1"+o):e.jsonp!==!1&&(e.url+=(Le.test(e.url)?"&":"?")+e.jsonp+"="+o),e.converters["script json"]=function(){return s||ot.error(o+" was not called"),s[0]},e.dataTypes[0]="json",a=t[o],t[o]=function(){s=arguments},i.always(function(){t[o]=a,e[o]&&(e.jsonpCallback=n.jsonpCallback,tn.push(o)),s&&ot.isFunction(a)&&a(s[0]),s=a=void 0}),"script"}),ot.parseHTML=function(t,e,n){if(!t||"string"!=typeof t)return null;"boolean"==typeof e&&(n=e,e=!1),e=e||ft;var i=ht.exec(t),o=!n&&[];return i?[e.createElement(i[1])]:(i=ot.buildFragment([t],e,o),o&&o.length&&ot(o).remove(),ot.merge([],i.childNodes))};var nn=ot.fn.load;ot.fn.load=function(t,e,n){if("string"!=typeof t&&nn)return nn.apply(this,arguments);var i,o,a,s=this,r=t.indexOf(" ");return r>=0&&(i=ot.trim(t.slice(r,t.length)),t=t.slice(0,r)),ot.isFunction(e)?(n=e,e=void 0):e&&"object"==typeof e&&(a="POST"),s.length>0&&ot.ajax({url:t,type:a,dataType:"html",data:e}).done(function(t){o=arguments,s.html(i?ot("
    ").append(ot.parseHTML(t)).find(i):t)}).complete(n&&function(t,e){s.each(n,o||[t.responseText,e,t])}),this},ot.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(t,e){ot.fn[e]=function(t){return this.on(e,t)}}),ot.expr.filters.animated=function(t){return ot.grep(ot.timers,function(e){return t===e.elem}).length};var on=t.document.documentElement;ot.offset={setOffset:function(t,e,n){var i,o,a,s,r,c,l,u=ot.css(t,"position"),h=ot(t),d={};"static"===u&&(t.style.position="relative"),r=h.offset(),a=ot.css(t,"top"),c=ot.css(t,"left"),l=("absolute"===u||"fixed"===u)&&ot.inArray("auto",[a,c])>-1,l?(i=h.position(),s=i.top,o=i.left):(s=parseFloat(a)||0,o=parseFloat(c)||0),ot.isFunction(e)&&(e=e.call(t,n,r)),null!=e.top&&(d.top=e.top-r.top+s),null!=e.left&&(d.left=e.left-r.left+o),"using"in e?e.using.call(t,d):h.css(d)}},ot.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ot.offset.setOffset(this,t,e)});var e,n,i={top:0,left:0},o=this[0],a=o&&o.ownerDocument;if(a)return e=a.documentElement,ot.contains(e,o)?(typeof o.getBoundingClientRect!==zt&&(i=o.getBoundingClientRect()),n=Y(a),{top:i.top+(n.pageYOffset||e.scrollTop)-(e.clientTop||0),left:i.left+(n.pageXOffset||e.scrollLeft)-(e.clientLeft||0)}):i},position:function(){if(this[0]){var t,e,n={top:0,left:0},i=this[0];return"fixed"===ot.css(i,"position")?e=i.getBoundingClientRect():(t=this.offsetParent(),e=this.offset(),ot.nodeName(t[0],"html")||(n=t.offset()),n.top+=ot.css(t[0],"borderTopWidth",!0),n.left+=ot.css(t[0],"borderLeftWidth",!0)),{top:e.top-n.top-ot.css(i,"marginTop",!0),left:e.left-n.left-ot.css(i,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var t=this.offsetParent||on;t&&!ot.nodeName(t,"html")&&"static"===ot.css(t,"position");)t=t.offsetParent;return t||on})}}),ot.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,e){var n=/Y/.test(e);ot.fn[t]=function(i){return St(this,function(t,i,o){var a=Y(t);return void 0===o?a?e in a?a[e]:a.document.documentElement[i]:t[i]:void(a?a.scrollTo(n?ot(a).scrollLeft():o,n?o:ot(a).scrollTop()):t[i]=o)},t,i,arguments.length,null)}}),ot.each(["top","left"],function(t,e){ot.cssHooks[e]=x(nt.pixelPosition,function(t,n){if(n)return n=ee(t,e),ie.test(n)?ot(t).position()[e]+"px":n})}),ot.each({Height:"height",Width:"width"},function(t,e){ot.each({padding:"inner"+t,content:e,"":"outer"+t},function(n,i){ot.fn[i]=function(i,o){var a=arguments.length&&(n||"boolean"!=typeof i),s=n||(i===!0||o===!0?"margin":"border");return St(this,function(e,n,i){var o;return ot.isWindow(e)?e.document.documentElement["client"+t]:9===e.nodeType?(o=e.documentElement,Math.max(e.body["scroll"+t],o["scroll"+t],e.body["offset"+t],o["offset"+t],o["client"+t])):void 0===i?ot.css(e,n,s):ot.style(e,n,i,s)},e,a?i:void 0,a,null)}})}),ot.fn.size=function(){return this.length},ot.fn.andSelf=ot.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return ot});var an=t.jQuery,sn=t.$;return ot.noConflict=function(e){return t.$===ot&&(t.$=sn),e&&t.jQuery===ot&&(t.jQuery=an),ot},typeof e===zt&&(t.jQuery=t.$=ot),ot}),function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)}(function(t){function e(e,i){var o,a,s,r=e.nodeName.toLowerCase();return"area"===r?(o=e.parentNode,a=o.name,!(!e.href||!a||"map"!==o.nodeName.toLowerCase())&&(s=t("img[usemap='#"+a+"']")[0],!!s&&n(s))):(/input|select|textarea|button|object/.test(r)?!e.disabled:"a"===r?e.href||i:i)&&n(e)}function n(e){return t.expr.filters.visible(e)&&!t(e).parents().addBack().filter(function(){return"hidden"===t.css(this,"visibility")}).length}function i(t){for(var e,n;t.length&&t[0]!==document;){if(e=t.css("position"),("absolute"===e||"relative"===e||"fixed"===e)&&(n=parseInt(t.css("zIndex"),10),!isNaN(n)&&0!==n))return n;t=t.parent()}return 0}function o(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},t.extend(this._defaults,this.regional[""]),this.regional.en=t.extend(!0,{},this.regional[""]),this.regional["en-US"]=t.extend(!0,{},this.regional.en),this.dpDiv=a(t("
    "))}function a(e){var n="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.delegate(n,"mouseout",function(){t(this).removeClass("ui-state-hover"),this.className.indexOf("ui-datepicker-prev")!==-1&&t(this).removeClass("ui-datepicker-prev-hover"),this.className.indexOf("ui-datepicker-next")!==-1&&t(this).removeClass("ui-datepicker-next-hover")}).delegate(n,"mouseover",s)}function s(){t.datepicker._isDisabledDatepicker(b.inline?b.dpDiv.parent()[0]:b.input[0])||(t(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),t(this).addClass("ui-state-hover"),this.className.indexOf("ui-datepicker-prev")!==-1&&t(this).addClass("ui-datepicker-prev-hover"),this.className.indexOf("ui-datepicker-next")!==-1&&t(this).addClass("ui-datepicker-next-hover"))}function r(e,n){t.extend(e,n);for(var i in n)null==n[i]&&(e[i]=n[i]);return e}function c(t){return function(){var e=this.element.val();t.apply(this,arguments),this._refresh(),e!==this.element.val()&&this._trigger("change")}}t.ui=t.ui||{},t.extend(t.ui,{version:"1.11.2",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),t.fn.extend({scrollParent:function(e){var n=this.css("position"),i="absolute"===n,o=e?/(auto|scroll|hidden)/:/(auto|scroll)/,a=this.parents().filter(function(){var e=t(this);return(!i||"static"!==e.css("position"))&&o.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==n&&a.length?a:t(this[0].ownerDocument||document)},uniqueId:function(){var t=0;return function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++t)})}}(),removeUniqueId:function(){return this.each(function(){/^ui-id-\d+$/.test(this.id)&&t(this).removeAttr("id")})}}),t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(n){return!!t.data(n,e)}}):function(e,n,i){return!!t.data(e,i[3])},focusable:function(n){return e(n,!isNaN(t.attr(n,"tabindex")))},tabbable:function(n){var i=t.attr(n,"tabindex"),o=isNaN(i);return(o||i>=0)&&e(n,!o)}}),t("").outerWidth(1).jquery||t.each(["Width","Height"],function(e,n){function i(e,n,i,a){return t.each(o,function(){n-=parseFloat(t.css(e,"padding"+this))||0,i&&(n-=parseFloat(t.css(e,"border"+this+"Width"))||0),a&&(n-=parseFloat(t.css(e,"margin"+this))||0)}),n}var o="Width"===n?["Left","Right"]:["Top","Bottom"],a=n.toLowerCase(),s={innerWidth:t.fn.innerWidth,innerHeight:t.fn.innerHeight,outerWidth:t.fn.outerWidth,outerHeight:t.fn.outerHeight};t.fn["inner"+n]=function(e){return void 0===e?s["inner"+n].call(this):this.each(function(){t(this).css(a,i(this,e)+"px")})},t.fn["outer"+n]=function(e,o){return"number"!=typeof e?s["outer"+n].call(this,e):this.each(function(){t(this).css(a,i(this,e,!0,o)+"px")})}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t("").data("a-b","a").removeData("a-b").data("a-b")&&(t.fn.removeData=function(e){return function(n){return arguments.length?e.call(this,t.camelCase(n)):e.call(this)}}(t.fn.removeData)),t.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),t.fn.extend({focus:function(e){return function(n,i){return"number"==typeof n?this.each(function(){var e=this;setTimeout(function(){t(e).focus(),i&&i.call(e)},n)}):e.apply(this,arguments)}}(t.fn.focus),disableSelection:function(){var t="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.bind(t+".ui-disableSelection",function(t){t.preventDefault()})}}(),enableSelection:function(){return this.unbind(".ui-disableSelection")},zIndex:function(e){if(void 0!==e)return this.css("zIndex",e);if(this.length)for(var n,i,o=t(this[0]);o.length&&o[0]!==document;){if(n=o.css("position"),("absolute"===n||"relative"===n||"fixed"===n)&&(i=parseInt(o.css("zIndex"),10),!isNaN(i)&&0!==i))return i;o=o.parent()}return 0}}),t.ui.plugin={add:function(e,n,i){var o,a=t.ui[e].prototype;for(o in i)a.plugins[o]=a.plugins[o]||[],a.plugins[o].push([n,i[o]])},call:function(t,e,n,i){var o,a=t.plugins[e];if(a&&(i||t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType))for(o=0;o",options:{disabled:!1,create:null},_createWidget:function(e,n){n=t(n||this.defaultElement||this)[0],this.element=t(n),this.uuid=l++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),n!==this&&(t.data(n,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===n&&this.destroy()}}),this.document=t(n.style?n.ownerDocument:n.document||n),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(e,n){var i,o,a,s=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(s={},i=e.split("."),e=i.shift(),i.length){for(o=s[e]=t.widget.extend({},this.options[e]),a=0;a=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}});!function(){function e(t,e,n){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?n/100:1)]}function n(e,n){return parseInt(t.css(e,n),10)||0}function i(e){var n=e[0];return 9===n.nodeType?{width:e.width(), -height:e.height(),offset:{top:0,left:0}}:t.isWindow(n)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:n.preventDefault?{width:0,height:0,offset:{top:n.pageY,left:n.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var o,a,s=Math.max,r=Math.abs,c=Math.round,l=/left|center|right/,u=/top|center|bottom/,h=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(void 0!==o)return o;var e,n,i=t("
    "),a=i.children()[0];return t("body").append(i),e=a.offsetWidth,i.css("overflow","scroll"),n=a.offsetWidth,e===n&&(n=i[0].clientWidth),i.remove(),o=e-n},getScrollInfo:function(e){var n=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),i=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),o="scroll"===n||"auto"===n&&e.width0?"right":"center",vertical:a<0?"top":i>0?"bottom":"middle"};ms(r(i),r(a))?c.important="horizontal":c.important="vertical",o.using.call(this,t,c)}),u.offset(t.extend(C,{using:l}))})},t.ui.position={fit:{left:function(t,e){var n,i=e.within,o=i.isWindow?i.scrollLeft:i.offset.left,a=i.width,r=t.left-e.collisionPosition.marginLeft,c=o-r,l=r+e.collisionWidth-a-o;e.collisionWidth>a?c>0&&l<=0?(n=t.left+c+e.collisionWidth-a-o,t.left+=c-n):l>0&&c<=0?t.left=o:c>l?t.left=o+a-e.collisionWidth:t.left=o:c>0?t.left+=c:l>0?t.left-=l:t.left=s(t.left-r,t.left)},top:function(t,e){var n,i=e.within,o=i.isWindow?i.scrollTop:i.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,c=o-r,l=r+e.collisionHeight-a-o;e.collisionHeight>a?c>0&&l<=0?(n=t.top+c+e.collisionHeight-a-o,t.top+=c-n):l>0&&c<=0?t.top=o:c>l?t.top=o+a-e.collisionHeight:t.top=o:c>0?t.top+=c:l>0?t.top-=l:t.top=s(t.top-r,t.top)}},flip:{left:function(t,e){var n,i,o=e.within,a=o.offset.left+o.scrollLeft,s=o.width,c=o.isWindow?o.scrollLeft:o.offset.left,l=t.left-e.collisionPosition.marginLeft,u=l-c,h=l+e.collisionWidth-s-c,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];u<0?(n=t.left+d+p+f+e.collisionWidth-s-a,(n<0||n0&&(i=t.left-e.collisionPosition.marginLeft+d+p+f-c,(i>0||r(i)u&&(i<0||i0&&(n=t.top-e.collisionPosition.marginTop+p+f+m-c,t.top+p+f+m>h&&(n>0||r(n)10&&o<11,e.innerHTML="",n.removeChild(e)}()}();t.ui.position,t.widget("ui.accordion",{version:"1.11.2",options:{active:0,animate:{},collapsible:!1,event:"click",header:"> li > :first-child,> :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},hideProps:{borderTopWidth:"hide",borderBottomWidth:"hide",paddingTop:"hide",paddingBottom:"hide",height:"hide"},showProps:{borderTopWidth:"show",borderBottomWidth:"show",paddingTop:"show",paddingBottom:"show",height:"show"},_create:function(){var e=this.options;this.prevShow=this.prevHide=t(),this.element.addClass("ui-accordion ui-widget ui-helper-reset").attr("role","tablist"),e.collapsible||e.active!==!1&&null!=e.active||(e.active=0),this._processPanels(),e.active<0&&(e.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():t()}},_createIcons:function(){var e=this.options.icons;e&&(t("").addClass("ui-accordion-header-icon ui-icon "+e.header).prependTo(this.headers),this.active.children(".ui-accordion-header-icon").removeClass(e.header).addClass(e.activeHeader),this.headers.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.removeClass("ui-accordion-icons").children(".ui-accordion-header-icon").remove()},_destroy:function(){var t;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.removeClass("ui-accordion-header ui-accordion-header-active ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("aria-controls").removeAttr("tabIndex").removeUniqueId(),this._destroyIcons(),t=this.headers.next().removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled").css("display","").removeAttr("role").removeAttr("aria-hidden").removeAttr("aria-labelledby").removeUniqueId(),"content"!==this.options.heightStyle&&t.css("height","")},_setOption:function(t,e){return"active"===t?void this._activate(e):("event"===t&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(e)),this._super(t,e),"collapsible"!==t||e||this.options.active!==!1||this._activate(0),"icons"===t&&(this._destroyIcons(),e&&this._createIcons()),void("disabled"===t&&(this.element.toggleClass("ui-state-disabled",!!e).attr("aria-disabled",e),this.headers.add(this.headers.next()).toggleClass("ui-state-disabled",!!e))))},_keydown:function(e){if(!e.altKey&&!e.ctrlKey){var n=t.ui.keyCode,i=this.headers.length,o=this.headers.index(e.target),a=!1;switch(e.keyCode){case n.RIGHT:case n.DOWN:a=this.headers[(o+1)%i];break;case n.LEFT:case n.UP:a=this.headers[(o-1+i)%i];break;case n.SPACE:case n.ENTER:this._eventHandler(e);break;case n.HOME:a=this.headers[0];break;case n.END:a=this.headers[i-1]}a&&(t(e.target).attr("tabIndex",-1),t(a).attr("tabIndex",0),a.focus(),e.preventDefault())}},_panelKeyDown:function(e){e.keyCode===t.ui.keyCode.UP&&e.ctrlKey&&t(e.currentTarget).prev().focus()},refresh:function(){var e=this.options;this._processPanels(),e.active===!1&&e.collapsible===!0||!this.headers.length?(e.active=!1,this.active=t()):e.active===!1?this._activate(0):this.active.length&&!t.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(e.active=!1,this.active=t()):this._activate(Math.max(0,e.active-1)):e.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){var t=this.headers,e=this.panels;this.headers=this.element.find(this.options.header).addClass("ui-accordion-header ui-state-default ui-corner-all"),this.panels=this.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom").filter(":not(.ui-accordion-content-active)").hide(),e&&(this._off(t.not(this.headers)),this._off(e.not(this.panels)))},_refresh:function(){var e,n=this.options,i=n.heightStyle,o=this.element.parent();this.active=this._findActive(n.active).addClass("ui-accordion-header-active ui-state-active ui-corner-top").removeClass("ui-corner-all"),this.active.next().addClass("ui-accordion-content-active").show(),this.headers.attr("role","tab").each(function(){var e=t(this),n=e.uniqueId().attr("id"),i=e.next(),o=i.uniqueId().attr("id");e.attr("aria-controls",o),i.attr("aria-labelledby",n)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(n.event),"fill"===i?(e=o.height(),this.element.siblings(":visible").each(function(){var n=t(this),i=n.css("position");"absolute"!==i&&"fixed"!==i&&(e-=n.outerHeight(!0))}),this.headers.each(function(){e-=t(this).outerHeight(!0)}),this.headers.next().each(function(){t(this).height(Math.max(0,e-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===i&&(e=0,this.headers.next().each(function(){e=Math.max(e,t(this).css("height","").height())}).height(e))},_activate:function(e){var n=this._findActive(e)[0];n!==this.active[0]&&(n=n||this.active[0],this._eventHandler({target:n,currentTarget:n,preventDefault:t.noop}))},_findActive:function(e){return"number"==typeof e?this.headers.eq(e):t()},_setupEvents:function(e){var n={keydown:"_keydown"};e&&t.each(e.split(" "),function(t,e){n[e]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,n),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(e){var n=this.options,i=this.active,o=t(e.currentTarget),a=o[0]===i[0],s=a&&n.collapsible,r=s?t():o.next(),c=i.next(),l={oldHeader:i,oldPanel:c,newHeader:s?t():o,newPanel:r};e.preventDefault(),a&&!n.collapsible||this._trigger("beforeActivate",e,l)===!1||(n.active=!s&&this.headers.index(o),this.active=a?t():o,this._toggle(l),i.removeClass("ui-accordion-header-active ui-state-active"),n.icons&&i.children(".ui-accordion-header-icon").removeClass(n.icons.activeHeader).addClass(n.icons.header),a||(o.removeClass("ui-corner-all").addClass("ui-accordion-header-active ui-state-active ui-corner-top"),n.icons&&o.children(".ui-accordion-header-icon").removeClass(n.icons.header).addClass(n.icons.activeHeader),o.next().addClass("ui-accordion-content-active")))},_toggle:function(e){var n=e.newPanel,i=this.prevShow.length?this.prevShow:e.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=n,this.prevHide=i,this.options.animate?this._animate(n,i,e):(i.hide(),n.show(),this._toggleComplete(e)),i.attr({"aria-hidden":"true"}),i.prev().attr("aria-selected","false"),n.length&&i.length?i.prev().attr({tabIndex:-1,"aria-expanded":"false"}):n.length&&this.headers.filter(function(){return 0===t(this).attr("tabIndex")}).attr("tabIndex",-1),n.attr("aria-hidden","false").prev().attr({"aria-selected":"true",tabIndex:0,"aria-expanded":"true"})},_animate:function(t,e,n){var i,o,a,s=this,r=0,c=t.length&&(!e.length||t.index()",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},items:"> *",menus:"ul",position:{my:"left-1 top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault()},"click .ui-menu-item":function(e){var n=t(e.target);!this.mouseHandled&&n.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),n.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&t(this.document[0].activeElement).closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){if(!this.previousFilter){var n=t(e.currentTarget);n.siblings(".ui-state-active").removeClass("ui-state-active"),this.focus(e,n)}},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var n=this.active||this.element.find(this.options.items).eq(0);e||this.focus(t,n)},blur:function(e){this._delay(function(){t.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t),this.mouseHandled=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeClass("ui-menu ui-widget ui-widget-content ui-menu-icons ui-front").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").removeUniqueId().removeClass("ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var e=t(this);e.data("ui-menu-submenu-carat")&&e.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(e){var n,i,o,a,s=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:s=!1,i=this.previousFilter||"",o=String.fromCharCode(e.keyCode),a=!1,clearTimeout(this.filterTimer),o===i?a=!0:o=i+o,n=this._filterMenuItems(o),n=a&&n.index(this.active.next())!==-1?this.active.nextAll(".ui-menu-item"):n,n.length||(o=String.fromCharCode(e.keyCode),n=this._filterMenuItems(o)),n.length?(this.focus(e,n),this.previousFilter=o,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}s&&e.preventDefault()},_activate:function(t){this.active.is(".ui-state-disabled")||(this.active.is("[aria-haspopup='true']")?this.expand(t):this.select(t))},refresh:function(){var e,n,i=this,o=this.options.icons.submenu,a=this.element.find(this.options.menus);this.element.toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length),a.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-front").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),n=e.parent(),i=t("").addClass("ui-menu-icon ui-icon "+o).data("ui-menu-submenu-carat",!0);n.attr("aria-haspopup","true").prepend(i),e.attr("aria-labelledby",n.attr("id"))}),e=a.add(this.element),n=e.find(this.options.items),n.not(".ui-menu-item").each(function(){var e=t(this);i._isDivider(e)&&e.addClass("ui-widget-content ui-menu-divider")}),n.not(".ui-menu-item, .ui-menu-divider").addClass("ui-menu-item").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),n.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){"icons"===t&&this.element.find(".ui-menu-icon").removeClass(this.options.icons.submenu).addClass(e.submenu),"disabled"===t&&this.element.toggleClass("ui-state-disabled",!!e).attr("aria-disabled",e),this._super(t,e)},focus:function(t,e){var n,i;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),i=this.active.addClass("ui-state-focus").removeClass("ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",i.attr("id")),this.active.parent().closest(".ui-menu-item").addClass("ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),n=e.children(".ui-menu"),n.length&&t&&/^mouse/.test(t.type)&&this._startOpening(n),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var n,i,o,a,s,r;this._hasScroll()&&(n=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,i=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,o=e.offset().top-this.activeMenu.offset().top-n-i,a=this.activeMenu.scrollTop(),s=this.activeMenu.height(),r=e.outerHeight(),o<0?this.activeMenu.scrollTop(a+o):o+r>s&&this.activeMenu.scrollTop(a+o-s+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this.active.removeClass("ui-state-focus"),this.active=null,this._trigger("blur",t,{item:this.active}))},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var n=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(n)},collapseAll:function(e,n){clearTimeout(this.timer),this.timer=this._delay(function(){var i=n?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));i.length||(i=this.element),this._close(i),this.blur(e),this.activeMenu=i},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find(".ui-state-active").not(".ui-state-focus").removeClass("ui-state-active")},_closeOnDocumentClick:function(e){return!t(e.target).closest(".ui-menu").length},_isDivider:function(t){return!/[^\-\u2014\u2013\s]/.test(t.text())},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").find(this.options.items).first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,n){var i;this.active&&(i="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),i&&i.length&&this.active||(i=this.activeMenu.find(this.options.items)[e]()),this.focus(n,i)},nextPage:function(e){var n,i,o;return this.active?void(this.isLastItem()||(this._hasScroll()?(i=this.active.offset().top,o=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return n=t(this),n.offset().top-i-o<0}),this.focus(e,n)):this.focus(e,this.activeMenu.find(this.options.items)[this.active?"last":"first"]()))):void this.next(e)},previousPage:function(e){var n,i,o;return this.active?void(this.isFirstItem()||(this._hasScroll()?(i=this.active.offset().top,o=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return n=t(this),n.offset().top-i+o>0}),this.focus(e,n)):this.focus(e,this.activeMenu.find(this.options.items).first()))):void this.next(e)},_hasScroll:function(){return this.element.outerHeight()",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var e,n,i,o=this.element[0].nodeName.toLowerCase(),a="textarea"===o,s="input"===o;this.isMultiLine=!!a||!s&&this.element.prop("isContentEditable"),this.valueMethod=this.element[a||s?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(o){if(this.element.prop("readOnly"))return e=!0,i=!0,void(n=!0);e=!1,i=!1,n=!1;var a=t.ui.keyCode;switch(o.keyCode){case a.PAGE_UP:e=!0,this._move("previousPage",o);break;case a.PAGE_DOWN:e=!0,this._move("nextPage",o);break;case a.UP:e=!0,this._keyEvent("previous",o);break;case a.DOWN:e=!0,this._keyEvent("next",o);break;case a.ENTER:this.menu.active&&(e=!0,o.preventDefault(),this.menu.select(o));break;case a.TAB:this.menu.active&&this.menu.select(o);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(o),o.preventDefault());break;default:n=!0,this._searchTimeout(o)}},keypress:function(i){if(e)return e=!1,void(this.isMultiLine&&!this.menu.element.is(":visible")||i.preventDefault());if(!n){var o=t.ui.keyCode;switch(i.keyCode){case o.PAGE_UP:this._move("previousPage",i);break;case o.PAGE_DOWN:this._move("nextPage",i);break;case o.UP:this._keyEvent("previous",i);break;case o.DOWN:this._keyEvent("next",i)}}},input:function(t){return i?(i=!1,void t.preventDefault()):void this._searchTimeout(t)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){return this.cancelBlur?void delete this.cancelBlur:(clearTimeout(this.searching),this.close(t),void this._change(t))}}),this._initSource(),this.menu=t("
    ");var r=t("a",n),c=r[0],l=r[1],u=r[2],h=r[3];e.oApi._fnBindAction(c,{action:"first"},s),e.oApi._fnBindAction(l,{action:"previous"},s),e.oApi._fnBindAction(u,{action:"next"},s),e.oApi._fnBindAction(h,{action:"last"},s),e.aanFeatures.p||(n.id=e.sTableId+"_paginate",c.id=e.sTableId+"_first",l.id=e.sTableId+"_previous",u.id=e.sTableId+"_next",h.id=e.sTableId+"_last")},fnUpdate:function(e,n){if(e.aanFeatures.p){var i,o,a,s,r,c=e.oInstance.fnPagingInfo(),l=t.fn.dataTableExt.oPagination.iFullNumbersShowPages,u=Math.floor(l/2),h=Math.ceil(e.fnRecordsDisplay()/e._iDisplayLength),d=Math.ceil(e._iDisplayStart/e._iDisplayLength)+1,p="",f=(e.oClasses,e.aanFeatures.p);for(e._iDisplayLength===-1?(i=1,o=1,d=1):h=h-u?(i=h-l+1,o=h):(i=d-Math.ceil(l/2)+1,o=i+l-1),a=i;a<=o;a++)p+=d!==a?'
  • '+e.fnFormatNumber(a)+"
  • ":'
  • '+e.fnFormatNumber(a)+"
  • ";for(a=0,s=f.length;a",o[0];);return 4h.a.l(e,t[n])&&e.push(t[n]);return e},ya:function(t,e){t=t||[];for(var n=[],i=0,o=t.length;ii?n&&t.push(e):n||t.splice(i,1)},na:l,extend:r,ra:c,sa:l?c:r,A:s,Oa:function(t,e){if(!t)return t;var n,i={};for(n in t)t.hasOwnProperty(n)&&(i[n]=e(t[n],n,t));return i},Fa:function(t){for(;t.firstChild;)h.removeNode(t.firstChild)},ec:function(t){t=h.a.R(t);for(var e=n.createElement("div"),i=0,o=t.length;if?t.setAttribute("selected",e):t.selected=e},ta:function(e){return null===e||e===t?"":e.trim?e.trim():e.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g,"")},oc:function(t,e){for(var n=[],i=(t||"").split(e),o=0,a=i.length;ot.length)&&t.substring(0,e.length)===e},Sb:function(t,e){if(t===e)return!0;if(11===t.nodeType)return!1;if(e.contains)return e.contains(3===t.nodeType?t.parentNode:t);if(e.compareDocumentPosition)return 16==(16&e.compareDocumentPosition(t));for(;t&&t!=e;)t=t.parentNode;return!!t},Ea:function(t){return h.a.Sb(t,t.ownerDocument.documentElement)},eb:function(t){return!!h.a.hb(t,h.a.Ea)},B:function(t){return t&&t.tagName&&t.tagName.toLowerCase()},q:function(t,e,n){var i=f&&p[e];if(!i&&o)o(t).bind(e,n);else if(i||"function"!=typeof t.addEventListener){if("undefined"==typeof t.attachEvent)throw Error("Browser doesn't support addEventListener or attachEvent");var a=function(e){n.call(t,e)},s="on"+e;t.attachEvent(s,a),h.a.u.ja(t,function(){t.detachEvent(s,a)})}else t.addEventListener(e,n,!1)},ha:function(t,i){if(!t||!t.nodeType)throw Error("element must be a DOM node when calling triggerEvent");var a;if("input"===h.a.B(t)&&t.type&&"click"==i.toLowerCase()?(a=t.type,a="checkbox"==a||"radio"==a):a=!1,o&&!a)o(t).trigger(i);else if("function"==typeof n.createEvent){if("function"!=typeof t.dispatchEvent)throw Error("The supplied element doesn't support dispatchEvent");a=n.createEvent(d[i]||"HTMLEvents"),a.initEvent(i,!0,!0,e,0,0,0,0,0,!1,!1,!1,!1,0,t),t.dispatchEvent(a)}else if(a&&t.click)t.click();else{if("undefined"==typeof t.fireEvent)throw Error("Browser doesn't support triggering events");t.fireEvent("on"+i)}},c:function(t){return h.v(t)?t():t},Sa:function(t){return h.v(t)?t.o():t},ua:function(t,e,n){if(e){var i=/\S+/g,o=t.className.match(i)||[];h.a.r(e.match(i),function(t){h.a.Y(o,t,n)}),t.className=o.join(" ")}},Xa:function(e,n){var i=h.a.c(n);null!==i&&i!==t||(i="");var o=h.e.firstChild(e);!o||3!=o.nodeType||h.e.nextSibling(o)?h.e.U(e,[e.ownerDocument.createTextNode(i)]):o.data=i,h.a.Vb(e)},Cb:function(t,e){if(t.name=e,7>=f)try{t.mergeAttributes(n.createElement(""),!1)}catch(i){}},Vb:function(t){9<=f&&(t=1==t.nodeType?t:t.parentNode,t.style&&(t.style.zoom=t.style.zoom))},Tb:function(t){if(f){var e=t.style.width;t.style.width=0,t.style.width=e}},ic:function(t,e){t=h.a.c(t),e=h.a.c(e);for(var n=[],i=t;i<=e;i++)n.push(i);return n},R:function(t){for(var e=[],n=0,i=t.length;n",""]||!a.indexOf("",""]||(!a.indexOf("",""]||[0,"",""],t="ignored
    "+a[1]+t+a[2]+"
    ","function"==typeof e.innerShiv?i.appendChild(e.innerShiv(t)):i.innerHTML=t;a[0]--;)i=i.lastChild;i=h.a.R(i.lastChild.childNodes)}return i},h.a.Va=function(e,n){if(h.a.Fa(e),n=h.a.c(n),null!==n&&n!==t)if("string"!=typeof n&&(n=n.toString()),o)o(e).html(n);else for(var i=h.a.Qa(n),a=0;a"},Hb:function(e,i){var o=n[e];if(o===t)throw Error("Couldn't find any memo with ID "+e+". Perhaps it's already been unmemoized.");try{return o.apply(null,i||[]),!0}finally{delete n[e]}},Ib:function(t,n){var i=[];e(t,i);for(var o=0,a=i.length;oa[0]?c+a[0]:a[0]),c);for(var c=1===l?c:Math.min(e+(a[1]||0),c),l=e+l-2,u=Math.max(c,l),d=[],p=[],f=2;ee;e++)t=t();return t})},h.toJSON=function(t,e,n){return t=h.Gb(t),h.a.Ya(t,e,n)},i.prototype={save:function(t,e){var n=h.a.l(this.keys,t);0<=n?this.ab[n]=e:(this.keys.push(t),this.ab.push(e))},get:function(e){return e=h.a.l(this.keys,e),0<=e?this.ab[e]:t}}}(),h.b("toJS",h.Gb),h.b("toJSON",h.toJSON),function(){h.i={p:function(e){switch(h.a.B(e)){case"option":return!0===e.__ko__hasDomDataOptionValue__?h.a.f.get(e,h.d.options.Pa):7>=h.a.oa?e.getAttributeNode("value")&&e.getAttributeNode("value").specified?e.value:e.text:e.value;case"select":return 0<=e.selectedIndex?h.i.p(e.options[e.selectedIndex]):t;default:return e.value}},X:function(e,n,i){switch(h.a.B(e)){case"option":switch(typeof n){case"string":h.a.f.set(e,h.d.options.Pa,t),"__ko__hasDomDataOptionValue__"in e&&delete e.__ko__hasDomDataOptionValue__,e.value=n;break;default:h.a.f.set(e,h.d.options.Pa,n),e.__ko__hasDomDataOptionValue__=!0,e.value="number"==typeof n?n:""}break;case"select":""!==n&&null!==n||(n=t);for(var o,a=-1,s=0,r=e.options.length;s=c){e&&s.push(n?{key:e,value:n.join("")}:{unknown:e}),e=n=c=0;continue}}else if(58===d){if(!n)continue}else if(47===d&&u&&1"===n.createComment("test").text,s=a?/^\x3c!--\s*ko(?:\s+([\s\S]+))?\s*--\x3e$/:/^\s*ko(?:\s+([\s\S]+))?\s*$/,r=a?/^\x3c!--\s*\/ko\s*--\x3e$/:/^\s*\/ko\s*$/,c={ul:!0,ol:!0};h.e={Q:{},childNodes:function(e){return t(e)?i(e):e.childNodes},da:function(e){if(t(e)){e=h.e.childNodes(e);for(var n=0,i=e.length;n=h.a.oa&&n in g?(n=g[n],o?e.removeAttribute(n):e[n]=i):o||e.setAttribute(n,i.toString()),"name"===n&&h.a.Cb(e,o?"":i.toString())})}},function(){h.d.checked={after:["value","attr"],init:function(e,n,i){function o(){return i.has("checkedValue")?h.a.c(i.get("checkedValue")):e.value}function a(){var t=e.checked,a=d?o():t;if(!h.ca.pa()&&(!c||t)){var s=h.k.t(n);l?u!==a?(t&&(h.a.Y(s,a,!0),h.a.Y(s,u,!1)),u=a):h.a.Y(s,a,t):h.g.va(s,i,"checked",a,!0)}}function s(){var t=h.a.c(n());e.checked=l?0<=h.a.l(t,o()):r?t:o()===t}var r="checkbox"==e.type,c="radio"==e.type;if(r||c){var l=r&&h.a.c(n())instanceof Array,u=l?o():t,d=c||l;c&&!e.name&&h.d.uniqueName.init(e,function(){return!0}),h.ba(a,null,{G:e}),h.a.q(e,"click",a),h.ba(s,null,{G:e})}}},h.g.W.checked=!0,h.d.checkedValue={update:function(t,e){t.value=h.a.c(e())}}}(),h.d.css={update:function(t,e){var n=h.a.c(e());"object"==typeof n?h.a.A(n,function(e,n){n=h.a.c(n),h.a.ua(t,e,n)}):(n=String(n||""),h.a.ua(t,t.__ko__cssValue,!1),t.__ko__cssValue=n,h.a.ua(t,n,!0))}},h.d.enable={update:function(t,e){var n=h.a.c(e());n&&t.disabled?t.removeAttribute("disabled"):n||t.disabled||(t.disabled=!0)}},h.d.disable={update:function(t,e){h.d.enable.update(t,function(){return!h.a.c(e())})}},h.d.event={init:function(t,e,n,i,o){var a=e()||{};h.a.A(a,function(a){"string"==typeof a&&h.a.q(t,a,function(t){var s,r=e()[a];if(r){try{var c=h.a.R(arguments);i=o.$data,c.unshift(i),s=r.apply(i,c)}finally{!0!==s&&(t.preventDefault?t.preventDefault():t.returnValue=!1)}!1===n.get(a+"Bubble")&&(t.cancelBubble=!0,t.stopPropagation&&t.stopPropagation())}})})}},h.d.foreach={vb:function(t){return function(){var e=t(),n=h.a.Sa(e);return n&&"number"!=typeof n.length?(h.a.c(e),{foreach:n.data,as:n.as,includeDestroyed:n.includeDestroyed,afterAdd:n.afterAdd,beforeRemove:n.beforeRemove,afterRender:n.afterRender,beforeMove:n.beforeMove,afterMove:n.afterMove,templateEngine:h.K.Ja}):{foreach:e,templateEngine:h.K.Ja}}},init:function(t,e){return h.d.template.init(t,h.d.foreach.vb(e))},update:function(t,e,n,i,o){return h.d.template.update(t,h.d.foreach.vb(e),n,i,o)}},h.g.aa.foreach=!1,h.e.Q.foreach=!0,h.d.hasfocus={init:function(t,e,n){function i(i){t.__ko_hasfocusUpdating=!0;var o=t.ownerDocument;if("activeElement"in o){var a;try{a=o.activeElement}catch(s){a=o.body}i=a===t}o=e(),h.g.va(o,n,"hasfocus",i,!0),t.__ko_hasfocusLastValue=i,t.__ko_hasfocusUpdating=!1}var o=i.bind(null,!0),a=i.bind(null,!1);h.a.q(t,"focus",o),h.a.q(t,"focusin",o),h.a.q(t,"blur",a),h.a.q(t,"focusout",a)},update:function(t,e){var n=!!h.a.c(e());t.__ko_hasfocusUpdating||t.__ko_hasfocusLastValue===n||(n?t.focus():t.blur(),h.k.t(h.a.ha,null,[t,n?"focusin":"focusout"]))}},h.g.W.hasfocus=!0,h.d.hasFocus=h.d.hasfocus,h.g.W.hasFocus=!0,h.d.html={init:function(){return{controlsDescendantBindings:!0}},update:function(t,e){h.a.Va(t,e())}},u("if"),u("ifnot",!1,!0),u("with",!0,!1,function(t,e){return t.createChildContext(e)});var b={};h.d.options={init:function(t){if("select"!==h.a.B(t))throw Error("options binding applies only to SELECT elements");for(;0","#comment",o)})},Mb:function(t,e){return h.w.Na(function(n,i){var o=n.nextSibling;o&&o.nodeName.toLowerCase()===e&&h.xa(o,t,i)})}}}(),h.b("__tr_ambtns",h.Za.Mb),function(){h.n={},h.n.j=function(t){this.j=t},h.n.j.prototype.text=function(){var t=h.a.B(this.j),t="script"===t?"text":"textarea"===t?"value":"innerHTML";if(0==arguments.length)return this.j[t];var e=arguments[0]; -"innerHTML"===t?h.a.Va(this.j,e):this.j[t]=e};var e=h.a.f.L()+"_";h.n.j.prototype.data=function(t){return 1===arguments.length?h.a.f.get(this.j,e+t):void h.a.f.set(this.j,e+t,arguments[1])};var n=h.a.f.L();h.n.Z=function(t){this.j=t},h.n.Z.prototype=new h.n.j,h.n.Z.prototype.text=function(){if(0==arguments.length){var e=h.a.f.get(this.j,n)||{};return e.$a===t&&e.Ba&&(e.$a=e.Ba.innerHTML),e.$a}h.a.f.set(this.j,n,{$a:arguments[0]})},h.n.j.prototype.nodes=function(){return 0==arguments.length?(h.a.f.get(this.j,n)||{}).Ba:void h.a.f.set(this.j,n,{Ba:arguments[0]})},h.b("templateSources",h.n),h.b("templateSources.domElement",h.n.j),h.b("templateSources.anonymousTemplate",h.n.Z)}(),function(){function e(t,e,n){var i;for(e=h.e.nextSibling(e);t&&(i=t)!==e;)t=h.e.nextSibling(i),n(i,t)}function n(t,n){if(t.length){var i=t[0],o=t[t.length-1],a=i.parentNode,s=h.J.instance,r=s.preprocessNode;if(r){if(e(i,o,function(t,e){var n=t.previousSibling,a=r.call(s,t);a&&(t===i&&(i=a[0]||e),t===o&&(o=a[a.length-1]||n))}),t.length=0,!i)return;i===o?t.push(i):(t.push(i,o),h.a.ea(t,a))}e(i,o,function(t){1!==t.nodeType&&8!==t.nodeType||h.fb(n,t)}),e(i,o,function(t){1!==t.nodeType&&8!==t.nodeType||h.w.Ib(t,[n])}),h.a.ea(t,a)}}function i(t){return t.nodeType?t:0h.a.oa?0:t.nodes)?t.nodes():null;return e?h.a.R(e.cloneNode(!0).childNodes):(t=t.text(),h.a.Qa(t))},h.K.Ja=new h.K,h.Wa(h.K.Ja),h.b("nativeTemplateEngine",h.K),function(){h.La=function(){var t=this.ac=function(){if(!o||!o.tmpl)return 0;try{if(0<=o.tmpl.tag.tmpl.open.toString().indexOf("__"))return 2}catch(t){}return 1}();this.renderTemplateSource=function(e,i,a){if(a=a||{},2>t)throw Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later.");var s=e.data("precompiled");return s||(s=e.text()||"",s=o.template(null,"{{ko_with $item.koBindingContext}}"+s+"{{/ko_with}}"),e.data("precompiled",s)),e=[i.$data],i=o.extend({koBindingContext:i},a.templateOptions),i=o.tmpl(s,e,i),i.appendTo(n.createElement("div")),o.fragments={},i},this.createJavaScriptEvaluatorBlock=function(t){return"{{ko_code ((function() { return "+t+" })()) }}"},this.addTemplate=function(t,e){n.write("")},0=0&&(u&&(u.splice(m,1),t.processAllDeferredBindingUpdates&&t.processAllDeferredBindingUpdates()),p.splice(g,0,A)),l(v,n,null),t.processAllDeferredBindingUpdates&&t.processAllDeferredBindingUpdates(),_.afterMove&&_.afterMove.call(this,b,s,r)}y&&y.apply(this,arguments)},connectWith:!!_.connectClass&&"."+_.connectClass})),void 0!==_.isEnabled&&t.computed({read:function(){A.sortable(r(_.isEnabled)?"enable":"disable")},disposeWhenNodeIsRemoved:u})},0);return t.utils.domNodeDisposal.addDisposeCallback(u,function(){(A.data("ui-sortable")||A.data("sortable"))&&A.sortable("destroy"),clearTimeout(T)}),{controlsDescendantBindings:!0}},update:function(e,n,i,a,s){var r=p(n,"foreach");l(e,o,r.foreach),t.bindingHandlers.template.update(e,function(){return r},i,a,s)},connectClass:"ko_container",allowDrop:!0,afterMove:null,beforeMove:null,options:{}},t.bindingHandlers.draggable={init:function(n,i,o,a,c){var u=r(i())||{},h=u.options||{},d=t.utils.extend({},t.bindingHandlers.draggable.options),f=p(i,"data"),m=u.connectClass||t.bindingHandlers.draggable.connectClass,g=void 0!==u.isEnabled?u.isEnabled:t.bindingHandlers.draggable.isEnabled;return u="data"in u?u.data:u,l(n,s,u),t.utils.extend(d,h),d.connectToSortable=!!m&&"."+m,e(n).draggable(d),void 0!==g&&t.computed({read:function(){e(n).draggable(r(g)?"enable":"disable")},disposeWhenNodeIsRemoved:n}),t.bindingHandlers.template.init(n,function(){return f},o,a,c)},update:function(e,n,i,o,a){var s=p(n,"data");return t.bindingHandlers.template.update(e,function(){return s},i,o,a)},connectClass:t.bindingHandlers.sortable.connectClass,options:{helper:"clone"}}}),function(){var t=this,e=t._,n=Array.prototype,i=Object.prototype,o=Function.prototype,a=n.push,s=n.slice,r=n.concat,c=i.toString,l=i.hasOwnProperty,u=Array.isArray,h=Object.keys,d=o.bind,p=function(t){return t instanceof p?t:this instanceof p?void(this._wrapped=t):new p(t)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=p),exports._=p):t._=p,p.VERSION="1.7.0";var f=function(t,e,n){if(void 0===e)return t;switch(null==n?3:n){case 1:return function(n){return t.call(e,n)};case 2:return function(n,i){return t.call(e,n,i)};case 3:return function(n,i,o){return t.call(e,n,i,o)};case 4:return function(n,i,o,a){return t.call(e,n,i,o,a)}}return function(){return t.apply(e,arguments)}};p.iteratee=function(t,e,n){return null==t?p.identity:p.isFunction(t)?f(t,e,n):p.isObject(t)?p.matches(t):p.property(t)},p.each=p.forEach=function(t,e,n){if(null==t)return t;e=f(e,n);var i,o=t.length;if(o===+o)for(i=0;i=0)},p.invoke=function(t,e){var n=s.call(arguments,2),i=p.isFunction(e);return p.map(t,function(t){return(i?e:t[e]).apply(t,n)})},p.pluck=function(t,e){return p.map(t,p.property(e))},p.where=function(t,e){return p.filter(t,p.matches(e))},p.findWhere=function(t,e){return p.find(t,p.matches(e))},p.max=function(t,e,n){var i,o,a=-(1/0),s=-(1/0);if(null==e&&null!=t){t=t.length===+t.length?t:p.values(t);for(var r=0,c=t.length;ra&&(a=i)}else e=p.iteratee(e,n),p.each(t,function(t,n,i){o=e(t,n,i),(o>s||o===-(1/0)&&a===-(1/0))&&(a=t,s=o)});return a},p.min=function(t,e,n){var i,o,a=1/0,s=1/0;if(null==e&&null!=t){t=t.length===+t.length?t:p.values(t);for(var r=0,c=t.length;ri||void 0===n)return 1;if(n>>1;n(t[r])=0;)if(t[i]===e)return i;return-1},p.range=function(t,e,n){arguments.length<=1&&(e=t||0,t=0),n=n||1;for(var i=Math.max(Math.ceil((e-t)/n),0),o=Array(i),a=0;ae?(clearTimeout(s),s=null,r=l,a=t.apply(i,o),s||(i=o=null)):s||n.trailing===!1||(s=setTimeout(c,u)),a}},p.debounce=function(t,e,n){var i,o,a,s,r,c=function(){var l=p.now()-s;l0?i=setTimeout(c,e-l):(i=null,n||(r=t.apply(a,o),i||(a=o=null)))};return function(){a=this,o=arguments,s=p.now();var l=n&&!i;return i||(i=setTimeout(c,e)),l&&(r=t.apply(a,o),a=o=null),r}},p.wrap=function(t,e){return p.partial(e,t)},p.negate=function(t){return function(){return!t.apply(this,arguments)}},p.compose=function(){var t=arguments,e=t.length-1;return function(){for(var n=e,i=t[e].apply(this,arguments);n--;)i=t[n].call(this,i);return i}},p.after=function(t,e){return function(){if(--t<1)return e.apply(this,arguments)}},p.before=function(t,e){var n;return function(){return--t>0?n=e.apply(this,arguments):e=null,n}},p.once=p.partial(p.before,2),p.keys=function(t){if(!p.isObject(t))return[];if(h)return h(t);var e=[];for(var n in t)p.has(t,n)&&e.push(n);return e},p.values=function(t){for(var e=p.keys(t),n=e.length,i=Array(n),o=0;o":">",'"':""","'":"'","`":"`"},A=p.invert(y),w=function(t){var e=function(e){return t[e]},n="(?:"+p.keys(t).join("|")+")",i=RegExp(n),o=RegExp(n,"g");return function(t){return t=null==t?"":""+t,i.test(t)?t.replace(o,e):t}};p.escape=w(y),p.unescape=w(A),p.result=function(t,e){if(null!=t){var n=t[e];return p.isFunction(n)?t[e]():n}};var z=0;p.uniqueId=function(t){var e=++z+"";return t?t+e:e},p.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var _=/(.)^/,T={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},x=/\\|'|\r|\n|\u2028|\u2029/g,C=function(t){return"\\"+T[t]};p.template=function(t,e,n){!e&&n&&(e=n),e=p.defaults({},e,p.templateSettings);var i=RegExp([(e.escape||_).source,(e.interpolate||_).source,(e.evaluate||_).source].join("|")+"|$","g"),o=0,a="__p+='";t.replace(i,function(e,n,i,s,r){return a+=t.slice(o,r).replace(x,C),o=r+e.length,n?a+="'+\n((__t=("+n+"))==null?'':_.escape(__t))+\n'":i?a+="'+\n((__t=("+i+"))==null?'':__t)+\n'":s&&(a+="';\n"+s+"\n__p+='"),e}),a+="';\n",e.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{var s=new Function(e.variable||"obj","_",a)}catch(r){throw r.source=a,r}var c=function(t){return s.call(this,t,p)},l=e.variable||"obj";return c.source="function("+l+"){\n"+a+"}",c},p.chain=function(t){var e=p(t);return e._chain=!0,e};var O=function(t){return this._chain?p(t).chain():t};p.mixin=function(t){p.each(p.functions(t),function(e){var n=p[e]=t[e];p.prototype[e]=function(){var t=[this._wrapped];return a.apply(t,arguments),O.call(this,n.apply(p,t))}})},p.mixin(p),p.each(["pop","push","reverse","shift","sort","splice","unshift"],function(t){var e=n[t];p.prototype[t]=function(){var n=this._wrapped;return e.apply(n,arguments),"shift"!==t&&"splice"!==t||0!==n.length||delete n[0],O.call(this,n)}}),p.each(["concat","join","slice"],function(t){var e=n[t];p.prototype[t]=function(){return O.call(this,e.apply(this._wrapped,arguments))}}),p.prototype.value=function(){return this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return p})}.call(this),function(t,e){function n(){return new Date(Date.UTC.apply(Date,arguments))}function i(){var t=new Date;return n(t.getFullYear(),t.getMonth(),t.getDate())}function o(t,e){return t.getUTCFullYear()===e.getUTCFullYear()&&t.getUTCMonth()===e.getUTCMonth()&&t.getUTCDate()===e.getUTCDate()}function a(t){return function(){return this[t].apply(this,arguments)}}function s(e,n){function i(t,e){return e.toLowerCase()}var o,a=t(e).data(),s={},r=new RegExp("^"+n.toLowerCase()+"([A-Z])");n=new RegExp("^"+n.toLowerCase());for(var c in a)n.test(c)&&(o=c.replace(r,i),s[o]=a[c]);return s}function r(e){var n={};if(m[e]||(e=e.split("-")[0],m[e])){var i=m[e];return t.each(f,function(t,e){e in i&&(n[e]=i[e])}),n}}var c=function(){var e={get:function(t){return this.slice(t)[0]},contains:function(t){for(var e=t&&t.valueOf(),n=0,i=this.length;no?(this.picker.addClass("datepicker-orient-right"),p=u.left+d-e):this.picker.addClass("datepicker-orient-left");var m,g,b=this.o.orientation.y;if("auto"===b&&(m=-s+f-n,g=s+a-(f+h+n),b=Math.max(m,g)===g?"top":"bottom"),this.picker.addClass("datepicker-orient-"+b),"top"===b?f+=h:f-=n+parseInt(this.picker.css("padding-top")),this.o.rtl){var v=o-(p+d);this.picker.css({top:f,right:v,zIndex:l})}else this.picker.css({top:f,left:p,zIndex:l});return this},_allow_update:!0,update:function(){if(!this._allow_update)return this;var e=this.dates.copy(),n=[],i=!1;return arguments.length?(t.each(arguments,t.proxy(function(t,e){e instanceof Date&&(e=this._local_to_utc(e)),n.push(e)},this)),i=!0):(n=this.isInput?this.element.val():this.element.data("date")||this.element.find("input").val(),n=n&&this.o.multidate?n.split(this.o.multidateSeparator):[n],delete this.element.data().date),n=t.map(n,t.proxy(function(t){return g.parseDate(t,this.o.format,this.o.language)},this)),n=t.grep(n,t.proxy(function(t){return tthis.o.endDate||!t},this),!0),this.dates.replace(n),this.dates.length?this.viewDate=new Date(this.dates.get(-1)):this.viewDatethis.o.endDate&&(this.viewDate=new Date(this.o.endDate)),i?this.setValue():n.length&&String(e)!==String(this.dates)&&this._trigger("changeDate"),!this.dates.length&&e.length&&this._trigger("clearDate"),this.fill(),this},fillDow:function(){var t=this.o.weekStart,e="";if(this.o.calendarWeeks){this.picker.find(".datepicker-days thead tr:first-child .datepicker-switch").attr("colspan",function(t,e){return parseInt(e)+1});var n=' ';e+=n}for(;t'+m[this.o.language].daysMin[t++%7]+"";e+="",this.picker.find(".datepicker-days thead").append(e)},fillMonths:function(){for(var t="",e=0;e<12;)t+=''+m[this.o.language].monthsShort[e++]+"";this.picker.find(".datepicker-months td").html(t)},setRange:function(e){e&&e.length?this.range=t.map(e,function(t){return t.valueOf()}):delete this.range,this.fill()},getClassNames:function(e){var n=[],i=this.viewDate.getUTCFullYear(),a=this.viewDate.getUTCMonth(),s=new Date;return e.getUTCFullYear()i||e.getUTCFullYear()===i&&e.getUTCMonth()>a)&&n.push("new"),this.focusDate&&e.valueOf()===this.focusDate.valueOf()&&n.push("focused"),this.o.todayHighlight&&e.getUTCFullYear()===s.getFullYear()&&e.getUTCMonth()===s.getMonth()&&e.getUTCDate()===s.getDate()&&n.push("today"),this.dates.contains(e)!==-1&&n.push("active"),(e.valueOf()this.o.endDate||t.inArray(e.getUTCDay(),this.o.daysOfWeekDisabled)!==-1)&&n.push("disabled"),this.o.datesDisabled.length>0&&t.grep(this.o.datesDisabled,function(t){return o(e,t)}).length>0&&n.push("disabled","disabled-date"),this.range&&(e>this.range[0]&&e"),this.o.calendarWeeks)){var y=new Date(+p+(this.o.weekStart-p.getUTCDay()-7)%7*864e5),A=new Date(Number(y)+(11-y.getUTCDay())%7*864e5),w=new Date(Number(w=n(A.getUTCFullYear(),0,1))+(11-w.getUTCDay())%7*864e5),z=(A-w)/864e5/7+1;M.push(''+z+"")}if(v=this.getClassNames(p),v.push("day"),this.o.beforeShowDay!==t.noop){var _=this.o.beforeShowDay(this._utc_to_local(p));_===e?_={}:"boolean"==typeof _?_={enabled:_}:"string"==typeof _&&(_={classes:_}),_.enabled===!1&&v.push("disabled"),_.classes&&(v=v.concat(_.classes.split(/\s+/))),_.tooltip&&(i=_.tooltip)}v=t.unique(v),M.push('"+p.getUTCDate()+""),i=null,p.getUTCDay()===this.o.weekEnd&&M.push(""),p.setUTCDate(p.getUTCDate()+1)}this.picker.find(".datepicker-days tbody").empty().append(M.join(""));var T=this.picker.find(".datepicker-months").find("th:eq(1)").text(a).end().find("span").removeClass("active");if(t.each(this.dates,function(t,e){e.getUTCFullYear()===a&&T.eq(e.getUTCMonth()).addClass("active")}),(al)&&T.addClass("disabled"),a===r&&T.slice(0,c).addClass("disabled"),a===l&&T.slice(u+1).addClass("disabled"),this.o.beforeShowMonth!==t.noop){var x=this;t.each(T,function(e,n){if(!t(n).hasClass("disabled")){var i=new Date(a,e,1),o=x.o.beforeShowMonth(i);o===!1&&t(n).addClass("disabled")}})}M="",a=10*parseInt(a/10,10);var C=this.picker.find(".datepicker-years").find("th:eq(1)").text(a+"-"+(a+9)).end().find("td");a-=1;for(var O,S=t.map(this.dates,function(t){return t.getUTCFullYear()}),N=-1;N<11;N++)O=["year"],N===-1?O.push("old"):10===N&&O.push("new"),t.inArray(a,S)!==-1&&O.push("active"),(al)&&O.push("disabled"),M+=''+a+"",a+=1;C.html(M)}},updateNavArrows:function(){if(this._allow_update){var t=new Date(this.viewDate),e=t.getUTCFullYear(),n=t.getUTCMonth();switch(this.viewMode){case 0:this.o.startDate!==-(1/0)&&e<=this.o.startDate.getUTCFullYear()&&n<=this.o.startDate.getUTCMonth()?this.picker.find(".prev").css({visibility:"hidden"}):this.picker.find(".prev").css({visibility:"visible"}),this.o.endDate!==1/0&&e>=this.o.endDate.getUTCFullYear()&&n>=this.o.endDate.getUTCMonth()?this.picker.find(".next").css({visibility:"hidden"}):this.picker.find(".next").css({visibility:"visible"});break;case 1:case 2:this.o.startDate!==-(1/0)&&e<=this.o.startDate.getUTCFullYear()?this.picker.find(".prev").css({visibility:"hidden"}):this.picker.find(".prev").css({visibility:"visible"}),this.o.endDate!==1/0&&e>=this.o.endDate.getUTCFullYear()?this.picker.find(".next").css({visibility:"hidden"}):this.picker.find(".next").css({visibility:"visible"})}}},click:function(e){e.preventDefault();var i,o,a,s=t(e.target).closest("span, td, th");if(1===s.length)switch(s[0].nodeName.toLowerCase()){case"th":switch(s[0].className){case"datepicker-switch":this.showMode(1);break;case"prev":case"next":var r=g.modes[this.viewMode].navStep*("prev"===s[0].className?-1:1);switch(this.viewMode){case 0:this.viewDate=this.moveMonth(this.viewDate,r),this._trigger("changeMonth",this.viewDate);break;case 1:case 2:this.viewDate=this.moveYear(this.viewDate,r),1===this.viewMode&&this._trigger("changeYear",this.viewDate)}this.fill();break;case"today":var c=new Date;c=n(c.getFullYear(),c.getMonth(),c.getDate(),0,0,0),this.showMode(-2);var l="linked"===this.o.todayBtn?null:"view";this._setDate(c,l);break;case"clear":this.clearDates()}break;case"span":s.hasClass("disabled")||(this.viewDate.setUTCDate(1),s.hasClass("month")?(a=1,o=s.parent().find("span").index(s),i=this.viewDate.getUTCFullYear(),this.viewDate.setUTCMonth(o),this._trigger("changeMonth",this.viewDate),1===this.o.minViewMode&&this._setDate(n(i,o,a))):(a=1,o=0,i=parseInt(s.text(),10)||0,this.viewDate.setUTCFullYear(i),this._trigger("changeYear",this.viewDate),2===this.o.minViewMode&&this._setDate(n(i,o,a))),this.showMode(-1),this.fill());break;case"td":s.hasClass("day")&&!s.hasClass("disabled")&&(a=parseInt(s.text(),10)||1,i=this.viewDate.getUTCFullYear(),o=this.viewDate.getUTCMonth(),s.hasClass("old")?0===o?(o=11,i-=1):o-=1:s.hasClass("new")&&(11===o?(o=0,i+=1):o+=1),this._setDate(n(i,o,a)))}this.picker.is(":visible")&&this._focused_from&&t(this._focused_from).focus(),delete this._focused_from},_toggle_multidate:function(t){var e=this.dates.contains(t);if(t||this.dates.clear(),e!==-1?(this.o.multidate===!0||this.o.multidate>1||this.o.toggleActive)&&this.dates.remove(e):this.o.multidate===!1?(this.dates.clear(),this.dates.push(t)):this.dates.push(t),"number"==typeof this.o.multidate)for(;this.dates.length>this.o.multidate;)this.dates.remove(0)},_setDate:function(t,e){e&&"date"!==e||this._toggle_multidate(t&&new Date(t)),e&&"view"!==e||(this.viewDate=t&&new Date(t)),this.fill(),this.setValue(),e&&"view"===e||this._trigger("changeDate");var n;this.isInput?n=this.element:this.component&&(n=this.element.find("input")),n&&n.change(),!this.o.autoclose||e&&"date"!==e||this.hide()},moveMonth:function(t,n){if(!t)return e;if(!n)return t;var i,o,a=new Date(t.valueOf()),s=a.getUTCDate(),r=a.getUTCMonth(),c=Math.abs(n);if(n=n>0?1:-1,1===c)o=n===-1?function(){return a.getUTCMonth()===r}:function(){return a.getUTCMonth()!==i},i=r+n,a.setUTCMonth(i),(i<0||i>11)&&(i=(i+12)%12);else{for(var l=0;l=this.o.startDate&&t<=this.o.endDate},keydown:function(t){if(!this.picker.is(":visible"))return void(27===t.keyCode&&this.show());var e,n,o,a=!1,s=this.focusDate||this.viewDate;switch(t.keyCode){case 27:this.focusDate?(this.focusDate=null,this.viewDate=this.dates.get(-1)||this.viewDate,this.fill()):this.hide(),t.preventDefault();break;case 37:case 39:if(!this.o.keyboardNavigation)break;e=37===t.keyCode?-1:1,t.ctrlKey?(n=this.moveYear(this.dates.get(-1)||i(),e),o=this.moveYear(s,e),this._trigger("changeYear",this.viewDate)):t.shiftKey?(n=this.moveMonth(this.dates.get(-1)||i(),e),o=this.moveMonth(s,e),this._trigger("changeMonth",this.viewDate)):(n=new Date(this.dates.get(-1)||i()),n.setUTCDate(n.getUTCDate()+e),o=new Date(s),o.setUTCDate(s.getUTCDate()+e)),this.dateWithinRange(o)&&(this.focusDate=this.viewDate=o,this.setValue(),this.fill(),t.preventDefault());break;case 38:case 40:if(!this.o.keyboardNavigation)break;e=38===t.keyCode?-1:1,t.ctrlKey?(n=this.moveYear(this.dates.get(-1)||i(),e),o=this.moveYear(s,e),this._trigger("changeYear",this.viewDate)):t.shiftKey?(n=this.moveMonth(this.dates.get(-1)||i(),e),o=this.moveMonth(s,e),this._trigger("changeMonth",this.viewDate)):(n=new Date(this.dates.get(-1)||i()),n.setUTCDate(n.getUTCDate()+7*e),o=new Date(s),o.setUTCDate(s.getUTCDate()+7*e)),this.dateWithinRange(o)&&(this.focusDate=this.viewDate=o,this.setValue(),this.fill(),t.preventDefault());break;case 32:break;case 13:s=this.focusDate||this.dates.get(-1)||this.viewDate,this.o.keyboardNavigation&&(this._toggle_multidate(s),a=!0),this.focusDate=null,this.viewDate=this.dates.get(-1)||this.viewDate,this.setValue(),this.fill(),this.picker.is(":visible")&&(t.preventDefault(),"function"==typeof t.stopPropagation?t.stopPropagation():t.cancelBubble=!0,this.o.autoclose&&this.hide());break;case 9:this.focusDate=null,this.viewDate=this.dates.get(-1)||this.viewDate,this.fill(),this.hide()}if(a){this.dates.length?this._trigger("changeDate"):this._trigger("clearDate");var r;this.isInput?r=this.element:this.component&&(r=this.element.find("input")),r&&r.change()}},showMode:function(t){t&&(this.viewMode=Math.max(this.o.minViewMode,Math.min(2,this.viewMode+t))),this.picker.children("div").hide().filter(".datepicker-"+g.modes[this.viewMode].clsName).css("display","block"),this.updateNavArrows()}};var u=function(e,n){this.element=t(e),this.inputs=t.map(n.inputs,function(t){return t.jquery?t[0]:t}),delete n.inputs,d.call(t(this.inputs),n).bind("changeDate",t.proxy(this.dateUpdated,this)),this.pickers=t.map(this.inputs,function(e){return t(e).data("datepicker")}),this.updateDates()};u.prototype={updateDates:function(){this.dates=t.map(this.pickers,function(t){return t.getUTCDate()}),this.updateRanges()},updateRanges:function(){var e=t.map(this.dates,function(t){return t.valueOf()});t.each(this.pickers,function(t,n){n.setRange(e)})},dateUpdated:function(e){if(!this.updating){this.updating=!0;var n=t(e.target).data("datepicker"),i=n.getUTCDate(),o=t.inArray(e.target,this.inputs),a=o-1,s=o+1,r=this.inputs.length;if(o!==-1){if(t.each(this.pickers,function(t,e){e.getUTCDate()||e.setUTCDate(i)}),i=0&&ithis.dates[s])for(;sthis.dates[s];)this.pickers[s++].setUTCDate(i);this.updateDates(),delete this.updating}}},remove:function(){t.map(this.pickers,function(t){t.remove()}),delete this.element.data().datepicker}};var h=t.fn.datepicker,d=function(n){var i=Array.apply(null,arguments);i.shift();var o;return this.each(function(){var a=t(this),c=a.data("datepicker"),h="object"==typeof n&&n;if(!c){var d=s(this,"date"),f=t.extend({},p,d,h),m=r(f.language),g=t.extend({},p,m,d,h);if(a.hasClass("input-daterange")||g.inputs){var b={inputs:g.inputs||a.find("input").toArray()};a.data("datepicker",c=new u(this,t.extend(g,b)))}else a.data("datepicker",c=new l(this,g))}if("string"==typeof n&&"function"==typeof c[n]&&(o=c[n].apply(c,i),o!==e))return!1}),o!==e?o:this};t.fn.datepicker=d;var p=t.fn.datepicker.defaults={autoclose:!1,beforeShowDay:t.noop,beforeShowMonth:t.noop,calendarWeeks:!1,clearBtn:!1,toggleActive:!1,daysOfWeekDisabled:[],datesDisabled:[],endDate:1/0,forceParse:!0,format:"mm/dd/yyyy",keyboardNavigation:!0,language:"en",minViewMode:0,multidate:!1,multidateSeparator:",",orientation:"auto",rtl:!1,startDate:-(1/0),startView:0,todayBtn:!1,todayHighlight:!1,weekStart:0,disableTouchKeyboard:!1,enableOnReadonly:!0,container:"body"},f=t.fn.datepicker.locale_opts=["format","rtl","weekStart"];t.fn.datepicker.Constructor=l;var m=t.fn.datepicker.dates={en:{days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat","Sun"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa","Su"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",clear:"Clear"}},g={modes:[{clsName:"days",navFnc:"Month",navStep:1},{clsName:"months",navFnc:"FullYear",navStep:1},{clsName:"years",navFnc:"FullYear",navStep:10}],isLeapYear:function(t){return t%4===0&&t%100!==0||t%400===0},getDaysInMonth:function(t,e){return[31,g.isLeapYear(t)?29:28,31,30,31,30,31,31,30,31,30,31][e]},validParts:/dd?|DD?|mm?|MM?|yy(?:yy)?/g,nonpunctuation:/[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,parseFormat:function(t){var e=t.replace(this.validParts,"\0").split("\0"),n=t.match(this.validParts);if(!e||!e.length||!n||0===n.length)throw new Error("Invalid date format.");return{separators:e,parts:n}},parseDate:function(i,o,a){function s(){var t=this.slice(0,d[u].length),e=d[u].slice(0,t.length);return t.toLowerCase()===e.toLowerCase()}if(!i)return e;if(i instanceof Date)return i;"string"==typeof o&&(o=g.parseFormat(o));var r,c,u,h=/([\-+]\d+)([dmwy])/,d=i.match(/([\-+]\d+)([dmwy])/g);if(/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(i)){for(i=new Date,u=0;u«»',contTemplate:'',footTemplate:''};g.template='
    '+g.headTemplate+""+g.footTemplate+'
    '+g.headTemplate+g.contTemplate+g.footTemplate+'
    '+g.headTemplate+g.contTemplate+g.footTemplate+"
    ",t.fn.datepicker.DPGlobal=g,t.fn.datepicker.noConflict=function(){return t.fn.datepicker=h,this},t.fn.datepicker.version="1.4.0",t(document).on("focus.datepicker.data-api click.datepicker.data-api",'[data-provide="datepicker"]',function(e){var n=t(this);n.data("datepicker")||(e.preventDefault(),d.call(n,"show"))}),t(function(){d.call(t('[data-provide="datepicker-inline"]'))})}(window.jQuery),!function(t){t.fn.datepicker.dates.de={days:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","Sonntag"],daysShort:["Son","Mon","Die","Mit","Don","Fre","Sam","Son"],daysMin:["So","Mo","Di","Mi","Do","Fr","Sa","So"],months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthsShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],today:"Heute",clear:"Löschen",weekStart:1,format:"dd.mm.yyyy"}}(jQuery),!function(t){t.fn.datepicker.dates.da={days:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag","Søndag"],daysShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør","Søn"],daysMin:["Sø","Ma","Ti","On","To","Fr","Lø","Sø"],months:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],today:"I Dag",clear:"Nulstil"}}(jQuery),!function(t){t.fn.datepicker.dates["pt-BR"]={days:["Domingo","Segunda","Terça","Quarta","Quinta","Sexta","Sábado","Domingo"],daysShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb","Dom"],daysMin:["Do","Se","Te","Qu","Qu","Se","Sa","Do"],months:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthsShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],today:"Hoje",clear:"Limpar"}}(jQuery),!function(t){t.fn.datepicker.dates.nl={days:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag","zondag"],daysShort:["zo","ma","di","wo","do","vr","za","zo"],daysMin:["zo","ma","di","wo","do","vr","za","zo"],months:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthsShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],today:"Vandaag",clear:"Wissen",weekStart:1,format:"dd-mm-yyyy"}}(jQuery),!function(t){t.fn.datepicker.dates.fr={days:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi","dimanche"],daysShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam.","dim."],daysMin:["d","l","ma","me","j","v","s","d"],months:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthsShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],today:"Aujourd'hui",clear:"Effacer",weekStart:1,format:"dd/mm/yyyy"}}(jQuery),!function(t){t.fn.datepicker.dates.it={days:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato","Domenica"],daysShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab","Dom"],daysMin:["Do","Lu","Ma","Me","Gi","Ve","Sa","Do"],months:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthsShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],today:"Oggi",clear:"Cancella",weekStart:1,format:"dd/mm/yyyy"}}(jQuery),!function(t){t.fn.datepicker.dates.lt={days:["Sekmadienis","Pirmadienis","Antradienis","Trečiadienis","Ketvirtadienis","Penktadienis","Šeštadienis","Sekmadienis"],daysShort:["S","Pr","A","T","K","Pn","Š","S"],daysMin:["Sk","Pr","An","Tr","Ke","Pn","Št","Sk"],months:["Sausis","Vasaris","Kovas","Balandis","Gegužė","Birželis","Liepa","Rugpjūtis","Rugsėjis","Spalis","Lapkritis","Gruodis"],monthsShort:["Sau","Vas","Kov","Bal","Geg","Bir","Lie","Rugp","Rugs","Spa","Lap","Gru"],today:"Šiandien",weekStart:1}}(jQuery),!function(t){t.fn.datepicker.dates.no={days:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],daysShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],daysMin:["Sø","Ma","Ti","On","To","Fr","Lø"],months:["Januar","Februar","Mars","April","Mai","Juni","Juli","August","September","Oktober","November","Desember"],monthsShort:["Jan","Feb","Mar","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Des"],today:"I dag",clear:"Nullstill",weekStart:1,format:"dd.mm.yyyy"}}(jQuery),!function(t){t.fn.datepicker.dates.es={days:["Domingo","Lunes","Martes","Miércoles","Jueves","Viernes","Sábado","Domingo"],daysShort:["Dom","Lun","Mar","Mié","Jue","Vie","Sáb","Dom"],daysMin:["Do","Lu","Ma","Mi","Ju","Vi","Sa","Do"],months:["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],monthsShort:["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"],today:"Hoy",clear:"Borrar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery),!function(t){t.fn.datepicker.dates.sv={days:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag","Söndag"],daysShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör","Sön"],daysMin:["Sö","Må","Ti","On","To","Fr","Lö","Sö"],months:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],today:"Idag",format:"yyyy-mm-dd",weekStart:1,clear:"Rensa"}}(jQuery),function(){var t,e,n,i,o,a,s,r,c=[].slice,l={}.hasOwnProperty,u=function(t,e){function n(){this.constructor=t}for(var i in e)l.call(e,i)&&(t[i]=e[i]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t};s=function(){},e=function(){function t(){}return t.prototype.addEventListener=t.prototype.on,t.prototype.on=function(t,e){return this._callbacks=this._callbacks||{},this._callbacks[t]||(this._callbacks[t]=[]),this._callbacks[t].push(e),this},t.prototype.emit=function(){var t,e,n,i,o,a;if(i=arguments[0],t=2<=arguments.length?c.call(arguments,1):[],this._callbacks=this._callbacks||{},n=this._callbacks[i])for(o=0,a=n.length;o
    '),this.element.appendChild(e)),i=e.getElementsByTagName("span")[0],i&&(null!=i.textContent?i.textContent=this.options.dictFallbackMessage:null!=i.innerText&&(i.innerText=this.options.dictFallbackMessage)),this.element.appendChild(this.getFallbackForm())},resize:function(t){var e,n,i;return e={srcX:0,srcY:0,srcWidth:t.width,srcHeight:t.height},n=t.width/t.height,e.optWidth=this.options.thumbnailWidth,e.optHeight=this.options.thumbnailHeight,null==e.optWidth&&null==e.optHeight?(e.optWidth=e.srcWidth,e.optHeight=e.srcHeight):null==e.optWidth?e.optWidth=n*e.optHeight:null==e.optHeight&&(e.optHeight=1/n*e.optWidth),i=e.optWidth/e.optHeight,t.heighti?(e.srcHeight=t.height,e.srcWidth=e.srcHeight*i):(e.srcWidth=t.width,e.srcHeight=e.srcWidth/i),e.srcX=(t.width-e.srcWidth)/2,e.srcY=(t.height-e.srcHeight)/2,e},drop:function(t){return this.element.classList.remove("dz-drag-hover")},dragstart:s,dragend:function(t){return this.element.classList.remove("dz-drag-hover")},dragenter:function(t){return this.element.classList.add("dz-drag-hover")},dragover:function(t){return this.element.classList.add("dz-drag-hover")},dragleave:function(t){return this.element.classList.remove("dz-drag-hover")},paste:s,reset:function(){return this.element.classList.remove("dz-started")},addedfile:function(t){var e,i,o,a,s,r,c,l,u,h,d,p,f;if(this.element===this.previewsContainer&&this.element.classList.add("dz-started"),this.previewsContainer){for(t.previewElement=n.createElement(this.options.previewTemplate.trim()),t.previewTemplate=t.previewElement,this.previewsContainer.appendChild(t.previewElement),h=t.previewElement.querySelectorAll("[data-dz-name]"),a=0,c=h.length;a'+this.options.dictRemoveFile+""),t.previewElement.appendChild(t._removeLink)),i=function(e){return function(i){return i.preventDefault(),i.stopPropagation(),t.status===n.UPLOADING?n.confirm(e.options.dictCancelUploadConfirmation,function(){return e.removeFile(t)}):e.options.dictRemoveFileConfirmation?n.confirm(e.options.dictRemoveFileConfirmation,function(){return e.removeFile(t)}):e.removeFile(t)}}(this),p=t.previewElement.querySelectorAll("[data-dz-remove]"),f=[],r=0,u=p.length;r\n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n \n Check\n \n \n \n \n \n
    \n
    \n \n Error\n \n \n \n \n \n \n \n
    \n'},i=function(){var t,e,n,i,o,a,s;for(i=arguments[0],n=2<=arguments.length?c.call(arguments,1):[],a=0,s=n.length;a'+this.options.dictDefaultMessage+"")),this.clickableElements.length&&(i=function(t){return function(){return t.hiddenFileInput&&t.hiddenFileInput.parentNode.removeChild(t.hiddenFileInput),t.hiddenFileInput=document.createElement("input"),t.hiddenFileInput.setAttribute("type","file"),(null==t.options.maxFiles||t.options.maxFiles>1)&&t.hiddenFileInput.setAttribute("multiple","multiple"),t.hiddenFileInput.className="dz-hidden-input",null!=t.options.acceptedFiles&&t.hiddenFileInput.setAttribute("accept",t.options.acceptedFiles),null!=t.options.capture&&t.hiddenFileInput.setAttribute("capture",t.options.capture),t.hiddenFileInput.style.visibility="hidden",t.hiddenFileInput.style.position="absolute",t.hiddenFileInput.style.top="0",t.hiddenFileInput.style.left="0",t.hiddenFileInput.style.height="0",t.hiddenFileInput.style.width="0",document.querySelector(t.options.hiddenInputContainer).appendChild(t.hiddenFileInput),t.hiddenFileInput.addEventListener("change",function(){var e,n,o,a;if(n=t.hiddenFileInput.files,n.length)for(o=0,a=n.length;o',this.options.dictFallbackText&&(i+="

    "+this.options.dictFallbackText+"

    "),i+='',e=n.createElement(i),"FORM"!==this.element.tagName?(o=n.createElement('
    '),o.appendChild(e)):(this.element.setAttribute("enctype","multipart/form-data"),this.element.setAttribute("method",this.options.method)),null!=o?o:e)},n.prototype.getExistingFallback=function(){var t,e,n,i,o,a;for(e=function(t){var e,n,i;for(n=0,i=t.length;n0){for(s=["TB","GB","MB","KB","b"],n=r=0,c=s.length;r=e){i=t/Math.pow(this.options.filesizeBase,4-n),o=a;break}i=Math.round(10*i)/10}return""+i+" "+o},n.prototype._updateMaxFilesReachedClass=function(){return null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(this.getAcceptedFiles().length===this.options.maxFiles&&this.emit("maxfilesreached",this.files),this.element.classList.add("dz-max-files-reached")):this.element.classList.remove("dz-max-files-reached")},n.prototype.drop=function(t){var e,n;t.dataTransfer&&(this.emit("drop",t),e=t.dataTransfer.files,this.emit("addedfiles",e),e.length&&(n=t.dataTransfer.items,n&&n.length&&null!=n[0].webkitGetAsEntry?this._addFilesFromItems(n):this.handleFiles(e)))},n.prototype.paste=function(t){var e,n;if(null!=(null!=t&&null!=(n=t.clipboardData)?n.items:void 0))return this.emit("paste",t),e=t.clipboardData.items,e.length?this._addFilesFromItems(e):void 0},n.prototype.handleFiles=function(t){var e,n,i,o;for(o=[],n=0,i=t.length;n0){for(a=0,s=n.length;a1024*this.options.maxFilesize*1024?e(this.options.dictFileTooBig.replace("{{filesize}}",Math.round(t.size/1024/10.24)/100).replace("{{maxFilesize}}",this.options.maxFilesize)):n.isValidFile(t,this.options.acceptedFiles)?null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(e(this.options.dictMaxFilesExceeded.replace("{{maxFiles}}",this.options.maxFiles)),this.emit("maxfilesexceeded",t)):this.options.accept.call(this,t,e):e(this.options.dictInvalidFileType)},n.prototype.addFile=function(t){return t.upload={progress:0,total:t.size,bytesSent:0},this.files.push(t),t.status=n.ADDED,this.emit("addedfile",t),this._enqueueThumbnail(t),this.accept(t,function(e){return function(n){return n?(t.accepted=!1,e._errorProcessing([t],n)):(t.accepted=!0,e.options.autoQueue&&e.enqueueFile(t)),e._updateMaxFilesReachedClass()}}(this))},n.prototype.enqueueFiles=function(t){var e,n,i;for(n=0,i=t.length;n=e)&&(i=this.getQueuedFiles(),i.length>0)){if(this.options.uploadMultiple)return this.processFiles(i.slice(0,e-n));for(;t=B;u=0<=B?++L:--L)a.append(this._getParamName(u),t[u],this._renameFilename(t[u].name));return this.submitRequest(w,a,t)},n.prototype.submitRequest=function(t,e,n){return t.send(e)},n.prototype._finished=function(t,e,i){var o,a,s;for(a=0,s=t.length;au;)e=o[4*(c-1)+3],0===e?a=c:u=c,c=a+u>>1;return l=c/s,0===l?1:l},a=function(t,e,n,i,a,s,r,c,l,u){var h;return h=o(e),t.drawImage(e,n,i,a,s,r,c,l,u/h)},i=function(t,e){var n,i,o,a,s,r,c,l,u;if(o=!1,u=!0,i=t.document,l=i.documentElement,n=i.addEventListener?"addEventListener":"attachEvent",c=i.addEventListener?"removeEventListener":"detachEvent",r=i.addEventListener?"":"on",a=function(n){if("readystatechange"!==n.type||"complete"===i.readyState)return("load"===n.type?t:i)[c](r+n.type,a,!1),!o&&(o=!0)?e.call(t,n.type||n):void 0},s=function(){var t;try{l.doScroll("left")}catch(e){return t=e,void setTimeout(s,50)}return a("poll")},"complete"!==i.readyState){if(i.createEventObject&&l.doScroll){try{u=!t.frameElement}catch(h){}u&&s()}return i[n](r+"DOMContentLoaded",a,!1),i[n](r+"readystatechange",a,!1),t[n](r+"load",a,!1)}},t._autoDiscoverFunction=function(){if(t.autoDiscover)return t.discover()},i(window,t._autoDiscoverFunction)}.call(this),function(t,e){"function"==typeof define&&define.amd?define("typeahead.js",["jquery"],function(t){return e(t)}):"object"==typeof exports?module.exports=e(require("jquery")):e(jQuery)}(this,function(t){var e=function(){"use strict";return{isMsie:function(){return!!/(msie|trident)/i.test(navigator.userAgent)&&navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]},isBlankString:function(t){return!t||/^\s*$/.test(t)},escapeRegExChars:function(t){return t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isString:function(t){return"string"==typeof t},isNumber:function(t){return"number"==typeof t},isArray:t.isArray,isFunction:t.isFunction,isObject:t.isPlainObject,isUndefined:function(t){return"undefined"==typeof t},isElement:function(t){return!(!t||1!==t.nodeType)},isJQuery:function(e){return e instanceof t},toStr:function(t){return e.isUndefined(t)||null===t?"":t+""},bind:t.proxy,each:function(e,n){function i(t,e){return n(e,t)}t.each(e,i)},map:t.map,filter:t.grep,every:function(e,n){var i=!0;return e?(t.each(e,function(t,o){if(!(i=n.call(null,o,t,e)))return!1}),!!i):i},some:function(e,n){var i=!1;return e?(t.each(e,function(t,o){if(i=n.call(null,o,t,e))return!1}),!!i):i},mixin:t.extend,identity:function(t){return t},clone:function(e){return t.extend(!0,{},e)},getIdGenerator:function(){var t=0;return function(){return t++}},templatify:function(e){function n(){return String(e)}return t.isFunction(e)?e:n},defer:function(t){setTimeout(t,0)},debounce:function(t,e,n){var i,o;return function(){var a,s,r=this,c=arguments;return a=function(){i=null,n||(o=t.apply(r,c))},s=n&&!i,clearTimeout(i),i=setTimeout(a,e),s&&(o=t.apply(r,c)),o}},throttle:function(t,e){var n,i,o,a,s,r;return s=0,r=function(){s=new Date,o=null,a=t.apply(n,i)},function(){var c=new Date,l=e-(c-s);return n=this,i=arguments,l<=0?(clearTimeout(o),o=null,s=c,a=t.apply(n,i)):o||(o=setTimeout(r,l)),a}},stringify:function(t){return e.isString(t)?t:JSON.stringify(t)},noop:function(){}}}(),n=function(){"use strict";function t(t){var s,r;return r=e.mixin({},a,t),s={css:o(),classes:r,html:n(r),selectors:i(r)},{css:s.css,html:s.html,classes:s.classes,selectors:s.selectors,mixin:function(t){e.mixin(t,s)}}}function n(t){return{wrapper:'',menu:'
    '}}function i(t){var n={};return e.each(t,function(t,e){n[e]="."+t}),n}function o(){var t={wrapper:{position:"relative",display:"inline-block"},hint:{position:"absolute",top:"0",left:"0",borderColor:"transparent",boxShadow:"none",opacity:"1"},input:{position:"relative",verticalAlign:"top",backgroundColor:"transparent"},inputWithNoHint:{position:"relative",verticalAlign:"top"},menu:{position:"absolute",top:"100%",left:"0",zIndex:"100",display:"none"},ltr:{left:"0",right:"auto"},rtl:{left:"auto",right:" 0"}};return e.isMsie()&&e.mixin(t.input,{backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"}),t}var a={wrapper:"twitter-typeahead",input:"tt-input",hint:"tt-hint",menu:"tt-menu",dataset:"tt-dataset",suggestion:"tt-suggestion",selectable:"tt-selectable",empty:"tt-empty",open:"tt-open",cursor:"tt-cursor",highlight:"tt-highlight"};return t}(),i=function(){"use strict";function n(e){e&&e.el||t.error("EventBus initialized without el"),this.$el=t(e.el)}var i,o;return i="typeahead:",o={render:"rendered",cursorchange:"cursorchanged",select:"selected",autocomplete:"autocompleted"},e.mixin(n.prototype,{_trigger:function(e,n){var o;return o=t.Event(i+e),(n=n||[]).unshift(o),this.$el.trigger.apply(this.$el,n),o},before:function(t){var e,n;return e=[].slice.call(arguments,1),n=this._trigger("before"+t,e),n.isDefaultPrevented()},trigger:function(t){var e;this._trigger(t,[].slice.call(arguments,1)),(e=o[t])&&this._trigger(e,[].slice.call(arguments,1))}}),n}(),o=function(){"use strict";function t(t,e,n,i){var o;if(!n)return this;for(e=e.split(c),n=i?r(n,i):n,this._callbacks=this._callbacks||{};o=e.shift();)this._callbacks[o]=this._callbacks[o]||{sync:[],async:[]},this._callbacks[o][t].push(n);return this}function e(e,n,i){return t.call(this,"async",e,n,i)}function n(e,n,i){return t.call(this,"sync",e,n,i)}function i(t){var e;if(!this._callbacks)return this;for(t=t.split(c);e=t.shift();)delete this._callbacks[e];return this}function o(t){var e,n,i,o,s;if(!this._callbacks)return this;for(t=t.split(c),i=[].slice.call(arguments,1);(e=t.shift())&&(n=this._callbacks[e]);)o=a(n.sync,this,[e].concat(i)),s=a(n.async,this,[e].concat(i)),o()&&l(s);return this}function a(t,e,n){function i(){for(var i,o=0,a=t.length;!i&&o