mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-27 11:41:09 -04:00
Compare commits
26 Commits
fix-paymen
...
dark-multi
| Author | SHA1 | Date | |
|---|---|---|---|
| 6abb76b1b9 | |||
| 97be70e88c | |||
| 15f3f566e3 | |||
| 98196194e2 | |||
| df04fd9e53 | |||
| 189c51cdf4 | |||
| bd5f0fe5cf | |||
| 787619b907 | |||
| fd70ab9a99 | |||
| 33315638df | |||
| bba14bf51a | |||
| ea41989034 | |||
| 3f3f83a00a | |||
| adc5962071 | |||
| c4c00002d7 | |||
| a7c1e12db6 | |||
| 32949d1eec | |||
| 7718f77f3d | |||
| a9e54981bf | |||
| ef35293f8a | |||
| 5c63770b6b | |||
| d130e20c92 | |||
| 586fb1ae10 | |||
| 20c2502e31 | |||
| 0e31f85c18 | |||
| e7301eb7a3 |
@ -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>
|
||||||
|
|||||||
@ -15,6 +15,13 @@
|
|||||||
bg-gradient-to-r
|
bg-gradient-to-r
|
||||||
from-primary-500
|
from-primary-500
|
||||||
to-primary-400
|
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
|
<router-link
|
||||||
@ -53,6 +60,7 @@
|
|||||||
cursor-pointer
|
cursor-pointer
|
||||||
md:hidden md:ml-0
|
md:hidden md:ml-0
|
||||||
hover:bg-gray-100
|
hover:bg-gray-100
|
||||||
|
dark:bg-gray-800 dark:border-gray-500 dark:border
|
||||||
"
|
"
|
||||||
@click.prevent="onToggle"
|
@click.prevent="onToggle"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -15,7 +15,9 @@
|
|||||||
leave-from="opacity-100"
|
leave-from="opacity-100"
|
||||||
leave-to="opacity-0"
|
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>
|
||||||
|
|
||||||
<TransitionChild
|
<TransitionChild
|
||||||
@ -27,7 +29,9 @@
|
|||||||
leave-from="translate-x-0"
|
leave-from="translate-x-0"
|
||||||
leave-to="-translate-x-full"
|
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
|
<TransitionChild
|
||||||
as="template"
|
as="template"
|
||||||
enter="ease-in-out duration-300"
|
enter="ease-in-out duration-300"
|
||||||
@ -40,18 +44,17 @@
|
|||||||
<div class="absolute top-0 right-0 pt-2 -mr-12">
|
<div class="absolute top-0 right-0 pt-2 -mr-12">
|
||||||
<button
|
<button
|
||||||
class="
|
class="
|
||||||
flex
|
flex
|
||||||
items-center
|
items-center
|
||||||
justify-center
|
justify-center
|
||||||
w-10
|
w-10
|
||||||
h-10
|
h-10
|
||||||
ml-1
|
ml-1
|
||||||
rounded-full
|
rounded-full
|
||||||
focus:outline-none
|
focus:outline-none
|
||||||
focus:ring-2
|
focus:ring-2
|
||||||
focus:ring-inset
|
focus:ring-inset
|
||||||
focus:ring-white
|
focus:ring-white"
|
||||||
"
|
|
||||||
@click="globalStore.setSidebarVisibility(false)"
|
@click="globalStore.setSidebarVisibility(false)"
|
||||||
>
|
>
|
||||||
<span class="sr-only">Close sidebar</span>
|
<span class="sr-only">Close sidebar</span>
|
||||||
@ -82,8 +85,8 @@
|
|||||||
:to="item.link"
|
:to="item.link"
|
||||||
:class="[
|
:class="[
|
||||||
hasActiveUrl(item.link)
|
hasActiveUrl(item.link)
|
||||||
? 'text-primary-500 border-primary-500 bg-gray-100 '
|
? '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',
|
: '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',
|
'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)"
|
@click="globalStore.setSidebarVisibility(false)"
|
||||||
@ -100,6 +103,10 @@
|
|||||||
/>
|
/>
|
||||||
{{ $t(item.title) }}
|
{{ $t(item.title) }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<LightDarkSwitch
|
||||||
|
:show-label="false"
|
||||||
|
class="absolute right-6 top-6 !w-auto"
|
||||||
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -113,17 +120,16 @@
|
|||||||
<!-- DESKTOP MENU -->
|
<!-- DESKTOP MENU -->
|
||||||
<div
|
<div
|
||||||
class="
|
class="
|
||||||
hidden
|
hidden
|
||||||
w-56
|
w-56
|
||||||
h-screen
|
h-screen
|
||||||
pb-32
|
bg-white
|
||||||
overflow-y-auto
|
border-r border-gray-200 border-solid
|
||||||
bg-white
|
xl:w-64
|
||||||
border-r border-gray-200 border-solid
|
md:fixed md:flex md:flex-col md:inset-y-0
|
||||||
xl:w-64
|
pt-16
|
||||||
md:fixed md:flex md:flex-col md:inset-y-0
|
dark:border-gray-800
|
||||||
pt-16
|
dark:bg-gray-800/80"
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="menu in globalStore.menuGroups"
|
v-for="menu in globalStore.menuGroups"
|
||||||
@ -136,8 +142,8 @@
|
|||||||
:to="item.link"
|
:to="item.link"
|
||||||
:class="[
|
:class="[
|
||||||
hasActiveUrl(item.link)
|
hasActiveUrl(item.link)
|
||||||
? 'text-primary-500 border-primary-500 bg-gray-100 '
|
? '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',
|
: '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',
|
'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"
|
:name="item.icon"
|
||||||
:class="[
|
:class="[
|
||||||
hasActiveUrl(item.link)
|
hasActiveUrl(item.link)
|
||||||
? 'text-primary-500 group-hover:text-primary-500 '
|
? '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',
|
: 'text-gray-400 group-hover:text-black dark:text-gray-400 dark:group-hover:text-white',
|
||||||
'mr-4 shrink-0 h-5 w-5 ',
|
'mr-4 shrink-0 h-5 w-5 ',
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
@ -154,6 +160,9 @@
|
|||||||
{{ $t(item.title) }}
|
{{ $t(item.title) }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
<LightDarkSwitch
|
||||||
|
class="absolute bottom-0 py-4 border-t border-gray-200 dark:border-gray-700"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -169,6 +178,7 @@ import {
|
|||||||
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||||
|
import LightDarkSwitch from '@/scripts/components/LightDarkSwitcher.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const globalStore = useGlobalStore()
|
const globalStore = useGlobalStore()
|
||||||
|
|||||||
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export const useGlobalStore = (useWindow = false) => {
|
|||||||
isAppLoaded: false,
|
isAppLoaded: false,
|
||||||
isSidebarOpen: false,
|
isSidebarOpen: false,
|
||||||
areCurrenciesLoading: false,
|
areCurrenciesLoading: false,
|
||||||
|
isDarkModeOn: false,
|
||||||
|
|
||||||
downloadReport: null,
|
downloadReport: null,
|
||||||
}),
|
}),
|
||||||
@ -70,8 +71,8 @@ export const useGlobalStore = (useWindow = false) => {
|
|||||||
moduleStore.apiToken = response.data.global_settings.api_token
|
moduleStore.apiToken = response.data.global_settings.api_token
|
||||||
moduleStore.enableModules = response.data.modules
|
moduleStore.enableModules = response.data.modules
|
||||||
|
|
||||||
// company store
|
// company store
|
||||||
companyStore.companies = response.data.companies
|
companyStore.companies = response.data.companies
|
||||||
companyStore.selectedCompany = response.data.current_company
|
companyStore.selectedCompany = response.data.current_company
|
||||||
companyStore.setSelectedCompany(response.data.current_company)
|
companyStore.setSelectedCompany(response.data.current_company)
|
||||||
companyStore.selectedCompanySettings =
|
companyStore.selectedCompanySettings =
|
||||||
|
|||||||
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
101
resources/scripts/components/LightDarkSwitcher.vue
Normal file
101
resources/scripts/components/LightDarkSwitcher.vue
Normal 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>
|
||||||
@ -437,21 +437,22 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
container:
|
container:
|
||||||
'p-0 relative mx-auto w-full flex items-center justify-end box-border cursor-pointer border border-gray-200 rounded-md bg-white text-sm leading-snug outline-none max-h-10',
|
'p-0 relative mx-auto w-full flex items-center justify-end box-border cursor-pointer border border-gray-200 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-sm leading-snug outline-none max-h-10',
|
||||||
containerDisabled:
|
containerDisabled:
|
||||||
'cursor-default bg-gray-200 bg-opacity-50 !text-gray-400',
|
'bg-gray-200 bg-opacity-50 !text-gray-400 dark:!text-gray-800 !dark:text-gray-500 !cursor-default dark:opacity-25',
|
||||||
containerOpen: '',
|
containerOpen: '',
|
||||||
containerOpenTop: '',
|
containerOpenTop: '',
|
||||||
containerActive: 'ring-1 ring-primary-400 border-primary-400',
|
containerActive: 'ring-1 ring-primary-400 border-primary-400',
|
||||||
containerInvalid:
|
containerInvalid:
|
||||||
'border-red-400 ring-red-400 focus:ring-red-400 focus:border-red-400',
|
'border-red-500 ring-red-500 focus:ring-red-500 focus:border-red-500 dark:border-red-500 dark:ring-red-500 dark:focus:ring-red-500 dark:focus:border-red-500',
|
||||||
containerInvalidActive: 'ring-1 border-red-400 ring-red-400',
|
containerInvalidActive:
|
||||||
|
'ring-1 border-red-500 ring-red-500 dark:ring-1 dark:border-red-500 dark:ring-red-500',
|
||||||
singleLabel:
|
singleLabel:
|
||||||
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5',
|
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 dark:text-white',
|
||||||
multipleLabel:
|
multipleLabel:
|
||||||
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5',
|
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 dark:text-white',
|
||||||
search:
|
search:
|
||||||
'w-full absolute inset-0 outline-none appearance-none box-border border-0 text-sm font-sans bg-white rounded-md pl-3.5',
|
'w-full absolute inset-0 outline-none appearance-none box-border border-0 text-sm font-sans bg-white rounded-md pl-3.5 border-transparent focus:border-transparent focus:ring-0 dark:bg-gray-700 dark:text-white',
|
||||||
tags: 'grow shrink flex flex-wrap mt-1 pl-2',
|
tags: 'grow shrink flex flex-wrap mt-1 pl-2',
|
||||||
tag: 'bg-primary-500 text-white text-sm font-semibold py-0.5 pl-2 rounded mr-1 mb-1 flex items-center whitespace-nowrap',
|
tag: 'bg-primary-500 text-white text-sm font-semibold py-0.5 pl-2 rounded mr-1 mb-1 flex items-center whitespace-nowrap',
|
||||||
tagDisabled: 'pr-2 !bg-gray-400 text-white',
|
tagDisabled: 'pr-2 !bg-gray-400 text-white',
|
||||||
@ -461,12 +462,12 @@ export default {
|
|||||||
'bg-multiselect-remove text-white bg-center bg-no-repeat opacity-30 inline-block w-3 h-3 group-hover:opacity-60',
|
'bg-multiselect-remove text-white bg-center bg-no-repeat opacity-30 inline-block w-3 h-3 group-hover:opacity-60',
|
||||||
tagsSearchWrapper: 'inline-block relative mx-1 mb-1 grow shrink h-full',
|
tagsSearchWrapper: 'inline-block relative mx-1 mb-1 grow shrink h-full',
|
||||||
tagsSearch:
|
tagsSearch:
|
||||||
'absolute inset-0 border-0 focus:outline-none !shadow-none !focus:shadow-none appearance-none p-0 text-sm font-sans box-border w-full',
|
'absolute inset-0 border-0 focus:outline-none !shadow-none !focus:shadow-none appearance-none p-0 sm:text-sm font-sans box-border w-full dark:bg-gray-700',
|
||||||
tagsSearchCopy: 'invisible whitespace-pre-wrap inline-block h-px',
|
tagsSearchCopy: 'invisible whitespace-pre-wrap inline-block h-px',
|
||||||
placeholder:
|
placeholder:
|
||||||
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 text-gray-400 text-sm',
|
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 text-gray-400 sm:text-sm dark:text-gray-500',
|
||||||
caret:
|
caret:
|
||||||
'bg-multiselect-caret bg-center bg-no-repeat w-5 h-5 py-px box-content z-5 relative mr-1 opacity-40 shrink-0 grow-0 transition-transform',
|
'bg-multiselect-caret-black dark:bg-multiselect-caret-white bg-center bg-no-repeat w-5 h-5 py-px box-content z-5 relative mr-1 opacity-40 shrink-0 grow-0 transition-transform dark:text-white',
|
||||||
caretOpen: 'rotate-180 pointer-events-auto',
|
caretOpen: 'rotate-180 pointer-events-auto',
|
||||||
clear:
|
clear:
|
||||||
'pr-3.5 relative z-10 opacity-40 transition duration-300 shrink-0 grow-0 flex hover:opacity-80',
|
'pr-3.5 relative z-10 opacity-40 transition duration-300 shrink-0 grow-0 flex hover:opacity-80',
|
||||||
@ -475,7 +476,7 @@ export default {
|
|||||||
spinner:
|
spinner:
|
||||||
'bg-multiselect-spinner bg-center bg-no-repeat w-4 h-4 z-10 mr-3.5 animate-spin shrink-0 grow-0',
|
'bg-multiselect-spinner bg-center bg-no-repeat w-4 h-4 z-10 mr-3.5 animate-spin shrink-0 grow-0',
|
||||||
dropdown:
|
dropdown:
|
||||||
'max-h-60 shadow-lg absolute -left-px -right-px -bottom-1 translate-y-full border border-gray-300 mt-1 overflow-y-auto z-50 bg-white flex flex-col rounded-md',
|
'max-h-60 shadow-lg absolute -left-px -right-px -bottom-1 translate-y-full border border-gray-300 mt-1 overflow-y-auto z-50 bg-white dark:border-gray-600 flex flex-col rounded-md dark:bg-gray-800 dark:shadow-glass',
|
||||||
dropdownTop:
|
dropdownTop:
|
||||||
'-translate-y-full -top-2 bottom-auto flex-col-reverse rounded-md',
|
'-translate-y-full -top-2 bottom-auto flex-col-reverse rounded-md',
|
||||||
dropdownHidden: 'hidden',
|
dropdownHidden: 'hidden',
|
||||||
@ -483,7 +484,7 @@ export default {
|
|||||||
optionsTop: 'flex-col-reverse',
|
optionsTop: 'flex-col-reverse',
|
||||||
group: 'p-0 m-0',
|
group: 'p-0 m-0',
|
||||||
groupLabel:
|
groupLabel:
|
||||||
'flex text-sm box-border items-center justify-start text-left py-1 px-3 font-semibold bg-gray-200 cursor-default leading-normal',
|
'flex text-sm box-border items-center justify-start text-left py-1 px-3 font-semibold bg-gray-200 dark:bg-gray-700 dark:text-gray-400 cursor-default leading-normal',
|
||||||
groupLabelPointable: 'cursor-pointer',
|
groupLabelPointable: 'cursor-pointer',
|
||||||
groupLabelPointed: 'bg-gray-300 text-gray-700',
|
groupLabelPointed: 'bg-gray-300 text-gray-700',
|
||||||
groupLabelSelected: 'bg-primary-600 text-white',
|
groupLabelSelected: 'bg-primary-600 text-white',
|
||||||
@ -493,15 +494,18 @@ export default {
|
|||||||
'text-primary-100 bg-primary-600 bg-opacity-50 cursor-not-allowed',
|
'text-primary-100 bg-primary-600 bg-opacity-50 cursor-not-allowed',
|
||||||
groupOptions: 'p-0 m-0',
|
groupOptions: 'p-0 m-0',
|
||||||
option:
|
option:
|
||||||
'flex items-center justify-start box-border text-left cursor-pointer text-sm leading-snug py-2 px-3',
|
'flex items-center justify-start box-border text-left cursor-pointer text-sm leading-snug py-2 px-3 dark:text-gray-200',
|
||||||
optionPointed: 'text-gray-800 bg-gray-100',
|
optionPointed:
|
||||||
|
'text-gray-800 bg-gray-100 dark:text-white dark:bg-gray-700/30',
|
||||||
optionSelected: 'text-white bg-primary-500',
|
optionSelected: 'text-white bg-primary-500',
|
||||||
optionDisabled: 'text-gray-300 cursor-not-allowed',
|
optionDisabled: 'text-gray-300 cursor-not-allowed dark:text-gray-400',
|
||||||
optionSelectedPointed: 'text-white bg-primary-500 opacity-90',
|
optionSelectedPointed: 'text-white bg-primary-500 opacity-90',
|
||||||
optionSelectedDisabled:
|
optionSelectedDisabled:
|
||||||
'text-primary-100 bg-primary-500 bg-opacity-50 cursor-not-allowed',
|
'text-primary-100 bg-primary-500 bg-opacity-50 cursor-not-allowed',
|
||||||
noOptions: 'py-2 px-3 text-gray-600 bg-white',
|
noOptions:
|
||||||
noResults: 'py-2 px-3 text-gray-600 bg-white',
|
'py-2 px-3 text-gray-600 bg-white dark:bg-gray-700 dark:text-gray-200',
|
||||||
|
noResults:
|
||||||
|
'py-2 px-3 text-gray-600 bg-white dark:bg-gray-700 dark:text-gray-200',
|
||||||
fakeInput:
|
fakeInput:
|
||||||
'bg-transparent absolute left-0 right-0 -bottom-px w-full h-px border-0 p-0 appearance-none outline-none text-transparent',
|
'bg-transparent absolute left-0 right-0 -bottom-px w-full h-px border-0 p-0 appearance-none outline-none text-transparent',
|
||||||
spacer: 'h-9 py-px box-content',
|
spacer: 'h-9 py-px box-content',
|
||||||
|
|||||||
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
<!-- Module Styles -->
|
<!-- 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 }}">
|
<link rel="stylesheet" href="/modules/styles/{{ $name }}">
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
@ -25,8 +25,8 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
class="h-full overflow-hidden bg-gray-100 font-base
|
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 ">
|
@if (isset($current_theme)) theme-{{ $current_theme }} @else theme-{{ get_app_setting('admin_portal_theme') ?? 'crater' }} @endif ">
|
||||||
|
|
||||||
<!-- Module Scripts -->
|
<!-- Module Scripts -->
|
||||||
@foreach (\Crater\Services\Module\ModuleFacade::allScripts() as $name => $path)
|
@foreach (\Crater\Services\Module\ModuleFacade::allScripts() as $name => $path)
|
||||||
@ -38,6 +38,14 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
<script type="module">
|
<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))
|
@if(isset($customer_logo))
|
||||||
|
|
||||||
window.customer_logo = "/storage/{{$customer_logo}}"
|
window.customer_logo = "/storage/{{$customer_logo}}"
|
||||||
@ -57,12 +65,12 @@
|
|||||||
|
|
||||||
window.login_page_description = "{{$login_page_description}}"
|
window.login_page_description = "{{$login_page_description}}"
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
@if(isset($copyright_text))
|
@if(isset($copyright_text))
|
||||||
|
|
||||||
window.copyright_text = "{{$copyright_text}}"
|
window.copyright_text = "{{$copyright_text}}"
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
window.Crater.start()
|
window.Crater.start()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -19,6 +19,7 @@ module.exports = {
|
|||||||
'./resources/scripts/**/*.js',
|
'./resources/scripts/**/*.js',
|
||||||
'./resources/scripts/**/*.vue',
|
'./resources/scripts/**/*.vue',
|
||||||
],
|
],
|
||||||
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
@ -43,10 +44,15 @@ module.exports = {
|
|||||||
88: '22rem',
|
88: '22rem',
|
||||||
},
|
},
|
||||||
backgroundImage: (theme) => ({
|
backgroundImage: (theme) => ({
|
||||||
'multiselect-caret': `url("${svgToDataUri(
|
'multiselect-caret-black': `url("${svgToDataUri(
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
`<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="black">
|
||||||
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
</svg>`
|
</svg>`,
|
||||||
|
)}")`,
|
||||||
|
'multiselect-caret-white': `url("${svgToDataUri(
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="white">
|
||||||
|
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>`,
|
||||||
)}")`,
|
)}")`,
|
||||||
'multiselect-spinner': `url("${svgToDataUri(
|
'multiselect-spinner': `url("${svgToDataUri(
|
||||||
`<svg viewBox="0 0 512 512" fill="${theme(
|
`<svg viewBox="0 0 512 512" fill="${theme(
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user