mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-11-04 06:23:17 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			476 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			476 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<template>
 | 
						|
  <CategoryModal />
 | 
						|
 | 
						|
  <BasePage class="relative">
 | 
						|
    <form action="" @submit.prevent="submitForm">
 | 
						|
      <!-- Page Header -->
 | 
						|
      <BasePageHeader :title="pageTitle" class="mb-5">
 | 
						|
        <BaseBreadcrumb>
 | 
						|
          <BaseBreadcrumbItem
 | 
						|
            :title="$t('general.home')"
 | 
						|
            to="/admin/dashboard"
 | 
						|
          />
 | 
						|
 | 
						|
          <BaseBreadcrumbItem
 | 
						|
            :title="$tc('expenses.expense', 2)"
 | 
						|
            to="/admin/expenses"
 | 
						|
          />
 | 
						|
 | 
						|
          <BaseBreadcrumbItem :title="pageTitle" to="#" active />
 | 
						|
        </BaseBreadcrumb>
 | 
						|
 | 
						|
        <template #actions>
 | 
						|
          <BaseButton
 | 
						|
            v-if="isEdit && expenseStore.currentExpense.attachment_receipt"
 | 
						|
            :href="receiptDownloadUrl"
 | 
						|
            tag="a"
 | 
						|
            variant="primary-outline"
 | 
						|
            type="button"
 | 
						|
            class="mr-2"
 | 
						|
          >
 | 
						|
            <template #left="slotProps">
 | 
						|
              <BaseIcon name="DownloadIcon" :class="slotProps.class" />
 | 
						|
            </template>
 | 
						|
            {{ $t('expenses.download_receipt') }}
 | 
						|
          </BaseButton>
 | 
						|
 | 
						|
          <div class="hidden md:block">
 | 
						|
            <BaseButton
 | 
						|
              :loading="isSaving"
 | 
						|
              :content-loading="isFetchingInitialData"
 | 
						|
              :disabled="isSaving"
 | 
						|
              variant="primary"
 | 
						|
              type="submit"
 | 
						|
            >
 | 
						|
              <template #left="slotProps">
 | 
						|
                <BaseIcon
 | 
						|
                  v-if="!isSaving"
 | 
						|
                  name="SaveIcon"
 | 
						|
                  :class="slotProps.class"
 | 
						|
                />
 | 
						|
              </template>
 | 
						|
              {{
 | 
						|
                isEdit
 | 
						|
                  ? $t('expenses.update_expense')
 | 
						|
                  : $t('expenses.save_expense')
 | 
						|
              }}
 | 
						|
            </BaseButton>
 | 
						|
          </div>
 | 
						|
        </template>
 | 
						|
      </BasePageHeader>
 | 
						|
 | 
						|
      <BaseCard>
 | 
						|
        <BaseInputGrid>
 | 
						|
          <BaseInputGroup
 | 
						|
            :label="$t('expenses.category')"
 | 
						|
            :error="
 | 
						|
              v$.currentExpense.expense_category_id.$error &&
 | 
						|
              v$.currentExpense.expense_category_id.$errors[0].$message
 | 
						|
            "
 | 
						|
            :content-loading="isFetchingInitialData"
 | 
						|
            required
 | 
						|
          >
 | 
						|
            <BaseMultiselect
 | 
						|
              v-model="expenseStore.currentExpense.expense_category_id"
 | 
						|
              :content-loading="isFetchingInitialData"
 | 
						|
              value-prop="id"
 | 
						|
              label="name"
 | 
						|
              track-by="id"
 | 
						|
              :options="searchCategory"
 | 
						|
              :filter-results="false"
 | 
						|
              resolve-on-load
 | 
						|
              :delay="500"
 | 
						|
              searchable
 | 
						|
              :invalid="v$.currentExpense.expense_category_id.$error"
 | 
						|
              :placeholder="$t('expenses.categories.select_a_category')"
 | 
						|
              @input="v$.currentExpense.expense_category_id.$touch()"
 | 
						|
            >
 | 
						|
              <template #action>
 | 
						|
                <BaseSelectAction @click="openCategoryModal">
 | 
						|
                  <BaseIcon
 | 
						|
                    name="PlusIcon"
 | 
						|
                    class="h-4 mr-2 -ml-2 text-center text-primary-400"
 | 
						|
                  />
 | 
						|
                  {{ $t('settings.expense_category.add_new_category') }}
 | 
						|
                </BaseSelectAction>
 | 
						|
              </template>
 | 
						|
            </BaseMultiselect>
 | 
						|
          </BaseInputGroup>
 | 
						|
 | 
						|
          <BaseInputGroup
 | 
						|
            :label="$t('expenses.expense_date')"
 | 
						|
            :error="
 | 
						|
              v$.currentExpense.expense_date.$error &&
 | 
						|
              v$.currentExpense.expense_date.$errors[0].$message
 | 
						|
            "
 | 
						|
            :content-loading="isFetchingInitialData"
 | 
						|
            required
 | 
						|
          >
 | 
						|
            <BaseDatePicker
 | 
						|
              v-model="expenseStore.currentExpense.expense_date"
 | 
						|
              :content-loading="isFetchingInitialData"
 | 
						|
              :calendar-button="true"
 | 
						|
              :invalid="v$.currentExpense.expense_date.$error"
 | 
						|
              @input="v$.currentExpense.expense_date.$touch()"
 | 
						|
            />
 | 
						|
          </BaseInputGroup>
 | 
						|
 | 
						|
          <BaseInputGroup
 | 
						|
            :label="$t('expenses.amount')"
 | 
						|
            :error="
 | 
						|
              v$.currentExpense.amount.$error &&
 | 
						|
              v$.currentExpense.amount.$errors[0].$message
 | 
						|
            "
 | 
						|
            :content-loading="isFetchingInitialData"
 | 
						|
            required
 | 
						|
          >
 | 
						|
            <BaseMoney
 | 
						|
              :key="expenseStore.currentExpense.selectedCurrency"
 | 
						|
              v-model="amountData"
 | 
						|
              class="focus:border focus:border-solid focus:border-primary-500"
 | 
						|
              :invalid="v$.currentExpense.amount.$error"
 | 
						|
              :currency="expenseStore.currentExpense.selectedCurrency"
 | 
						|
              @input="v$.currentExpense.amount.$touch()"
 | 
						|
            />
 | 
						|
          </BaseInputGroup>
 | 
						|
          <BaseInputGroup
 | 
						|
            :label="$t('expenses.currency')"
 | 
						|
            :content-loading="isFetchingInitialData"
 | 
						|
            :error="
 | 
						|
              v$.currentExpense.currency_id.$error &&
 | 
						|
              v$.currentExpense.currency_id.$errors[0].$message
 | 
						|
            "
 | 
						|
            required
 | 
						|
          >
 | 
						|
            <BaseMultiselect
 | 
						|
              v-model="expenseStore.currentExpense.currency_id"
 | 
						|
              value-prop="id"
 | 
						|
              label="name"
 | 
						|
              track-by="name"
 | 
						|
              :content-loading="isFetchingInitialData"
 | 
						|
              :options="globalStore.currencies"
 | 
						|
              searchable
 | 
						|
              :can-deselect="false"
 | 
						|
              :placeholder="$t('customers.select_currency')"
 | 
						|
              :invalid="v$.currentExpense.currency_id.$error"
 | 
						|
              class="w-full"
 | 
						|
              @update:modelValue="onCurrencyChange"
 | 
						|
            >
 | 
						|
            </BaseMultiselect>
 | 
						|
          </BaseInputGroup>
 | 
						|
 | 
						|
          <!-- Exchange rate converter -->
 | 
						|
          <ExchangeRateConverter
 | 
						|
            :store="expenseStore"
 | 
						|
            store-prop="currentExpense"
 | 
						|
            :v="v$.currentExpense"
 | 
						|
            :is-loading="isFetchingInitialData"
 | 
						|
            :is-edit="isEdit"
 | 
						|
            :customer-currency="expenseStore.currentExpense.currency_id"
 | 
						|
          />
 | 
						|
 | 
						|
          <BaseInputGroup
 | 
						|
            :content-loading="isFetchingInitialData"
 | 
						|
            :label="$t('expenses.customer')"
 | 
						|
          >
 | 
						|
            <BaseMultiselect
 | 
						|
              v-model="expenseStore.currentExpense.customer_id"
 | 
						|
              :content-loading="isFetchingInitialData"
 | 
						|
              value-prop="id"
 | 
						|
              label="name"
 | 
						|
              track-by="id"
 | 
						|
              :options="searchCustomer"
 | 
						|
              :filter-results="false"
 | 
						|
              resolve-on-load
 | 
						|
              :delay="500"
 | 
						|
              searchable
 | 
						|
              :placeholder="$t('customers.select_a_customer')"
 | 
						|
            />
 | 
						|
          </BaseInputGroup>
 | 
						|
 | 
						|
          <BaseInputGroup
 | 
						|
            :content-loading="isFetchingInitialData"
 | 
						|
            :label="$t('payments.payment_mode')"
 | 
						|
          >
 | 
						|
            <BaseMultiselect
 | 
						|
              v-model="expenseStore.currentExpense.payment_method_id"
 | 
						|
              :content-loading="isFetchingInitialData"
 | 
						|
              label="name"
 | 
						|
              value-prop="id"
 | 
						|
              track-by="name"
 | 
						|
              :options="expenseStore.paymentModes"
 | 
						|
              :placeholder="$t('payments.select_payment_mode')"
 | 
						|
              searchable
 | 
						|
            >
 | 
						|
              <!-- <template #action>
 | 
						|
                <BaseSelectAction @click="addPaymentMode">
 | 
						|
                  <BaseIcon
 | 
						|
                    name="PlusIcon"
 | 
						|
                    class="h-4 mr-2 -ml-2 text-center text-primary-400"
 | 
						|
                  />
 | 
						|
                  {{ $t('settings.payment_modes.add_payment_mode') }}
 | 
						|
                </BaseSelectAction>
 | 
						|
              </template> -->
 | 
						|
            </BaseMultiselect>
 | 
						|
          </BaseInputGroup>
 | 
						|
 | 
						|
          <BaseInputGroup
 | 
						|
            :content-loading="isFetchingInitialData"
 | 
						|
            :label="$t('expenses.note')"
 | 
						|
            :error="
 | 
						|
              v$.currentExpense.notes.$error &&
 | 
						|
              v$.currentExpense.notes.$errors[0].$message
 | 
						|
            "
 | 
						|
          >
 | 
						|
            <BaseTextarea
 | 
						|
              v-model="expenseStore.currentExpense.notes"
 | 
						|
              :content-loading="isFetchingInitialData"
 | 
						|
              :row="4"
 | 
						|
              rows="4"
 | 
						|
              @input="v$.currentExpense.notes.$touch()"
 | 
						|
            />
 | 
						|
          </BaseInputGroup>
 | 
						|
 | 
						|
          <BaseInputGroup :label="$t('expenses.receipt')">
 | 
						|
            <BaseFileUploader
 | 
						|
              v-model="expenseStore.currentExpense.receiptFiles"
 | 
						|
              accept="image/*,.doc,.docx,.pdf,.csv,.xlsx,.xls"
 | 
						|
              @change="onFileInputChange"
 | 
						|
              @remove="onFileInputRemove"
 | 
						|
            />
 | 
						|
          </BaseInputGroup>
 | 
						|
 | 
						|
          <!-- Expense Custom Fields -->
 | 
						|
          <ExpenseCustomFields
 | 
						|
            :is-edit="isEdit"
 | 
						|
            class="col-span-2"
 | 
						|
            :is-loading="isFetchingInitialData"
 | 
						|
            type="Expense"
 | 
						|
            :store="expenseStore"
 | 
						|
            store-prop="currentExpense"
 | 
						|
            :custom-field-scope="expenseValidationScope"
 | 
						|
          />
 | 
						|
 | 
						|
          <div class="block md:hidden">
 | 
						|
            <BaseButton
 | 
						|
              :loading="isSaving"
 | 
						|
              :tabindex="6"
 | 
						|
              variant="primary"
 | 
						|
              type="submit"
 | 
						|
              class="flex justify-center w-full"
 | 
						|
            >
 | 
						|
              <template #left="slotProps">
 | 
						|
                <BaseIcon
 | 
						|
                  v-if="!isSaving"
 | 
						|
                  name="SaveIcon"
 | 
						|
                  :class="slotProps.class"
 | 
						|
                />
 | 
						|
              </template>
 | 
						|
              {{
 | 
						|
                isEdit
 | 
						|
                  ? $t('expenses.update_expense')
 | 
						|
                  : $t('expenses.save_expense')
 | 
						|
              }}
 | 
						|
            </BaseButton>
 | 
						|
          </div>
 | 
						|
        </BaseInputGrid>
 | 
						|
      </BaseCard>
 | 
						|
    </form>
 | 
						|
  </BasePage>
 | 
						|
