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') }}
-
-
+
{{ $t('general.home') }}
-
-
+
{{ $tc('estimates.estimate', 2) }}
@@ -33,11 +29,9 @@
-
- {{ $t('estimates.new_estimate') }}
+
+ {{ $t('estimates.new_estimate') }}
@@ -46,7 +40,7 @@
-
+
-
+
-
+
-
-
+
+
-
+
-
{{ $t('general.showing') }}: {{ estimates.length }} {{ $t('general.of') }} {{ totalEstimates }}
+
+ {{ $t('general.showing') }}: {{ estimates.length }}
+ {{ $t('general.of') }} {{ totalEstimates }}
+
-
+
{{ $t('general.actions') }}
-
+
{{ $t('general.delete') }}
@@ -153,8 +183,12 @@
type="checkbox"
class="custom-control-input"
@change="selectAllEstimates"
+ />
+
@@ -178,7 +212,7 @@
:value="row.id"
type="checkbox"
class="custom-control-input"
- >
+ />
@@ -186,30 +220,30 @@
+ show="formattedEstimateDate"
+ />
+ show="name"
+ />
-
-
+
+
{{ $t('estimates.status') }}
- {{ row.status }}
+ {{
+ row.status
+ }}
-
+ show="estimate_number"
+ />
+
{{ $t('estimates.total') }}
@@ -227,50 +261,114 @@
-
-
+
+
{{ $t('general.edit') }}
-
+
{{ $t('general.delete') }}
-
+
{{ $t('general.view') }}
-
-
+
+
{{ $t('estimates.convert_to_invoice') }}
-
-
+
+
{{ $t('estimates.mark_as_sent') }}
-
-
-
+
+
+
{{ $t('estimates.send_estimate') }}
+
+
+
+
+ {{ $t('estimates.resend_estimate') }}
+
+
+
-
-
+
+
{{ $t('estimates.mark_as_accepted') }}
-
-
+
+
{{ $t('estimates.mark_as_rejected') }}
diff --git a/resources/assets/js/views/estimates/View.vue b/resources/assets/js/views/estimates/View.vue
index 1525ba9e..58ff3271 100644
--- a/resources/assets/js/views/estimates/View.vue
+++ b/resources/assets/js/views/estimates/View.vue
@@ -1,7 +1,7 @@
@@ -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',
diff --git a/resources/assets/js/views/invoices/Index.vue b/resources/assets/js/views/invoices/Index.vue
index 5a59f728..b116fc71 100644
--- a/resources/assets/js/views/invoices/Index.vue
+++ b/resources/assets/js/views/invoices/Index.vue
@@ -1,19 +1,15 @@
-
+
{{ $t('invoices.new_invoice') }}
@@ -44,7 +44,7 @@
-
{{ $tc('customers.customer',1) }}
+
{{ $tc('customers.customer', 1) }}
{{ $t('invoices.invoice_number') }}
-
+
-
{{ $t('general.clear_all') }}
+
{{
+ $t('general.clear_all')
+ }}
-
-
+
+
{{ $t('invoices.no_invoices') }}
- {{ $t('invoices.list_of_invoices') }}
+ {{
+ $t('invoices.list_of_invoices')
+ }}
-
{{ $t('general.showing') }}: {{ invoices.length }} {{ $t('general.of') }} {{ totalInvoices }}
+
+ {{ $t('general.showing') }}: {{ invoices.length }}
+ {{ $t('general.of') }} {{ totalInvoices }}
+
-
+
{{ $t('general.actions') }}
-
+
{{ $t('general.delete') }}
@@ -155,8 +199,12 @@
type="checkbox"
class="custom-control-input"
@change="selectAllInvoices"
+ />
+
-
{{ $t('general.select_all') }}
@@ -180,8 +228,8 @@
:value="row.id"
type="checkbox"
class="custom-control-input"
- >
-
+ />
+
@@ -195,35 +243,33 @@
width="20%"
show="name"
/>
-
-
+
+
{{ $t('invoices.status') }}
- {{ (row.status != 'PARTIALLY_PAID')? row.status : row.status.replace('_', ' ') }}
+ {{
+ row.status != 'PARTIALLY_PAID'
+ ? row.status
+ : row.status.replace('_', ' ')
+ }}
-
+
{{ $t('invoices.paid_status') }}
- {{ (row.paid_status != 'PARTIALLY_PAID')? row.paid_status : row.paid_status.replace('_', ' ') }}
+ {{
+ row.paid_status != 'PARTIALLY_PAID'
+ ? row.paid_status
+ : row.paid_status.replace('_', ' ')
+ }}
-
-
+
+
{{ $t('invoices.amount_due') }}
-
+
-
-
+
+
{{ $t('general.edit') }}
-
+
{{ $t('invoices.view') }}
-
-
+
+
{{ $t('invoices.send_invoice') }}
+
+
+
+ {{ $t('invoices.resend_invoice') }}
+
+
-
-
+
+
{{ $t('invoices.mark_as_sent') }}
-
-
-
+
+
+
{{ $t('payments.record_payment') }}
-
+
{{ $t('invoices.clone_invoice') }}
-
+
{{ $t('general.delete') }}
diff --git a/resources/assets/js/views/invoices/View.vue b/resources/assets/js/views/invoices/View.vue
index e62a91b3..384d843e 100644
--- a/resources/assets/js/views/invoices/View.vue
+++ b/resources/assets/js/views/invoices/View.vue
@@ -1,7 +1,7 @@