mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-27 11:41:09 -04:00
Merge master branch
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,5 +11,3 @@ Homestead.yaml
|
||||
.rnd
|
||||
/.expo
|
||||
/.vscode
|
||||
docker-compose.yml
|
||||
docker-compose.yaml
|
||||
|
||||
74
Dockerfile
74
Dockerfile
@ -1,53 +1,39 @@
|
||||
##### STAGE 1 #####
|
||||
FROM php:7.4-fpm
|
||||
|
||||
FROM composer as composer
|
||||
# Arguments defined in docker-compose.yml
|
||||
ARG user
|
||||
ARG uid
|
||||
|
||||
# Copy composer files from project root into composer container's working dir
|
||||
COPY composer.* /app/
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
libpng-dev \
|
||||
libonig-dev \
|
||||
libxml2-dev \
|
||||
zip \
|
||||
unzip \
|
||||
libzip-dev \
|
||||
libmagickwand-dev
|
||||
|
||||
# Copy database directory for autoloader optimization
|
||||
COPY database /app/database
|
||||
# Clear cache
|
||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Run composer to build dependencies in vendor folder
|
||||
RUN composer install --no-scripts --no-suggest --no-interaction --prefer-dist --optimize-autoloader
|
||||
RUN pecl install imagick \
|
||||
&& docker-php-ext-enable imagick
|
||||
|
||||
# Copy everything from project root into composer container's working dir
|
||||
COPY . /app
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl bcmath gd
|
||||
|
||||
RUN composer dump-autoload --optimize --classmap-authoritative
|
||||
# Get latest Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
##### STAGE 2 #####
|
||||
# Create system user to run Composer and Artisan Commands
|
||||
RUN useradd -G www-data,root -u $uid -d /home/$user $user
|
||||
RUN mkdir -p /home/$user/.composer && \
|
||||
chown -R $user:$user /home/$user
|
||||
|
||||
FROM php:7.3.12-fpm-alpine
|
||||
# Set working directory
|
||||
WORKDIR /var/www
|
||||
|
||||
# Use the default production configuration
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
RUN apk add --no-cache libpng-dev libxml2-dev oniguruma-dev libzip-dev gnu-libiconv && \
|
||||
docker-php-ext-install bcmath ctype json gd mbstring pdo pdo_mysql tokenizer xml zip
|
||||
|
||||
ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so php
|
||||
|
||||
# Set container's working dir
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything from project root into php container's working dir
|
||||
COPY . /app
|
||||
|
||||
# Copy vendor folder from composer container into php container
|
||||
COPY --from=composer /app/vendor /app/vendor
|
||||
|
||||
RUN touch database/database.sqlite && \
|
||||
cp .env.example .env && \
|
||||
php artisan config:cache && \
|
||||
php artisan passport:keys && \
|
||||
php artisan key:generate && \
|
||||
chown -R www-data:www-data . && \
|
||||
chmod -R 755 . && \
|
||||
chmod -R 775 storage/framework/ && \
|
||||
chmod -R 775 storage/logs/ && \
|
||||
chmod -R 775 bootstrap/cache/
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
CMD ["php-fpm", "--nodaemonize"]
|
||||
USER $user
|
||||
|
||||
190
app/Console/Commands/UpdateCommand.php
Normal file
190
app/Console/Commands/UpdateCommand.php
Normal file
@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Crater\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Crater\Space\Updater;
|
||||
use Crater\Setting;
|
||||
|
||||
// Implementation taken from Akaunting - https://github.com/akaunting/akaunting
|
||||
class UpdateCommand extends Command
|
||||
{
|
||||
public $installed;
|
||||
|
||||
public $version;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'crater:update';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Automatically update your crater app';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
set_time_limit(3600); // 1 hour
|
||||
|
||||
$this->installed = $this->getInstalledVersion();
|
||||
$this->version = $this->getLatestVersion();
|
||||
|
||||
if (!$this->version) {
|
||||
$this->info('No Update Available! You are already on the latest version.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->confirm("Do you wish to update to {$this->version}?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$path = $this->download()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$path = $this->unzip($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->copyFiles($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->migrateUpdate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->finish()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info('Successfully updated to ' . $this->version);
|
||||
}
|
||||
|
||||
public function getInstalledVersion()
|
||||
{
|
||||
return Setting::getSetting('version');
|
||||
}
|
||||
|
||||
public function getLatestVersion()
|
||||
{
|
||||
$this->info('Your currently installed version is ' . $this->installed);
|
||||
$this->line('');
|
||||
$this->info('Checking for update...');
|
||||
|
||||
try {
|
||||
$response = Updater::checkForUpdate($this->installed);
|
||||
|
||||
if ($response->success) {
|
||||
return $response->version->version;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function download()
|
||||
{
|
||||
$this->info('Downloading update...');
|
||||
|
||||
try {
|
||||
$path = Updater::download($this->version);
|
||||
if (!is_string($path)) {
|
||||
$this->error('Download exception');
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function unzip($path)
|
||||
{
|
||||
$this->info('Unzipping update package...');
|
||||
|
||||
try {
|
||||
$path = Updater::unzip($path);
|
||||
if (!is_string($path)) {
|
||||
$this->error('Unzipping exception');
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function copyFiles($path)
|
||||
{
|
||||
$this->info('Copying update files...');
|
||||
|
||||
try {
|
||||
Updater::copyFiles($path);
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function migrateUpdate()
|
||||
{
|
||||
$this->info('Running Migrations...');
|
||||
|
||||
try {
|
||||
Updater::migrateUpdate();
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function finish()
|
||||
{
|
||||
$this->info('Finishing update...');
|
||||
|
||||
try {
|
||||
Updater::finishUpdate($this->installed, $this->version);
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,8 @@ class Kernel extends ConsoleKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
Commands\ResetApp::class
|
||||
Commands\ResetApp::class,
|
||||
Commands\UpdateCommand::class
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -2,23 +2,81 @@
|
||||
|
||||
namespace Crater\Http\Controllers;
|
||||
|
||||
use Crater\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
use Crater\Space\Updater;
|
||||
use Crater\Space\SiteApi;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class UpdateController extends Controller
|
||||
{
|
||||
public function update(Request $request)
|
||||
|
||||
public function download(Request $request)
|
||||
{
|
||||
set_time_limit(600); // 10 minutes
|
||||
$request->validate([
|
||||
'version' => 'required',
|
||||
]);
|
||||
|
||||
$json = Updater::update($request->installed, $request->version);
|
||||
$path = Updater::download($request->version);
|
||||
|
||||
return response()->json($json);
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $path
|
||||
]);
|
||||
}
|
||||
|
||||
public function unzip(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'path' => 'required',
|
||||
]);
|
||||
|
||||
try {
|
||||
$path = Updater::unzip($request->path);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $path
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function copyFiles(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'path' => 'required',
|
||||
]);
|
||||
|
||||
$path = Updater::copyFiles($request->path);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $path
|
||||
]);
|
||||
}
|
||||
|
||||
public function migrate(Request $request)
|
||||
{
|
||||
Updater::migrateUpdate();
|
||||
|
||||
return response()->json([
|
||||
'success' => true
|
||||
]);
|
||||
}
|
||||
|
||||
public function finishUpdate(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'installed' => 'required',
|
||||
'version' => 'required',
|
||||
]);
|
||||
|
||||
$json = Updater::finishUpdate($request->installed, $request->version);
|
||||
|
||||
return response()->json($json);
|
||||
@ -28,7 +86,7 @@ class UpdateController extends Controller
|
||||
{
|
||||
set_time_limit(600); // 10 minutes
|
||||
|
||||
$json = Updater::checkForUpdate();
|
||||
$json = Updater::checkForUpdate(Setting::getSetting('version'));
|
||||
|
||||
return response()->json($json);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Crater\Listeners\Updates;
|
||||
|
||||
// Implementation taken from Akaunting - https://github.com/akaunting/akaunting
|
||||
class Listener
|
||||
{
|
||||
const VERSION = '';
|
||||
|
||||
@ -10,25 +10,17 @@ use Crater\Events\UpdateFinished;
|
||||
use Crater\Setting;
|
||||
use Crater\Currency;
|
||||
use Schema;
|
||||
use Artisan;
|
||||
|
||||
class Version310 extends Listener
|
||||
{
|
||||
const VERSION = '3.1.0';
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @param UpdateFinished $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(UpdateFinished $event)
|
||||
@ -52,12 +44,7 @@ class Version310 extends Listener
|
||||
]
|
||||
);
|
||||
|
||||
if (!Schema::hasColumn('expenses', 'user_id')) {
|
||||
Schema::table('expenses', function (Blueprint $table) {
|
||||
$table->integer('user_id')->unsigned()->nullable();
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
Artisan::call('migrate', ['--force' => true]);
|
||||
|
||||
// Update Crater app version
|
||||
Setting::setSetting('version', static::VERSION);
|
||||
|
||||
@ -6,6 +6,7 @@ use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Crater\Setting;
|
||||
|
||||
// Implementation taken from Akaunting - https://github.com/akaunting/akaunting
|
||||
trait SiteApi
|
||||
{
|
||||
|
||||
|
||||
@ -2,31 +2,46 @@
|
||||
namespace Crater\Space;
|
||||
|
||||
use File;
|
||||
use ZipArchive;
|
||||
use Artisan;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Crater\Space\SiteApi;
|
||||
use Crater\Events\UpdateFinished;
|
||||
use Crater\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
use ZipArchive;
|
||||
|
||||
// Implementation taken from Akaunting - https://github.com/akaunting/akaunting
|
||||
class Updater
|
||||
{
|
||||
use SiteApi;
|
||||
|
||||
public static function update($installed, $version)
|
||||
public static function checkForUpdate($installed_version)
|
||||
{
|
||||
$data = null;
|
||||
if(env('APP_ENV') === 'development')
|
||||
{
|
||||
$url = 'https://craterapp.com/downloads/check/latest/'. $installed_version . '?type=update&is_dev=1';
|
||||
} else {
|
||||
$url = 'https://craterapp.com/downloads/check/latest/'. $installed_version . '?type=update';
|
||||
}
|
||||
|
||||
$response = static::getRemote($url, ['timeout' => 100, 'track_redirects' => true]);
|
||||
|
||||
if ($response && ($response->getStatusCode() == 200)) {
|
||||
$data = $response->getBody()->getContents();
|
||||
}
|
||||
|
||||
return json_decode($data);
|
||||
}
|
||||
|
||||
public static function download($new_version)
|
||||
{
|
||||
$data = null;
|
||||
$path = null;
|
||||
|
||||
if(env('APP_ENV') === 'development')
|
||||
{
|
||||
$url = 'https://craterapp.com/downloads/file/'.$version.'?type=update&is_dev=1';
|
||||
if (env('APP_ENV') === 'development') {
|
||||
$url = 'https://craterapp.com/downloads/file/' . $new_version . '?type=update&is_dev=1';
|
||||
} else {
|
||||
$url = 'https://craterapp.com/downloads/file/'.$version.'?type=update';
|
||||
$url = 'https://craterapp.com/downloads/file/' . $new_version . '?type=update';
|
||||
}
|
||||
|
||||
|
||||
$response = static::getRemote($url, ['timeout' => 100, 'track_redirects' => true]);
|
||||
|
||||
// Exception
|
||||
@ -45,66 +60,68 @@ class Updater
|
||||
}
|
||||
|
||||
// Create temp directory
|
||||
$path = 'temp-' . md5(mt_rand());
|
||||
$path2 = 'temp2-' . md5(mt_rand());
|
||||
$temp_path = storage_path('app') . '/' . $path;
|
||||
$temp_path2 = storage_path('app') . '/' . $path2;
|
||||
$temp_dir = storage_path('app/temp-' . md5(mt_rand()));
|
||||
|
||||
if (!File::isDirectory($temp_path)) {
|
||||
File::makeDirectory($temp_path);
|
||||
File::makeDirectory($temp_path2);
|
||||
if (!File::isDirectory($temp_dir)) {
|
||||
File::makeDirectory($temp_dir);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$file = $temp_path . '/upload.zip';
|
||||
$zip_file_path = $temp_dir . '/upload.zip';
|
||||
|
||||
// Add content to the Zip file
|
||||
$uploaded = is_int(file_put_contents($file, $data)) ? true : false;
|
||||
$uploaded = is_int(file_put_contents($zip_file_path, $data)) ? true : false;
|
||||
|
||||
if (!$uploaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $zip_file_path;
|
||||
}
|
||||
|
||||
public static function unzip($zip_file_path)
|
||||
{
|
||||
if(!file_exists($zip_file_path)) {
|
||||
throw new \Exception('Zip file not found');
|
||||
}
|
||||
|
||||
$temp_extract_dir = storage_path('app/temp2-' . md5(mt_rand()));
|
||||
|
||||
if (!File::isDirectory($temp_extract_dir)) {
|
||||
File::makeDirectory($temp_extract_dir);
|
||||
}
|
||||
// Unzip the file
|
||||
$zip = new ZipArchive();
|
||||
|
||||
if ($zip->open($file)) {
|
||||
$zip->extractTo($temp_path2);
|
||||
if ($zip->open($zip_file_path)) {
|
||||
$zip->extractTo($temp_extract_dir);
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
// Delete zip file
|
||||
File::delete($file);
|
||||
File::delete($zip_file_path);
|
||||
|
||||
if (!File::copyDirectory($temp_path2.'/Crater', base_path())) {
|
||||
return $temp_extract_dir;
|
||||
}
|
||||
|
||||
public static function copyFiles($temp_extract_dir)
|
||||
{
|
||||
|
||||
if (!File::copyDirectory($temp_extract_dir . '/Crater', base_path())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete temp directory
|
||||
File::deleteDirectory($temp_path);
|
||||
File::deleteDirectory($temp_path2);
|
||||
File::deleteDirectory($temp_extract_dir);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'error' => false,
|
||||
'data' => []
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
|
||||
if (File::isDirectory($temp_path)) {
|
||||
// Delete temp directory
|
||||
File::deleteDirectory($temp_path);
|
||||
File::deleteDirectory($temp_path2);
|
||||
return true;
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Update error',
|
||||
'data' => []
|
||||
];
|
||||
}
|
||||
public static function migrateUpdate()
|
||||
{
|
||||
Artisan::call('migrate --force');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function finishUpdate($installed, $version)
|
||||
@ -118,22 +135,4 @@ class Updater
|
||||
];
|
||||
}
|
||||
|
||||
public static function checkForUpdate()
|
||||
{
|
||||
$data = null;
|
||||
if(env('APP_ENV') === 'development')
|
||||
{
|
||||
$url = 'https://craterapp.com/downloads/check/latest/'. Setting::getSetting('version') . '?type=update&is_dev=1';
|
||||
} else {
|
||||
$url = 'https://craterapp.com/downloads/check/latest/'. Setting::getSetting('version') . '?type=update';
|
||||
}
|
||||
|
||||
$response = static::getRemote($url, ['timeout' => 100, 'track_redirects' => true]);
|
||||
|
||||
if ($response && ($response->getStatusCode() == 200)) {
|
||||
$data = $response->getBody()->getContents();
|
||||
}
|
||||
|
||||
return json_decode($data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,20 +54,11 @@
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"scripts": {
|
||||
"initial-setup": [
|
||||
"test -f .env || (cp .env.example .env; php artisan key:generate 2>/dev/null; exit 0)"
|
||||
],
|
||||
"pre-install-cmd": [
|
||||
"@initial-setup"
|
||||
],
|
||||
"pre-update-cmd": [
|
||||
"@initial-setup"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@initial-setup"
|
||||
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@initial-setup"
|
||||
"php artisan key:generate --ansi"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
|
||||
@ -13,6 +13,7 @@ class CreateUnitsTable extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (!Schema::hasTable('units')) {
|
||||
Schema::create('units', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name');
|
||||
@ -21,6 +22,7 @@ class CreateUnitsTable extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
|
||||
@ -23,8 +23,6 @@ class CreateExpensesTable extends Migration
|
||||
$table->foreign('expense_category_id')->references('id')->on('expense_categories')->onDelete('cascade');
|
||||
$table->integer('company_id')->unsigned()->nullable();
|
||||
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
||||
$table->integer('user_id')->unsigned()->nullable();
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ class CreatePaymentMethodsTable extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (!Schema::hasTable('payment_methods')) {
|
||||
Schema::create('payment_methods', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name');
|
||||
@ -21,6 +22,7 @@ class CreatePaymentMethodsTable extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddUserIdToExpensesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('expenses', function (Blueprint $table) {
|
||||
$table->integer('user_id')->unsigned()->nullable();
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('expenses', function (Blueprint $table) {
|
||||
$table->dropColumn('paid');
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
|
||||
web:
|
||||
image: nginx
|
||||
depends_on:
|
||||
- php
|
||||
ports:
|
||||
- 8080:80
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- app:/app
|
||||
restart: always
|
||||
|
||||
php:
|
||||
build: .
|
||||
depends_on:
|
||||
- db
|
||||
expose:
|
||||
- 9000
|
||||
volumes:
|
||||
- app:/app
|
||||
restart: always
|
||||
|
||||
db:
|
||||
image: mariadb
|
||||
restart: always
|
||||
volumes:
|
||||
- db:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_USER: crater
|
||||
MYSQL_PASSWORD: crater
|
||||
MYSQL_DATABASE: crater
|
||||
MYSQL_ROOT_PASSWORD: crater
|
||||
|
||||
volumes:
|
||||
app:
|
||||
db:
|
||||
|
||||
50
docker-compose.yml
Normal file
50
docker-compose.yml
Normal file
@ -0,0 +1,50 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
args:
|
||||
user: crater-user
|
||||
uid: 1000
|
||||
context: ./
|
||||
dockerfile: Dockerfile
|
||||
image: crater-php
|
||||
restart: unless-stopped
|
||||
working_dir: /var/www/
|
||||
volumes:
|
||||
- ./:/var/www
|
||||
networks:
|
||||
- crater
|
||||
|
||||
db:
|
||||
image: mariadb
|
||||
restart: always
|
||||
volumes:
|
||||
- db:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_USER: crater
|
||||
MYSQL_PASSWORD: crater
|
||||
MYSQL_DATABASE: crater
|
||||
MYSQL_ROOT_PASSWORD: crater
|
||||
ports:
|
||||
- '33006:3306'
|
||||
networks:
|
||||
- crater
|
||||
|
||||
nginx:
|
||||
image: nginx:1.17-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- ./:/var/www
|
||||
- ./docker-compose/nginx:/etc/nginx/conf.d/
|
||||
networks:
|
||||
- crater
|
||||
|
||||
volumes:
|
||||
db:
|
||||
|
||||
networks:
|
||||
crater:
|
||||
driver: bridge
|
||||
20
docker-compose/nginx/nginx.conf
Normal file
20
docker-compose/nginx/nginx.conf
Normal file
@ -0,0 +1,20 @@
|
||||
server {
|
||||
listen 80;
|
||||
index index.php index.html;
|
||||
error_log /var/log/nginx/error.log;
|
||||
access_log /var/log/nginx/access.log;
|
||||
root /var/www/public;
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass app:9000;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
gzip_static on;
|
||||
}
|
||||
}
|
||||
7
docker-compose/setup.sh
Executable file
7
docker-compose/setup.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
docker-compose exec app composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
docker-compose exec app php artisan storage:link || true
|
||||
docker-compose exec app php artisan key:generate
|
||||
docker-compose exec app php artisan passport:keys || true
|
||||
53
nginx.conf
53
nginx.conf
@ -1,53 +0,0 @@
|
||||
worker_processes 8;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 4096;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
|
||||
root /app/public;
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
|
||||
access_log off;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Robots-Tag none;
|
||||
add_header Content-Security-Policy "frame-ancestors 'self'";
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,6 +63,7 @@ Crater is a product of [Bytefury](https://bytefury.com)
|
||||
**Special thanks to:**
|
||||
* [Birkhoff Lee](https://github.com/BirkhoffLee)
|
||||
* [Hassan A. Ba Abdullah](https://github.com/hsnapps)
|
||||
* [Akaunting](https://github.com/akaunting/akaunting)
|
||||
|
||||
## Translate
|
||||
Help us translate or suggest changes to existing languages if you find any mistakes by creating a new PR.
|
||||
|
||||
@ -39,7 +39,12 @@ export default {
|
||||
|
||||
formatMoney(amount, currency = 0) {
|
||||
if (!currency) {
|
||||
currency = {precision: 2, thousand_separator: ',', decimal_separator: '.', symbol: '$'}
|
||||
currency = {
|
||||
precision: 2,
|
||||
thousand_separator: ',',
|
||||
decimal_separator: '.',
|
||||
symbol: '$',
|
||||
}
|
||||
}
|
||||
|
||||
amount = amount / 100
|
||||
@ -52,12 +57,26 @@ export default {
|
||||
|
||||
const negativeSign = amount < 0 ? '-' : ''
|
||||
|
||||
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(precision)).toString()
|
||||
let j = (i.length > 3) ? i.length % 3 : 0
|
||||
let i = parseInt(
|
||||
(amount = Math.abs(Number(amount) || 0).toFixed(precision))
|
||||
).toString()
|
||||
let j = i.length > 3 ? i.length % 3 : 0
|
||||
|
||||
let moneySymbol = `<span style="font-family: sans-serif">${symbol}</span>`
|
||||
|
||||
return moneySymbol + ' ' + negativeSign + (j ? i.substr(0, j) + thousand_separator : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousand_separator) + (precision ? decimal_separator + Math.abs(amount - i).toFixed(precision).slice(2) : '')
|
||||
return (
|
||||
moneySymbol +
|
||||
' ' +
|
||||
negativeSign +
|
||||
(j ? i.substr(0, j) + thousand_separator : '') +
|
||||
i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousand_separator) +
|
||||
(precision
|
||||
? decimal_separator +
|
||||
Math.abs(amount - i)
|
||||
.toFixed(precision)
|
||||
.slice(2)
|
||||
: '')
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
@ -65,7 +84,12 @@ export default {
|
||||
|
||||
formatGraphMoney(amount, currency = 0) {
|
||||
if (!currency) {
|
||||
currency = {precision: 2, thousand_separator: ',', decimal_separator: '.', symbol: '$'}
|
||||
currency = {
|
||||
precision: 2,
|
||||
thousand_separator: ',',
|
||||
decimal_separator: '.',
|
||||
symbol: '$',
|
||||
}
|
||||
}
|
||||
|
||||
amount = amount / 100
|
||||
@ -78,25 +102,76 @@ export default {
|
||||
|
||||
const negativeSign = amount < 0 ? '-' : ''
|
||||
|
||||
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(precision)).toString()
|
||||
let j = (i.length > 3) ? i.length % 3 : 0
|
||||
let i = parseInt(
|
||||
(amount = Math.abs(Number(amount) || 0).toFixed(precision))
|
||||
).toString()
|
||||
let j = i.length > 3 ? i.length % 3 : 0
|
||||
|
||||
let moneySymbol = `${symbol}`
|
||||
|
||||
return moneySymbol + ' ' + negativeSign + (j ? i.substr(0, j) + thousand_separator : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousand_separator) + (precision ? decimal_separator + Math.abs(amount - i).toFixed(precision).slice(2) : '')
|
||||
return (
|
||||
moneySymbol +
|
||||
' ' +
|
||||
negativeSign +
|
||||
(j ? i.substr(0, j) + thousand_separator : '') +
|
||||
i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousand_separator) +
|
||||
(precision
|
||||
? decimal_separator +
|
||||
Math.abs(amount - i)
|
||||
.toFixed(precision)
|
||||
.slice(2)
|
||||
: '')
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
|
||||
checkValidUrl(url) {
|
||||
let pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
|
||||
let pattern = new RegExp(
|
||||
'^(https?:\\/\\/)?' + // protocol
|
||||
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
|
||||
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
|
||||
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
|
||||
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
|
||||
'(\\#[-a-z\\d_]*)?$', 'i') // fragment locator
|
||||
'(\\#[-a-z\\d_]*)?$',
|
||||
'i'
|
||||
) // fragment locator
|
||||
|
||||
return !!pattern.test(url)
|
||||
},
|
||||
|
||||
fallbackCopyTextToClipboard(text) {
|
||||
var textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = '0'
|
||||
textArea.style.left = '0'
|
||||
textArea.style.position = 'fixed'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
try {
|
||||
var successful = document.execCommand('copy')
|
||||
var msg = successful ? 'successful' : 'unsuccessful'
|
||||
console.log('Fallback: Copying text command was ' + msg)
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err)
|
||||
}
|
||||
document.body.removeChild(textArea)
|
||||
},
|
||||
copyTextToClipboard(text) {
|
||||
if (!navigator.clipboard) {
|
||||
this.fallbackCopyTextToClipboard(text)
|
||||
return
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(
|
||||
function () {
|
||||
return true
|
||||
},
|
||||
function (err) {
|
||||
return false
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@ -67,14 +67,14 @@
|
||||
"four_zero_four": "404",
|
||||
"you_got_lost": "عفواً! يبدو أنك قد تهت!",
|
||||
"go_home": "عودة إلى الرئيسية",
|
||||
|
||||
"setting_updated": "تم تحديث الإعدادات بنجاح",
|
||||
"select_state": "اختر الولاية/المنطقة",
|
||||
"select_country": "اختر الدولة",
|
||||
"select_city": "اختر المدينة",
|
||||
"street_1": "عنوان الشارع 1",
|
||||
"street_2": "عنوان الشارع 2",
|
||||
"action_failed": "فشلت العملية"
|
||||
"action_failed": "فشلت العملية",
|
||||
"retry": "أعد المحاولة"
|
||||
},
|
||||
"dashboard": {
|
||||
"select_year": "اختر السنة",
|
||||
@ -785,7 +785,14 @@
|
||||
"progress_text": "سوف يستغرق التحديث بضع دقائق. يرجى عدم تحديث الشاشة أو إغلاق النافذة قبل انتهاء التحديث",
|
||||
"update_success": "تم تحديث النظام! يرجى الانتظار حتى يتم إعادة تحميل نافذة المتصفح تلقائيًا.",
|
||||
"latest_message": "لا يوجد تحديثات متوفرة! لديك حالياً أحدث نسخة.",
|
||||
"current_version": "النسخة الحالية"
|
||||
"current_version": "النسخة الحالية",
|
||||
"download_zip_file": "تنزيل ملف ZIP",
|
||||
"unzipping_package": "حزمة فك الضغط",
|
||||
"copying_files": "نسخ الملفات",
|
||||
"running_migrations": "إدارة عمليات الترحيل",
|
||||
"finishing_update": "تحديث التشطيب",
|
||||
"update_failed": "فشل التحديث",
|
||||
"update_failed_text": "آسف! فشل التحديث الخاص بك في: {step} خطوة"
|
||||
}
|
||||
},
|
||||
"wizard": {
|
||||
|
||||
@ -76,7 +76,8 @@
|
||||
"select_city": "Stadt wählen",
|
||||
"street_1": "Straße",
|
||||
"street_2": "Zusatz Strasse",
|
||||
"action_failed": "Aktion fehlgeschlagen"
|
||||
"action_failed": "Aktion fehlgeschlagen",
|
||||
"retry": "Wiederholen"
|
||||
},
|
||||
"dashboard": {
|
||||
"select_year": "Jahr wählen",
|
||||
@ -781,7 +782,14 @@
|
||||
"progress_text": "Es dauert nur ein paar Minuten. Bitte aktualisieren Sie den Bildschirm nicht und schließen Sie das Fenster nicht, bevor das Update abgeschlossen ist.",
|
||||
"update_success": "App wurde aktualisiert! Bitte warten Sie, während Ihr Browserfenster automatisch neu geladen wird.",
|
||||
"latest_message": "Kein Update verfügbar! Du bist auf der neuesten Version.",
|
||||
"current_version": "Aktuelle Version"
|
||||
"current_version": "Aktuelle Version",
|
||||
"download_zip_file": "Laden Sie die ZIP-Datei herunter",
|
||||
"unzipping_package": "Paket entpacken",
|
||||
"copying_files": "Dateien kopieren",
|
||||
"running_migrations": "Ausführen von Migrationen",
|
||||
"finishing_update": "Update beenden",
|
||||
"update_failed": "Update fehlgeschlagen",
|
||||
"update_failed_text": "Es tut uns leid! Ihr Update ist am folgenden Schritt fehlgeschlagen: {step}"
|
||||
}
|
||||
},
|
||||
"wizard": {
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
},
|
||||
"general": {
|
||||
"view_pdf": "View PDF",
|
||||
"copy_pdf_url": "Copy PDF Url",
|
||||
"download_pdf": "Download PDF",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
@ -70,14 +71,14 @@
|
||||
"go_home": "Go Home",
|
||||
"test_mail_conf": "Test Mail Configuration",
|
||||
"send_mail_successfully": "Mail sent successfully",
|
||||
|
||||
"setting_updated": "Setting updated successfully",
|
||||
"select_state": "Select state",
|
||||
"select_country": "Select Country",
|
||||
"select_city": "Select City",
|
||||
"street_1": "Street 1",
|
||||
"street_2": "Street 2",
|
||||
"action_failed": "Action Failed"
|
||||
"action_failed": "Action Failed",
|
||||
"retry": "Retry"
|
||||
},
|
||||
"dashboard": {
|
||||
"select_year": "Select year",
|
||||
@ -230,6 +231,7 @@
|
||||
"convert_to_invoice": "Convert to Invoice",
|
||||
"mark_as_sent": "Mark as Sent",
|
||||
"send_estimate": "Send Estimate",
|
||||
"resend_estimate": "Resend Estimate",
|
||||
"record_payment": "Record Payment",
|
||||
"add_estimate": "Add Estimate",
|
||||
"save_estimate": "Save Estimate",
|
||||
@ -316,6 +318,7 @@
|
||||
"notes": "Notes",
|
||||
"view": "View",
|
||||
"send_invoice": "Send Invoice",
|
||||
"resend_invoice": "Resend Invoice",
|
||||
"invoice_template": "Invoice Template",
|
||||
"template": "Template",
|
||||
"mark_as_sent": "Mark as sent",
|
||||
@ -792,7 +795,14 @@
|
||||
"progress_text": "It will just take a few minutes. Please do not refresh the screen or close the window before the update finishes",
|
||||
"update_success": "App has been updated! Please wait while your browser window gets reloaded automatically.",
|
||||
"latest_message": "No update available! You are on the latest version.",
|
||||
"current_version": "Current Version"
|
||||
"current_version": "Current Version",
|
||||
"download_zip_file": "Download ZIP file",
|
||||
"unzipping_package": "Unzipping Package",
|
||||
"copying_files": "Copying Files",
|
||||
"running_migrations": "Running Migrations",
|
||||
"finishing_update": "Finishing Update",
|
||||
"update_failed": "Update Failed",
|
||||
"update_failed_text": "Sorry! Your update failed on : {step} step"
|
||||
}
|
||||
},
|
||||
"wizard": {
|
||||
|
||||
@ -75,7 +75,8 @@
|
||||
"select_city": "Seleccionar ciudad",
|
||||
"street_1": "Calle 1",
|
||||
"street_2": "Calle 2",
|
||||
"action_failed": "Accion Fallida"
|
||||
"action_failed": "Accion Fallida",
|
||||
"retry": "Procesar de nuevo"
|
||||
},
|
||||
"dashboard": {
|
||||
"select_year": "Seleccionar año",
|
||||
@ -786,7 +787,14 @@
|
||||
"progress_text": "Solo tomará unos minutos. No actualice la pantalla ni cierre la ventana antes de que finalice la actualización.",
|
||||
"update_success": "¡La aplicación ha sido actualizada! Espere mientras la ventana de su navegador se vuelve a cargar automáticamente.",
|
||||
"latest_message": "¡Actualización no disponible! Estás en la última versión.",
|
||||
"current_version": "Versión actual"
|
||||
"current_version": "Versión actual",
|
||||
"download_zip_file": "Descargar archivo ZIP",
|
||||
"unzipping_package": "Descomprimir paquete",
|
||||
"copying_files": "Copiando documentos",
|
||||
"running_migrations": "Ejecutar migraciones",
|
||||
"finishing_update": "Actualización final",
|
||||
"update_failed": "Actualización fallida",
|
||||
"update_failed_text": "¡Lo siento! Su actualización falló el: {step} paso"
|
||||
}
|
||||
},
|
||||
"wizard": {
|
||||
|
||||
@ -76,7 +76,8 @@
|
||||
"ascending": "Ascendant",
|
||||
"descending": "Descendant",
|
||||
"subject": "matière",
|
||||
"message": "Message"
|
||||
"message": "Message",
|
||||
"retry": "Réessayez"
|
||||
},
|
||||
"dashboard": {
|
||||
"select_year": "Sélectionnez l'année",
|
||||
@ -799,7 +800,14 @@
|
||||
"progress_text": "Cela ne prendra que quelques minutes. S'il vous plaît ne pas actualiser l'écran ou fermer la fenêtre avant la fin de la mise à jour",
|
||||
"update_success": "App a été mis à jour! Veuillez patienter pendant le rechargement automatique de la fenêtre de votre navigateur.",
|
||||
"latest_message": "Pas de mise a jour disponible! Vous êtes sur la dernière version.",
|
||||
"current_version": "Version actuelle"
|
||||
"current_version": "Version actuelle",
|
||||
"download_zip_file": "Télécharger le fichier ZIP",
|
||||
"unzipping_package": "Dézipper le package",
|
||||
"copying_files": "Copie de fichiers",
|
||||
"running_migrations": "Exécution de migrations",
|
||||
"finishing_update": "Mise à jour de finition",
|
||||
"update_failed": "Mise à jour a échoué",
|
||||
"update_failed_text": "Désolé! Votre mise à jour a échoué à: {step} étape"
|
||||
}
|
||||
},
|
||||
"wizard": {
|
||||
|
||||
@ -71,14 +71,14 @@
|
||||
"go_home": "Vai alla Home",
|
||||
"test_mail_conf": "Configurazione della mail di test",
|
||||
"send_mail_successfully": "Mail inviata con successo",
|
||||
|
||||
"setting_updated": "Configurazioni aggiornate con successo",
|
||||
"select_state": "Seleziona lo Stato",
|
||||
"select_country": "Seleziona Paese",
|
||||
"select_city": "Seleziona Città",
|
||||
"street_1": "Indirizzo 1",
|
||||
"street_2": "Indirizzo 2",
|
||||
"action_failed": "Errore"
|
||||
"action_failed": "Errore",
|
||||
"retry": "Retry"
|
||||
},
|
||||
"dashboard": {
|
||||
"select_year": "Seleziona anno",
|
||||
@ -792,7 +792,14 @@
|
||||
"progress_text": "Sarà necessario qualche minuto. Per favore non aggiornare la pagina e non chiudere la finestra prima che l'aggiornamento sia completato",
|
||||
"update_success": "L'App è aggiornata! Attendi che la pagina venga ricaricata automaticamente.",
|
||||
"latest_message": "Nessun aggiornamneto disponibile! Sei già alla versione più recente.",
|
||||
"current_version": "Versione corrente"
|
||||
"current_version": "Versione corrente",
|
||||
"download_zip_file": "Scarica il file ZIP",
|
||||
"unzipping_package": "Pacchetto di decompressione",
|
||||
"copying_files": "Copia dei file",
|
||||
"running_migrations": "Esecuzione delle migrazioni",
|
||||
"finishing_update": "Aggiornamento di finitura",
|
||||
"update_failed": "Aggiornamento non riuscito",
|
||||
"update_failed_text": "Scusate! L'aggiornamento non è riuscito il: passaggio {step}"
|
||||
}
|
||||
},
|
||||
"wizard": {
|
||||
|
||||
@ -70,14 +70,14 @@
|
||||
"go_home": "Ir para Home",
|
||||
"test_mail_conf": "Testar configuração de email",
|
||||
"send_mail_successfully": "Correio enviado com sucesso",
|
||||
|
||||
"setting_updated": "Configuração atualizada com sucesso",
|
||||
"select_state": "Selecione Estado",
|
||||
"select_country": "Selecionar pais",
|
||||
"select_city": "Selecionar cidade",
|
||||
"street_1": "Rua 1",
|
||||
"street_2": "Rua # 2",
|
||||
"action_failed": "Ação: Falhou"
|
||||
"action_failed": "Ação: Falhou",
|
||||
"retry": "Atualização falhou"
|
||||
},
|
||||
"dashboard": {
|
||||
"select_year": "Selecione Ano",
|
||||
@ -787,7 +787,14 @@
|
||||
"progress_text": "Levará apenas alguns minutos. Não atualize a tela ou feche a janela antes que a atualização seja concluída",
|
||||
"update_success": "O aplicativo foi atualizado! Aguarde enquanto a janela do navegador é recarregada automaticamente.",
|
||||
"latest_message": "Nenhuma atualização disponível! Você está na versão mais recente.",
|
||||
"current_version": "Versão Atual"
|
||||
"current_version": "Versão Atual",
|
||||
"download_zip_file": "Baixar arquivo ZIP",
|
||||
"unzipping_package": "Descompactando o pacote",
|
||||
"copying_files": "Copiando arquivos",
|
||||
"running_migrations": "Executando migrações",
|
||||
"finishing_update": "Atualização de acabamento",
|
||||
"update_failed": "Atualização falhou",
|
||||
"update_failed_text": "Desculpa! Sua atualização falhou em: {step} step"
|
||||
}
|
||||
},
|
||||
"wizard": {
|
||||
|
||||
@ -4,16 +4,12 @@
|
||||
<h3 class="page-title">{{ $t('estimates.title') }}</h3>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="dashboard">
|
||||
<router-link slot="item-title" to="dashboard">
|
||||
{{ $t('general.home') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="#">
|
||||
<router-link slot="item-title" to="#">
|
||||
{{ $tc('estimates.estimate', 2) }}
|
||||
</router-link>
|
||||
</li>
|
||||
@ -33,11 +29,9 @@
|
||||
</base-button>
|
||||
</div>
|
||||
<router-link slot="item-title" class="col-xs-2" to="estimates/create">
|
||||
<base-button
|
||||
size="large"
|
||||
icon="plus"
|
||||
color="theme" >
|
||||
{{ $t('estimates.new_estimate') }}</base-button>
|
||||
<base-button size="large" icon="plus" color="theme">
|
||||
{{ $t('estimates.new_estimate') }}</base-button
|
||||
>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@ -85,21 +79,28 @@
|
||||
</div>
|
||||
<div class="filter-estimate">
|
||||
<label>{{ $t('estimates.estimate_number') }}</label>
|
||||
<base-input
|
||||
v-model="filters.estimate_number"
|
||||
icon="hashtag"/>
|
||||
<base-input v-model="filters.estimate_number" icon="hashtag" />
|
||||
</div>
|
||||
</div>
|
||||
<label class="clear-filter" @click="clearFilter">{{ $t('general.clear_all') }}</label>
|
||||
<label class="clear-filter" @click="clearFilter">{{
|
||||
$t('general.clear_all')
|
||||
}}</label>
|
||||
</div>
|
||||
</transition>
|
||||
<div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center">
|
||||
<div
|
||||
v-cloak
|
||||
v-show="showEmptyScreen"
|
||||
class="col-xs-1 no-data-info"
|
||||
align="center"
|
||||
>
|
||||
<moon-walker-icon class="mt-5 mb-4" />
|
||||
<div class="row" align="center">
|
||||
<label class="col title">{{ $t('estimates.no_estimates') }}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="description col mt-1" align="center">{{ $t('estimates.list_of_estimates') }}</label>
|
||||
<label class="description col mt-1" align="center">{{
|
||||
$t('estimates.list_of_estimates')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<base-button
|
||||
@ -116,28 +117,57 @@
|
||||
|
||||
<div v-show="!showEmptyScreen" class="table-container">
|
||||
<div class="table-actions mt-5">
|
||||
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ estimates.length }}</b> {{ $t('general.of') }} <b>{{ totalEstimates }}</b></p>
|
||||
<p class="table-stats">
|
||||
{{ $t('general.showing') }}: <b>{{ estimates.length }}</b>
|
||||
{{ $t('general.of') }} <b>{{ totalEstimates }}</b>
|
||||
</p>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="tabs">
|
||||
<li class="tab" @click="getStatus('DRAFT')">
|
||||
<a :class="['tab-link', {'a-active': filters.status === 'DRAFT'}]" href="#">{{ $t('general.draft') }}</a>
|
||||
<a
|
||||
:class="['tab-link', { 'a-active': filters.status === 'DRAFT' }]"
|
||||
href="#"
|
||||
>{{ $t('general.draft') }}</a
|
||||
>
|
||||
</li>
|
||||
<li class="tab" @click="getStatus('SENT')">
|
||||
<a :class="['tab-link', {'a-active': filters.status === 'SENT'}]" href="#" >{{ $t('general.sent') }}</a>
|
||||
<a
|
||||
:class="['tab-link', { 'a-active': filters.status === 'SENT' }]"
|
||||
href="#"
|
||||
>{{ $t('general.sent') }}</a
|
||||
>
|
||||
</li>
|
||||
<li class="tab" @click="getStatus('')">
|
||||
<a :class="['tab-link', {'a-active': filters.status === '' || filters.status !== 'DRAFT' && filters.status !== 'SENT'}]" href="#">{{ $t('general.all') }}</a>
|
||||
<a
|
||||
:class="[
|
||||
'tab-link',
|
||||
{
|
||||
'a-active':
|
||||
filters.status === '' ||
|
||||
(filters.status !== 'DRAFT' && filters.status !== 'SENT'),
|
||||
},
|
||||
]"
|
||||
href="#"
|
||||
>{{ $t('general.all') }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<transition name="fade">
|
||||
<v-dropdown v-if="selectedEstimates.length" :show-arrow="false">
|
||||
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
|
||||
<span
|
||||
slot="activator"
|
||||
href="#"
|
||||
class="table-actions-button dropdown-toggle"
|
||||
>
|
||||
{{ $t('general.actions') }}
|
||||
</span>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeMultipleEstimates">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trash']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
@ -153,8 +183,12 @@
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
@change="selectAllEstimates"
|
||||
/>
|
||||
<label
|
||||
v-show="!isRequestOngoing"
|
||||
for="select-all"
|
||||
class="custom-control-label selectall"
|
||||
>
|
||||
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
|
||||
<span class="select-all-label">{{ $t('general.select_all') }} </span>
|
||||
</label>
|
||||
</div>
|
||||
@ -178,7 +212,7 @@
|
||||
:value="row.id"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
>
|
||||
/>
|
||||
<label :for="row.id" class="custom-control-label" />
|
||||
</div>
|
||||
</template>
|
||||
@ -186,30 +220,30 @@
|
||||
<table-column
|
||||
:label="$t('estimates.date')"
|
||||
sort-as="estimate_date"
|
||||
show="formattedEstimateDate" />
|
||||
show="formattedEstimateDate"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('estimates.customer')"
|
||||
sort-as="name"
|
||||
show="name" />
|
||||
show="name"
|
||||
/>
|
||||
<!-- <table-column
|
||||
:label="$t('estimates.expiry_date')"
|
||||
sort-as="expiry_date"
|
||||
show="formattedExpiryDate" /> -->
|
||||
<table-column
|
||||
:label="$t('estimates.status')"
|
||||
show="status" >
|
||||
<table-column :label="$t('estimates.status')" show="status">
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('estimates.status') }}</span>
|
||||
<span :class="'est-status-'+row.status.toLowerCase()">{{ row.status }}</span>
|
||||
<span :class="'est-status-' + row.status.toLowerCase()">{{
|
||||
row.status
|
||||
}}</span>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:label="$tc('estimates.estimate', 1)"
|
||||
show="estimate_number"/>
|
||||
<table-column
|
||||
:label="$t('invoices.total')"
|
||||
sort-as="total"
|
||||
>
|
||||
show="estimate_number"
|
||||
/>
|
||||
<table-column :label="$t('invoices.total')" sort-as="total">
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('estimates.total') }}</span>
|
||||
<div v-html="$utils.formatMoney(row.total, row.user.currency)" />
|
||||
@ -227,50 +261,114 @@
|
||||
<dot-icon />
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<router-link :to="{path: `estimates/${row.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
|
||||
<router-link
|
||||
:to="{ path: `estimates/${row.id}/edit` }"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'pencil-alt']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeEstimate(row.id)">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trash']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<router-link :to="{path: `estimates/${row.id}/view`}" class="dropdown-item">
|
||||
<router-link
|
||||
:to="{ path: `estimates/${row.id}/view` }"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon icon="eye" class="dropdown-item-icon" />
|
||||
{{ $t('general.view') }}
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<a class="dropdown-item" href="#/" @click="convertInToinvoice(row.id)">
|
||||
<font-awesome-icon icon="file-alt" class="dropdown-item-icon" />
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#/"
|
||||
@click="convertInToinvoice(row.id)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="file-alt"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('estimates.convert_to_invoice') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status !== 'SENT'">
|
||||
<a class="dropdown-item" href="#/" @click.self="onMarkAsSent(row.id)">
|
||||
<font-awesome-icon icon="check-circle" class="dropdown-item-icon" />
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#/"
|
||||
@click.self="onMarkAsSent(row.id)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="check-circle"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('estimates.mark_as_sent') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status !== 'SENT'">
|
||||
<a class="dropdown-item" href="#/" @click.self="sendEstimate(row.id)">
|
||||
<font-awesome-icon icon="paper-plane" class="dropdown-item-icon" />
|
||||
<v-dropdown-item v-if="row.status === 'DRAFT'">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#/"
|
||||
@click.self="sendEstimate(row.id)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="paper-plane"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('estimates.send_estimate') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<!-- resend estimte -->
|
||||
<v-dropdown-item
|
||||
v-if="row.status == 'SENT' || row.status == 'VIEWED'"
|
||||
>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#/"
|
||||
@click.self="sendEstimate(row.id)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="paper-plane"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('estimates.resend_estimate') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<!-- -->
|
||||
<v-dropdown-item v-if="row.status !== 'ACCEPTED'">
|
||||
<a class="dropdown-item" href="#/" @click.self="onMarkAsAccepted(row.id)">
|
||||
<font-awesome-icon icon="check-circle" class="dropdown-item-icon" />
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#/"
|
||||
@click.self="onMarkAsAccepted(row.id)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="check-circle"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('estimates.mark_as_accepted') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status !== 'REJECTED'">
|
||||
<a class="dropdown-item" href="#/" @click.self="onMarkAsRejected(row.id)">
|
||||
<font-awesome-icon icon="times-circle" class="dropdown-item-icon" />
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#/"
|
||||
@click.self="onMarkAsRejected(row.id)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="times-circle"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('estimates.mark_as_rejected') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
|
||||
@ -26,19 +26,42 @@
|
||||
{{ $t('estimates.send_estimate') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<v-dropdown :close-on-select="false" align="left" class="filter-container">
|
||||
<v-dropdown
|
||||
:close-on-select="true"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
>
|
||||
<a slot="activator" href="#">
|
||||
<base-button color="theme">
|
||||
<font-awesome-icon icon="ellipsis-h" />
|
||||
</base-button>
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<router-link :to="{path: `/admin/estimates/${$route.params.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon"/>
|
||||
<div class="dropdown-item" @click="copyPdfUrl()">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'link']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ path: `/admin/estimates/${$route.params.id}/edit` }"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'pencil-alt']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<div class="dropdown-item" @click="removeEstimate($route.params.id)">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
<div
|
||||
class="dropdown-item"
|
||||
@click="removeEstimate($route.params.id)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trash']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
@ -57,14 +80,18 @@
|
||||
align-icon="right"
|
||||
@input="onSearched()"
|
||||
/>
|
||||
<div
|
||||
class="btn-group ml-3"
|
||||
role="group"
|
||||
aria-label="First group"
|
||||
<div class="btn-group ml-3" role="group" aria-label="First group">
|
||||
<v-dropdown
|
||||
:close-on-select="false"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
>
|
||||
<v-dropdown :close-on-select="false" align="left" class="filter-container">
|
||||
<a slot="activator" href="#">
|
||||
<base-button class="inv-button inv-filter-fields-btn" color="default" size="medium">
|
||||
<base-button
|
||||
class="inv-button inv-filter-fields-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
>
|
||||
<font-awesome-icon icon="filter" />
|
||||
</base-button>
|
||||
</a>
|
||||
@ -80,8 +107,10 @@
|
||||
class="inv-radio"
|
||||
value="estimate_date"
|
||||
@change="onSearched"
|
||||
>
|
||||
<label class="inv-label" for="filter_estimate_date">{{ $t('reports.estimates.estimate_date') }}</label>
|
||||
/>
|
||||
<label class="inv-label" for="filter_estimate_date">{{
|
||||
$t('reports.estimates.estimate_date')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
@ -92,8 +121,10 @@
|
||||
class="inv-radio"
|
||||
value="expiry_date"
|
||||
@change="onSearched"
|
||||
>
|
||||
<label class="inv-label" for="filter_due_date">{{ $t('estimates.due_date') }}</label>
|
||||
/>
|
||||
<label class="inv-label" for="filter_due_date">{{
|
||||
$t('estimates.due_date')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
@ -104,11 +135,19 @@
|
||||
class="inv-radio"
|
||||
value="estimate_number"
|
||||
@change="onSearched"
|
||||
>
|
||||
<label class="inv-label" for="filter_estimate_number">{{ $t('estimates.estimate_number') }}</label>
|
||||
/>
|
||||
<label class="inv-label" for="filter_estimate_number">{{
|
||||
$t('estimates.estimate_number')
|
||||
}}</label>
|
||||
</div>
|
||||
</v-dropdown>
|
||||
<base-button v-tooltip.top-center="{ content: getOrderName }" class="inv-button inv-filter-sorting-btn" color="default" size="medium" @click="sortData">
|
||||
<base-button
|
||||
v-tooltip.top-center="{ content: getOrderName }"
|
||||
class="inv-button inv-filter-sorting-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
@click="sortData"
|
||||
>
|
||||
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
|
||||
<font-awesome-icon v-else icon="sort-amount-down" />
|
||||
</base-button>
|
||||
@ -124,10 +163,20 @@
|
||||
<div class="left">
|
||||
<div class="inv-name">{{ estimate.user.name }}</div>
|
||||
<div class="inv-number">{{ estimate.estimate_number }}</div>
|
||||
<div :class="'est-status-'+estimate.status.toLowerCase()" class="inv-status">{{ estimate.status }}</div>
|
||||
<div
|
||||
:class="'est-status-' + estimate.status.toLowerCase()"
|
||||
class="inv-status"
|
||||
>
|
||||
{{ estimate.status }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="inv-amount" v-html="$utils.formatMoney(estimate.total, estimate.user.currency)" />
|
||||
<div
|
||||
class="inv-amount"
|
||||
v-html="
|
||||
$utils.formatMoney(estimate.total, estimate.user.currency)
|
||||
"
|
||||
/>
|
||||
<div class="inv-date">{{ estimate.formattedEstimateDate }}</div>
|
||||
</div>
|
||||
</router-link>
|
||||
@ -289,6 +338,13 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
copyPdfUrl () {
|
||||
let pdfUrl = `${window.location.origin}/estimates/pdf/${this.estimate.unique_hash}`
|
||||
|
||||
let response = this.$utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
window.toastr['success'](this.$tc('Copied PDF url to clipboard!'))
|
||||
},
|
||||
async removeEstimate (id) {
|
||||
window.swal({
|
||||
title: 'Deleted',
|
||||
|
||||
@ -4,16 +4,12 @@
|
||||
<h3 class="page-title">{{ $t('invoices.title') }}</h3>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="dashboard">
|
||||
<router-link slot="item-title" to="dashboard">
|
||||
{{ $t('general.home') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="#">
|
||||
<router-link slot="item-title" to="#">
|
||||
{{ $tc('invoices.invoice', 2) }}
|
||||
</router-link>
|
||||
</li>
|
||||
@ -32,7 +28,11 @@
|
||||
{{ $t('general.filter') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<router-link slot="item-title" class="col-xs-2" to="/admin/invoices/create">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
class="col-xs-2"
|
||||
to="/admin/invoices/create"
|
||||
>
|
||||
<base-button size="large" icon="plus" color="theme">
|
||||
{{ $t('invoices.new_invoice') }}
|
||||
</base-button>
|
||||
@ -88,22 +88,29 @@
|
||||
</div>
|
||||
<div class="filter-invoice">
|
||||
<label>{{ $t('invoices.invoice_number') }}</label>
|
||||
<base-input
|
||||
v-model="filters.invoice_number"
|
||||
icon="hashtag"/>
|
||||
<base-input v-model="filters.invoice_number" icon="hashtag" />
|
||||
</div>
|
||||
</div>
|
||||
<label class="clear-filter" @click="clearFilter">{{ $t('general.clear_all') }}</label>
|
||||
<label class="clear-filter" @click="clearFilter">{{
|
||||
$t('general.clear_all')
|
||||
}}</label>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center">
|
||||
<div
|
||||
v-cloak
|
||||
v-show="showEmptyScreen"
|
||||
class="col-xs-1 no-data-info"
|
||||
align="center"
|
||||
>
|
||||
<moon-walker-icon class="mt-5 mb-4" />
|
||||
<div class="row" align="center">
|
||||
<label class="col title">{{ $t('invoices.no_invoices') }}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="description col mt-1" align="center">{{ $t('invoices.list_of_invoices') }}</label>
|
||||
<label class="description col mt-1" align="center">{{
|
||||
$t('invoices.list_of_invoices')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<base-button
|
||||
@ -120,28 +127,65 @@
|
||||
|
||||
<div v-show="!showEmptyScreen" class="table-container">
|
||||
<div class="table-actions mt-5">
|
||||
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ invoices.length }}</b> {{ $t('general.of') }} <b>{{ totalInvoices }}</b></p>
|
||||
<p class="table-stats">
|
||||
{{ $t('general.showing') }}: <b>{{ invoices.length }}</b>
|
||||
{{ $t('general.of') }} <b>{{ totalInvoices }}</b>
|
||||
</p>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="tabs">
|
||||
<li class="tab" @click="getStatus('UNPAID')">
|
||||
<a :class="['tab-link', {'a-active': filters.status.value === 'UNPAID'}]" href="#" >{{ $t('general.due') }}</a>
|
||||
<a
|
||||
:class="[
|
||||
'tab-link',
|
||||
{ 'a-active': filters.status.value === 'UNPAID' },
|
||||
]"
|
||||
href="#"
|
||||
>{{ $t('general.due') }}</a
|
||||
>
|
||||
</li>
|
||||
<li class="tab" @click="getStatus('DRAFT')">
|
||||
<a :class="['tab-link', {'a-active': filters.status.value === 'DRAFT'}]" href="#">{{ $t('general.draft') }}</a>
|
||||
<a
|
||||
:class="[
|
||||
'tab-link',
|
||||
{ 'a-active': filters.status.value === 'DRAFT' },
|
||||
]"
|
||||
href="#"
|
||||
>{{ $t('general.draft') }}</a
|
||||
>
|
||||
</li>
|
||||
<li class="tab" @click="getStatus('')">
|
||||
<a :class="['tab-link', {'a-active': filters.status.value === '' || filters.status.value === null || filters.status.value !== 'DRAFT' && filters.status.value !== 'UNPAID'}]" href="#">{{ $t('general.all') }}</a>
|
||||
<a
|
||||
:class="[
|
||||
'tab-link',
|
||||
{
|
||||
'a-active':
|
||||
filters.status.value === '' ||
|
||||
filters.status.value === null ||
|
||||
(filters.status.value !== 'DRAFT' &&
|
||||
filters.status.value !== 'UNPAID'),
|
||||
},
|
||||
]"
|
||||
href="#"
|
||||
>{{ $t('general.all') }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<transition name="fade">
|
||||
<v-dropdown v-if="selectedInvoices.length" :show-arrow="false">
|
||||
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
|
||||
<span
|
||||
slot="activator"
|
||||
href="#"
|
||||
class="table-actions-button dropdown-toggle"
|
||||
>
|
||||
{{ $t('general.actions') }}
|
||||
</span>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeMultipleInvoices">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trash']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
@ -155,8 +199,12 @@
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
@change="selectAllInvoices"
|
||||
/>
|
||||
<label
|
||||
v-show="!isRequestOngoing"
|
||||
for="select-all"
|
||||
class="custom-control-label selectall"
|
||||
>
|
||||
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
|
||||
<span class="select-all-label">{{ $t('general.select_all') }} </span>
|
||||
</label>
|
||||
</div>
|
||||
@ -180,7 +228,7 @@
|
||||
:value="row.id"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
>
|
||||
/>
|
||||
<label :for="row.id" class="custom-control-label" />
|
||||
</div>
|
||||
</template>
|
||||
@ -195,35 +243,33 @@
|
||||
width="20%"
|
||||
show="name"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('invoices.status')"
|
||||
sort-as="status"
|
||||
>
|
||||
<table-column :label="$t('invoices.status')" sort-as="status">
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('invoices.status') }}</span>
|
||||
<span :class="'inv-status-'+row.status.toLowerCase()">{{ (row.status != 'PARTIALLY_PAID')? row.status : row.status.replace('_', ' ') }}</span>
|
||||
<span :class="'inv-status-' + row.status.toLowerCase()">{{
|
||||
row.status != 'PARTIALLY_PAID'
|
||||
? row.status
|
||||
: row.status.replace('_', ' ')
|
||||
}}</span>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:label="$t('invoices.paid_status')"
|
||||
sort-as="paid_status"
|
||||
>
|
||||
<table-column :label="$t('invoices.paid_status')" sort-as="paid_status">
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('invoices.paid_status') }}</span>
|
||||
<span :class="'inv-status-'+row.paid_status.toLowerCase()">{{ (row.paid_status != 'PARTIALLY_PAID')? row.paid_status : row.paid_status.replace('_', ' ') }}</span>
|
||||
<span :class="'inv-status-' + row.paid_status.toLowerCase()">{{
|
||||
row.paid_status != 'PARTIALLY_PAID'
|
||||
? row.paid_status
|
||||
: row.paid_status.replace('_', ' ')
|
||||
}}</span>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:label="$t('invoices.number')"
|
||||
show="invoice_number"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('invoices.amount_due')"
|
||||
sort-as="due_amount"
|
||||
>
|
||||
<table-column :label="$t('invoices.number')" show="invoice_number" />
|
||||
<table-column :label="$t('invoices.amount_due')" sort-as="due_amount">
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('invoices.amount_due') }}</span>
|
||||
<div v-html="$utils.formatMoney(row.due_amount, row.user.currency)"/>
|
||||
<div
|
||||
v-html="$utils.formatMoney(row.due_amount, row.user.currency)"
|
||||
/>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
@ -238,42 +284,91 @@
|
||||
<dot-icon />
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<router-link :to="{path: `invoices/${row.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon"/>
|
||||
<router-link
|
||||
:to="{ path: `invoices/${row.id}/edit` }"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'pencil-alt']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<router-link :to="{path: `invoices/${row.id}/view`}" class="dropdown-item">
|
||||
<router-link
|
||||
:to="{ path: `invoices/${row.id}/view` }"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon icon="eye" class="dropdown-item-icon" />
|
||||
{{ $t('invoices.view') }}
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status == 'DRAFT'">
|
||||
<a class="dropdown-item" href="#/" @click="sendInvoice(row.id)">
|
||||
<font-awesome-icon icon="paper-plane" class="dropdown-item-icon" />
|
||||
<font-awesome-icon
|
||||
icon="paper-plane"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('invoices.send_invoice') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item
|
||||
v-if="row.status === 'SENT' || row.status === 'VIEWED'"
|
||||
>
|
||||
<a class="dropdown-item" href="#/" @click="sendInvoice(row.id)">
|
||||
<font-awesome-icon
|
||||
icon="paper-plane"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('invoices.resend_invoice') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status == 'DRAFT'">
|
||||
<a class="dropdown-item" href="#/" @click="markInvoiceAsSent(row.id)">
|
||||
<font-awesome-icon icon="check-circle" class="dropdown-item-icon" />
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#/"
|
||||
@click="markInvoiceAsSent(row.id)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="check-circle"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('invoices.mark_as_sent') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status === 'SENT' || row.status === 'VIEWED' || row.status === 'OVERDUE'">
|
||||
<router-link :to="`/admin/payments/${row.id}/create`" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'credit-card']" class="dropdown-item-icon"/>
|
||||
<v-dropdown-item
|
||||
v-if="
|
||||
row.status === 'SENT' ||
|
||||
row.status === 'VIEWED' ||
|
||||
row.status === 'OVERDUE'
|
||||
"
|
||||
>
|
||||
<router-link
|
||||
:to="`/admin/payments/${row.id}/create`"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'credit-card']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('payments.record_payment') }}
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<a class="dropdown-item" href="#/" @click="onCloneInvoice(row.id)">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#/"
|
||||
@click="onCloneInvoice(row.id)"
|
||||
>
|
||||
<font-awesome-icon icon="copy" class="dropdown-item-icon" />
|
||||
{{ $t('invoices.clone_invoice') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeInvoice(row.id)">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trash']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
|
||||
@ -24,26 +24,47 @@
|
||||
>
|
||||
{{ $t('invoices.send_invoice') }}
|
||||
</base-button>
|
||||
<router-link v-if="invoice.status === 'SENT'" :to="`/admin/payments/${$route.params.id}/create`">
|
||||
<base-button
|
||||
color="theme"
|
||||
<router-link
|
||||
v-if="invoice.status === 'SENT'"
|
||||
:to="`/admin/payments/${$route.params.id}/create`"
|
||||
>
|
||||
<base-button color="theme">
|
||||
{{ $t('payments.record_payment') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
<v-dropdown :close-on-select="false" align="left" class="filter-container">
|
||||
<v-dropdown
|
||||
:close-on-select="true"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
>
|
||||
<a slot="activator" href="#">
|
||||
<base-button color="theme">
|
||||
<font-awesome-icon icon="ellipsis-h" />
|
||||
</base-button>
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<router-link :to="{path: `/admin/invoices/${$route.params.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon"/>
|
||||
<div class="dropdown-item" @click="copyPdfUrl">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'link']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ path: `/admin/invoices/${$route.params.id}/edit` }"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'pencil-alt']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<div class="dropdown-item" @click="removeInvoice($route.params.id)">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trash']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
@ -61,14 +82,18 @@
|
||||
align-icon="right"
|
||||
@input="onSearch"
|
||||
/>
|
||||
<div
|
||||
class="btn-group ml-3"
|
||||
role="group"
|
||||
aria-label="First group"
|
||||
<div class="btn-group ml-3" role="group" aria-label="First group">
|
||||
<v-dropdown
|
||||
:close-on-select="false"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
>
|
||||
<v-dropdown :close-on-select="false" align="left" class="filter-container">
|
||||
<a slot="activator" href="#">
|
||||
<base-button class="inv-button inv-filter-fields-btn" color="default" size="medium">
|
||||
<base-button
|
||||
class="inv-button inv-filter-fields-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
>
|
||||
<font-awesome-icon icon="filter" />
|
||||
</base-button>
|
||||
</a>
|
||||
@ -84,8 +109,10 @@
|
||||
class="inv-radio"
|
||||
value="invoice_date"
|
||||
@change="onSearch"
|
||||
>
|
||||
<label class="inv-label" for="filter_invoice_date">{{ $t('invoices.invoice_date') }}</label>
|
||||
/>
|
||||
<label class="inv-label" for="filter_invoice_date">{{
|
||||
$t('invoices.invoice_date')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
@ -96,8 +123,10 @@
|
||||
class="inv-radio"
|
||||
value="due_date"
|
||||
@change="onSearch"
|
||||
>
|
||||
<label class="inv-label" for="filter_due_date">{{ $t('invoices.due_date') }}</label>
|
||||
/>
|
||||
<label class="inv-label" for="filter_due_date">{{
|
||||
$t('invoices.due_date')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
@ -108,11 +137,19 @@
|
||||
class="inv-radio"
|
||||
value="invoice_number"
|
||||
@change="onSearch"
|
||||
>
|
||||
<label class="inv-label" for="filter_invoice_number">{{ $t('invoices.invoice_number') }}</label>
|
||||
/>
|
||||
<label class="inv-label" for="filter_invoice_number">{{
|
||||
$t('invoices.invoice_number')
|
||||
}}</label>
|
||||
</div>
|
||||
</v-dropdown>
|
||||
<base-button v-tooltip.top-center="{ content: getOrderName }" class="inv-button inv-filter-sorting-btn" color="default" size="medium" @click="sortData">
|
||||
<base-button
|
||||
v-tooltip.top-center="{ content: getOrderName }"
|
||||
class="inv-button inv-filter-sorting-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
@click="sortData"
|
||||
>
|
||||
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
|
||||
<font-awesome-icon v-else icon="sort-amount-down" />
|
||||
</base-button>
|
||||
@ -129,10 +166,20 @@
|
||||
<div class="left">
|
||||
<div class="inv-name">{{ invoice.user.name }}</div>
|
||||
<div class="inv-number">{{ invoice.invoice_number }}</div>
|
||||
<div :class="'inv-status-'+invoice.status.toLowerCase()" class="inv-status">{{ invoice.status }}</div>
|
||||
<div
|
||||
:class="'inv-status-' + invoice.status.toLowerCase()"
|
||||
class="inv-status"
|
||||
>
|
||||
{{ invoice.status }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="inv-amount" v-html="$utils.formatMoney(invoice.due_amount, invoice.user.currency)" />
|
||||
<div
|
||||
class="inv-amount"
|
||||
v-html="
|
||||
$utils.formatMoney(invoice.due_amount, invoice.user.currency)
|
||||
"
|
||||
/>
|
||||
<div class="inv-date">{{ invoice.formattedInvoiceDate }}</div>
|
||||
</div>
|
||||
</router-link>
|
||||
@ -291,6 +338,14 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
copyPdfUrl () {
|
||||
let pdfUrl = `${window.location.origin}/invoices/pdf/${this.invoice.unique_hash}`
|
||||
|
||||
let response = this.$utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
window.toastr['success'](this.$tc('Copied PDF url to clipboard!'))
|
||||
|
||||
},
|
||||
async removeInvoice (id) {
|
||||
this.selectInvoice([parseInt(id)])
|
||||
this.id = id
|
||||
|
||||
@ -11,19 +11,39 @@
|
||||
>
|
||||
{{ $t('payments.send_payment_receipt') }}
|
||||
</base-button>
|
||||
<v-dropdown :close-on-select="false" align="left" class="filter-container">
|
||||
<v-dropdown
|
||||
:close-on-select="true"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
>
|
||||
<a slot="activator" href="#">
|
||||
<base-button color="theme">
|
||||
<font-awesome-icon icon="ellipsis-h" />
|
||||
</base-button>
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<router-link :to="{path: `/admin/payments/${$route.params.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon"/>
|
||||
<div class="dropdown-item" @click="copyPdfUrl">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'link']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ path: `/admin/payments/${$route.params.id}/edit` }"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'pencil-alt']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<div class="dropdown-item" @click="removePayment($route.params.id)">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trash']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
@ -41,14 +61,18 @@
|
||||
align-icon="right"
|
||||
@input="onSearch"
|
||||
/>
|
||||
<div
|
||||
class="btn-group ml-3"
|
||||
role="group"
|
||||
aria-label="First group"
|
||||
<div class="btn-group ml-3" role="group" aria-label="First group">
|
||||
<v-dropdown
|
||||
:close-on-select="false"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
>
|
||||
<v-dropdown :close-on-select="false" align="left" class="filter-container">
|
||||
<a slot="activator" href="#">
|
||||
<base-button class="inv-button inv-filter-fields-btn" color="default" size="medium">
|
||||
<base-button
|
||||
class="inv-button inv-filter-fields-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
>
|
||||
<font-awesome-icon icon="filter" />
|
||||
</base-button>
|
||||
</a>
|
||||
@ -64,8 +88,10 @@
|
||||
class="inv-radio"
|
||||
value="invoice_number"
|
||||
@change="onSearch"
|
||||
>
|
||||
<label class="inv-label" for="filter_invoice_number">{{ $t('invoices.title') }}</label>
|
||||
/>
|
||||
<label class="inv-label" for="filter_invoice_number">{{
|
||||
$t('invoices.title')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
@ -76,8 +102,10 @@
|
||||
class="inv-radio"
|
||||
value="payment_date"
|
||||
@change="onSearch"
|
||||
>
|
||||
<label class="inv-label" for="filter_payment_date">{{ $t('payments.date') }}</label>
|
||||
/>
|
||||
<label class="inv-label" for="filter_payment_date">{{
|
||||
$t('payments.date')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
@ -88,11 +116,19 @@
|
||||
class="inv-radio"
|
||||
value="payment_number"
|
||||
@change="onSearch"
|
||||
>
|
||||
<label class="inv-label" for="filter_payment_number">{{ $t('payments.payment_number') }}</label>
|
||||
/>
|
||||
<label class="inv-label" for="filter_payment_number">{{
|
||||
$t('payments.payment_number')
|
||||
}}</label>
|
||||
</div>
|
||||
</v-dropdown>
|
||||
<base-button v-tooltip.top-center="{ content: getOrderName }" class="inv-button inv-filter-sorting-btn" color="default" size="medium" @click="sortData">
|
||||
<base-button
|
||||
v-tooltip.top-center="{ content: getOrderName }"
|
||||
class="inv-button inv-filter-sorting-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
@click="sortData"
|
||||
>
|
||||
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
|
||||
<font-awesome-icon v-else icon="sort-amount-down" />
|
||||
</base-button>
|
||||
@ -112,7 +148,10 @@
|
||||
<div class="inv-number">{{ payment.invoice_number }}</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="inv-amount" v-html="$utils.formatMoney(payment.amount, payment.user.currency)" />
|
||||
<div
|
||||
class="inv-amount"
|
||||
v-html="$utils.formatMoney(payment.amount, payment.user.currency)"
|
||||
/>
|
||||
<div class="inv-date">{{ payment.formattedPaymentDate }}</div>
|
||||
<!-- <div class="inv-number">{{ payment.payment_method.name }}</div> -->
|
||||
</div>
|
||||
@ -260,6 +299,13 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
copyPdfUrl () {
|
||||
let pdfUrl = `${window.location.origin}/payments/pdf/${this.payment.unique_hash}`
|
||||
|
||||
let response = this.$utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
window.toastr['success'](this.$tc('Copied PDF url to clipboard!'))
|
||||
},
|
||||
async removePayment (id) {
|
||||
this.id = id
|
||||
window.swal({
|
||||
|
||||
@ -1,33 +1,76 @@
|
||||
<template>
|
||||
<div class="setting-main-container">
|
||||
<div class="setting-main-container update-container">
|
||||
<div class="card setting-card">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ $t('settings.update_app.title') }}</h3>
|
||||
<p class="page-sub-title">
|
||||
{{ $t('settings.update_app.description') }}
|
||||
</p>
|
||||
<label class="input-label">{{ $t('settings.update_app.current_version') }}</label><br>
|
||||
<label class="input-label">{{
|
||||
$t('settings.update_app.current_version')
|
||||
}}</label
|
||||
><br />
|
||||
<label class="version mb-4">{{ currentVersion }}</label>
|
||||
<base-button :outline="true" :disabled="isCheckingforUpdate || isUpdating" size="large" color="theme" class="mb-4" @click="checkUpdate">
|
||||
<font-awesome-icon :class="{'update': isCheckingforUpdate}" style="margin-right: 10px;" icon="sync-alt" />
|
||||
<base-button
|
||||
:outline="true"
|
||||
:disabled="isCheckingforUpdate || isUpdating"
|
||||
size="large"
|
||||
color="theme"
|
||||
class="mb-4"
|
||||
@click="checkUpdate"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:class="{ update: isCheckingforUpdate }"
|
||||
style="margin-right: 10px;"
|
||||
icon="sync-alt"
|
||||
/>
|
||||
{{ $t('settings.update_app.check_update') }}
|
||||
</base-button>
|
||||
<hr>
|
||||
<hr />
|
||||
<div v-show="!isUpdating" v-if="isUpdateAvailable" class="mt-4 content">
|
||||
<h3 class="page-title mb-3">{{ $t('settings.update_app.avail_update') }}</h3>
|
||||
<label class="input-label">{{ $t('settings.update_app.next_version') }}</label><br>
|
||||
<h3 class="page-title mb-3">
|
||||
{{ $t('settings.update_app.avail_update') }}
|
||||
</h3>
|
||||
<label class="input-label">{{
|
||||
$t('settings.update_app.next_version')
|
||||
}}</label
|
||||
><br />
|
||||
<label class="version">{{ updateData.version }}</label>
|
||||
<p class="page-sub-title" style="white-space: pre-wrap;">{{ description }}</p>
|
||||
<base-button size="large" icon="rocket" color="theme" @click="onUpdateApp">
|
||||
<base-button
|
||||
size="large"
|
||||
icon="rocket"
|
||||
color="theme"
|
||||
@click="onUpdateApp"
|
||||
>
|
||||
{{ $t('settings.update_app.update') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<div v-if="isUpdating" class="mt-4 content">
|
||||
<h3 class="page-title">{{ $t('settings.update_app.update_progress') }}</h3>
|
||||
<div class="d-flex flex-row justify-content-between">
|
||||
<div>
|
||||
<h3 class="page-title">
|
||||
{{ $t('settings.update_app.update_progress') }}
|
||||
</h3>
|
||||
<p class="page-sub-title">
|
||||
{{ $t('settings.update_app.progress_text') }}
|
||||
</p>
|
||||
<font-awesome-icon icon="spinner" class="fa-spin"/>
|
||||
</div>
|
||||
<font-awesome-icon icon="spinner" class="update-spinner fa-spin" />
|
||||
</div>
|
||||
<ul class="update-steps-container">
|
||||
<li class="update-step" v-for="step in updateSteps">
|
||||
<p class="update-step-text">{{ $t(step.translationKey) }}</p>
|
||||
<div class="update-status-container">
|
||||
<span v-if="step.time" class="update-time">{{
|
||||
step.time
|
||||
}}</span>
|
||||
<span :class="'update-status status-' + getStatus(step)">{{
|
||||
getStatus(step)
|
||||
}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,11 +89,48 @@ export default {
|
||||
interval: null,
|
||||
description: '',
|
||||
currentVersion: '',
|
||||
updateSteps: [
|
||||
{
|
||||
translationKey: 'settings.update_app.download_zip_file',
|
||||
stepUrl: '/api/update/download',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'settings.update_app.unzipping_package',
|
||||
stepUrl: '/api/update/unzip',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'settings.update_app.copying_files',
|
||||
stepUrl: '/api/update/copy',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'settings.update_app.running_migrations',
|
||||
stepUrl: '/api/update/migrate',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'settings.update_app.finishing_update',
|
||||
stepUrl: '/api/update/finish',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
],
|
||||
updateData: {
|
||||
isMinor: Boolean,
|
||||
installed: '',
|
||||
version: ''
|
||||
}
|
||||
version: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -69,36 +149,17 @@ export default {
|
||||
closeHandler() {
|
||||
console.log('closing')
|
||||
},
|
||||
async onUpdateApp () {
|
||||
try {
|
||||
this.isUpdating = true
|
||||
this.updateData.installed = this.currentVersion
|
||||
let res = await window.axios.post('/api/update', this.updateData)
|
||||
|
||||
if (res.data.success) {
|
||||
setTimeout(async () => {
|
||||
await window.axios.post('/api/update/finish', this.updateData)
|
||||
|
||||
window.toastr['success'](this.$t('settings.update_app.update_success'))
|
||||
this.currentVersion = this.updateData.version
|
||||
this.isUpdateAvailable = false
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 2000)
|
||||
}, 1000)
|
||||
getStatus(step) {
|
||||
if (step.started && step.completed) {
|
||||
return 'finished'
|
||||
} else if (step.started && !step.completed) {
|
||||
return 'running'
|
||||
} else if (!step.started && !step.completed) {
|
||||
return 'pending'
|
||||
} else {
|
||||
console.log(res.data)
|
||||
window.toastr['error'](res.data.error)
|
||||
return 'error'
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
window.toastr['error']('Something went wrong')
|
||||
}
|
||||
|
||||
this.isUpdating = false
|
||||
},
|
||||
|
||||
async checkUpdate() {
|
||||
try {
|
||||
this.isCheckingforUpdate = true
|
||||
@ -122,8 +183,67 @@ export default {
|
||||
this.isCheckingforUpdate = false
|
||||
window.toastr['error']('Something went wrong')
|
||||
}
|
||||
},
|
||||
async onUpdateApp() {
|
||||
let path = null
|
||||
|
||||
for (let index = 0; index < this.updateSteps.length; index++) {
|
||||
let currentStep = this.updateSteps[index]
|
||||
try {
|
||||
this.isUpdating = true
|
||||
currentStep.started = true
|
||||
let updateParams = {
|
||||
version: this.updateData.version,
|
||||
installed: this.currentVersion,
|
||||
path: path || null,
|
||||
}
|
||||
|
||||
let requestResponse = await window.axios.post(
|
||||
currentStep.stepUrl,
|
||||
updateParams
|
||||
)
|
||||
currentStep.completed = true
|
||||
if (requestResponse.data && requestResponse.data.path) {
|
||||
path = requestResponse.data.path
|
||||
}
|
||||
|
||||
// on finish
|
||||
if (
|
||||
currentStep.translationKey == 'settings.update_app.finishing_update'
|
||||
) {
|
||||
this.isUpdating = false
|
||||
window.toastr['success'](
|
||||
this.$t('settings.update_app.update_success')
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
currentStep.started = false
|
||||
currentStep.completed = true
|
||||
window.toastr['error'](this.$t('validation.something_went_wrong'))
|
||||
this.onUpdateFailed(currentStep.translationKey)
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
onUpdateFailed(translationKey) {
|
||||
let stepName = this.$t(translationKey)
|
||||
swal({
|
||||
title: this.$t('settings.update_app.update_failed'),
|
||||
text: this.$tc('settings.update_app.update_failed_text', stepName, {step: stepName}),
|
||||
buttons: [this.$t('general.cancel'), this.$t('general.retry')],
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
this.onUpdateApp()
|
||||
return
|
||||
}
|
||||
this.isUpdating = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -54,7 +54,8 @@ import {
|
||||
faEyeSlash,
|
||||
faSyncAlt,
|
||||
faRocket,
|
||||
faCamera
|
||||
faCamera,
|
||||
faLink,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { far } from '@fortawesome/free-regular-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
@ -119,7 +120,8 @@ library.add(
|
||||
faPaperPlane,
|
||||
faSyncAlt,
|
||||
faRocket,
|
||||
faCamera
|
||||
faCamera,
|
||||
faLink
|
||||
)
|
||||
|
||||
Vue.component('font-awesome-icon', FontAwesomeIcon)
|
||||
|
||||
78
resources/assets/sass/pages/settings.scss
vendored
78
resources/assets/sass/pages/settings.scss
vendored
@ -122,6 +122,84 @@
|
||||
|
||||
}
|
||||
|
||||
.update-container {
|
||||
|
||||
.update-spinner {
|
||||
font-size: 30px;
|
||||
color: $ls-color-gray--dark;
|
||||
}
|
||||
|
||||
.update-steps-container {
|
||||
list-style-type: none;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
|
||||
.update-step {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0px;
|
||||
border-bottom: 1px solid $ls-color-gray--light;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0px solid;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.update-step-text {
|
||||
font-size: $font-size-base;
|
||||
margin: 0px;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.update-status-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.update-time {
|
||||
font-size: 10px;
|
||||
color: $ls-color-gray--dark;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.update-status {
|
||||
font-size: 13px;
|
||||
width: 88px;
|
||||
height: 28px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
border-radius: 30px;
|
||||
text-transform: uppercase;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #EAF1FB;
|
||||
color: $ls-color-secondary;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background-color: rgba(21, 178, 236, 0.15);
|
||||
color: $ls-color-light-blue;
|
||||
}
|
||||
|
||||
.status-finished {
|
||||
background-color: #D4F6EE;
|
||||
color: $ls-color-green;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: rgba(251, 113, 120, 0.22);
|
||||
color: $ls-color-red;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.add-new-tax {
|
||||
height: 45px;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -122,23 +122,41 @@ Route::group(['middleware' => 'api'], function () {
|
||||
'middleware' => 'admin'
|
||||
], function () {
|
||||
|
||||
|
||||
// Auto update routes
|
||||
// Self Update
|
||||
//----------------------------------
|
||||
Route::post('/update', [
|
||||
'as' => 'auto.update',
|
||||
'uses' => 'UpdateController@update'
|
||||
|
||||
Route::get('/check/update', [
|
||||
'as' => 'update.check',
|
||||
'uses' => 'UpdateController@checkLatestVersion'
|
||||
]);
|
||||
|
||||
Route::post('/update/download', [
|
||||
'as' => 'update.download',
|
||||
'uses' => 'UpdateController@download'
|
||||
]);
|
||||
|
||||
Route::post('/update/unzip', [
|
||||
'as' => 'update.unzip',
|
||||
'uses' => 'UpdateController@unzip'
|
||||
]);
|
||||
|
||||
Route::post('/update/copy', [
|
||||
'as' => 'update.copy',
|
||||
'uses' => 'UpdateController@copyFiles'
|
||||
]);
|
||||
|
||||
Route::post('/update/migrate', [
|
||||
'as' => 'update.migrate',
|
||||
'uses' => 'UpdateController@migrate'
|
||||
]);
|
||||
|
||||
Route::post('/update/finish', [
|
||||
'as' => 'auto.update.finish',
|
||||
'as' => 'update.finish',
|
||||
'uses' => 'UpdateController@finishUpdate'
|
||||
]);
|
||||
|
||||
Route::get('/check/update', [
|
||||
'as' => 'check.update',
|
||||
'uses' => 'UpdateController@checkLatestVersion'
|
||||
]);
|
||||
// Bootstrap
|
||||
//----------------------------------
|
||||
|
||||
Route::get('/bootstrap', [
|
||||
'as' => 'bootstrap',
|
||||
|
||||
@ -88,7 +88,7 @@ Route::get('/expenses/{id}/receipt/{hash}', [
|
||||
'uses' => 'ExpensesController@downloadReceipt'
|
||||
]);
|
||||
|
||||
// Setup for instalation of app
|
||||
// Setup for installation of app
|
||||
// ----------------------------------------------
|
||||
Route::get('/on-boarding', function () {
|
||||
return view('app');
|
||||
|
||||
Reference in New Issue
Block a user