</template>
 | 
						|
 | 
						|
<script setup>
 | 
						|
import { ref, computed, onMounted } from 'vue'
 | 
						|
import { useRoute, useRouter } from 'vue-router'
 | 
						|
import { useI18n } from 'vue-i18n'
 | 
						|
import {
 | 
						|
  required,
 | 
						|
  minValue,
 | 
						|
  maxLength,
 | 
						|
  helpers,
 | 
						|
  requiredIf,
 | 
						|
  decimal,
 | 
						|
} from '@vuelidate/validators'
 | 
						|
import useVuelidate from '@vuelidate/core'
 | 
						|
import { useExpenseStore } from '@/scripts/stores/expense'
 | 
						|
import { useCategoryStore } from '@/scripts/stores/category'
 | 
						|
import { useCompanyStore } from '@/scripts/stores/company'
 | 
						|
import { useCustomerStore } from '@/scripts/stores/customer'
 | 
						|
import { useCustomFieldStore } from '@/scripts/stores/custom-field'
 | 
						|
import { useModalStore } from '@/scripts/stores/modal'
 | 
						|
import ExpenseCustomFields from '@/scripts/components/custom-fields/CreateCustomFields.vue'
 | 
						|
import CategoryModal from '@/scripts/components/modal-components/CategoryModal.vue'
 | 
						|
