mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-27 11:41:09 -04:00
Compare commits
26 Commits
master
...
test-uffiz
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a8e30d748 | |||
| e0e9aff6d1 | |||
| 98196194e2 | |||
| abc66dd768 | |||
| df04fd9e53 | |||
| 189c51cdf4 | |||
| bd5f0fe5cf | |||
| 787619b907 | |||
| fd70ab9a99 | |||
| 33315638df | |||
| bba14bf51a | |||
| ea41989034 | |||
| 3f3f83a00a | |||
| adc5962071 | |||
| c4c00002d7 | |||
| a7c1e12db6 | |||
| 32949d1eec | |||
| 7718f77f3d | |||
| a9e54981bf | |||
| ef35293f8a | |||
| 5c63770b6b | |||
| d130e20c92 | |||
| 586fb1ae10 | |||
| 20c2502e31 | |||
| 0e31f85c18 | |||
| e7301eb7a3 |
9
.github/workflows/uffizzi-build.yml
vendored
9
.github/workflows/uffizzi-build.yml
vendored
@ -14,8 +14,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
- name: Generate UUID image name
|
- name: Generate UUID image name
|
||||||
id: uuid
|
id: uuid
|
||||||
run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV
|
run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV
|
||||||
@ -33,11 +31,10 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
file: ./uffizzi/Dockerfile
|
file: ./uffizzi/Dockerfile
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
|
|
||||||
build-nginx:
|
build-nginx:
|
||||||
|
needs:
|
||||||
|
- build-application
|
||||||
name: Build and Push `nginx`
|
name: Build and Push `nginx`
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
|
||||||
@ -65,6 +62,8 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
file: ./uffizzi/nginx/Dockerfile
|
file: ./uffizzi/nginx/Dockerfile
|
||||||
|
build-args: |
|
||||||
|
BASE_IMAGE=${{ needs.build-application.outputs.tags }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
|||||||
@ -103,6 +103,7 @@ class CustomerStatsController extends Controller
|
|||||||
)
|
)
|
||||||
->whereCompany()
|
->whereCompany()
|
||||||
->whereCustomer($customer->id)
|
->whereCustomer($customer->id)
|
||||||
|
->where('status', '<>', Invoice::STATUS_DRAFT)
|
||||||
->sum('total');
|
->sum('total');
|
||||||
$totalReceipts = Payment::whereBetween(
|
$totalReceipts = Payment::whereBetween(
|
||||||
'payment_date',
|
'payment_date',
|
||||||
|
|||||||
@ -104,6 +104,7 @@ class DashboardController extends Controller
|
|||||||
'invoice_date',
|
'invoice_date',
|
||||||
[$startDate->format('Y-m-d'), $start->format('Y-m-d')]
|
[$startDate->format('Y-m-d'), $start->format('Y-m-d')]
|
||||||
)
|
)
|
||||||
|
->where('status', '<>', Invoice::STATUS_DRAFT)
|
||||||
->whereCompany()
|
->whereCompany()
|
||||||
->sum('base_total');
|
->sum('base_total');
|
||||||
|
|
||||||
@ -141,6 +142,7 @@ class DashboardController extends Controller
|
|||||||
$recent_due_invoices = Invoice::with('customer')
|
$recent_due_invoices = Invoice::with('customer')
|
||||||
->whereCompany()
|
->whereCompany()
|
||||||
->where('base_due_amount', '>', 0)
|
->where('base_due_amount', '>', 0)
|
||||||
|
->where('status', '<>', Invoice::STATUS_DRAFT)
|
||||||
->take(5)
|
->take(5)
|
||||||
->latest()
|
->latest()
|
||||||
->get();
|
->get();
|
||||||
|
|||||||
@ -24,6 +24,7 @@ class InvoicesController extends Controller
|
|||||||
$limit = $request->has('limit') ? $request->limit : 10;
|
$limit = $request->has('limit') ? $request->limit : 10;
|
||||||
|
|
||||||
$invoices = Invoice::whereCompany()
|
$invoices = Invoice::whereCompany()
|
||||||
|
->whereTabFilters($request->tab_status)
|
||||||
->join('customers', 'customers.id', '=', 'invoices.customer_id')
|
->join('customers', 'customers.id', '=', 'invoices.customer_id')
|
||||||
->applyFilters($request->all())
|
->applyFilters($request->all())
|
||||||
->select('invoices.*', 'customers.name')
|
->select('invoices.*', 'customers.name')
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
namespace Crater\Http\Requests;
|
namespace Crater\Http\Requests;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class CompaniesRequest extends FormRequest
|
class CompaniesRequest extends FormRequest
|
||||||
@ -34,6 +33,10 @@ class CompaniesRequest extends FormRequest
|
|||||||
'currency' => [
|
'currency' => [
|
||||||
'required'
|
'required'
|
||||||
],
|
],
|
||||||
|
'slug' => [
|
||||||
|
'required',
|
||||||
|
Rule::unique('companies')
|
||||||
|
],
|
||||||
'address.name' => [
|
'address.name' => [
|
||||||
'nullable',
|
'nullable',
|
||||||
],
|
],
|
||||||
@ -68,11 +71,11 @@ class CompaniesRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
return collect($this->validated())
|
return collect($this->validated())
|
||||||
->only([
|
->only([
|
||||||
'name'
|
'name',
|
||||||
|
'slug'
|
||||||
])
|
])
|
||||||
->merge([
|
->merge([
|
||||||
'owner_id' => $this->user()->id,
|
'owner_id' => $this->user()->id
|
||||||
'slug' => Str::slug($this->name)
|
|
||||||
])
|
])
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,8 @@ class CompanyRequest extends FormRequest
|
|||||||
Rule::unique('companies')->ignore($this->header('company'), 'id'),
|
Rule::unique('companies')->ignore($this->header('company'), 'id'),
|
||||||
],
|
],
|
||||||
'slug' => [
|
'slug' => [
|
||||||
'nullable'
|
'required',
|
||||||
|
Rule::unique('companies')->ignore($this->header('company'), 'id'),
|
||||||
],
|
],
|
||||||
'address.country_id' => [
|
'address.country_id' => [
|
||||||
'required',
|
'required',
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class EstimateResource extends JsonResource
|
|||||||
'reference_number' => $this->reference_number,
|
'reference_number' => $this->reference_number,
|
||||||
'tax_per_item' => $this->tax_per_item,
|
'tax_per_item' => $this->tax_per_item,
|
||||||
'discount_per_item' => $this->discount_per_item,
|
'discount_per_item' => $this->discount_per_item,
|
||||||
'notes' => $this->getNotes(),
|
'notes' => $this->notes,
|
||||||
'discount' => $this->discount,
|
'discount' => $this->discount,
|
||||||
'discount_type' => $this->discount_type,
|
'discount_type' => $this->discount_type,
|
||||||
'discount_val' => $this->discount_val,
|
'discount_val' => $this->discount_val,
|
||||||
|
|||||||
@ -18,7 +18,7 @@ class PaymentResource extends JsonResource
|
|||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'payment_number' => $this->payment_number,
|
'payment_number' => $this->payment_number,
|
||||||
'payment_date' => $this->payment_date,
|
'payment_date' => $this->payment_date,
|
||||||
'notes' => $this->getNotes(),
|
'notes' => $this->notes,
|
||||||
'amount' => $this->amount,
|
'amount' => $this->amount,
|
||||||
'unique_hash' => $this->unique_hash,
|
'unique_hash' => $this->unique_hash,
|
||||||
'invoice_id' => $this->invoice_id,
|
'invoice_id' => $this->invoice_id,
|
||||||
|
|||||||
@ -217,7 +217,7 @@ class Company extends Model implements HasMedia
|
|||||||
'estimate_billing_address_format' => $billingAddressFormat,
|
'estimate_billing_address_format' => $billingAddressFormat,
|
||||||
'payment_company_address_format' => $companyAddressFormat,
|
'payment_company_address_format' => $companyAddressFormat,
|
||||||
'payment_from_customer_address_format' => $paymentFromCustomerAddress,
|
'payment_from_customer_address_format' => $paymentFromCustomerAddress,
|
||||||
'currency' => request()->currency ?? 13,
|
'currency' => request()->currency ?? 1,
|
||||||
'time_zone' => 'Asia/Kolkata',
|
'time_zone' => 'Asia/Kolkata',
|
||||||
'language' => 'en',
|
'language' => 'en',
|
||||||
'fiscal_year' => '1-12',
|
'fiscal_year' => '1-12',
|
||||||
|
|||||||
@ -483,7 +483,8 @@ class Estimate extends Model implements HasMedia
|
|||||||
'{ESTIMATE_DATE}' => $this->formattedEstimateDate,
|
'{ESTIMATE_DATE}' => $this->formattedEstimateDate,
|
||||||
'{ESTIMATE_EXPIRY_DATE}' => $this->formattedExpiryDate,
|
'{ESTIMATE_EXPIRY_DATE}' => $this->formattedExpiryDate,
|
||||||
'{ESTIMATE_NUMBER}' => $this->estimate_number,
|
'{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)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -240,7 +240,7 @@ class Expense extends Model implements HasMedia
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($request->hasFile('attachment_receipt')) {
|
if ($request->hasFile('attachment_receipt')) {
|
||||||
$expense->addMediaFromRequest('attachment_receipt')->toMediaCollection('receipts');
|
$expense->addMediaFromRequest('attachment_receipt')->toMediaCollection('receipts', 'local');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->customFields) {
|
if ($request->customFields) {
|
||||||
@ -262,12 +262,12 @@ class Expense extends Model implements HasMedia
|
|||||||
ExchangeRateLog::addExchangeRateLog($this);
|
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');
|
$this->clearMediaCollection('receipts');
|
||||||
}
|
}
|
||||||
if ($request->hasFile('attachment_receipt')) {
|
if ($request->hasFile('attachment_receipt')) {
|
||||||
$this->clearMediaCollection('receipts');
|
$this->clearMediaCollection('receipts');
|
||||||
$this->addMediaFromRequest('attachment_receipt')->toMediaCollection('receipts');
|
$this->addMediaFromRequest('attachment_receipt')->toMediaCollection('receipts', 'local');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->customFields) {
|
if ($request->customFields) {
|
||||||
|
|||||||
@ -187,16 +187,6 @@ class Invoice extends Model implements HasMedia
|
|||||||
return Carbon::parse($this->invoice_date)->format($dateFormat);
|
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)
|
public function scopeWhereDueStatus($query, $status)
|
||||||
{
|
{
|
||||||
return $query->whereIn('invoices.paid_status', [
|
return $query->whereIn('invoices.paid_status', [
|
||||||
@ -234,6 +224,40 @@ class Invoice extends Model implements HasMedia
|
|||||||
$query->orderBy($orderByField, $orderBy);
|
$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)
|
public function scopeApplyFilters($query, array $filters)
|
||||||
{
|
{
|
||||||
$filters = collect($filters);
|
$filters = collect($filters);
|
||||||
@ -249,17 +273,11 @@ class Invoice extends Model implements HasMedia
|
|||||||
$filters->get('status') == self::STATUS_PAID
|
$filters->get('status') == self::STATUS_PAID
|
||||||
) {
|
) {
|
||||||
$query->wherePaidStatus($filters->get('status'));
|
$query->wherePaidStatus($filters->get('status'));
|
||||||
} elseif ($filters->get('status') == 'DUE') {
|
|
||||||
$query->whereDueStatus($filters->get('status'));
|
|
||||||
} else {
|
} else {
|
||||||
$query->whereStatus($filters->get('status'));
|
$query->whereStatus($filters->get('status'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($filters->get('paid_status')) {
|
|
||||||
$query->wherePaidStatus($filters->get('status'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($filters->get('invoice_id')) {
|
if ($filters->get('invoice_id')) {
|
||||||
$query->whereInvoice($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_DATE}' => $this->formattedInvoiceDate,
|
||||||
'{INVOICE_DUE_DATE}' => $this->formattedDueDate,
|
'{INVOICE_DUE_DATE}' => $this->formattedDueDate,
|
||||||
'{INVOICE_NUMBER}' => $this->invoice_number,
|
'{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)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -435,7 +435,8 @@ class Payment extends Model implements HasMedia
|
|||||||
'{PAYMENT_DATE}' => $this->formattedPaymentDate,
|
'{PAYMENT_DATE}' => $this->formattedPaymentDate,
|
||||||
'{PAYMENT_MODE}' => $this->paymentMethod ? $this->paymentMethod->name : null,
|
'{PAYMENT_MODE}' => $this->paymentMethod ? $this->paymentMethod->name : null,
|
||||||
'{PAYMENT_NUMBER}' => $this->payment_number,
|
'{PAYMENT_NUMBER}' => $this->payment_number,
|
||||||
'{PAYMENT_AMOUNT}' => $this->reference_number,
|
'{PDF_LINK}' => $this->paymentPdfUrl,
|
||||||
|
'{PAYMENT_AMOUNT}' => format_money_pdf($this->amount, $this->customer->currency)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
"vite": "^2.6.1"
|
"vite": "^2.6.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/vue": "^1.4.0",
|
"@headlessui/vue": "^1.5.0",
|
||||||
"@heroicons/vue": "^1.0.1",
|
"@heroicons/vue": "^1.0.1",
|
||||||
"@popperjs/core": "^2.9.2",
|
"@popperjs/core": "^2.9.2",
|
||||||
"@stripe/stripe-js": "^1.21.2",
|
"@stripe/stripe-js": "^1.21.2",
|
||||||
@ -48,7 +48,8 @@
|
|||||||
"mini-svg-data-uri": "^1.3.3",
|
"mini-svg-data-uri": "^1.3.3",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"pinia": "^2.0.4",
|
"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",
|
"v-tooltip": "^4.0.0-alpha.1",
|
||||||
"vue": "^3.2.0-beta.5",
|
"vue": "^3.2.0-beta.5",
|
||||||
"vue-flatpickr-component": "^9.0.3",
|
"vue-flatpickr-component": "^9.0.3",
|
||||||
|
|||||||
@ -64,7 +64,7 @@ function mergeExistingValues() {
|
|||||||
if (props.isEdit) {
|
if (props.isEdit) {
|
||||||
props.store[props.storeProp].fields.forEach((field) => {
|
props.store[props.storeProp].fields.forEach((field) => {
|
||||||
const existingIndex = props.store[props.storeProp].customFields.findIndex(
|
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) {
|
if (existingIndex > -1) {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { computed } from 'vue'
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: moment().format('YYYY-MM-DD hh:MM'),
|
default: moment().format('YYYY-MM-DD HH:mm'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,24 @@
|
|||||||
/>
|
/>
|
||||||
</BaseInputGroup>
|
</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
|
<BaseInputGroup
|
||||||
:content-loading="isFetchingInitialData"
|
:content-loading="isFetchingInitialData"
|
||||||
:label="$tc('settings.company_info.country')"
|
:label="$tc('settings.company_info.country')"
|
||||||
@ -130,7 +148,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useModalStore } from '@/scripts/stores/modal'
|
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 { useI18n } from 'vue-i18n'
|
||||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||||
import { useVuelidate } from '@vuelidate/core'
|
import { useVuelidate } from '@vuelidate/core'
|
||||||
@ -152,6 +170,7 @@ let companyLogoName = ref(null)
|
|||||||
|
|
||||||
const newCompanyForm = reactive({
|
const newCompanyForm = reactive({
|
||||||
name: null,
|
name: null,
|
||||||
|
slug: null,
|
||||||
currency: '',
|
currency: '',
|
||||||
address: {
|
address: {
|
||||||
country_id: null,
|
country_id: null,
|
||||||
@ -162,6 +181,9 @@ const modalActive = computed(() => {
|
|||||||
return modalStore.active && modalStore.componentName === 'CompanyModal'
|
return modalStore.active && modalStore.componentName === 'CompanyModal'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const slugValidator = (value) => {
|
||||||
|
return value == slugify(value)
|
||||||
|
}
|
||||||
const rules = {
|
const rules = {
|
||||||
newCompanyForm: {
|
newCompanyForm: {
|
||||||
name: {
|
name: {
|
||||||
@ -171,6 +193,17 @@ const rules = {
|
|||||||
minLength(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: {
|
address: {
|
||||||
country_id: {
|
country_id: {
|
||||||
required: helpers.withMessage(t('validation.required'), required),
|
required: helpers.withMessage(t('validation.required'), required),
|
||||||
@ -243,6 +276,7 @@ async function submitCompanyData() {
|
|||||||
|
|
||||||
function resetNewCompanyForm() {
|
function resetNewCompanyForm() {
|
||||||
newCompanyForm.name = ''
|
newCompanyForm.name = ''
|
||||||
|
newCompanyForm.slug = ''
|
||||||
newCompanyForm.currency = ''
|
newCompanyForm.currency = ''
|
||||||
newCompanyForm.address.country_id = ''
|
newCompanyForm.address.country_id = ''
|
||||||
|
|
||||||
@ -257,4 +291,24 @@ function closeCompanyModal() {
|
|||||||
v$.value.$reset()
|
v$.value.$reset()
|
||||||
}, 300)
|
}, 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>
|
</script>
|
||||||
|
|||||||
@ -12,6 +12,8 @@
|
|||||||
class="h-screen h-screen-ios overflow-y-auto md:pl-56 xl:pl-64 min-h-0"
|
class="h-screen h-screen-ios overflow-y-auto md:pl-56 xl:pl-64 min-h-0"
|
||||||
>
|
>
|
||||||
<div class="pt-16 pb-16">
|
<div class="pt-16 pb-16">
|
||||||
|
<h1>Testing</h1>
|
||||||
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-screen overflow-y-auto text-base">
|
<div class="h-screen overflow-y-auto text-base">
|
||||||
|
<h1>Testing</h1>
|
||||||
<NotificationRoot />
|
<NotificationRoot />
|
||||||
|
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
|
|||||||
@ -93,6 +93,9 @@
|
|||||||
>
|
>
|
||||||
{{ pageHeading }}
|
{{ pageHeading }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<h1>Testing</h1>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="
|
class="
|
||||||
hidden
|
hidden
|
||||||
|
|||||||
@ -184,6 +184,20 @@ export const useCompanyStore = (useWindow = false) => {
|
|||||||
setDefaultCurrency(data) {
|
setDefaultCurrency(data) {
|
||||||
this.defaultCurrency = data.currency
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,8 @@
|
|||||||
:content-loading="isLoading"
|
:content-loading="isLoading"
|
||||||
:calendar-button="true"
|
:calendar-button="true"
|
||||||
calendar-button-icon="calendar"
|
calendar-button-icon="calendar"
|
||||||
|
:show-extra-options="true"
|
||||||
|
:source-date="estimateStore.newEstimate.estimate_date"
|
||||||
/>
|
/>
|
||||||
</BaseInputGroup>
|
</BaseInputGroup>
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,24 @@
|
|||||||
/>
|
/>
|
||||||
</BaseInputGroup>
|
</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
|
<BaseInputGroup
|
||||||
:label="$t('wizard.country')"
|
:label="$t('wizard.country')"
|
||||||
:error="
|
:error="
|
||||||
@ -57,9 +75,7 @@
|
|||||||
track-by="name"
|
track-by="name"
|
||||||
/>
|
/>
|
||||||
</BaseInputGroup>
|
</BaseInputGroup>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
|
|
||||||
<BaseInputGroup :label="$t('wizard.state')">
|
<BaseInputGroup :label="$t('wizard.state')">
|
||||||
<BaseInput
|
<BaseInput
|
||||||
v-model="companyForm.address.state"
|
v-model="companyForm.address.state"
|
||||||
@ -144,9 +160,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, reactive } from 'vue'
|
import { ref, computed, onMounted, reactive, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
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 { useVuelidate } from '@vuelidate/core'
|
||||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||||
@ -162,6 +178,7 @@ let logoFileName = ref(null)
|
|||||||
|
|
||||||
const companyForm = reactive({
|
const companyForm = reactive({
|
||||||
name: null,
|
name: null,
|
||||||
|
slug: null,
|
||||||
address: {
|
address: {
|
||||||
address_street_1: '',
|
address_street_1: '',
|
||||||
address_street_2: '',
|
address_street_2: '',
|
||||||
@ -188,10 +205,28 @@ onMounted(async () => {
|
|||||||
})?.id
|
})?.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const slugValidator = (value) => {
|
||||||
|
return value == slugify(value)
|
||||||
|
}
|
||||||
const rules = {
|
const rules = {
|
||||||
companyForm: {
|
companyForm: {
|
||||||
name: {
|
name: {
|
||||||
required: helpers.withMessage(t('validation.required'), required),
|
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: {
|
address: {
|
||||||
country_id: {
|
country_id: {
|
||||||
@ -249,4 +284,24 @@ async function next() {
|
|||||||
emit('next', 7)
|
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>
|
</script>
|
||||||
|
|||||||
@ -56,7 +56,7 @@
|
|||||||
<BaseMultiselect
|
<BaseMultiselect
|
||||||
v-model="filters.status"
|
v-model="filters.status"
|
||||||
:groups="true"
|
:groups="true"
|
||||||
:options="status"
|
:options="invoiceStatus"
|
||||||
searchable
|
searchable
|
||||||
:placeholder="$t('general.select_a_status')"
|
:placeholder="$t('general.select_a_status')"
|
||||||
@update:modelValue="setActiveTab"
|
@update:modelValue="setActiveTab"
|
||||||
@ -130,11 +130,27 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<BaseTabGroup class="-mb-5" @change="setStatusFilter">
|
<BaseTabGroup
|
||||||
<BaseTab :title="$t('general.all')" filter="" />
|
class="-mb-5"
|
||||||
<BaseTab :title="$t('general.draft')" filter="DRAFT" />
|
:selected-index="selectedIndex"
|
||||||
<BaseTab :title="$t('general.sent')" filter="SENT" />
|
@change="changeTabStatus"
|
||||||
<BaseTab :title="$t('general.due')" filter="DUE" />
|
>
|
||||||
|
<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>
|
</BaseTabGroup>
|
||||||
|
|
||||||
<BaseDropdown
|
<BaseDropdown
|
||||||
@ -289,10 +305,10 @@ const utils = inject('$utils')
|
|||||||
const table = ref(null)
|
const table = ref(null)
|
||||||
const showFilters = ref(false)
|
const showFilters = ref(false)
|
||||||
|
|
||||||
const status = ref([
|
const invoiceStatus = ref([
|
||||||
{
|
{
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
options: ['DRAFT', 'DUE', 'SENT', 'VIEWED', 'COMPLETED'],
|
options: ['DRAFT', 'SENT', 'VIEWED', 'COMPLETED'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Paid Status',
|
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 isRequestOngoing = ref(true)
|
||||||
const activeTab = ref('general.draft')
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const selectedIndex = ref(0)
|
||||||
|
|
||||||
let filters = reactive({
|
let filters = reactive({
|
||||||
customer_id: '',
|
customer_id: '',
|
||||||
@ -311,6 +346,7 @@ let filters = reactive({
|
|||||||
from_date: '',
|
from_date: '',
|
||||||
to_date: '',
|
to_date: '',
|
||||||
invoice_number: '',
|
invoice_number: '',
|
||||||
|
tab_status: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const showEmptyScreen = computed(
|
const showEmptyScreen = computed(
|
||||||
@ -401,6 +437,7 @@ async function fetchData({ page, filter, sort }) {
|
|||||||
from_date: filters.from_date,
|
from_date: filters.from_date,
|
||||||
to_date: filters.to_date,
|
to_date: filters.to_date,
|
||||||
invoice_number: filters.invoice_number,
|
invoice_number: filters.invoice_number,
|
||||||
|
tab_status: filters.tab_status,
|
||||||
orderByField: sort.fieldName || 'created_at',
|
orderByField: sort.fieldName || 'created_at',
|
||||||
orderBy: sort.order || 'desc',
|
orderBy: sort.order || 'desc',
|
||||||
page,
|
page,
|
||||||
@ -423,29 +460,9 @@ async function fetchData({ page, filter, sort }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatusFilter(val) {
|
function changeTabStatus(val, index) {
|
||||||
if (activeTab.value == val.title) {
|
filters.tab_status = val['tab-status']
|
||||||
return true
|
selectedIndex.value = index
|
||||||
}
|
|
||||||
|
|
||||||
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 setFilters() {
|
function setFilters() {
|
||||||
@ -463,8 +480,6 @@ function clearFilter() {
|
|||||||
filters.from_date = ''
|
filters.from_date = ''
|
||||||
filters.to_date = ''
|
filters.to_date = ''
|
||||||
filters.invoice_number = ''
|
filters.invoice_number = ''
|
||||||
|
|
||||||
activeTab.value = t('general.all')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeMultipleInvoices() {
|
async function removeMultipleInvoices() {
|
||||||
@ -505,39 +520,21 @@ function toggleFilter() {
|
|||||||
function setActiveTab(val) {
|
function setActiveTab(val) {
|
||||||
switch (val) {
|
switch (val) {
|
||||||
case 'DRAFT':
|
case 'DRAFT':
|
||||||
activeTab.value = t('general.draft')
|
selectedIndex.value = 1
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'SENT':
|
case 'SENT':
|
||||||
activeTab.value = t('general.sent')
|
case 'VIEWED':
|
||||||
break
|
|
||||||
|
|
||||||
case 'DUE':
|
|
||||||
activeTab.value = t('general.due')
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'COMPLETED':
|
case 'COMPLETED':
|
||||||
activeTab.value = t('invoices.completed')
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'PAID':
|
case 'PAID':
|
||||||
activeTab.value = t('invoices.paid')
|
selectedIndex.value = 2
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'UNPAID':
|
case 'UNPAID':
|
||||||
activeTab.value = t('invoices.unpaid')
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'PARTIALLY_PAID':
|
case 'PARTIALLY_PAID':
|
||||||
activeTab.value = t('invoices.partially_paid')
|
selectedIndex.value = 3
|
||||||
break
|
|
||||||
|
|
||||||
case 'VIEWED':
|
|
||||||
activeTab.value = t('invoices.viewed')
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
activeTab.value = t('general.all')
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
filters.tab_status = invoiceTabStatus[selectedIndex.value].value
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -32,6 +32,8 @@
|
|||||||
:content-loading="isLoading"
|
:content-loading="isLoading"
|
||||||
:calendar-button="true"
|
:calendar-button="true"
|
||||||
calendar-button-icon="calendar"
|
calendar-button-icon="calendar"
|
||||||
|
:show-extra-options="true"
|
||||||
|
:source-date="invoiceStore.newInvoice.invoice_date"
|
||||||
/>
|
/>
|
||||||
</BaseInputGroup>
|
</BaseInputGroup>
|
||||||
|
|
||||||
|
|||||||
@ -82,9 +82,9 @@
|
|||||||
required
|
required
|
||||||
>
|
>
|
||||||
<BaseCustomerSelectInput
|
<BaseCustomerSelectInput
|
||||||
|
v-if="!isLoadingContent"
|
||||||
v-model="paymentStore.currentPayment.customer_id"
|
v-model="paymentStore.currentPayment.customer_id"
|
||||||
:content-loading="isLoadingContent"
|
:content-loading="isLoadingContent"
|
||||||
v-if="!isLoadingContent"
|
|
||||||
:invalid="v$.currentPayment.customer_id.$error"
|
:invalid="v$.currentPayment.customer_id.$error"
|
||||||
:placeholder="$t('customers.select_a_customer')"
|
:placeholder="$t('customers.select_a_customer')"
|
||||||
show-action
|
show-action
|
||||||
@ -423,7 +423,7 @@ function onCustomerChange(customer_id) {
|
|||||||
if (customer_id) {
|
if (customer_id) {
|
||||||
let data = {
|
let data = {
|
||||||
customer_id: customer_id,
|
customer_id: customer_id,
|
||||||
status: 'DUE',
|
tab_status: 'DUE',
|
||||||
limit: 'all',
|
limit: 'all',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -446,7 +446,11 @@ function onCustomerChange(customer_id) {
|
|||||||
paymentStore.currentPayment.selectedCustomer = res2.data.data
|
paymentStore.currentPayment.selectedCustomer = res2.data.data
|
||||||
paymentStore.currentPayment.customer = res2.data.data
|
paymentStore.currentPayment.customer = res2.data.data
|
||||||
paymentStore.currentPayment.currency = res2.data.data.currency
|
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
|
customerStore.editCustomer = res2.data.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,19 @@
|
|||||||
/>
|
/>
|
||||||
</BaseInputGroup>
|
</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')">
|
<BaseInputGroup :label="$tc('settings.company_info.phone')">
|
||||||
<BaseInput v-model="companyForm.address.phone" />
|
<BaseInput v-model="companyForm.address.phone" />
|
||||||
</BaseInputGroup>
|
</BaseInputGroup>
|
||||||
@ -160,6 +173,7 @@ let isSaving = ref(false)
|
|||||||
|
|
||||||
const companyForm = reactive({
|
const companyForm = reactive({
|
||||||
name: null,
|
name: null,
|
||||||
|
slug: null,
|
||||||
logo: null,
|
logo: null,
|
||||||
address: {
|
address: {
|
||||||
address_street_1: '',
|
address_street_1: '',
|
||||||
@ -193,7 +207,14 @@ const rules = computed(() => {
|
|||||||
name: {
|
name: {
|
||||||
required: helpers.withMessage(t('validation.required'), required),
|
required: helpers.withMessage(t('validation.required'), required),
|
||||||
minLength: helpers.withMessage(
|
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)
|
minLength(3)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,11 @@
|
|||||||
<BaseInputGroup
|
<BaseInputGroup
|
||||||
:content-loading="isFetchingInitialData"
|
:content-loading="isFetchingInitialData"
|
||||||
:label="$tc('settings.preferences.currency')"
|
: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"
|
:error="v$.currency.$error && v$.currency.$errors[0].$message"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
@ -21,7 +25,7 @@
|
|||||||
:searchable="true"
|
:searchable="true"
|
||||||
track-by="name"
|
track-by="name"
|
||||||
:invalid="v$.currency.$error"
|
:invalid="v$.currency.$error"
|
||||||
disabled
|
:disabled="isCurrencyDisabled"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
</BaseMultiselect>
|
</BaseMultiselect>
|
||||||
@ -187,6 +191,7 @@ const { t, tm } = useI18n()
|
|||||||
let isSaving = ref(false)
|
let isSaving = ref(false)
|
||||||
let isDataSaving = ref(false)
|
let isDataSaving = ref(false)
|
||||||
let isFetchingInitialData = ref(false)
|
let isFetchingInitialData = ref(false)
|
||||||
|
let isCurrencyDisabled = ref(true)
|
||||||
|
|
||||||
const settingsForm = reactive({ ...companyStore.selectedCompanySettings })
|
const settingsForm = reactive({ ...companyStore.selectedCompanySettings })
|
||||||
|
|
||||||
@ -282,10 +287,14 @@ setInitialData()
|
|||||||
async function setInitialData() {
|
async function setInitialData() {
|
||||||
isFetchingInitialData.value = true
|
isFetchingInitialData.value = true
|
||||||
Promise.all([
|
Promise.all([
|
||||||
|
companyStore.checkCompanyHasCurrencyTransactions(),
|
||||||
globalStore.fetchCurrencies(),
|
globalStore.fetchCurrencies(),
|
||||||
globalStore.fetchDateFormats(),
|
globalStore.fetchDateFormats(),
|
||||||
globalStore.fetchTimeZones(),
|
globalStore.fetchTimeZones(),
|
||||||
]).then(([res1]) => {
|
]).then(([res1]) => {
|
||||||
|
if (res1.data?.has_transactions == false) {
|
||||||
|
isCurrencyDisabled.value = false
|
||||||
|
}
|
||||||
isFetchingInitialData.value = false
|
isFetchingInitialData.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -126,7 +126,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const value = computed({
|
const value = computed({
|
||||||
get: () => props.modelValue,
|
get: () => (props.modelValue ? props.modelValue : ''),
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
},
|
},
|
||||||
@ -195,7 +195,9 @@ async function getFields() {
|
|||||||
{ label: 'Date', value: 'INVOICE_DATE' },
|
{ label: 'Date', value: 'INVOICE_DATE' },
|
||||||
{ label: 'Due Date', value: 'INVOICE_DUE_DATE' },
|
{ label: 'Due Date', value: 'INVOICE_DUE_DATE' },
|
||||||
{ label: 'Number', value: 'INVOICE_NUMBER' },
|
{ 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) => ({
|
...invoiceFields.value.map((i) => ({
|
||||||
label: i.label,
|
label: i.label,
|
||||||
value: i.slug,
|
value: i.slug,
|
||||||
@ -211,7 +213,8 @@ async function getFields() {
|
|||||||
{ label: 'Date', value: 'ESTIMATE_DATE' },
|
{ label: 'Date', value: 'ESTIMATE_DATE' },
|
||||||
{ label: 'Expiry Date', value: 'ESTIMATE_EXPIRY_DATE' },
|
{ label: 'Expiry Date', value: 'ESTIMATE_EXPIRY_DATE' },
|
||||||
{ label: 'Number', value: 'ESTIMATE_NUMBER' },
|
{ 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) => ({
|
...estimateFields.value.map((i) => ({
|
||||||
label: i.label,
|
label: i.label,
|
||||||
value: i.slug,
|
value: i.slug,
|
||||||
@ -228,6 +231,7 @@ async function getFields() {
|
|||||||
{ label: 'Number', value: 'PAYMENT_NUMBER' },
|
{ label: 'Number', value: 'PAYMENT_NUMBER' },
|
||||||
{ label: 'Mode', value: 'PAYMENT_MODE' },
|
{ label: 'Mode', value: 'PAYMENT_MODE' },
|
||||||
{ label: 'Amount', value: 'PAYMENT_AMOUNT' },
|
{ label: 'Amount', value: 'PAYMENT_AMOUNT' },
|
||||||
|
{ label: 'PDF Link', value: 'PDF_LINK' },
|
||||||
...paymentFields.value.map((i) => ({
|
...paymentFields.value.map((i) => ({
|
||||||
label: i.label,
|
label: i.label,
|
||||||
value: i.slug,
|
value: i.slug,
|
||||||
|
|||||||
@ -7,52 +7,108 @@
|
|||||||
/>
|
/>
|
||||||
</BaseContentPlaceholders>
|
</BaseContentPlaceholders>
|
||||||
|
|
||||||
<div v-else :class="computedContainerClass" class="relative flex flex-row">
|
<div v-else :class="computedContainerClass">
|
||||||
<svg
|
<date-picker
|
||||||
v-if="showCalendarIcon && !hasIconSlot"
|
ref="vCalendar"
|
||||||
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"
|
|
||||||
v-model="date"
|
v-model="date"
|
||||||
v-bind="$attrs"
|
:mode="mode"
|
||||||
:disabled="disabled"
|
:is24hr="time24hr"
|
||||||
:config="config"
|
class="w-full"
|
||||||
:class="[defaultInputClass, inputInvalidClass, inputDisabledClass]"
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script type="text/babel" setup>
|
<script type="text/babel" setup>
|
||||||
import FlatPickr from 'vue-flatpickr-component'
|
import { Calendar, DatePicker } from 'v-calendar'
|
||||||
import 'flatpickr/dist/flatpickr.css'
|
import 'v-calendar/dist/style.css'
|
||||||
import { computed, reactive, watch, ref, useSlots } from 'vue'
|
import { computed, reactive, watch, ref, useSlots } from 'vue'
|
||||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||||
|
import moment from 'moment'
|
||||||
const dp = ref(null)
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@ -90,36 +146,31 @@ const props = defineProps({
|
|||||||
defaultInputClass: {
|
defaultInputClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default:
|
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: {
|
time24hr: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
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 emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
const companyStore = useCompanyStore()
|
const companyStore = useCompanyStore()
|
||||||
|
const { global } = window.i18n
|
||||||
let config = reactive({
|
const vCalendar = ref(null)
|
||||||
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 hasIconSlot = computed(() => {
|
const hasIconSlot = computed(() => {
|
||||||
return !!slots.icon
|
return !!slots.icon
|
||||||
@ -135,7 +186,6 @@ const inputInvalidClass = computed(() => {
|
|||||||
if (props.invalid) {
|
if (props.invalid) {
|
||||||
return 'border-red-400 ring-red-400 focus:ring-red-400 focus:border-red-400'
|
return 'border-red-400 ring-red-400 focus:ring-red-400 focus:border-red-400'
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -143,35 +193,97 @@ const inputDisabledClass = computed(() => {
|
|||||||
if (props.disabled) {
|
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 '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 ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
function onClickDp(params) {
|
// to convert YYYY-MM-DD | YYYY-MM-DD HH:mm format
|
||||||
dp.value.fp.open()
|
function convertYMDFormat(date) {
|
||||||
|
let format = props.enableTime ? 'YYYY-MM-DD HH:mm' : 'YYYY-MM-DD'
|
||||||
|
return date ? moment(date).format(format) : date
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
const date = computed({
|
||||||
() => props.enableTime,
|
get: () => props.modelValue,
|
||||||
(val) => {
|
set: (value) => {
|
||||||
if (props.enableTime) {
|
emit('update:modelValue', value)
|
||||||
config.enableTime = props.enableTime
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ 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(
|
watch(
|
||||||
() => carbonFormat,
|
() => carbonFormat,
|
||||||
() => {
|
() => {
|
||||||
if (!props.enableTime) {
|
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 {
|
} else {
|
||||||
config.altFormat = carbonFormat.value
|
let timeFormat = 'HH:mm'
|
||||||
? `${carbonFormat.value} H:i `
|
if (props.time24hr) {
|
||||||
: 'd M Y H:i'
|
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 }
|
{ 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>
|
</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>
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<TabGroup :default-index="defaultIndex" @change="onChange">
|
<TabGroup
|
||||||
|
:selected-index="selectedIndex"
|
||||||
|
:default-index="defaultIndex"
|
||||||
|
@change="onChange"
|
||||||
|
>
|
||||||
<TabList
|
<TabList
|
||||||
:class="[
|
:class="[
|
||||||
'flex border-b border-grey-light',
|
'flex border-b border-grey-light',
|
||||||
@ -54,6 +58,10 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
filter: {
|
filter: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
@ -67,6 +75,6 @@ const slots = useSlots()
|
|||||||
const tabs = computed(() => slots.default().map((tab) => tab.props))
|
const tabs = computed(() => slots.default().map((tab) => tab.props))
|
||||||
|
|
||||||
function onChange(d) {
|
function onChange(d) {
|
||||||
emit('change', tabs.value[d])
|
emit('change', tabs.value[d], d)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -7,12 +7,12 @@ export function usePopper(options) {
|
|||||||
let popper = ref(null)
|
let popper = ref(null)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
watchEffect(onInvalidate => {
|
watchEffect((onInvalidate) => {
|
||||||
if (!container.value) return
|
if (!container.value) return
|
||||||
if (!activator.value) return
|
if (!activator.value) return
|
||||||
|
|
||||||
let containerEl = container.value.el || container.value
|
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 (!(activatorEl instanceof HTMLElement)) return
|
||||||
if (!(containerEl instanceof HTMLElement)) return
|
if (!(containerEl instanceof HTMLElement)) return
|
||||||
|
|||||||
@ -863,6 +863,8 @@
|
|||||||
"company_info": {
|
"company_info": {
|
||||||
"company_info": "Company info",
|
"company_info": "Company info",
|
||||||
"company_name": "Company Name",
|
"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",
|
"company_logo": "Company Logo",
|
||||||
"section_description": "Information about your company that will be displayed on invoices, estimates and other documents created by Crater.",
|
"section_description": "Information about your company that will be displayed on invoices, estimates and other documents created by Crater.",
|
||||||
"phone": "Phone",
|
"phone": "Phone",
|
||||||
@ -1324,6 +1326,8 @@
|
|||||||
"company_info": "Company Information",
|
"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_info_desc": "This information will be displayed on invoices. Note that you can edit this later on settings page.",
|
||||||
"company_name": "Company Name",
|
"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",
|
"company_logo": "Company Logo",
|
||||||
"logo_preview": "Logo Preview",
|
"logo_preview": "Logo Preview",
|
||||||
"preferences": "Company Preferences",
|
"preferences": "Company Preferences",
|
||||||
@ -1454,7 +1458,8 @@
|
|||||||
"at_least_one_ability": "Please select atleast one Permission.",
|
"at_least_one_ability": "Please select atleast one Permission.",
|
||||||
"valid_driver_key": "Please enter a valid {driver} key.",
|
"valid_driver_key": "Please enter a valid {driver} key.",
|
||||||
"valid_exchange_rate": "Please enter a valid exchange rate.",
|
"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": {
|
"errors": {
|
||||||
"starter_plan": "This feature is available on Starter plan and onwards!",
|
"starter_plan": "This feature is available on Starter plan and onwards!",
|
||||||
@ -1522,5 +1527,13 @@
|
|||||||
"pdf_bill_to": "Bill to,",
|
"pdf_bill_to": "Bill to,",
|
||||||
"pdf_ship_to": "Ship to,",
|
"pdf_ship_to": "Ship to,",
|
||||||
"pdf_received_from": "Received from:",
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -415,32 +415,31 @@ test('update estimate with EUR currency', function () {
|
|||||||
|
|
||||||
$response = putJson('api/v1/estimates/'.$estimate->id, $estimate2);
|
$response = putJson('api/v1/estimates/'.$estimate->id, $estimate2);
|
||||||
|
|
||||||
$this->assertDatabaseHas('estimates', [
|
$estimate_assert = collect($estimate2)
|
||||||
'id' => $estimate['id'],
|
->only([
|
||||||
'template_name' => $estimate2['template_name'],
|
'id',
|
||||||
'estimate_number' => $estimate2['estimate_number'],
|
'template_name',
|
||||||
'discount_type' => $estimate2['discount_type'],
|
'estimate_number',
|
||||||
'discount_val' => $estimate2['discount_val'],
|
'discount_type',
|
||||||
'sub_total' => $estimate2['sub_total'],
|
'discount_val',
|
||||||
'discount' => $estimate2['discount'],
|
'sub_total',
|
||||||
'customer_id' => $estimate2['customer_id'],
|
'discount',
|
||||||
'total' => $estimate2['total'],
|
'customer_id',
|
||||||
'tax' => $estimate2['tax'],
|
'total',
|
||||||
'exchange_rate' => $estimate2['exchange_rate'],
|
'tax'
|
||||||
'base_discount_val' => $estimate2['base_discount_val'],
|
])
|
||||||
'base_sub_total' => $estimate2['base_sub_total'],
|
->toArray();
|
||||||
'base_total' => $estimate2['base_total'],
|
|
||||||
'base_tax' => $estimate2['base_tax'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('estimate_items', [
|
$this->assertDatabaseHas('estimates', $estimate_assert);
|
||||||
'estimate_id' => $estimate2['items'][0]['estimate_id'],
|
|
||||||
'exchange_rate' => $estimate2['items'][0]['exchange_rate'],
|
$estimate_item_assert = collect($estimate2['items'][0])
|
||||||
'base_price' => $estimate2['items'][0]['base_price'],
|
->only([
|
||||||
'base_discount_val' => $estimate2['items'][0]['base_discount_val'],
|
'estimate_id',
|
||||||
'base_tax' => $estimate2['items'][0]['base_tax'],
|
'amount'
|
||||||
'base_total' => $estimate2['items'][0]['base_total'],
|
])
|
||||||
]);
|
->toArray();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('estimate_items', $estimate_item_assert);
|
||||||
|
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -37,13 +37,15 @@ test('create expense', function () {
|
|||||||
|
|
||||||
postJson('api/v1/expenses', $expense)->assertStatus(201);
|
postJson('api/v1/expenses', $expense)->assertStatus(201);
|
||||||
|
|
||||||
$this->assertDatabaseHas('expenses', [
|
$expense = collect($expense)
|
||||||
'notes' => $expense['notes'],
|
->only([
|
||||||
'expense_category_id' => $expense['expense_category_id'],
|
'notes',
|
||||||
'amount' => $expense['amount'],
|
'expense_category_id',
|
||||||
'exchange_rate' => $expense['exchange_rate'],
|
'amount'
|
||||||
'base_amount' => $expense['base_amount'],
|
])
|
||||||
]);
|
->toArray();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('expenses', $expense);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('store validates using a form request', function () {
|
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();
|
putJson('api/v1/expenses/'.$expense->id, $expense2)->assertOk();
|
||||||
|
|
||||||
$this->assertDatabaseHas('expenses', [
|
$expense2 = collect($expense2)
|
||||||
'id' => $expense->id,
|
->only([
|
||||||
'expense_category_id' => $expense2['expense_category_id'],
|
'id',
|
||||||
'amount' => $expense2['amount'],
|
'expense_category_id',
|
||||||
'exchange_rate' => $expense2['exchange_rate'],
|
'amount'
|
||||||
'base_amount' => $expense2['base_amount'],
|
])
|
||||||
]);
|
->toArray();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('expenses', $expense2);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,7 +9,6 @@ use Crater\Models\Tax;
|
|||||||
use Crater\Models\User;
|
use Crater\Models\User;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Laravel\Sanctum\Sanctum;
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
use function Pest\Laravel\getJson;
|
use function Pest\Laravel\getJson;
|
||||||
use function Pest\Laravel\postJson;
|
use function Pest\Laravel\postJson;
|
||||||
use function Pest\Laravel\putJson;
|
use function Pest\Laravel\putJson;
|
||||||
@ -431,31 +430,36 @@ test('update invoice with EUR currency', function () {
|
|||||||
|
|
||||||
putJson('api/v1/invoices/'.$invoice->id, $invoice2)->assertOk();
|
putJson('api/v1/invoices/'.$invoice->id, $invoice2)->assertOk();
|
||||||
|
|
||||||
$this->assertDatabaseHas('invoices', [
|
$invoice_assert = collect($invoice2)
|
||||||
'id' => $invoice['id'],
|
->only([
|
||||||
'invoice_number' => $invoice2['invoice_number'],
|
'invoice_number',
|
||||||
'sub_total' => $invoice2['sub_total'],
|
'template_name',
|
||||||
'total' => $invoice2['total'],
|
'sub_total',
|
||||||
'tax' => $invoice2['tax'],
|
'total',
|
||||||
'discount' => $invoice2['discount'],
|
'tax',
|
||||||
'customer_id' => $invoice2['customer_id'],
|
'discount',
|
||||||
'template_name' => $invoice2['template_name'],
|
'customer_id',
|
||||||
'exchange_rate' => $invoice2['exchange_rate'],
|
])
|
||||||
'base_total' => $invoice2['base_total'],
|
->toArray();
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('invoice_items', [
|
$this->assertDatabaseHas('invoices', $invoice_assert);
|
||||||
'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('taxes', [
|
$invoice_item_assert = collect($invoice2['items'][0])
|
||||||
'amount' => $invoice2['taxes'][0]['amount'],
|
->only([
|
||||||
'name' => $invoice2['taxes'][0]['name'],
|
'invoice_id',
|
||||||
'base_amount' => $invoice2['taxes'][0]['base_amount'],
|
'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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,7 +11,8 @@ RUN apt-get update && apt-get install -y \
|
|||||||
unzip \
|
unzip \
|
||||||
libzip-dev \
|
libzip-dev \
|
||||||
libmagickwand-dev \
|
libmagickwand-dev \
|
||||||
mariadb-client
|
mariadb-client \
|
||||||
|
npm
|
||||||
|
|
||||||
# Clear cache
|
# Clear cache
|
||||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
@ -45,4 +46,19 @@ RUN chmod -R 775 composer.json composer.lock \
|
|||||||
RUN chown -R $(whoami):$(whoami) /var/log/
|
RUN chown -R $(whoami):$(whoami) /var/log/
|
||||||
RUN chmod -R 775 /var/log
|
RUN chmod -R 775 /var/log
|
||||||
|
|
||||||
|
# Cleanup manually generated build files
|
||||||
|
RUN rm -rf /var/www/public/build
|
||||||
|
RUN npm config set user 0
|
||||||
|
RUN npm config set unsafe-perm true
|
||||||
|
# Frontend bulding
|
||||||
|
RUN sed -i 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/g' /var/www/.env
|
||||||
|
RUN sed -i 's/DB_DATABASE=crater/DB_DATABASE=\/tmp\/crater.sqlite/g' /var/www/.env
|
||||||
|
RUN touch /tmp/crater.sqlite
|
||||||
|
RUN composer install --no-interaction --prefer-dist
|
||||||
|
RUN npm i -f
|
||||||
|
RUN npm install --save-dev sass
|
||||||
|
RUN export NODE_OPTIONS="--max-old-space-size=4096" && /usr/bin/npx vite build --target=es2020
|
||||||
|
RUN sed -i 's/DB_CONNECTION=sqlite/DB_CONNECTION=mysql/g' /var/www/.env
|
||||||
|
RUN sed -i 's/DB_DATABASE=\/tmp\/crater.sqlite/DB_DATABASE=crater/g' /var/www/.env
|
||||||
|
|
||||||
USER crater-user
|
USER crater-user
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
ARG BASE_IMAGE
|
||||||
|
|
||||||
|
FROM $BASE_IMAGE as build
|
||||||
FROM nginx:1.17-alpine
|
FROM nginx:1.17-alpine
|
||||||
|
|
||||||
RUN rm /etc/nginx/conf.d/default.conf
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
COPY ./ /var/www
|
COPY --from=build /var/www /var/www
|
||||||
COPY ./uffizzi/nginx/nginx /etc/nginx/conf.d/
|
COPY ./uffizzi/nginx/nginx /etc/nginx/conf.d/
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user