diff --git a/.gitignore b/.gitignore index 5f355185..0afb940b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,3 @@ Homestead.yaml .rnd /.expo /.vscode -docker-compose.yml -docker-compose.yaml diff --git a/Dockerfile b/Dockerfile index 28c46c56..0862fdc1 100644 --- a/Dockerfile +++ b/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 - -RUN composer dump-autoload --optimize --classmap-authoritative +# Install PHP extensions +RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl bcmath gd -##### STAGE 2 ##### +# Get latest Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer -FROM php:7.3.12-fpm-alpine +# 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 -# Use the default production configuration -RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" +# Set working directory +WORKDIR /var/www -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 diff --git a/app/Console/Commands/UpdateCommand.php b/app/Console/Commands/UpdateCommand.php new file mode 100644 index 00000000..a01a94d3 --- /dev/null +++ b/app/Console/Commands/UpdateCommand.php @@ -0,0 +1,190 @@ +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; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 39bbe5c7..cd270548 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,7 +12,8 @@ class Kernel extends ConsoleKernel * @var array */ protected $commands = [ - Commands\ResetApp::class + Commands\ResetApp::class, + Commands\UpdateCommand::class ]; /** diff --git a/app/Http/Controllers/UpdateController.php b/app/Http/Controllers/UpdateController.php index 130c5096..2b75d837 100644 --- a/app/Http/Controllers/UpdateController.php +++ b/app/Http/Controllers/UpdateController.php @@ -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); } diff --git a/app/Listeners/Updates/Listener.php b/app/Listeners/Updates/Listener.php index 66b50655..8a4995f5 100644 --- a/app/Listeners/Updates/Listener.php +++ b/app/Listeners/Updates/Listener.php @@ -2,6 +2,7 @@ namespace Crater\Listeners\Updates; +// Implementation taken from Akaunting - https://github.com/akaunting/akaunting class Listener { const VERSION = ''; diff --git a/app/Listeners/Updates/v3/Version310.php b/app/Listeners/Updates/v3/Version310.php index 2945bede..e07adebe 100644 --- a/app/Listeners/Updates/v3/Version310.php +++ b/app/Listeners/Updates/v3/Version310.php @@ -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); diff --git a/app/Space/SiteApi.php b/app/Space/SiteApi.php index 7c9ac7ea..86730ea0 100644 --- a/app/Space/SiteApi.php +++ b/app/Space/SiteApi.php @@ -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 { diff --git a/app/Space/Updater.php b/app/Space/Updater.php index 6acb8f6a..536f7534 100644 --- a/app/Space/Updater.php +++ b/app/Space/Updater.php @@ -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 { + $zip_file_path = $temp_dir . '/upload.zip'; - $file = $temp_path . '/upload.zip'; + // Add content to the Zip file + $uploaded = is_int(file_put_contents($zip_file_path, $data)) ? true : false; - // Add content to the Zip file - $uploaded = is_int(file_put_contents($file, $data)) ? true : false; - - if (!$uploaded) { - return false; - } - - // Unzip the file - $zip = new ZipArchive(); - - if ($zip->open($file)) { - $zip->extractTo($temp_path2); - } - - $zip->close(); - - // Delete zip file - File::delete($file); - - if (!File::copyDirectory($temp_path2.'/Crater', base_path())) { - return false; - } - - // Delete temp directory - File::deleteDirectory($temp_path); - File::deleteDirectory($temp_path2); - - 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 [ - 'success' => false, - 'error' => 'Update error', - 'data' => [] - ]; + 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($zip_file_path)) { + $zip->extractTo($temp_extract_dir); + } + + $zip->close(); + + // Delete zip file + File::delete($zip_file_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_extract_dir); + + return true; + } + + 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); - } } diff --git a/composer.json b/composer.json index 900a1f4f..f5e52922 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/database/migrations/2017_04_11_064308_create_units_table.php b/database/migrations/2017_04_11_064308_create_units_table.php index 0cc1cb91..5b14a3b6 100644 --- a/database/migrations/2017_04_11_064308_create_units_table.php +++ b/database/migrations/2017_04_11_064308_create_units_table.php @@ -13,13 +13,15 @@ class CreateUnitsTable extends Migration */ public function up() { - Schema::create('units', function (Blueprint $table) { - $table->increments('id'); - $table->string('name'); - $table->integer('company_id')->unsigned()->nullable(); - $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); - $table->timestamps(); - }); + if (!Schema::hasTable('units')) { + Schema::create('units', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->integer('company_id')->unsigned()->nullable(); + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); + $table->timestamps(); + }); + } } /** diff --git a/database/migrations/2018_11_02_133956_create_expenses_table.php b/database/migrations/2018_11_02_133956_create_expenses_table.php index a0e745a5..25912644 100644 --- a/database/migrations/2018_11_02_133956_create_expenses_table.php +++ b/database/migrations/2018_11_02_133956_create_expenses_table.php @@ -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(); }); } diff --git a/database/migrations/2019_09_02_053155_create_payment_methods_table.php b/database/migrations/2019_09_02_053155_create_payment_methods_table.php index 8880c6a4..8a2c7caf 100644 --- a/database/migrations/2019_09_02_053155_create_payment_methods_table.php +++ b/database/migrations/2019_09_02_053155_create_payment_methods_table.php @@ -13,13 +13,15 @@ class CreatePaymentMethodsTable extends Migration */ public function up() { - Schema::create('payment_methods', function (Blueprint $table) { - $table->increments('id'); - $table->string('name'); - $table->integer('company_id')->unsigned()->nullable(); - $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); - $table->timestamps(); - }); + if (!Schema::hasTable('payment_methods')) { + Schema::create('payment_methods', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->integer('company_id')->unsigned()->nullable(); + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); + $table->timestamps(); + }); + } } /** diff --git a/database/migrations/2020_05_12_154129_add_user_id_to_expenses_table.php b/database/migrations/2020_05_12_154129_add_user_id_to_expenses_table.php new file mode 100644 index 00000000..130994c1 --- /dev/null +++ b/database/migrations/2020_05_12_154129_add_user_id_to_expenses_table.php @@ -0,0 +1,33 @@ +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'); + }); + } +} diff --git a/docker-compose.yaml.example b/docker-compose.yaml.example deleted file mode 100644 index 590e4c2a..00000000 --- a/docker-compose.yaml.example +++ /dev/null @@ -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: - diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..7a828768 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/docker-compose/nginx/nginx.conf b/docker-compose/nginx/nginx.conf new file mode 100644 index 00000000..4c6cbf44 --- /dev/null +++ b/docker-compose/nginx/nginx.conf @@ -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; + } +} \ No newline at end of file diff --git a/docker-compose/setup.sh b/docker-compose/setup.sh new file mode 100755 index 00000000..8733a1c0 --- /dev/null +++ b/docker-compose/setup.sh @@ -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 \ No newline at end of file diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index 36645fa7..00000000 --- a/nginx.conf +++ /dev/null @@ -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; - } - } -} diff --git a/readme.md b/readme.md index 24cbc379..875400d9 100644 --- a/readme.md +++ b/readme.md @@ -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. diff --git a/resources/assets/js/helpers/utilities.js b/resources/assets/js/helpers/utilities.js index 442af4c4..be9afd51 100644 --- a/resources/assets/js/helpers/utilities.js +++ b/resources/assets/js/helpers/utilities.js @@ -1,16 +1,16 @@ export default { - toggleSidebar () { + toggleSidebar() { let icon = document.getElementsByClassName('hamburger')[0] document.body.classList.toggle('sidebar-open') icon.classList.toggle('is-active') }, - addClass (el, className) { + addClass(el, className) { if (el.classList) el.classList.add(className) else el.className += ' ' + className }, - hasClass (el, className) { + hasClass(el, className) { const hasClass = el.classList ? el.classList.contains(className) : new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className) @@ -18,33 +18,38 @@ export default { return hasClass }, - reset (prefix) { + reset(prefix) { let regx = new RegExp('\\b' + prefix + '(.*)?\\b', 'g') document.body.className = document.body.className.replace(regx, '') }, - setLayout (layoutName) { + setLayout(layoutName) { this.reset('layout-') document.body.classList.add('layout-' + layoutName) }, - setSkin (skinName) { + setSkin(skinName) { this.reset('skin-') document.body.classList.add('skin-' + skinName) }, - setLogo (logoSrc) { + setLogo(logoSrc) { document.getElementById('logo-desk').src = logoSrc }, - formatMoney (amount, currency = 0) { + 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 - let {precision, decimal_separator, thousand_separator, symbol} = currency + let { precision, decimal_separator, thousand_separator, symbol } = currency try { precision = Math.abs(precision) @@ -52,25 +57,44 @@ 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) } }, - formatGraphMoney (amount, currency = 0) { + 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 - let {precision, decimal_separator, thousand_separator, symbol} = currency + let { precision, decimal_separator, thousand_separator, symbol } = currency try { precision = Math.abs(precision) @@ -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 + checkValidUrl(url) { + 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 + } + ) + }, } diff --git a/resources/assets/js/plugins/ar.json b/resources/assets/js/plugins/ar.json index 757fb59d..ab647546 100644 --- a/resources/assets/js/plugins/ar.json +++ b/resources/assets/js/plugins/ar.json @@ -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": { diff --git a/resources/assets/js/plugins/de.json b/resources/assets/js/plugins/de.json index d3ff7ec4..1d7a6027 100644 --- a/resources/assets/js/plugins/de.json +++ b/resources/assets/js/plugins/de.json @@ -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": { diff --git a/resources/assets/js/plugins/en.json b/resources/assets/js/plugins/en.json index 47066cae..650a8e6e 100644 --- a/resources/assets/js/plugins/en.json +++ b/resources/assets/js/plugins/en.json @@ -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": { diff --git a/resources/assets/js/plugins/es.json b/resources/assets/js/plugins/es.json index d51a9b45..fc2bec22 100644 --- a/resources/assets/js/plugins/es.json +++ b/resources/assets/js/plugins/es.json @@ -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": { diff --git a/resources/assets/js/plugins/fr.json b/resources/assets/js/plugins/fr.json index bce09802..b8764790 100644 --- a/resources/assets/js/plugins/fr.json +++ b/resources/assets/js/plugins/fr.json @@ -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": { diff --git a/resources/assets/js/plugins/it.json b/resources/assets/js/plugins/it.json index 3ffabcbd..75221f24 100644 --- a/resources/assets/js/plugins/it.json +++ b/resources/assets/js/plugins/it.json @@ -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": { diff --git a/resources/assets/js/plugins/pt-br.json b/resources/assets/js/plugins/pt-br.json index 620f9ecf..e4035c9c 100644 --- a/resources/assets/js/plugins/pt-br.json +++ b/resources/assets/js/plugins/pt-br.json @@ -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": { diff --git a/resources/assets/js/views/estimates/Index.vue b/resources/assets/js/views/estimates/Index.vue index 2ca0cd1d..96cb300d 100644 --- a/resources/assets/js/views/estimates/Index.vue +++ b/resources/assets/js/views/estimates/Index.vue @@ -4,16 +4,12 @@

{{ $t('estimates.title') }}