import ExchangeRateConverter from '@/scripts/components/estimate-invoice-common/ExchangeRateConverter.vue'
 | 
						|
import { useGlobalStore } from '@/scripts/stores/global'
 | 
						|
 | 
						|
const customerStore = useCustomerStore()
 | 
						|
const companyStore = useCompanyStore()
 | 
						|
const expenseStore = useExpenseStore()
 | 
						|
const categoryStore = useCategoryStore()
 | 
						|
const customFieldStore = useCustomFieldStore()
 | 
						|
const modalStore = useModalStore()
 | 
						|
const route = useRoute()
 | 
						|
const router = useRouter()
 | 
						|
const { t } = useI18n()
 | 
						|
const globalStore = useGlobalStore()
 | 
						|
 | 
						|
let isSaving = ref(false)
 | 
						|
let isFetchingInitialData = ref(false)
 | 
						|
const expenseValidationScope = 'newExpense'
 | 
						|
 | 
						|
const rules = computed(() => {
 | 
						|
  return {
 | 
						|
    currentExpense: {
 | 
						|
      expense_category_id: {
 | 
						|
        required: helpers.withMessage(t('validation.required'), required),
 | 
						|
      },
 | 
						|
      expense_date: {
 | 
						|
        required: helpers.withMessage(t('validation.required'), required),
 | 
						|
      },
 | 
						|
 | 
						|
      amount: {
 | 
						|
        required: helpers.withMessage(t('validation.required'), required),
 | 
						|
        minValue: helpers.withMessage(
 | 
						|
          t('validation.price_minvalue'),
 | 
						|
          minValue(0.1)
 | 
						|
        ),
 | 
						|
        maxLength: helpers.withMessage(
 | 
						|
          t('validation.price_maxlength'),
 | 
						|
          maxLength(20)
 | 
						|
        ),
 | 
						|
      },
 | 
						|
 | 
						|
      notes: {
 | 
						|
        maxLength: helpers.withMessage(
 | 
						|
          t('validation.description_maxlength'),
 | 
						|
          maxLength(65000)
 | 
						|
        ),
 | 
						|
      },
 | 
						|
      currency_id: {
 | 
						|
        required: helpers.withMessage(t('validation.required'), required),
 | 
						|
      },
 | 
						|
      exchange_rate: {
 | 
						|
        required: requiredIf(function () {
 | 
						|
          helpers.withMessage(t('validation.required'), required)
 | 
						|
          return expenseStore.showExchangeRate
 | 
						|
        }),
 | 
						|
        decimal: helpers.withMessage(
 | 
						|
          t('validation.valid_exchange_rate'),
 | 
						|
          decimal
 | 
						|
        ),
 | 
						|
      },
 | 
						|
    },
 | 
						|
  }
 | 
						|
})
 | 
						|
 | 
						|
