Compare commits

...

41 Commits

Author SHA1 Message Date
948c71f365 fix indentation on dashboard chart 2023-03-28 15:24:10 +05:30
3710ba74e5 add dark mode in dashboard stats & chart 2023-03-21 15:52:29 +05:30
15f3f566e3 Dark sidebar header (#1156)
* add dark mode in sidebar and header

* add dark theme in blade file
2023-03-20 15:50:27 +05:30
98196194e2 Merge branch 'master' of https://github.com/bytefury/crater 2022-12-13 11:38:49 +05:30
7447cc24f9 feat: uffizzi integration (#1091)
Co-authored-by: Aramayis <>
2022-11-21 18:35:59 +05:30
889d22d92c Update php version (#1083)
Version 7.4 no longer works when running docker-compose/setup.sh. Updated to 8.1 in Dockerfile. Issue is now resolved and Crater sets up as expected without version conflicts.
2022-11-03 19:29:16 +05:30
bc8f2cd484 fix tax issue (#953)
* fix tax issue

* remove console log

* update cs fixer package
2022-10-26 19:51:36 +05:30
4e47f58bad fixed - No query results for model [Crater\Models\Currency] (#1070)
* fixed report pdf issue

* Removed telescope service provider file
2022-10-26 19:33:25 +05:30
d8c429912e fix composer issue with pest 2022-10-17 13:04:27 +05:30
df04fd9e53 Merge branch 'master' of https://github.com/bytefury/crater 2022-10-17 12:48:24 +05:30
0aaf0e7e75 Adding object-fit rules so thumbnail image does not appear stretched. (#1065) 2022-10-13 23:44:39 +05:30
3d0b89bb4d bug: fix missing hyphen in setup.sh (#1067)
Co-authored-by: Aramayis <>
2022-10-13 23:40:07 +05:30
38c4b9ebce Patch setup.sh script to deploy without plugins error. (#1034) 2022-09-13 14:52:51 +05:30
7be59e78e0 Formatting + Netherlands rename (#973)
* Formatting + Netherlands rename

* Revert change
2022-07-09 20:03:08 +05:30
189c51cdf4 Merge branch 'master' of https://github.com/bytefury/crater 2022-06-15 20:39:16 +05:30
204483836a 🌐Update: support Thai language (#768)
* add thai language config

* update thai translation

* update thai translation

* add THSarabunNew fonts to using in pdf

* update: pdf file to support thai language by checking isLocale('th') and include thai font-family

* update: thai translation

* remove the index.php file (not used)

* move THSarabunNew fonts to resoureces/static/fonts

* Add .gitkeep to storage/fonts to pre-build the fonts directory

Co-authored-by: Ritthikrai (Champ) Wiengchai <ritthikrai.w@aware.co.th>
2022-06-15 18:17:36 +05:30
33bc9ded65 Bump guzzlehttp/guzzle from 7.4.1 to 7.4.3 (#936)
Bumps [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) from 7.4.1 to 7.4.3.
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/master/CHANGELOG.md)
- [Commits](https://github.com/guzzle/guzzle/compare/7.4.1...7.4.3)

---
updated-dependencies:
- dependency-name: guzzlehttp/guzzle
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-05 14:10:27 +05:30
4271ef451e Php 8 compliant (#914) 2022-06-05 14:09:42 +05:30
6eb44fba93 Update cron.dockerfile (#949) 2022-06-05 14:09:11 +05:30
96e7300583 Make unit clearable (#918) 2022-05-28 11:55:09 +05:30
a479d966d1 Update en.json (#925)
Corrected sentence case from "Expiry date" to "Expiry Date"
2022-05-28 11:54:32 +05:30
bd5f0fe5cf Merge branch 'v-calendar' into 'master'
apply v-calendar

See merge request mohit.panjvani/crater-web!1468
2022-04-01 10:24:57 +00:00
787619b907 add v-calendar 2022-04-01 10:24:56 +00:00
fd70ab9a99 Merge branch 'fix-expense-receipt-delete-issue' into 'master'
Fix expense receipt delete issue while update

See merge request mohit.panjvani/crater-web!1477
2022-04-01 10:24:20 +00:00
33315638df Fix expense receipt delete issue while update 2022-04-01 15:51:12 +05:30
bba14bf51a Merge branch 'master' 2022-04-01 12:04:06 +05:30
ea41989034 Merge branch 'fix-expense-receipt-issue' into 'master'
fix expense receipt issue

See merge request mohit.panjvani/crater-web!1473
2022-03-30 09:02:56 +00:00
3f3f83a00a fix expense receipt issue 2022-03-30 09:02:56 +00:00
adc5962071 Merge branch 'fix-invoice-status-issue' into 'master'
Fix invoice status issue

See merge request mohit.panjvani/crater-web!1470
2022-03-30 07:38:38 +00:00
c4c00002d7 Fix invoice status issue 2022-03-30 07:38:38 +00:00
a7c1e12db6 Merge branch 'new-custom-fields' into 'master'
added "Due Amount, Total Amount and PDF Link" new custom fields

See merge request mohit.panjvani/crater-web!1464
2022-03-30 07:35:53 +00:00
32949d1eec added "Due Amount, Total Amount and PDF Link" new custom fields 2022-03-30 07:35:53 +00:00
7718f77f3d Merge branch 'master' of https://github.com/bytefury/crater 2022-03-29 13:16:03 +05:30
a9e54981bf Merge branch 'fix-minor-issues' into 'master'
Fix minor issues

See merge request mohit.panjvani/crater-web!1469
2022-03-21 06:45:14 +00:00
ef35293f8a update company slug help text 2022-03-21 12:13:05 +05:30
5c63770b6b added unique validation and help text for company slug field 2022-03-21 12:04:17 +05:30
d130e20c92 Merge branch 'fix-default-custom-value' into 'master'
fixed "Custom fields always set to default value"

See merge request mohit.panjvani/crater-web!1471
2022-03-16 06:04:21 +00:00
586fb1ae10 fixed "Custom fields always set to default value" 2022-03-16 11:28:08 +05:30
20c2502e31 added company slug 2022-03-14 17:45:58 +05:30
0e31f85c18 change company currency if it does not have any transaction. 2022-03-14 12:55:38 +05:30
e7301eb7a3 Fix minor issues 2022-03-14 11:54:13 +05:30
93 changed files with 4876 additions and 3232 deletions

162
.github/workflows/uffizzi-build.yml vendored Normal file
View File

@ -0,0 +1,162 @@
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: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- 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
cache-from: type=gha
cache-to: type=gha,mode=max
build-nginx:
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
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

84
.github/workflows/uffizzi-preview.yml vendored Normal file
View File

@ -0,0 +1,84 @@
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

1
.gitignore vendored
View File

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

View File

@ -1,4 +1,4 @@
FROM php:7.4-fpm
FROM php:8.1-fpm
# Arguments defined in docker-compose.yml
ARG user

View File

@ -103,6 +103,7 @@ class CustomerStatsController extends Controller
)
->whereCompany()
->whereCustomer($customer->id)
->where('status', '<>', Invoice::STATUS_DRAFT)
->sum('total');
$totalReceipts = Payment::whereBetween(
'payment_date',

View File

@ -104,6 +104,7 @@ class DashboardController extends Controller
'invoice_date',
[$startDate->format('Y-m-d'), $start->format('Y-m-d')]
)
->where('status', '<>', Invoice::STATUS_DRAFT)
->whereCompany()
->sum('base_total');
@ -141,6 +142,7 @@ class DashboardController extends Controller
$recent_due_invoices = Invoice::with('customer')
->whereCompany()
->where('base_due_amount', '>', 0)
->where('status', '<>', Invoice::STATUS_DRAFT)
->take(5)
->latest()
->get();

View File

@ -24,6 +24,7 @@ class InvoicesController extends Controller
$limit = $request->has('limit') ? $request->limit : 10;
$invoices = Invoice::whereCompany()
->whereTabFilters($request->tab_status)
->join('customers', 'customers.id', '=', 'invoices.customer_id')
->applyFilters($request->all())
->select('invoices.*', 'customers.name')

View File

@ -2,24 +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\Currency;
use Crater\Models\Customer;
use Illuminate\Http\Request;
use Crater\Models\CompanySetting;
use Illuminate\Support\Facades\App;
use PDF;
use Crater\Http\Controllers\Controller;
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();
@ -56,6 +57,7 @@ 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',
@ -80,6 +82,7 @@ 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,24 +2,25 @@
namespace Crater\Http\Controllers\V1\Admin\Report;
use Carbon\Carbon;
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;
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;
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();
@ -43,6 +44,7 @@ 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',
@ -66,6 +68,7 @@ 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,24 +2,25 @@
namespace Crater\Http\Controllers\V1\Admin\Report;
use Carbon\Carbon;
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;
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;
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();
@ -43,6 +44,7 @@ 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',
@ -66,6 +68,7 @@ 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,25 +2,26 @@
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 PDF;
use Crater\Http\Controllers\Controller;
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();
@ -49,6 +50,8 @@ 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',
@ -74,6 +77,7 @@ 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,24 +2,25 @@
namespace Crater\Http\Controllers\V1\Admin\Report;
use Carbon\Carbon;
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;
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;
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();
@ -44,6 +45,8 @@ 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',
@ -68,6 +71,7 @@ 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,7 +3,6 @@
namespace Crater\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class CompaniesRequest extends FormRequest
@ -34,6 +33,10 @@ class CompaniesRequest extends FormRequest
'currency' => [
'required'
],
'slug' => [
'required',
Rule::unique('companies')
],
'address.name' => [
'nullable',
],
@ -68,11 +71,11 @@ class CompaniesRequest extends FormRequest
{
return collect($this->validated())
->only([
'name'
'name',
'slug'
])
->merge([
'owner_id' => $this->user()->id,
'slug' => Str::slug($this->name)
'owner_id' => $this->user()->id
])
->toArray();
}

View File

@ -30,7 +30,8 @@ class CompanyRequest extends FormRequest
Rule::unique('companies')->ignore($this->header('company'), 'id'),
],
'slug' => [
'nullable'
'required',
Rule::unique('companies')->ignore($this->header('company'), 'id'),
],
'address.country_id' => [
'required',

View File

@ -23,7 +23,7 @@ class EstimateResource extends JsonResource
'reference_number' => $this->reference_number,
'tax_per_item' => $this->tax_per_item,
'discount_per_item' => $this->discount_per_item,
'notes' => $this->getNotes(),
'notes' => $this->notes,
'discount' => $this->discount,
'discount_type' => $this->discount_type,
'discount_val' => $this->discount_val,

View File

@ -18,7 +18,7 @@ class PaymentResource extends JsonResource
'id' => $this->id,
'payment_number' => $this->payment_number,
'payment_date' => $this->payment_date,
'notes' => $this->getNotes(),
'notes' => $this->notes,
'amount' => $this->amount,
'unique_hash' => $this->unique_hash,
'invoice_id' => $this->invoice_id,

View File

@ -217,7 +217,7 @@ class Company extends Model implements HasMedia
'estimate_billing_address_format' => $billingAddressFormat,
'payment_company_address_format' => $companyAddressFormat,
'payment_from_customer_address_format' => $paymentFromCustomerAddress,
'currency' => request()->currency ?? 13,
'currency' => request()->currency ?? 1,
'time_zone' => 'Asia/Kolkata',
'language' => 'en',
'fiscal_year' => '1-12',

View File

@ -483,7 +483,8 @@ class Estimate extends Model implements HasMedia
'{ESTIMATE_DATE}' => $this->formattedEstimateDate,
'{ESTIMATE_EXPIRY_DATE}' => $this->formattedExpiryDate,
'{ESTIMATE_NUMBER}' => $this->estimate_number,
'{ESTIMATE_REF_NUMBER}' => $this->reference_number,
'{PDF_LINK}' => $this->estimatePdfUrl,
'{TOTAL_AMOUNT}' => format_money_pdf($this->total, $this->customer->currency)
];
}

View File

@ -240,7 +240,7 @@ class Expense extends Model implements HasMedia
}
if ($request->hasFile('attachment_receipt')) {
$expense->addMediaFromRequest('attachment_receipt')->toMediaCollection('receipts');
$expense->addMediaFromRequest('attachment_receipt')->toMediaCollection('receipts', 'local');
}
if ($request->customFields) {
@ -262,12 +262,12 @@ class Expense extends Model implements HasMedia
ExchangeRateLog::addExchangeRateLog($this);
}
if (isset($request->is_attachment_receipt_removed) && (bool) $request->is_attachment_receipt_removed) {
if (isset($request->is_attachment_receipt_removed) && $request->is_attachment_receipt_removed == "true") {
$this->clearMediaCollection('receipts');
}
if ($request->hasFile('attachment_receipt')) {
$this->clearMediaCollection('receipts');
$this->addMediaFromRequest('attachment_receipt')->toMediaCollection('receipts');
$this->addMediaFromRequest('attachment_receipt')->toMediaCollection('receipts', 'local');
}
if ($request->customFields) {

View File

@ -187,16 +187,6 @@ class Invoice extends Model implements HasMedia
return Carbon::parse($this->invoice_date)->format($dateFormat);
}
public function scopeWhereStatus($query, $status)
{
return $query->where('invoices.status', $status);
}
public function scopeWherePaidStatus($query, $status)
{
return $query->where('invoices.paid_status', $status);
}
public function scopeWhereDueStatus($query, $status)
{
return $query->whereIn('invoices.paid_status', [
@ -234,6 +224,40 @@ class Invoice extends Model implements HasMedia
$query->orderBy($orderByField, $orderBy);
}
public function scopeWhereStatus($query, $status)
{
return $query->where('invoices.status', $status);
}
public function scopeWherePaidStatus($query, $status)
{
return $query->where('invoices.paid_status', $status);
}
public function scopeWhereTabFilters($query, $status)
{
if ($status == "DRAFT") {
return $query->where('invoices.status', $status);
}
if ($status == "SENT") {
return $query->whereIn('invoices.status', [
self::STATUS_SENT,
self::STATUS_VIEWED,
self::STATUS_COMPLETED
]);
}
if ($status == 'DUE') {
return $query->whereIn('invoices.paid_status', [
self::STATUS_UNPAID,
self::STATUS_PARTIALLY_PAID,
]);
}
return ;
}
public function scopeApplyFilters($query, array $filters)
{
$filters = collect($filters);
@ -249,17 +273,11 @@ class Invoice extends Model implements HasMedia
$filters->get('status') == self::STATUS_PAID
) {
$query->wherePaidStatus($filters->get('status'));
} elseif ($filters->get('status') == 'DUE') {
$query->whereDueStatus($filters->get('status'));
} else {
$query->whereStatus($filters->get('status'));
}
}
if ($filters->get('paid_status')) {
$query->wherePaidStatus($filters->get('status'));
}
if ($filters->get('invoice_id')) {
$query->whereInvoice($filters->get('invoice_id'));
}
@ -651,7 +669,9 @@ class Invoice extends Model implements HasMedia
'{INVOICE_DATE}' => $this->formattedInvoiceDate,
'{INVOICE_DUE_DATE}' => $this->formattedDueDate,
'{INVOICE_NUMBER}' => $this->invoice_number,
'{INVOICE_REF_NUMBER}' => $this->reference_number,
'{PDF_LINK}' => $this->invoicePdfUrl,
'{DUE_AMOUNT}' => format_money_pdf($this->due_amount, $this->customer->currency),
'{TOTAL_AMOUNT}' => format_money_pdf($this->total, $this->customer->currency)
];
}

View File

@ -435,7 +435,8 @@ class Payment extends Model implements HasMedia
'{PAYMENT_DATE}' => $this->formattedPaymentDate,
'{PAYMENT_MODE}' => $this->paymentMethod ? $this->paymentMethod->name : null,
'{PAYMENT_NUMBER}' => $this->payment_number,
'{PAYMENT_AMOUNT}' => $this->reference_number,
'{PDF_LINK}' => $this->paymentPdfUrl,
'{PAYMENT_AMOUNT}' => format_money_pdf($this->amount, $this->customer->currency)
];
}

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.0",
"fzaninotto/faker": "^1.9.1",
"friendsofphp/php-cs-fixer": "^3.8",
"fakerphp/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.0"
"phpunit/phpunit": "^9.3"
},
"autoload": {
"psr-4": {
@ -81,11 +81,14 @@
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"extra": {
"laravel": {
"dont-discover": []
}
}
}
}

2347
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -71,6 +71,7 @@ return [
["code" => "cs", "name" => "Czech"],
["code" => "el", "name" => "Greek"],
["code" => "hr", "name" => "Crotian"],
["code" => "th", "name" => "ไทย"],
],
/*

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 The",'phonecode' => 31],
['id' => 155,'code' => 'NL','name' => "Netherlands",'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

@ -1,7 +1,7 @@
FROM php:7.4-fpm-alpine
FROM php:8.0-fpm-alpine
RUN apk add --no-cache \
php7-bcmath
php8-bcmath
RUN docker-php-ext-install pdo pdo_mysql bcmath

View File

@ -27,7 +27,7 @@
"vite": "^2.6.1"
},
"dependencies": {
"@headlessui/vue": "^1.4.0",
"@headlessui/vue": "^1.5.0",
"@heroicons/vue": "^1.0.1",
"@popperjs/core": "^2.9.2",
"@stripe/stripe-js": "^1.21.2",
@ -48,7 +48,8 @@
"mini-svg-data-uri": "^1.3.3",
"moment": "^2.29.1",
"pinia": "^2.0.4",
"v-money3": "^3.13.5",
"v-calendar": "3.0.0-alpha.8",
"v-money3": "3.16.1",
"v-tooltip": "^4.0.0-alpha.1",
"vue": "^3.2.0-beta.5",
"vue-flatpickr-component": "^9.0.3",

View File

@ -6,8 +6,17 @@
<script setup>
import Chart from 'chart.js'
import { ref, reactive, computed, onMounted, watchEffect, inject } from 'vue'
import {
ref,
reactive,
computed,
onMounted,
watchEffect,
inject,
watch,
} from 'vue'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import { useGlobalStore } from '@/scripts/admin/stores/global'
const utils = inject('utils')
@ -44,9 +53,11 @@ const props = defineProps({
},
})
const isDarkModeOn = document.documentElement.classList.contains('dark')
let myLineChart = null
const graph = ref(null)
const companyStore = useCompanyStore()
const globalStore = useGlobalStore()
const defaultCurrency = computed(() => {
return companyStore.selectedCompanyCurrency
})
@ -60,6 +71,14 @@ watchEffect(() => {
}
})
watch(
() => globalStore.isDarkModeOn,
() => {
myLineChart.reset()
updateColors()
}
)
onMounted(() => {
let context = graph.value.getContext('2d')
let options = reactive({
@ -81,6 +100,8 @@ onMounted(() => {
},
})
const salesColor = globalStore.isDarkModeOn ? '#ffffff' : '#040405'
let data = reactive({
labels: props.labels,
datasets: [
@ -89,16 +110,16 @@ onMounted(() => {
fill: false,
lineTension: 0.3,
backgroundColor: 'rgba(230, 254, 249)',
borderColor: '#040405',
borderColor: salesColor,
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: '#040405',
pointBorderColor: salesColor,
pointBackgroundColor: '#fff',
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: '#040405',
pointHoverBackgroundColor: salesColor,
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 4,
@ -194,4 +215,12 @@ function update() {
lazy: true,
})
}
function updateColors() {
const newColor = globalStore.isDarkModeOn ? '#ffffff' : '#040405'
myLineChart.data.datasets[0].borderColor = newColor
myLineChart.data.datasets[0].pointBorderColor = newColor
myLineChart.data.datasets[0].pointHoverBackgroundColor = newColor
}
</script>

View File

@ -64,7 +64,7 @@ function mergeExistingValues() {
if (props.isEdit) {
props.store[props.storeProp].fields.forEach((field) => {
const existingIndex = props.store[props.storeProp].customFields.findIndex(
(f) => f.id === field.custom_field_id
(f) => f.id == field.custom_field_id
)
if (existingIndex > -1) {

View File

@ -9,7 +9,7 @@ import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: moment().format('YYYY-MM-DD hh:MM'),
default: moment().format('YYYY-MM-DD HH:mm'),
},
})

View File

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

View File

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

View File

@ -29,14 +29,7 @@
<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"
@ -66,14 +59,7 @@
<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>
@ -98,7 +84,7 @@
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText
:lines="1"
class="w-24 h-8 rounded-md border"
class="w-24 h-8 border rounded-md"
/>
</BaseContentPlaceholders>
<div v-else class="flex" style="width: 140px" role="group">
@ -114,7 +100,7 @@
<BaseDropdown position="bottom-end">
<template #activator>
<BaseButton
class="rounded-tr-md rounded-br-md p-2 rounded-none"
class="p-2 rounded-none rounded-tr-md rounded-br-md"
type="button"
variant="white"
>
@ -127,7 +113,7 @@
<BaseIcon
name="ChevronDownIcon"
class="w-4 h-4 text-gray-500 ml-1"
class="w-4 h-4 ml-1 text-gray-500"
/>
</span>
</BaseButton>
@ -180,15 +166,7 @@
</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" />
@ -204,14 +182,7 @@
</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>
@ -334,6 +305,7 @@ 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

@ -48,6 +48,24 @@
/>
</BaseInputGroup>
<BaseInputGroup
:label="$tc('settings.company_info.company_slug')"
:help-text="$t('settings.company_info.company_slug_help_text')"
:error="
v$.newCompanyForm.slug.$error &&
v$.newCompanyForm.slug.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseInput
v-model="newCompanyForm.slug"
:invalid="v$.newCompanyForm.slug.$error"
:content-loading="isFetchingInitialData"
@input="v$.newCompanyForm.slug.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:content-loading="isFetchingInitialData"
:label="$tc('settings.company_info.country')"
@ -130,7 +148,7 @@
<script setup>
import { useModalStore } from '@/scripts/stores/modal'
import { computed, onMounted, ref, reactive } from 'vue'
import { computed, onMounted, ref, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { required, minLength, helpers } from '@vuelidate/validators'
import { useVuelidate } from '@vuelidate/core'
@ -152,6 +170,7 @@ let companyLogoName = ref(null)
const newCompanyForm = reactive({
name: null,
slug: null,
currency: '',
address: {
country_id: null,
@ -162,6 +181,9 @@ const modalActive = computed(() => {
return modalStore.active && modalStore.componentName === 'CompanyModal'
})
const slugValidator = (value) => {
return value == slugify(value)
}
const rules = {
newCompanyForm: {
name: {
@ -171,6 +193,17 @@ const rules = {
minLength(3)
),
},
slug: {
required: helpers.withMessage(t('validation.required'), required),
minLength: helpers.withMessage(
t('validation.name_min_length', { count: 3 }),
minLength(3)
),
slugValidator: helpers.withMessage(
t('validation.invalid_slug'),
slugValidator
),
},
address: {
country_id: {
required: helpers.withMessage(t('validation.required'), required),
@ -243,6 +276,7 @@ async function submitCompanyData() {
function resetNewCompanyForm() {
newCompanyForm.name = ''
newCompanyForm.slug = ''
newCompanyForm.currency = ''
newCompanyForm.address.country_id = ''
@ -257,4 +291,24 @@ function closeCompanyModal() {
v$.value.$reset()
}, 300)
}
// watcher for if change company name then auto fill company slug value
watch(
() => newCompanyForm.name,
(currentValue) => {
newCompanyForm.slug = slugify(currentValue)
}
)
function slugify(string) {
return string
.toString()
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
}
</script>

View File

@ -15,6 +15,13 @@
bg-gradient-to-r
from-primary-500
to-primary-400
dark:from-gray-700/70 dark:to-gray-800/70
bg-primary-500
dark:bg-transparent
dark:backdrop-blur-xl
dark:shadow-glass
dark:border
dark:border-white/10
"
>
<router-link
@ -53,6 +60,7 @@
cursor-pointer
md:hidden md:ml-0
hover:bg-gray-100
dark:bg-gray-800 dark:border-gray-500 dark:border
"
@click.prevent="onToggle"
>
@ -143,7 +151,7 @@
<template #activator>
<img
:src="previewAvatar"
class="block w-8 h-8 rounded md:h-9 md:w-9"
class="block w-8 h-8 rounded md:h-9 md:w-9 object-cover"
/>
</template>

View File

@ -15,7 +15,9 @@
leave-from="opacity-100"
leave-to="opacity-0"
>
<DialogOverlay class="fixed inset-0 bg-gray-600 bg-opacity-75" />
<DialogOverlay
class="fixed inset-0 bg-gray-600 bg-opacity-75 dark:bg-gray-900/90"
/>
</TransitionChild>
<TransitionChild
@ -27,7 +29,9 @@
leave-from="translate-x-0"
leave-to="-translate-x-full"
>
<div class="relative flex flex-col flex-1 w-full max-w-xs bg-white">
<div
class="relative flex flex-col flex-1 w-full max-w-xs bg-white dark:bg-gray-800"
>
<TransitionChild
as="template"
enter="ease-in-out duration-300"
@ -40,18 +44,17 @@
<div class="absolute top-0 right-0 pt-2 -mr-12">
<button
class="
flex
items-center
justify-center
w-10
h-10
ml-1
rounded-full
focus:outline-none
focus:ring-2
focus:ring-inset
focus:ring-white
"
flex
items-center
justify-center
w-10
h-10
ml-1
rounded-full
focus:outline-none
focus:ring-2
focus:ring-inset
focus:ring-white"
@click="globalStore.setSidebarVisibility(false)"
>
<span class="sr-only">Close sidebar</span>
@ -82,8 +85,8 @@
:to="item.link"
:class="[
hasActiveUrl(item.link)
? 'text-primary-500 border-primary-500 bg-gray-100 '
: 'text-black',
? 'text-primary-500 border-primary-500 bg-gray-100 dark:shadow-glass dark:backdrop-blur-xl dark:hover:bg-gray-700 dark:bg-gray-700/50 dark:text-primary-400 dark:font-medium'
: 'text-black dark:text-gray-300',
'cursor-pointer px-0 pl-4 py-3 border-transparent flex items-center border-l-4 border-solid text-sm not-italic font-medium',
]"
@click="globalStore.setSidebarVisibility(false)"
@ -100,6 +103,10 @@
/>
{{ $t(item.title) }}
</router-link>
<LightDarkSwitch
:show-label="false"
class="absolute right-6 top-6 !w-auto"
/>
</nav>
</div>
</div>
@ -113,17 +120,16 @@
<!-- DESKTOP MENU -->
<div
class="
hidden
w-56
h-screen
pb-32
overflow-y-auto
bg-white
border-r border-gray-200 border-solid
xl:w-64
md:fixed md:flex md:flex-col md:inset-y-0
pt-16
"
hidden
w-56
h-screen
bg-white
border-r border-gray-200 border-solid
xl:w-64
md:fixed md:flex md:flex-col md:inset-y-0
pt-16
dark:border-gray-800
dark:bg-gray-800/80"
>
<div
v-for="menu in globalStore.menuGroups"
@ -136,8 +142,8 @@
:to="item.link"
:class="[
hasActiveUrl(item.link)
? 'text-primary-500 border-primary-500 bg-gray-100 '
: 'text-black',
? 'text-primary-500 border-primary-500 bg-gray-100 dark:border-primary-400 dark:shadow-glass dark:backdrop-blur-xl dark:hover:bg-gray-700 dark:bg-gray-700/50 dark:text-primary-400 dark:font-medium'
: 'text-black dark:hover:bg-transparent dark:hover:text-white dark:text-gray-300',
'cursor-pointer px-0 pl-6 hover:bg-gray-50 py-3 group flex items-center border-l-4 border-solid border-transparent text-sm not-italic font-medium',
]"
>
@ -145,8 +151,8 @@
:name="item.icon"
:class="[
hasActiveUrl(item.link)
? 'text-primary-500 group-hover:text-primary-500 '
: 'text-gray-400 group-hover:text-black',
? 'text-primary-500 group-hover:text-primary-500 dark:text-primary-400 dark:group-hover:text-primary-500 '
: 'text-gray-400 group-hover:text-black dark:text-gray-400 dark:group-hover:text-white',
'mr-4 shrink-0 h-5 w-5 ',
]"
/>
@ -154,6 +160,9 @@
{{ $t(item.title) }}
</router-link>
</div>
<LightDarkSwitch
class="absolute bottom-0 py-4 border-t border-gray-200 dark:border-gray-700"
/>
</div>
</template>
@ -169,6 +178,7 @@ import {
import { useRoute } from 'vue-router'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import LightDarkSwitch from '@/scripts/components/LightDarkSwitcher.vue'
const route = useRoute()
const globalStore = useGlobalStore()

View File

@ -184,6 +184,20 @@ export const useCompanyStore = (useWindow = false) => {
setDefaultCurrency(data) {
this.defaultCurrency = data.currency
},
checkCompanyHasCurrencyTransactions() {
return new Promise((resolve, reject) => {
axios
.get(`/api/v1/company/has-transactions`)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})()
}

View File

@ -34,6 +34,7 @@ export const useGlobalStore = (useWindow = false) => {
isAppLoaded: false,
isSidebarOpen: false,
areCurrenciesLoading: false,
isDarkModeOn: false,
downloadReport: null,
}),
@ -70,8 +71,8 @@ export const useGlobalStore = (useWindow = false) => {
moduleStore.apiToken = response.data.global_settings.api_token
moduleStore.enableModules = response.data.modules
// company store
companyStore.companies = response.data.companies
// company store
companyStore.companies = response.data.companies
companyStore.selectedCompany = response.data.current_company
companyStore.setSelectedCompany(response.data.current_company)
companyStore.selectedCompanySettings =

View File

@ -2,8 +2,23 @@
<div>
<div
v-if="dashboardStore.isDashboardDataLoaded"
class="grid grid-cols-10 mt-8 bg-white rounded shadow"
class="
grid
grid-cols-10
mt-8
bg-white
rounded shadow
dark:text-white
dark:backdrop-blur-xl
dark:shadow-glass
dark:border
dark:bg-opacity-70
dark:border-white/10
dark:bg-gray-800
relative
"
>
<BaseDarkHighlight />
<!-- Chart -->
<div
class="
@ -54,6 +69,7 @@
lg:border-t-0 lg:text-right lg:col-span-3
xl:col-span-2
lg:grid-cols-1
dark:border-white/10
"
>
<div class="p-6">
@ -96,15 +112,7 @@
</span>
<br />
<span
class="
block
mt-1
text-xl
font-semibold
leading-8
lg:text-2xl
text-red-400
"
class="block mt-1 text-xl font-semibold leading-8 lg:text-2xl text-red-400"
>
<BaseFormatMoney
:amount="dashboardStore.totalExpenses"
@ -116,8 +124,10 @@
class="
col-span-3
p-6
border-t border-gray-200 border-solid
border-t
border-gray-200 border-solid
lg:col-span-1
dark:border-white/10
"
>
<span class="text-xs leading-5 lg:text-sm">
@ -132,7 +142,7 @@
font-semibold
leading-8
lg:text-2xl
text-primary-500
text-primary-500 dark:text-primary-400
"
>
<BaseFormatMoney

View File

@ -1,6 +1,6 @@
<template>
<BaseContentPlaceholders
class="grid grid-cols-10 mt-8 bg-white rounded shadow"
class="grid grid-cols-10 mt-8 bg-white rounded shadow dark:bg-gray-800"
>
<!-- Chart -->
<div
@ -29,6 +29,7 @@
text-center
border-t border-l border-gray-200 border-solid
lg:border-t-0 lg:text-right lg:col-span-3
dark:border-gray-600
xl:col-span-2
lg:grid-cols-1
"
@ -77,6 +78,7 @@
col-span-3
p-6
border-t border-gray-200 border-solid
dark:border-gray-600
lg:justify-end lg:items-end lg:col-span-1
"
>

View File

@ -12,18 +12,24 @@
hover:bg-gray-50
xl:p-4
lg:col-span-2
dark:backdrop-blur-xl
dark:shadow-glass
dark:border
dark:border-white/10
dark:bg-gray-800/70
"
:class="{ 'lg:!col-span-3': large }"
:to="route"
>
<div>
<span class="text-xl font-semibold leading-tight text-black xl:text-3xl">
<span class="text-xl font-semibold leading-tight text-black xl:text-3xl dark:text-white">
<slot />
</span>
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg">
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg dark:text-gray-300">
{{ label }}
</span>
</div>
<BaseDarkHighlight class="!bg-highlight/[.17] !top-5" />
<div class="flex items-center">
<component :is="iconComponent" class="w-10 h-10 xl:w-12 xl:h-12" />
</div>

View File

@ -1,7 +1,7 @@
<template>
<BaseContentPlaceholders
:rounded="true"
class="relative flex justify-between w-full p-3 bg-white rounded shadow lg:col-span-3 xl:p-4"
class="relative flex justify-between w-full p-3 bg-white rounded shadow lg:col-span-3 xl:p-4 dark:bg-gray-800"
>
<div>
<BaseContentPlaceholdersText

View File

@ -12,6 +12,7 @@
shadow
lg:col-span-2
xl:p-4
dark:bg-gray-800
"
>
<div>

View File

@ -32,6 +32,8 @@
:content-loading="isLoading"
:calendar-button="true"
calendar-button-icon="calendar"
:show-extra-options="true"
:source-date="estimateStore.newEstimate.estimate_date"
/>
</BaseInputGroup>

View File

@ -34,6 +34,24 @@
/>
</BaseInputGroup>
<BaseInputGroup
:label="$tc('wizard.company_slug')"
:help-text="$t('wizard.company_slug_help_text')"
:error="
v$.companyForm.slug.$error &&
v$.companyForm.slug.$errors[0].$message
"
required
>
<BaseInput
v-model="companyForm.slug"
:invalid="v$.companyForm.slug.$error"
type="text"
name="slug"
@input="v$.companyForm.slug.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.country')"
:error="
@ -57,9 +75,7 @@
track-by="name"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup :label="$t('wizard.state')">
<BaseInput
v-model="companyForm.address.state"
@ -144,9 +160,9 @@
</template>
<script setup>
import { ref, computed, onMounted, reactive } from 'vue'
import { ref, computed, onMounted, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { required, maxLength, helpers } from '@vuelidate/validators'
import { required, minLength, maxLength, helpers } from '@vuelidate/validators'
import { useVuelidate } from '@vuelidate/core'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import { useCompanyStore } from '@/scripts/admin/stores/company'
@ -162,6 +178,7 @@ let logoFileName = ref(null)
const companyForm = reactive({
name: null,
slug: null,
address: {
address_street_1: '',
address_street_2: '',
@ -188,10 +205,28 @@ onMounted(async () => {
})?.id
})
const slugValidator = (value) => {
return value == slugify(value)
}
const rules = {
companyForm: {
name: {
required: helpers.withMessage(t('validation.required'), required),
minLength: helpers.withMessage(
t('validation.name_min_length', { count: 3 }),
minLength(3)
),
},
slug: {
required: helpers.withMessage(t('validation.required'), required),
minLength: helpers.withMessage(
t('validation.name_min_length', { count: 3 }),
minLength(3)
),
slugValidator: helpers.withMessage(
t('validation.invalid_slug'),
slugValidator
),
},
address: {
country_id: {
@ -249,4 +284,24 @@ async function next() {
emit('next', 7)
}
}
// watcher for if change company name then auto fill company slug value
watch(
() => companyForm.name,
(currentValue) => {
companyForm.slug = slugify(currentValue)
}
)
function slugify(string) {
return string
.toString()
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
}
</script>

View File

@ -56,7 +56,7 @@
<BaseMultiselect
v-model="filters.status"
:groups="true"
:options="status"
:options="invoiceStatus"
searchable
:placeholder="$t('general.select_a_status')"
@update:modelValue="setActiveTab"
@ -130,11 +130,27 @@
"
>
<!-- Tabs -->
<BaseTabGroup class="-mb-5" @change="setStatusFilter">
<BaseTab :title="$t('general.all')" filter="" />
<BaseTab :title="$t('general.draft')" filter="DRAFT" />
<BaseTab :title="$t('general.sent')" filter="SENT" />
<BaseTab :title="$t('general.due')" filter="DUE" />
<BaseTabGroup
class="-mb-5"
:selected-index="selectedIndex"
@change="changeTabStatus"
>
<BaseTab
:title="invoiceTabStatus[0].title"
:tab-status="invoiceTabStatus[0].value"
/>
<BaseTab
:title="invoiceTabStatus[1].title"
:tab-status="invoiceTabStatus[1].value"
/>
<BaseTab
:title="invoiceTabStatus[2].title"
:tab-status="invoiceTabStatus[2].value"
/>
<BaseTab
:title="invoiceTabStatus[3].title"
:tab-status="invoiceTabStatus[3].value"
/>
</BaseTabGroup>
<BaseDropdown
@ -289,10 +305,10 @@ const utils = inject('$utils')
const table = ref(null)
const showFilters = ref(false)
const status = ref([
const invoiceStatus = ref([
{
label: 'Status',
options: ['DRAFT', 'DUE', 'SENT', 'VIEWED', 'COMPLETED'],
options: ['DRAFT', 'SENT', 'VIEWED', 'COMPLETED'],
},
{
label: 'Paid Status',
@ -300,10 +316,29 @@ const status = ref([
},
,
])
const invoiceTabStatus = {
0: {
title: t('general.all'),
value: '',
},
1: {
title: t('general.draft'),
value: 'DRAFT',
},
2: {
title: t('general.sent'),
value: 'SENT',
},
3: {
title: t('general.due'),
value: 'DUE',
},
}
const isRequestOngoing = ref(true)
const activeTab = ref('general.draft')
const router = useRouter()
const userStore = useUserStore()
const selectedIndex = ref(0)
let filters = reactive({
customer_id: '',
@ -311,6 +346,7 @@ let filters = reactive({
from_date: '',
to_date: '',
invoice_number: '',
tab_status: '',
})
const showEmptyScreen = computed(
@ -401,6 +437,7 @@ async function fetchData({ page, filter, sort }) {
from_date: filters.from_date,
to_date: filters.to_date,
invoice_number: filters.invoice_number,
tab_status: filters.tab_status,
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page,
@ -423,29 +460,9 @@ async function fetchData({ page, filter, sort }) {
}
}
function setStatusFilter(val) {
if (activeTab.value == val.title) {
return true
}
activeTab.value = val.title
switch (val.title) {
case t('general.draft'):
filters.status = 'DRAFT'
break
case t('general.sent'):
filters.status = 'SENT'
break
case t('general.due'):
filters.status = 'DUE'
break
default:
filters.status = ''
break
}
function changeTabStatus(val, index) {
filters.tab_status = val['tab-status']
selectedIndex.value = index
}
function setFilters() {
@ -463,8 +480,6 @@ function clearFilter() {
filters.from_date = ''
filters.to_date = ''
filters.invoice_number = ''
activeTab.value = t('general.all')
}
async function removeMultipleInvoices() {
@ -505,39 +520,21 @@ function toggleFilter() {
function setActiveTab(val) {
switch (val) {
case 'DRAFT':
activeTab.value = t('general.draft')
selectedIndex.value = 1
break
case 'SENT':
activeTab.value = t('general.sent')
break
case 'DUE':
activeTab.value = t('general.due')
break
case 'VIEWED':
case 'COMPLETED':
activeTab.value = t('invoices.completed')
break
case 'PAID':
activeTab.value = t('invoices.paid')
selectedIndex.value = 2
break
case 'UNPAID':
activeTab.value = t('invoices.unpaid')
break
case 'PARTIALLY_PAID':
activeTab.value = t('invoices.partially_paid')
break
case 'VIEWED':
activeTab.value = t('invoices.viewed')
break
default:
activeTab.value = t('general.all')
selectedIndex.value = 3
break
}
filters.tab_status = invoiceTabStatus[selectedIndex.value].value
}
</script>

View File

@ -32,6 +32,8 @@
:content-loading="isLoading"
:calendar-button="true"
calendar-button-icon="calendar"
:show-extra-options="true"
:source-date="invoiceStore.newInvoice.invoice_date"
/>
</BaseInputGroup>

View File

@ -54,8 +54,6 @@
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

@ -82,9 +82,9 @@
required
>
<BaseCustomerSelectInput
v-if="!isLoadingContent"
v-model="paymentStore.currentPayment.customer_id"
:content-loading="isLoadingContent"
v-if="!isLoadingContent"
:invalid="v$.currentPayment.customer_id.$error"
:placeholder="$t('customers.select_a_customer')"
show-action
@ -423,7 +423,7 @@ function onCustomerChange(customer_id) {
if (customer_id) {
let data = {
customer_id: customer_id,
status: 'DUE',
tab_status: 'DUE',
limit: 'all',
}
@ -446,7 +446,11 @@ function onCustomerChange(customer_id) {
paymentStore.currentPayment.selectedCustomer = res2.data.data
paymentStore.currentPayment.customer = res2.data.data
paymentStore.currentPayment.currency = res2.data.data.currency
if(isEdit.value && !customerStore.editCustomer && paymentStore.currentPayment.customer_id) {
if (
isEdit.value &&
!customerStore.editCustomer &&
paymentStore.currentPayment.customer_id
) {
customerStore.editCustomer = res2.data.data
}
}

View File

@ -28,6 +28,19 @@
/>
</BaseInputGroup>
<BaseInputGroup
:label="$tc('settings.company_info.company_slug')"
:help-text="$t('settings.company_info.company_slug_help_text')"
:error="v$.slug.$error && v$.slug.$errors[0].$message"
required
>
<BaseInput
v-model="companyForm.slug"
:invalid="v$.slug.$error"
@blur="v$.slug.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup :label="$tc('settings.company_info.phone')">
<BaseInput v-model="companyForm.address.phone" />
</BaseInputGroup>
@ -160,6 +173,7 @@ let isSaving = ref(false)
const companyForm = reactive({
name: null,
slug: null,
logo: null,
address: {
address_street_1: '',
@ -193,7 +207,14 @@ const rules = computed(() => {
name: {
required: helpers.withMessage(t('validation.required'), required),
minLength: helpers.withMessage(
t('validation.name_min_length'),
t('validation.name_min_length', { count: 3 }),
minLength(3)
),
},
slug: {
required: helpers.withMessage(t('validation.required'), required),
minLength: helpers.withMessage(
t('validation.name_min_length', { count: 3 }),
minLength(3)
),
},

View File

@ -8,7 +8,11 @@
<BaseInputGroup
:content-loading="isFetchingInitialData"
:label="$tc('settings.preferences.currency')"
:help-text="$t('settings.preferences.company_currency_unchangeable')"
:help-text="
isCurrencyDisabled
? $t('settings.preferences.company_currency_unchangeable')
: ''
"
:error="v$.currency.$error && v$.currency.$errors[0].$message"
required
>
@ -21,7 +25,7 @@
:searchable="true"
track-by="name"
:invalid="v$.currency.$error"
disabled
:disabled="isCurrencyDisabled"
class="w-full"
>
</BaseMultiselect>
@ -187,6 +191,7 @@ const { t, tm } = useI18n()
let isSaving = ref(false)
let isDataSaving = ref(false)
let isFetchingInitialData = ref(false)
let isCurrencyDisabled = ref(true)
const settingsForm = reactive({ ...companyStore.selectedCompanySettings })
@ -282,10 +287,14 @@ setInitialData()
async function setInitialData() {
isFetchingInitialData.value = true
Promise.all([
companyStore.checkCompanyHasCurrencyTransactions(),
globalStore.fetchCurrencies(),
globalStore.fetchDateFormats(),
globalStore.fetchTimeZones(),
]).then(([res1]) => {
if (res1.data?.has_transactions == false) {
isCurrencyDisabled.value = false
}
isFetchingInitialData.value = false
})
}

View File

@ -0,0 +1,101 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<script lang="ts" setup>
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import { computed, ref } from 'vue'
defineProps({
showLabel: {
type: Boolean,
default: true,
},
vertical: {
type: Boolean,
default: false,
},
})
const globalStore = useGlobalStore()
const enabled = ref(
localStorage.getItem('theme') === 'dark' ||
document.documentElement.classList.contains('dark')
)
globalStore.isDarkModeOn = enabled
function onChange(val) {
if (val) {
localStorage.theme = 'dark'
document.documentElement.classList.add('dark')
document.documentElement.style.setProperty('color-scheme', 'dark')
globalStore.isDarkModeOn = true
} else {
localStorage.theme = 'light'
document.documentElement.classList.remove('dark')
document.documentElement.style.setProperty('color-scheme', 'light')
globalStore.isDarkModeOn = false
}
}
</script>
<template>
<div class="w-full flex justify-center">
<SwitchGroup
as="div"
class="flex items-center"
:class="vertical ? 'flex-col justify-center' : 'flex-row'"
>
<Switch
v-model="enabled"
class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:ring-offset-gray-700"
:class="[enabled ? 'bg-primary-600' : 'bg-gray-200']"
@update:modelValue="onChange"
>
<span class="sr-only">Use setting</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
:class="[enabled ? 'translate-x-5' : 'translate-x-0']"
>
<span
class="absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
:class="[
enabled
? 'opacity-0 ease-out duration-100'
: 'opacity-100 ease-in duration-200',
]"
aria-hidden="true"
>
<BaseIcon class="h-3 w-3 text-yellow-500" name="SunIcon" />
</span>
<span
class="absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
:class="[
enabled
? 'opacity-100 ease-in duration-200'
: 'opacity-0 ease-out duration-100',
]"
aria-hidden="true"
>
<BaseIcon class="h-3 w-3 text-primary-500" name="MoonIcon" />
</span>
</span>
</Switch>
<SwitchLabel
v-if="showLabel"
as="span"
class="cursor-pointer"
:class="vertical ? 'px-1 text-center mt-2' : 'ml-3'"
>
<span
v-if="enabled"
class="text-sm font-medium text-gray-500 dark:text-gray-400"
>
Dark Mode
</span>
<span v-else class="text-sm font-medium text-gray-500">
Light Mode
</span>
</SwitchLabel>
</SwitchGroup>
</div>
</template>

View File

@ -126,7 +126,7 @@ onMounted(() => {
})
const value = computed({
get: () => props.modelValue,
get: () => (props.modelValue ? props.modelValue : ''),
set: (value) => {
emit('update:modelValue', value)
},
@ -195,7 +195,9 @@ async function getFields() {
{ label: 'Date', value: 'INVOICE_DATE' },
{ label: 'Due Date', value: 'INVOICE_DUE_DATE' },
{ label: 'Number', value: 'INVOICE_NUMBER' },
{ label: 'Ref Number', value: 'INVOICE_REF_NUMBER' },
{ label: 'PDF Link', value: 'PDF_LINK' },
{ label: 'Due Amount', value: 'DUE_AMOUNT' },
{ label: 'Total Amount', value: 'TOTAL_AMOUNT' },
...invoiceFields.value.map((i) => ({
label: i.label,
value: i.slug,
@ -211,7 +213,8 @@ async function getFields() {
{ label: 'Date', value: 'ESTIMATE_DATE' },
{ label: 'Expiry Date', value: 'ESTIMATE_EXPIRY_DATE' },
{ label: 'Number', value: 'ESTIMATE_NUMBER' },
{ label: 'Ref Number', value: 'ESTIMATE_REF_NUMBER' },
{ label: 'PDF Link', value: 'PDF_LINK' },
{ label: 'Total Amount', value: 'TOTAL_AMOUNT' },
...estimateFields.value.map((i) => ({
label: i.label,
value: i.slug,
@ -228,6 +231,7 @@ async function getFields() {
{ label: 'Number', value: 'PAYMENT_NUMBER' },
{ label: 'Mode', value: 'PAYMENT_MODE' },
{ label: 'Amount', value: 'PAYMENT_AMOUNT' },
{ label: 'PDF Link', value: 'PDF_LINK' },
...paymentFields.value.map((i) => ({
label: i.label,
value: i.slug,

View File

@ -7,52 +7,108 @@
/>
</BaseContentPlaceholders>
<div v-else :class="computedContainerClass" class="relative flex flex-row">
<svg
v-if="showCalendarIcon && !hasIconSlot"
viewBox="0 0 20 20"
fill="currentColor"
class="
absolute
w-4
h-4
mx-2
my-2.5
text-sm
not-italic
font-black
text-gray-400
cursor-pointer
"
@click="onClickDp"
>
<path
fill-rule="evenodd"
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
clip-rule="evenodd"
></path>
</svg>
<slot v-if="showCalendarIcon && hasIconSlot" name="icon" />
<FlatPickr
ref="dp"
<div v-else :class="computedContainerClass">
<date-picker
ref="vCalendar"
v-model="date"
v-bind="$attrs"
:disabled="disabled"
:config="config"
:class="[defaultInputClass, inputInvalidClass, inputDisabledClass]"
/>
:mode="mode"
:is24hr="time24hr"
class="w-full"
color="indigo"
:input-debounce="500"
:update-on-input="false"
:is-range="false"
trim-weeks
:is-required="isRequired"
:popover="{
visibility: disabled ? 'hidden' : 'focus',
showDelay: 0,
hideDelay: 1,
}"
:attributes="attrs"
:model-config="config"
:masks="masks"
:locale="global.locale"
>
<template
#default="{ inputValue, inputEvents, togglePopover, hidePopover }"
>
<!-- calendar icon -->
<svg
v-if="showCalendarIcon && !hasIconSlot"
viewBox="0 0 20 20"
fill="currentColor"
class="
absolute
w-4
h-4
mx-2
my-2.5
text-sm
not-italic
font-black
text-gray-400
cursor-pointer
"
@click="togglePopover()"
>
<path
fill-rule="evenodd"
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
clip-rule="evenodd"
></path>
</svg>
<slot v-if="showCalendarIcon && hasIconSlot" name="icon" />
<input
:value="inputValue"
:class="[defaultInputClass, inputInvalidClass, inputDisabledClass]"
readonly
v-on="inputEvents"
@blur="hidePopover()"
/>
</template>
<template v-if="showExtraOptions" #footer>
<div
class="bg-gray-100 grid grid-cols-3 gap-2 p-2 border-t rounded-b-lg"
>
<button type="button" class="extra-button" @click="moveToDate(sourceDate)">
{{ global.t('date_picker.same_day') }}
</button>
<button type="button" class="extra-button" @click="withInDays(7)">
{{ global.t('date_picker.within_7_days') }}
</button>
<button type="button" class="extra-button" @click="withInDays(15)">
{{ global.t('date_picker.within_15_days') }}
</button>
<button type="button" class="extra-button" @click="withInDays(30)">
{{ global.t('date_picker.within_30_days') }}
</button>
<button type="button" class="extra-button" @click="withInDays(45)">
{{ global.t('date_picker.within_45_days') }}
</button>
<button type="button" class="extra-button" @click="withInDays(60)">
{{ global.t('date_picker.within_60_days') }}
</button>
</div>
</template>
</date-picker>
</div>
</template>
<script type="text/babel" setup>
import FlatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import { Calendar, DatePicker } from 'v-calendar'
import 'v-calendar/dist/style.css'
import { computed, reactive, watch, ref, useSlots } from 'vue'
import { useCompanyStore } from '@/scripts/admin/stores/company'
const dp = ref(null)
import moment from 'moment'
const props = defineProps({
modelValue: {
@ -90,36 +146,31 @@ const props = defineProps({
defaultInputClass: {
type: String,
default:
'font-base pl-8 py-2 outline-none focus:ring-primary-400 focus:outline-none focus:border-primary-400 block w-full sm:text-sm border-gray-200 rounded-md text-black',
'border-2 font-base pl-8 py-2 outline-none focus:ring-primary-400 focus:outline-none focus:border-primary-400 block w-full sm:text-sm border-gray-200 rounded-md text-black',
},
time24hr: {
type: Boolean,
default: false,
},
isRequired: {
type: Boolean,
default: false,
},
showExtraOptions: {
type: Boolean,
default: false,
},
sourceDate: {
type: [String, Date],
default: () => new Date(),
}
})
const emit = defineEmits(['update:modelValue'])
const slots = useSlots()
const companyStore = useCompanyStore()
let config = reactive({
altInput: true,
enableTime: props.enableTime,
time_24hr: props.time24hr,
})
const date = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:modelValue', value)
},
})
const carbonFormat = computed(() => {
return companyStore.selectedCompanySettings?.carbon_date_format
})
const { global } = window.i18n
const vCalendar = ref(null)
const hasIconSlot = computed(() => {
return !!slots.icon
@ -135,7 +186,6 @@ const inputInvalidClass = computed(() => {
if (props.invalid) {
return 'border-red-400 ring-red-400 focus:ring-red-400 focus:border-red-400'
}
return ''
})
@ -143,35 +193,97 @@ const inputDisabledClass = computed(() => {
if (props.disabled) {
return 'border border-solid rounded-md outline-none input-field box-border-2 base-date-picker-input placeholder-gray-400 bg-gray-200 text-gray-600 border-gray-200'
}
return ''
})
function onClickDp(params) {
dp.value.fp.open()
// to convert YYYY-MM-DD | YYYY-MM-DD HH:mm format
function convertYMDFormat(date) {
let format = props.enableTime ? 'YYYY-MM-DD HH:mm' : 'YYYY-MM-DD'
return date ? moment(date).format(format) : date
}
watch(
() => props.enableTime,
(val) => {
if (props.enableTime) {
config.enableTime = props.enableTime
}
const date = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:modelValue', value)
},
{ immediate: true }
)
})
const mode = computed(() => {
return props.enableTime ? 'dateTime' : 'date'
})
const config = reactive({
type: 'string',
mask: 'YYYY-MM-DD', // Uses 'iso' if missing
//timeAdjust: '00:00:00',
})
const masks = reactive({
input: null,
inputDateTime: null,
inputDateTime24hr: null,
})
const attrs = reactive([
{
dates: new Date(),
highlight: {
fillMode: 'outline',
},
/* popover: {
label: 'Today Date',
visibility: 'hover',
}, */
},
])
const carbonFormat = computed(() => {
return companyStore.selectedCompanySettings?.moment_date_format
})
watch(
() => carbonFormat,
() => {
if (!props.enableTime) {
config.altFormat = carbonFormat.value ? carbonFormat.value : 'd M Y'
masks.input = carbonFormat.value ? carbonFormat.value : 'DD MMM YYYY'
config.mask = 'YYYY-MM-DD'
} else {
config.altFormat = carbonFormat.value
? `${carbonFormat.value} H:i `
: 'd M Y H:i'
let timeFormat = 'HH:mm'
if (props.time24hr) {
masks.inputDateTime24hr = carbonFormat.value
? `${carbonFormat.value} ${timeFormat}`
: `DD MMM YYYY ${timeFormat}`
} else {
masks.inputDateTime = carbonFormat.value
? `${carbonFormat.value} ${timeFormat}`
: `DD MMM YYYY ${timeFormat}`
}
config.mask = `YYYY-MM-DD ${timeFormat}`
}
},
{ immediate: true }
)
async function moveToDate(_date) {
const calendar = vCalendar.value
_date = _date ? _date : convertYMDFormat(new Date())
date.value = _date
// await calendar.move(_date)
calendar.hidePopover()
}
async function withInDays(noOfDays) {
if (!noOfDays) return false
let newDate = moment(props.sourceDate).add(noOfDays, 'days').toDate()
newDate = convertYMDFormat(newDate)
moveToDate(newDate)
}
</script>
<style scoped>
.extra-button {
@apply bg-primary-500 text-white text-sm font-semibold px-2 py-1 rounded hover:bg-primary-700;
}
</style>

View File

@ -1,6 +1,10 @@
<template>
<div>
<TabGroup :default-index="defaultIndex" @change="onChange">
<TabGroup
:selected-index="selectedIndex"
:default-index="defaultIndex"
@change="onChange"
>
<TabList
:class="[
'flex border-b border-grey-light',
@ -54,6 +58,10 @@ const props = defineProps({
type: Number,
default: 0,
},
selectedIndex: {
type: Number,
default: 0,
},
filter: {
type: String,
default: null,
@ -67,6 +75,6 @@ const slots = useSlots()
const tabs = computed(() => slots.default().map((tab) => tab.props))
function onChange(d) {
emit('change', tabs.value[d])
emit('change', tabs.value[d], d)
}
</script>

View File

@ -3,23 +3,26 @@
width="50"
height="50"
viewBox="0 0 50 50"
:class="colorClass"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="25" cy="25" r="25" fill="#EAF1FB" />
<circle cx="25" cy="25" r="25" :class="bgColor" />
<path
d="M28.2656 23.0547C27.3021 24.0182 26.1302 24.5 24.75 24.5C23.3698 24.5 22.1849 24.0182 21.1953 23.0547C20.2318 22.0651 19.75 20.8802 19.75 19.5C19.75 18.1198 20.2318 16.9479 21.1953 15.9844C22.1849 14.9948 23.3698 14.5 24.75 14.5C26.1302 14.5 27.3021 14.9948 28.2656 15.9844C29.2552 16.9479 29.75 18.1198 29.75 19.5C29.75 20.8802 29.2552 22.0651 28.2656 23.0547ZM28.2656 25.75C29.6979 25.75 30.9219 26.2708 31.9375 27.3125C32.9792 28.3281 33.5 29.5521 33.5 30.9844V32.625C33.5 33.1458 33.3177 33.5885 32.9531 33.9531C32.5885 34.3177 32.1458 34.5 31.625 34.5H17.875C17.3542 34.5 16.9115 34.3177 16.5469 33.9531C16.1823 33.5885 16 33.1458 16 32.625V30.9844C16 29.5521 16.5078 28.3281 17.5234 27.3125C18.5651 26.2708 19.8021 25.75 21.2344 25.75H21.8984C22.8099 26.1667 23.7604 26.375 24.75 26.375C25.7396 26.375 26.6901 26.1667 27.6016 25.75H28.2656Z"
fill="currentColor"
:class="color"
/>
</svg>
</template>
<script setup>
const props = defineProps({
colorClass: {
color: {
type: String,
default: 'text-primary-500',
default: 'fill-primary-500 dark:fill-white',
},
bgColor: {
type: String,
default: 'fill-gray-100 dark:fill-primary-400',
},
})
</script>

View File

@ -6,10 +6,24 @@
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="25" cy="25" r="25" fill="#FDE4E5" />
<circle :class="bgColor" cx="25" cy="25" r="25" />
<path
:class="color"
d="M27.2031 23.6016C28.349 23.9401 29.2083 24.6562 29.7812 25.75C30.3802 26.8438 30.4714 27.9766 30.0547 29.1484C29.7422 30.0078 29.2083 30.6979 28.4531 31.2188C27.6979 31.7135 26.8516 31.974 25.9141 32V33.875C25.9141 34.0573 25.849 34.2005 25.7188 34.3047C25.6146 34.4349 25.4714 34.5 25.2891 34.5H24.0391C23.8568 34.5 23.7005 34.4349 23.5703 34.3047C23.4661 34.2005 23.4141 34.0573 23.4141 33.875V32C22.1641 32 21.0443 31.6094 20.0547 30.8281C19.8984 30.6979 19.8073 30.5417 19.7812 30.3594C19.7552 30.1771 19.8203 30.0208 19.9766 29.8906L21.3047 28.5625C21.5651 28.3281 21.8255 28.3021 22.0859 28.4844C22.4766 28.7448 22.9193 28.875 23.4141 28.875H25.9922C26.3307 28.875 26.6042 28.7708 26.8125 28.5625C27.0469 28.3281 27.1641 28.0417 27.1641 27.7031C27.1641 27.1302 26.8906 26.7656 26.3438 26.6094L22.3203 25.4375C21.4349 25.1771 20.6927 24.7083 20.0938 24.0312C19.4948 23.3542 19.1432 22.5729 19.0391 21.6875C18.9349 20.4115 19.2995 19.3177 20.1328 18.4062C20.9922 17.4688 22.0599 17 23.3359 17H23.4141V15.125C23.4141 14.9427 23.4661 14.7995 23.5703 14.6953C23.7005 14.5651 23.8568 14.5 24.0391 14.5H25.2891C25.4714 14.5 25.6146 14.5651 25.7188 14.6953C25.849 14.7995 25.9141 14.9427 25.9141 15.125V17C27.1641 17 28.2839 17.3906 29.2734 18.1719C29.4297 18.3021 29.5208 18.4583 29.5469 18.6406C29.5729 18.8229 29.5078 18.9792 29.3516 19.1094L28.0234 20.4375C27.763 20.6719 27.5026 20.6979 27.2422 20.5156C26.8516 20.2552 26.4089 20.125 25.9141 20.125H23.3359C22.9974 20.125 22.7109 20.2422 22.4766 20.4766C22.2682 20.6849 22.1641 20.9583 22.1641 21.2969C22.1641 21.5312 22.2422 21.7526 22.3984 21.9609C22.5547 22.1693 22.75 22.3125 22.9844 22.3906L27.2031 23.6016Z"
fill="#FB7178"
/>
</svg>
</template>
<script setup>
const props = defineProps({
color: {
type: String,
default: 'fill-red-400 dark:fill-white',
},
bgColor: {
type: String,
default: 'fill-red-100 dark:fill-red-400',
},
})
</script>

View File

@ -5,21 +5,24 @@
viewBox="0 0 50 50"
fill="none"
xmlns="http://www.w3.org/2000/svg"
:class="colorClass"
>
<circle cx="25" cy="25" r="25" fill="#EAF1FB" />
<circle cx="25" cy="25" r="25" :class="bgColor" />
<path
d="M26.75 19.8125C26.75 20.0729 26.8411 20.2943 27.0234 20.4766C27.2057 20.6589 27.4271 20.75 27.6875 20.75H33V33.5625C33 33.8229 32.9089 34.0443 32.7266 34.2266C32.5443 34.4089 32.3229 34.5 32.0625 34.5H18.9375C18.6771 34.5 18.4557 34.4089 18.2734 34.2266C18.0911 34.0443 18 33.8229 18 33.5625V15.4375C18 15.1771 18.0911 14.9557 18.2734 14.7734C18.4557 14.5911 18.6771 14.5 18.9375 14.5H26.75V19.8125ZM33 19.2656V19.5H28V14.5H28.2344C28.4948 14.5 28.7161 14.5911 28.8984 14.7734L32.7266 18.6016C32.9089 18.7839 33 19.0052 33 19.2656Z"
fill="currentColor"
:class="color"
/>
</svg>
</template>
<script setup>
const props = defineProps({
colorClass: {
color: {
type: String,
default: 'text-primary-500',
default: 'fill-primary-500 dark:fill-white',
},
bgColor: {
type: String,
default: 'fill-gray-100 dark:fill-primary-400',
},
})
</script>

View File

@ -5,21 +5,24 @@
viewBox="0 0 50 50"
fill="none"
xmlns="http://www.w3.org/2000/svg"
:class="colorClass"
>
<circle cx="25" cy="25" r="25" fill="#EAF1FB" />
<circle cx="25" cy="25" r="25" :class="bgColor" />
<path
d="M28.25 24.5V27H20.75V24.5H28.25ZM31.7266 18.6016C31.9089 18.7839 32 19.0052 32 19.2656V19.5H27V14.5H27.2344C27.4948 14.5 27.7161 14.5911 27.8984 14.7734L31.7266 18.6016ZM25.75 19.8125C25.75 20.0729 25.8411 20.2943 26.0234 20.4766C26.2057 20.6589 26.4271 20.75 26.6875 20.75H32V33.5625C32 33.8229 31.9089 34.0443 31.7266 34.2266C31.5443 34.4089 31.3229 34.5 31.0625 34.5H17.9375C17.6771 34.5 17.4557 34.4089 17.2734 34.2266C17.0911 34.0443 17 33.8229 17 33.5625V15.4375C17 15.1771 17.0911 14.9557 17.2734 14.7734C17.4557 14.5911 17.6771 14.5 17.9375 14.5H25.75V19.8125ZM19.5 17.3125V17.9375C19.5 18.1458 19.6042 18.25 19.8125 18.25H22.9375C23.1458 18.25 23.25 18.1458 23.25 17.9375V17.3125C23.25 17.1042 23.1458 17 22.9375 17H19.8125C19.6042 17 19.5 17.1042 19.5 17.3125ZM19.5 19.8125V20.4375C19.5 20.6458 19.6042 20.75 19.8125 20.75H22.9375C23.1458 20.75 23.25 20.6458 23.25 20.4375V19.8125C23.25 19.6042 23.1458 19.5 22.9375 19.5H19.8125C19.6042 19.5 19.5 19.6042 19.5 19.8125ZM29.5 31.6875V31.0625C29.5 30.8542 29.3958 30.75 29.1875 30.75H26.0625C25.8542 30.75 25.75 30.8542 25.75 31.0625V31.6875C25.75 31.8958 25.8542 32 26.0625 32H29.1875C29.3958 32 29.5 31.8958 29.5 31.6875ZM29.5 23.875C29.5 23.6927 29.4349 23.5495 29.3047 23.4453C29.2005 23.3151 29.0573 23.25 28.875 23.25H20.125C19.9427 23.25 19.7865 23.3151 19.6562 23.4453C19.5521 23.5495 19.5 23.6927 19.5 23.875V27.625C19.5 27.8073 19.5521 27.9635 19.6562 28.0938C19.7865 28.1979 19.9427 28.25 20.125 28.25H28.875C29.0573 28.25 29.2005 28.1979 29.3047 28.0938C29.4349 27.9635 29.5 27.8073 29.5 27.625V23.875Z"
fill="currentColor"
:class="color"
/>
</svg>
</template>
<script setup>
const props = defineProps({
colorClass: {
color: {
type: String,
default: 'text-primary-500',
default: 'fill-primary-500 dark:fill-white',
},
bgColor: {
type: String,
default: 'fill-gray-100 dark:fill-primary-400',
},
})
</script>

View File

@ -7,12 +7,12 @@ export function usePopper(options) {
let popper = ref(null)
onMounted(() => {
watchEffect(onInvalidate => {
watchEffect((onInvalidate) => {
if (!container.value) return
if (!activator.value) return
let containerEl = container.value.el || container.value
let activatorEl = activator.value.el || activator.value
let activatorEl = activator.value.$el || activator.value
if (!(activatorEl instanceof HTMLElement)) return
if (!(containerEl instanceof HTMLElement)) return

View File

@ -863,6 +863,8 @@
"company_info": {
"company_info": "Company info",
"company_name": "Company Name",
"company_slug": "Company Slug",
"company_slug_help_text": "A unique URL friendly name for your company (It will appear on Customer Portal URL)",
"company_logo": "Company Logo",
"section_description": "Information about your company that will be displayed on invoices, estimates and other documents created by Crater.",
"phone": "Phone",
@ -1324,6 +1326,8 @@
"company_info": "Company Information",
"company_info_desc": "This information will be displayed on invoices. Note that you can edit this later on settings page.",
"company_name": "Company Name",
"company_slug": "Company Slug",
"company_slug_help_text": "A unique URL friendly name for your company (It will appear on Customer Portal URL)",
"company_logo": "Company Logo",
"logo_preview": "Logo Preview",
"preferences": "Company Preferences",
@ -1454,7 +1458,8 @@
"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": "Company name must match with given name."
"company_name_not_same": "Company name must match with given name.",
"invalid_slug": "Invalid Slug"
},
"errors": {
"starter_plan": "This feature is available on Starter plan and onwards!",
@ -1485,7 +1490,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",
@ -1522,5 +1527,13 @@
"pdf_bill_to": "Bill to,",
"pdf_ship_to": "Ship to,",
"pdf_received_from": "Received from:",
"pdf_tax_label": "Tax"
"pdf_tax_label": "Tax",
"date_picker": {
"same_day": "Same Day",
"within_7_days": "Within 7 Days",
"within_15_days": "Within 15 Days",
"within_30_days": "Within 30 Days",
"within_45_days": "Within 45 Days",
"within_60_days": "Within 60 Days"
}
}

View File

@ -17,6 +17,7 @@ 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,
@ -37,5 +38,6 @@ export default {
vi,
pl,
el,
hr
hr,
th
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -17,7 +17,7 @@
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Module Styles -->
@foreach(\Crater\Services\Module\ModuleFacade::allStyles() as $name => $path)
@foreach (\Crater\Services\Module\ModuleFacade::allStyles() as $name => $path)
<link rel="stylesheet" href="/modules/styles/{{ $name }}">
@endforeach
@ -25,8 +25,8 @@
</head>
<body
class="h-full overflow-hidden bg-gray-100 font-base
@if(isset($current_theme)) theme-{{ $current_theme }} @else theme-{{get_app_setting('admin_portal_theme') ?? 'crater'}} @endif ">
class="h-full overflow-hidden bg-gray-100 dark:bg-gray-900 dark:text-white font-base
@if (isset($current_theme)) theme-{{ $current_theme }} @else theme-{{ get_app_setting('admin_portal_theme') ?? 'crater' }} @endif ">
<!-- Module Scripts -->
@foreach (\Crater\Services\Module\ModuleFacade::allScripts() as $name => $path)
@ -38,6 +38,14 @@
@endforeach
<script type="module">
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
document.documentElement.style.setProperty('color-scheme', 'dark');
} else {
document.documentElement.classList.remove('dark')
document.documentElement.style.setProperty('color-scheme', 'light')
}
@if(isset($customer_logo))
window.customer_logo = "/storage/{{$customer_logo}}"
@ -57,12 +65,12 @@
window.login_page_description = "{{$login_page_description}}"
@endif
@endif
@if(isset($copyright_text))
window.copyright_text = "{{$copyright_text}}"
@endif
@endif
window.Crater.start()
</script>

View File

@ -386,6 +386,10 @@
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>

View File

@ -408,6 +408,10 @@
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>

View File

@ -346,6 +346,10 @@
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>

View File

@ -327,6 +327,10 @@
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>

View File

@ -377,6 +377,10 @@
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>

View File

@ -2,7 +2,7 @@
<html>
<head>
<title>@lang('pdf_invoice_label') - {{$invoice->invoice_number}}</title>
<title>@lang('pdf_invoice_label') - {{ $invoice->invoice_number }}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
@ -187,7 +187,7 @@
.total-display-table {
border-top: none;
border-top: none;
page-break-inside: avoid;
page-break-before: auto;
page-break-after: auto;
@ -304,7 +304,12 @@
.pl-0 {
padding-left: 0;
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>
@ -312,10 +317,10 @@
<table width="100%">
<tr>
<td width="50%" class="header-section-left">
@if($logo)
<img class="header-logo" style="height: 50px;" src="{{ $logo }}" alt="Company Logo">
@if ($logo)
<img class="header-logo" style="height: 50px;" src="{{ $logo }}" alt="Company Logo">
@else
<h1 class="header-logo"> {{$invoice->customer->company->name}} </h1>
<h1 class="header-logo"> {{ $invoice->customer->company->name }} </h1>
@endif
</td>
<td width="50%" class="text-right company-address-container company-address">
@ -331,14 +336,14 @@
<div class="main-content">
<div class="customer-address-container">
<div class="billing-address-container billing-address">
@if($billing_address)
@if ($billing_address)
<b>@lang('pdf_bill_to')</b> <br>
{!! $billing_address !!}
@endif
</div>
<div @if($billing_address !== '</br>') class="shipping-address-container shipping-address" @else class="shipping-address-container--left shipping-address" @endif>
@if($shipping_address)
<div @if ($billing_address !== '</br>') class="shipping-address-container shipping-address" @else class="shipping-address-container--left shipping-address" @endif>
@if ($shipping_address)
<b>@lang('pdf_ship_to')</b> <br>
{!! $shipping_address !!}
@endif
@ -350,15 +355,15 @@
<table>
<tr>
<td class="attribute-label">@lang('pdf_invoice_number')</td>
<td class="attribute-value"> &nbsp;{{$invoice->invoice_number}}</td>
<td class="attribute-value"> &nbsp;{{ $invoice->invoice_number }}</td>
</tr>
<tr>
<td class="attribute-label">@lang('pdf_invoice_date')</td>
<td class="attribute-value"> &nbsp;{{$invoice->formattedInvoiceDate}}</td>
<td class="attribute-value"> &nbsp;{{ $invoice->formattedInvoiceDate }}</td>
</tr>
<tr>
<td class="attribute-label">@lang('pdf_invoice_due_date')</td>
<td class="attribute-value"> &nbsp;{{$invoice->formattedDueDate}}</td>
<td class="attribute-value"> &nbsp;{{ $invoice->formattedDueDate }}</td>
</tr>
</table>
</div>
@ -368,7 +373,7 @@
@include('app.pdf.invoice.partials.table')
<div class="notes">
@if($notes)
@if ($notes)
<div class="notes-label">
@lang('pdf_notes')
</div>

View File

@ -0,0 +1,34 @@
<style type="text/css">
@font-face {
font-family: 'THSarabunNew';
font-style: normal;
font-weight: normal;
src: url("{{ resource_path('static/fonts/THSarabunNew.ttf') }}") format('truetype');
}
@font-face {
font-family: 'THSarabunNew';
font-style: normal;
font-weight: bold;
src: url("{{ resource_path('static/fonts/THSarabunNew-Bold.ttf') }}") format('truetype');
}
@font-face {
font-family: 'THSarabunNew';
font-style: italic;
font-weight: normal;
src: url("{{ resource_path('static/fonts/THSarabunNew-Italic.ttf') }}") format('truetype');
}
@font-face {
font-family: 'THSarabunNew';
font-style: italic;
font-weight: bold;
src: url("{{ resource_path('static/fonts/THSarabunNew-BoldItalic.ttf') }}") format('truetype');
}
body {
font-family: "THSarabunNew", sans-serif !important;
}
</style>

View File

@ -276,6 +276,10 @@
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>@lang('pdf_expense_report_label')</title>
<style type="text/css">
@ -11,7 +12,7 @@
border-collapse: collapse;
}
.sub-container{
.sub-container {
padding: 0px 20px;
}
@ -133,7 +134,12 @@
color: #5851D8;
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>
<div class="sub-container">
<table class="report-header">
@ -155,18 +161,18 @@
<div class="expenses-table-container">
<table class="expenses-table">
@foreach ($expenseCategories as $expenseCategory)
<tr>
<td>
<p class="expense-title">
{{ $expenseCategory->category->name }}
</p>
</td>
<td>
<p class="expense-amount">
{!! format_money_pdf($expenseCategory->total_amount) !!}
</p>
</td>
</tr>
<tr>
<td>
<p class="expense-title">
{{ $expenseCategory->category->name }}
</p>
</td>
<td>
<p class="expense-amount">
{!! format_money_pdf($expenseCategory->total_amount, $currency) !!}
</p>
</td>
</tr>
@endforeach
</table>
</div>
@ -175,7 +181,7 @@
<table class="expense-total-table">
<tr>
<td class="expense-total-cell">
<p class="expense-total">{!! format_money_pdf($totalExpense) !!}</p>
<p class="expense-total">{!! format_money_pdf($totalExpense, $currency) !!}</p>
</td>
</tr>
</table>
@ -185,9 +191,10 @@
<p class="report-footer-label">@lang('pdf_total_expenses_label')</p>
</td>
<td>
<p class="report-footer-value">{!! format_money_pdf($totalExpense) !!}</p>
<p class="report-footer-value">{!! format_money_pdf($totalExpense, $currency) !!}</p>
</td>
</tr>
</table>
</body>
</html>
</html>

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>@lang('pdf_profit_loss_label')</title>
<style type="text/css">
@ -11,7 +12,7 @@
border-collapse: collapse;
}
.sub-container{
.sub-container {
padding: 0px 20px;
}
@ -158,7 +159,12 @@
color: #5851D8;
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>
<div class="sub-container">
<table class="report-header">
@ -183,7 +189,7 @@
<p class="income-title">@lang("pdf_income_label")</p>
</td>
<td>
<p class="income-amount">{!! format_money_pdf($income) !!}</p>
<p class="income-amount">{!! format_money_pdf($income, $currency) !!}</p>
</td>
</tr>
</table>
@ -191,18 +197,18 @@
<div class="expenses-table-container">
<table class="expenses-table">
@foreach ($expenseCategories as $expenseCategory)
<tr>
<td>
<p class="expense-title">
{{ $expenseCategory->category->name }}
</p>
</td>
<td>
<p class="expense-amount">
{!! format_money_pdf($expenseCategory->total_amount) !!}
</p>
</td>
</tr>
<tr>
<td>
<p class="expense-title">
{{ $expenseCategory->category->name }}
</p>
</td>
<td>
<p class="expense-amount">
{!! format_money_pdf($expenseCategory->total_amount, $currency) !!}
</p>
</td>
</tr>
@endforeach
</table>
@ -212,7 +218,7 @@
<table class="expense-total-indicator-table">
<tr>
<td class="expense-total-cell">
<p class="expense-total">{!! format_money_pdf($totalExpense) !!}</p>
<p class="expense-total">{!! format_money_pdf($totalExpense, $currency) !!}</p>
</td>
</tr>
</table>
@ -222,9 +228,10 @@
<p class="report-footer-label">@lang("pdf_net_profit_label")</p>
</td>
<td>
<p class="report-footer-value">{!! format_money_pdf(($income-$totalExpense)) !!}</p>
<p class="report-footer-value">{!! format_money_pdf($income - $totalExpense, $currency) !!}</p>
</td>
</tr>
</table>
</body>
</html>
</html>

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>@lang('pdf_sales_customers_label')</title>
<style type="text/css">
@ -11,7 +12,7 @@
border-collapse: collapse;
}
.sub-container{
.sub-container {
padding: 0px 20px;
}
@ -132,11 +133,17 @@
line-height: 21px;
color: #5851D8;
}
.text-center {
text-align: center;
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>
<div class="sub-container">
<table class="report-header">
@ -156,34 +163,34 @@
</table>
@foreach ($customers as $customer)
<p class="sales-customer-name">{{ $customer->name }}</p>
<div class="sales-table-container">
<table class="sales-table">
@foreach ($customer->invoices as $invoice)
<tr>
<td>
<p class="sales-information-text">
{{ $invoice->formattedInvoiceDate }} ({{ $invoice->invoice_number }})
</p>
</td>
<td>
<p class="sales-amount">
{!! format_money_pdf($invoice->base_total) !!}
</p>
</td>
</tr>
@endforeach
</table>
</div>
<table class="sales-total-indicator-table">
<p class="sales-customer-name">{{ $customer->name }}</p>
<div class="sales-table-container">
<table class="sales-table">
@foreach ($customer->invoices as $invoice)
<tr>
<td class="sales-total-cell">
<p class="sales-total-amount">
{!! format_money_pdf($customer->totalAmount) !!}
<td>
<p class="sales-information-text">
{{ $invoice->formattedInvoiceDate }} ({{ $invoice->invoice_number }})
</p>
</td>
<td>
<p class="sales-amount">
{!! format_money_pdf($invoice->base_total, $currency) !!}
</p>
</td>
</tr>
@endforeach
</table>
</div>
<table class="sales-total-indicator-table">
<tr>
<td class="sales-total-cell">
<p class="sales-total-amount">
{!! format_money_pdf($customer->totalAmount, $currency) !!}
</p>
</td>
</tr>
</table>
@endforeach
</div>
@ -195,10 +202,11 @@
</td>
<td>
<p class="report-footer-value">
{!! format_money_pdf($totalAmount) !!}
{!! format_money_pdf($totalAmount, $currency) !!}
</p>
</td>
</tr>
</table>
</body>
</html>
</html>

View File

@ -1,17 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>@lang('pdf_sales_items_label')</title>
<style type="text/css">
body {
font-family: "DejaVu Sans";
}
table {
border-collapse: collapse;
}
.sub-container{
.sub-container {
padding: 0px 20px;
}
@ -132,11 +133,17 @@
line-height: 21px;
color: #5851D8;
}
.text-center {
text-align: center;
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>
<div class="sub-container">
<table class="report-header">
@ -154,36 +161,36 @@
</td>
</tr>
</table>
<p class="sales-items-title">@lang('pdf_items_label')</p>
@foreach ($items as $item)
<div class="items-table-container">
<table class="items-table">
<tr>
<td>
<p class="item-title">
{{ $item->name }}
</p>
</td>
<td>
<p class="item-sales-amount">
{!! format_money_pdf($item->total_amount) !!}
</p>
</td>
</tr>
</table>
</div>
@endforeach
<table class="sales-total-indicator-table">
<div class="items-table-container">
<table class="items-table">
<tr>
<td class="sales-total-cell">
<p class="sales-total-amount">
{!! format_money_pdf($totalAmount) !!}
<td>
<p class="item-title">
{{ $item->name }}
</p>
</td>
<td>
<p class="item-sales-amount">
{!! format_money_pdf($item->total_amount, $currency) !!}
</p>
</td>
</tr>
</table>
</div>
@endforeach
<table class="sales-total-indicator-table">
<tr>
<td class="sales-total-cell">
<p class="sales-total-amount">
{!! format_money_pdf($totalAmount, $currency) !!}
</p>
</td>
</tr>
</table>
</div>
@ -194,10 +201,11 @@
</td>
<td>
<p class="report-footer-value">
{!! format_money_pdf($totalAmount) !!}
{!! format_money_pdf($totalAmount, $currency) !!}
</p>
</td>
</tr>
</table>
</body>
</html>
</html>

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>@lang('pdf_tax_summery_label')</title>
<style type="text/css">
@ -11,7 +12,7 @@
border-collapse: collapse;
}
.sub-container{
.sub-container {
padding: 0px 20px;
}
@ -134,7 +135,12 @@
color: #5851D8;
}
</style>
@if (App::isLocale('th'))
@include('app.pdf.locale.th')
@endif
</head>
<body>
<div class="sub-container">
<table class="report-header">
@ -160,18 +166,18 @@
<div class="tax-table-container">
<table class="tax-table">
@foreach ($taxTypes as $tax)
<tr>
<td>
<p class="tax-title">
{{ $tax->taxType->name }}
</p>
</td>
<td>
<p class="tax-amount">
{!! format_money_pdf($tax->total_tax_amount) !!}
</p>
</td>
</tr>
<tr>
<td>
<p class="tax-title">
{{ $tax->taxType->name }}
</p>
</td>
<td>
<p class="tax-amount">
{!! format_money_pdf($tax->total_tax_amount, $currency) !!}
</p>
</td>
</tr>
@endforeach
</table>
@ -182,7 +188,7 @@
<tr>
<td class="tax-total-cell">
<p class="tax-total">
{!! format_money_pdf($totalTaxAmount) !!}
{!! format_money_pdf($totalTaxAmount, $currency) !!}
</p>
</td>
</tr>
@ -194,10 +200,11 @@
</td>
<td>
<p class="report-footer-value">
{!! format_money_pdf($totalTaxAmount) !!}
{!! format_money_pdf($totalTaxAmount, $currency) !!}
</p>
</td>
</tr>
</table>
</body>
</html>
</html>

0
storage/fonts/.gitkeep Normal file
View File

View File

@ -19,6 +19,7 @@ module.exports = {
'./resources/scripts/**/*.js',
'./resources/scripts/**/*.vue',
],
darkMode: 'class',
theme: {
extend: {
colors: {

View File

@ -415,32 +415,31 @@ test('update estimate with EUR currency', function () {
$response = putJson('api/v1/estimates/'.$estimate->id, $estimate2);
$this->assertDatabaseHas('estimates', [
'id' => $estimate['id'],
'template_name' => $estimate2['template_name'],
'estimate_number' => $estimate2['estimate_number'],
'discount_type' => $estimate2['discount_type'],
'discount_val' => $estimate2['discount_val'],
'sub_total' => $estimate2['sub_total'],
'discount' => $estimate2['discount'],
'customer_id' => $estimate2['customer_id'],
'total' => $estimate2['total'],
'tax' => $estimate2['tax'],
'exchange_rate' => $estimate2['exchange_rate'],
'base_discount_val' => $estimate2['base_discount_val'],
'base_sub_total' => $estimate2['base_sub_total'],
'base_total' => $estimate2['base_total'],
'base_tax' => $estimate2['base_tax'],
]);
$estimate_assert = collect($estimate2)
->only([
'id',
'template_name',
'estimate_number',
'discount_type',
'discount_val',
'sub_total',
'discount',
'customer_id',
'total',
'tax'
])
->toArray();
$this->assertDatabaseHas('estimate_items', [
'estimate_id' => $estimate2['items'][0]['estimate_id'],
'exchange_rate' => $estimate2['items'][0]['exchange_rate'],
'base_price' => $estimate2['items'][0]['base_price'],
'base_discount_val' => $estimate2['items'][0]['base_discount_val'],
'base_tax' => $estimate2['items'][0]['base_tax'],
'base_total' => $estimate2['items'][0]['base_total'],
]);
$this->assertDatabaseHas('estimates', $estimate_assert);
$estimate_item_assert = collect($estimate2['items'][0])
->only([
'estimate_id',
'amount'
])
->toArray();
$this->assertDatabaseHas('estimate_items', $estimate_item_assert);
$response->assertStatus(200);
});

View File

@ -37,13 +37,15 @@ test('create expense', function () {
postJson('api/v1/expenses', $expense)->assertStatus(201);
$this->assertDatabaseHas('expenses', [
'notes' => $expense['notes'],
'expense_category_id' => $expense['expense_category_id'],
'amount' => $expense['amount'],
'exchange_rate' => $expense['exchange_rate'],
'base_amount' => $expense['base_amount'],
]);
$expense = collect($expense)
->only([
'notes',
'expense_category_id',
'amount'
])
->toArray();
$this->assertDatabaseHas('expenses', $expense);
});
test('store validates using a form request', function () {
@ -146,11 +148,13 @@ test('update expense with EUR currency', function () {
putJson('api/v1/expenses/'.$expense->id, $expense2)->assertOk();
$this->assertDatabaseHas('expenses', [
'id' => $expense->id,
'expense_category_id' => $expense2['expense_category_id'],
'amount' => $expense2['amount'],
'exchange_rate' => $expense2['exchange_rate'],
'base_amount' => $expense2['base_amount'],
]);
$expense2 = collect($expense2)
->only([
'id',
'expense_category_id',
'amount'
])
->toArray();
$this->assertDatabaseHas('expenses', $expense2);
});

View File

@ -9,7 +9,6 @@ use Crater\Models\Tax;
use Crater\Models\User;
use Illuminate\Support\Facades\Artisan;
use Laravel\Sanctum\Sanctum;
use function Pest\Laravel\getJson;
use function Pest\Laravel\postJson;
use function Pest\Laravel\putJson;
@ -431,31 +430,36 @@ test('update invoice with EUR currency', function () {
putJson('api/v1/invoices/'.$invoice->id, $invoice2)->assertOk();
$this->assertDatabaseHas('invoices', [
'id' => $invoice['id'],
'invoice_number' => $invoice2['invoice_number'],
'sub_total' => $invoice2['sub_total'],
'total' => $invoice2['total'],
'tax' => $invoice2['tax'],
'discount' => $invoice2['discount'],
'customer_id' => $invoice2['customer_id'],
'template_name' => $invoice2['template_name'],
'exchange_rate' => $invoice2['exchange_rate'],
'base_total' => $invoice2['base_total'],
]);
$invoice_assert = collect($invoice2)
->only([
'invoice_number',
'template_name',
'sub_total',
'total',
'tax',
'discount',
'customer_id',
])
->toArray();
$this->assertDatabaseHas('invoice_items', [
'invoice_id' => $invoice2['items'][0]['invoice_id'],
'item_id' => $invoice2['items'][0]['item_id'],
'name' => $invoice2['items'][0]['name'],
'exchange_rate' => $invoice2['items'][0]['exchange_rate'],
'base_price' => $invoice2['items'][0]['base_price'],
'base_total' => $invoice2['items'][0]['base_total'],
]);
$this->assertDatabaseHas('invoices', $invoice_assert);
$this->assertDatabaseHas('taxes', [
'amount' => $invoice2['taxes'][0]['amount'],
'name' => $invoice2['taxes'][0]['name'],
'base_amount' => $invoice2['taxes'][0]['base_amount'],
]);
$invoice_item_assert = collect($invoice2['items'][0])
->only([
'invoice_id',
'item_id',
'name',
])
->toArray();
$this->assertDatabaseHas('invoice_items', $invoice_item_assert);
$invoice_tax_assert = collect($invoice2['taxes'][0])
->only([
'name',
'amount'
])
->toArray();
$this->assertDatabaseHas('taxes', $invoice_tax_assert);
});

40
uffizzi/.env.example Normal file
View File

@ -0,0 +1,40 @@
APP_ENV=production
APP_KEY=base64:kgk/4DW1vEVy7aEvet5FPp5un6PIGe/so8H0mvoUtW0=
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://crater.test
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=crater
DB_USERNAME=crater
DB_PASSWORD=crater
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_DRIVER=sync
SESSION_DRIVER=cookie
SESSION_LIFETIME=1440
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_HOST=
MAIL_PORT=
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=
PUSHER_APP_ID=
PUSHER_KEY=
PUSHER_SECRET=
SANCTUM_STATEFUL_DOMAINS=crater.test
SESSION_DOMAIN=crater.test
TRUSTED_PROXIES="*"
CRON_JOB_AUTH_TOKEN=""

48
uffizzi/Dockerfile Normal file
View File

@ -0,0 +1,48 @@
FROM php:8.1-fpm
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip \
libzip-dev \
libmagickwand-dev \
mariadb-client
# Clear cache
RUN 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
# 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 1000 -d /home/crater-user crater-user
RUN mkdir -p /home/crater-user/.composer && \
chown -R crater-user:crater-user /home/crater-user
# Mounted volumes
COPY ./ /var/www
COPY ./docker-compose/php/uploads.ini /usr/local/etc/php/conf.d/uploads.ini
COPY ./uffizzi/.env.example /var/www/.env
# Set working directory
WORKDIR /var/www
RUN chown -R crater-user:crater-user ./
RUN chmod -R 775 composer.json composer.lock \
composer.lock storage/framework/ \
storage/logs/ bootstrap/cache/ /home/crater-user/.composer
RUN chown -R $(whoami):$(whoami) /var/log/
RUN chmod -R 775 /var/log
USER crater-user

68
uffizzi/crond/Dockerfile Normal file
View File

@ -0,0 +1,68 @@
FROM php:8.1-fpm as build
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip \
libzip-dev \
libmagickwand-dev \
mariadb-client
# Clear cache
RUN 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
# 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 1000 -d /home/crater-user crater-user
RUN mkdir -p /home/crater-user/.composer && \
chown -R crater-user:crater-user /home/crater-user
# Mounted volumes
COPY ./ /var/www
COPY ./docker-compose/php/uploads.ini /usr/local/etc/php/conf.d/uploads.ini
COPY ./uffizzi/.env.example /var/www/.env
# Set working directory
WORKDIR /var/www
RUN chown -R crater-user:crater-user ./
RUN chmod -R 775 composer.json composer.lock \
composer.lock storage/framework/ \
storage/logs/ bootstrap/cache/ /home/crater-user/.composer
RUN composer config --no-plugins allow-plugins.pestphp/pest-plugin true && \
composer install --no-interaction --prefer-dist --optimize-autoloader && \
php artisan storage:link || true && \
php artisan key:generate
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
# Mounted volumes
COPY --from=build /var/www /var/www
RUN chown -R $(whoami):$(whoami) /var/www/
RUN chmod -R 775 /var/www/
RUN chown -R $(whoami):$(whoami) /var/log/
RUN chmod -R 775 /var/log/
CMD ["crond", "-f"]

View File

@ -0,0 +1,58 @@
version: '3'
x-uffizzi:
ingress:
service: nginx
port: 80
services:
app:
image: "${APP_IMAGE}"
restart: unless-stopped
working_dir: /var/www/
command: ["-c","
composer config --no-plugins allow-plugins.pestphp/pest-plugin true &&
composer install --no-interaction --prefer-dist --optimize-autoloader &&
php artisan storage:link || true &&
php artisan key:generate --force &&
php-fpm",
]
entrypoint: /bin/sh
depends_on:
- db
deploy:
resources:
limits:
memory: 1000m
db:
image: mariadb
restart: always
environment:
MYSQL_USER: crater
MYSQL_PASSWORD: crater
MYSQL_DATABASE: crater
MYSQL_ROOT_PASSWORD: crater
ports:
- '33006:3306'
deploy:
resources:
limits:
memory: 500m
nginx:
image: "${NGINX_IMAGE}"
restart: unless-stopped
ports:
- 80:80
depends_on:
- app
resources:
limits:
memory: 500m
cron:
image: "${CROND_IMAGE}"
restart: always

7
uffizzi/nginx/Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM nginx:1.17-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY ./ /var/www
COPY ./uffizzi/nginx/nginx /etc/nginx/conf.d/

View File

@ -0,0 +1,22 @@
server {
client_max_body_size 64M;
listen 80;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/public;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass localhost:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 300;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}
}

1979
yarn.lock

File diff suppressed because it is too large Load Diff