Compare commits

..

1 Commits

Author SHA1 Message Date
e03320d27b simplified deployment with docker (#639)
* updated Dockerfile and docker-compose.yml, replaced cron with ofelia and setup.sh with automatically executed startup.sh

* fixed permissions by setting them in setup-script
2022-03-16 18:29:21 +05:30
126 changed files with 5388 additions and 8307 deletions

View File

@ -1,161 +0,0 @@
name: Build PR Image
on:
pull_request:
types: [opened,synchronize,reopened,closed]
jobs:
build-application:
name: Build and Push `application`
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
outputs:
tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Generate UUID image name
id: uuid
run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: registry.uffizzi.com/${{ env.UUID_TAG_APP }}
tags: type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
uses: docker/build-push-action@v2
with:
push: true
context: ./
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./uffizzi/Dockerfile
build-nginx:
needs:
- build-application
name: Build and Push `nginx`
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
outputs:
tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_TAG_NGINX=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: registry.uffizzi.com/${{ env.UUID_TAG_NGINX }}
tags: type=raw,value=60d
- name: Build and Push Image to Uffizzi ephemeral registry
uses: docker/build-push-action@v2
with:
push: true
context: ./
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./uffizzi/nginx/Dockerfile
build-args: |
BASE_IMAGE=${{ needs.build-application.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-crond:
name: Build and Push `crond`
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
outputs:
tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_TAG_CROND=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: registry.uffizzi.com/${{ env.UUID_TAG_CROND }}
tags: type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
uses: docker/build-push-action@v2
with:
push: true
context: ./
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./uffizzi/crond/Dockerfile
cache-from: type=gha
cache-to: type=gha,mode=max
render-compose-file:
name: Render Docker Compose File
# Pass output of this workflow to another triggered by `workflow_run` event.
runs-on: ubuntu-latest
outputs:
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
needs:
- build-application
- build-nginx
- build-crond
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
APP_IMAGE=$(echo ${{ needs.build-application.outputs.tags }})
export APP_IMAGE
NGINX_IMAGE=$(echo ${{ needs.build-nginx.outputs.tags }})
export NGINX_IMAGE
CROND_IMAGE=$(echo ${{ needs.build-crond.outputs.tags }})
export CROND_IMAGE
# Render simple template from environment variables.
envsubst < ./uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2
delete-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: echo '${{ toJSON(github.event) }}' > event.json
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2

View File

@ -1,84 +0,0 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "Build PR Image"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.rendered.yml
key: ${{ env.COMPOSE_FILE_HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.COMPOSE_FILE_HASH }}"
cat event.json
deploy-uffizzi-preview:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2.6.1
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com/
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: write
id-token: write

2
.gitignore vendored
View File

@ -16,5 +16,3 @@ Homestead.yaml
.gitkeep
/public/docs
/.scribe
!storage/fonts/.gitkeep
.DS_Store

30
Dockerfile Normal file → Executable file
View File

@ -1,8 +1,9 @@
FROM php:8.1-fpm
FROM php:7.4-fpm
# Arguments defined in docker-compose.yml
ARG user
ARG uid
WORKDIR /var/www
COPY ./docker-compose/php/uploads.ini /usr/local/etc/php/conf.d/uploads.ini
# Install system dependencies
RUN apt-get update && apt-get install -y \
@ -15,26 +16,21 @@ RUN apt-get update && apt-get install -y \
unzip \
libzip-dev \
libmagickwand-dev \
mariadb-client
# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
mariadb-client \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN pecl install imagick \
&& docker-php-ext-enable imagick
# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl bcmath gd
RUN rmdir html && docker-php-ext-install pdo_mysql mbstring zip exif pcntl bcmath gd
# Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# 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
RUN useradd -G www-data,root -u 1000 -d /home/crater crater && chmod 777 /var/www/ && chown 1000:1000 /var/www/
USER 0
# Set working directory
WORKDIR /var/www
USER $user
COPY ./docker-compose/startup.sh /startup.sh
CMD ["/startup.sh"]

View File

@ -55,7 +55,7 @@ class CreateTemplateCommand extends Command
copy(public_path("/build/img/PDF/{$type}1.png"), public_path("/build/img/PDF/{$templateName}.png"));
copy(resource_path("/static/img/PDF/{$type}1.png"), resource_path("/static/img/PDF/{$templateName}.png"));
$path = resource_path("views/app/pdf/{$type}/{$templateName}.blade.php");
$path = resource_path("app/pdf/{$type}/{$templateName}.blade.php");
$type = ucfirst($type);
$this->info("{$type} Template created successfully at ".$path);

View File

@ -1,24 +0,0 @@
<?php
namespace Crater\Http\Controllers\V1\Admin\MailSender;
use Crater\Http\Controllers\Controller;
use Crater\Http\Resources\MailSenderResource;
use Crater\Models\MailSender;
use Illuminate\Http\Request;
class GetAllMailSendersController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
$mailSenders = MailSender::whereCompany()->get();
return MailSenderResource::collection($mailSenders);
}
}

View File

@ -1,98 +0,0 @@
<?php
namespace Crater\Http\Controllers\V1\Admin\MailSender;
use Crater\Http\Controllers\Controller;
use Crater\Http\Requests\MailSenderRequest;
use Crater\Http\Resources\MailSenderResource;
use Crater\Models\MailSender;
use Illuminate\Http\Request;
class MailSenderController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$this->authorize('viewAny', MailSender::class);
$limit = $request->has('limit') ? $request->limit : 10;
$mailSenders = MailSender::whereCompany()
->applyFilters($request->all())
->paginateData($limit);
return (MailSenderResource::collection($mailSenders))
->additional(['meta' => [
'mail_sender_total_count' => MailSender::whereCompany()->count(),
]]);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(MailSenderRequest $request)
{
$this->authorize('create', MailSender::class);
$mailSender = MailSender::createFromRequest($request);
return new MailSenderResource($mailSender);
}
/**
* Display the specified resource.
*
* @param \Crater\Models\SenderMail $senderMail
* @return \Illuminate\Http\Response
*/
public function show(MailSender $mailSender)
{
$this->authorize('view', $mailSender);
return new MailSenderResource($mailSender);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \Crater\Models\SenderMail $senderMail
* @return \Illuminate\Http\Response
*/
public function update(MailSenderRequest $request, MailSender $mailSender)
{
$this->authorize('update', $mailSender);
$mailSender->updateFromRequest($request);
return new MailSenderResource($mailSender);
}
/**
* Remove the specified resource from storage.
*
* @param \Crater\Models\SenderMail $senderMail
* @return \Illuminate\Http\Response
*/
public function destroy(MailSender $mailSender)
{
$this->authorize('delete', $mailSender);
if ($mailSender->is_default) {
return respondJson('You can\'t remove default mail sender.', 'You can\'t remove default mail sender.');
}
$mailSender->delete();
return response()->json([
'success' => true,
]);
}
}

View File

@ -3,18 +3,18 @@
namespace Crater\Http\Controllers\V1\Admin\Modules;
use Crater\Http\Controllers\Controller;
use Crater\Http\Requests\UnzipUpdateRequest;
use Crater\Space\ModuleInstaller;
use Illuminate\Http\Request;
class UnzipModuleController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Crater\Http\Requests\UnzipUpdateRequest $request
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(UnzipUpdateRequest $request)
public function __invoke(Request $request)
{
$this->authorize('manage modules');

View File

@ -3,18 +3,18 @@
namespace Crater\Http\Controllers\V1\Admin\Modules;
use Crater\Http\Controllers\Controller;
use Crater\Http\Requests\UploadModuleRequest;
use Crater\Space\ModuleInstaller;
use Illuminate\Http\Request;
class UploadModuleController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Crater\Http\Requests\UploadModuleRequest $request
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(UploadModuleRequest $request)
public function __invoke(Request $request)
{
$this->authorize('manage modules');

View File

@ -2,25 +2,24 @@
namespace Crater\Http\Controllers\V1\Admin\Report;
use PDF;
use Carbon\Carbon;
use Crater\Http\Controllers\Controller;
use Crater\Models\Company;
use Crater\Models\Currency;
use Crater\Models\CompanySetting;
use Crater\Models\Customer;
use Illuminate\Http\Request;
use Crater\Models\CompanySetting;
use Illuminate\Support\Facades\App;
use Crater\Http\Controllers\Controller;
use PDF;
class CustomerSalesReportController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $hash
* @return \Illuminate\Http\JsonResponse
*/
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $hash
* @return \Illuminate\Http\JsonResponse
*/
public function __invoke(Request $request, $hash)
{
$company = Company::where('unique_hash', $hash)->first();
@ -57,7 +56,6 @@ class CustomerSalesReportController extends Controller
$dateFormat = CompanySetting::getSetting('carbon_date_format', $company->id);
$from_date = Carbon::createFromFormat('Y-m-d', $request->from_date)->format($dateFormat);
$to_date = Carbon::createFromFormat('Y-m-d', $request->to_date)->format($dateFormat);
$currency = Currency::findOrFail(CompanySetting::getSetting('currency', $company->id));
$colors = [
'primary_text_color',
@ -82,7 +80,6 @@ class CustomerSalesReportController extends Controller
'company' => $company,
'from_date' => $from_date,
'to_date' => $to_date,
'currency' => $currency,
]);
$pdf = PDF::loadView('app.pdf.reports.sales-customers');

View File

@ -2,25 +2,24 @@
namespace Crater\Http\Controllers\V1\Admin\Report;
use PDF;
use Carbon\Carbon;
use Crater\Models\Company;
use Crater\Models\Expense;
use Crater\Models\Currency;
use Illuminate\Http\Request;
use Crater\Models\CompanySetting;
use Illuminate\Support\Facades\App;
use Crater\Http\Controllers\Controller;
use Crater\Models\Company;
use Crater\Models\CompanySetting;
use Crater\Models\Expense;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use PDF;
class ExpensesReportController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $hash
* @return \Illuminate\Http\JsonResponse
*/
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $hash
* @return \Illuminate\Http\JsonResponse
*/
public function __invoke(Request $request, $hash)
{
$company = Company::where('unique_hash', $hash)->first();
@ -44,7 +43,6 @@ class ExpensesReportController extends Controller
$dateFormat = CompanySetting::getSetting('carbon_date_format', $company->id);
$from_date = Carbon::createFromFormat('Y-m-d', $request->from_date)->format($dateFormat);
$to_date = Carbon::createFromFormat('Y-m-d', $request->to_date)->format($dateFormat);
$currency = Currency::findOrFail(CompanySetting::getSetting('currency', $company->id));
$colors = [
'primary_text_color',
@ -68,7 +66,6 @@ class ExpensesReportController extends Controller
'company' => $company,
'from_date' => $from_date,
'to_date' => $to_date,
'currency' => $currency,
]);
$pdf = PDF::loadView('app.pdf.reports.expenses');

View File

@ -2,25 +2,24 @@
namespace Crater\Http\Controllers\V1\Admin\Report;
use PDF;
use Carbon\Carbon;
use Crater\Models\Company;
use Crater\Models\Currency;
use Illuminate\Http\Request;
use Crater\Models\InvoiceItem;
use Crater\Models\CompanySetting;
use Illuminate\Support\Facades\App;
use Crater\Http\Controllers\Controller;
use Crater\Models\Company;
use Crater\Models\CompanySetting;
use Crater\Models\InvoiceItem;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use PDF;
class ItemSalesReportController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $hash
* @return \Illuminate\Http\JsonResponse
*/
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $hash
* @return \Illuminate\Http\JsonResponse
*/
public function __invoke(Request $request, $hash)
{
$company = Company::where('unique_hash', $hash)->first();
@ -44,7 +43,6 @@ class ItemSalesReportController extends Controller
$dateFormat = CompanySetting::getSetting('carbon_date_format', $company->id);
$from_date = Carbon::createFromFormat('Y-m-d', $request->from_date)->format($dateFormat);
$to_date = Carbon::createFromFormat('Y-m-d', $request->to_date)->format($dateFormat);
$currency = Currency::findOrFail(CompanySetting::getSetting('currency', $company->id));
$colors = [
'primary_text_color',
@ -68,7 +66,6 @@ class ItemSalesReportController extends Controller
'company' => $company,
'from_date' => $from_date,
'to_date' => $to_date,
'currency' => $currency,
]);
$pdf = PDF::loadView('app.pdf.reports.sales-items');

View File

@ -2,26 +2,25 @@
namespace Crater\Http\Controllers\V1\Admin\Report;
use PDF;
use Carbon\Carbon;
use Crater\Http\Controllers\Controller;
use Crater\Models\Company;
use Crater\Models\CompanySetting;
use Crater\Models\Expense;
use Crater\Models\Payment;
use Crater\Models\Currency;
use Illuminate\Http\Request;
use Crater\Models\CompanySetting;
use Illuminate\Support\Facades\App;
use Crater\Http\Controllers\Controller;
use PDF;
class ProfitLossReportController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $hash
* @return \Illuminate\Http\JsonResponse
*/
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $hash
* @return \Illuminate\Http\JsonResponse
*/
public function __invoke(Request $request, $hash)
{
$company = Company::where('unique_hash', $hash)->first();
@ -50,8 +49,6 @@ class ProfitLossReportController extends Controller
$dateFormat = CompanySetting::getSetting('carbon_date_format', $company->id);
$from_date = Carbon::createFromFormat('Y-m-d', $request->from_date)->format($dateFormat);
$to_date = Carbon::createFromFormat('Y-m-d', $request->to_date)->format($dateFormat);
$currency = Currency::findOrFail(CompanySetting::getSetting('currency', $company->id));
$colors = [
'primary_text_color',
@ -77,7 +74,6 @@ class ProfitLossReportController extends Controller
'company' => $company,
'from_date' => $from_date,
'to_date' => $to_date,
'currency' => $currency,
]);
$pdf = PDF::loadView('app.pdf.reports.profit-loss');

View File

@ -2,25 +2,24 @@
namespace Crater\Http\Controllers\V1\Admin\Report;
use PDF;
use Carbon\Carbon;
use Crater\Models\Tax;
use Crater\Models\Company;
use Crater\Models\Currency;
use Illuminate\Http\Request;
use Crater\Models\CompanySetting;
use Illuminate\Support\Facades\App;
use Crater\Http\Controllers\Controller;
use Crater\Models\Company;
use Crater\Models\CompanySetting;
use Crater\Models\Tax;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use PDF;
class TaxSummaryReportController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $hash
* @return \Illuminate\Http\JsonResponse
*/
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $hash
* @return \Illuminate\Http\JsonResponse
*/
public function __invoke(Request $request, $hash)
{
$company = Company::where('unique_hash', $hash)->first();
@ -45,8 +44,6 @@ class TaxSummaryReportController extends Controller
$dateFormat = CompanySetting::getSetting('carbon_date_format', $company->id);
$from_date = Carbon::createFromFormat('Y-m-d', $request->from_date)->format($dateFormat);
$to_date = Carbon::createFromFormat('Y-m-d', $request->to_date)->format($dateFormat);
$currency = Currency::findOrFail(CompanySetting::getSetting('currency', $company->id));
$colors = [
'primary_text_color',
@ -71,7 +68,6 @@ class TaxSummaryReportController extends Controller
'company' => $company,
'from_date' => $from_date,
'to_date' => $to_date,
'currency' => $currency,
]);
$pdf = PDF::loadView('app.pdf.reports.tax-summary');

View File

@ -3,29 +3,80 @@
namespace Crater\Http\Controllers\V1\Admin\Settings;
use Crater\Http\Controllers\Controller;
use Crater\Http\Requests\TestMailDriverRequest;
use Crater\Http\Requests\MailEnvironmentRequest;
use Crater\Mail\TestMail;
use Crater\Models\MailSender;
use Crater\Models\Setting;
use Crater\Space\EnvironmentManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Mail;
class MailConfigurationController extends Controller
{
public function TestMailDriver(TestMailDriverRequest $request)
/**
* @var EnvironmentManager
*/
protected $environmentManager;
/**
* @param EnvironmentManager $environmentManager
*/
public function __construct(EnvironmentManager $environmentManager)
{
$this->environmentManager = $environmentManager;
}
/**
*
* @param MailEnvironmentRequest $request
* @return JsonResponse
*/
public function saveMailEnvironment(MailEnvironmentRequest $request)
{
$this->authorize('manage email config');
MailSender::setMailConfiguration($request->mail_sender_id);
$setting = Setting::getSetting('profile_complete');
$results = $this->environmentManager->saveMailVariables($request);
Mail::to($request->to)->send(new TestMail($request->subject, $request->message));
if ($setting !== 'COMPLETED') {
Setting::setSetting('profile_complete', 4);
}
return response()->json([
'success' => true,
]);
return response()->json($results);
}
public function getMailDrivers(Request $request)
public function getMailEnvironment()
{
$this->authorize('manage email config');
$MailData = [
'mail_driver' => config('mail.driver'),
'mail_host' => config('mail.host'),
'mail_port' => config('mail.port'),
'mail_username' => config('mail.username'),
'mail_password' => config('mail.password'),
'mail_encryption' => config('mail.encryption'),
'from_name' => config('mail.from.name'),
'from_mail' => config('mail.from.address'),
'mail_mailgun_endpoint' => config('services.mailgun.endpoint'),
'mail_mailgun_domain' => config('services.mailgun.domain'),
'mail_mailgun_secret' => config('services.mailgun.secret'),
'mail_ses_key' => config('services.ses.key'),
'mail_ses_secret' => config('services.ses.secret'),
];
return response()->json($MailData);
}
/**
*
* @return JsonResponse
*/
public function getMailDrivers()
{
$this->authorize('manage email config');
$drivers = [
'smtp',
'mail',
@ -36,4 +87,21 @@ class MailConfigurationController extends Controller
return response()->json($drivers);
}
public function testEmailConfig(Request $request)
{
$this->authorize('manage email config');
$this->validate($request, [
'to' => 'required|email',
'subject' => 'required',
'message' => 'required',
]);
Mail::to($request->to)->send(new TestMail($request->subject, $request->message));
return response()->json([
'success' => true,
]);
}
}

View File

@ -9,7 +9,6 @@ use Crater\Models\CompanySetting;
use Crater\Models\Customer;
use Crater\Models\EmailLog;
use Crater\Models\Estimate;
use Crater\Models\MailSender;
use Illuminate\Http\Request;
class EstimatePdfController extends Controller
@ -28,16 +27,14 @@ class EstimatePdfController extends Controller
);
if ($notifyEstimateViewed == 'YES') {
$notificationEmail = CompanySetting::getSetting('notification_email', $estimate->company_id);
$mailSender = MailSender::where('company_id', $estimate->company_id)->where('is_default', true)->first();
MailSender::setMailConfiguration($mailSender->id);
$data['from_address'] = $mailSender->from_address;
$data['from_name'] = $mailSender->from_name;
$data['user'] = Customer::find($estimate->customer_id)->toArray();
$data['estimate'] = Estimate::findOrFail($estimate->id)->toArray();
$data['user'] = Customer::find($estimate->customer_id)->toArray();
$notificationEmail = CompanySetting::getSetting(
'notification_email',
$estimate->company_id
);
send_mail(new EstimateViewedMail($data), $mailSender, $notificationEmail);
\Mail::to($notificationEmail)->send(new EstimateViewedMail($data));
}
}

View File

@ -9,7 +9,6 @@ use Crater\Models\CompanySetting;
use Crater\Models\Customer;
use Crater\Models\EmailLog;
use Crater\Models\Invoice;
use Crater\Models\MailSender;
use Illuminate\Http\Request;
class InvoicePdfController extends Controller
@ -29,16 +28,14 @@ class InvoicePdfController extends Controller
);
if ($notifyInvoiceViewed == 'YES') {
$notificationEmail = CompanySetting::getSetting('notification_email', $invoice->company_id);
$mailSender = MailSender::where('company_id', $invoice->company_id)->where('is_default', true)->first();
MailSender::setMailConfiguration($mailSender->id);
$data['from_address'] = $mailSender->from_address;
$data['from_name'] = $mailSender->from_name;
$data['invoice'] = Invoice::findOrFail($invoice->id)->toArray();
$data['user'] = Customer::find($invoice->customer_id)->toArray();
$notificationEmail = CompanySetting::getSetting(
'notification_email',
$invoice->company_id
);
send_mail(new InvoiceViewedMail($data), $mailSender, $notificationEmail);
\Mail::to($notificationEmail)->send(new InvoiceViewedMail($data));
}
}

View File

@ -4,7 +4,6 @@ namespace Crater\Http\Middleware;
use Closure;
use Crater\Models\FileDisk;
use Crater\Models\MailSender;
class ConfigMiddleware
{
@ -29,12 +28,6 @@ class ConfigMiddleware
}
}
$default_mail_sender = MailSender::where('company_id', $request->header('company'))->where('is_default', true)->first();
if ($default_mail_sender) {
$default_mail_sender->setMailConfiguration($default_mail_sender->id);
}
return $next($request);
}
}

View File

@ -1,85 +0,0 @@
<?php
namespace Crater\Http\Requests;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;
class MailSenderRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$rules = [
'name' => [
'required',
Rule::unique('mail_senders')
->where('company_id', $this->header('company'))
],
'driver' => [
'required',
],
'is_default' => [
'nullable'
],
'bcc' => [
'nullable'
],
'cc' => [
'nullable'
],
'from_address' => [
'nullable'
],
'from_name' => [
'nullable'
],
'settings' => [
'nullable'
],
'settings.*' => [
'nullable'
]
];
if ($this->isMethod('PUT')) {
$rules['name'] = [
'nullable',
Rule::unique('mail_senders')
->ignore($this->route('mail_sender')->id)
->where('company_id', $this->header('company'))
];
}
return $rules;
}
public function getMailSenderPayload()
{
$data = $this->validated();
if ($data['settings'] && $data['settings']['encryption'] == 'none') {
$data['settings']['encryption'] = '';
}
return collect($data)
->merge([
'company_id' => $this->header('company'),
])
->toArray();
}
}

View File

@ -30,7 +30,7 @@ class SendEstimatesRequest extends FormRequest
'body' => [
'required',
],
'mail_sender_id' => [
'from' => [
'required',
],
'to' => [

View File

@ -30,7 +30,7 @@ class SendInvoiceRequest extends FormRequest
'subject' => [
'required',
],
'mail_sender_id' => [
'from' => [
'required',
],
'to' => [

View File

@ -30,7 +30,7 @@ class SendPaymentRequest extends FormRequest
'body' => [
'required',
],
'mail_sender_id' => [
'from' => [
'required',
],
'to' => [

View File

@ -1,39 +0,0 @@
<?php
namespace Crater\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class TestMailDriverRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'to' => [
'required',
'email'
],
'subject' => [
'required'
],
'message' => [
'required'
],
];
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace Crater\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UnzipUpdateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'path' => [
'required',
'regex:/^[\.\/\w\-]+$/'
],
'module' => [
'required',
'string'
]
];
}
}

View File

@ -25,7 +25,7 @@ class UploadExpenseReceiptRequest extends FormRequest
public function rules()
{
return [
'attachment_receipt' => [
'upload_receipt' => [
'nullable',
new Base64Mime(['gif', 'jpg', 'png'])
]

View File

@ -1,40 +0,0 @@
<?php
namespace Crater\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UploadModuleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'avatar' => [
'required',
'file',
'mimes:zip',
'max:20000'
],
'module' => [
'required',
'string',
'max:100'
]
];
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace Crater\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class MailSenderResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'driver' => $this->driver,
'is_default' => $this->is_default,
'bcc' => $this->bcc,
'cc' => $this->cc,
'from_address' => $this->from_address,
'from_name' => $this->from_name,
'company_id' => $this->company_id,
'settings' => $this->settings
];
}
}

View File

@ -30,7 +30,7 @@ class EstimateViewedMail extends Mailable
*/
public function build()
{
return $this->from($this->data['from_address'], $this->data['from_name'])
return $this->from(config('mail.from.address'), config('mail.from.name'))
->markdown('emails.viewed.estimate', ['data', $this->data]);
}
}

View File

@ -30,7 +30,7 @@ class InvoiceViewedMail extends Mailable
*/
public function build()
{
return $this->from($this->data['from_address'], $this->data['from_name'])
return $this->from(config('mail.from.address'), config('mail.from.name'))
->markdown('emails.viewed.invoice', ['data', $this->data]);
}
}

View File