const v$ = useVuelidate(rules, expenseStore, {
 | 
						|
  $scope: expenseValidationScope,
 | 
						|
})
 | 
						|
 | 
						|
const amountData = computed({
 | 
						|
  get: () => expenseStore.currentExpense.amount / 100,
 | 
						|
  set: (value) => {
 | 
						|
    expenseStore.currentExpense.amount = Math.round(value * 100)
 | 
						|
  },
 | 
						|
})
 | 
						|
 | 
						|
const isEdit = computed(() => route.name === 'expenses.edit')
 | 
						|
 | 
						|
const pageTitle = computed(() =>
 | 
						|
  isEdit.value ? t('expenses.edit_expense') : t('expenses.new_expense')
 | 
						|
)
 | 
						|
 | 
						|
const receiptDownloadUrl = computed(() =>
 | 
						|
  isEdit.value ? `/expenses/${route.params.id}/download-receipt` : ''
 | 
						|
)
 | 
						|
 | 
						|
expenseStore.resetCurrentExpenseData()
 | 
						|
customFieldStore.resetCustomFields()
 | 
						|
 | 
						|
loadData()
 | 
						|
 | 
						|
function onFileInputChange(fileName, file) {
 | 
						|
  expenseStore.currentExpense.attachment_receipt = file
 | 
						|
}
 | 
						|
 | 
						|
