diff --git a/app/Http/Requests/CompaniesRequest.php b/app/Http/Requests/CompaniesRequest.php index 5394592b..661d8777 100644 --- a/app/Http/Requests/CompaniesRequest.php +++ b/app/Http/Requests/CompaniesRequest.php @@ -3,7 +3,6 @@ namespace Crater\Http\Requests; use Illuminate\Foundation\Http\FormRequest; -use Illuminate\Support\Str; use Illuminate\Validation\Rule; class CompaniesRequest extends FormRequest @@ -34,6 +33,10 @@ class CompaniesRequest extends FormRequest 'currency' => [ 'required' ], + 'slug' => [ + 'required', + Rule::unique('companies') + ], 'address.name' => [ 'nullable', ], @@ -68,11 +71,11 @@ class CompaniesRequest extends FormRequest { return collect($this->validated()) ->only([ - 'name' + 'name', + 'slug' ]) ->merge([ - 'owner_id' => $this->user()->id, - 'slug' => Str::slug($this->name) + 'owner_id' => $this->user()->id ]) ->toArray(); } diff --git a/app/Http/Requests/CompanyRequest.php b/app/Http/Requests/CompanyRequest.php index c86cd645..dc22b1ae 100644 --- a/app/Http/Requests/CompanyRequest.php +++ b/app/Http/Requests/CompanyRequest.php @@ -30,7 +30,8 @@ class CompanyRequest extends FormRequest Rule::unique('companies')->ignore($this->header('company'), 'id'), ], 'slug' => [ - 'nullable' + 'required', + Rule::unique('companies')->ignore($this->header('company'), 'id'), ], 'address.country_id' => [ 'required', diff --git a/app/Models/Company.php b/app/Models/Company.php index bf87820b..a0d9fddb 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -217,7 +217,7 @@ class Company extends Model implements HasMedia 'estimate_billing_address_format' => $billingAddressFormat, 'payment_company_address_format' => $companyAddressFormat, 'payment_from_customer_address_format' => $paymentFromCustomerAddress, - 'currency' => request()->currency ?? 13, + 'currency' => request()->currency ?? 1, 'time_zone' => 'Asia/Kolkata', 'language' => 'en', 'fiscal_year' => '1-12', diff --git a/resources/scripts/admin/components/modal-components/CompanyModal.vue b/resources/scripts/admin/components/modal-components/CompanyModal.vue index 019a065c..011218a1 100644 --- a/resources/scripts/admin/components/modal-components/CompanyModal.vue +++ b/resources/scripts/admin/components/modal-components/CompanyModal.vue @@ -48,6 +48,24 @@ /> + + + + import { useModalStore } from '@/scripts/stores/modal' -import { computed, onMounted, ref, reactive } from 'vue' +import { computed, onMounted, ref, reactive, watch } from 'vue' import { useI18n } from 'vue-i18n' import { required, minLength, helpers } from '@vuelidate/validators' import { useVuelidate } from '@vuelidate/core' @@ -152,6 +170,7 @@ let companyLogoName = ref(null) const newCompanyForm = reactive({ name: null, + slug: null, currency: '', address: { country_id: null, @@ -162,6 +181,9 @@ const modalActive = computed(() => { return modalStore.active && modalStore.componentName === 'CompanyModal' }) +const slugValidator = (value) => { + return value == slugify(value) +} const rules = { newCompanyForm: { name: { @@ -171,6 +193,17 @@ const rules = { minLength(3) ), }, + slug: { + required: helpers.withMessage(t('validation.required'), required), + minLength: helpers.withMessage( + t('validation.name_min_length', { count: 3 }), + minLength(3) + ), + slugValidator: helpers.withMessage( + t('validation.invalid_slug'), + slugValidator + ), + }, address: { country_id: { required: helpers.withMessage(t('validation.required'), required), @@ -243,6 +276,7 @@ async function submitCompanyData() { function resetNewCompanyForm() { newCompanyForm.name = '' + newCompanyForm.slug = '' newCompanyForm.currency = '' newCompanyForm.address.country_id = '' @@ -257,4 +291,24 @@ function closeCompanyModal() { v$.value.$reset() }, 300) } + +// watcher for if change company name then auto fill company slug value +watch( + () => newCompanyForm.name, + (currentValue) => { + newCompanyForm.slug = slugify(currentValue) + } +) + +function slugify(string) { + return string + .toString() + .trim() + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^\w\-]+/g, '') + .replace(/\-\-+/g, '-') + .replace(/^-+/, '') + .replace(/-+$/, '') +} diff --git a/resources/scripts/admin/stores/company.js b/resources/scripts/admin/stores/company.js index 1e10489b..f5462ae0 100644 --- a/resources/scripts/admin/stores/company.js +++ b/resources/scripts/admin/stores/company.js @@ -184,6 +184,20 @@ export const useCompanyStore = (useWindow = false) => { setDefaultCurrency(data) { this.defaultCurrency = data.currency }, + + checkCompanyHasCurrencyTransactions() { + return new Promise((resolve, reject) => { + axios + .get(`/api/v1/company/has-transactions`) + .then((response) => { + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + }) + }, }, })() } diff --git a/resources/scripts/admin/views/installation/Step7CompanyInfo.vue b/resources/scripts/admin/views/installation/Step7CompanyInfo.vue index 3890ccbd..9cd2f0c4 100644 --- a/resources/scripts/admin/views/installation/Step7CompanyInfo.vue +++ b/resources/scripts/admin/views/installation/Step7CompanyInfo.vue @@ -34,6 +34,24 @@ /> + + + + - -
diff --git a/resources/scripts/admin/views/settings/CompanyInfoSettings.vue b/resources/scripts/admin/views/settings/CompanyInfoSettings.vue index 357b7dd6..e55880e5 100644 --- a/resources/scripts/admin/views/settings/CompanyInfoSettings.vue +++ b/resources/scripts/admin/views/settings/CompanyInfoSettings.vue @@ -28,6 +28,19 @@ /> + + + + @@ -160,6 +173,7 @@ let isSaving = ref(false) const companyForm = reactive({ name: null, + slug: null, logo: null, address: { address_street_1: '', @@ -193,7 +207,14 @@ const rules = computed(() => { name: { required: helpers.withMessage(t('validation.required'), required), minLength: helpers.withMessage( - t('validation.name_min_length'), + t('validation.name_min_length', { count: 3 }), + minLength(3) + ), + }, + slug: { + required: helpers.withMessage(t('validation.required'), required), + minLength: helpers.withMessage( + t('validation.name_min_length', { count: 3 }), minLength(3) ), }, diff --git a/resources/scripts/admin/views/settings/PreferencesSetting.vue b/resources/scripts/admin/views/settings/PreferencesSetting.vue index 6c46f645..531cb83d 100644 --- a/resources/scripts/admin/views/settings/PreferencesSetting.vue +++ b/resources/scripts/admin/views/settings/PreferencesSetting.vue @@ -8,7 +8,11 @@ @@ -21,7 +25,7 @@ :searchable="true" track-by="name" :invalid="v$.currency.$error" - disabled + :disabled="isCurrencyDisabled" class="w-full" > @@ -187,6 +191,7 @@ const { t, tm } = useI18n() let isSaving = ref(false) let isDataSaving = ref(false) let isFetchingInitialData = ref(false) +let isCurrencyDisabled = ref(true) const settingsForm = reactive({ ...companyStore.selectedCompanySettings }) @@ -282,10 +287,14 @@ setInitialData() async function setInitialData() { isFetchingInitialData.value = true Promise.all([ + companyStore.checkCompanyHasCurrencyTransactions(), globalStore.fetchCurrencies(), globalStore.fetchDateFormats(), globalStore.fetchTimeZones(), ]).then(([res1]) => { + if (res1.data?.has_transactions == false) { + isCurrencyDisabled.value = false + } isFetchingInitialData.value = false }) } diff --git a/resources/scripts/locales/en.json b/resources/scripts/locales/en.json index 5c224428..a462b9fb 100644 --- a/resources/scripts/locales/en.json +++ b/resources/scripts/locales/en.json @@ -863,6 +863,8 @@ "company_info": { "company_info": "Company info", "company_name": "Company Name", + "company_slug": "Company Slug", + "company_slug_help_text": "A unique URL friendly name for your company (It will appear on Customer Portal URL)", "company_logo": "Company Logo", "section_description": "Information about your company that will be displayed on invoices, estimates and other documents created by Crater.", "phone": "Phone", @@ -1324,6 +1326,8 @@ "company_info": "Company Information", "company_info_desc": "This information will be displayed on invoices. Note that you can edit this later on settings page.", "company_name": "Company Name", + "company_slug": "Company Slug", + "company_slug_help_text": "A unique URL friendly name for your company (It will appear on Customer Portal URL)", "company_logo": "Company Logo", "logo_preview": "Logo Preview", "preferences": "Company Preferences", @@ -1454,7 +1458,8 @@ "at_least_one_ability": "Please select atleast one Permission.", "valid_driver_key": "Please enter a valid {driver} key.", "valid_exchange_rate": "Please enter a valid exchange rate.", - "company_name_not_same": "Company name must match with given name." + "company_name_not_same": "Company name must match with given name.", + "invalid_slug": "Invalid Slug" }, "errors": { "starter_plan": "This feature is available on Starter plan and onwards!", @@ -1523,4 +1528,4 @@ "pdf_ship_to": "Ship to,", "pdf_received_from": "Received from:", "pdf_tax_label": "Tax" -} +} \ No newline at end of file diff --git a/tests/Feature/Admin/EstimateTest.php b/tests/Feature/Admin/EstimateTest.php index c726c043..0a06f83d 100644 --- a/tests/Feature/Admin/EstimateTest.php +++ b/tests/Feature/Admin/EstimateTest.php @@ -415,32 +415,31 @@ test('update estimate with EUR currency', function () { $response = putJson('api/v1/estimates/'.$estimate->id, $estimate2); - $this->assertDatabaseHas('estimates', [ - 'id' => $estimate['id'], - 'template_name' => $estimate2['template_name'], - 'estimate_number' => $estimate2['estimate_number'], - 'discount_type' => $estimate2['discount_type'], - 'discount_val' => $estimate2['discount_val'], - 'sub_total' => $estimate2['sub_total'], - 'discount' => $estimate2['discount'], - 'customer_id' => $estimate2['customer_id'], - 'total' => $estimate2['total'], - 'tax' => $estimate2['tax'], - 'exchange_rate' => $estimate2['exchange_rate'], - 'base_discount_val' => $estimate2['base_discount_val'], - 'base_sub_total' => $estimate2['base_sub_total'], - 'base_total' => $estimate2['base_total'], - 'base_tax' => $estimate2['base_tax'], - ]); + $estimate_assert = collect($estimate2) + ->only([ + 'id', + 'template_name', + 'estimate_number', + 'discount_type', + 'discount_val', + 'sub_total', + 'discount', + 'customer_id', + 'total', + 'tax' + ]) + ->toArray(); - $this->assertDatabaseHas('estimate_items', [ - 'estimate_id' => $estimate2['items'][0]['estimate_id'], - 'exchange_rate' => $estimate2['items'][0]['exchange_rate'], - 'base_price' => $estimate2['items'][0]['base_price'], - 'base_discount_val' => $estimate2['items'][0]['base_discount_val'], - 'base_tax' => $estimate2['items'][0]['base_tax'], - 'base_total' => $estimate2['items'][0]['base_total'], - ]); + $this->assertDatabaseHas('estimates', $estimate_assert); + + $estimate_item_assert = collect($estimate2['items'][0]) + ->only([ + 'estimate_id', + 'amount' + ]) + ->toArray(); + + $this->assertDatabaseHas('estimate_items', $estimate_item_assert); $response->assertStatus(200); }); diff --git a/tests/Feature/Admin/ExpenseTest.php b/tests/Feature/Admin/ExpenseTest.php index 0e46c0f4..38a5c8dd 100644 --- a/tests/Feature/Admin/ExpenseTest.php +++ b/tests/Feature/Admin/ExpenseTest.php @@ -37,13 +37,15 @@ test('create expense', function () { postJson('api/v1/expenses', $expense)->assertStatus(201); - $this->assertDatabaseHas('expenses', [ - 'notes' => $expense['notes'], - 'expense_category_id' => $expense['expense_category_id'], - 'amount' => $expense['amount'], - 'exchange_rate' => $expense['exchange_rate'], - 'base_amount' => $expense['base_amount'], - ]); + $expense = collect($expense) + ->only([ + 'notes', + 'expense_category_id', + 'amount' + ]) + ->toArray(); + + $this->assertDatabaseHas('expenses', $expense); }); test('store validates using a form request', function () { @@ -146,11 +148,13 @@ test('update expense with EUR currency', function () { putJson('api/v1/expenses/'.$expense->id, $expense2)->assertOk(); - $this->assertDatabaseHas('expenses', [ - 'id' => $expense->id, - 'expense_category_id' => $expense2['expense_category_id'], - 'amount' => $expense2['amount'], - 'exchange_rate' => $expense2['exchange_rate'], - 'base_amount' => $expense2['base_amount'], - ]); + $expense2 = collect($expense2) + ->only([ + 'id', + 'expense_category_id', + 'amount' + ]) + ->toArray(); + + $this->assertDatabaseHas('expenses', $expense2); }); diff --git a/tests/Feature/Admin/InvoiceTest.php b/tests/Feature/Admin/InvoiceTest.php index ef8a716e..dce605a5 100644 --- a/tests/Feature/Admin/InvoiceTest.php +++ b/tests/Feature/Admin/InvoiceTest.php @@ -9,7 +9,6 @@ use Crater\Models\Tax; use Crater\Models\User; use Illuminate\Support\Facades\Artisan; use Laravel\Sanctum\Sanctum; - use function Pest\Laravel\getJson; use function Pest\Laravel\postJson; use function Pest\Laravel\putJson; @@ -431,31 +430,36 @@ test('update invoice with EUR currency', function () { putJson('api/v1/invoices/'.$invoice->id, $invoice2)->assertOk(); - $this->assertDatabaseHas('invoices', [ - 'id' => $invoice['id'], - 'invoice_number' => $invoice2['invoice_number'], - 'sub_total' => $invoice2['sub_total'], - 'total' => $invoice2['total'], - 'tax' => $invoice2['tax'], - 'discount' => $invoice2['discount'], - 'customer_id' => $invoice2['customer_id'], - 'template_name' => $invoice2['template_name'], - 'exchange_rate' => $invoice2['exchange_rate'], - 'base_total' => $invoice2['base_total'], - ]); + $invoice_assert = collect($invoice2) + ->only([ + 'invoice_number', + 'template_name', + 'sub_total', + 'total', + 'tax', + 'discount', + 'customer_id', + ]) + ->toArray(); - $this->assertDatabaseHas('invoice_items', [ - 'invoice_id' => $invoice2['items'][0]['invoice_id'], - 'item_id' => $invoice2['items'][0]['item_id'], - 'name' => $invoice2['items'][0]['name'], - 'exchange_rate' => $invoice2['items'][0]['exchange_rate'], - 'base_price' => $invoice2['items'][0]['base_price'], - 'base_total' => $invoice2['items'][0]['base_total'], - ]); + $this->assertDatabaseHas('invoices', $invoice_assert); - $this->assertDatabaseHas('taxes', [ - 'amount' => $invoice2['taxes'][0]['amount'], - 'name' => $invoice2['taxes'][0]['name'], - 'base_amount' => $invoice2['taxes'][0]['base_amount'], - ]); + $invoice_item_assert = collect($invoice2['items'][0]) + ->only([ + 'invoice_id', + 'item_id', + 'name', + ]) + ->toArray(); + + $this->assertDatabaseHas('invoice_items', $invoice_item_assert); + + $invoice_tax_assert = collect($invoice2['taxes'][0]) + ->only([ + 'name', + 'amount' + ]) + ->toArray(); + + $this->assertDatabaseHas('taxes', $invoice_tax_assert); });