@ -34,7 +34,7 @@ class SendEstimateMail extends Mailable
public function build()
{
$log = EmailLog::create([
'from' => $this->data['from_address'],
'from' => $this->data['from'],
'to' => $this->data['to'],
'subject' => $this->data['subject'],
'body' => $this->data['body'],
@ -47,10 +47,9 @@ class SendEstimateMail extends Mailable
$this->data['url'] = route('estimate', ['email_log' => $log->token]);
$mailContent = $this->from($this->data['from_address'], $this->data['from_name'])
->subject($this->data['subject'])
->markdown("emails.send.estimate", ['data', $this->data]);
$mailContent = $this->from($this->data['from'], config('mail.from.name'))
->subject($this->data['subject'])
->markdown('emails.send.estimate', ['data', $this->data]);
if ($this->data['attach']['data']) {
$mailContent->attachData(

View File

@ -34,7 +34,7 @@ class SendInvoiceMail extends Mailable
public function build()
{
$log = EmailLog::create([
'from' => $this->data['from_address'],
'from' => $this->data['from'],
'to' => $this->data['to'],
'subject' => $this->data['subject'],
'body' => $this->data['body'],
@ -47,9 +47,9 @@ class SendInvoiceMail extends Mailable
$this->data['url'] = route('invoice', ['email_log' => $log->token]);
$mailContent = $this->from($this->data['from_address'], $this->data['from_name'])
$mailContent = $this->from($this->data['from'], config('mail.from.name'))
->subject($this->data['subject'])
->markdown("emails.send.invoice", ['data', $this->data]);
->markdown('emails.send.invoice', ['data', $this->data]);
if ($this->data['attach']['data']) {
$mailContent->attachData(

View File

@ -34,7 +34,7 @@ class SendPaymentMail extends Mailable
public function build()
{
$log = EmailLog::create([
'from' => $this->data['from_address'],
'from' => $this->data['from'],
'to' => $this->data['to'],
'subject' => $this->data['subject'],
'body' => $this->data['body'],
@ -47,9 +47,9 @@ class SendPaymentMail extends Mailable
$this->data['url'] = route('payment', ['email_log' => $log->token]);
$mailContent = $this->from($this->data['from_address'], $this->data['from_name'])
->subject($this->data['subject'])
->markdown("emails.send.payment", ['data', $this->data]);
$mailContent = $this->from($this->data['from'], config('mail.from.name'))
->subject($this->data['subject'])
->markdown('emails.send.payment', ['data', $this->data]);
if ($this->data['attach']['data']) {
$mailContent->attachData(

View File

@ -5,10 +5,10 @@ namespace Crater\Models;
use App;
use Barryvdh\DomPDF\Facade as PDF;
use Carbon\Carbon;
use Crater\Mail\SendEstimateMail;
use Crater\Services\SerialNumberFormatter;
use Crater\Traits\GeneratesPdfTrait;
use Crater\Traits\HasCustomFieldsTrait;
use Crater\Traits\MailTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
@ -20,7 +20,6 @@ use Vinkla\Hashids\Facades\Hashids;
class Estimate extends Model implements HasMedia
{
use HasFactory;
use MailTrait;
use InteractsWithMedia;
use GeneratesPdfTrait;
use HasCustomFieldsTrait;
@ -364,7 +363,7 @@ class Estimate extends Model implements HasMedia
$this->save();
}
$this->setMail('estimate', $data);
\Mail::to($data['to'])->send(new SendEstimateMail($data));
return [
'success' => true,

View File

@ -9,7 +9,6 @@ use Crater\Mail\SendInvoiceMail;
use Crater\Services\SerialNumberFormatter;
use Crater\Traits\GeneratesPdfTrait;
use Crater\Traits\HasCustomFieldsTrait;
use Crater\Traits\MailTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
@ -22,7 +21,6 @@ use Vinkla\Hashids\Facades\Hashids;
class Invoice extends Model implements HasMedia
{
use HasFactory;
use MailTrait;
use InteractsWithMedia;
use GeneratesPdfTrait;
use HasCustomFieldsTrait;
@ -445,8 +443,7 @@ class Invoice extends Model implements HasMedia
$data['invoice'] = $this->toArray();
$data['customer'] = $this->customer->toArray();
$data['company'] = Company::find($this->company_id);
$data['subject'] = $this->getEmailString($data['subject']);
$data['body'] = $this->getEmailString($data['body']);
$data['body'] = $this->getEmailBody($data['body']);
$data['attach']['data'] = ($this->getEmailAttachmentSetting()) ? $this->getPDFData() : null;
return $data;
@ -466,7 +463,7 @@ class Invoice extends Model implements HasMedia
{
$data = $this->sendInvoiceData($data);
$this->setMail('invoice', $data);
\Mail::to($data['to'])->send(new SendInvoiceMail($data));
if ($this->status == Invoice::STATUS_DRAFT) {
$this->status = Invoice::STATUS_SENT;
@ -638,7 +635,7 @@ class Invoice extends Model implements HasMedia
return $this->getFormattedString($this->notes);
}
public function getEmailString($body)
public function getEmailBody($body)
{
$values = array_merge($this->getFieldsArray(), $this->getExtraFields());

View File

@ -1,111 +0,0 @@
<?php
namespace Crater\Models;
use Crater\Http\Requests\MailSenderRequest;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Config;
class MailSender extends Model
{
use HasFactory;
protected $guarded = [
'id'
];
protected $casts = [
'settings' => 'array',
'is_default' => 'boolean'
];
public function company()
{
return $this->belongsTo(Company::class);
}
public function scopeWhereOrder($query, $orderByField, $orderBy)
{
$query->orderBy($orderByField, $orderBy);
}
public function scopeApplyFilters($query, array $filters)
{
$filters = collect($filters);
if ($filters->get('orderByField') || $filters->get('orderBy')) {
$field = $filters->get('orderByField') ? $filters->get('orderByField') : 'name';
$orderBy = $filters->get('orderBy') ? $filters->get('orderBy') : 'desc';
$query->whereOrder($field, $orderBy);
}
}
public function scopePaginateData($query, $limit)
{
if ($limit == 'all') {
return $query->get();
}
return $query->paginate($limit);
}
public function scopeWhereCompany($query)
{
$query->where('mail_senders.company_id', request()->header('company'));
}
public static function createFromRequest(MailSenderRequest $request)
{
$senderMail = self::create($request->getMailSenderPayload());
if ($request->is_default) {
$senderMail->removeOtherDefaultMailSenders($request);
}
return $senderMail;
}
public function updateFromRequest(MailSenderRequest $request)
{
$data = $request->getMailSenderPayload();
$this->update($data);
if ($request->is_default) {
$this->removeOtherDefaultMailSenders($request);
}
return $this;
}
public static function setMailConfiguration($id, $check = null)
{
$mailSender = MailSender::find($id);
$settings = $mailSender->settings;
$settings['driver'] = $mailSender->driver;
$settings['from'] = [
'address' => $mailSender->from_address,
'name' => $mailSender->from_name
];
$settings['sendmail'] = config('mail.sendmail');
$settings['markdown'] = config('mail.markdown');
$settings['log_channel'] = config('mail.log_channel');
Config::set('mail', $settings);
if ($check) {
return $mailSender;
}
return true;
}
public function removeOtherDefaultMailSenders($request) {
MailSender::where('company_id', $request->header('company'))
->where('is_default', true)
->where('id', '<>', $this->id)
->update(['is_default' => false]);
}
}

View File

@ -5,10 +5,10 @@ namespace Crater\Models;
use Barryvdh\DomPDF\Facade as PDF;
use Carbon\Carbon;
use Crater\Jobs\GeneratePaymentPdfJob;
use Crater\Mail\SendPaymentMail;
use Crater\Services\SerialNumberFormatter;
use Crater\Traits\GeneratesPdfTrait;
use Crater\Traits\HasCustomFieldsTrait;
use Crater\Traits\MailTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
@ -18,7 +18,6 @@ use Vinkla\Hashids\Facades\Hashids;
class Payment extends Model implements HasMedia
{
use HasFactory;
use MailTrait;
use InteractsWithMedia;
use GeneratesPdfTrait;
use HasCustomFieldsTrait;
@ -136,7 +135,7 @@ class Payment extends Model implements HasMedia
{
$data = $this->sendPaymentData($data);
$this->setMail('payment', $data);
\Mail::to($data['to'])->send(new SendPaymentMail($data));
return [
'success' => true,

View File

@ -1,123 +0,0 @@
<?php
namespace Crater\Policies;
use Crater\Models\MailSender;
use Crater\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Silber\Bouncer\BouncerFacade;
class MailSenderPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @param \Crater\Models\User $user
* @return \Illuminate\Auth\Access\Response|bool
*/
public function viewAny(User $user)
{
if (BouncerFacade::can('view-mail-sender', MailSender::class)) {
return true;
}
return false;
}
/**
* Determine whether the user can view the model.
*
* @param \Crater\Models\User $user
* @param \Crater\Models\MailSender $mailSender
* @return \Illuminate\Auth\Access\Response|bool
*/
public function view(User $user, MailSender $mailSender)
{
if (BouncerFacade::can('view-mail-sender', $mailSender) && $user->hasCompany($mailSender->company_id)) {
return true;
}
return false;
}
/**
* Determine whether the user can create models.
*
* @param \Crater\Models\User $user
* @return \Illuminate\Auth\Access\Response|bool
*/
public function create(User $user)
{
if (BouncerFacade::can('create-mail-sender', MailSender::class)) {
return true;
}
return false;
}
/**
* Determine whether the user can update the model.
*
* @param \Crater\Models\User $user
* @param \Crater\Models\MailSender $mailSender
* @return \Illuminate\Auth\Access\Response|bool
*/
public function update(User $user, MailSender $mailSender)
{
if (BouncerFacade::can('edit-mail-sender', $mailSender) && $user->hasCompany($mailSender->company_id)) {
return true;
}
return false;
}
/**
* Determine whether the user can delete the model.
*
* @param \Crater\Models\User $user
* @param \Crater\Models\MailSender $mailSender
* @return \Illuminate\Auth\Access\Response|bool
*/
public function delete(User $user, MailSender $mailSender)
{
if (BouncerFacade::can('delete-mail-sender', $mailSender) && $user->hasCompany($mailSender->company_id)) {
return true;
}
return false;
}
/**
* Determine whether the user can restore the model.
*
* @param \Crater\Models\User $user
* @param \Crater\Models\MailSender $mailSender
* @return \Illuminate\Auth\Access\Response|bool
*/
public function restore(User $user, MailSender $mailSender)
{
if (BouncerFacade::can('delete-mail-sender', $mailSender) && $user->hasCompany($mailSender->company_id)) {
return true;
}
return false;
}
/**
* Determine whether the user can permanently delete the model.
*
* @param \Crater\Models\User $user
* @param \Crater\Models\MailSender $mailSender
* @return \Illuminate\Auth\Access\Response|bool
*/
public function forceDelete(User $user, MailSender $mailSender)
{
if (BouncerFacade::can('delete-mail-sender', $mailSender) && $user->hasCompany($mailSender->company_id)) {
return true;
}
return false;
}
}

View File

@ -39,7 +39,6 @@ class AuthServiceProvider extends ServiceProvider
\Crater\Models\CustomField::class => \Crater\Policies\CustomFieldPolicy::class,
\Crater\Models\User::class => \Crater\Policies\UserPolicy::class,
\Crater\Models\Item::class => \Crater\Policies\ItemPolicy::class,
\Crater\Models\MailSender::class => \Crater\Policies\MailSenderPolicy::class,
\Silber\Bouncer\Database\Role::class => \Crater\Policies\RolePolicy::class,
\Crater\Models\Unit::class => \Crater\Policies\UnitPolicy::class,
\Crater\Models\RecurringInvoice::class => \Crater\Policies\RecurringInvoicePolicy::class,

View File

@ -223,6 +223,204 @@ class EnvironmentManager
return false;
}
/**
* Save the mail content to the .env file.
*
* @param Request $request
* @return array
*/
public function saveMailVariables(MailEnvironmentRequest $request)
{
$mailData = $this->getMailData($request);
try {
file_put_contents($this->envPath, str_replace(
$mailData['old_mail_data'],
$mailData['new_mail_data'],
file_get_contents($this->envPath)
));
if ($mailData['extra_old_mail_data']) {
file_put_contents($this->envPath, str_replace(
$mailData['extra_old_mail_data'],
$mailData['extra_mail_data'],
file_get_contents($this->envPath)
));
} else {
file_put_contents(
$this->envPath,
"\n".$mailData['extra_mail_data'],
FILE_APPEND
);
}
} catch (Exception $e) {
return [
'error' => 'mail_variables_save_error',
];
}
return [
'success' => 'mail_variables_save_successfully',
];
}
private function getMailData($request)
{
$mailFromCredential = "";
$extraMailData = "";
$extraOldMailData = "";
$oldMailData = "";
$newMailData = "";
if (env('MAIL_FROM_ADDRESS') !== null && env('MAIL_FROM_NAME') !== null) {
$mailFromCredential =
'MAIL_FROM_ADDRESS='.config('mail.from.address')."\n".
'MAIL_FROM_NAME="'.config('mail.from.name')."\"\n\n";
}
switch ($request->mail_driver) {
case 'smtp':
$oldMailData =
'MAIL_DRIVER='.config('mail.driver')."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
$mailFromCredential;
$newMailData =
'MAIL_DRIVER='.$request->mail_driver."\n".
'MAIL_HOST='.$request->mail_host."\n".
'MAIL_PORT='.$request->mail_port."\n".
'MAIL_USERNAME='.$request->mail_username."\n".
'MAIL_PASSWORD='.$request->mail_password."\n".
'MAIL_ENCRYPTION='.$request->mail_encryption."\n\n".
'MAIL_FROM_ADDRESS='.$request->from_mail."\n".
'MAIL_FROM_NAME="'.$request->from_name."\"\n\n";
break;
case 'mailgun':
$oldMailData =
'MAIL_DRIVER='.config('mail.driver')."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
$mailFromCredential;
$newMailData =
'MAIL_DRIVER='.$request->mail_driver."\n".
'MAIL_HOST='.$request->mail_host."\n".
'MAIL_PORT='.$request->mail_port."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.$request->mail_encryption."\n\n".
'MAIL_FROM_ADDRESS='.$request->from_mail."\n".
'MAIL_FROM_NAME="'.$request->from_name."\"\n\n";
$extraMailData =
'MAILGUN_DOMAIN='.$request->mail_mailgun_domain."\n".
'MAILGUN_SECRET='.$request->mail_mailgun_secret."\n".
'MAILGUN_ENDPOINT='.$request->mail_mailgun_endpoint."\n";
if (env('MAILGUN_DOMAIN') !== null && env('MAILGUN_SECRET') !== null && env('MAILGUN_ENDPOINT') !== null) {
$extraOldMailData =
'MAILGUN_DOMAIN='.config('services.mailgun.domain')."\n".
'MAILGUN_SECRET='.config('services.mailgun.secret')."\n".
'MAILGUN_ENDPOINT='.config('services.mailgun.endpoint')."\n";
}
break;
case 'ses':
$oldMailData =
'MAIL_DRIVER='.config('mail.driver')."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
$mailFromCredential;
$newMailData =
'MAIL_DRIVER='.$request->mail_driver."\n".
'MAIL_HOST='.$request->mail_host."\n".
'MAIL_PORT='.$request->mail_port."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.$request->mail_encryption."\n\n".
'MAIL_FROM_ADDRESS='.$request->from_mail."\n".
'MAIL_FROM_NAME="'.$request->from_name."\"\n\n";
$extraMailData =
'SES_KEY='.$request->mail_ses_key."\n".
'SES_SECRET='.$request->mail_ses_secret."\n";
if (env('SES_KEY') !== null && env('SES_SECRET') !== null) {
$extraOldMailData =
'SES_KEY='.config('services.ses.key')."\n".
'SES_SECRET='.config('services.ses.secret')."\n";
}
break;
case 'mail':
$oldMailData =
'MAIL_DRIVER='.config('mail.driver')."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
$mailFromCredential;
$newMailData =
'MAIL_DRIVER='.$request->mail_driver."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
'MAIL_FROM_ADDRESS='.$request->from_mail."\n".
'MAIL_FROM_NAME="'.$request->from_name."\"\n\n";
break;
case 'sendmail':
$oldMailData =
'MAIL_DRIVER='.config('mail.driver')."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
$mailFromCredential;
$newMailData =
'MAIL_DRIVER='.$request->mail_driver."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
'MAIL_FROM_ADDRESS='.$request->from_mail."\n".
'MAIL_FROM_NAME="'.$request->from_name."\"\n\n";
break;
}
return [
'old_mail_data' => $oldMailData,
'new_mail_data' => $newMailData,
'extra_mail_data' => $extraMailData,
'extra_old_mail_data' => $extraOldMailData,
];
}
/**
* Save the disk content to the .env file.
*

View File

@ -5,7 +5,6 @@ use Crater\Models\Currency;
use Crater\Models\CustomField;
use Crater\Models\Setting;
use Illuminate\Support\Str;
use Illuminate\Mail\Mailable;
/**
* Get company setting
@ -71,42 +70,6 @@ function set_active($path, $active = 'active')
return call_user_func_array('Request::is', (array)$path) ? $active : '';
}
/**
* Send Mail
*
* @param Mailable $mailable
* @param object $mailSender
* @return string $to
*/
function send_mail(Mailable $mailable, object $mailSender = null, string $to)
{
if ($mailSender->bcc && $mailSender->cc) {
\Mail::to($to)
->bcc(explode(',', $mailSender->bcc))
->cc(explode(',', $mailSender->cc))
->send($mailable);
}
if ($mailSender->bcc && $mailSender->cc == null) {
\Mail::to($to)
->bcc(explode(',', $mailSender->bcc))
->send($mailable);
}
if ($mailSender->bcc == null && $mailSender->cc) {
\Mail::to($to)
->cc(explode(',', $mailSender->cc))
->send($mailable);
}
if ($mailSender->bcc == null && $mailSender->cc == null) {
\Mail::to($to)
->send($mailable);
}
return true;
}
/**
* @param $path
* @return mixed

View File

@ -1,40 +0,0 @@
<?php
namespace Crater\Traits;
use Crater\Mail\EstimateViewedMail;
use Crater\Mail\InvoiceViewedMail;
use Crater\Mail\SendEstimateMail;
use Crater\Mail\SendInvoiceMail;
use Crater\Mail\SendPaymentMail;
use Crater\Models\MailSender;
trait MailTrait
{
public function setMail($model, $data)
{
$mailSender = MailSender::setMailConfiguration($data['mail_sender_id'], true);
$data['from_address'] = $mailSender->from_address;
$data['from_name'] = $mailSender->from_name;
switch ($model) {
case 'invoice':
send_mail(new SendInvoiceMail($data), $mailSender, $data['to']);
break;
case 'estimate':
send_mail(new SendEstimateMail($data), $mailSender, $data['to']);
break;
case 'payment':
send_mail(new SendPaymentMail($data), $mailSender, $data['to']);
break;
}
return true;
}
}

View File

@ -38,15 +38,15 @@
"barryvdh/laravel-ide-helper": "^2.6",
"beyondcode/laravel-dump-server": "^1.0",
"facade/ignition": "^2.3.6",
"friendsofphp/php-cs-fixer": "^3.8",
"fakerphp/faker": "^1.9.1",
"friendsofphp/php-cs-fixer": "^3.0",
"fzaninotto/faker": "^1.9.1",
"mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^5.0",
"pestphp/pest": "^1.0",
"pestphp/pest-plugin-faker": "^1.0",
"pestphp/pest-plugin-laravel": "^1.0",
"pestphp/pest-plugin-parallel": "^0.2.1",
"phpunit/phpunit": "^9.3"
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
@ -81,14 +81,11 @@
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
"sort-packages": true
},
"extra": {
"laravel": {
"dont-discover": []
}
}
}
}

2361
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@ use Crater\Models\ExchangeRateProvider;
use Crater\Models\Expense;
use Crater\Models\Invoice;
use Crater\Models\Item;
use Crater\Models\MailSender;
use Crater\Models\Note;
use Crater\Models\Payment;
use Crater\Models\RecurringInvoice;
@ -398,41 +397,6 @@ return [
]
],
// Mail Sender
[
"name" => "view mail sender",
"ability" => "view-mail-sender",
"model" => MailSender::class,
'owner_only' => false,
],
[
"name" => "create mail sender",
"ability" => "create-mail-sender",
"model" => MailSender::class,
'owner_only' => false,
"depends_on" => [
'view-mail-sender',
]
],
[
"name" => "edit mail sender",
"ability" => "edit-mail-sender",
"model" => MailSender::class,
'owner_only' => false,
"depends_on" => [
'view-mail-sender',
]
],
[
"name" => "delete mail sender",
"ability" => "delete-mail-sender",
"model" => MailSender::class,
'owner_only' => false,
"depends_on" => [
'view-mail-sender',
]
],
// Settings
[
"name" => "view company dashboard",

View File

@ -7,7 +7,6 @@ use Crater\Models\ExchangeRateProvider;
use Crater\Models\Expense;
use Crater\Models\Invoice;
use Crater\Models\Item;
use Crater\Models\MailSender;
use Crater\Models\Note;
use Crater\Models\Payment;
use Crater\Models\RecurringInvoice;
@ -72,7 +71,6 @@ return [
["code" => "cs", "name" => "Czech"],
["code" => "el", "name" => "Greek"],
["code" => "hr", "name" => "Crotian"],
["code" => "th", "name" => "ไทย"],
],
/*
@ -226,17 +224,6 @@ return [
'ability' => 'view-all-notes',
'model' => Note::class
],
[
'title' => 'settings.menu_title.mail_sender',
'group' => '',
'name' => 'Mail Sender',
'link' => '/admin/settings/mail-sender',
'icon' => 'MailIcon',
'owner_only' => false,
'ability' => 'view-mail-sender',
'model' => MailSender::class
],
[
'title' => 'settings.menu_title.expense_category',
'group' => '',
@ -247,6 +234,16 @@ return [
'ability' => 'view-expense',
'model' => Expense::class
],
[
'title' => 'settings.mail.mail_config',
'group' => '',
'name' => 'Mail Configuration',
'link' => '/admin/settings/mail-configuration',
'icon' => 'MailIcon',
'owner_only' => true,
'ability' => '',
'model' => ''
],
[
'title' => 'settings.menu_title.file_disk',
'group' => '',
@ -277,7 +274,6 @@ return [
'ability' => '',
'model' => ''
],
],
/*

View File

@ -27,7 +27,6 @@ return [
'tokenizer',
'JSON',
'cURL',
'zip',
],
'apache' => [
'mod_rewrite',

View File

@ -1,106 +0,0 @@
<?php
use Crater\Models\Company;
use Crater\Models\MailSender;
use Crater\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Silber\Bouncer\BouncerFacade;
class CreateMailSendersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('mail_senders', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('driver');
$table->boolean('is_default')->default(false);
$table->string('bcc')->nullable();
$table->string('cc')->nullable();
$table->string('from_address')->nullable();
$table->string('from_name')->nullable();
$table->json('settings')->nullable();
$table->integer('company_id')->unsigned()->nullable();
$table->foreign('company_id')->references('id')->on('companies');
$table->timestamps();
});
$users = User::where('role', 'super admin')->get();
foreach ($users as $user) {
BouncerFacade::allow($user)->toManage(MailSender::class);
}
$companies = Company::all();
$companies->map(function ($company) {
if (env('MAIL_DRIVER') == 'smtp') {
$settings = [
'MAIL_HOST' => env('MAIL_HOST'),
'MAIL_PORT' => env('MAIL_PORT'),
'MAIL_USERNAME' => env('MAIL_USERNAME'),
'MAIL_PASSWORD' => env('MAIL_PASSWORD'),
'MAIL_ENCRYPTION' => env('MAIL_ENCRYPTION')
];
$this->createSender($settings, $company->id);
}
if (env('MAIL_DRIVER') == 'mail' || env('MAIL_DRIVER') == 'sendmail') {
$this->createSender(null, $company->id);
}
if (env('MAIL_DRIVER') == 'mailgun') {
$settings = [
'MAILGUN_DOMAIN' => env('MAILGUN_DOMAIN'),
'MAILGUN_SECRET' => env('MAILGUN_SECRET'),
'MAILGUN_ENDPOINT' => env('MAILGUN_ENDPOINT'),
];
$this->createSender($settings, $company->id);
}
if (env('MAIL_DRIVER') == 'ses') {
$settings = [
'MAIL_HOST' => env('MAIL_HOST'),
'MAIL_PORT' => env('MAIL_PORT'),
'MAIL_ENCRYPTION' => env('MAIL_ENCRYPTION'),
'MAILGUN_DOMAIN' => env('MAILGUN_DOMAIN'),
'SES_KEY' => env('SES_KEY'),
'SES_SECRET' => env('SES_SECRET'),
];
$this->createSender($settings, $company->id);
}
});
}
public function createSender($settings, $company_id)
{
$data = [
'name' => env('MAIL_DRIVER'),
'driver' => env('MAIL_DRIVER'),
'is_default' => true,
'from_address' => env('MAIL_FROM_ADDRESS'),
'from_name' => env('MAIL_FROM_NAME'),
'settings' => $settings ?? null,
'company_id' => $company_id
];
MailSender::create($data);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('mail_senders');
}
}

View File

@ -170,7 +170,7 @@ class CountriesTableSeeder extends Seeder
['id' => 152,'code' => 'NR','name' => "Nauru",'phonecode' => 674],
['id' => 153,'code' => 'NP','name' => "Nepal",'phonecode' => 977],
['id' => 154,'code' => 'AN','name' => "Netherlands Antilles",'phonecode' => 599],
['id' => 155,'code' => 'NL','name' => "Netherlands",'phonecode' => 31],
['id' => 155,'code' => 'NL','name' => "Netherlands The",'phonecode' => 31],
['id' => 156,'code' => 'NC','name' => "New Caledonia",'phonecode' => 687],
['id' => 157,'code' => 'NZ','name' => "New Zealand",'phonecode' => 64],
['id' => 158,'code' => 'NI','name' => "Nicaragua",'phonecode' => 505],

View File

@ -275,14 +275,6 @@ class CurrenciesTableSeeder extends Seeder
'thousand_separator' => '.',
'decimal_separator' => ',',
],
[
'name' => 'Central African Franc',
'code' => 'XAF',
'symbol' => 'CFA ',
'precision' => '2',
'thousand_separator' => ',',
'decimal_separator' => '.',
],
[
'name' => 'West African Franc',
'code' => 'XOF',

View File

@ -1,25 +1,22 @@
version: '3'
services:
app:
build:
args:
user: crater-user
uid: 1000
context: ./
dockerfile: Dockerfile
image: crater-php
build: .
image: craterapp/crater
restart: unless-stopped
working_dir: /var/www/
volumes:
- ./:/var/www
- ./docker-compose/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:rw,delegated
- ./:/var/www:z
labels:
ofelia.enabled: "true"
ofelia.job-exec.somecron.schedule: "@every 60s"
ofelia.job-exec.somecron.command: "php artisan schedule:run"
networks:
- crater
db:
image: mariadb
restart: always
restart: unless-stopped
volumes:
- db:/var/lib/mysql
# If you want to persist data on the host, comment the line above this one...
@ -41,19 +38,19 @@ services:
ports:
- 80:80
volumes:
- ./:/var/www
- ./docker-compose/nginx:/etc/nginx/conf.d/
- ./:/var/www:z
- ./docker-compose/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- crater
cron:
build:
context: ./
dockerfile: ./docker-compose/cron.dockerfile
ofelia:
image: mcuadros/ofelia
restart: unless-stopped
command: daemon --docker
volumes:
- ./:/var/www
networks:
- crater
- /var/run/docker.sock:/var/run/docker.sock:ro
depends_on:
- app
volumes:
db:

View File

@ -1,10 +0,0 @@
FROM php:8.0-fpm-alpine
RUN apk add --no-cache \
php8-bcmath
RUN docker-php-ext-install pdo pdo_mysql bcmath
COPY docker-compose/crontab /etc/crontabs/root
CMD ["crond", "-f"]

View File

@ -1 +0,0 @@
* * * * * cd /var/www && php artisan schedule:run >> /dev/stdout 2>&1

View File

@ -1,6 +0,0 @@
#!/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

16
docker-compose/startup.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/sh
chmod 775 /var/www/ -R
chown 1000:33 /var/www -R
if [ ! -f ".env" ]; then
cp .env.example .env
echo "created .env from .env.example"
fi
composer install --no-interaction --prefer-dist --optimize-autoloader
php artisan storage:link || true
php artisan key:generate
php-fpm

View File

@ -47,6 +47,8 @@ const ExpenseCategory = () =>
import('@/scripts/admin/views/settings/ExpenseCategorySetting.vue')
const ExchangeRateSetting = () =>
import('@/scripts/admin/views/settings/ExchangeRateProviderSetting.vue')
const MailConfig = () =>
import('@/scripts/admin/views/settings/MailConfigSetting.vue')
const FileDisk = () =>
import('@/scripts/admin/views/settings/FileDiskSetting.vue')
const Backup = () => import('@/scripts/admin/views/settings/BackupSetting.vue')
@ -54,8 +56,6 @@ const UpdateApp = () =>
import('@/scripts/admin/views/settings/UpdateAppSetting.vue')
const RolesSettings = () =>
import('@/scripts/admin/views/settings/RolesSettings.vue')
const MailSender = () =>
import('@/scripts/admin/views/settings/mail-sender/Index.vue')
// Items
const ItemsIndex = () => import('@/scripts/admin/views/items/Index.vue')
@ -302,6 +302,13 @@ export default [
meta: { ability: abilities.VIEW_EXPENSE },
component: ExpenseCategory,
},
{
path: 'mail-configuration',
name: 'mailconfig',
meta: { isOwner: true },
component: MailConfig,
},
{
path: 'file-disk',
name: 'file-disk',
@ -320,13 +327,6 @@ export default [
meta: { isOwner: true },
component: UpdateApp,
},
{
path: 'mail-sender',
name: 'mailsender',
meta: { ability: abilities.VIEW_MAIL_SENDER },
component: MailSender,
},
],
},

View File

@ -1,123 +0,0 @@
<template>
<!-- warning alert -->
<div
v-if="type == 'warning'"
class="rounded-md p-4 m-5"
:class="{
'bg-yellow-50': type == 'warning',
'bg-blue-50': type == 'info',
'bg-red-50': type == 'error',
'bg-green-50': type == 'success',
}"
>
<div class="flex">
<div class="flex-shrink-0">
<!-- Heroicon name: solid/exclamation -->
<svg
v-if="type == 'warning'"
class="h-5 w-5 text-yellow-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
<!-- Heroicon name: solid/information-circle -->
<svg
v-else-if="type == 'info'"
class="h-5 w-5 text-blue-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
/>
</svg>
<!-- Heroicon name: solid/x-circle -->
<svg
v-else-if="type == 'error'"
class="h-5 w-5 text-red-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
<!-- Heroicon name: solid/check-circle -->
<svg
v-else
class="h-5 w-5 text-green-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
<h3
class="text-sm font-medium"
:class="{
'text-yellow-800': type == 'warning',
'text-blue-800': type == 'info',
'text-red-800': type == 'error',
'text-green-800': type == 'success',
}"
>
{{ title }}
</h3>
<div
class="text-sm"
:class="{
'text-yellow-700': type == 'warning',
'text-blue-700': type == 'info',
'text-red-700': type == 'error',
'text-green-700': type == 'success',
}"
>
<p>{{ description }}</p>
</div>
<slot name="action"></slot>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
title: {
type: String,
default: null,
required: false,
},
description: {
type: String,
default: null,
required: false,
},
type: {
type: String,
default: 'success',
required: true,
},
})
</script>

View File

@ -1,111 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton v-if="route.name === 'mailsender.view'" variant="primary">
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
</template>
<!-- edit mail-sender -->
<BaseDropdownItem @click="editMailSender(row.id)">
<BaseIcon
name="PencilIcon"
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
/>
{{ $t('general.edit') }}
</BaseDropdownItem>
<!-- send test mail-sender -->
<BaseDropdownItem @click="openMailSenderTestModal(row.id)">
<BaseIcon
name="PaperAirplaneIcon"
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
/>
{{ $t('general.send_test_mail') }}
</BaseDropdownItem>
<!-- delete mail-sender -->
<BaseDropdownItem v-if="!row.is_default" @click="removeMailSender(row.id)">
<BaseIcon
name="TrashIcon"
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
/>
{{ $t('general.delete') }}
</BaseDropdownItem>
</BaseDropdown>
</template>
<script setup>
import { useDialogStore } from '@/scripts/stores/dialog'
import { useI18n } from 'vue-i18n'
import { useMailSenderStore } from '@/scripts/admin/stores/mail-sender'
import { useRoute, useRouter } from 'vue-router'
import { inject } from 'vue'
import { useModalStore } from '@/scripts/stores/modal'
const props = defineProps({
row: {
type: Object,
default: null,
},
table: {
type: Object,
default: null,
},
loadData: {
type: Function,
default: null,
},
})
const pre_t = 'settings.mail_sender'
const dialogStore = useDialogStore()
const { t } = useI18n()
const mailSenderStore = useMailSenderStore()
const route = useRoute()
const modalStore = useModalStore()
async function editMailSender(id) {
await mailSenderStore.fetchMailSender(id)
modalStore.openModal({
title: t(`${pre_t}.edit_mail_sender`),
componentName: 'MailSenderModal',
size: 'md',
refreshData: props.loadData && props.loadData,
})
}
function removeMailSender(id) {
dialogStore
.openDialog({
title: t('general.are_you_sure'),
message: t(`${pre_t}.confirm_delete`),
yesLabel: t('general.ok'),
noLabel: t('general.cancel'),
variant: 'danger',
hideNoButton: false,
size: 'lg',
})
.then(async (res) => {
if (res) {
let response = await mailSenderStore.deleteMailSender(id)
if (response.data.success) {
props.loadData && props.loadData()
return true
}
props.loadData && props.loadData()
}
})
}
async function openMailSenderTestModal(id) {
modalStore.openModal({
title: t(`general.send_test_mail`),
componentName: 'MailSenderTestModal',
size: 'md',
id: id,
refreshData: props.loadData && props.loadData,
})
}
</script>

View File

@ -17,7 +17,18 @@
<td class="px-5 py-4 text-left align-top">
<div class="flex justify-start">
<div
class="flex items-center justify-center w-5 h-5 mt-2 mr-2 text-gray-300 cursor-move handle"
class="
flex
items-center
justify-center
w-5
h-5
mt-2
text-gray-300
cursor-move
handle
mr-2
"
>
<DragIcon />
</div>
@ -97,7 +108,7 @@
<BaseIcon
name="ChevronDownIcon"
class="w-4 h-4 ml-1 text-gray-500"
class="w-4 h-4 text-gray-500 ml-1"
/>
</span>
</BaseButton>
@ -144,7 +155,7 @@
<BaseContentPlaceholders v-if="loading">
<BaseContentPlaceholdersText
:lines="1"
class="w-24 h-8 border rounded-md"
class="w-24 h-8 rounded-md border"
/>
</BaseContentPlaceholders>
@ -164,7 +175,6 @@
:ability="abilities.CREATE_INVOICE"
:store="store"
:store-prop="storeProp"
:discount="discount"
@update="updateTax"
/>
</td>

View File

@ -30,13 +30,24 @@
<template v-if="userStore.hasAbilities(ability)" #action>
<button
type="button"
class="flex items-center justify-center w-full px-2 py-2 bg-gray-200 border-none outline-none cursor-pointer "
class="
flex
items-center
justify-center
w-full
px-2
cursor-pointer
py-2
bg-gray-200
border-none
outline-none
"
@click="openTaxModal"
>
<BaseIcon name="CheckCircleIcon" class="h-5 text-primary-400" />
<label
class="ml-2 text-sm leading-none cursor-pointer text-primary-400"
class="ml-2 text-sm leading-none text-primary-400 cursor-pointer"
>{{ $t('invoices.add_new_tax') }}</label
>
</button>
@ -104,10 +115,6 @@ const props = defineProps({
type: Number,
default: 0,
},
discountedTotal: {
type: Number,
default: 0,
},
currency: {
type: [Object, String],
required: true,
@ -146,19 +153,19 @@ const filteredTypes = computed(() => {
})
const taxAmount = computed(() => {
if (localTax.compound_tax && props.discountedTotal) {
return ((props.discountedTotal + props.totalTax) * localTax.percent) / 100
if (localTax.compound_tax && props.total) {
return ((props.total + props.totalTax) * localTax.percent) / 100
}
if (props.discountedTotal && localTax.percent) {
return (props.discountedTotal * localTax.percent) / 100
if (props.total && localTax.percent) {
return (props.total * localTax.percent) / 100
}
return 0
})
watch(
() => props.discountedTotal,
() => props.total,
() => {
updateRowTax()
}

View File

@ -29,7 +29,14 @@
<label
v-else
class="flex items-center justify-center m-0 text-lg text-black uppercase "
class="
flex
items-center
justify-center
m-0
text-lg text-black
uppercase
"
>
<BaseFormatMoney
:amount="store.getSubTotal"
@ -59,7 +66,14 @@
<label
v-else-if="store[storeProp].tax_per_item === 'YES'"
class="flex items-center justify-center m-0 text-lg text-black uppercase "
class="
flex
items-center
justify-center
m-0
text-lg text-black
uppercase
"
>
<BaseFormatMoney :amount="tax.amount" :currency="defaultCurrency" />
</label>
@ -84,7 +98,7 @@
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText
:lines="1"
class="w-24 h-8 border rounded-md"
class="w-24 h-8 rounded-md border"
/>
</BaseContentPlaceholders>
<div v-else class="flex" style="width: 140px" role="group">
@ -100,7 +114,7 @@
<BaseDropdown position="bottom-end">
<template #activator>
<BaseButton
class="p-2 rounded-none rounded-tr-md rounded-br-md"
class="rounded-tr-md rounded-br-md p-2 rounded-none"
type="button"
variant="white"
>
@ -113,7 +127,7 @@
<BaseIcon
name="ChevronDownIcon"
class="w-4 h-4 ml-1 text-gray-500"
class="w-4 h-4 text-gray-500 ml-1"
/>
</span>
</BaseButton>
@ -166,7 +180,15 @@
</div>
<div
class="flex items-center justify-between w-full pt-2 mt-5 border-t border-gray-200 border-solid "
class="
flex
items-center
justify-between
w-full
pt-2
mt-5
border-t border-gray-200 border-solid
"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
@ -182,7 +204,14 @@
</BaseContentPlaceholders>
<label
v-else
class="flex items-center justify-center text-lg uppercase text-primary-400"
class="
flex
items-center
justify-center
text-lg
uppercase
text-primary-400
"
>
<BaseFormatMoney :amount="store.getTotal" :currency="defaultCurrency" />
</label>
@ -305,7 +334,6 @@ function selectPercentage() {
function onSelectTax(selectedTax) {
let amount = 0
if (selectedTax.compound_tax && props.store.getSubtotalWithDiscount) {
amount = Math.round(
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *

View File

@ -453,7 +453,7 @@
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
@ -549,6 +549,7 @@ const rules = computed(() => {
website: {
url: helpers.withMessage(t('validation.invalid_url'), url),
},
billing: {
address_street_1: {
maxLength: helpers.withMessage(

View File

@ -1,287 +0,0 @@
<template>
<BaseModal
:show="modalStore.active && modalStore.componentName === 'MailSenderModal'"
>
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XIcon"
class="h-6 w-6 text-gray-500 cursor-pointer"
@click="closeMailSenderModal"
/>
</div>
</template>
<form action="" @submit.prevent="submitMailSenderData">
<div class="p-4 sm:p-6 my-2">
<!-- Name -->
<BaseInputGrid>
<BaseInputGroup
:label="$t(`${pre_t}.name`)"
:error="v$.name.$error && v$.name.$errors[0].$message"
:help-text="$t(`${pre_t}.name_help`)"
required
>
<BaseInput
v-model.trim="mailSenderStore.currentMailSender.name"
:invalid="v$.name.$error"
type="text"
@input="v$.name.$touch()"
/>
</BaseInputGroup>
<!-- From Name -->
<BaseInputGroup
:label="$t(`${pre_t}.from_name`)"
:error="v$.from_name.$error && v$.from_name.$errors[0].$message"
required
>
<BaseInput
v-model="mailSenderStore.currentMailSender.from_name"
:invalid="v$.from_name.$error"
type="text"
@input="v$.from_name.$touch()"
/>
</BaseInputGroup>
<!-- From Address -->
<BaseInputGroup
:label="$t(`${pre_t}.from_address`)"
:error="
v$.from_address.$error && v$.from_address.$errors[0].$message
"
required
>
<BaseInput
v-model="mailSenderStore.currentMailSender.from_address"
:invalid="v$.from_address.$error"
type="text"
@input="v$.from_address.$touch()"
/>
</BaseInputGroup>
<!-- CC -->
<BaseInputGroup
:label="$t(`${pre_t}.cc`)"
:error="v$.cc.$error && v$.cc.$errors[0].$message"
:help-text="$t(`${pre_t}.email_list`)"
>
<BaseInput
v-model="mailSenderStore.currentMailSender.cc"
:invalid="v$.cc.$error"
type="text"
@input="v$.cc.$touch()"
/>
</BaseInputGroup>
<!-- BCC -->
<BaseInputGroup
:label="$t(`${pre_t}.bcc`)"
:error="v$.bcc.$error && v$.bcc.$errors[0].$message"
:help-text="$t(`${pre_t}.email_list`)"
>
<BaseInput
v-model="mailSenderStore.currentMailSender.bcc"
:invalid="v$.bcc.$error"
type="text"
@input="v$.bcc.$touch()"
/>
</BaseInputGroup>
<!-- Mail Driver -->
<BaseInputGroup
:label="$t(`${pre_t}.driver`)"
:error="v$.driver.$error && v$.driver.$errors[0].$message"
required
>
<BaseMultiselect
v-model="mailSenderStore.currentMailSender.driver"
:options="mailSenderStore.mail_drivers"
:can-deselect="false"
:invalid="v$.driver.$error"
/>
</BaseInputGroup>
<component
:is="loadMailDriver"
:mail-sender-store="mailSenderStore"
/>
</BaseInputGrid>
<BaseDivider class="mt-4 mb-0" />
<!-- Is Default? -->
<BaseSwitchSection
v-if="!mailSenderStore.isDisable"
v-model="mailSenderStore.currentMailSender.is_default"
:title="$t(`${pre_t}.is_default`)"
:description="$t(`${pre_t}.is_default_description`)"
/>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-solid border--200 border-modal-bg"
>
<BaseButton
class="mr-3 text-sm"
variant="primary-outline"
type="button"
@click="closeMailSenderModal"
>
{{ $t('general.cancel') }}
</BaseButton>
<BaseButton
:loading="isSaving"
:disabled="isSaving"
variant="primary"
type="submit"
>
<template #left="slotProps">
<BaseIcon
v-if="!isSaving"
name="SaveIcon"
:class="slotProps.class"
/>
</template>
{{
mailSenderStore.isEdit ? $t('general.update') : $t('general.save')
}}
</BaseButton>
</div>
</form>
</BaseModal>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { required, email, minLength, helpers } from '@vuelidate/validators'
import { useVuelidate } from '@vuelidate/core'
import { useModalStore } from '@/scripts/stores/modal'
import { useMailSenderStore } from '@/scripts/admin/stores/mail-sender'
import SmtpDriver from '@/scripts/admin/views/settings/mail-sender/SmtpDriver.vue'
import MailgunDriver from '@/scripts/admin/views/settings/mail-sender/MailgunDriver.vue'
import SesDriver from '@/scripts/admin/views/settings/mail-sender/SesDriver.vue'
const pre_t = 'settings.mail_sender'
const modalStore = useModalStore()
const mailSenderStore = useMailSenderStore()
const { t } = useI18n()
let isSaving = ref(false)
const loadMailDriver = computed(() => {
switch (mailSenderStore.currentMailSender.driver) {
case 'smtp':
return SmtpDriver
case 'mail':
return false
case 'sendmail':
return false
case 'mailgun':
return MailgunDriver
case 'ses':
return SesDriver
default:
return false
}
})
// This is multiple email custom validation
const multiEmail = (value) => {
if (value == '' || value === null) return true
const emailRegex =
/^(?:[A-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9]{2,}(?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i
const emailArr = value.split(',')
let isValid = emailArr.every((v) => {
return emailRegex.test(v)
})
return isValid
}
const rules = computed(() => {
return {
name: {
required: helpers.withMessage(t('validation.required'), required),
minLength: helpers.withMessage(
t('validation.name_min_length', { count: 3 }),
minLength(3)
),
},
from_name: {
required: helpers.withMessage(t('validation.required'), required),
},
from_address: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
cc: {
multiEmail: helpers.withMessage(
t('validation.email_incorrect'),
multiEmail
),
},
bcc: {
multiEmail: helpers.withMessage(
t('validation.email_incorrect'),
multiEmail
),
},
driver: {
required: helpers.withMessage(t('validation.required'), required),
},
}
})
const v$ = useVuelidate(
rules,
computed(() => mailSenderStore.currentMailSender)
)
async function submitMailSenderData() {
v$.value.$touch()
if (v$.value.$invalid) {
return true
}
try {
const action = mailSenderStore.isEdit
? mailSenderStore.updateMailSender
: mailSenderStore.addMailSender
isSaving.value = true
var mailDriverConfig = null
switch (mailSenderStore.currentMailSender.driver) {
case 'smtp':
mailDriverConfig = mailSenderStore.smtpConfig
break
case 'mailgun':
mailDriverConfig = mailSenderStore.mailgunConfig
break
case 'ses':
mailDriverConfig = mailSenderStore.sesConfig
break
}
mailSenderStore.currentMailSender.settings = mailDriverConfig
let res = await action(mailSenderStore.currentMailSender)
isSaving.value = false
modalStore.refreshData ? modalStore.refreshData(res.data.data) : ''
closeMailSenderModal()
} catch (err) {
isSaving.value = false
return true
}
}
function closeMailSenderModal() {
modalStore.closeModal()
setTimeout(() => {
mailSenderStore.resetCurrentMailSender()
v$.value.$reset()
}, 300)
}
onMounted(async () => {
await mailSenderStore.fetchMailDrivers()
})
</script>

View File

@ -1,208 +0,0 @@
<template>
<BaseModal :show="modalActive" @open="setInitialData">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XIcon"
class="w-6 h-6 text-gray-500 cursor-pointer"
@click="closeTestModal"
/>
</div>
</template>
<form action="" @submit.prevent="onTestMailSend">
<div class="p-4 md:p-8">
<BaseInputGrid layout="one-column">
<BaseInputGroup
:label="$t(`${pre_t}.title`)"
variant="horizontal"
:content-loading="isFetchingInitialData"
:error="
v$.mail_sender_id.$error && v$.mail_sender_id.$errors[0].$message
"
>
<BaseMultiselect
v-model="formData.mail_sender_id"
:invalid="v$.mail_sender_id.$error"
label="name"
:options="mailSenderStore.mailSenders"
value-prop="id"
:can-deselect="false"
:can-clear="false"
:placeholder="$t(`${pre_t}.select_mail_sender`)"
searchable
track-by="name"
:content-loading="isFetchingInitialData"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('general.to')"
:error="v$.to.$error && v$.to.$errors[0].$message"
variant="horizontal"
required
:content-loading="isFetchingInitialData"
>
<BaseInput
v-model="formData.to"
type="text"
:invalid="v$.to.$error"
:content-loading="isFetchingInitialData"
@input="v$.to.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('general.subject')"
:error="v$.subject.$error && v$.subject.$errors[0].$message"
variant="horizontal"
required
:content-loading="isFetchingInitialData"
>
<BaseInput
v-model="formData.subject"
type="text"
:invalid="v$.subject.$error"
:content-loading="isFetchingInitialData"
@input="v$.subject.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('general.message')"
:error="v$.message.$error && v$.message.$errors[0].$message"
variant="horizontal"
required
:content-loading="isFetchingInitialData"
>
<BaseTextarea
v-model="formData.message"
rows="4"
cols="50"
:invalid="v$.message.$error"
:content-loading="isFetchingInitialData"
@input="v$.message.$touch()"
/>
</BaseInputGroup>
</BaseInputGrid>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
<BaseButton
variant="primary-outline"
type="button"
class="mr-3"
:content-loading="isFetchingInitialData"
@click="closeTestModal()"
>
{{ $t('general.cancel') }}
</BaseButton>
<BaseButton
:loading="isSaving"
variant="primary"
type="submit"
:content-loading="isFetchingInitialData"
>
<template #left="slotProps">
<BaseIcon
v-if="!isSaving"
name="PaperAirplaneIcon"
:class="slotProps.class"
/>
</template>
{{ $t('general.send') }}
</BaseButton>
</div>
</form>
</BaseModal>
</template>
<script setup>
import { reactive, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { required, email, maxLength, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useModalStore } from '@/scripts/stores/modal'
import { useMailSenderStore } from '@/scripts/admin/stores/mail-sender'
const pre_t = 'settings.mail_sender'
const modalStore = useModalStore()
const mailSenderStore = useMailSenderStore()
const { t } = useI18n()
let isSaving = ref(false)
let formData = reactive({
mail_sender_id: '',
to: '',
subject: '',
message: '',
})
const isFetchingInitialData = ref(false)
const modalActive = computed(() => {
return modalStore.active && modalStore.componentName === 'MailSenderTestModal'
})
function setInitialData() {
isFetchingInitialData.value = true
formData.mail_sender_id = modalStore.id
setTimeout(() => {
isFetchingInitialData.value = false
}, 100)
}
const rules = {
mail_sender_id: {
required: helpers.withMessage(t('validation.required'), required),
},
to: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
subject: {
required: helpers.withMessage(t('validation.required'), required),
maxLength: helpers.withMessage(
t('validation.subject_maxlength'),
maxLength(100)
),
},
message: {
required: helpers.withMessage(t('validation.required'), required),
maxLength: helpers.withMessage(
t('validation.message_maxlength'),
maxLength(255)
),
},
}
const v$ = useVuelidate(rules, formData)
function resetFormData() {
formData.mail_sender_id = ''
formData.to = ''
formData.subject = ''
formData.message = ''
v$.value.$reset()
}
async function onTestMailSend() {
v$.value.$touch()
if (v$.value.$invalid) {
return true
}
isSaving.value = true
let response = await mailSenderStore.sendTestMail(formData)
if (response.data) {
closeTestModal()
}
}
function closeTestModal() {
modalStore.closeModal()
setTimeout(() => {
isSaving.value = false
modalStore.resetModalData()
resetFormData()
}, 300)
}
</script>

View File

@ -16,28 +16,18 @@
</template>
<form v-if="!isPreview" action="">
<!-- v-if -->
<div v-if="isMailSenderExist" class="px-8 py-8 sm:p-6">
<div class="px-8 py-8 sm:p-6">
<BaseInputGrid layout="one-column">
<BaseInputGroup
:label="$tc('settings.mail_sender.title', 1)"
:label="$t('general.from')"
required
:error="
v$.mail_sender_id.$error && v$.mail_sender_id.$errors[0].$message
"
:error="v$.from.$error && v$.from.$errors[0].$message"
>
<BaseMultiselect
v-model="estimateMailForm.mail_sender_id"
:invalid="v$.mail_sender_id.$error"
label="name"
:options="mailSenders"
value-prop="id"
:can-deselect="false"
:can-clear="false"
:placeholder="$t(`settings.mail_sender.select_mail_sender`)"
searchable
track-by="name"
:content-loading="isFetchingInitialData"
<BaseInput
v-model="estimateMailForm.from"
type="text"
:invalid="v$.from.$error"
@input="v$.from.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
@ -72,45 +62,6 @@
</BaseInputGroup>
</BaseInputGrid>
</div>
<!-- v-else -->
<div v-else-if="!isMailSenderExist && !isFetchingInitialData">
<FeedbackAlert
:title="$t('settings.mail_sender.no_mail_sender_found')"
:description="
$t('settings.mail_sender.no_mail_sender_found_description')
"
type="warning"
>
<template #action>
<div class="mt-4">
<div class="-mx-2 -my-1.5 flex">
<button
type="button"
class="
bg-yellow-50
px-2
py-1.5
rounded-md
text-sm
font-medium
text-yellow-800
hover:bg-yellow-100
focus:outline-none
focus:ring-2
focus:ring-offset-2
focus:ring-offset-yellow-50
focus:ring-yellow-600
"
@click="gotoMailSender"
>
{{ $t('settings.mail_sender.manage_mail_sender') }}
</button>
</div>
</div>
</template>
</FeedbackAlert>
</div>
<!-- end v-if-else -->
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
@ -124,7 +75,6 @@
</BaseButton>
<BaseButton
v-if="isMailSenderExist"
:loading="isLoading"
:disabled="isLoading"
variant="primary"
@ -191,24 +141,18 @@ import { useModalStore } from '@/scripts/stores/modal'
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
import { useNotificationStore } from '@/scripts/stores/notification'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import { useMailSenderStore } from '@/scripts/admin/stores/mail-sender'
import FeedbackAlert from '@/scripts/admin/components/FeedbackAlert.vue'
import { useRouter } from 'vue-router'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
const modalStore = useModalStore()
const estimateStore = useEstimateStore()
const notificationStore = useNotificationStore()
const companyStore = useCompanyStore()
const mailSenderStore = useMailSenderStore()
const router = useRouter()
const mailDriverStore = useMailDriverStore()
const { t } = useI18n()
const isLoading = ref(false)
const templateUrl = ref('')
const isPreview = ref(false)
const mailSenders = ref(null)
const isFetchingInitialData = ref(false)
const emailTemplates = ref(null)
const estimateMailFields = ref([
'customer',
@ -220,7 +164,7 @@ const estimateMailFields = ref([
let estimateMailForm = reactive({
id: null,
mail_sender_id: null,
from: null,
to: null,
subject: 'New Estimate',
body: null,
@ -237,8 +181,9 @@ const modalData = computed(() => {
})
const rules = {
mail_sender_id: {
from: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
to: {
required: helpers.withMessage(t('validation.required'), required),
@ -262,26 +207,20 @@ function cancelPreview() {
}
async function setInitialData() {
let admin = await companyStore.fetchBasicMailConfig()
estimateMailForm.id = modalStore.id
if (admin.data) {
estimateMailForm.from = admin.data.from_mail
}
if (modalData.value) {
estimateMailForm.to = modalData.value.customer.email
}
estimateMailForm.body =
companyStore.selectedCompanySettings.estimate_mail_body
isFetchingInitialData.value = true
let mailSenderData = await mailSenderStore.fetchMailSenders({ limit: 'all' })
if (mailSenderData.data) {
mailSenders.value = mailSenderData.data.data
let defaultMailSender = mailSenderData.data.data.find(
(mailSender) => mailSender.is_default == true
)
estimateMailForm.mail_sender_id = defaultMailSender
? defaultMailSender.id
: null
isFetchingInitialData.value = false
}
}
async function submitForm() {
@ -335,18 +274,4 @@ function closeSendEstimateModal() {
templateUrl.value = null
}, 300)
}
function getTickImage() {
const imgUrl = new URL('/img/tick.png', import.meta.url)
return imgUrl
}
const isMailSenderExist = computed(() => {
return mailSenders.value && mailSenders.value.length
})
function gotoMailSender() {
closeSendEstimateModal()
router.push('/admin/settings/mail-sender')
}
</script>

View File

@ -15,28 +15,18 @@
</div>
</template>
<form v-if="!isPreview" action="">
<!-- v-if -->
<div v-if="isMailSenderExist" class="px-8 py-8 sm:p-6">
<div class="px-8 py-8 sm:p-6">
<BaseInputGrid layout="one-column" class="col-span-7">
<BaseInputGroup
:label="$tc('settings.mail_sender.title', 1)"
:label="$t('general.from')"
required
:error="
v$.mail_sender_id.$error && v$.mail_sender_id.$errors[0].$message
"
:error="v$.from.$error && v$.from.$errors[0].$message"
>
<BaseMultiselect
v-model="invoiceMailForm.mail_sender_id"
:invalid="v$.mail_sender_id.$error"
label="name"
:options="mailSenders"
value-prop="id"
:can-deselect="false"
:can-clear="false"
:placeholder="$t(`settings.mail_sender.select_mail_sender`)"
searchable
track-by="name"
:content-loading="isFetchingInitialData"
<BaseInput
v-model="invoiceMailForm.from"
type="text"
:invalid="v$.from.$error"
@input="v$.from.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
@ -75,45 +65,6 @@
</BaseInputGroup>
</BaseInputGrid>
</div>
<!-- v-else -->
<div v-else-if="!isMailSenderExist && !isFetchingInitialData">
<FeedbackAlert
:title="$t('settings.mail_sender.no_mail_sender_found')"
:description="
$t('settings.mail_sender.no_mail_sender_found_description')
"
type="warning"
>
<template #action>
<div class="mt-4">
<div class="-mx-2 -my-1.5 flex">
<button
type="button"
class="
bg-yellow-50
px-2
py-1.5
rounded-md
text-sm
font-medium
text-yellow-800
hover:bg-yellow-100
focus:outline-none
focus:ring-2
focus:ring-offset-2
focus:ring-offset-yellow-50
focus:ring-yellow-600
"
@click="gotoMailSender"
>
{{ $t('settings.mail_sender.manage_mail_sender') }}
</button>
</div>
</div>
</template>
</FeedbackAlert>
</div>
<!-- end v-if-else -->
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
@ -126,7 +77,6 @@
{{ $t('general.cancel') }}
</BaseButton>
<BaseButton
v-if="isMailSenderExist"
:loading="isLoading"
:disabled="isLoading"
variant="primary"
@ -204,24 +154,18 @@ import { useI18n } from 'vue-i18n'
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
import { useVuelidate } from '@vuelidate/core'
import { required, email, helpers } from '@vuelidate/validators'
import { useMailSenderStore } from '@/scripts/admin/stores/mail-sender'
import FeedbackAlert from '@/scripts/admin/components/FeedbackAlert.vue'
import { useRouter } from 'vue-router'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
const modalStore = useModalStore()
const companyStore = useCompanyStore()
const notificationStore = useNotificationStore()
const invoiceStore = useInvoiceStore()
const mailSenderStore = useMailSenderStore()
const router = useRouter()
const mailDriverStore = useMailDriverStore()
const { t } = useI18n()
let isLoading = ref(false)
const templateUrl = ref('')
const isPreview = ref(false)
const mailSenders = ref(null)
const isFetchingInitialData = ref(false)
const emailTemplates = ref(null)
const emit = defineEmits(['update'])
@ -235,7 +179,7 @@ const invoiceMailFields = ref([
const invoiceMailForm = reactive({
id: null,
mail_sender_id: null,
from: null,
to: null,
subject: 'New Invoice',
body: null,
@ -254,8 +198,9 @@ const modalData = computed(() => {
})
const rules = {
mail_sender_id: {
from: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
to: {
required: helpers.withMessage(t('validation.required'), required),
@ -279,25 +224,19 @@ function cancelPreview() {
}
async function setInitialData() {
let admin = await companyStore.fetchBasicMailConfig()
invoiceMailForm.id = modalStore.id
if (admin.data) {
invoiceMailForm.from = admin.data.from_mail
}
if (modalData.value) {
invoiceMailForm.to = modalData.value.customer.email
}
invoiceMailForm.body = companyStore.selectedCompanySettings.invoice_mail_body
isFetchingInitialData.value = true
let mailSenderData = await mailSenderStore.fetchMailSenders({ limit: 'all' })
if (mailSenderData.data) {
mailSenders.value = mailSenderData.data.data
let defaultMailSender = mailSenderData.data.data.find(
(mailSender) => mailSender.is_default == true
)
invoiceMailForm.mail_sender_id = defaultMailSender
? defaultMailSender.id
: null
isFetchingInitialData.value = false
}
}
async function submitForm() {
@ -348,18 +287,4 @@ function closeSendInvoiceModal() {
templateUrl.value = null
}, 300)
}
function getTickImage() {
const imgUrl = new URL('/img/tick.png', import.meta.url)
return imgUrl
}
const isMailSenderExist = computed(() => {
return mailSenders.value && mailSenders.value.length
})
function gotoMailSender() {
closeSendInvoiceModal()
router.push('/admin/settings/mail-sender')
}
</script>

View File

@ -15,28 +15,18 @@
</div>
</template>
<form v-if="!isPreview" action="">
<!-- v-if -->
<div v-if="isMailSenderExist" class="px-8 py-8 sm:p-6">
<div class="px-8 py-8 sm:p-6">
<BaseInputGrid layout="one-column" class="col-span-7">
<BaseInputGroup
:label="$tc('settings.mail_sender.title', 1)"
:label="$t('general.from')"
required
:error="
v$.mail_sender_id.$error && v$.mail_sender_id.$errors[0].$message
"
:error="v$.from.$error && v$.from.$errors[0].$message"
>
<BaseMultiselect
v-model="paymentMailForm.mail_sender_id"
:invalid="v$.mail_sender_id.$error"
label="name"
:options="mailSenders"
value-prop="id"
:can-deselect="false"
:can-clear="false"
:placeholder="$t(`settings.mail_sender.select_mail_sender`)"
searchable
track-by="name"
:content-loading="isFetchingInitialData"
<BaseInput
v-model="paymentMailForm.from"
type="text"
:invalid="v$.from.$error"
@input="v$.from.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
@ -75,45 +65,6 @@
</BaseInputGroup>
</BaseInputGrid>
</div>
<!-- v-else -->
<div v-else-if="!isMailSenderExist && !isFetchingInitialData">
<FeedbackAlert
:title="$t('settings.mail_sender.no_mail_sender_found')"
:description="
$t('settings.mail_sender.no_mail_sender_found_description')
"
type="warning"
>
<template #action>
<div class="mt-4">
<div class="-mx-2 -my-1.5 flex">
<button
type="button"
class="
bg-yellow-50
px-2
py-1.5
rounded-md
text-sm
font-medium
text-yellow-800
hover:bg-yellow-100
focus:outline-none
focus:ring-2
focus:ring-offset-2
focus:ring-offset-yellow-50
focus:ring-yellow-600
"
@click="gotoMailSender"
>
{{ $t('settings.mail_sender.manage_mail_sender') }}
</button>
</div>
</div>
</template>
</FeedbackAlert>
</div>
<!-- end v-if-else -->
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
@ -126,7 +77,6 @@
{{ $t('general.cancel') }}
</BaseButton>
<BaseButton
v-if="isMailSenderExist"
:loading="isLoading"
:disabled="isLoading"
variant="primary"
@ -204,26 +154,20 @@ import { usePaymentStore } from '@/scripts/admin/stores/payment'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import { useNotificationStore } from '@/scripts/stores/notification'
import { useModalStore } from '@/scripts/stores/modal'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
import { useDialogStore } from '@/scripts/stores/dialog'
import { useMailSenderStore } from '@/scripts/admin/stores/mail-sender'
import FeedbackAlert from '@/scripts/admin/components/FeedbackAlert.vue'
import { useRouter } from 'vue-router'
const paymentStore = usePaymentStore()
const companyStore = useCompanyStore()
const modalStore = useModalStore()
const notificationStore = useNotificationStore()
const mailDriversStore = useMailDriverStore()
const dialogStore = useDialogStore()
const mailSenderStore = useMailSenderStore()
const router = useRouter()
const { t } = useI18n()
let isLoading = ref(false)
const templateUrl = ref('')
const isPreview = ref(false)
const mailSenders = ref(null)
const isFetchingInitialData = ref(false)
const emailTemplates = ref(null)
const paymentMailFields = ref([
'customer',
@ -235,7 +179,7 @@ const paymentMailFields = ref([
const paymentMailForm = reactive({
id: null,
mail_sender_id: null,
from: null,
to: null,
subject: 'New Payment',
body: null,
@ -254,8 +198,9 @@ const modalData = computed(() => {
})
const rules = {
mail_sender_id: {
from: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
to: {
required: helpers.withMessage(t('validation.required'), required),
@ -276,25 +221,18 @@ function cancelPreview() {
}
async function setInitialData() {
let admin = await companyStore.fetchBasicMailConfig()
paymentMailForm.id = modalStore.id
if (admin.data) {
paymentMailForm.from = admin.data.from_mail
}
if (modalData.value) {
paymentMailForm.to = modalData.value.customer.email
}
paymentMailForm.body = companyStore.selectedCompanySettings.payment_mail_body
isFetchingInitialData.value = true
let mailSenderData = await mailSenderStore.fetchMailSenders({ limit: 'all' })
if (mailSenderData.data) {
mailSenders.value = mailSenderData.data.data
let defaultMailSender = mailSenderData.data.data.find(
(mailSender) => mailSender.is_default == true
)
paymentMailForm.mail_sender_id = defaultMailSender
? defaultMailSender.id
: null
isFetchingInitialData.value = false
}
}
async function sendPaymentData() {
@ -342,18 +280,4 @@ function closeSendPaymentModal() {
modalStore.resetModalData()
}, 300)
}
function getTickImage() {
const imgUrl = new URL('/img/tick.png', import.meta.url)
return imgUrl
}
const isMailSenderExist = computed(() => {
return mailSenders.value && mailSenders.value.length
})
function gotoMailSender() {
closeSendPaymentModal()
router.push('/admin/settings/mail-sender')
}
</script>

View File

@ -143,7 +143,7 @@
<template #activator>
<img
:src="previewAvatar"
class="block w-8 h-8 rounded md:h-9 md:w-9 object-cover"
class="block w-8 h-8 rounded md:h-9 md:w-9"
/>
</template>

View File

@ -0,0 +1,146 @@
import axios from 'axios'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useMailDriverStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc({
id: 'mail-driver',
state: () => ({
mailConfigData: null,
mail_driver: 'smtp',
mail_drivers: [],
basicMailConfig: {
mail_driver: '',
mail_host: '',
from_mail: '',
from_name: '',
},
mailgunConfig: {
mail_driver: '',
mail_mailgun_domain: '',
mail_mailgun_secret: '',
mail_mailgun_endpoint: '',
from_mail: '',
from_name: '',
},
sesConfig: {
mail_driver: '',
mail_host: '',
mail_port: null,
mail_ses_key: '',
mail_ses_secret: '',
mail_encryption: 'tls',
from_mail: '',
from_name: '',
},
smtpConfig: {
mail_driver: '',
mail_host: '',
mail_port: null,
mail_username: '',
mail_password: '',
mail_encryption: 'tls',
from_mail: '',
from_name: '',
},
}),
actions: {
fetchMailDrivers() {
return new Promise((resolve, reject) => {
axios
.get('/api/v1/mail/drivers')
.then((response) => {
if (response.data) {
this.mail_drivers = response.data
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchMailConfig() {
return new Promise((resolve, reject) => {
axios
.get('/api/v1/mail/config')
.then((response) => {
if (response.data) {
this.mailConfigData = response.data
this.mail_driver = response.data.mail_driver
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateMailConfig(data) {
return new Promise((resolve, reject) => {
axios
.post('/api/v1/mail/config', data)
.then((response) => {
const notificationStore = useNotificationStore()
if (response.data.success) {
notificationStore.showNotification({
type: 'success',
message: global.t('wizard.success.' + response.data.success),
})
} else {
notificationStore.showNotification({
type: 'error',
message: global.t('wizard.errors.' + response.data.error),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
sendTestMail(data) {
return new Promise((resolve, reject) => {
axios
.post('/api/v1/mail/test', data)
.then((response) => {
const notificationStore = useNotificationStore()
if (response.data.success) {
notificationStore.showNotification({
type: 'success',
message: global.t('general.send_mail_successfully'),
})
} else {
notificationStore.showNotification({
type: 'error',
message: global.t('validation.something_went_wrong'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})()
}

View File

@ -1,202 +0,0 @@
import axios from 'axios'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
import mailSenderStub from '@/scripts/admin/stub/mail-sender.js'
export const useMailSenderStore = (useWindow = false) => {
const pre_t = 'settings.mail_sender'
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc({
id: 'mailSender',
state: () => ({
mailSenders: [],
mail_drivers: [], // list of mail drivers
currentMailSender: { ...mailSenderStub.basicConfig },
smtpConfig: { ...mailSenderStub.smtpConfig },
mailgunConfig: { ...mailSenderStub.mailgunConfig },
sesConfig: { ...mailSenderStub.sesConfig },
mail_encryptions: ['none', 'tls', 'ssl', 'starttls'],
isDisable: false
}),
getters: {
isEdit: (state) => (state.currentMailSender.id ? true : false),
},
actions: {
resetCurrentMailSender() {
this.currentMailSender = { ...mailSenderStub.basicConfig }
this.smtpConfig = { ...mailSenderStub.smtpConfig }
this.mailgunConfig = { ...mailSenderStub.mailgunConfig }
this.sesConfig = { ...mailSenderStub.sesConfig }
this.isDisable = false
},
fetchMailDrivers() {
return new Promise((resolve, reject) => {
axios
.get('/api/v1/mail-drivers')
.then((response) => {
if (response.data) {
this.mail_drivers = response.data
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchMailSenders(params) {
return new Promise((resolve, reject) => {
axios
.get(`/api/v1/mail-senders`, { params })
.then((response) => {
this.mailSenders = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchMailSender(id) {
return new Promise((resolve, reject) => {
axios
.get(`/api/v1/mail-senders/${id}`)
.then((response) => {
this.currentMailSender = response.data.data
this.isDisable = response.data.data.is_default
if (response.data.data.settings) {
var settings = response.data.data.settings
const encryptionNone = settings.encryption == '' || settings.encryption == undefined
switch (response.data.data.driver) {
case 'smtp':
this.smtpConfig = settings
encryptionNone ? this.smtpConfig.encryption = 'none' : ''
break
case 'mailgun':
this.mailgunConfig = settings
break
case 'ses':
this.sesConfig = settings
encryptionNone ? this.sesConfig.encryption = 'none' : ''
break
}
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addMailSender(data) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
axios
.post('/api/v1/mail-senders', data)
.then((response) => {
this.mailSenders.push(response.data.data)
notificationStore.showNotification({
type: 'success',
message: global.t(`${pre_t}.created_message`),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateMailSender(data) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
axios
.put(`/api/v1/mail-senders/${data.id}`, data)
.then((response) => {
if (response.data) {
let pos = this.mailSenders.findIndex(
(mailSender) => mailSender.id === response.data.data.id
)
this.mailSenders[pos] = data
notificationStore.showNotification({
type: 'success',
message: global.t(`${pre_t}.updated_message`),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteMailSender(id) {
return new Promise((resolve, reject) => {
axios
.delete(`/api/v1/mail-senders/${id}`)
.then((response) => {
if (response.data.success) {
let index = this.mailSenders.findIndex(
(mailSender) => mailSender.id === id
)
this.mailSenders.splice(index, 1)
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t(`${pre_t}.deleted_message`),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
sendTestMail(data) {
return new Promise((resolve, reject) => {
axios
.post('/api/v1/mail-test', data)
.then((response) => {
const notificationStore = useNotificationStore()
if (response.data.success) {
notificationStore.showNotification({
type: 'success',
message: global.t('general.send_mail_successfully'),
})
} else {
notificationStore.showNotification({
type: 'error',
message: global.t('validation.something_went_wrong'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})()
}

View File

@ -25,7 +25,6 @@ export const useUsersStore = (useWindow = false) => {
password: null,
phone: null,
companies: [],
sender_id: null,
},
}),

View File

@ -64,13 +64,6 @@ export default {
EDIT_ROLE: 'edit-role',
VIEW_ROLE: 'view-role',
// Mail Sender
CREATE_MAIL_SENDER: 'view-mail-sender',
DELETE_MAIL_SENDER: 'delete-mail-sender',
EDIT_MAIL_SENDER: 'edit-mail-sender',
VIEW_MAIL_SENDER: 'view-mail-sender',
// exchange rates
VIEW_EXCHANGE_RATE: 'view-exchange-rate-provider',
CREATE_EXCHANGE_RATE: 'create-exchange-rate-provider',

View File

@ -15,6 +15,5 @@ export default function () {
customFields: [],
fields: [],
enable_portal: false,
mail_sender_id: null,
}
}

View File

@ -1,31 +0,0 @@
export default {
basicConfig: {
name: '',
from_name: '',
from_address: '',
cc: '',
bcc: '',
is_default: false,
driver: 'smtp', // 'smtp', 'mail', 'sendmail', 'mailgun', 'ses'
settings: '',
},
smtpConfig: {
host: '',
port: null,
username: '',
password: '',
encryption: 'tls', // 'tls', 'ssl', 'starttls'
},
mailgunConfig: {
domain: '',
secret: '',
endpoint: '',
},
sesConfig: {
host: '',
port: null,
encryption: 'tls', // 'tls', 'ssl', 'starttls'
ses_key: '',
ses_secret: '',
},
}

View File

@ -256,7 +256,6 @@
/> </template
></BaseInput>
</BaseInputGroup>
<!-- && setPasswordMethod !== 'manual' -->
</BaseInputGrid>
</div>
@ -651,7 +650,10 @@ const rules = computed(() => {
},
email: {
required: helpers.withMessage(t('validation.required'), required),
required: helpers.withMessage(
t('validation.required'),
requiredIf(customerStore.currentCustomer.enable_portal == true)
),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
password: {

View File

@ -3,238 +3,75 @@
:title="$t('wizard.mail.mail_config')"
:description="$t('wizard.mail.mail_config_desc')"
>
<form action="" @submit.prevent="submitMailSenderData">
<div class="p-4 sm:p-6 my-2">
<!-- Name -->
<BaseInputGrid>
<BaseInputGroup
:label="$t(`${pre_t}.name`)"
:error="v$.name.$error && v$.name.$errors[0].$message"
:help-text="$t(`${pre_t}.name_help`)"
required
>
<BaseInput
v-model.trim="mailSenderStore.currentMailSender.name"
:invalid="v$.name.$error"
type="text"
@input="v$.name.$touch()"
/>
</BaseInputGroup>
<!-- From Name -->
<BaseInputGroup
:label="$t(`${pre_t}.from_name`)"
:error="v$.from_name.$error && v$.from_name.$errors[0].$message"
required
>
<BaseInput
v-model="mailSenderStore.currentMailSender.from_name"
:invalid="v$.from_name.$error"
type="text"
@input="v$.from_name.$touch()"
/>
</BaseInputGroup>
<!-- From Address -->
<BaseInputGroup
:label="$t(`${pre_t}.from_address`)"
:error="
v$.from_address.$error && v$.from_address.$errors[0].$message
"
required
>
<BaseInput
v-model="mailSenderStore.currentMailSender.from_address"
:invalid="v$.from_address.$error"
type="text"
@input="v$.from_address.$touch()"
/>
</BaseInputGroup>
<!-- CC -->
<BaseInputGroup
:label="$t(`${pre_t}.cc`)"
:error="v$.cc.$error && v$.cc.$errors[0].$message"
:help-text="$t(`${pre_t}.email_list`)"
>
<BaseInput
v-model="mailSenderStore.currentMailSender.cc"
:invalid="v$.cc.$error"
type="text"
@input="v$.cc.$touch()"
/>
</BaseInputGroup>
<!-- BCC -->
<BaseInputGroup
:label="$t(`${pre_t}.bcc`)"
:error="v$.bcc.$error && v$.bcc.$errors[0].$message"
:help-text="$t(`${pre_t}.email_list`)"
>
<BaseInput
v-model="mailSenderStore.currentMailSender.bcc"
:invalid="v$.bcc.$error"
type="text"
@input="v$.bcc.$touch()"
/>
</BaseInputGroup>
<!-- Mail Driver -->
<BaseInputGroup
:label="$t(`${pre_t}.driver`)"
:error="v$.driver.$error && v$.driver.$errors[0].$message"
required
>
<BaseMultiselect
v-model="mailSenderStore.currentMailSender.driver"
:options="mailSenderStore.mail_drivers"
:can-deselect="false"
:invalid="v$.driver.$error"
/>
</BaseInputGroup>
<component
:is="loadMailDriver"
:mail-sender-store="mailSenderStore"
/>
</BaseInputGrid>
<BaseDivider class="my-4" />
<BaseButton
:loading="isSaving"
:disabled="isSaving"
variant="primary"
type="submit"
>
<template #left="slotProps">
<BaseIcon
v-if="!isSaving"
name="SaveIcon"
:class="slotProps.class"
/>
</template>
{{ $t('wizard.save_cont') }}
</BaseButton>
</div>
<form action="" @submit.prevent="next">
<component
:is="mailDriverStore.mail_driver"
:config-data="mailDriverStore.mailConfigData"
:is-saving="isSaving"
:is-fetching-initial-data="isFetchingInitialData"
@on-change-driver="(val) => changeDriver(val)"
@submit-data="next"
/>
</form>
</BaseWizardStep>
</template>
<script setup>
import { computed, ref, inject, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useMailSenderStore } from '@/scripts/admin/stores/mail-sender'
import { useVuelidate } from '@vuelidate/core'
import { required, email, minLength, helpers } from '@vuelidate/validators'
import MailSenderDropdown from '@/scripts/admin/components/dropdowns/MailSenderIndexDropdown.vue'
import MailSenderTestModal from '@/scripts/admin/components/modal-components/MailSenderTestModal.vue'
import SmtpDriver from '@/scripts/admin/views/settings/mail-sender/SmtpDriver.vue'
import MailgunDriver from '@/scripts/admin/views/settings/mail-sender/MailgunDriver.vue'
import SesDriver from '@/scripts/admin/views/settings/mail-sender/SesDriver.vue'
<script>
import Smtp from './mail-driver/SmtpMailDriver.vue'
import Mailgun from './mail-driver/MailgunMailDriver.vue'
import Ses from './mail-driver/SesMailDriver.vue'
import Basic from './mail-driver/BasicMailDriver.vue'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
import { ref } from 'vue'
const pre_t = 'settings.mail_sender'
const mailSenderStore = useMailSenderStore()
const { t } = useI18n()
const table = ref(null)
const utils = inject('utils')
let isSaving = ref(false)
const emit = defineEmits(['next'])
export default {
components: {
Smtp,
Mailgun,
Ses,
sendmail: Basic,
Mail: Basic,
},
const loadMailDriver = computed(() => {
switch (mailSenderStore.currentMailSender.driver) {
case 'smtp':
return SmtpDriver
break
case 'mail':
return false
break
case 'sendmail':
return false
break
case 'mailgun':
return MailgunDriver
break
case 'ses':
return SesDriver
break
default:
return false
}
})
emits: ['next'],
// This is multiple email custom validation
const multiEmail = (value) => {
if (value == '' || value === null) return true
const emailRegex =
/^(?:[A-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9]{2,}(?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i
setup(props, { emit }) {
const isSaving = ref(false)
const isFetchingInitialData = ref(false)
const emailArr = value.split(',')
let isValid = emailArr.every((v) => {
return emailRegex.test(v)
})
return isValid
}
const mailDriverStore = useMailDriverStore()
const rules = computed(() => {
return {
name: {
required: helpers.withMessage(t('validation.required'), required),
minLength: helpers.withMessage(
t('validation.name_min_length', { count: 3 }),
minLength(3)
),
},
from_name: {
required: helpers.withMessage(t('validation.required'), required),
},
from_address: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
cc: {
multiEmail: helpers.withMessage(
t('validation.email_incorrect'),
multiEmail
),
},
bcc: {
multiEmail: helpers.withMessage(
t('validation.email_incorrect'),
multiEmail
),
},
driver: {
required: helpers.withMessage(t('validation.required'), required),
},
}
})
mailDriverStore.mail_driver = 'mail'
const v$ = useVuelidate(
rules,
computed(() => mailSenderStore.currentMailSender)
)
loadData()
async function submitMailSenderData() {
v$.value.$touch()
if (v$.value.$invalid) {
return true
}
try {
let data = {
...mailSenderStore.currentMailSender,
is_default: true,
function changeDriver(value) {
mailDriverStore.mail_driver = value
}
await mailSenderStore.addMailSender(data)
isSaving.value = true
emit('next')
isSaving.value = false
} catch (err) {
isSaving.value = false
return true
}
}
onMounted(async () => {
await mailSenderStore.fetchMailDrivers()
})
async function loadData() {
isFetchingInitialData.value = true
await mailDriverStore.fetchMailDrivers()
isFetchingInitialData.value = false
}
async function next(mailConfigData) {
isSaving.value = true
let res = await mailDriverStore.updateMailConfig(mailConfigData)
isSaving.value = false
if (res.data.success) {
await emit('next', 5)
}
}
return {
mailDriverStore,
isSaving,
isFetchingInitialData,
changeDriver,
next,
}
},
}
</script>

View File

@ -54,6 +54,8 @@
label="name"
:options="itemStore.itemUnits"
value-prop="id"
:can-deselect="false"
:can-clear="false"
:placeholder="$t('items.select_a_unit')"
searchable
track-by="name"

View File

@ -0,0 +1,158 @@
<template>
<form @submit.prevent="saveEmailConfig">
<BaseInputGrid>
<BaseInputGroup
:label="$t('settings.mail.driver')"
:content-loading="isFetchingInitialData"
:error="
v$.basicMailConfig.mail_driver.$error &&
v$.basicMailConfig.mail_driver.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="mailDriverStore.basicMailConfig.mail_driver"
:content-loading="isFetchingInitialData"
:options="mailDrivers"
:can-deselect="false"
:invalid="v$.basicMailConfig.mail_driver.$error"
@update:modelValue="onChangeDriver"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.from_mail')"
:content-loading="isFetchingInitialData"
:error="
v$.basicMailConfig.from_mail.$error &&
v$.basicMailConfig.from_mail.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.basicMailConfig.from_mail"
:content-loading="isFetchingInitialData"
type="text"
name="from_mail"
:invalid="v$.basicMailConfig.from_mail.$error"
@input="v$.basicMailConfig.from_mail.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.from_name')"
:content-loading="isFetchingInitialData"
:error="
v$.basicMailConfig.from_name.$error &&
v$.basicMailConfig.from_name.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.basicMailConfig.from_name"
:content-loading="isFetchingInitialData"
type="text"
name="name"
:invalid="v$.basicMailConfig.from_name.$error"
@input="v$.basicMailConfig.from_name.$touch()"
/>
</BaseInputGroup>
</BaseInputGrid>
<div class="flex mt-8">
<BaseButton
:content-loading="isFetchingInitialData"
:disabled="isSaving"
:loading="isSaving"
variant="primary"
type="submit"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="SaveIcon" />
</template>
{{ $t('general.save') }}
</BaseButton>
<slot />
</div>
</form>
</template>
<script setup>
import { onMounted, computed } from 'vue'
import { required, email, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useI18n } from 'vue-i18n'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
const props = defineProps({
configData: {
type: Object,
require: true,
default: Object,
},
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
require: true,
default: false,
},
mailDrivers: {
type: Array,
require: true,
default: Array,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
const mailDriverStore = useMailDriverStore()
const { t } = useI18n()
const rules = computed(() => {
return {
basicMailConfig: {
mail_driver: {
required: helpers.withMessage(t('validation.required'), required),
},
from_mail: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
from_name: {
required: helpers.withMessage(t('validation.required'), required),
},
},
}
})
const v$ = useVuelidate(
rules,
computed(() => mailDriverStore)
)
onMounted(() => {
for (const key in mailDriverStore.basicMailConfig) {
if (props.configData.hasOwnProperty(key)) {
mailDriverStore.$patch((state) => {
state.basicMailConfig[key] = props.configData[key]
})
}
}
})
async function saveEmailConfig() {
v$.value.basicMailConfig.$touch()
if (!v$.value.basicMailConfig.$invalid) {
emit('submit-data', mailDriverStore.basicMailConfig)
}
return false
}
function onChangeDriver() {
v$.value.basicMailConfig.mail_driver.$touch()
emit('on-change-driver', mailDriverStore.basicMailConfig.mail_driver)
}
</script>

View File

@ -0,0 +1,247 @@
<template>
<form @submit.prevent="saveEmailConfig">
<BaseInputGrid>
<BaseInputGroup
:label="$t('settings.mail.driver')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.mail_driver.$error &&
v$.mailgunConfig.mail_driver.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="mailDriverStore.mailgunConfig.mail_driver"
:content-loading="isFetchingInitialData"
:options="mailDrivers"
:can-deselect="false"
:invalid="v$.mailgunConfig.mail_driver.$error"
@update:modelValue="onChangeDriver"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.mailgun_domain')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.mail_mailgun_domain.$error &&
v$.mailgunConfig.mail_mailgun_domain.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.mailgunConfig.mail_mailgun_domain"
:content-loading="isFetchingInitialData"
type="text"
name="mailgun_domain"
:invalid="v$.mailgunConfig.mail_mailgun_domain.$error"
@input="v$.mailgunConfig.mail_mailgun_domain.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.mailgun_secret')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.mail_mailgun_secret.$error &&
v$.mailgunConfig.mail_mailgun_secret.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.mailgunConfig.mail_mailgun_secret"
:content-loading="isFetchingInitialData"
:type="getInputType"
name="mailgun_secret"
autocomplete="off"
:invalid="v$.mailgunConfig.mail_mailgun_secret.$error"
@input="v$.mailgunConfig.mail_mailgun_secret.$touch()"
>
<template #right>
<BaseIcon
v-if="isShowPassword"
class="mr-1 text-gray-500 cursor-pointer"
name="EyeOffIcon"
@click="isShowPassword = !isShowPassword"
/>
<BaseIcon
v-else
class="mr-1 text-gray-500 cursor-pointer"
name="EyeIcon"
@click="isShowPassword = !isShowPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.mailgun_endpoint')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.mail_mailgun_endpoint.$error &&
v$.mailgunConfig.mail_mailgun_endpoint.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.mailgunConfig.mail_mailgun_endpoint"
:content-loading="isFetchingInitialData"
type="text"
name="mailgun_endpoint"
:invalid="v$.mailgunConfig.mail_mailgun_endpoint.$error"
@input="v$.mailgunConfig.mail_mailgun_endpoint.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.from_mail')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.from_mail.$error &&
v$.mailgunConfig.from_mail.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.mailgunConfig.from_mail"
:content-loading="isFetchingInitialData"
type="text"
name="from_mail"
:invalid="v$.mailgunConfig.from_mail.$error"
@input="v$.mailgunConfig.from_mail.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.from_name')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.from_name.$error &&
v$.mailgunConfig.from_name.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.mailgunConfig.from_name"
:content-loading="isFetchingInitialData"
type="text"
name="from_name"
:invalid="v$.mailgunConfig.from_name.$error"
@input="v$.mailgunConfig.from_name.$touch()"
/>
</BaseInputGroup>
</BaseInputGrid>
<div class="flex my-10">
<BaseButton
:disabled="isSaving"
:content-loading="isFetchingInitialData"
:loading="isSaving"
variant="primary"
type="submit"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('general.save') }}
</BaseButton>
<slot />
</div>
</form>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue'
import { required, email, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useI18n } from 'vue-i18n'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
const props = defineProps({
configData: {
type: Object,
require: true,
default: Object,
},
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
require: true,
default: false,
},
mailDrivers: {
type: Array,
require: true,
default: Array,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
const mailDriverStore = useMailDriverStore()
const { t } = useI18n()
let isShowPassword = ref(false)
const getInputType = computed(() => {
if (isShowPassword.value) {
return 'text'
}
return 'password'
})
const rules = computed(() => {
return {
mailgunConfig: {
mail_driver: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_mailgun_domain: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_mailgun_endpoint: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_mailgun_secret: {
required: helpers.withMessage(t('validation.required'), required),
},
from_mail: {
required: helpers.withMessage(t('validation.required'), required),
email,
},
from_name: {
required: helpers.withMessage(t('validation.required'), required),
},
},
}
})
const v$ = useVuelidate(
rules,
computed(() => mailDriverStore)
)
onMounted(() => {
for (const key in mailDriverStore.mailgunConfig) {
if (props.configData.hasOwnProperty(key)) {
mailDriverStore.mailgunConfig[key] = props.configData[key]
}
}
})
async function saveEmailConfig() {
v$.value.mailgunConfig.$touch()
if (!v$.value.mailgunConfig.$invalid) {
emit('submit-data', mailDriverStore.mailgunConfig)
}
return false
}
function onChangeDriver() {
v$.value.mailgunConfig.mail_driver.$touch()
emit('on-change-driver', mailDriverStore.mailgunConfig.mail_driver)
}
</script>

View File

@ -0,0 +1,294 @@
<template>
<form @submit.prevent="saveEmailConfig">
<BaseInputGrid>
<BaseInputGroup
:label="$t('settings.mail.driver')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_driver.$error &&
v$.sesConfig.mail_driver.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="mailDriverStore.sesConfig.mail_driver"
:content-loading="isFetchingInitialData"
:options="mailDrivers"
:can-deselect="false"
:invalid="v$.sesConfig.mail_driver.$error"
@update:modelValue="onChangeDriver"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.host')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_host.$error &&
v$.sesConfig.mail_host.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.sesConfig.mail_host"
:content-loading="isFetchingInitialData"
type="text"
name="mail_host"
:invalid="v$.sesConfig.mail_host.$error"
@input="v$.sesConfig.mail_host.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.port')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_port.$error &&
v$.sesConfig.mail_port.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.sesConfig.mail_port"
:content-loading="isFetchingInitialData"
type="text"
name="mail_port"
:invalid="v$.sesConfig.mail_port.$error"
@input="v$.sesConfig.mail_port.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.encryption')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_encryption.$error &&
v$.sesConfig.mail_encryption.$errors[0].$message
"
required
>
<BaseMultiselect
v-model.trim="mailDriverStore.sesConfig.mail_encryption"
:content-loading="isFetchingInitialData"
:options="encryptions"
:invalid="v$.sesConfig.mail_encryption.$error"
placeholder="Select option"
@input="v$.sesConfig.mail_encryption.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.from_mail')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.from_mail.$error &&
v$.sesConfig.from_mail.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.sesConfig.from_mail"
:content-loading="isFetchingInitialData"
type="text"
name="from_mail"
:invalid="v$.sesConfig.from_mail.$error"
@input="v$.sesConfig.from_mail.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.from_name')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.from_name.$error &&
v$.sesConfig.from_name.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.sesConfig.from_name"
:content-loading="isFetchingInitialData"
type="text"
name="name"
:invalid="v$.sesConfig.from_name.$error"
@input="v$.sesConfig.from_name.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.ses_key')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_ses_key.$error &&
v$.sesConfig.mail_ses_key.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.sesConfig.mail_ses_key"
:content-loading="isFetchingInitialData"
type="text"
name="mail_ses_key"
:invalid="v$.sesConfig.mail_ses_key.$error"
@input="v$.sesConfig.mail_ses_key.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.ses_secret')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_ses_secret.$error &&
v$.mail_ses_secret.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.sesConfig.mail_ses_secret"
:content-loading="isFetchingInitialData"
:type="getInputType"
name="mail_ses_secret"
autocomplete="off"
:invalid="v$.sesConfig.mail_ses_secret.$error"
@input="v$.sesConfig.mail_ses_secret.$touch()"
>
<template #right>
<BaseIcon
v-if="isShowPassword"
class="mr-1 text-gray-500 cursor-pointer"
name="EyeOffIcon"
@click="isShowPassword = !isShowPassword"
/>
<BaseIcon
v-else
class="mr-1 text-gray-500 cursor-pointer"
name="EyeIcon"
@click="isShowPassword = !isShowPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
</BaseInputGrid>
<div class="flex my-10">
<BaseButton
:disabled="isSaving"
:content-loading="isFetchingInitialData"
:loading="isSaving"
variant="primary"
type="submit"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('general.save') }}
</BaseButton>
<slot />
</div>
</form>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { required, email, numeric, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useI18n } from 'vue-i18n'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
const props = defineProps({
configData: {
type: Object,
require: true,
default: Object,
},
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
require: true,
default: false,
},
mailDrivers: {
type: Array,
require: true,
default: Array,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
const mailDriverStore = useMailDriverStore()
const { t } = useI18n()
let isShowPassword = ref(false)
const encryptions = reactive(['tls', 'ssl', 'starttls'])
const rules = computed(() => {
return {
sesConfig: {
mail_driver: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_host: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_port: {
required: helpers.withMessage(t('validation.required'), required),
numeric,
},
mail_ses_key: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_ses_secret: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_encryption: {
required: helpers.withMessage(t('validation.required'), required),
},
from_mail: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
from_name: {
required: helpers.withMessage(t('validation.required'), required),
},
},
}
})
const v$ = useVuelidate(
rules,
computed(() => mailDriverStore)
)
const getInputType = computed(() => {
if (isShowPassword.value) {
return 'text'
}
return 'password'
})
onMounted(() => {
for (const key in mailDriverStore.sesConfig) {
if (props.configData.hasOwnProperty(key)) {
mailDriverStore.sesConfig[key] = props.configData[key]
}
}
})
async function saveEmailConfig() {
v$.value.sesConfig.$touch()
if (!v$.value.sesConfig.$invalid) {
emit('submit-data', mailDriverStore.sesConfig)
}
return false
}
function onChangeDriver() {
v$.value.sesConfig.mail_driver.$touch()
emit('on-change-driver', mailDriverStore.sesConfig.mail_driver)
}
</script>

View File

@ -0,0 +1,275 @@
<template>
<form @submit.prevent="saveEmailConfig">
<BaseInputGrid>
<BaseInputGroup
:label="$t('settings.mail.driver')"
:content-loading="isFetchingInitialData"
:error="
v$.smtpConfig.mail_driver.$error &&
v$.smtpConfig.mail_driver.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="mailDriverStore.smtpConfig.mail_driver"
:content-loading="isFetchingInitialData"
:options="mailDrivers"
:can-deselect="false"
:invalid="v$.smtpConfig.mail_driver.$error"
@update:modelValue="onChangeDriver"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.host')"
:content-loading="isFetchingInitialData"
:error="
v$.smtpConfig.mail_host.$error &&
v$.smtpConfig.mail_host.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.smtpConfig.mail_host"
:content-loading="isFetchingInitialData"
type="text"
name="mail_host"
:invalid="v$.smtpConfig.mail_host.$error"
@input="v$.smtpConfig.mail_host.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:content-loading="isFetchingInitialData"
:label="$t('settings.mail.username')"
>
<BaseInput
v-model.trim="mailDriverStore.smtpConfig.mail_username"
:content-loading="isFetchingInitialData"
type="text"
name="db_name"
/>
</BaseInputGroup>
<BaseInputGroup
:content-loading="isFetchingInitialData"
:label="$t('settings.mail.password')"
>
<BaseInput
v-model.trim="mailDriverStore.smtpConfig.mail_password"
:content-loading="isFetchingInitialData"
:type="getInputType"
name="password"
>
<template #right>
<BaseIcon
v-if="isShowPassword"
class="mr-1 text-gray-500 cursor-pointer"
name="EyeOffIcon"
@click="isShowPassword = !isShowPassword"
/>
<BaseIcon
v-else
class="mr-1 text-gray-500 cursor-pointer"
name="EyeIcon"
@click="isShowPassword = !isShowPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.port')"
:content-loading="isFetchingInitialData"
:error="
v$.smtpConfig.mail_port.$error &&
v$.smtpConfig.mail_port.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.smtpConfig.mail_port"
:content-loading="isFetchingInitialData"
type="text"
name="mail_port"
:invalid="v$.smtpConfig.mail_port.$error"
@input="v$.smtpConfig.mail_port.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.encryption')"
:content-loading="isFetchingInitialData"
:error="
v$.smtpConfig.mail_encryption.$error &&
v$.smtpConfig.mail_encryption.$errors[0].$message
"
required
>
<BaseMultiselect
v-model.trim="mailDriverStore.smtpConfig.mail_encryption"
:content-loading="isFetchingInitialData"
:options="encryptions"
:searchable="true"
:show-labels="false"
placeholder="Select option"
:invalid="v$.smtpConfig.mail_encryption.$error"
@input="v$.smtpConfig.mail_encryption.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.from_mail')"
:content-loading="isFetchingInitialData"
:error="
v$.smtpConfig.from_mail.$error &&
v$.smtpConfig.from_mail.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.smtpConfig.from_mail"
:content-loading="isFetchingInitialData"
type="text"
name="from_mail"
:invalid="v$.smtpConfig.from_mail.$error"
@input="v$.smtpConfig.from_mail.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.mail.from_name')"
:content-loading="isFetchingInitialData"
:error="
v$.smtpConfig.from_name.$error &&
v$.smtpConfig.from_name.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailDriverStore.smtpConfig.from_name"
:content-loading="isFetchingInitialData"
type="text"
name="from_name"
:invalid="v$.smtpConfig.from_name.$error"
@input="v$.smtpConfig.from_name.$touch()"
/>
</BaseInputGroup>
</BaseInputGrid>
<div class="flex my-10">
<BaseButton
:disabled="isSaving"
:content-loading="isFetchingInitialData"
:loading="isSaving"
type="submit"
variant="primary"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('general.save') }}
</BaseButton>
<slot />
</div>
</form>
</template>
<script setup>
import { reactive, onMounted, ref, computed } from 'vue'
import { required, email, numeric, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useI18n } from 'vue-i18n'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
const props = defineProps({
configData: {
type: Object,
require: true,
default: Object,
},
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
require: true,
default: false,
},
mailDrivers: {
type: Array,
require: true,
default: Array,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
const mailDriverStore = useMailDriverStore()
const { t } = useI18n()
let isShowPassword = ref(false)
const encryptions = reactive(['tls', 'ssl', 'starttls'])
const getInputType = computed(() => {
if (isShowPassword.value) {
return 'text'
}
return 'password'
})
const rules = computed(() => {
return {
smtpConfig: {
mail_driver: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_host: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_port: {
required: helpers.withMessage(t('validation.required'), required),
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
},
mail_encryption: {
required: helpers.withMessage(t('validation.required'), required),
},
from_mail: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
from_name: {
required: helpers.withMessage(t('validation.required'), required),
},
},
}
})
const v$ = useVuelidate(
rules,
computed(() => mailDriverStore)
)
onMounted(() => {
for (const key in mailDriverStore.smtpConfig) {
if (props.configData.hasOwnProperty(key)) {
mailDriverStore.smtpConfig[key] = props.configData[key]
}
}
})
async function saveEmailConfig() {
v$.value.smtpConfig.$touch()
if (!v$.value.smtpConfig.$invalid) {
emit('submit-data', mailDriverStore.smtpConfig)
}
return false
}
function onChangeDriver() {
v$.value.smtpConfig.mail_driver.$touch()
emit('on-change-driver', mailDriverStore.smtpConfig.mail_driver)
}
</script>

View File

@ -1,136 +0,0 @@
<template>
<BaseSettingCard
:title="$tc(`${pre_t}.title`, 2)"
:description="$t(`${pre_t}.description`)"
>
<MailSenderModal />
<MailSenderTestModal />
<template #action>
<BaseButton
type="submit"
variant="primary-outline"
@click="openMailSenderModal"
>
<template #left="slotProps">
<BaseIcon :class="slotProps.class" name="PlusIcon" />
</template>
{{ $t(`${pre_t}.add_new_mail_sender`) }}
</BaseButton>
</template>
<BaseTable
ref="table"
class="mt-16"
:data="fetchData"
:columns="mailSenderColumns"
>
<template #cell-is_default="{ row }">
<BaseBadge
:bg-color="
utils.getBadgeStatusColor(row.data.is_default ? 'YES' : 'NO')
.bgColor
"
:color="
utils.getBadgeStatusColor(row.data.is_default ? 'YES' : 'NO').color
"
>
{{ row.data.is_default ? $t('general.yes') : $t('general.no') }}
</BaseBadge>
</template>
<template #cell-actions="{ row }">
<MailSenderDropdown
:row="row.data"
:table="table"
:load-data="refreshTable"
/>
</template>
</BaseTable>
</BaseSettingCard>
</template>
<script setup>
import { computed, ref, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import { useModalStore } from '@/scripts/stores/modal'
import MailSenderModal from '@/scripts/admin/components/modal-components/MailSenderModal.vue'
import { useMailSenderStore } from '@/scripts/admin/stores/mail-sender'
import MailSenderDropdown from '@/scripts/admin/components/dropdowns/MailSenderIndexDropdown.vue'
import MailSenderTestModal from '@/scripts/admin/components/modal-components/MailSenderTestModal.vue'
const pre_t = 'settings.mail_sender'
const modalStore = useModalStore()
const mailSenderStore = useMailSenderStore()
const { t } = useI18n()
const table = ref(null)
const utils = inject('utils')
function openMailSenderModal() {
modalStore.openModal({
title: t(`${pre_t}.add_new_mail_sender`),
componentName: 'MailSenderModal',
size: 'md',
refreshData: refreshTable,
})
}
const mailSenderColumns = computed(() => {
return [
{
key: 'name',
label: t(`${pre_t}.name`),
thClass: 'extra',
tdClass: 'font-medium text-gray-900',
},
{
key: 'driver',
label: t(`${pre_t}.driver`),
thClass: 'extra',
tdClass: 'font-medium text-gray-900',
},
{
key: 'from_address',
label: t(`${pre_t}.from_address`),
thClass: 'extra',
tdClass: 'font-medium text-gray-900',
},
{
key: 'is_default',
label: t(`${pre_t}.is_default`),
thClass: 'extra',
tdClass: 'font-medium text-gray-900',
},
{
key: 'actions',
label: '',
tdClass: 'text-right text-sm font-medium',
sortable: false,
},
]
})
async function fetchData({ page, filter, sort }) {
let data = {
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page,
}
let response = await mailSenderStore.fetchMailSenders(data)
return {
data: response.data.data,
pagination: {
totalPages: response.data.meta.last_page,
currentPage: page,
totalCount: response.data.meta.total,
limit: response.data.meta.per_page ? response.data.meta.per_page : 10,
},
}
}
async function refreshTable() {
table.value && table.value.refresh()
}
</script>

View File

@ -1,104 +0,0 @@
<template>
<!-- Domain -->
<BaseInputGroup
:label="$t(`${pre_t}.domain`)"
:error="v$.domain.$error && v$.domain.$errors[0].$message"
required
>
<BaseInput
v-model.trim="mailSenderStore.mailgunConfig.domain"
:invalid="v$.domain.$error"
type="text"
@input="v$.domain.$touch()"
/>
</BaseInputGroup>
<!-- Mailgun Secret -->
<BaseInputGroup
:label="$t(`${pre_t}.secret`)"
:error="v$.secret.$error && v$.secret.$errors[0].$message"
required
>
<BaseInput
v-model="mailSenderStore.mailgunConfig.secret"
:type="getInputType"
autocomplete="off"
:invalid="v$.secret.$error"
@input="v$.secret.$touch()"
>
<template #right>
<BaseIcon
v-if="isShowPassword"
class="mr-1 text-gray-500 cursor-pointer"
name="EyeOffIcon"
@click="isShowPassword = !isShowPassword"
/>
<BaseIcon
v-else
class="mr-1 text-gray-500 cursor-pointer"
name="EyeIcon"
@click="isShowPassword = !isShowPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
<!-- Mailgun Endpoint -->
<BaseInputGroup
:label="$t(`${pre_t}.endpoint`)"
:error="v$.endpoint.$error && v$.endpoint.$errors[0].$message"
required
>
<BaseInput
v-model.trim="mailSenderStore.mailgunConfig.endpoint"
type="text"
:invalid="v$.endpoint.$error"
@input="v$.endpoint.$touch()"
/>
</BaseInputGroup>
</template>
<script setup>
import { computed, ref } from "vue"
import { useI18n } from "vue-i18n"
import { required, email, numeric, helpers } from "@vuelidate/validators"
import { useVuelidate } from "@vuelidate/core"
const pre_t = "settings.mail_sender.mailgun_config"
const { t } = useI18n()
const props = defineProps({
mailSenderStore: {
type: Object,
require: true,
default: Object,
},
})
let isShowPassword = ref(false)
const getInputType = computed(() => {
if (isShowPassword.value) {
return "text"
}
return "password"
})
const rules = computed(() => {
return {
domain: {
required: helpers.withMessage(t("validation.required"), required),
},
endpoint: {
required: helpers.withMessage(t("validation.required"), required),
},
secret: {
required: helpers.withMessage(t("validation.required"), required),
},
}
})
const v$ = useVuelidate(
rules,
computed(() => props.mailSenderStore.mailgunConfig)
)
</script>

View File

@ -1,143 +0,0 @@
<template>
<!-- Host -->
<BaseInputGroup
:label="$t(`${pre_t}.host`)"
:error="v$.host.$error && v$.host.$errors[0].$message"
required
>
<BaseInput
v-model.trim="mailSenderStore.sesConfig.host"
:invalid="v$.host.$error"
type="text"
@input="v$.host.$touch()"
/>
</BaseInputGroup>
<!-- Port -->
<BaseInputGroup
:label="$t(`${pre_t}.port`)"
:error="v$.port.$error && v$.port.$errors[0].$message"
required
>
<BaseInput
v-model.trim="mailSenderStore.sesConfig.port"
type="text"
:invalid="v$.port.$error"
@input="v$.port.$touch()"
/>
</BaseInputGroup>
<!-- Encryption -->
<BaseInputGroup
:label="$t(`${pre_t}.encryption`)"
:error="v$.encryption.$error && v$.encryption.$errors[0].$message"
required
>
<BaseMultiselect
v-model.trim="mailSenderStore.sesConfig.encryption"
:options="encryptions"
:searchable="true"
:show-labels="false"
:placeholder="$t('general.select_option')"
:invalid="v$.encryption.$error"
@input="v$.encryption.$touch()"
/>
</BaseInputGroup>
<!-- SES Key -->
<BaseInputGroup
:label="$t(`${pre_t}.ses_key`)"
:error="v$.ses_key.$error && v$.ses_key.$errors[0].$message"
required
>
<BaseInput
v-model.trim="mailSenderStore.sesConfig.ses_key"
type="text"
:invalid="v$.ses_key.$error"
@input="v$.ses_key.$touch()"
/>
</BaseInputGroup>
<!-- SES Secret -->
<BaseInputGroup
:label="$t(`${pre_t}.ses_secret`)"
:error="v$.ses_secret.$error && v$.ses_secret.$errors[0].$message"
required
>
<BaseInput
v-model="mailSenderStore.sesConfig.ses_secret"
:type="getInputType"
autocomplete="off"
:invalid="v$.ses_secret.$error"
@input="v$.ses_secret.$touch()"
>
<template #right>
<BaseIcon
v-if="isShowPassword"
class="mr-1 text-gray-500 cursor-pointer"
name="EyeOffIcon"
@click="isShowPassword = !isShowPassword"
/>
<BaseIcon
v-else
class="mr-1 text-gray-500 cursor-pointer"
name="EyeIcon"
@click="isShowPassword = !isShowPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
</template>
<script setup>
import { computed, ref, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { required, email, numeric, helpers } from '@vuelidate/validators'
import { useVuelidate } from '@vuelidate/core'
const pre_t = 'settings.mail_sender.ses_config'
const { t } = useI18n()
const props = defineProps({
mailSenderStore: {
type: Object,
require: true,
default: Object,
},
})
let isShowPassword = ref(false)
const getInputType = computed(() => {
if (isShowPassword.value) {
return 'text'
}
return 'password'
})
const encryptions = props.mailSenderStore.mail_encryptions
const rules = computed(() => {
return {
host: {
required: helpers.withMessage(t('validation.required'), required),
},
port: {
required: helpers.withMessage(t('validation.required'), required),
numeric,
},
encryption: {
required: helpers.withMessage(t('validation.required'), required),
},
ses_key: {
required: helpers.withMessage(t('validation.required'), required),
},
ses_secret: {
required: helpers.withMessage(t('validation.required'), required),
},
}
})
const v$ = useVuelidate(
rules,
computed(() => props.mailSenderStore.sesConfig)
)
</script>

View File

@ -1,120 +0,0 @@
<template>
<!-- Host -->
<BaseInputGroup
:label="$t(`${pre_t}.host`)"
:error="v$.host.$error && v$.host.$errors[0].$message"
required
>
<BaseInput
v-model.trim="mailSenderStore.smtpConfig.host"
:invalid="v$.host.$error"
type="text"
@input="v$.host.$touch()"
/>
</BaseInputGroup>
<!-- Port -->
<BaseInputGroup
:label="$t(`${pre_t}.port`)"
:error="v$.port.$error && v$.port.$errors[0].$message"
required
>
<BaseInput
v-model.trim="mailSenderStore.smtpConfig.port"
type="text"
:invalid="v$.port.$error"
@input="v$.port.$touch()"
/>
</BaseInputGroup>
<!-- Username -->
<BaseInputGroup :label="$t(`${pre_t}.username`)">
<BaseInput v-model.trim="mailSenderStore.smtpConfig.username" type="text" />
</BaseInputGroup>
<!-- Password -->
<BaseInputGroup :label="$t(`${pre_t}.password`)">
<BaseInput
v-model="mailSenderStore.smtpConfig.password"
:type="getInputType"
>
<template #right>
<BaseIcon
v-if="isShowPassword"
class="mr-1 text-gray-500 cursor-pointer"
name="EyeOffIcon"
@click="isShowPassword = !isShowPassword"
/>
<BaseIcon
v-else
class="mr-1 text-gray-500 cursor-pointer"
name="EyeIcon"
@click="isShowPassword = !isShowPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
<!-- Encryption -->
<BaseInputGroup
:label="$t(`${pre_t}.encryption`)"
:error="v$.encryption.$error && v$.encryption.$errors[0].$message"
required
>
<BaseMultiselect
v-model.trim="mailSenderStore.smtpConfig.encryption"
:options="encryptions"
:searchable="true"
:show-labels="false"
:placeholder="$t('general.select_option')"
:invalid="v$.encryption.$error"
@input="v$.encryption.$touch()"
/>
</BaseInputGroup>
</template>
<script setup>
import { computed, ref, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { required, numeric, helpers } from '@vuelidate/validators'
import { useVuelidate } from '@vuelidate/core'
const pre_t = 'settings.mail_sender.smtp_config'
const { t } = useI18n()
const props = defineProps({
mailSenderStore: {
type: Object,
require: true,
default: Object,
},
})
let isShowPassword = ref(false)
const getInputType = computed(() => {
if (isShowPassword.value) {
return 'text'
}
return 'password'
})
const encryptions = props.mailSenderStore.mail_encryptions
const rules = computed(() => {
return {
host: {
required: helpers.withMessage(t('validation.required'), required),
},
port: {
required: helpers.withMessage(t('validation.required'), required),
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
},
encryption: {
required: helpers.withMessage(t('validation.required'), required),
},
}
})
const v$ = useVuelidate(
rules,
computed(() => props.mailSenderStore.smtpConfig)
)
</script>

View File

@ -162,7 +162,7 @@
</template>
<script setup>
import { ref, computed, reactive, onMounted } from 'vue'
import { ref, computed, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import {

View File

@ -54,6 +54,7 @@
bg-white
rounded-lg
text-left
overflow-hidden
relative
shadow-xl
transition-all

View File

@ -12,7 +12,7 @@
"settings": "Nastavení",
"logout": "Odhlásit se",
"users": "Uživatelé",
"modules": "Moduly"
"modules": "Modules"
},
"general": {
"add_company": "Přidat firmu",
@ -93,14 +93,14 @@
"no_note_found": "Nebyly nalezeny žádné poznámky",
"insert_note": "Vložit poznámku",
"copied_pdf_url_clipboard": "Adresa PDF zkopírována do schránky!",
"copied_url_clipboard": "Zkopírováno do schránky!",
"copied_url_clipboard": "Copied url to clipboard!",
"docs": "Dokumentace",
"do_you_wish_to_continue": "Přejete si pokračovat?",
"note": "Poznámka",
"pay_invoice": "Zaplatit fakturu",
"login_successfully": "Přihlášení proběhlo úspěšně!",
"logged_out_successfully": "Odhlášení proběhlo úspěšně",
"mark_as_default": "Označit jako výchozí"
"pay_invoice": "Pay Invoice",
"login_successfully": "Logged in successfully!",
"logged_out_successfully": "Logged out successfully",
"mark_as_default": "Mark as default"
},
"dashboard": {
"select_year": "Vybrat rok",
@ -108,8 +108,8 @@
"due_amount": "Částka k zaplacení",
"customers": "Zákazníci",
"invoices": "Faktury",
"estimates": "Nabídky",
"payments": "Platby"
"estimates": "Odhady",
"payments": "Payments"
},
"chart_info": {
"total_sales": "Slevy",
@ -187,7 +187,7 @@
"website": "Webová stránka",
"overview": "Přehled",
"invoice_prefix": "Prefix pro faktury",
"estimate_prefix": "Prefix pro nabídky",
"estimate_prefix": "Prefix pro odhady",
"payment_prefix": "Prefix pro platby",
"enable_portal": "Povolit portál",
"country": "Země",
@ -208,10 +208,10 @@
"new_customer": "Nový zákazník",
"edit_customer": "Upravit zákazníka",
"basic_info": "Základní informace",
"portal_access": "Přístup do portálu",
"portal_access_text": "Chcete povolit tomuto zákazníkovi možnost přihlásit se na zákaznický portál?",
"portal_access_url": "URL pro přihlášení do zákaznického portálu",
"portal_access_url_help": "Zkopírujte a pošlete výše uvedenou adresu URL vašemu zákazníkovi pro poskytnutí přístupu.",
"portal_access": "Portal Access",
"portal_access_text": "Would you like to allow this customer to login to the Customer Portal?",
"portal_access_url": "Customer Portal Login URL",
"portal_access_url_help": "Please copy & forward the above given URL to your customer for providing access.",
"billing_address": "Fakturační adresa",
"shipping_address": "Doručovací adresa",
"copy_billing_address": "Zkopírovat z fakturace",
@ -231,7 +231,7 @@
"confirm_delete": "Nebudete moci obnovit tohoto zákazníka a všechny jeho faktury, odhady a platby. | Nebudete moci obnovit tyto zákazníky a všechny jejich faktury, odhady a platby.",
"created_message": "Zákazník úspěšně vytvořen",
"updated_message": "Zákazník úspěšně upraven",
"address_updated_message": "Adresa úspěšně aktualizována",
"address_updated_message": "Address Information Updated succesfully",
"deleted_message": "Zákazník úspěšně smazán | Zákazníci úspěšně smazáni",
"edit_currency_not_allowed": "Po vytvoření transakce nelze změnit měnu."
},
@ -264,11 +264,11 @@
"deleted_message": "Položka byla úspěšně odstraněna | Položky byly úspěšně odstraněny"
},
"estimates": {
"title": "Nabídky",
"accept_estimate": "Přijmout nabídku",
"reject_estimate": "Odmítnout nabídku",
"estimate": "Nabídka | Nabídky",
"estimates_list": "Seznam nabídek",
"title": "Odhady",
"accept_estimate": "Accept Estimate",
"reject_estimate": "Reject Estimate",
"estimate": "Odhad | Odhady",
"estimates_list": "Seznam odhadů",
"days": "{days} dní",
"months": "{months} měsíc",
"years": "{years} rok",
@ -283,7 +283,7 @@
"total": "Celkem",
"discount": "Sleva",
"sub_total": "Mezisoučet",
"estimate_number": "Číslo nabídky",
"estimate_number": "Odhadované číslo",
"ref_number": "Referenční číslo",
"contact": "Kontakt",
"add_item": "Přidat položku",
@ -299,11 +299,11 @@
"estimate_template": "Šablona",
"convert_to_invoice": "Převést na fakturu",
"mark_as_sent": "Označit jako odeslané",
"send_estimate": "Odeslat nabídku",
"resend_estimate": "Znovu odeslat nabídku",
"send_estimate": "Odeslat odhad",
"resend_estimate": "Znovu odeslat odhad",
"record_payment": "Zaznamenat platbu",
"add_estimate": "Přidat nabídku",
"save_estimate": "Uložit nabídku",
"add_estimate": "Přidat odhad",
"save_estimate": "Uložit odhad",
"confirm_conversion": "Tento odhad bude použit k vytvoření nové faktury.",
"conversion_message": "Faktura byla úspěšně vytvořena",
"confirm_send_estimate": "Tento odhad bude zaslán e-mailem zákazníkovi",
@ -318,10 +318,10 @@
},
"accepted": "Přijato",
"rejected": "Odmítnuto",
"expired": "Vypršela platnost",
"expired": "Expired",
"sent": "Odesláno",
"draft": "Koncept",
"viewed": "Zobrazené",
"viewed": "Viewed",
"declined": "Odmítnuto",
"new_estimate": "Nový odhad",
"add_new_estimate": "Přidat nový odhad",
@ -355,14 +355,14 @@
"select_an_item": "Pište nebo klikněte pro výběr položky",
"type_item_description": "Zadejte popis položky (volitelné)"
},
"mark_as_default_estimate_template_description": "Je-li povoleno, bude vybraná šablona automaticky vybrána pro nové nabídky."
"mark_as_default_estimate_template_description": "If enabled, the selected template will be automatically selected for new estimates."
},
"invoices": {
"title": "Faktury",
"download": "Stáhnout",
"pay_invoice": "Zaplatit fakturu",
"download": "Download",
"pay_invoice": "Pay Invoice",
"invoices_list": "Seznam faktur",
"invoice_information": "Informace o faktuře",
"invoice_information": "Invoice Information",
"days": "{days} dní",
"months": "{months} měsíc",
"years": "{years} rok",
@ -447,7 +447,7 @@
"marked_as_sent_message": "Faktura označena jako úspěšně odeslaná",
"something_went_wrong": "něco se nezdařilo",
"invalid_due_amount_message": "Celková částka faktury nemůže být nižší než celková částka zaplacená za tuto fakturu. Chcete-li pokračovat, upravte fakturu nebo smažte související platby.",
"mark_as_default_invoice_template_description": "Je-li povoleno, bude vybraná šablona automaticky vybrána pro nové faktury."
"mark_as_default_invoice_template_description": "If enabled, the selected template will be automatically selected for new invoices."
},
"recurring_invoices": {
"title": "Opakující se faktury",
@ -526,7 +526,7 @@
"cloned_successfully": "Opakující se faktura úspěšně naklonována",
"clone_invoice": "Naklonovat opakující se fakturu",
"confirm_clone": "Tato opakující se faktura bude naklonována do nové opakující se faktury",
"add_customer_email": "Pro automatické odesílání faktur prosím přidejte e-mailovou adresu tohoto zákazníka.",
"add_customer_email": "Please add an email address for this customer to send invoices automatically.",
"item": {
"title": "Název položky",
"description": "Popis",
@ -658,49 +658,49 @@
"retype_password": "Zadejte heslo znovu"
},
"modules": {
"buy_now": "Koupit",
"install": "Instalovat",
"price": "Cena",
"download_zip_file": "Stáhnout soubor ZIP",
"unzipping_package": "Rozbalování balíku",
"copying_files": "Kopírování souborů",
"deleting_files": "Odstraňování nepoužitých souborů",
"completing_installation": "Dokončování instalace",
"update_failed": "Aktualizace se nezdařila",
"install_success": "Modul byl úspěšně nainstalován!",
"customer_reviews": "Recenze",
"license": "Licence",
"faq": "Často kladené dotazy (FAQ)",
"monthly": "Měsíčně",
"yearly": "Ročně",
"updated": "Aktualizováno",
"version": "Verze",
"disable": "Zakázat",
"module_disabled": "Modul zakázán",
"enable": "Povolit",
"module_enabled": "Modul povolen",
"update_to": "Aktualizovat na",
"module_updated": "Modul byl úspěšně aktualizován!",
"title": "Moduly",
"module": "Modul | Moduly",
"buy_now": "Buy Now",
"install": "Install",
"price": "Price",
"download_zip_file": "Download ZIP file",
"unzipping_package": "Unzipping Package",
"copying_files": "Copying Files",
"deleting_files": "Deleting Unused files",
"completing_installation": "Completing Installation",
"update_failed": "Update Failed",
"install_success": "Module has been installed successfully!",
"customer_reviews": "Reviews",
"license": "License",
"faq": "FAQ",
"monthly": "Monthly",
"yearly": "Yearly",
"updated": "Updated",
"version": "Version",
"disable": "Disable",
"module_disabled": "Module Disabled",
"enable": "Enable",
"module_enabled": "Module Enabled",
"update_to": "Update To",
"module_updated": "Module Updated Successfully!",
"title": "Modules",
"module": "Module | Modules",
"api_token": "API token",
"invalid_api_token": "Neplatný API token.",
"other_modules": "Další moduly",
"view_all": "Zobrazit vše",
"no_reviews_found": "Pro tento modul zatím neexistují žádné recenze!",
"module_not_purchased": "Modul není zakoupený",
"module_not_found": "Modul nebyl nalezen",
"invalid_api_token": "Invalid API Token.",
"other_modules": "Other Modules",
"view_all": "View All",
"no_reviews_found": "There are no reviews for this module yet!",
"module_not_purchased": "Module Not Purchased",
"module_not_found": "Module Not Found",
"version_not_supported": "This module version doesn't support the current version of Crater",
"last_updated": "Naposledy aktualizováno",
"connect_installation": "Připojte vaši instalaci",
"api_token_description": "Přihlaste se k {url} a připojte tuto instalaci zadáním API tokenu. Vaše zakoupené moduly se zde zobrazí po navázání připojení.",
"view_module": "Zobrazit modul",
"update_available": "Je k dispozici aktualizace",
"purchased": "Zakoupeno",
"installed": "Nainstalováno",
"no_modules_installed": "Nejsou nainstalovány žádné moduly!",
"disable_warning": "Všechna nastavení pro tuto konkrétní položku budou vrácena zpět.",
"what_you_get": "Co získáte"
"last_updated": "Last Updated On",
"connect_installation": "Connect your installation",
"api_token_description": "Login to {url} and connect this installation by entering the API Token. Your purchased modules will show up here after the connection is established.",
"view_module": "View Module",
"update_available": "Update Available",
"purchased": "Purchased",
"installed": "Installed",
"no_modules_installed": "No Modules Installed Yet!",
"disable_warning": "All the settings for this particular will be reverted.",
"what_you_get": "What you get"
},
"users": {
"title": "Uživatelé",
@ -731,7 +731,7 @@
"companies": "Společnosti"
},
"reports": {
"title": "Hlášení",
"title": "Report",
"from_date": "Datum od",
"to_date": "Do data",
"status": "Stav",
@ -739,8 +739,8 @@
"unpaid": "Nezaplaceno",
"download_pdf": "Stáhnout PDF",
"view_pdf": "Zobrazit PDF",
"update_report": "Upravit hlášení",
"report": "Hlášení | Hlášení",
"update_report": "Upravit report",
"report": "Report | Reporty",
"profit_loss": {
"profit_loss": "Zisk a ztráta",
"to_date": "Do data",
@ -752,7 +752,7 @@
"date_range": "Vybrat časový rozsah",
"to_date": "Do data",
"from_date": "Od data",
"report_type": "Typ hlášení"
"report_type": "Typ reportu"
},
"taxes": {
"taxes": "Daně",
@ -807,10 +807,10 @@
"payment_modes": "Způsoby plateb",
"notes": "Poznámky",
"exchange_rate": "Směnný kurz",
"address_information": "Adresa"
"address_information": "Address Information"
},
"address_information": {
"section_description": " Adresu můžete aktualizovat pomocí formuláře níže."
"section_description": " You can update Your Address information using form below."
},
"title": "Nastavení",
"setting": "Nastavení | Nastavení",
@ -1114,7 +1114,7 @@
"exchange_help_text": "Zadejte směnný kurz pro převod z {currency} do {baseCurrency}",
"currency_freak": "Currency Freak",
"currency_layer": "Currency Layer",
"open_exchange_rate": "Otevřít směnný kurz",
"open_exchange_rate": "Open Exchange Rate",
"currency_converter": "Převodník měn",
"server": "Server",
"url": "URL",
@ -1150,8 +1150,8 @@
"payment_mode_added": "Platební metoda přidána",
"payment_mode_updated": "Platební metoda upravena",
"payment_mode_confirm_delete": "Nebudete moci obnovit tuto platební metodu",
"payments_attached": "Tento způsob platby je již připojen k platbám. Chcete-li pokračovat v odstranění, odstraňte připojené platby.",
"expenses_attached": "Tento způsob platby je již připojen k výdajům. Chcete-li pokračovat v odstranění, odstraňte připojené výdaje.",
"payments_attached": "This payment method is already attached to payments. Please delete the attached payments to proceed with deletion.",
"expenses_attached": "This payment method is already attached to expenses. Please delete the attached expenses to proceed with deletion.",
"deleted_message": "Platební metoda byla úspěšně odstraněna"
},
"expense_category": {
@ -1178,8 +1178,8 @@
"discount_setting": "Nastavení slev",
"discount_per_item": "Sleva za položku ",
"discount_setting_description": "Povolte tuto možnost, pokud chcete přidat slevu do jednotlivých položek faktury. Ve výchozím nastavení je sleva přidána přímo na fakturu.",
"expire_public_links": "Automaticky zrušit platnost veřejných odkazů",
"expire_setting_description": "Určete, zda chcete zrušit všechny odkazy odeslané aplikací k zobrazení faktur, odhadů, plateb atd. po stanovené době trvání.",
"expire_public_links": "Automatically Expire Public Links",
"expire_setting_description": "Specify whether you would like to expire all the links sent by application to view invoices, estimates & payments, etc after a specified duration.",
"save": "Uložit",
"preference": "Předvolba | Předvolby",
"general_settings": "Výchozí předvolby systému.",
@ -1301,16 +1301,16 @@
"invalid_disk_credentials": "Nesprávné přihlašovací údaje pro vybraný disk"
},
"taxations": {
"add_billing_address": "Zadejte fakturační adresu",
"add_shipping_address": "Zadejte doručovací adresu",
"add_company_address": "Zadejte adresu firmy",
"modal_description": "Níže uvedené informace jsou vyžadovány pro načtení daně z prodeje.",
"add_address": "Přidat adresu pro načtení daně z prodeje.",
"address_placeholder": "Například: Moje Ulice 123",
"city_placeholder": "Například: Praha",
"state_placeholder": "Například: CZ",
"zip_placeholder": "Například: 90024",
"invalid_address": "Zadejte prosím platnou adresu."
"add_billing_address": "Enter Billing Address",
"add_shipping_address": "Enter Shipping Address",
"add_company_address": "Enter Company Address",
"modal_description": "The information below is required in order to fetch sales tax.",
"add_address": "Add Address for fetching sales tax.",
"address_placeholder": "Example: 123, My Street",
"city_placeholder": "Example: Los Angeles",
"state_placeholder": "Example: CA",
"zip_placeholder": "Example: 90024",
"invalid_address": "Please provide valid address details."
}
},
"wizard": {
@ -1470,17 +1470,17 @@
"not_allowed": "Není povoleno",
"login_invalid_credentials": "Tyto údaje neodpovídají našim záznamům.",
"enter_valid_cron_format": "Zadejte platný formát cronu",
"email_could_not_be_sent": "E-mail nemohl být odeslán na tuto e-mailovou adresu.",
"invalid_address": "Zadejte prosím platnou adresu.",
"invalid_key": "Zadejte prosím platný klíč.",
"invalid_state": "Zadejte prosím platný název státu.",
"invalid_city": "Zadejte prosím platný název města.",
"invalid_postal_code": "Zadejte prosím platné PSČ.",
"invalid_format": "Zadejte prosím data v platném formátu.",
"api_error": "Server neodpovídá.",
"feature_not_enabled": "Funkce není zapnuta.",
"request_limit_met": "Limit požadavků API překročen.",
"address_incomplete": "Neúplná adresa"
"email_could_not_be_sent": "Email could not be sent to this email address.",
"invalid_address": "Please enter a valid address.",
"invalid_key": "Please enter valid key.",
"invalid_state": "Please enter a valid state.",
"invalid_city": "Please enter a valid city.",
"invalid_postal_code": "Please enter a valid zip.",
"invalid_format": "Please enter valid query string format.",
"api_error": "Server not responding.",
"feature_not_enabled": "Feature not enabled.",
"request_limit_met": "Api request limit exceeded.",
"address_incomplete": "Incomplete Address"
},
"pdf_estimate_label": "Odhad",
"pdf_estimate_number": "Číslo odhadu",
@ -1504,18 +1504,18 @@
"pdf_payment_number": "Číslo platby",
"pdf_payment_mode": "Platební metoda",
"pdf_payment_amount_received_label": "Obdržená částka",
"pdf_expense_report_label": "HLÁŠENÍ VÝDAJŮ",
"pdf_expense_report_label": "REPORT VÝDAJŮ",
"pdf_total_expenses_label": "VÝDAJE CELKEM",
"pdf_profit_loss_label": "HLÁŠENÍ ZISKU A ZTRÁT",
"pdf_sales_customers_label": "Hlášení o zákaznících prodeje",
"pdf_sales_items_label": "Hlášení o položkách prodeje",
"pdf_tax_summery_label": "Hlášení o daních",
"pdf_profit_loss_label": "REPORT ZISKU A ZTRÁT",
"pdf_sales_customers_label": "Report o zákaznících prodeje",
"pdf_sales_items_label": "Report o položkách prodeje",
"pdf_tax_summery_label": "Report o shrnutí daní",
"pdf_income_label": "PŘÍJEM",
"pdf_net_profit_label": "ČISTÝ ZISK",
"pdf_customer_sales_report": "Hlášení o prodeji: Podle zákazníka",
"pdf_customer_sales_report": "Report o prodeji: Podle zákazníka",
"pdf_total_sales_label": "PRODEJE CELKEM",
"pdf_item_sales_label": "Hlášení o prodeji: Podle položky",
"pdf_tax_report_label": "DAŇOVÉ HLÁŠENÍ",
"pdf_item_sales_label": "Report o prodeji: Podle položky",
"pdf_tax_report_label": "DAŇOVÝ REPORT",
"pdf_total_tax_label": "DANĚ CELKEM",
"pdf_tax_types_label": "Typy daní",
"pdf_expenses_label": "Výdaje",

View File

@ -526,7 +526,7 @@
"cloned_successfully": "Serienrechnung erfolgreich kopiert",
"clone_invoice": "Serienrechnung kopieren",
"confirm_clone": "Diese Serienrechnung wird in eine neue Serienrechnung kopiert",
"add_customer_email": "Bitte fügen Sie eine E-Mail-Adresse für diesen Kunden hinzu, um Rechnungen automatisch zu senden.",
"add_customer_email": "Please add an email address for this customer to send invoices automatically.",
"item": {
"title": "Titel des Artikels",
"description": "Beschreibung",
@ -682,25 +682,25 @@
"update_to": "Update auf",
"module_updated": "Modul erfolgreich aktualisiert!",
"title": "Module",
"module": "Modul | Module",
"module": "Module | Modules",
"api_token": "API Schlüssel",
"invalid_api_token": "Ungültiger API-Schlüssel.",
"invalid_api_token": "Invalid API Token.",
"other_modules": "Weitere Module",
"view_all": "Alle Anzeigen",
"no_reviews_found": "Für dieses Modul gibt es noch keine Bewertungen!",
"no_reviews_found": "There are no reviews for this module yet!",
"module_not_purchased": "Module Not Purchased",
"module_not_found": "Modul nicht gefunden",
"module_not_found": "Module Not Found",
"version_not_supported": "This module version doesn't support the current version of Crater",
"last_updated": "Zuletzt aktualisiert am",
"connect_installation": "Installation verbinden",
"api_token_description": "Melden Sie sich bei {url} an und verbinden Sie diese Installation durch Eingabe des API-Token. Ihre gekauften Module werden hier angezeigt, nachdem die Verbindung hergestellt wurde.",
"api_token_description": "Login to {url} and connect this installation by entering the API Token. Your purchased modules will show up here after the connection is established.",
"view_module": "Modul anzeigen",
"update_available": "Aktualisierung verfügbar",
"purchased": "Gekauft",
"installed": "Installiert",
"no_modules_installed": "Noch keine Module installiert!",
"disable_warning": "Alle Einstellungen für diesen speziellen Wert werden zurückgesetzt.",
"what_you_get": "Was Sie erhalten"
"disable_warning": "All the settings for this particular will be reverted.",
"what_you_get": "What you get"
},
"users": {
"title": "Benutzer",
@ -1304,8 +1304,8 @@
"add_billing_address": "Rechnungsadresse eingeben",
"add_shipping_address": "Lieferadresse eingeben",
"add_company_address": "Firmenadresse eingeben",
"modal_description": "Die untenstehenden Informationen sind erforderlich, um die Steuer berücksichtigen zu können.",
"add_address": "Fügen Sie eine Adresse hinzu, um die Steuer abrufen zu können.",
"modal_description": "The information below is required in order to fetch sales tax.",
"add_address": "Add Address for fetching sales tax.",
"address_placeholder": "Beispiel: 123, meine Straße",
"city_placeholder": "Beispiel: Los Angeles",
"state_placeholder": "Beispiel: CA",
@ -1471,21 +1471,21 @@
"login_invalid_credentials": "Diese Anmeldeinformationen stimmen nicht mit unseren Aufzeichnungen überein.",
"enter_valid_cron_format": "Bitte geben Sie ein gültiges Cron-Format ein",
"email_could_not_be_sent": "Die E-Mail konnte nicht an diese Adresse gesendet werden.",
"invalid_address": "Bitte geben Sie eine gültige Adresse ein.",
"invalid_key": "Bitte geben Sie einen gültigen Schlüssel ein.",
"invalid_state": "Bitte geben Sie ein gültiges Bundesland ein.",
"invalid_city": "Bitte geben Sie eine gültige Stadt an.",
"invalid_postal_code": "Bitte geben Sie eine gültige PLZ an.",
"invalid_format": "Bitte geben Sie ein gültiges Abfrageformat ein.",
"invalid_address": "Please enter a valid address.",
"invalid_key": "Please enter valid key.",
"invalid_state": "Please enter a valid state.",
"invalid_city": "Please enter a valid city.",
"invalid_postal_code": "Please enter a valid zip.",
"invalid_format": "Please enter valid query string format.",
"api_error": "Der Server antwortet nicht.",
"feature_not_enabled": "Funktion nicht aktiviert.",
"request_limit_met": "Api Anfragelimit überschritten.",
"request_limit_met": "Api request limit exceeded.",
"address_incomplete": "Unvollständige Adresse"
},
"pdf_estimate_label": "Angebot",
"pdf_estimate_number": "Angebotsnummer",
"pdf_estimate_date": "Angebotsdatum",
"pdf_estimate_expire_date": "Gültig bis",
"pdf_estimate_expire_date": "Ablaufdatum",
"pdf_invoice_label": "Rechnung",
"pdf_invoice_number": "Rechnungsnummer",
"pdf_invoice_date": "Rechnungsdatum",
@ -1519,8 +1519,8 @@
"pdf_total_tax_label": "Gesamte Umsatzsteuer",
"pdf_tax_types_label": "Steuersätze",
"pdf_expenses_label": "Ausgaben",
"pdf_bill_to": "Rechnungsanschrift",
"pdf_ship_to": "Lieferanschrift",
"pdf_bill_to": "Rechnungsempfänger:",
"pdf_ship_to": "Versand an:",
"pdf_received_from": "Erhalten von:",
"pdf_tax_label": "Steuer"
}

View File

@ -6,13 +6,13 @@
"invoices": "Τιμολόγια",
"recurring-invoices": "Επαναλαμβανόμενα τιμολόγια",
"expenses": "Έξοδα",
"estimates": "Προσφορές",
"estimates": "Εκτιμήσεις",
"payments": "Πληρωμές",
"reports": "Αναφορές",
"settings": "Ρυθμίσεις",
"logout": "Αποσύνδεση",
"users": "Χρήστες",
"modules": "Πρόσθετα"
"modules": "Modules"
},
"general": {
"add_company": "Προσθήκη Εταιρείας",
@ -93,14 +93,14 @@
"no_note_found": "Δεν Βρέθηκε Σημείωση",
"insert_note": "Εισαγωγή Σημείωσης",
"copied_pdf_url_clipboard": "Αντιγράφηκε το url του PDF στo πρόχειρο!",
"copied_url_clipboard": "Ο σύνδεσμος αντιγράφηκε στο πρόχειρο!",
"copied_url_clipboard": "Copied url to clipboard!",
"docs": "Έγγραφα",
"do_you_wish_to_continue": "Θέλετε να συνεχίσετε;",
"note": "Σημείωση",
"pay_invoice": "Πληρωμή τιμολογίου",
"login_successfully": "Η είσοδος ήταν επιτυχής!",
"logged_out_successfully": "Η έξοδος ήταν επιτυχής",
"mark_as_default": "Σημείωση ως προεπιλογή"
"pay_invoice": "Pay Invoice",
"login_successfully": "Logged in successfully!",
"logged_out_successfully": "Logged out successfully",
"mark_as_default": "Mark as default"
},
"dashboard": {
"select_year": "Επιλογή έτους",
@ -108,8 +108,8 @@
"due_amount": "Οφειλόμενο Ποσό",
"customers": "Πελάτες",
"invoices": "Τιμολόγια",
"estimates": "Προσφορές",
"payments": "Πληρωμές"
"estimates": "Εκτιμήσεις",
"payments": "Payments"
},
"chart_info": {
"total_sales": "Πωλήσεις",
@ -130,7 +130,7 @@
"view_all": "Προβολή Όλων"
},
"recent_estimate_card": {
"title": "Πρόσφατες προσφορές",
"title": "Πρόσφατες Εκτιμήσεις",
"date": "Ημερομηνία",
"customer": "Πελάτης",
"amount_due": "Οφειλόμενο Ποσό",
@ -264,18 +264,18 @@
"deleted_message": "Ο υπολογισμός διαγράφηκε επιτυχώς"
},
"estimates": {
"title": "Προσφορές",
"title": "Εκτιμήσεις",
"accept_estimate": "Accept Estimate",
"reject_estimate": "Reject Estimate",
"estimate": "Προσφορά | Προσφορές",
"estimates_list": "Λίστα προσφορών",
"estimate": "Εκτίμηση | Εκτιμήσεις",
"estimates_list": "Λίστα Εκτιμήσεων",
"days": "{days} Ημέρες",
"months": "{months} Μήνας",
"years": "{years} Έτος",
"all": "Όλα",
"paid": "Εξοφλημένο",
"unpaid": "Ανεξόφλητο",
"customer": "Πελάτης",
"customer": "ΤΕΛΩΝΕΙΑΚΗ",
"ref_no": "REF NO.",
"number": "ΑΡΙΘΜΟΣ",
"amount_due": "ΠΟΣΟ ΠΡΟΣ ΠΛΗΡΩΜΗ",
@ -300,7 +300,7 @@
"convert_to_invoice": "Μετατράπηκε σε Τιμολόγιο",
"mark_as_sent": "Σήμανση ως απεσταλμένου",
"send_estimate": "Νέα Εκτίμηση",
"resend_estimate": "Πρόσφατες προσφορές",
"resend_estimate": "Πρόσφατες Εκτιμήσεις",
"record_payment": "Καταγραφή Πληρωμής",
"add_estimate": "Νέα Εκτίμηση",
"save_estimate": "Νέα Εκτίμηση",
@ -310,7 +310,7 @@
"confirm_mark_as_sent": "Η εκτίμηση αυτή θα επισημανθεί ως εστάλη",
"confirm_mark_as_accepted": "Αυτό το τιμολόγιο θα επισημανθεί ως Απορριπτόμενο",
"confirm_mark_as_rejected": "Αυτό το τιμολόγιο θα επισημανθεί ως Απορριπτόμενο",
"no_matching_estimates": "Δεν υπάρχουν αντίστοιχες προσφορές!",
"no_matching_estimates": "Δεν υπάρχουν αντίστοιχες εκτιμήσεις!",
"mark_as_sent_successfully": "Το τιμολόγιο επισημάνθηκε ως απεσταλμένο επιτυχώς",
"send_estimate_successfully": "Το τιμολόγιο εστάλη επιτυχώς",
"errors": {
@ -328,9 +328,9 @@
"update_Estimate": "Ενημέρωση εκτίμησης",
"edit_estimate": "Επεξεργασία Εκτίμησης",
"items": "στοιχεία",
"Estimate": "Προσφορά | Προσφορές",
"Estimate": "Εκτίμηση | Εκτιμήσεις",
"add_new_tax": "Προσθήκη Νέου Φόρου",
"no_estimates": "Δεν υπάρχουν προσφορές ακόμα!",
"no_estimates": "Δεν υπάρχουν εκτιμήσεις ακόμα!",
"list_of_estimates": "Αυτή η ενότητα θα περιέχει τη λίστα των στοιχείων.",
"mark_as_rejected": "Σήμανση ως απορρίφθηκε",
"mark_as_accepted": "Σήμανση ως αποδεκτό",
@ -372,7 +372,7 @@
"viewed": "Προβλήθηκαν",
"overdue": "Εκπρόθεσμα",
"completed": "Ολοκληρώθηκε",
"customer": "Πελάτης",
"customer": "ΤΕΛΩΝΕΙΑΚΗ",
"paid_status": "ΚΑΤΑΣΤΑΣΗ ΠΛΗΡΩΜΗΣ",
"ref_no": "REF NO.",
"number": "ΑΡΙΘΜΟΣ",
@ -462,7 +462,7 @@
"overdue": "Εκπρόθεσμα",
"active": "Ενεργή",
"completed": "Ολοκληρώθηκε",
"customer": "Πελάτης",
"customer": "ΤΕΛΩΝΕΙΑΚΗ",
"paid_status": "ΚΑΤΑΣΤΑΣΗ ΠΛΗΡΩΜΗΣ",
"ref_no": "REF NO.",
"number": "ΑΡΙΘΜΟΣ",
@ -681,7 +681,7 @@
"module_enabled": "Module Enabled",
"update_to": "Update To",
"module_updated": "Module Updated Successfully!",
"title": "Πρόσθετα",
"title": "Modules",
"module": "Module | Modules",
"api_token": "API token",
"invalid_api_token": "Invalid API Token.",
@ -692,13 +692,13 @@
"module_not_found": "Module Not Found",
"version_not_supported": "This module version doesn't support the current version of Crater",
"last_updated": "Last Updated On",
"connect_installation": "Σύνδεση της εγκατάστασης σας",
"api_token_description": "Συνδεθείτε στο {url} και συνδέστε αυτήν την εγκατάσταση εισάγοντας το API Token. Τα πρόσθετα που αγοράσατε θα εμφανιστούν εδώ μετά την ολοκλήρωση της σύνδεσης.",
"view_module": "Δείτε το πρόσθετο",
"update_available": "Διαθέσιμη ανανέωση",
"connect_installation": "Connect your installation",
"api_token_description": "Login to {url} and connect this installation by entering the API Token. Your purchased modules will show up here after the connection is established.",
"view_module": "View Module",
"update_available": "Update Available",
"purchased": "Purchased",
"installed": "Εγκαταστάθηκε",
"no_modules_installed": "Δεν υπάρχουν ακόμα εγκατεστημένα πρόσθετα!",
"installed": "Installed",
"no_modules_installed": "No Modules Installed Yet!",
"disable_warning": "All the settings for this particular will be reverted.",
"what_you_get": "What you get"
},
@ -815,7 +815,7 @@
"title": "Ρυθμίσεις",
"setting": "Ρύθμιση Ρυθμίσεων",
"general": "General",
"language": "Γλώσσα",
"language": "Language",
"primary_currency": "Κύριο Νόμισμα",
"timezone": "Ζώνη Ώρας",
"date_format": "Μορφή Ημερομηνίας",

View File

@ -100,9 +100,7 @@
"pay_invoice": "Pay Invoice",
"login_successfully": "Logged in successfully!",
"logged_out_successfully": "Logged out successfully",
"mark_as_default": "Mark as default",
"select_option": "Select option",
"send_test_mail": "Send Test Mail"
"mark_as_default": "Mark as default"
},
"dashboard": {
"select_year": "Select year",
@ -235,8 +233,7 @@
"updated_message": "Customer updated successfully",
"address_updated_message": "Address Information Updated succesfully",
"deleted_message": "Customer deleted successfully | Customers deleted successfully",
"edit_currency_not_allowed": "Cannot change currency once transactions created.",
"select_sender": "Select Sender"
"edit_currency_not_allowed": "Cannot change currency once transactions created."
},
"items": {
"title": "Items",
@ -731,8 +728,7 @@
"updated_message": "User updated successfully",
"deleted_message": "User deleted successfully | Users deleted successfully",
"select_company_role": "Select Role for {company}",
"companies": "Companies",
"select_sender": "Select Sender"
"companies": "Companies"
},
"reports": {
"title": "Report",
@ -811,8 +807,7 @@
"payment_modes": "Payment Modes",
"notes": "Notes",
"exchange_rate": "Exchange Rate",
"address_information": "Address Information",
"mail_sender": "Mail Senders"
"address_information": "Address Information"
},
"address_information": {
"section_description": " You can update Your Address information using form below."
@ -1316,51 +1311,6 @@
"state_placeholder": "Example: CA",
"zip_placeholder": "Example: 90024",
"invalid_address": "Please provide valid address details."
},
"mail_sender": {
"title": "Mail Sender | Mail Senders",
"description": "Configure & test your mail senders for the selected company.",
"add_new_mail_sender": "New Mail Sender",
"name": "Sender Name",
"name_help": "Type a name to identify the sender for users.",
"driver": "Mail Driver",
"is_default": "Set as default",
"is_default_description": "You can only set one sender as default at a given time.",
"cc": "CC",
"bcc": "BCC",
"from_address": "From Mail Address",
"from_name": "From Mail Name",
"edit_mail_sender": "Edit Mail Sender",
"delete_mail_sender": "Delete Mail Sender",
"confirm_delete": "You will not be able to recover this Mail Sender",
"created_message": "Mail Sender created successfully",
"updated_message": "Mail Sender updated successfully",
"deleted_message": "Mail Sender deleted successfully",
"default_record_exists": "Default mail sender already exist",
"email_list": "Supports a comma separated list of email addresses",
"select_mail_sender": "Select Mail Sender",
"manage_mail_sender": "Manage Mail Senders",
"no_mail_sender_found": "No mail senders found!",
"no_mail_sender_found_description": "You must configure at-least one mail sender for the selected company in order to continue.",
"smtp_config": {
"host": "Host",
"port": "Port",
"username": "Username",
"password": "Password",
"encryption": "Encryption"
},
"mailgun_config": {
"domain": "Domain",
"secret": "Maingun Secret",
"endpoint": "Mailgun Endpoint"
},
"ses_config": {
"host": "Host",
"port": "Port",
"encryption": "Encryption",
"ses_key": "SES Key",
"ses_secret": "SES Secret"
}
}
},
"wizard": {
@ -1535,7 +1485,7 @@
"pdf_estimate_label": "Estimate",
"pdf_estimate_number": "Estimate Number",
"pdf_estimate_date": "Estimate Date",
"pdf_estimate_expire_date": "Expiry Date",
"pdf_estimate_expire_date": "Expiry date",
"pdf_invoice_label": "Invoice",
"pdf_invoice_number": "Invoice Number",
"pdf_invoice_date": "Invoice Date",

View File

@ -12,7 +12,7 @@
"settings": "Ajustes",
"logout": "Cerrar sesión",
"users": "Usuarios",
"modules": "Módulos"
"modules": "Modules"
},
"general": {
"add_company": "Añadir empresa",
@ -49,7 +49,7 @@
"view": "Ver",
"add_new_item": "Agregar ítem nuevo",
"clear_all": "Limpiar todo",
"showing": "Mostrar",
"showing": "Mostrando",
"of": "de",
"actions": "Acciones",
"subtotal": "SUBTOTAL",
@ -93,14 +93,14 @@
"no_note_found": "No se encontró ninguna nota",
"insert_note": "Insertar una nota",
"copied_pdf_url_clipboard": "Copiar Url al portapapeles",
"copied_url_clipboard": "¡URL copiada al portapapeles!",
"copied_url_clipboard": "Copied url to clipboard!",
"docs": "Documentación",
"do_you_wish_to_continue": "¿Deseas continuar?",
"note": "Nota",
"pay_invoice": "Pagar factura",
"login_successfully": "Logeado Satisfactoriamente!",
"logged_out_successfully": "Logeado Satisfactoriamente",
"mark_as_default": "Marcar como predeterminado"
"pay_invoice": "Pay Invoice",
"login_successfully": "Logged in successfully!",
"logged_out_successfully": "Logged out successfully",
"mark_as_default": "Mark as default"
},
"dashboard": {
"select_year": "Seleccionar año",
@ -109,7 +109,7 @@
"customers": "Clientes",
"invoices": "Facturas",
"estimates": "Presupuestos",
"payments": "Ver Medios de Pago"
"payments": "Payments"
},
"chart_info": {
"total_sales": "Ventas",
@ -208,10 +208,10 @@
"new_customer": "Nuevo cliente",
"edit_customer": "Editar cliente",
"basic_info": "Información básica",
"portal_access": "Acceso al portal",
"portal_access_text": "¿Le gustaría permitir que este cliente inicie sesión en el Portal del Cliente?",
"portal_access_url": "Portal URL del cliente",
"portal_access_url_help": "Por favor, copie y reenvíe la URL anterior a su cliente para proporcionar acceso.",
"portal_access": "Portal Access",
"portal_access_text": "Would you like to allow this customer to login to the Customer Portal?",
"portal_access_url": "Customer Portal Login URL",
"portal_access_url_help": "Please copy & forward the above given URL to your customer for providing access.",
"billing_address": "Dirección de Facturación",
"shipping_address": "Dirección de Envío",
"copy_billing_address": "Copia de facturación",
@ -231,7 +231,7 @@
"confirm_delete": "No podrá recuperar este cliente y todas las facturas, estimaciones y pagos relacionados. | No podrá recuperar estos clientes y todas las facturas, estimaciones y pagos relacionados.",
"created_message": "Cliente creado con éxito",
"updated_message": "Cliente actualizado con éxito",
"address_updated_message": "Información del domicilio actualizado correctamente",
"address_updated_message": "Address Information Updated succesfully",
"deleted_message": "Cliente eliminado correctamente | Clientes eliminados exitosamente",
"edit_currency_not_allowed": "No se puede cambiar la divisa una vez creadas las transacciones."
},
@ -265,8 +265,8 @@
},
"estimates": {
"title": "Presupuestos",
"accept_estimate": "Aceptar cotización",
"reject_estimate": "Rechazar cotización",
"accept_estimate": "Accept Estimate",
"reject_estimate": "Reject Estimate",
"estimate": "Presupuesto | Presupuestos",
"estimates_list": "Lista de presupuestos",
"days": "{días} Días",
@ -318,10 +318,10 @@
},
"accepted": "Aceptado",
"rejected": "Rechazado",
"expired": "Caducado",
"expired": "Expired",
"sent": "Enviado",
"draft": "Borrador",
"viewed": "Visto",
"viewed": "Viewed",
"declined": "Rechazado",
"new_estimate": "Nuevo presupuesto",
"add_new_estimate": "Añadir nuevo presupuesto",
@ -355,14 +355,14 @@
"select_an_item": "Escriba o haga clic para seleccionar un elemento",
"type_item_description": "Descripción del tipo de elemento(opcional)"
},
"mark_as_default_estimate_template_description": "Si se activa, esta plantilla se selccionará automáticamente para nuevos presupuestos. "
"mark_as_default_estimate_template_description": "If enabled, the selected template will be automatically selected for new estimates."
},
"invoices": {
"title": "Facturas",
"download": "Descargar",
"pay_invoice": "Pagar factura",
"download": "Download",
"pay_invoice": "Pay Invoice",
"invoices_list": "Lista de facturas",
"invoice_information": "Información de la factura",
"invoice_information": "Invoice Information",
"days": "{días} Días",
"months": "{meses} Mes",
"years": "{años} Año",
@ -447,7 +447,7 @@
"marked_as_sent_message": "Factura marcada como enviada con éxito",
"something_went_wrong": "Algo fue mal",
"invalid_due_amount_message": "El pago introducido es mayor que el importe total pendiente de esta factura. Por favor, verificalo y vuelve a intentarlo.",
"mark_as_default_invoice_template_description": "Si se activa, esta plantilla se seleccionará automáticamente para nuevas facturas. "
"mark_as_default_invoice_template_description": "If enabled, the selected template will be automatically selected for new invoices."
},
"recurring_invoices": {
"title": "Facturas recurrentes",
@ -526,7 +526,7 @@
"cloned_successfully": "Factura recurrente clonada con éxito",
"clone_invoice": "Clonar factura recurrente",
"confirm_clone": "Esta factura recurrente será clonada en una nueva factura recurrente",
"add_customer_email": "Por favor, agregue una dirección de correo electrónico para que este cliente envíe las facturas automáticamente.",
"add_customer_email": "Please add an email address for this customer to send invoices automatically.",
"item": {
"title": "Título del artículo",
"description": "Descripción",
@ -658,49 +658,49 @@
"retype_password": "Reescriba la contraseña"
},
"modules": {
"buy_now": "Comprar ahora",
"install": "Instalar",
"price": "Precio",
"download_zip_file": "Descargar archivo ZIP",
"unzipping_package": "Descomprimir paquete",
"copying_files": "Copiando archivos",
"deleting_files": "Eliminando archivos no usados",
"completing_installation": "Completando la instalación",
"update_failed": "Falló la actualización",
"install_success": "¡El módulo se ha instalado correctamente!",
"customer_reviews": "Reseñas",
"license": "Licencia",
"faq": "Preguntas Frecuentes (FAQ)",
"monthly": "Mensual",
"yearly": "Anual",
"updated": "Actualizado",
"version": "Versión",
"disable": "Deshabilitar",
"module_disabled": "Módulo desactivado",
"enable": "Habilitar",
"module_enabled": "Módulo habilitado",
"update_to": "Actualizar a",
"module_updated": "¡Módulo actualizado correctamente!",
"title": "Módulos",
"module": "Módulo | Módulos",
"buy_now": "Buy Now",
"install": "Install",
"price": "Price",
"download_zip_file": "Download ZIP file",
"unzipping_package": "Unzipping Package",
"copying_files": "Copying Files",
"deleting_files": "Deleting Unused files",
"completing_installation": "Completing Installation",
"update_failed": "Update Failed",
"install_success": "Module has been installed successfully!",
"customer_reviews": "Reviews",
"license": "License",
"faq": "FAQ",
"monthly": "Monthly",
"yearly": "Yearly",
"updated": "Updated",
"version": "Version",
"disable": "Disable",
"module_disabled": "Module Disabled",
"enable": "Enable",
"module_enabled": "Module Enabled",
"update_to": "Update To",
"module_updated": "Module Updated Successfully!",
"title": "Modules",
"module": "Module | Modules",
"api_token": "API token",
"invalid_api_token": "API Token inválido.",
"other_modules": "Otros módulos",
"view_all": "Ver todo",
"no_reviews_found": "¡Este módulo aún no tiene reseñas!",
"module_not_purchased": "Módulo no comprado",
"module_not_found": "Módulo no encontrado",
"invalid_api_token": "Invalid API Token.",
"other_modules": "Other Modules",
"view_all": "View All",
"no_reviews_found": "There are no reviews for this module yet!",
"module_not_purchased": "Module Not Purchased",
"module_not_found": "Module Not Found",
"version_not_supported": "This module version doesn't support the current version of Crater",
"last_updated": "Actualizado",
"connect_installation": "Conecte su instalación",
"api_token_description": "Inicie sesión en {url} y conecte esta instalación introduciendo el token de API. Los módulos comprados aparecerán aquí después de establecer la conexión.",
"view_module": "Ver módulo",
"update_available": "Actualización disponible",
"purchased": "Comprado",
"installed": "Instalado",
"no_modules_installed": "¡No hay módulos instalados todavía!",
"disable_warning": "Se revertirán todos los ajustes para este particular.",
"what_you_get": "Beneficios que obtiene"
"last_updated": "Last Updated On",
"connect_installation": "Connect your installation",
"api_token_description": "Login to {url} and connect this installation by entering the API Token. Your purchased modules will show up here after the connection is established.",
"view_module": "View Module",
"update_available": "Update Available",
"purchased": "Purchased",
"installed": "Installed",
"no_modules_installed": "No Modules Installed Yet!",
"disable_warning": "All the settings for this particular will be reverted.",
"what_you_get": "What you get"
},
"users": {
"title": "Usuarios",
@ -807,10 +807,10 @@
"payment_modes": "Formas de pago",
"notes": "Notas",
"exchange_rate": "Tasa de cambio",
"address_information": "Información de dirección"
"address_information": "Address Information"
},
"address_information": {
"section_description": "Puede actualizar la información de su dirección utilizando el siguiente formulario."
"section_description": " You can update Your Address information using form below."
},
"title": "Configuraciones",
"setting": "Configuraciones | Configuraciones",
@ -1112,9 +1112,9 @@
"error": " No puede eliminar el controlador activo",
"default_currency_error": "Esta moneda ya se usa en uno de los proveedores activos",
"exchange_help_text": "Ingrese el tipo de cambio para convertir de {currency} a {baseCurrency}",
"currency_freak": "Moneda",
"currency_layer": "Capa de moneda",
"open_exchange_rate": "Tasa de cambio",
"currency_freak": "Currency Freak",
"currency_layer": "Currency Layer",
"open_exchange_rate": "Open Exchange Rate",
"currency_converter": "Conversor de moneda",
"server": "Servidor",
"url": "URL",
@ -1150,8 +1150,8 @@
"payment_mode_added": "Forma de pago añadida",
"payment_mode_updated": "Forma de pago actualizada",
"payment_mode_confirm_delete": "No podrás recuperar este Modo de Pago",
"payments_attached": "Esta forma de pago ya está vinculada a los pagos. Por favor, elimine los pagos adjuntos para proceder con la eliminación.",
"expenses_attached": "Esta forma de pago ya está adjunta a los gastos. Por favor, elimine los gastos adjuntos para proceder con la eliminación.",
"payments_attached": "This payment method is already attached to payments. Please delete the attached payments to proceed with deletion.",
"expenses_attached": "This payment method is already attached to expenses. Please delete the attached expenses to proceed with deletion.",
"deleted_message": "Método de pago eliminado correctamente"
},
"expense_category": {
@ -1178,8 +1178,8 @@
"discount_setting": "Ajuste de descuento",
"discount_per_item": "Descuento por artículo",
"discount_setting_description": "Habilítelo si desea agregar Descuento a artículos de factura individuales. Por defecto, los descuentos se agregan directamente a la factura.",
"expire_public_links": "Expirar automáticamente enlaces públicos",
"expire_setting_description": "Especifique si desea expirar todos los enlaces enviados por la aplicación para ver facturas, estimaciones y pagos, etc. después de una duración especificada.",
"expire_public_links": "Automatically Expire Public Links",
"expire_setting_description": "Specify whether you would like to expire all the links sent by application to view invoices, estimates & payments, etc after a specified duration.",
"save": "Guardar",
"preference": "Preferencia | Preferencias",
"general_settings": "Preferencias predeterminadas para el sistema.",
@ -1301,16 +1301,16 @@
"invalid_disk_credentials": "Credencial no válida del disco seleccionado"
},
"taxations": {
"add_billing_address": "Introduzca su dirección de facturación",
"add_shipping_address": "Introduzca la dirección de envío",
"add_company_address": "Introduzca la dirección de la empresa",
"modal_description": "La siguiente información es requerida para obtener el impuesto de venta.",
"add_address": "Añadir dirección para obtener impuestos de venta.",
"address_placeholder": "Ejemplo: 123, Mi Calle",
"city_placeholder": "Ejemplo: Los Angeles",
"state_placeholder": "Ejemplo: CA",
"zip_placeholder": "Ejemplo: 90024",
"invalid_address": "Proporciona una dirección válida."
"add_billing_address": "Enter Billing Address",
"add_shipping_address": "Enter Shipping Address",
"add_company_address": "Enter Company Address",
"modal_description": "The information below is required in order to fetch sales tax.",
"add_address": "Add Address for fetching sales tax.",
"address_placeholder": "Example: 123, My Street",
"city_placeholder": "Example: Los Angeles",
"state_placeholder": "Example: CA",
"zip_placeholder": "Example: 90024",
"invalid_address": "Please provide valid address details."
}
},
"wizard": {
@ -1470,17 +1470,17 @@
"not_allowed": "No permitido",
"login_invalid_credentials": "Estas credenciales no coinciden con nuestros registros.",
"enter_valid_cron_format": "Por favor, introduzca un formato cron válido",
"email_could_not_be_sent": "No se pudo enviar el correo a esta dirección de correo electrónico.",
"invalid_address": "Por favor, introduzca una dirección válida.",
"invalid_key": "Por favor, introduzca una clave válida.",
"invalid_state": "Por favor, introduzca un estado válido.",
"invalid_city": "Por favor, introduzca una ciudad válida.",
"invalid_postal_code": "Por favor, introduzca un código postal válido.",
"invalid_format": "Por favor, introduzca un formato de consulta válido.",
"api_error": "El servidor no responde.",
"feature_not_enabled": "Característica no habilitada.",
"request_limit_met": "Ha alcanzado el límite de solicitudes.",
"address_incomplete": "Dirección incompleta"
"email_could_not_be_sent": "Email could not be sent to this email address.",
"invalid_address": "Please enter a valid address.",
"invalid_key": "Please enter valid key.",
"invalid_state": "Please enter a valid state.",
"invalid_city": "Please enter a valid city.",
"invalid_postal_code": "Please enter a valid zip.",
"invalid_format": "Please enter valid query string format.",
"api_error": "Server not responding.",
"feature_not_enabled": "Feature not enabled.",
"request_limit_met": "Api request limit exceeded.",
"address_incomplete": "Incomplete Address"
},
"pdf_estimate_label": "Presupuestar",
"pdf_estimate_number": "Número de Presupuesto",

View File

@ -100,7 +100,7 @@
"pay_invoice": "Payer facture",
"login_successfully": "Identifié avec succès!",
"logged_out_successfully": "Déconnecté avec succès",
"mark_as_default": "Marquer par défaut"
"mark_as_default": "Mark as default"
},
"dashboard": {
"select_year": "Sélectionnez l'année",
@ -109,7 +109,7 @@
"customers": "Clients",
"invoices": "Factures",
"estimates": "Devis",
"payments": "Paiements"
"payments": "Payments"
},
"chart_info": {
"total_sales": "Ventes",
@ -318,10 +318,10 @@
},
"accepted": "Accepté",
"rejected": "Refusé",
"expired": "Expiré",
"expired": "Expired",
"sent": "Envoyé",
"draft": "Brouillon",
"viewed": "Consultée",
"viewed": "Viewed",
"declined": "Refusé",
"new_estimate": "Nouveau devis",
"add_new_estimate": "Nouveau devis",
@ -658,49 +658,49 @@
"retype_password": "Retaper le mot de passe"
},
"modules": {
"buy_now": "Acheter maintenant",
"install": "Installer",
"price": "Prix",
"download_zip_file": "Télécharger le fichier ZIP",
"unzipping_package": "Décompresser le fichier",
"copying_files": "Copie de fichiers en cours",
"deleting_files": "Supprimer les fichiers inutilisés",
"completing_installation": "Terminer l'installation",
"update_failed": "Échec de la mise à jour",
"install_success": "Votre module a été correctement installé !",
"customer_reviews": "Évaluations",
"buy_now": "Buy Now",
"install": "Install",
"price": "Price",
"download_zip_file": "Download ZIP file",
"unzipping_package": "Unzipping Package",
"copying_files": "Copying Files",
"deleting_files": "Deleting Unused files",
"completing_installation": "Completing Installation",
"update_failed": "Update Failed",
"install_success": "Module has been installed successfully!",
"customer_reviews": "Reviews",
"license": "License",
"faq": "FAQ",
"monthly": "Mensuel",
"yearly": "Annuel",
"updated": "Mis à jour",
"monthly": "Monthly",
"yearly": "Yearly",
"updated": "Updated",
"version": "Version",
"disable": "Désactiver",
"module_disabled": "Module désactivé",
"enable": "Activer",
"module_enabled": "Module activé",
"update_to": "Mise à jour vers",
"module_updated": "Le module a bien été mis à jour !",
"disable": "Disable",
"module_disabled": "Module Disabled",
"enable": "Enable",
"module_enabled": "Module Enabled",
"update_to": "Update To",
"module_updated": "Module Updated Successfully!",
"title": "Modules",
"module": "Module | Modules",
"api_token": "Jeton API",
"invalid_api_token": "Jeton API invalide.",
"other_modules": "Autres modules",
"view_all": "Tout afficher",
"no_reviews_found": "Il n'y a pas encore d'avis pour ce module !",
"module_not_purchased": "Module non acheté",
"module_not_found": "Module non trouvé",
"api_token": "API token",
"invalid_api_token": "Invalid API Token.",
"other_modules": "Other Modules",
"view_all": "View All",
"no_reviews_found": "There are no reviews for this module yet!",
"module_not_purchased": "Module Not Purchased",
"module_not_found": "Module Not Found",
"version_not_supported": "This module version doesn't support the current version of Crater",
"last_updated": "Mis à jour le",
"connect_installation": "Connectez votre installation",
"api_token_description": "Rendez-vous à {url} et connectez votre application en entrant le jeton d'API. Vos modules achetés apparaîtront ici une fois la connexion établie.",
"view_module": "Afficher le module",
"update_available": "Mise à jour disponible",
"purchased": "Acheté",
"installed": "Installé",
"no_modules_installed": "Aucun module installé !",
"disable_warning": "Tous les paramètres de ce module seront réinitialisés.",
"what_you_get": "Ce que vous obtenez"
"last_updated": "Last Updated On",
"connect_installation": "Connect your installation",
"api_token_description": "Login to {url} and connect this installation by entering the API Token. Your purchased modules will show up here after the connection is established.",
"view_module": "View Module",
"update_available": "Update Available",
"purchased": "Purchased",
"installed": "Installed",
"no_modules_installed": "No Modules Installed Yet!",
"disable_warning": "All the settings for this particular will be reverted.",
"what_you_get": "What you get"
},
"users": {
"title": "Utilisateurs",
@ -807,7 +807,7 @@
"payment_modes": "Modes de paiement",
"notes": "Notes de bas de page",
"exchange_rate": "Taux de change",
"address_information": "Information d'adresse"
"address_information": "Address Information"
},
"address_information": {
"section_description": " Vous pouvez mettre à jour vos informations d'adresse via le formulaire ci dessous."
@ -842,7 +842,7 @@
"port": "Port",
"driver": "Fournisseur",
"secret": "Secret",
"mailgun_secret": "Secret Mailgun",
"mailgun_secret": "Mailgun Secret",
"mailgun_domain": "Domaine",
"mailgun_endpoint": "Mailgun Endpoint",
"ses_secret": "SES Secret",

View File

@ -4,7 +4,7 @@
"customers": "ग्राहक",
"items": "चीज़ें",
"invoices": "चालान",
"recurring-invoices": "आवर्ती बिल",
"recurring-invoices": "Recurring Invoices",
"expenses": "लागत",
"estimates": "अनुमान",
"payments": "भुगतान",
@ -12,7 +12,7 @@
"settings": "समायोजन",
"logout": "लॉग आउट",
"users": "कर्मचारी",
"modules": "मॉड्यूल"
"modules": "Modules"
},
"general": {
"add_company": "कंपनी जोड़ें",
@ -29,9 +29,9 @@
"to_date": "इस तारीख तक",
"from": "से",
"to": "के लिये",
"ok": "ठीक",
"yes": "हां",
"no": "नहीं",
"ok": "Ok",
"yes": "Yes",
"no": "No",
"sort_by": "इसके अनुसार क्रमबद्ध करें",
"ascending": "आरोही",
"descending": "उतरते",
@ -39,7 +39,7 @@
"body": "बॉडी",
"message": "संदेश",
"send": "भेजे",
"preview": "पूर्व दर्शन",
"preview": "Preview",
"go_back": "पिचे जाओ",
"back_to_login": "लॉगिन पर वापस जाएं",
"home": "होम",
@ -65,7 +65,7 @@
"sent": "भेजा गया",
"all": "सभी",
"select_all": "सभी चुनें",
"select_template": "टेंपलेट चुने",
"select_template": "Select Template",
"choose_file": "फ़ाइल चुनने के लिए यहां क्लिक करें",
"choose_template": "एक टेम्पलेट चुनें",
"choose": "चुनें",
@ -93,14 +93,14 @@
"no_note_found": "कोई नोट नहीं मिला",
"insert_note": "टिप्पणी डालें...",
"copied_pdf_url_clipboard": "पीडीएफ यूआरएल\nको क्लिपबोर्ड पर कॉपी किया गया!",
"copied_url_clipboard": "यूआरएल को क्लिपबोर्ड पर कॉपी किया गया!",
"docs": "डॉक्स",
"do_you_wish_to_continue": "क्या आप जारी रखना चाहते हैं?",
"note": "ध्यान दें",
"pay_invoice": "बिल का भुगतान करो",
"login_successfully": "सफलतापूर्वक लॉगिन किया गया",
"logged_out_successfully": "सफलतापूर्वक लॉग आउट किया गया",
"mark_as_default": "डिफ़ॉल्ट के रूप में चिह्नित करें"
"copied_url_clipboard": "Copied url to clipboard!",
"docs": "Docs",
"do_you_wish_to_continue": "Do you wish to continue?",
"note": "Note",
"pay_invoice": "Pay Invoice",
"login_successfully": "Logged in successfully!",
"logged_out_successfully": "Logged out successfully",
"mark_as_default": "Mark as default"
},
"dashboard": {
"select_year": "वर्ष चुनें",
@ -109,7 +109,7 @@
"customers": "ग्राहक",
"invoices": "चालान",
"estimates": "अनुमान",
"payments": "भुगतान"
"payments": "Payments"
},
"chart_info": {
"total_sales": "बिक्री",
@ -151,27 +151,27 @@
"no_results_found": "कोई परिणाम नहीं मिला"
},
"company_switcher": {
"label": "स्विच कंपनी",
"no_results_found": "कोई परिणाम नहीं मिला",
"add_new_company": "नई कंपनी जोड़ें",
"new_company": "नई कंपनी",
"created_message": "कंपनी सफलतापूर्वक बनाई गई"
"label": "SWITCH COMPANY",
"no_results_found": "No Results Found",
"add_new_company": "Add new company",
"new_company": "New company",
"created_message": "Company created successfully"
},
"dateRange": {
"today": "आज",
"this_week": "इस सप्ताह",
"this_month": "इस महीने",
"this_quarter": "इस तिमाही",
"this_year": "इस वर्ष",
"previous_week": "पिछला सप्ताह",
"previous_month": "पिछला महीना",
"previous_quarter": "पिछली तिमाही",
"previous_year": "पिछला साल",
"custom": "कस्टम"
"today": "Today",
"this_week": "This Week",
"this_month": "This Month",
"this_quarter": "This Quarter",
"this_year": "This Year",
"previous_week": "Previous Week",
"previous_month": "Previous Month",
"previous_quarter": "Previous Quarter",
"previous_year": "Previous Year",
"custom": "Custom"
},
"customers": {
"title": "ग्राहक",
"prefix": "प्रीफ़िक्स",
"prefix": "Prefix",
"add_customer": "ग्राहक जोड़ें",
"contacts_list": "ग्राहक सूची",
"name": "नाम",
@ -186,9 +186,9 @@
"phone": "फ़ोन",
"website": "वेबसाइट",
"overview": "अवलोकन",
"invoice_prefix": "बिल उपसर्ग",
"estimate_prefix": "अनुमान उपसर्ग",
"payment_prefix": "भुगतान उपसर्ग",
"invoice_prefix": "Invoice Prefix",
"estimate_prefix": "Estimate Prefix",
"payment_prefix": "Payment Prefix",
"enable_portal": "पोर्टल सक्षम करें",
"country": "देश",
"state": "राज्य",
@ -197,7 +197,7 @@
"added_on": "पर जोड़ा",
"action": "कार्य",
"password": "पासवर्ड",
"confirm_password": "पासवर्ड की पुष्टि करें",
"confirm_password": "Confirm Password",
"street_number": "गली संख्या",
"primary_currency": "प्राथमिक मुद्रा",
"description": "विवरण",
@ -208,10 +208,10 @@
"new_customer": "नए ग्राहक",
"edit_customer": "ग्राहक संपादित करें",
"basic_info": "आधारभूत जानकारी",
"portal_access": "पोर्टल एक्सेस",
"portal_access_text": "क्या आप इस ग्राहक को ग्राहक पोर्टल में लॉगिन करने की अनुमति देना चाहेंगे?",
"portal_access_url": "ग्राहक पोर्टल लॉगिन URL",
"portal_access_url_help": "कृपया ऊपर दिए गए URL को कॉपी करके अपने ग्राहक को एक्सेस प्रदान करने के लिए अग्रेषित करें।",
"portal_access": "Portal Access",
"portal_access_text": "Would you like to allow this customer to login to the Customer Portal?",
"portal_access_url": "Customer Portal Login URL",
"portal_access_url_help": "Please copy & forward the above given URL to your customer for providing access.",
"billing_address": "बिल भेजने का पता",
"shipping_address": "शिपिंग पता",
"copy_billing_address": "बिलिंग से प्रतिलिपि",
@ -231,9 +231,9 @@
"confirm_delete": "आप इस ग्राहक और सभी संबंधित चालानों, अनुमानों और भुगतानों को पुनर्प्राप्त नहीं कर पाएंगे। | आप इन ग्राहकों और सभी संबंधित चालानों, अनुमानों और भुगतानों को पुनर्प्राप्त नहीं कर पाएंगे।",
"created_message": "सफलतापूर्वक प्रेषित",
"updated_message": "सफलतापूर्वक प्रेषित",
"address_updated_message": "पते की जानकारी सफलतापूर्वक अपडेट की गई",
"address_updated_message": "Address Information Updated succesfully",
"deleted_message": "ग्राहक सफलतापूर्वक हटा दिया गया | ग्राहक सफलतापूर्वक हटा दिए गए",
"edit_currency_not_allowed": "एक बार लेन-देन करने के बाद मुद्रा नहीं बदल सकते।"
"edit_currency_not_allowed": "Cannot change currency once transactions created."
},
"items": {
"title": "चीज़ें",
@ -265,8 +265,8 @@
},
"estimates": {
"title": "अनुमान",
"accept_estimate": "अनुमान स्वीकार करें",
"reject_estimate": "अनुमान अस्वीकार करें",
"accept_estimate": "Accept Estimate",
"reject_estimate": "Reject Estimate",
"estimate": "अनुमान | अनुमान",
"estimates_list": "अनुमान सूची",
"days": "{days} दिनों",
@ -318,10 +318,10 @@
},
"accepted": "स्वीकृत",
"rejected": "अस्वीकृत",
"expired": "समाप्त हुआ",
"expired": "Expired",
"sent": "भेजा गया",
"draft": "प्रारूप",
"viewed": "देखा गया",
"viewed": "Viewed",
"declined": "नामंज़ूर किया गया",
"new_estimate": "नया अनुमान",
"add_new_estimate": "नया अनुमान जोड़ें",
@ -355,14 +355,14 @@
"select_an_item": "चुनने के लिए टाइप करें या क्लिक करें",
"type_item_description": "आइटम विवरण टाइप करें (वैकल्पिक)"
},
"mark_as_default_estimate_template_description": "यदि सक्षम किया गया है, तो चयनित टेम्पलेट स्वचालित रूप से नए अनुमानों के लिए चयनित हो जाएगा।"
"mark_as_default_estimate_template_description": "If enabled, the selected template will be automatically selected for new estimates."
},
"invoices": {
"title": "चालान",
"download": "डाउनलोड",
"pay_invoice": "रसीद का भुगतान करो",
"download": "Download",
"pay_invoice": "Pay Invoice",
"invoices_list": "चालान सूची",
"invoice_information": "चालान जानकारी",
"invoice_information": "Invoice Information",
"days": "{days} दिनों",
"months": "{months} महीने",
"years": "{years} साल",
@ -379,7 +379,7 @@
"amount_due": "देय राशि",
"partially_paid": "आंशिक रूप से भुगतान किया",
"total": "कुल",
"discount": "छूट",
"discount": "Discount",
"sub_total": "उप राशि",
"invoice": "चालान | चालान",
"invoice_number": "इनवॉयस संख्या:",
@ -397,13 +397,13 @@
"send_invoice": "चालान भेजें",
"resend_invoice": "चालान फिर से भेजें",
"invoice_template": "चालान टेम्पलेट",
"conversion_message": "बिल क्लोन सफल",
"conversion_message": "Invoice cloned successful",
"template": "टेम्प्लेट",
"mark_as_sent": "भेजे गए के रूप में चिह्नित करें",
"confirm_send_invoice": "यह चालान ग्राहक को ईमेल के माध्यम से भेजा जाएगा",
"invoice_mark_as_sent": "यह चालान भेजा के रूप में चिह्नित किया जाएगा",
"confirm_mark_as_accepted": "इस बिल को स्वीकृत के रूप में चिह्नित किया जाएगा",
"confirm_mark_as_rejected": "इस बिल को अस्वीकृत के रूप में चिह्नित किया जाएगा",
"confirm_mark_as_accepted": "This invoice will be marked as Accepted",
"confirm_mark_as_rejected": "This invoice will be marked as Rejected",
"confirm_send": "यह चालान ग्राहक को ईमेल के माध्यम से भेजा जाएगा",
"invoice_date": "चालान की तारीख",
"record_payment": "रिकॉर्ड भुगतान",
@ -415,13 +415,13 @@
"update_invoice": "चालान संपादित करें",
"add_new_tax": "नया टैक्स जोड़ें",
"no_invoices": "अभी तक कोई चालान नहीं!",
"mark_as_rejected": "अस्वीकृत के रूप में चिह्नित करें",
"mark_as_accepted": "स्वीकृत के रूप में चिह्नित करें",
"mark_as_rejected": "Mark as rejected",
"mark_as_accepted": "Mark as accepted",
"list_of_invoices": "इस खंड में वस्तुओं की सूची होगी।",
"select_invoice": "चालान का चयन करें",
"no_matching_invoices": "कोई मेल खाने वाले ग्राहक नहीं हैं!",
"mark_as_sent_successfully": "चालान को सफलतापूर्वक भेजा गया के रूप में चिह्नित किया गया",
"invoice_sent_successfully": "चालान सफलतापूर्वक भेजा गया",
"invoice_sent_successfully": "Invoice sent successfully",
"cloned_successfully": "चालान सफलतापूर्वक क्लोन किया गया",
"clone_invoice": "क्लोन चालान",
"confirm_clone": "यह चालान एक नए चालान में क्लोन किया जाएगा",
@ -447,66 +447,66 @@
"marked_as_sent_message": "अनुमान को सफलतापूर्वक भेजा गया के रूप में चिह्नित किया गया",
"something_went_wrong": "कुछ गलत हो गया",
"invalid_due_amount_message": "कुल चालान राशि इस चालान के लिए कुल भुगतान की गई राशि से कम नहीं हो सकती है। जारी रखने के लिए कृपया इनवॉइस अपडेट करें या संबद्ध भुगतानों को हटा दें।",
"mark_as_default_invoice_template_description": "यदि सक्षम किया गया है, तो चयनित टेम्पलेट स्वचालित रूप से नए चालानों के लिए चयनित हो जाएगा।"
"mark_as_default_invoice_template_description": "If enabled, the selected template will be automatically selected for new invoices."
},
"recurring_invoices": {
"title": "आवर्ती बिल",
"invoices_list": "आवर्ती बिल सूची",
"days": "{days} दिन",
"months": "{months} महीना",
"years": "{years} वर्ष",
"all": "सभी",
"paid": "भुगतान किया गया",
"unpaid": "अवैतनिक",
"viewed": "देखा गया",
"overdue": "अतिदेय",
"active": "सक्रिय",
"completed": "पूर्ण",
"customer": "ग्राहक",
"paid_status": "भुगतान की स्थिति",
"ref_no": "प्रसंग संख्या",
"number": "संख्या",
"amount_due": "देय राशि",
"partially_paid": "आंशिक रूप से भुगतान किया",
"total": "संपूर्ण",
"discount": "छूट",
"sub_total": "उप-योग",
"invoice": "आवर्ती बिल",
"invoice_number": "आवर्ती बिल संख्या",
"next_invoice_date": "अगली बिल तिथि",
"ref_number": "संदर्भ संख्या",
"contact": "संपर्क",
"add_item": "आइटम जोड़ें",
"date": "दिनांक",
"limit_by": "द्वारा सीमित करें",
"limit_date": "सीमा तिथि",
"limit_count": "सीमा गिनती",
"count": "गिनती",
"status": "स्थिति",
"select_a_status": "स्टेटस चुनें",
"working": "काम कर रहा है",
"on_hold": "रुका हुआ है",
"complete": "पूर्ण",
"add_tax": "कर जोड़ें",
"amount": "मात्रा",
"action": "कार्य",
"notes": "नोट्स",
"view": "देखे",
"basic_info": "आधारभूत जानकारी",
"send_invoice": "आवर्ती चालान भेजें",
"auto_send": "ऑटो भेजें",
"resend_invoice": "आवर्ती चालान फिर से भेजें",
"invoice_template": "आवर्ती चालान टेम्पलेट",
"conversion_message": "आवर्ती चालान क्लोन सफल",
"template": "टेम्प्लेट",
"mark_as_sent": "भेजे गए के रूप में चिह्नित करें",
"confirm_send_invoice": "यह आवर्ती चालान ग्राहक को ईमेल के माध्यम से भेजा जाएगा",
"invoice_mark_as_sent": "यह आवर्ती चालान भेजा गया के रूप में चिह्नित किया जाएगा",
"confirm_send": "यह आवर्ती चालान ग्राहक को ईमेल के माध्यम से भेजा जाएगा",
"starts_at": "आरंभ करने की तिथि",
"due_date": "बिल की देय तिथि",
"record_payment": "भुगतान रिकॉर्ड करें",
"add_new_invoice": "आवर्ती बिल फिर से भेजें",
"title": "Recurring Invoices",
"invoices_list": "Recurring Invoices List",
"days": "{days} Days",
"months": "{months} Month",
"years": "{years} Year",
"all": "All",
"paid": "Paid",
"unpaid": "Unpaid",
"viewed": "Viewed",
"overdue": "Overdue",
"active": "Active",
"completed": "Completed",
"customer": "CUSTOMER",
"paid_status": "PAID STATUS",
"ref_no": "REF NO.",
"number": "NUMBER",
"amount_due": "AMOUNT DUE",
"partially_paid": "Partially Paid",
"total": "Total",
"discount": "Discount",
"sub_total": "Sub Total",
"invoice": "Recurring Invoice | Recurring Invoices",
"invoice_number": "Recurring Invoice Number",
"next_invoice_date": "Next Invoice Date",
"ref_number": "Ref Number",
"contact": "Contact",
"add_item": "Add an Item",
"date": "Date",
"limit_by": "Limit by",
"limit_date": "Limit Date",
"limit_count": "Limit Count",
"count": "Count",
"status": "Status",
"select_a_status": "Select a status",
"working": "Working",
"on_hold": "On Hold",
"complete": "Completed",
"add_tax": "Add Tax",
"amount": "Amount",
"action": "Action",
"notes": "Notes",
"view": "View",
"basic_info": "Basic Info",
"send_invoice": "Send Recurring Invoice",
"auto_send": "Auto Send",
"resend_invoice": "Resend Recurring Invoice",
"invoice_template": "Recurring Invoice Template",
"conversion_message": "Recurring Invoice cloned successful",
"template": "Template",
"mark_as_sent": "Mark as sent",
"confirm_send_invoice": "This recurring invoice will be sent via email to the customer",
"invoice_mark_as_sent": "This recurring invoice will be marked as sent",
"confirm_send": "This recurring invoice will be sent via email to the customer",
"starts_at": "Start Date",
"due_date": "Invoice Due Date",
"record_payment": "Record Payment",
"add_new_invoice": "Add New Recurring Invoice",
"update_expense": "Update Expense",
"edit_invoice": "Edit Recurring Invoice",
"new_invoice": "New Recurring Invoice",
@ -659,46 +659,46 @@
},
"modules": {
"buy_now": "Buy Now",
"install": "इंस्टॉल",
"price": "मूल्य",
"download_zip_file": "ज़िप डाउनलोड करे",
"unzipping_package": "पैकेज खोल रहा है",
"copying_files": "फ़ाइलें कॉपी हो रही है",
"deleting_files": "अप्रयुक्त फाइलों को हटाना",
"completing_installation": "स्थापना पूर्ण करना",
"update_failed": "अद्यतनीकरण असफल रहा",
"install_success": "मॉड्यूल सफलतापूर्वक स्थापित किया गया है!",
"customer_reviews": "समीक्षा",
"license": "लाइसेन्स",
"faq": "हमेशा पूछे जाने वाले प्रश्न",
"monthly": "महीने के",
"yearly": "हर वर्ष",
"updated": "अपडेट किया गया",
"version": "वर्ज़न",
"disable": "अक्षम करें",
"module_disabled": "मॉड्यूल अक्षम",
"enable": "सक्षम",
"module_enabled": "मॉड्यूल सक्षम",
"update_to": "अपडेट करें",
"module_updated": "मॉड्यूल सफलतापूर्वक अपडेट किया गया!",
"title": "मॉड्यूल",
"module": "मॉड्यूल | मॉड्यूल",
"api_token": "एपीआई टोकन",
"invalid_api_token": "अमान्य एपीआई टोकन।",
"other_modules": "अन्य मॉड्यूल",
"view_all": "सभी को देखें",
"no_reviews_found": "इस मॉड्युल के लिए अभी तक वहां कोई समीक्षा नहीं है!",
"module_not_purchased": "मॉड्यूल खरीदा नहीं गया",
"module_not_found": "मॉड्यूल नहीं मिला",
"install": "Install",
"price": "Price",
"download_zip_file": "Download ZIP file",
"unzipping_package": "Unzipping Package",
"copying_files": "Copying Files",
"deleting_files": "Deleting Unused files",
"completing_installation": "Completing Installation",
"update_failed": "Update Failed",
"install_success": "Module has been installed successfully!",
"customer_reviews": "Reviews",
"license": "License",
"faq": "FAQ",
"monthly": "Monthly",
"yearly": "Yearly",
"updated": "Updated",
"version": "Version",
"disable": "Disable",
"module_disabled": "Module Disabled",
"enable": "Enable",
"module_enabled": "Module Enabled",
"update_to": "Update To",
"module_updated": "Module Updated Successfully!",
"title": "Modules",
"module": "Module | Modules",
"api_token": "API token",
"invalid_api_token": "Invalid API Token.",
"other_modules": "Other Modules",
"view_all": "View All",
"no_reviews_found": "There are no reviews for this module yet!",
"module_not_purchased": "Module Not Purchased",
"module_not_found": "Module Not Found",
"version_not_supported": "This module version doesn't support the current version of Crater",
"last_updated": "अंतिम बार अद्यतन किया गया",
"connect_installation": "अपनी स्थापना कनेक्ट करें",
"api_token_description": "{url} में लॉग इन करें और API टोकन दर्ज करके इस इंस्टॉलेशन को कनेक्ट करें। कनेक्शन स्थापित होने के बाद आपके खरीदे गए मॉड्यूल यहां दिखाई देंगे।",
"view_module": "मॉड्यूल देखें",
"update_available": "उपलब्ध अद्यतन",
"purchased": "खरीदी",
"installed": "इंस्टॉल हुआ।",
"no_modules_installed": "अभी तक कोई मॉड्यूल स्थापित नहीं है!",
"last_updated": "Last Updated On",
"connect_installation": "Connect your installation",
"api_token_description": "Login to {url} and connect this installation by entering the API Token. Your purchased modules will show up here after the connection is established.",
"view_module": "View Module",
"update_available": "Update Available",
"purchased": "Purchased",
"installed": "Installed",
"no_modules_installed": "No Modules Installed Yet!",
"disable_warning": "All the settings for this particular will be reverted.",
"what_you_get": "What you get"
},

View File

@ -12,7 +12,7 @@
"settings": "Pengaturan",
"logout": "Keluar",
"users": "Pengguna",
"modules": "Modul"
"modules": "Modules"
},
"general": {
"add_company": "Tambahkan Perusahaan",
@ -93,14 +93,14 @@
"no_note_found": "Tidak ada catatan yang ditemukan",
"insert_note": "Sisipkan Catatan",
"copied_pdf_url_clipboard": "URL file PDF disalin ke clipboard!",
"copied_url_clipboard": "Disalin ke clipboard!",
"copied_url_clipboard": "Copied url to clipboard!",
"docs": "Dokumen",
"do_you_wish_to_continue": "Apakah anda ingin melanjutkan?",
"note": "Catatan",
"pay_invoice": "Bayar tagihan",
"login_successfully": "Login berhasil!",
"logged_out_successfully": "Berhasil keluar",
"mark_as_default": "Tandai sebagai default"
"pay_invoice": "Pay Invoice",
"login_successfully": "Logged in successfully!",
"logged_out_successfully": "Logged out successfully",
"mark_as_default": "Mark as default"
},
"dashboard": {
"select_year": "Pilih tahun",
@ -109,7 +109,7 @@
"customers": "Pelanggan",
"invoices": "Faktur",
"estimates": "Perkiraan",
"payments": "Pembayaran"
"payments": "Payments"
},
"chart_info": {
"total_sales": "Penjualan",
@ -208,10 +208,10 @@
"new_customer": "Pelanggan Baru",
"edit_customer": "Ubah Pelanggan",
"basic_info": "Info dasar",
"portal_access": "Akses Portal",
"portal_access_text": "Apakah Anda ingin mengizinkan pelanggan ini untuk masuk ke Portal Pelanggan?",
"portal_access_url": "URL Masuk Portal Pelanggan",
"portal_access_url_help": "Harap salin & teruskan URL yang diberikan di atas kepada pelanggan Anda untuk memberikan akses.",
"portal_access": "Portal Access",
"portal_access_text": "Would you like to allow this customer to login to the Customer Portal?",
"portal_access_url": "Customer Portal Login URL",
"portal_access_url_help": "Please copy & forward the above given URL to your customer for providing access.",
"billing_address": "Alamat Tagihan",
"shipping_address": "Alamat Pengiriman",
"copy_billing_address": "Menyalin dari Tagihan",
@ -231,7 +231,7 @@
"confirm_delete": "Anda tidak akan dapat mengembalikan pelanggan dan semua tagihan terkait. | Anda tidak akan dapat mengembalikan pelanggan dan semua Tagihan terkait, Penawaran dan Pembayaran.",
"created_message": "Pelanggan berhasil dibuat",
"updated_message": "Pelanggan berhasil diperbarui",
"address_updated_message": "Informasi Alamat Berhasil Diperbarui",
"address_updated_message": "Address Information Updated succesfully",
"deleted_message": "Pelanggan berhasil dihapus",
"edit_currency_not_allowed": "Ketika transaksi telah dibuat, mata uang tidak dapat dirubah."
},
@ -265,8 +265,8 @@
},
"estimates": {
"title": "Perkiraan",
"accept_estimate": "Perkiraan",
"reject_estimate": "Tolak Perkiraan",
"accept_estimate": "Accept Estimate",
"reject_estimate": "Reject Estimate",
"estimate": "Estimasi",
"estimates_list": "Daftar Penawaran",
"days": "{days} Hari",
@ -276,7 +276,7 @@
"paid": "Lunas",
"unpaid": "Belum lunas",
"customer": "PELANGGAN",
"ref_no": "NO. REF.",
"ref_no": "REF NO.",
"number": "NOMOR",
"amount_due": "Jumlah yang harus dibayar",
"partially_paid": "Pembayaran Sebagian",
@ -318,10 +318,10 @@
},
"accepted": "Diterima",
"rejected": "Ditolak",
"expired": "Kadaluarsa",
"expired": "Expired",
"sent": "Terkirim",
"draft": "Draf",
"viewed": "Dilihat",
"viewed": "Viewed",
"declined": "Ditolak",
"new_estimate": "Penawaran Baru",
"add_new_estimate": "Tambah Penawaran Baru",
@ -355,14 +355,14 @@
"select_an_item": "Ketik atau klik untuk memilih",
"type_item_description": "Ketik Deskripsi Item (opsional)"
},
"mark_as_default_estimate_template_description": "Jika diaktifkan, template terpilih akan secara otomatis digunakan saat pembuatan estimate baru."
"mark_as_default_estimate_template_description": "If enabled, the selected template will be automatically selected for new estimates."
},
"invoices": {
"title": "Faktur",
"download": "Unduh",
"pay_invoice": "Bayar tagihan",
"download": "Download",
"pay_invoice": "Pay Invoice",
"invoices_list": "Daftar Faktur",
"invoice_information": "Informasi tagihan",
"invoice_information": "Invoice Information",
"days": "{days} Hari",
"months": "{months} Bulan",
"years": "{years} Tahun",
@ -374,7 +374,7 @@
"completed": "Selesai",
"customer": "PELANGGAN",
"paid_status": "STATUS PEMBAYARAN",
"ref_no": "NO. REF.",
"ref_no": "REF NO.",
"number": "NOMOR",
"amount_due": "Jumlah yang harus dibayar",
"partially_paid": "Pembayaran Sebagian",
@ -439,19 +439,19 @@
"select_an_item": "Ketik atau klik untuk memilih",
"type_item_description": "Ketik Deskripsi Item (opsional)"
},
"payment_attached_message": "Salah satu faktur yang dipilih sudah memiliki pembayaran yang menyertainya. Pastikan untuk menghapus pembayaran terlampir terlebih dahulu untuk melanjutkan penghapusan",
"confirm_delete": "Anda tidak akan dapat memulihkan Faktur ini | Anda tidak akan dapat memulihkan Faktur ini",
"created_message": "Faktur berhasil dibuat",
"updated_message": "Faktur berhasil diperbarui",
"deleted_message": "Faktur berhasil dihapus | Faktur berhasil dihapus",
"marked_as_sent_message": "Tandai Faktur sudah dikirim",
"something_went_wrong": "terjadi kesalahan",
"invalid_due_amount_message": "Jumlah Total Faktur tidak boleh kurang dari jumlah total yang dibayarkan untuk Faktur ini. Harap perbarui faktur atau hapus pembayaran terkait untuk melanjutkan.",
"mark_as_default_invoice_template_description": "Jika diaktifkan, template terpilih akan secara otomatis digunakan saat pembuatan estimate baru."
"payment_attached_message": "One of the selected invoices already have a payment attached to it. Make sure to delete the attached payments first in order to go ahead with the removal",
"confirm_delete": "You will not be able to recover this Invoice | You will not be able to recover these Invoices",
"created_message": "Invoice created successfully",
"updated_message": "Invoice updated successfully",
"deleted_message": "Invoice deleted successfully | Invoices deleted successfully",
"marked_as_sent_message": "Invoice marked as sent successfully",
"something_went_wrong": "something went wrong",
"invalid_due_amount_message": "Total Invoice amount cannot be less than total paid amount for this Invoice. Please update the invoice or delete the associated payments to continue.",
"mark_as_default_invoice_template_description": "If enabled, the selected template will be automatically selected for new invoices."
},
"recurring_invoices": {
"title": "Tagihan-Tagihan Berulang",
"invoices_list": "Daftar Faktur Berulang",
"title": "Recurring Invoices",
"invoices_list": "Recurring Invoices List",
"days": "{days} Hari",
"months": "{months} Bulan",
"years": "{years} Tahun",
@ -459,61 +459,61 @@
"paid": "Lunas",
"unpaid": "Belum lunas",
"viewed": "Dilihat",
"overdue": "Lewat jatuh tempo",
"overdue": "Overdue",
"active": "Aktif",
"completed": "Selesai",
"customer": "PELANGGAN",
"paid_status": "STATUS PEMBAYARAN",
"ref_no": "NO. REF.",
"paid_status": "PAID STATUS",
"ref_no": "REF NO.",
"number": "NOMOR",
"amount_due": "Jumlah yang harus dibayar",
"partially_paid": "Angsuran",
"amount_due": "AMOUNT DUE",
"partially_paid": "Partially Paid",
"total": "Total",
"discount": "Diskon",
"sub_total": "Sub Total",
"invoice": "Faktur Berulang | Faktur Berulang",
"invoice_number": "Nomor Faktur Berulang",
"next_invoice_date": "Tanggal Faktur Berikutnya",
"ref_number": "Nomor Referensi",
"invoice": "Recurring Invoice | Recurring Invoices",
"invoice_number": "Recurring Invoice Number",
"next_invoice_date": "Next Invoice Date",
"ref_number": "Ref Number",
"contact": "Kontak",
"add_item": "Tambah Barang",
"date": "Tanggal",
"limit_by": "Batasi oleh",
"limit_date": "Batas Tanggal",
"limit_count": "Batas Jumlah",
"count": "Hitung",
"limit_by": "Limit by",
"limit_date": "Limit Date",
"limit_count": "Limit Count",
"count": "Count",
"status": "Status",
"select_a_status": "Pilih status",
"working": "Sedang mengerjakan",
"on_hold": "Ditangguhkan",
"working": "Working",
"on_hold": "On Hold",
"complete": "Selesai",
"add_tax": "Tambah Pajak",
"amount": "Jumlah",
"action": "Aksi",
"notes": "Catatan",
"view": "Tampilan",
"basic_info": "Informasi dasar",
"send_invoice": "Kirim Ulang Faktur Berulang",
"auto_send": "Kirim Otomatis",
"resend_invoice": "Kirim Ulang Faktur Berulang",
"invoice_template": "Nomor Faktur Berulang",
"conversion_message": "Faktur Berulang berhasil dikloning",
"view": "View",
"basic_info": "Basic Info",
"send_invoice": "Send Recurring Invoice",
"auto_send": "Auto Send",
"resend_invoice": "Resend Recurring Invoice",
"invoice_template": "Recurring Invoice Template",
"conversion_message": "Recurring Invoice cloned successful",
"template": "Template",
"mark_as_sent": "Tandai sebagai terkirim",
"confirm_send_invoice": "Faktur berulang ini akan dikirim melalui email ke pelanggan",
"invoice_mark_as_sent": "Faktur berulang ini akan ditandai sebagai terkirim",
"confirm_send": "Faktur berulang ini akan dikirim melalui email ke pelanggan",
"mark_as_sent": "Mark as sent",
"confirm_send_invoice": "This recurring invoice will be sent via email to the customer",
"invoice_mark_as_sent": "This recurring invoice will be marked as sent",
"confirm_send": "This recurring invoice will be sent via email to the customer",
"starts_at": "Tanggal Mulai",
"due_date": "Tanggal Jatuh Tempo Faktur",
"record_payment": "Rekam Pembayaran",
"add_new_invoice": "Tambahkan Faktur Berulang Baru",
"update_expense": "Perbarui Biaya",
"due_date": "Invoice Due Date",
"record_payment": "Record Payment",
"add_new_invoice": "Add New Recurring Invoice",
"update_expense": "Update Expense",
"edit_invoice": "Edit Recurring Invoice",
"new_invoice": "New Recurring Invoice",
"send_automatically": "Send Automatically",
"send_automatically_desc": "Enable this, if you would like to send the invoice automatically to the customer when its created.",
"save_invoice": "Save Recurring Invoice",
"update_invoice": "Perbarui Faktur Berulang",
"update_invoice": "Update Recurring Invoice",
"add_new_tax": "Tambah Pajak Baru",
"no_invoices": "Belum ada Faktur Berulang!",
"mark_as_rejected": "Ditandai telah ditolak",
@ -526,7 +526,7 @@
"cloned_successfully": "Faktur Berulang berhasil digandakan",
"clone_invoice": "Gandakan Faktur Berulang",
"confirm_clone": "Faktur Berulang ini akan digandakan menjadi Faktur Berulang yang baru",
"add_customer_email": "Tambahkan alamat email pelanggan untuk mengirimkan tagihan secara otomatis.",
"add_customer_email": "Please add an email address for this customer to send invoices automatically.",
"item": {
"title": "Judul item",
"description": "Deskripsi",
@ -550,19 +550,19 @@
"month": "Bulan",
"day_week": "Hari dalam minggu"
},
"confirm_delete": "Anda tidak akan dapat memulihkan Faktur ini | Anda tidak akan dapat memulihkan Faktur ini",
"created_message": "Faktur Berulang berhasil dibuat",
"updated_message": "Faktur Berulang berhasil diperbaharui",
"deleted_message": "Faktur Berulang berhasil dihapus | Faktur Berulang berhasil dihapus",
"marked_as_sent_message": "Tandai Faktur Berulang sudah dikirim",
"user_email_does_not_exist": "Email pengguna tidak ada",
"something_went_wrong": "terjadi kesalahan",
"invalid_due_amount_message": "Jumlah Total Faktur Berulang tidak boleh kurang dari jumlah total yang dibayarkan untuk Faktur Berulang ini. Harap perbarui faktur atau hapus pembayaran terkait untuk melanjutkan."
"confirm_delete": "You will not be able to recover this Invoice | You will not be able to recover these Invoices",
"created_message": "Recurring Invoice created successfully",
"updated_message": "Recurring Invoice updated successfully",
"deleted_message": "Recurring Invoice deleted successfully | Recurring Invoices deleted successfully",
"marked_as_sent_message": "Recurring Invoice marked as sent successfully",
"user_email_does_not_exist": "User email does not exist",
"something_went_wrong": "something went wrong",
"invalid_due_amount_message": "Total Recurring Invoice amount cannot be less than total paid amount for this Recurring Invoice. Please update the invoice or delete the associated payments to continue."
},
"payments": {
"title": "Pembayaran",
"payments_list": "Daftar Pembayaran",
"record_payment": "Rekam Pembayaran",
"record_payment": "Record Payment",
"customer": "Pelanggan",
"date": "Tanggal",
"amount": "Jumlah",
@ -573,29 +573,29 @@
"note": "Catatan",
"add_payment": "Tambah Pembayaran",
"new_payment": "Pembayaran Baru",
"edit_payment": "Edit Pembayaran",
"view_payment": "Lihat Pembayaran",
"add_new_payment": "Tambahkan Pembayaran Baru",
"send_payment_receipt": "Kirim Tanda Terima Pembayaran",
"send_payment": "Kirim Pembayaran",
"save_payment": "Simpan Pembayaran",
"update_payment": "Perbaharui Pembayaran",
"edit_payment": "Edit Payment",
"view_payment": "View Payment",
"add_new_payment": "Add New Payment",
"send_payment_receipt": "Send Payment Receipt",
"send_payment": "Send Payment",
"save_payment": "Save Payment",
"update_payment": "Update Payment",
"payment": "Pembayaran",
"no_payments": "Belum ada pembayaran!",
"no_payments": "No payments yet!",
"not_selected": "Tidak dipilih",
"no_invoice": "Tidak ada faktur",
"no_matching_payments": "Tidak ada pembayaran yang cocok!",
"list_of_payments": "Bagian ini akan berisi daftar pembayaran.",
"select_payment_mode": "Pilih mode pembayaran",
"confirm_mark_as_sent": "Penawaran ini akan ditandai telah dikirim",
"confirm_send_payment": "Pembayaran ini akan dikirim melalui email ke pelanggan",
"send_payment_successfully": "Pembayaran berhasil dikirim",
"something_went_wrong": "terjadi kesalahan",
"confirm_delete": "Anda tidak akan dapat memulihkan Pembayaran ini | Anda tidak akan dapat memulihkan Pembayaran ini",
"created_message": "Pembayaran berhasil dibuat",
"updated_message": "Pembayaran berhasil diperbaharui",
"deleted_message": "Pembayaran berhasil dihapus | Pembayaran berhasil dihapus",
"invalid_amount_message": "Jumlah pembayaran tidak valid"
"no_matching_payments": "There are no matching payments!",
"list_of_payments": "This section will contain the list of payments.",
"select_payment_mode": "Select payment mode",
"confirm_mark_as_sent": "This estimate will be marked as sent",
"confirm_send_payment": "This payment will be sent via email to the customer",
"send_payment_successfully": "Payment sent successfully",
"something_went_wrong": "something went wrong",
"confirm_delete": "You will not be able to recover this Payment | You will not be able to recover these Payments",
"created_message": "Payment created successfully",
"updated_message": "Payment updated successfully",
"deleted_message": "Payment deleted successfully | Payments deleted successfully",
"invalid_amount_message": "Payment amount is invalid"
},
"expenses": {
"title": "Pengeluaran",
@ -610,29 +610,29 @@
"to_date": "Sampai Tanggal",
"expense_date": "Tanggal",
"description": "Deskripsi",
"receipt": "Tanda Terima",
"receipt": "Receipt",
"amount": "Jumlah",
"action": "Aksi",
"not_selected": "Tidak dipilih",
"note": "Catatan",
"category_id": "Id kategori",
"category_id": "Category Id",
"date": "Tanggal",
"add_expense": "Tambahkan pengeluaran",
"add_new_expense": "Tambah Pengeluaran Baru",
"save_expense": "Simpan Pengeluaran",
"update_expense": "Edit Pengeluaran",
"download_receipt": "Unduh Tanda Terima",
"edit_expense": "Edit Pengeluaran",
"new_expense": "Pengeluaran Baru",
"expense": "Biaya | Pengeluaran",
"no_expenses": "Belum ada pengeluaran!",
"list_of_expenses": "Bagian ini akan berisi daftar pengeluaran.",
"confirm_delete": "Anda tidak akan dapat memulihkan Pengeluaran ini | Anda tidak akan dapat memulihkan Pengeluaran ini",
"created_message": "Pengeluaran berhasil dibuat",
"updated_message": "Pengeluaran berhasil diperbaharui",
"deleted_message": "Pengeluaran berhasil dihapus | Pengeluaran berhasil dihapus",
"add_expense": "Add Expense",
"add_new_expense": "Add New Expense",
"save_expense": "Save Expense",
"update_expense": "Update Expense",
"download_receipt": "Download Receipt",
"edit_expense": "Edit Expense",
"new_expense": "New Expense",
"expense": "Expense | Expenses",
"no_expenses": "No expenses yet!",
"list_of_expenses": "This section will contain the list of expenses.",
"confirm_delete": "You will not be able to recover this Expense | You will not be able to recover these Expenses",
"created_message": "Expense created successfully",
"updated_message": "Expense updated successfully",
"deleted_message": "Expense deleted successfully | Expenses deleted successfully",
"categories": {
"categories_list": "Daftar Kategori",
"categories_list": "Categories List",
"title": "Title",
"name": "Name",
"description": "Description",
@ -694,65 +694,65 @@
"last_updated": "Last Updated On",
"connect_installation": "Connect your installation",
"api_token_description": "Login to {url} and connect this installation by entering the API Token. Your purchased modules will show up here after the connection is established.",
"view_module": "Lihat Module",
"update_available": "Pembaruan Tersedia",
"purchased": "Pembelian",
"installed": "Terinstal",
"no_modules_installed": "Belum Ada Modul yang Terpasang!",
"disable_warning": "Semua pengaturan untuk saat ini akan dikembalikan.",
"what_you_get": "Apa yang bisa Anda dapatkan"
"view_module": "View Module",
"update_available": "Update Available",
"purchased": "Purchased",
"installed": "Installed",
"no_modules_installed": "No Modules Installed Yet!",
"disable_warning": "All the settings for this particular will be reverted.",
"what_you_get": "What you get"
},
"users": {
"title": "Pengguna",
"users_list": "Daftar Pengguna",
"name": "Nama",
"description": "Deskripsi",
"added_on": "Ditambahkan Pada",
"date_of_creation": "Tanggal pembuatan",
"action": "Aksi",
"add_user": "Tambah Pengguna",
"save_user": "Simpan Pengguna",
"update_user": "Edit Pengguna",
"user": "Pengguna | Pengguna",
"add_new_user": "Tambahkan pengguna baru",
"new_user": "Pengguna baru",
"edit_user": "Edit Pengguna",
"no_users": "Belum ada pengguna!",
"list_of_users": "Bagian ini akan berisi daftar pengguna.",
"title": "Users",
"users_list": "Users List",
"name": "Name",
"description": "Description",
"added_on": "Added On",
"date_of_creation": "Date Of Creation",
"action": "Action",
"add_user": "Add User",
"save_user": "Save User",
"update_user": "Update User",
"user": "User | Users",
"add_new_user": "Add New User",
"new_user": "New User",
"edit_user": "Edit User",
"no_users": "No users yet!",
"list_of_users": "This section will contain the list of users.",
"email": "Email",
"phone": "Telepon",
"password": "Kata Sandi",
"user_attached_message": "Tidak dapat menghapus item yang sudah digunakan",
"confirm_delete": "Anda tidak akan dapat memulihkan Pengguna ini | Anda tidak akan dapat memulihkan Pengguna ini",
"created_message": "Pengguna berhasil dibuat",
"updated_message": "Pengguna berhasil diedit",
"deleted_message": "Pengguna berhasil dihapus | Pengguna berhasil dihapus",
"select_company_role": "Pilih Peran untuk {company}",
"companies": "Perusahaan"
"user_attached_message": "Cannot delete an item which is already in use",
"confirm_delete": "You will not be able to recover this User | You will not be able to recover these Users",
"created_message": "User created successfully",
"updated_message": "User updated successfully",
"deleted_message": "User deleted successfully | Users deleted successfully",
"select_company_role": "Select Role for {company}",
"companies": "Companies"
},
"reports": {
"title": "Laporan",
"from_date": "Dari tanggal",
"to_date": "Sampai tanggal",
"title": "Report",
"from_date": "From Date",
"to_date": "To Date",
"status": "Status",
"paid": "Lunas",
"unpaid": "Belum dibayar",
"paid": "Paid",
"unpaid": "Unpaid",
"download_pdf": "Download PDF",
"view_pdf": "Lihat PDF",
"update_report": "Update Laporan",
"report": "Laporan | Laporan",
"view_pdf": "View PDF",
"update_report": "Update Report",
"report": "Report | Reports",
"profit_loss": {
"profit_loss": "Laba rugi",
"to_date": "Sampai tanggal",
"from_date": "Dari tanggal",
"date_range": "Pilih Rentang Tanggal"
"profit_loss": "Profit & Loss",
"to_date": "To Date",
"from_date": "From Date",
"date_range": "Select Date Range"
},
"sales": {
"sales": "Penjualan",
"date_range": "Pilih Rentang Tanggal",
"to_date": "Sampai tanggal",
"from_date": "Dari tanggal",
"report_type": "Jenis laporan"
"sales": "Sales",
"date_range": "Select Date Range",
"to_date": "To Date",
"from_date": "From Date",
"report_type": "Report Type"
},
"taxes": {
"taxes": "Taxes",

View File

@ -17,7 +17,6 @@ import sk from './sk.json'
import vi from './vi.json'
import el from './el.json'
import hr from './hr.json'
import th from './th.json'
export default {
cs,
@ -38,6 +37,5 @@ export default {
vi,
pl,
el,
hr,
th
hr
}

View File

@ -12,7 +12,7 @@
"settings": "Nustatymai",
"logout": "Atsijungti",
"users": "Vartotojai",
"modules": "Moduliai"
"modules": "Modules"
},
"general": {
"add_company": "Pridėti įmonę",
@ -93,14 +93,14 @@
"no_note_found": "Jokių žinučių nerasta",
"insert_note": "Terpti prierašą",
"copied_pdf_url_clipboard": "Nukopijuotas PDF url į iškarpinę!",
"copied_url_clipboard": "Nuoroda nukopijuota!",
"copied_url_clipboard": "Copied url to clipboard!",
"docs": "Dokumentacija",
"do_you_wish_to_continue": "Ar norite tęsti?",
"note": "Užrašas",
"pay_invoice": "Apmokėti",
"login_successfully": "Prisijungta sėkmingai!",
"logged_out_successfully": "Atsijungta sėkmingai",
"mark_as_default": "Pažymėti kaip numatytąjį"
"pay_invoice": "Pay Invoice",
"login_successfully": "Logged in successfully!",
"logged_out_successfully": "Logged out successfully",
"mark_as_default": "Mark as default"
},
"dashboard": {
"select_year": "Pasirinkite metus",
@ -109,7 +109,7 @@
"customers": "Klientai",
"invoices": "Sąskaitos",
"estimates": "Įverčiai",
"payments": "Mokėjimai"
"payments": "Payments"
},
"chart_info": {
"total_sales": "Pardavimai",

View File

@ -93,14 +93,14 @@
"no_note_found": "Geen notitie gevonden",
"insert_note": "Notitie invoegen",
"copied_pdf_url_clipboard": "PDF link naar klembord gekopieerd!",
"copied_url_clipboard": "URL naar klembord gekopieerd!",
"copied_url_clipboard": "Copied url to clipboard!",
"docs": "Documenten",
"do_you_wish_to_continue": "Wilt u Doorgaan?",
"note": "Notitie",
"pay_invoice": "Betaal factuur",
"login_successfully": "Succesvol ingelogd!",
"logged_out_successfully": "Succesvol afgemeld",
"mark_as_default": "Markeren als standaard"
"pay_invoice": "Pay Invoice",
"login_successfully": "Logged in successfully!",
"logged_out_successfully": "Logged out successfully",
"mark_as_default": "Mark as default"
},
"dashboard": {
"select_year": "Selecteer jaar",
@ -109,7 +109,7 @@
"customers": "Klanten",
"invoices": "Facturen",
"estimates": "Offertes",
"payments": "Betalingen"
"payments": "Payments"
},
"chart_info": {
"total_sales": "Verkoop",
@ -208,10 +208,10 @@
"new_customer": "Nieuwe klant",
"edit_customer": "Klant bewerken",
"basic_info": "Basis informatie",
"portal_access": "Portaaltoegang",
"portal_access_text": "Wilt u deze klant toestaan om in te loggen op het Klantenportaal?",
"portal_access_url": "Klantenportaal login URL",
"portal_access_url_help": "Kopieer & stuur de bovenstaande URL door naar uw klant om toegang te geven.",
"portal_access": "Portal Access",
"portal_access_text": "Would you like to allow this customer to login to the Customer Portal?",
"portal_access_url": "Customer Portal Login URL",
"portal_access_url_help": "Please copy & forward the above given URL to your customer for providing access.",
"billing_address": "factuur adres",
"shipping_address": "Verzendingsadres",
"copy_billing_address": "Kopiëren van facturering",
@ -231,7 +231,7 @@
"confirm_delete": "Deze klant en alle gerelateerde facturen, offertes en betalingen worden permanent verwijderd. | Deze klanten en alle gerelateerde facturen, offertes en betalingen worden permanent verwijderd.",
"created_message": "Klant succesvol aangemaakt",
"updated_message": "Klant succesvol geüpdatet",
"address_updated_message": "Adresinformatie succesvol bijgewerkt",
"address_updated_message": "Address Information Updated succesfully",
"deleted_message": "Klant succesvol verwijderd | Klanten zijn succesvol verwijderd",
"edit_currency_not_allowed": "Kan valuta niet wijzigen zodra de transacties zijn aangemaakt."
},
@ -265,8 +265,8 @@
},
"estimates": {
"title": "Offertes",
"accept_estimate": "Offerte accepteren",
"reject_estimate": "Offerte afwijzen",
"accept_estimate": "Accept Estimate",
"reject_estimate": "Reject Estimate",
"estimate": "Offerte | Offertes",
"estimates_list": "Lijst met offertes",
"days": "{dagen} dagen",
@ -318,10 +318,10 @@
},
"accepted": "Geaccepteerd",
"rejected": "Afgewezen",
"expired": "Verlopen",
"expired": "Expired",
"sent": "Verzonden",
"draft": "Concept",
"viewed": "Bekeken",
"viewed": "Viewed",
"declined": "Geweigerd",
"new_estimate": "Nieuwe offerte",
"add_new_estimate": "Offerte toevoegen",
@ -355,14 +355,14 @@
"select_an_item": "Typ of klik om een item te selecteren",
"type_item_description": "Type Item Beschrijving (optioneel)"
},
"mark_as_default_estimate_template_description": "Indien ingeschakeld, zal het geselecteerde sjabloon automatisch worden gekozen voor nieuwe offertes."
"mark_as_default_estimate_template_description": "If enabled, the selected template will be automatically selected for new estimates."
},
"invoices": {
"title": "Facturen",
"download": "Download",
"pay_invoice": "Betaal factuur",
"pay_invoice": "Pay Invoice",
"invoices_list": "Facturenlijst",
"invoice_information": "Factuurgegevens",
"invoice_information": "Invoice Information",
"days": "{dagen} dagen",
"months": "{months} Maand",
"years": "{jaar} jaar",
@ -447,7 +447,7 @@
"marked_as_sent_message": "Factuur gemarkeerd als succesvol verzonden",
"something_went_wrong": "Er is iets fout gegaan",
"invalid_due_amount_message": "Het totale factuurbedrag mag niet lager zijn dan het totale betaalde bedrag voor deze factuur. Werk de factuur bij of verwijder de bijbehorende betalingen om door te gaan.",
"mark_as_default_invoice_template_description": "Indien ingeschakeld, zal het geselecteerde sjabloon automatisch worden gekozen voor nieuwe offertes."
"mark_as_default_invoice_template_description": "If enabled, the selected template will be automatically selected for new invoices."
},
"recurring_invoices": {
"title": "Periodieke facturen",
@ -526,7 +526,7 @@
"cloned_successfully": "Terugkerende factuur succesvol gekopieerd",
"clone_invoice": "Kopieer periodieke factuur",
"confirm_clone": "Deze periodieke factuur wordt gekopieerd naar een nieuwe periodieke factuur",
"add_customer_email": "Voeg een e-mailadres aan deze klant toe, zodat facturen automatisch verzonden kunnen worden.",
"add_customer_email": "Please add an email address for this customer to send invoices automatically.",
"item": {
"title": "Item titel",
"description": "Beschrijving",
@ -658,49 +658,49 @@
"retype_password": "Geef nogmaals het wachtwoord"
},
"modules": {
"buy_now": "Nu kopen",
"install": "Installeer",
"price": "Prijs",
"download_zip_file": "Download ZIP-bestand",
"unzipping_package": "Pakket uitpakken",
"copying_files": "Bestanden kopiëren",
"deleting_files": "Ongebruikte bestanden verwijderen",
"completing_installation": "Installatie voltooien",
"update_failed": "Update mislukt",
"install_success": "Module is succesvol geïnstalleerd!",
"customer_reviews": "Beoordelingen",
"license": "Licentie",
"faq": "Veelgestelde vragen",
"monthly": "Maandelijks",
"yearly": "Jaarlijks",
"updated": "Bijgewerkt",
"version": "Versie",
"disable": "Uitschakelen",
"module_disabled": "Module uitgeschakeld",
"enable": "Inschakelen",
"module_enabled": "Module ingeschakeld",
"update_to": "Update naar",
"module_updated": "Module succesvol bijgewerkt!",
"buy_now": "Buy Now",
"install": "Install",
"price": "Price",
"download_zip_file": "Download ZIP file",
"unzipping_package": "Unzipping Package",
"copying_files": "Copying Files",
"deleting_files": "Deleting Unused files",
"completing_installation": "Completing Installation",
"update_failed": "Update Failed",
"install_success": "Module has been installed successfully!",
"customer_reviews": "Reviews",
"license": "License",
"faq": "FAQ",
"monthly": "Monthly",
"yearly": "Yearly",
"updated": "Updated",
"version": "Version",
"disable": "Disable",
"module_disabled": "Module Disabled",
"enable": "Enable",
"module_enabled": "Module Enabled",
"update_to": "Update To",
"module_updated": "Module Updated Successfully!",
"title": "Modules",
"module": "Module | Modules",
"api_token": "API-token",
"invalid_api_token": "Ongeldig API-token.",
"other_modules": "Andere modules",
"view_all": "Toon alles",
"no_reviews_found": "Er zijn nog geen beoordelingen voor deze module!",
"module_not_purchased": "Module niet gekocht",
"module_not_found": "Module niet gevonden",
"api_token": "API token",
"invalid_api_token": "Invalid API Token.",
"other_modules": "Other Modules",
"view_all": "View All",
"no_reviews_found": "There are no reviews for this module yet!",
"module_not_purchased": "Module Not Purchased",
"module_not_found": "Module Not Found",
"version_not_supported": "This module version doesn't support the current version of Crater",
"last_updated": "Laatst bijgewerkt op",
"connect_installation": "Verbind uw installatie",
"api_token_description": "Log in op {url} en verbind deze installatie door het API Token in te voeren. Uw gekochte modules zullen hier verschijnen nadat de verbinding is gemaakt.",
"view_module": "Bekijk Module",
"update_available": "Update beschikbaar",
"purchased": "Gekocht",
"installed": "Geïnstalleerd",
"no_modules_installed": "Nog geen modules geïnstalleerd!",
"disable_warning": "Alle instellingen voor dit specifiek zullen worden teruggedraaid.",
"what_you_get": "Wat je krijgt"
"last_updated": "Last Updated On",
"connect_installation": "Connect your installation",
"api_token_description": "Login to {url} and connect this installation by entering the API Token. Your purchased modules will show up here after the connection is established.",
"view_module": "View Module",
"update_available": "Update Available",
"purchased": "Purchased",
"installed": "Installed",
"no_modules_installed": "No Modules Installed Yet!",
"disable_warning": "All the settings for this particular will be reverted.",
"what_you_get": "What you get"
},
"users": {
"title": "Gebruikers",
@ -807,10 +807,10 @@
"payment_modes": "Betaalmethodes",
"notes": "Opmerkingen",
"exchange_rate": "Wisselkoers",
"address_information": "Adresgegevens"
"address_information": "Address Information"
},
"address_information": {
"section_description": " U kunt uw adresgegevens bijwerken via het onderstaande formulier."
"section_description": " You can update Your Address information using form below."
},
"title": "Instellingen",
"setting": "Instellingen | Instellingen",
@ -839,13 +839,13 @@
},
"mail": {
"host": "Mail host",
"port": "E-mail poort",
"port": "Mail Port",
"driver": "Mail-stuurprogramma",
"secret": "Geheim",
"mailgun_secret": "Mailgun geheim",
"mailgun_secret": "Mailgun Secret",
"mailgun_domain": "Domein",
"mailgun_endpoint": "Mailgun-eindpunt",
"ses_secret": "SES geheim",
"ses_secret": "SES Secret",
"ses_key": "SES-sleutel",
"password": "Mail wachtwoord",
"username": "Mail gebruikersnaam",
@ -1113,7 +1113,7 @@
"default_currency_error": "Deze valuta wordt al gebruikt in een van de Actieve Provider",
"exchange_help_text": "Voer de wisselkoers in om te converteren van {currency} naar {baseCurrency}",
"currency_freak": "Valuta Freak",
"currency_layer": "Valuta-laag",
"currency_layer": "Currency Layer",
"open_exchange_rate": "Open Exchange Rate",
"currency_converter": "Valuta omzetter",
"server": "Server",
@ -1263,7 +1263,7 @@
"media_root": "Media schijf",
"aws_driver": "AWS Stuurprogramma",
"aws_key": "AWS Sleutel",
"aws_secret": "AWS-geheim",
"aws_secret": "AWS Secret",
"aws_region": "AWS Regio",
"aws_bucket": "AWS Bucket",
"aws_root": "AWS Root",
@ -1273,13 +1273,13 @@
"do_spaces_region": "Do Spaces Regio",
"do_spaces_bucket": "Do Spaces Bucket",
"do_spaces_endpoint": "Do Spaces Endpoint",
"do_spaces_root": "Do Spaces hoofdmap",
"do_spaces_root": "Do Spaces Root",
"dropbox_type": "Dropbox Type",
"dropbox_token": "Dropbox Token",
"dropbox_key": "Dropbox Sleutel",
"dropbox_secret": "Dropbox Geheim",
"dropbox_key": "Dropbox Key",
"dropbox_secret": "Dropbox Secret",
"dropbox_app": "Dropbox App",
"dropbox_root": "Dropbox Hoofdmap",
"dropbox_root": "Dropbox Root",
"default_driver": "Standaard stuurprogramma",
"is_default": "IS STANDAARD",
"set_default_disk": "Standaardschijf instellen",
@ -1301,16 +1301,16 @@
"invalid_disk_credentials": "Ongeldige inloggegevens voor geselecteerde schijf"
},
"taxations": {
"add_billing_address": "Vul factuuradres in",
"add_shipping_address": "Vul bezorgadres in",
"add_company_address": "Vul bedrijfsadres in",
"modal_description": "De onderstaande informatie is vereist om de verkoopbelasting op te halen.",
"add_address": "Voeg adres toe voor het ophalen van verkoopbelasting.",
"address_placeholder": "Voorbeeld: 123, Mijn Straat",
"city_placeholder": "Voorbeeld: Amsterdam",
"state_placeholder": "Voorbeeld: Noord-Holland",
"zip_placeholder": "Voorbeeld: 1234 AB",
"invalid_address": "Vul een geldig adres in a.u.b."
"add_billing_address": "Enter Billing Address",
"add_shipping_address": "Enter Shipping Address",
"add_company_address": "Enter Company Address",
"modal_description": "The information below is required in order to fetch sales tax.",
"add_address": "Add Address for fetching sales tax.",
"address_placeholder": "Example: 123, My Street",
"city_placeholder": "Example: Los Angeles",
"state_placeholder": "Example: CA",
"zip_placeholder": "Example: 90024",
"invalid_address": "Please provide valid address details."
}
},
"wizard": {
@ -1328,7 +1328,7 @@
"logo_preview": "Logo Voorbeeld",
"preferences": "Voorkeuren",
"preferences_desc": "Standaardvoorkeuren voor het systeem.",
"currency_set_alert": "De valuta van het bedrijf kan later niet worden gewijzigd.",
"currency_set_alert": "The company's currency cannot be changed later.",
"country": "Land",
"state": "Provincie",
"city": "Stad",
@ -1372,7 +1372,7 @@
"app_domain": "App Domein",
"verify_now": "Nu verifiëren",
"success": "E-mailadres succesvol geverifieerd.",
"failed": "Domeinverificatie is mislukt. Voer een geldige domeinnaam in.",
"failed": "Domain verification failed. Please enter valid domain name.",
"verify_and_continue": "Verifiëren en doorgaan"
},
"mail": {
@ -1380,10 +1380,10 @@
"port": "E-mail Poort",
"driver": "Mail-stuurprogramma",
"secret": "Geheim",
"mailgun_secret": "Mailgun geheim",
"mailgun_secret": "Mailgun Secret",
"mailgun_domain": "Domein",
"mailgun_endpoint": "Mailgun-eindpunt",
"ses_secret": "SES geheim",
"ses_secret": "SES Secret",
"ses_key": "SES-sleutel",
"password": "Mail wachtwoord",
"username": "Mail gebruikersnaam",
@ -1425,7 +1425,7 @@
"not_yet": "Nog niet? Stuur het opnieuw",
"password_min_length": "Wachtwoord moet {count} tekens bevatten",
"name_min_length": "Naam moet minimaal {count} letters bevatten.",
"prefix_min_length": "Voorvoegsel moet minstens {count} letters bevatten.",
"prefix_min_length": "Prefix must have at least {count} letters.",
"enter_valid_tax_rate": "Voer een geldig belastingtarief in",
"numbers_only": "Alleen nummers.",
"characters_only": "Alleen tekens.",
@ -1440,7 +1440,7 @@
"price_minvalue": "Prijs moet hoger zijn dan 0.",
"amount_maxlength": "Bedrag mag niet groter zijn dan 20 cijfers.",
"amount_minvalue": "Bedrag moet groter zijn dan 0.",
"discount_maxlength": "Korting mag niet meer zijn dan maximale korting",
"discount_maxlength": "Discount should not be greater than max discount",
"description_maxlength": "De beschrijving mag niet meer dan 255 tekens bevatten.",
"subject_maxlength": "Het onderwerp mag niet meer dan 100 tekens bevatten.",
"message_maxlength": "Bericht mag niet groter zijn dan 255 tekens.",
@ -1451,9 +1451,9 @@
"prefix_maxlength": "Het voorvoegsel mag niet meer dan 5 tekens bevatten.",
"something_went_wrong": "Er is iets fout gegaan",
"number_length_minvalue": "Het getal moet groter zijn dan 0",
"at_least_one_ability": "Selecteer minstens één machtiging.",
"valid_driver_key": "Voer een geldige {driver} sleutel in.",
"valid_exchange_rate": "Voer a.u.b. een geldige wisselkoers in.",
"at_least_one_ability": "Please select atleast one Permission.",
"valid_driver_key": "Please enter a valid {driver} key.",
"valid_exchange_rate": "Please enter a valid exchange rate.",
"company_name_not_same": "Bedrijfsnaam moet overeenkomen met de opgegeven naam."
},
"errors": {
@ -1461,26 +1461,26 @@
"invalid_provider_key": "Voer een geldige API-sleutel in.",
"estimate_number_used": "Dit offertenummer is reeds in gebruik.",
"invoice_number_used": "Dit factuurnummer is reeds in gebruik.",
"payment_attached": "Deze factuur heeft al een betaling gekoppeld. Zorg ervoor dat u de bijgevoegde betalingen eerst verwijdert om door te gaan met de verwijdering.",
"payment_attached": "This invoice already has a payment attached to it. Make sure to delete the attached payments first in order to go ahead with the removal.",
"payment_number_used": "Dit factuurnummer is reeds in gebruik.",
"name_already_taken": "Deze naam is reeds in gebruik.",
"receipt_does_not_exist": "Kwitantie bestaat niet.",
"customer_cannot_be_changed_after_payment_is_added": "Klant kan niet worden gewijzigd nadat een betaling is toegevoegd",
"receipt_does_not_exist": "Receipt does not exist.",
"customer_cannot_be_changed_after_payment_is_added": "Customer cannot be change after payment is added",
"invalid_credentials": "Inloggegevens ongeldig.",
"not_allowed": "Niet toegestaan",
"login_invalid_credentials": "Deze gegevens zijn niet correct.",
"enter_valid_cron_format": "Voer een geldig cron-formaat in",
"email_could_not_be_sent": "E-mail kon niet naar dit e-mailadres worden verzonden.",
"invalid_address": "Vul een geldig adres in.",
"invalid_key": "Vul een geldige sleutel in.",
"invalid_state": "Vul een geldige provincie in.",
"invalid_city": "Vul een geldige woonplaats in.",
"invalid_postal_code": "Vul een geldige postcode in.",
"invalid_format": "Vul een geldig query-string-formaat in.",
"api_error": "Server reageert niet.",
"feature_not_enabled": "Functie niet ingeschakeld.",
"request_limit_met": "API-verzoeklimiet overschreden.",
"address_incomplete": "Onvolledig adres"
"enter_valid_cron_format": "Please enter a valid cron format",
"email_could_not_be_sent": "Email could not be sent to this email address.",
"invalid_address": "Please enter a valid address.",
"invalid_key": "Please enter valid key.",
"invalid_state": "Please enter a valid state.",
"invalid_city": "Please enter a valid city.",
"invalid_postal_code": "Please enter a valid zip.",
"invalid_format": "Please enter valid query string format.",
"api_error": "Server not responding.",
"feature_not_enabled": "Feature not enabled.",
"request_limit_met": "Api request limit exceeded.",
"address_incomplete": "Incomplete Address"
},
"pdf_estimate_label": "Offerte",
"pdf_estimate_number": "Offerte nummer",

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
"settings": "Настройки",
"logout": "Выйти",
"users": "Пользователи",
"modules": "Инструменты"
"modules": "Modules"
},
"general": {
"add_company": "Добавить компанию",
@ -29,9 +29,9 @@
"to_date": "До даты",
"from": "Отправитель",
"to": "Получатель",
"ok": "Ок",
"yes": "Да",
"no": "Нет",
"ok": "Ok",
"yes": "Yes",
"no": "No",
"sort_by": "Сортировать",
"ascending": "По возрастанию",
"descending": "По убыванию",
@ -39,7 +39,7 @@
"body": "Содержание",
"message": "Сообщение",
"send": "Отправить",
"preview": "Предпросмотр",
"preview": "Preview",
"go_back": "Назад",
"back_to_login": "Вернуться к логину?",
"home": "Домой",
@ -65,7 +65,7 @@
"sent": "Отправлено",
"all": "Все",
"select_all": "Выбрать всё",
"select_template": "Выбрать шаблон",
"select_template": "Select Template",
"choose_file": "Нажмите сюда чтобы выбрать файлы",
"choose_template": "Выберите шаблон",
"choose": "Выбор",
@ -93,14 +93,14 @@
"no_note_found": "Заметка не найдена",
"insert_note": "Вставить заметку",
"copied_pdf_url_clipboard": "Ссылка на PDF скопирована в буфер обмена!",
"copied_url_clipboard": "Ссылка скопирована!",
"copied_url_clipboard": "Copied url to clipboard!",
"docs": "Docs",
"do_you_wish_to_continue": "Хотите продолжить?",
"do_you_wish_to_continue": "Do you wish to continue?",
"note": "Note",
"pay_invoice": "Оплатить счет",
"login_successfully": "Вход выполнен!",
"logged_out_successfully": "Вы успешно вышли",
"mark_as_default": "Установить по умолчанию"
"pay_invoice": "Pay Invoice",
"login_successfully": "Logged in successfully!",
"logged_out_successfully": "Logged out successfully",
"mark_as_default": "Mark as default"
},
"dashboard": {
"select_year": "Выберите год",
@ -109,7 +109,7 @@
"customers": "Клиенты",
"invoices": "Счет-фактуры",
"estimates": "Заказы",
"payments": "Платежи"
"payments": "Payments"
},
"chart_info": {
"total_sales": "Продажи",
@ -151,27 +151,27 @@
"no_results_found": "Ничего не найдено"
},
"company_switcher": {
"label": "СМЕНИТЬ КОМПАНИЮ",
"no_results_found": "Результаты не найдены",
"add_new_company": "Добавить новую компанию",
"new_company": "Новая компания",
"created_message": "Компания создана успешно"
"label": "SWITCH COMPANY",
"no_results_found": "No Results Found",
"add_new_company": "Add new company",
"new_company": "New company",
"created_message": "Company created successfully"
},
"dateRange": {
"today": "Сегодня",
"this_week": "На этой неделе",
"this_month": "В этом месяце",
"this_quarter": "Текущий квартал",
"this_year": "В этом году",
"previous_week": "Предыдущая неделя",
"previous_month": "Предыдущий месяц",
"previous_quarter": "Предыдущий квартал",
"previous_year": "Предыдущий год",
"custom": "Выбор"
"today": "Today",
"this_week": "This Week",
"this_month": "This Month",
"this_quarter": "This Quarter",
"this_year": "This Year",
"previous_week": "Previous Week",
"previous_month": "Previous Month",
"previous_quarter": "Previous Quarter",
"previous_year": "Previous Year",
"custom": "Custom"
},
"customers": {
"title": "Клиенты",
"prefix": "Префикс",
"prefix": "Prefix",
"add_customer": "Добавить клиента",
"contacts_list": "Список клиентов",
"name": "Имя",
@ -186,9 +186,9 @@
"phone": "Телефон",
"website": "Сайт",
"overview": "Обзор",
"invoice_prefix": "Префикс счета",
"invoice_prefix": "Invoice Prefix",
"estimate_prefix": "Estimate Prefix",
"payment_prefix": "Префикс платежа",
"payment_prefix": "Payment Prefix",
"enable_portal": "Разрешить портал",
"country": "Страна",
"state": "Область",
@ -197,7 +197,7 @@
"added_on": "Добавлено",
"action": "Действие",
"password": "Пароль",
"confirm_password": "Подтвердить пароль",
"confirm_password": "Confirm Password",
"street_number": "Номер дома",
"primary_currency": "Основная валюта",
"description": "Описание",
@ -208,10 +208,10 @@
"new_customer": "New Клиент",
"edit_customer": "Редактировать клиента",
"basic_info": "Основное",
"portal_access": "Доступ к порталу",
"portal_access_text": "Хотите ли вы разрешить данному клиенту доступ к порталу для клиентов?",
"portal_access_url": "URL для входа на портал клиента",
"portal_access_url_help": "Пожалуйста, скопируйте и перешлите вышеуказанный URL вашему клиенту для предоставления доступа.",
"portal_access": "Portal Access",
"portal_access_text": "Would you like to allow this customer to login to the Customer Portal?",
"portal_access_url": "Customer Portal Login URL",
"portal_access_url_help": "Please copy & forward the above given URL to your customer for providing access.",
"billing_address": "Адрес плательщика",
"shipping_address": "Адрес доставки",
"copy_billing_address": "Скопировать из биллинга",
@ -231,9 +231,9 @@
"confirm_delete": "Восстановление клиента вместе со всеми его оплатами, сметами и счетами-фактурами будет невозможно. | Восстановление клиентов вместе со всеми их оплатами, сметами и счетами-фактурами будет невозможно.",
"created_message": "Клиент добавлен",
"updated_message": "Клиент обновлён",
"address_updated_message": "Информация об адресе успешно обновлена",
"address_updated_message": "Address Information Updated succesfully",
"deleted_message": "Клиент удалён | Клиенты удалены",
"edit_currency_not_allowed": "Невозможно изменить валюту после создания транзакций."
"edit_currency_not_allowed": "Cannot change currency once transactions created."
},
"items": {
"title": "Товары",
@ -265,8 +265,8 @@
},
"estimates": {
"title": "Заказы",
"accept_estimate": "Принять заказ",
"reject_estimate": "Отклонить заказ",
"accept_estimate": "Accept Estimate",
"reject_estimate": "Reject Estimate",
"estimate": "Заказ | Заказы",
"estimates_list": "Список заказов",
"days": "{days} дней",
@ -318,10 +318,10 @@
},
"accepted": "Принято",
"rejected": "Отклонено",
"expired": "Истёк",
"expired": "Expired",
"sent": "Отправлено",
"draft": "Черновик",
"viewed": "Просмотрено",
"viewed": "Viewed",
"declined": "Отказано",
"new_estimate": "Новый заказ",
"add_new_estimate": "Добавить новый заказ",
@ -355,14 +355,14 @@
"select_an_item": "Выберите товар",
"type_item_description": "Описание товара (необязательно)"
},
"mark_as_default_estimate_template_description": "Если включено, выбранный шаблон будет автоматически выбираться для новых заказов."
"mark_as_default_estimate_template_description": "If enabled, the selected template will be automatically selected for new estimates."
},
"invoices": {
"title": "Счет-фактуры",
"download": "Загрузить",
"pay_invoice": "Оплатить счет",
"download": "Download",
"pay_invoice": "Pay Invoice",
"invoices_list": "Список счетов",
"invoice_information": "Информация о счете",
"invoice_information": "Invoice Information",
"days": "{days} дн.",
"months": "{months} мес.",
"years": "{years} г.",
@ -397,16 +397,16 @@
"send_invoice": "Отправить счёт",
"resend_invoice": "Повторно отправить счет",
"invoice_template": "Шаблон счета",
"conversion_message": "Счет успешно скопирован",
"conversion_message": "Invoice cloned successful",
"template": "Шаблон",
"mark_as_sent": "Пометить как отправленное",
"confirm_send_invoice": "Этот счет будет отправлен клиенту по электронной почте",
"invoice_mark_as_sent": "Этот счет будет помечен как отправленный",
"confirm_mark_as_accepted": "Данный счет будет помечен как принятый",
"confirm_mark_as_rejected": "Данный счет будет помечен как отклоненный",
"confirm_mark_as_accepted": "This invoice will be marked as Accepted",
"confirm_mark_as_rejected": "This invoice will be marked as Rejected",
"confirm_send": "Этот счет будет отправлен клиенту по электронной почте",
"invoice_date": "Дата счета-фактуры",
"record_payment": "Добавить платёж",
"record_payment": "Record Payment",
"add_new_invoice": "Добавить новый счёт",
"update_expense": "Обновить расходы",
"edit_invoice": "Редактировать счет-фактуру",
@ -415,13 +415,13 @@
"update_invoice": "Обновить счет",
"add_new_tax": "Добавить новый налог",
"no_invoices": "Пока нет счетов!",
"mark_as_rejected": "Пометить как отклонённый",
"mark_as_accepted": "Пометить как принятый",
"mark_as_rejected": "Mark as rejected",
"mark_as_accepted": "Mark as accepted",
"list_of_invoices": "Этот раздел будет содержать список счетов-фактур.",
"select_invoice": "Выберите счет",
"no_matching_invoices": "Нет соответствующих счетов!",
"mark_as_sent_successfully": "Счет помечен как успешно отправленный",
"invoice_sent_successfully": "Счет-фактура успешно отправлен",
"invoice_sent_successfully": "Invoice sent successfully",
"cloned_successfully": "Счет успешно клонирован",
"clone_invoice": "Клонировать счет",
"confirm_clone": "Этот счет будет клонирован в новый счет",
@ -439,74 +439,74 @@
"select_an_item": "Выберите товар",
"type_item_description": "Описание товара (необязательно)"
},
"payment_attached_message": "К одному из выбранных счетов уже прикреплен платеж. Для удаления сначала удалите прикрепленные платежи",
"confirm_delete": "Восстановление данного счета будет невозможно | Восстановление данного счета будет невозможно",
"payment_attached_message": "One of the selected invoices already have a payment attached to it. Make sure to delete the attached payments first in order to go ahead with the removal",
"confirm_delete": "You will not be able to recover this Invoice | You will not be able to recover these Invoices",
"created_message": "Счет-фактура успешно создан",
"updated_message": "Счет-фактура успешно обновлен",
"deleted_message": "Счет успешно удален | Счета успешно удалены",
"marked_as_sent_message": "Счет помечен как успешно отправленный",
"something_went_wrong": "что-то пошло не так",
"invalid_due_amount_message": "Итоговая сумма счета не может быть меньше оплаченной суммы по данному счету. Пожалуйста, обновите счет или удалите связанные с ним платежи, чтобы продолжить.",
"mark_as_default_invoice_template_description": "Если включено, выбранный шаблон будет автоматически выбираться для новых счетов."
"invalid_due_amount_message": "Total Invoice amount cannot be less than total paid amount for this Invoice. Please update the invoice or delete the associated payments to continue.",
"mark_as_default_invoice_template_description": "If enabled, the selected template will be automatically selected for new invoices."
},
"recurring_invoices": {
"title": "Recurring Invoices",
"invoices_list": "Recurring Invoices List",
"days": "{days} Дней",
"months": "{months} Месяц",
"years": "{years} Год",
"all": "Все",
"paid": "Оплачено",
"unpaid": "Не оплачено",
"viewed": "Просмотрено",
"overdue": "Просрочен",
"active": "Активный",
"completed": "Выполнен",
"customer": "КЛИЕНТ",
"paid_status": "СТАТУС ПЛАТЕЖА",
"days": "{days} Days",
"months": "{months} Month",
"years": "{years} Year",
"all": "All",
"paid": "Paid",
"unpaid": "Unpaid",
"viewed": "Viewed",
"overdue": "Overdue",
"active": "Active",
"completed": "Completed",
"customer": "CUSTOMER",
"paid_status": "PAID STATUS",
"ref_no": "REF NO.",
"number": "НОМЕР",
"amount_due": "К ОПЛАТЕ",
"partially_paid": "Частично оплачен",
"total": "Итого",
"discount": "Скидка",
"sub_total": "Промежуточный итог",
"number": "NUMBER",
"amount_due": "AMOUNT DUE",
"partially_paid": "Partially Paid",
"total": "Total",
"discount": "Discount",
"sub_total": "Sub Total",
"invoice": "Recurring Invoice | Recurring Invoices",
"invoice_number": "Recurring Invoice Number",
"next_invoice_date": "Дата следующего счета",
"next_invoice_date": "Next Invoice Date",
"ref_number": "Ref Number",
"contact": "Контакты",
"add_item": "Добавить элемент",
"date": "Дата",
"contact": "Contact",
"add_item": "Add an Item",
"date": "Date",
"limit_by": "Limit by",
"limit_date": "Limit Date",
"limit_count": "Limit Count",
"count": "Количество",
"status": "Статус",
"select_a_status": "Выбрать статус",
"working": "В процессе",
"on_hold": "На удержании",
"complete": "Завершено",
"add_tax": "Добавить налог",
"amount": "Сумма",
"action": "Действие",
"notes": "Заметки",
"view": "Просмотр",
"basic_info": "Общая информация",
"count": "Count",
"status": "Status",
"select_a_status": "Select a status",
"working": "Working",
"on_hold": "On Hold",
"complete": "Completed",
"add_tax": "Add Tax",
"amount": "Amount",
"action": "Action",
"notes": "Notes",
"view": "View",
"basic_info": "Basic Info",
"send_invoice": "Send Recurring Invoice",
"auto_send": "Автоотправка",
"auto_send": "Auto Send",
"resend_invoice": "Resend Recurring Invoice",
"invoice_template": "Recurring Invoice Template",
"conversion_message": "Recurring Invoice cloned successful",
"template": "Шаблон",
"mark_as_sent": "Пометить как отправленное",
"template": "Template",
"mark_as_sent": "Mark as sent",
"confirm_send_invoice": "This recurring invoice will be sent via email to the customer",
"invoice_mark_as_sent": "This recurring invoice will be marked as sent",
"confirm_send": "This recurring invoice will be sent via email to the customer",
"starts_at": "Дата начала",
"due_date": "Срок оплаты счёта",
"record_payment": "Добавить платёж",
"add_new_invoice": "Добавить новый повторяющийся счет",
"starts_at": "Start Date",
"due_date": "Invoice Due Date",
"record_payment": "Record Payment",
"add_new_invoice": "Add New Recurring Invoice",
"update_expense": "Update Expense",
"edit_invoice": "Edit Recurring Invoice",
"new_invoice": "New Recurring Invoice",
@ -526,14 +526,14 @@
"cloned_successfully": "Recurring Invoice cloned successfully",
"clone_invoice": "Clone Recurring Invoice",
"confirm_clone": "This recurring invoice will be cloned into a new Recurring Invoice",
"add_customer_email": "Пожалуйста, добавьте адрес электронной почты для этого клиента, чтобы отправлять счета автоматически.",
"add_customer_email": "Please add an email address for this customer to send invoices automatically.",
"item": {
"title": "Название товара",
"description": "Описание",
"quantity": "Кол-во",
"price": "Цена",
"discount": "Скидка",
"total": "Итого",
"title": "Item Title",
"description": "Description",
"quantity": "Quantity",
"price": "Price",
"discount": "Discount",
"total": "Total",
"total_discount": "Total Discount",
"sub_total": "Sub Total",
"tax": "Tax",
@ -547,22 +547,22 @@
"minute": "Minute",
"hour": "Hour",
"day_month": "Day of month",
"month": "Месяц",
"day_week": "День недели"
"month": "Month",
"day_week": "Day of week"
},
"confirm_delete": "You will not be able to recover this Invoice | You will not be able to recover these Invoices",
"created_message": "Recurring Invoice created successfully",
"updated_message": "Recurring Invoice updated successfully",
"deleted_message": "Recurring Invoice deleted successfully | Recurring Invoices deleted successfully",
"marked_as_sent_message": "Recurring Invoice marked as sent successfully",
"user_email_does_not_exist": "Адрес электронной почты пользователя не найден",
"something_went_wrong": "что-то пошло не так",
"user_email_does_not_exist": "User email does not exist",
"something_went_wrong": "something went wrong",
"invalid_due_amount_message": "Total Recurring Invoice amount cannot be less than total paid amount for this Recurring Invoice. Please update the invoice or delete the associated payments to continue."
},
"payments": {
"title": "Платежи",
"payments_list": "Список платежей",
"record_payment": "Добавить платёж",
"record_payment": "Record Payment",
"customer": "Клиент",
"date": "Дата",
"amount": "Сумма",
@ -603,7 +603,7 @@
"select_a_customer": "Выберите клиента",
"expense_title": "Заголовок",
"customer": "Клиент",
"currency": "Валюта",
"currency": "Currency",
"contact": "Контакт",
"category": "Категория",
"from_date": "От даты",
@ -626,11 +626,11 @@
"new_expense": "Новый расход",
"expense": "Расход | Расходы",
"no_expenses": "No expenses yet!",
"list_of_expenses": "В этом разделе будет содержаться список расходов.",
"list_of_expenses": "This section will contain the list of expenses.",
"confirm_delete": "You will not be able to recover this Expense | You will not be able to recover these Expenses",
"created_message": "Расход создан успешно",
"updated_message": "Расход успешно обновлен",
"deleted_message": "Расход успешно удален | Расходы успешно удалены",
"created_message": "Expense created successfully",
"updated_message": "Expense updated successfully",
"deleted_message": "Expense deleted successfully | Expenses deleted successfully",
"categories": {
"categories_list": "Список категорий",
"title": "Заголовок",
@ -658,40 +658,40 @@
"retype_password": "Повторите пароль"
},
"modules": {
"buy_now": "Купить",
"install": "Установить",
"price": "Цена",
"download_zip_file": "Скачать ZIP-файл",
"unzipping_package": "Распаковка пакета",
"copying_files": "Копирование файлов",
"deleting_files": "Удаление неиспользуемых файлов",
"completing_installation": "Завершение установки",
"update_failed": "Не удалось обновить",
"install_success": "Модуль успешно установлен!",
"customer_reviews": "Отзывы",
"license": "Лицензия",
"faq": "Часто задаваемые вопросы",
"monthly": "Ежемесячно",
"yearly": "Ежегодно",
"updated": "Обновлено",
"version": "Версия",
"disable": "Отключить",
"module_disabled": "Модуль отключен",
"enable": "Включить",
"module_enabled": "Модуль включен",
"update_to": "Обновить до",
"module_updated": "Модуль успешно обновлен!",
"title": "Модули",
"module": "Модуль | Модули",
"api_token": "API-токен",
"invalid_api_token": "Неверный API-токен.",
"other_modules": "Другие модули",
"view_all": "Показать всё",
"no_reviews_found": "Для данного модуля пока нет отзывов!",
"module_not_purchased": "Модуль не приобретен",
"module_not_found": "Модуль не найден",
"buy_now": "Buy Now",
"install": "Install",
"price": "Price",
"download_zip_file": "Download ZIP file",
"unzipping_package": "Unzipping Package",
"copying_files": "Copying Files",
"deleting_files": "Deleting Unused files",
"completing_installation": "Completing Installation",
"update_failed": "Update Failed",
"install_success": "Module has been installed successfully!",
"customer_reviews": "Reviews",
"license": "License",
"faq": "FAQ",
"monthly": "Monthly",
"yearly": "Yearly",
"updated": "Updated",
"version": "Version",
"disable": "Disable",
"module_disabled": "Module Disabled",
"enable": "Enable",
"module_enabled": "Module Enabled",
"update_to": "Update To",
"module_updated": "Module Updated Successfully!",
"title": "Modules",
"module": "Module | Modules",
"api_token": "API token",
"invalid_api_token": "Invalid API Token.",
"other_modules": "Other Modules",
"view_all": "View All",
"no_reviews_found": "There are no reviews for this module yet!",
"module_not_purchased": "Module Not Purchased",
"module_not_found": "Module Not Found",
"version_not_supported": "This module version doesn't support the current version of Crater",
"last_updated": "Последнее обновление",
"last_updated": "Last Updated On",
"connect_installation": "Connect your installation",
"api_token_description": "Login to {url} and connect this installation by entering the API Token. Your purchased modules will show up here after the connection is established.",
"view_module": "View Module",
@ -728,7 +728,7 @@
"updated_message": "Пользователь успешно обновлен",
"deleted_message": "Пользователь успешно удален | Пользователи успешно удалены",
"select_company_role": "Select Role for {company}",
"companies": "Компании"
"companies": "Companies"
},
"reports": {
"title": "Отчёт",
@ -872,13 +872,13 @@
"address": "Адрес",
"zip": "Индекс",
"save": "Сохранить",
"delete": "Удалить",
"delete": "Delete",
"updated_message": "Информация о компании успешно обновлена",
"delete_company": "Удалить компанию",
"delete_company": "Delete Company",
"delete_company_description": "Once you delete your company, you will lose all the data and files associated with it permanently.",
"are_you_absolutely_sure": "Вы уверены?",
"are_you_absolutely_sure": "Are you absolutely sure?",
"delete_company_modal_desc": "This action cannot be undone. This will permanently delete {company} and all of its associated data.",
"delete_company_modal_label": "Пожалуйста, введите {company} для подтверждения"
"delete_company_modal_label": "Please type {company} to confirm"
},
"custom_fields": {
"title": "Пользовательские поля",
@ -890,7 +890,7 @@
"type": "Тип",
"name": "Название",
"slug": "Slug",
"required": "Обязательно для заполнения",
"required": "Required",
"placeholder": "Placeholder",
"help_text": "Справка",
"default_value": "Default Value",
@ -911,49 +911,49 @@
"sort_in_alphabetical_order": "Sort in Alphabetical Order",
"add_options_in_bulk": "Add options in bulk",
"use_predefined_options": "Use Predefined Options",
"select_custom_date": "Выберите произвольную дату",
"select_relative_date": "Выберите относительную дату",
"ticked_by_default": "Отмечен по умолчанию",
"updated_message": "Пользовательское поле успешно обновлено",
"added_message": "Пользовательское поле успешно добавлено",
"press_enter_to_add": "Нажмите ввод для добавления новой опции",
"model_in_use": "Невозможно обновить модель для полей, которые уже используются.",
"type_in_use": "Невозможно обновить тип для полей, которые уже используются."
"select_custom_date": "Select Custom Date",
"select_relative_date": "Select Relative Date",
"ticked_by_default": "Ticked by default",
"updated_message": "Custom Field updated successfully",
"added_message": "Custom Field added successfully",
"press_enter_to_add": "Press enter to add new option",
"model_in_use": "Cannot update model for fields which are already in use.",
"type_in_use": "Cannot update type for fields which are already in use."
},
"customization": {
"customization": "персонализация",
"customization": "customization",
"updated_message": "Информация о компании успешно обновлена",
"save": "Сохранить",
"insert_fields": "Вставить поля",
"insert_fields": "Insert Fields",
"learn_custom_format": "Learn how to use custom format",
"add_new_component": "Добавить компонент",
"component": "Компонент",
"Parameter": "Параметр",
"add_new_component": "Add New Component",
"component": "Component",
"Parameter": "Parameter",
"series": "Series",
"series_description": "To set a static prefix/postfix like 'INV' across your company. It supports character length of up to 6 chars.",
"series_param_label": "Series Value",
"delimiter": "Разделитель",
"delimiter_description": "Символ для обозначения границы между двумя компонентами. По умолчанию имеет значение -",
"delimiter_param_label": "Разделитель",
"date_format": "Формат даты",
"delimiter": "Delimiter",
"delimiter_description": "Single character for specifying the boundary between 2 separate components. By default its set to -",
"delimiter_param_label": "Delimiter Value",
"date_format": "Date Format",
"date_format_description": "A local date and time field which accepts a format parameter. The default format: 'Y' renders the current year.",
"date_format_param_label": "Формат",
"sequence": "Последовательность",
"date_format_param_label": "Format",
"sequence": "Sequence",
"sequence_description": "Consecutive sequence of numbers across your company. You can specify the length on the given parameter.",
"sequence_param_label": "Длина последовательности",
"sequence_param_label": "Sequence Length",
"customer_series": "Customer Series",
"customer_series_description": "Установить отдельный префикс/постфикс для каждого клиента.",
"customer_series_description": "To set a different prefix/postfix for each customer.",
"customer_sequence": "Customer Sequence",
"customer_sequence_description": "Consecutive sequence of numbers for each of your customer.",
"customer_sequence_param_label": "Длина последовательности",
"random_sequence": "Случайная последовательность",
"random_sequence_description": "Произвольная буквенно-цифровая строка. Вы можете указать длину в качестве параметра.",
"random_sequence_param_label": "Длина последовательности",
"customer_sequence_param_label": "Sequence Length",
"random_sequence": "Random Sequence",
"random_sequence_description": "Random alphanumeric string. You can specify the length on the given parameter.",
"random_sequence_param_label": "Sequence Length",
"invoices": {
"title": "Счет-фактуры",
"invoice_number_format": "Формат номера счета",
"invoice_number_format": "Invoice Number Format",
"invoice_number_format_description": "Customize how your invoice number gets generated automatically when you create a new invoice.",
"preview_invoice_number": "Предосмотр номера счета",
"preview_invoice_number": "Preview Invoice Number",
"due_date": "Due Date",
"due_date_description": "Specify how due date is automatically set when you create an invoice.",
"due_date_days": "Invoice Due after days",
@ -969,7 +969,7 @@
"invoice_email_attachment_setting_description": "Включите, если вы хотите отправлять счета-фактуры как вложение по электронной почте. Пожалуйста, обратите внимание, что кнопка «Просмотр счета» в письмах больше не будет отображаться, если включено.",
"invoice_settings_updated": "Invoice Settings updated successfully",
"retrospective_edits": "Retrospective Edits",
"allow": "Разрешить",
"allow": "Allow",
"disable_on_invoice_partial_paid": "Disable after partial payment is recorded",
"disable_on_invoice_paid": "Disable after full payment is recorded",
"disable_on_invoice_sent": "Disable after invoice is sent",
@ -1116,9 +1116,9 @@
"currency_layer": "Currency Layer",
"open_exchange_rate": "Open Exchange Rate",
"currency_converter": "Currency Converter",
"server": "Сервер",
"url": "URL-адрес",
"active": "Активный",
"server": "Server",
"url": "URL",
"active": "Active",
"currency_help_text": "This provider will only be used on above selected currencies",
"currency_in_used": "The following currencies are already active on another provider. Please remove these currencies from selection to activate this provider again."
},

File diff suppressed because it is too large Load Diff

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