function onFileInputRemove() {
 | 
						|
  expenseStore.currentExpense.attachment_receipt = null
 | 
						|
}
 | 
						|
 | 
						|
function openCategoryModal() {
 | 
						|
  modalStore.openModal({
 | 
						|
    title: t('settings.expense_category.add_category'),
 | 
						|
    componentName: 'CategoryModal',
 | 
						|
    size: 'sm',
 | 
						|
  })
 | 
						|
}
 | 
						|
 | 
						|
function onCurrencyChange(v) {
 | 
						|
  expenseStore.currentExpense.selectedCurrency = globalStore.currencies.find(
 | 
						|
    (c) => c.id === v
 | 
						|
  )
 | 
						|
}
 | 
						|
 | 
						|
async function searchCategory(search) {
 | 
						|
  let res = await categoryStore.fetchCategories({ search })
 | 
						|
  return res.data.data
 | 
						|
}
 | 
						|
 | 
						|
async function searchCustomer(search) {
 | 
						|
  let res = await customerStore.fetchCustomers({ search })
 | 
						|
  return res.data.data
 | 
						|
}
 | 
						|
 | 
						|
async function loadData() {
 | 
						|
  if (!isEdit.value) {
 | 
						|
    expenseStore.currentExpense.currency_id =
 | 
						|
      companyStore.selectedCompanyCurrency.id
 | 
						|
    expenseStore.currentExpense.selectedCurrency =
 | 
						|
      companyStore.selectedCompanyCurrency
 | 
						|
  }
 | 
						|
 | 
						|
  isFetchingInitialData.value = true
 | 
						|
  await expenseStore.fetchPaymentModes({ limit: 'all' })
 | 
						|
 | 
						|
  if (isEdit.value) {
 | 
						|
    await expenseStore.fetchExpense(route.params.id)
 | 
						|
 | 
						|
    expenseStore.currentExpense.currency_id =
 | 
						|
      expenseStore.currentExpense.selectedCurrency.id
 | 
						|
  } else if (route.query.customer) {
 | 
						|
    expenseStore.currentExpense.customer_id = route.query.customer
 | 
						|
  }
 | 
						|
 | 
						|
  isFetchingInitialData.value = false
 | 
						|
}
 | 
						|
 | 
						|
async function submitForm() {
 | 
						|
  v$.value.$touch()
 | 
						|
 | 
						|
  if (v$.value.$invalid) {
 | 
						|
    return
 | 
						|
  }
 | 
						|
 | 
						|
  isSaving.value = true
 | 
						|
 | 
						|
  let formData = expenseStore.currentExpense
 | 
						|
 | 
						|
  try {
 | 
						|
    if (isEdit.value) {
 | 
						|
      await expenseStore.updateExpense({
 | 
						|
        id: route.params.id,
 | 
						|
        data: formData,
 | 
						|
      })
 | 
						|
    } else {
 | 
						|
      await expenseStore.addExpense(formData)
 | 
						|
    }
 | 
						|
    isSaving.value = false
 | 
						|
    router.push('/admin/expenses')
 | 
						|
  } catch (err) {
 | 
						|
    console.error(err)
 | 
						|
    isSaving.value = false
 | 
						|
    return
 | 
						|
  }
 | 
						|
}
 | 
						|
</script>
 |