v5.0.0 update

This commit is contained in:
Mohit Panjwani
2021-11-30 18:58:19 +05:30
parent d332712c22
commit 082d5cacf2
1253 changed files with 88309 additions and 71741 deletions

View File

@ -0,0 +1,513 @@
<template>
<tr class="box-border bg-white border border-gray-200 border-solid rounded-b">
<td colspan="5" class="p-0 text-left align-top">
<table class="w-full">
<colgroup>
<col style="width: 40%; min-width: 280px" />
<col style="width: 10%; min-width: 120px" />
<col style="width: 15%; min-width: 120px" />
<col
v-if="store[storeProp].discount_per_item === 'YES'"
style="width: 15%; min-width: 160px"
/>
<col style="width: 15%; min-width: 120px" />
</colgroup>
<tbody>
<tr>
<td class="px-5 py-4 text-left align-top">
<div class="flex justify-start">
<div
class="
flex
items-center
justify-center
w-5
h-5
mt-2
text-gray-300
cursor-move
handle
mr-2
"
>
<DragIcon />
</div>
<BaseItemSelect
type="Invoice"
:item="itemData"
:invalid="v$.name.$error"
:invalid-description="v$.description.$error"
:taxes="itemData.taxes"
:index="index"
:store-prop="storeProp"
:store="store"
@search="searchVal"
@select="onSelectItem"
/>
</div>
</td>
<td class="px-5 py-4 text-right align-top">
<BaseInput
v-model="quantity"
:invalid="v$.quantity.$error"
:content-loading="loading"
type="number"
small
min="1"
@change="syncItemToStore()"
@input="v$.quantity.$touch()"
/>
</td>
<td class="px-5 py-4 text-left align-top">
<div class="flex flex-col">
<div class="flex-auto flex-fill bd-highlight">
<div class="relative w-full">
<BaseMoney
:key="selectedCurrency"
v-model="price"
:invalid="v$.price.$error"
:content-loading="loading"
:currency="selectedCurrency"
/>
</div>
</div>
</div>
</td>
<td
v-if="store[storeProp].discount_per_item === 'YES'"
class="px-5 py-4 text-left align-top"
>
<div class="flex flex-col">
<div class="flex" style="width: 120px" role="group">
<BaseInput
v-model="discount"
:invalid="v$.discount_val.$error"
:content-loading="loading"
class="
border-r-0
focus:border-r-2
rounded-tr-sm rounded-br-sm
h-[38px]
"
/>
<BaseDropdown position="bottom-end">
<template #activator>
<BaseButton
:content-loading="loading"
class="rounded-tr-md rounded-br-md !p-2 rounded-none"
type="button"
variant="white"
>
<span class="flex items-center">
{{
itemData.discount_type == 'fixed'
? currency.symbol
: '%'
}}
<BaseIcon
name="ChevronDownIcon"
class="w-4 h-4 text-gray-500 ml-1"
/>
</span>
</BaseButton>
</template>
<BaseDropdownItem @click="selectFixed">
{{ $t('general.fixed') }}
</BaseDropdownItem>
<BaseDropdownItem @click="selectPercentage">
{{ $t('general.percentage') }}
</BaseDropdownItem>
</BaseDropdown>
</div>
</div>
</td>
<td class="px-5 py-4 text-right align-top">
<div class="flex items-center justify-end text-sm">
<span>
<BaseContentPlaceholders v-if="loading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<BaseFormatMoney
v-else
:amount="total"
:currency="selectedCurrency"
/>
</span>
<div class="flex items-center justify-center w-6 h-10 mx-2">
<BaseIcon
v-if="showRemoveButton"
class="h-5 text-gray-700 cursor-pointer"
name="TrashIcon"
@click="store.removeItem(index)"
/>
</div>
</div>
</td>
</tr>
<tr v-if="store[storeProp].tax_per_item === 'YES'">
<td class="px-5 py-4 text-left align-top" />
<td colspan="4" class="px-5 py-4 text-left align-top">
<BaseContentPlaceholders v-if="loading">
<BaseContentPlaceholdersText
:lines="1"
class="w-24 h-8 rounded-md border"
/>
</BaseContentPlaceholders>
<ItemTax
v-for="(tax, index1) in itemData.taxes"
v-else
:key="tax.id"
:index="index1"
:item-index="index"
:tax-data="tax"
:taxes="itemData.taxes"
:discounted-total="total"
:total-tax="totalSimpleTax"
:total="subtotal"
:currency="currency"
:update-items="syncItemToStore"
:ability="abilities.CREATE_INVOICE"
:store="store"
:store-prop="storeProp"
@update="updateTax"
/>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</template>
<script setup>
import { computed, ref, inject } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import Guid from 'guid'
import TaxStub from '@/scripts/stub/tax'
import ItemTax from './CreateItemRowTax.vue'
import { sumBy } from 'lodash'
import abilities from '@/scripts/stub/abilities'
import {
required,
between,
maxLength,
helpers,
minValue,
} from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useCompanyStore } from '@/scripts/stores/company'
import { useItemStore } from '@/scripts/stores/item'
import DragIcon from '@/scripts/components/icons/DragIcon.vue'
const props = defineProps({
store: {
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
itemData: {
type: Object,
default: null,
},
index: {
type: Number,
default: null,
},
type: {
type: String,
default: '',
},
loading: {
type: Boolean,
default: false,
},
currency: {
type: [Object, String],
required: true,
},
invoiceItems: {
type: Array,
required: true,
},
itemValidationScope: {
type: String,
default: '',
},
})
const emit = defineEmits(['update', 'remove', 'itemValidate'])
const companyStore = useCompanyStore()
const itemStore = useItemStore()
let route = useRoute()
const { t } = useI18n()
const quantity = computed({
get: () => {
return props.itemData.quantity
},
set: (newValue) => {
updateItemAttribute('quantity', parseInt(newValue))
},
})
const price = computed({
get: () => {
const price = props.itemData.price
if (parseFloat(price) > 0) {
return price / 100
}
return price
},
set: (newValue) => {
if (parseFloat(newValue) > 0) {
let price = Math.round(newValue * 100)
updateItemAttribute('price', price)
} else {
updateItemAttribute('price', newValue)
}
},
})
const subtotal = computed(() => props.itemData.price * props.itemData.quantity)
const discount = computed({
get: () => {
return props.itemData.discount
},
set: (newValue) => {
if (props.itemData.discount_type === 'percentage') {
updateItemAttribute('discount_val', (subtotal.value * newValue) / 100)
} else {
updateItemAttribute('discount_val', Math.round(newValue * 100))
}
updateItemAttribute('discount', newValue)
},
})
const total = computed(() => {
return subtotal.value - props.itemData.discount_val
})
const selectedCurrency = computed(() => {
if (props.currency) {
return props.currency
} else {
return companyStore.selectedCompanyCurrency
}
})
const showRemoveButton = computed(() => {
if (props.store[props.storeProp].items.length == 1) {
return false
}
return true
})
const totalSimpleTax = computed(() => {
return Math.round(
sumBy(props.itemData.taxes, function (tax) {
if (!tax.compound_tax) {
return tax.amount
}
return 0
})
)
})
const totalCompoundTax = computed(() => {
return Math.round(
sumBy(props.itemData.taxes, function (tax) {
if (tax.compound_tax) {
return tax.amount
}
return 0
})
)
})
const totalTax = computed(() => totalSimpleTax.value + totalCompoundTax.value)
const rules = {
name: {
required: helpers.withMessage(t('validation.required'), required),
},
quantity: {
required: helpers.withMessage(t('validation.required'), required),
minValue: helpers.withMessage(
t('validation.qty_must_greater_than_zero'),
minValue(1)
),
maxLength: helpers.withMessage(
t('validation.amount_maxlength'),
maxLength(20)
),
},
price: {
required: helpers.withMessage(t('validation.required'), required),
minValue: helpers.withMessage(
t('validation.number_length_minvalue'),
minValue(1)
),
maxLength: helpers.withMessage(
t('validation.price_maxlength'),
maxLength(20)
),
},
discount_val: {
between: helpers.withMessage(
t('validation.discount_maxlength'),
between(
0,
computed(() => subtotal.value)
)
),
},
description: {
maxLength: helpers.withMessage(
t('validation.notes_maxlength'),
maxLength(65000)
),
},
}
const v$ = useVuelidate(
rules,
computed(() => props.store[props.storeProp].items[props.index]),
{ $scope: props.itemValidationScope }
)
//
// if (
// route.params.id &&
// (props.store[props.storeProp].tax_per_item === 'YES' || 'NO')
// ) {
// if (props.store[props.storeProp].items[props.index].taxes === undefined) {
// props.store.$patch((state) => {
// state[props.storeProp].items[props.index].taxes = [
// { ...TaxStub, id: Guid.raw() },
// ]
// })
// }
// }
function updateTax(data) {
props.store.$patch((state) => {
state[props.storeProp].items[props.index]['taxes'][data.index] = data.item
})
let lastTax = props.itemData.taxes[props.itemData.taxes.length - 1]
if (lastTax?.tax_type_id !== 0) {
props.store.$patch((state) => {
state[props.storeProp].items[props.index].taxes.push({
...TaxStub,
id: Guid.raw(),
})
})
}
syncItemToStore()
}
function searchVal(val) {
updateItemAttribute('name', val)
}
function onSelectItem(itm) {
props.store.$patch((state) => {
state[props.storeProp].items[props.index].name = itm.name
state[props.storeProp].items[props.index].price = itm.price
state[props.storeProp].items[props.index].item_id = itm.id
state[props.storeProp].items[props.index].description = itm.description
if (itm.unit) {
state[props.storeProp].items[props.index].unit_name = itm.unit.name
}
if (props.store[props.storeProp].tax_per_item === 'YES' && itm.taxes) {
let index = 0
itm.taxes.forEach((tax) => {
updateTax({ index, item: { ...tax } })
index++
})
}
if (state[props.storeProp].exchange_rate) {
state[props.storeProp].items[props.index].price /=
state[props.storeProp].exchange_rate
}
})
itemStore.fetchItems()
syncItemToStore()
}
function selectFixed() {
if (props.itemData.discount_type === 'fixed') {
return
}
updateItemAttribute('discount_val', Math.round(props.itemData.discount * 100))
updateItemAttribute('discount_type', 'fixed')
}
function selectPercentage() {
if (props.itemData.discount_type === 'percentage') {
return
}
updateItemAttribute(
'discount_val',
(subtotal.value * props.itemData.discount) / 100
)
updateItemAttribute('discount_type', 'percentage')
}
function syncItemToStore() {
let itemTaxes = props.store[props.storeProp]?.items[props.index]?.taxes
if (!itemTaxes) {
itemTaxes = []
}
let data = {
...props.store[props.storeProp].items[props.index],
index: props.index,
total: total.value,
sub_total: subtotal.value,
totalSimpleTax: totalSimpleTax.value,
totalCompoundTax: totalCompoundTax.value,
totalTax: totalTax.value,
tax: totalTax.value,
taxes: [...itemTaxes],
}
props.store.updateItem(data)
}
function updateItemAttribute(attribute, value) {
props.store.$patch((state) => {
state[props.storeProp].items[props.index][attribute] = value
})
syncItemToStore()
}
</script>

View File

@ -0,0 +1,232 @@
<template>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center text-base" style="flex: 4">
<label class="pr-2 mb-0" align="right">
{{ $t('invoices.item.tax') }}
</label>
<BaseMultiselect
v-model="selectedTax"
value-prop="id"
:options="filteredTypes"
:placeholder="$t('general.select_a_tax')"
open-direction="top"
track-by="name"
searchable
object
label="name"
@update:modelValue="(val) => onSelectTax(val)"
>
<template #singlelabel="{ value }">
<div class="absolute left-3.5">
{{ value.name }} - {{ value.percent }} %
</div>
</template>
<template #option="{ option }">
{{ option.name }} - {{ option.percent }} %
</template>
<template v-if="userStore.hasAbilities(ability)" #action>
<button
type="button"
class="
flex
items-center
justify-center
w-full
px-2
cursor-pointer
py-2
bg-gray-200
border-none
outline-none
"
@click="openTaxModal"
>
<BaseIcon name="CheckCircleIcon" class="h-5 text-primary-400" />
<label
class="ml-2 text-sm leading-none text-primary-400 cursor-pointer"
>{{ $t('invoices.add_new_tax') }}</label
>
</button>
</template>
</BaseMultiselect>
<br />
</div>
<div class="text-sm text-right" style="flex: 3">
<BaseFormatMoney :amount="taxAmount" :currency="currency" />
</div>
<div class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer">
<BaseIcon
v-if="taxes.length && index !== taxes.length - 1"
name="TrashIcon"
class="h-5 text-gray-700 cursor-pointer"
@click="removeTax(index)"
/>
</div>
</div>
</template>
<script setup>
import { computed, ref, inject, reactive, watch } from 'vue'
import { useTaxTypeStore } from '@/scripts/stores/tax-type'
import { useModalStore } from '@/scripts/stores/modal'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/scripts/stores/user'
const props = defineProps({
ability: {
type: String,
default: '',
},
store: {
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
itemIndex: {
type: Number,
required: true,
},
index: {
type: Number,
required: true,
},
taxData: {
type: Object,
required: true,
},
taxes: {
type: Array,
default: [],
},
total: {
type: Number,
default: 0,
},
totalTax: {
type: Number,
default: 0,
},
currency: {
type: [Object, String],
required: true,
},
updateItems: {
type: Function,
default: () => {},
},
})
const emit = defineEmits(['remove', 'update'])
const taxTypeStore = useTaxTypeStore()
const modalStore = useModalStore()
const userStore = useUserStore()
const selectedTax = ref(null)
const localTax = reactive({ ...props.taxData })
const utils = inject('utils')
const { t } = useI18n()
const filteredTypes = computed(() => {
const clonedTypes = taxTypeStore.taxTypes.map((a) => ({ ...a }))
return clonedTypes.map((taxType) => {
let found = props.taxes.find((tax) => tax.tax_type_id === taxType.id)
if (found) {
taxType.disabled = true
} else {
taxType.disabled = false
}
return taxType
})
})
const taxAmount = computed(() => {
if (localTax.compound_tax && props.total) {
return ((props.total + props.totalTax) * localTax.percent) / 100
}
if (props.total && localTax.percent) {
return (props.total * localTax.percent) / 100
}
return 0
})
watch(
() => props.total,
() => {
updateRowTax()
}
)
watch(
() => props.totalTax,
() => {
updateRowTax()
}
)
// Set SelectedTax
if (props.taxData.tax_type_id > 0) {
selectedTax.value = taxTypeStore.taxTypes.find(
(_type) => _type.id === props.taxData.tax_type_id
)
}
updateRowTax()
function onSelectTax(val) {
localTax.percent = val.percent
localTax.tax_type_id = val.id
localTax.compound_tax = val.compound_tax
localTax.name = val.name
updateRowTax()
}
function updateRowTax() {
if (localTax.tax_type_id === 0) {
return
}
emit('update', {
index: props.index,
item: {
...localTax,
amount: taxAmount.value,
},
})
}
function openTaxModal() {
let data = {
itemIndex: props.itemIndex,
taxIndex: props.index,
}
modalStore.openModal({
title: t('settings.tax_types.add_tax'),
componentName: 'TaxTypeModal',
data: data,
size: 'sm',
})
}
function removeTax(index) {
props.store.$patch((state) => {
state[props.storeProp].items[props.itemIndex].taxes.splice(index, 1)
})
}
</script>

View File

@ -0,0 +1,194 @@
<template>
<table class="text-center item-table min-w-full">
<colgroup>
<col style="width: 40%; min-width: 280px" />
<col style="width: 10%; min-width: 120px" />
<col style="width: 15%; min-width: 120px" />
<col
v-if="store[storeProp].discount_per_item === 'YES'"
style="width: 15%; min-width: 160px"
/>
<col style="width: 15%; min-width: 120px" />
</colgroup>
<thead class="bg-white border border-gray-200 border-solid">
<tr>
<th
class="
px-5
py-3
text-sm
not-italic
font-medium
leading-5
text-left text-gray-700
border-t border-b border-gray-200 border-solid
"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<span v-else class="pl-7">
{{ $tc('items.item', 2) }}
</span>
</th>
<th
class="
px-5
py-3
text-sm
not-italic
font-medium
leading-5
text-right text-gray-700
border-t border-b border-gray-200 border-solid
"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<span v-else>
{{ $t('invoices.item.quantity') }}
</span>
</th>
<th
class="
px-5
py-3
text-sm
not-italic
font-medium
leading-5
text-left text-gray-700
border-t border-b border-gray-200 border-solid
"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<span v-else>
{{ $t('invoices.item.price') }}
</span>
</th>
<th
v-if="store[storeProp].discount_per_item === 'YES'"
class="
px-5
py-3
text-sm
not-italic
font-medium
leading-5
text-left text-gray-700
border-t border-b border-gray-200 border-solid
"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<span v-else>
{{ $t('invoices.item.discount') }}
</span>
</th>
<th
class="
px-5
py-3
text-sm
not-italic
font-medium
leading-5
text-right text-gray-700
border-t border-b border-gray-200 border-solid
"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<span v-else class="pr-10 column-heading">
{{ $t('invoices.item.amount') }}
</span>
</th>
</tr>
</thead>
<draggable
v-model="store[storeProp].items"
item-key="id"
tag="tbody"
handle=".handle"
>
<template #item="{ element, index }">
<Item
:key="element.id"
:index="index"
:item-data="element"
:loading="isLoading"
:currency="defaultCurrency"
:item-validation-scope="itemValidationScope"
:invoice-items="store[storeProp].items"
:store="store"
:store-prop="storeProp"
/>
</template>
</draggable>
</table>
<div
class="
flex
items-center
justify-center
w-full
px-6
py-3
text-base
border border-t-0 border-gray-200 border-solid
cursor-pointer
text-primary-400
hover:bg-primary-100
"
@click="store.addItem"
>
<BaseIcon name="PlusCircleIcon" class="mr-2" />
{{ $t('general.add_new_item') }}
</div>
</template>
<script setup>
import { useCompanyStore } from '@/scripts/stores/company'
import { computed } from 'vue'
import draggable from 'vuedraggable'
import Item from './CreateItemRow.vue'
const props = defineProps({
store: {
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
currency: {
type: [Object, String, null],
required: true,
},
isLoading: {
type: Boolean,
default: false,
},
itemValidationScope: {
type: String,
default: '',
},
})
const companyStore = useCompanyStore()
const defaultCurrency = computed(() => {
if (props.currency) {
return props.currency
} else {
return companyStore.selectedCompanyCurrency
}
})
</script>

View File

@ -0,0 +1,46 @@
<template>
<div class="mb-6">
<div
class="z-20 text-sm font-semibold leading-5 text-primary-400 float-right"
>
<SelectNotePopup :type="type" @select="onSelectNote" />
</div>
<label class="text-primary-800 font-medium mb-4 text-sm">
{{ $t('invoices.notes') }}
</label>
<BaseCustomInput
v-model="store[storeProp].notes"
:content-loading="store.isFetchingInitialSettings"
:fields="fields"
class="mt-1"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import SelectNotePopup from '../SelectNotePopup.vue'
const props = defineProps({
store: {
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
fields: {
type: Object,
default: null,
},
type: {
type: String,
default: null,
},
})
function onSelectNote(data) {
props.store[props.storeProp].notes = '' + data.notes
}
</script>

View File

@ -0,0 +1,381 @@
<template>
<div
class="
px-5
py-4
mt-6
bg-white
border border-gray-200 border-solid
rounded
md:min-w-[390px]
min-w-[300px]
lg:mt-7
"
>
<div class="flex items-center justify-between w-full">
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<label
v-else
class="text-sm font-semibold leading-5 text-gray-400 uppercase"
>
{{ $t('estimates.sub_total') }}
</label>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<label
v-else
class="
flex
items-center
justify-center
m-0
text-lg text-black
uppercase
"
>
<BaseFormatMoney
:amount="store.getSubTotal"
:currency="defaultCurrency"
/>
</label>
</div>
<div
v-for="tax in itemWiseTaxes"
:key="tax.tax_type_id"
class="flex items-center justify-between w-full"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<label
v-else-if="store[storeProp].tax_per_item === 'YES'"
class="m-0 text-sm font-semibold leading-5 text-gray-500 uppercase"
>
{{ tax.name }} - {{ tax.percent }}%
</label>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<label
v-else-if="store[storeProp].tax_per_item === 'YES'"
class="
flex
items-center
justify-center
m-0
text-lg text-black
uppercase
"
>
<BaseFormatMoney :amount="tax.amount" :currency="defaultCurrency" />
</label>
</div>
<div
v-if="
store[storeProp].discount_per_item === 'NO' ||
store[storeProp].discount_per_item === null
"
class="flex items-center justify-between w-full mt-2"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<label
v-else
class="text-sm font-semibold leading-5 text-gray-400 uppercase"
>
{{ $t('estimates.discount') }}
</label>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText
:lines="1"
class="w-24 h-8 rounded-md border"
/>
</BaseContentPlaceholders>
<div v-else class="flex" style="width: 140px" role="group">
<BaseInput
v-model="totalDiscount"
class="
border-r-0
focus:border-r-2
rounded-tr-sm rounded-br-sm
h-[38px]
"
/>
<BaseDropdown position="bottom-end">
<template #activator>
<BaseButton
class="rounded-tr-md rounded-br-md p-2 rounded-none"
type="button"
variant="white"
>
<span class="flex items-center">
{{
store[storeProp].discount_type == 'fixed'
? defaultCurrency.symbol
: '%'
}}
<BaseIcon
name="ChevronDownIcon"
class="w-4 h-4 text-gray-500 ml-1"
/>
</span>
</BaseButton>
</template>
<BaseDropdownItem @click="selectFixed">
{{ $t('general.fixed') }}
</BaseDropdownItem>
<BaseDropdownItem @click="selectPercentage">
{{ $t('general.percentage') }}
</BaseDropdownItem>
</BaseDropdown>
</div>
</div>
<div
v-if="
store[storeProp].tax_per_item === 'NO' ||
store[storeProp].tax_per_item === null
"
>
<Tax
v-for="(tax, index) in taxes"
:key="tax.id"
:index="index"
:tax="tax"
:taxes="taxes"
:currency="currency"
:store="store"
@remove="removeTax"
@update="updateTax"
/>
</div>
<div
v-if="
store[storeProp].tax_per_item === 'NO' ||
store[storeProp].tax_per_item === null
"
ref="taxModal"
class="float-right pt-2 pb-4"
>
<SelectTaxPopup
:store-prop="storeProp"
:store="store"
:type="taxPopupType"
@select:taxType="onSelectTax"
/>
</div>
<div
class="
flex
items-center
justify-between
w-full
pt-2
mt-5
border-t border-gray-200 border-solid
"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<label
v-else
class="m-0 text-sm font-semibold leading-5 text-gray-400 uppercase"
>{{ $t('estimates.total') }} {{ $t('estimates.amount') }}:</label
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<label
v-else
class="
flex
items-center
justify-center
text-lg
uppercase
text-primary-400
"
>
<BaseFormatMoney :amount="store.getTotal" :currency="defaultCurrency" />
</label>
</div>
</div>
</template>
<script setup>
import { computed, inject, ref } from 'vue'
import Guid from 'guid'
import Tax from './CreateTotalTaxes.vue'
import TaxStub from '@/scripts/stub/tax'
import SelectTaxPopup from './SelectTaxPopup.vue'
import { useCompanyStore } from '@/scripts/stores/company'
const taxModal = ref(null)
const props = defineProps({
store: {
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
taxPopupType: {
type: String,
default: '',
},
currency: {
type: [Object, String],
default: '',
},
isLoading: {
type: Boolean,
default: false,
},
})
const utils = inject('$utils')
const companyStore = useCompanyStore()
const totalDiscount = computed({
get: () => {
return props.store[props.storeProp].discount
},
set: (newValue) => {
if (props.store[props.storeProp].discount_type === 'percentage') {
props.store[props.storeProp].discount_val = Math.round(
(props.store.getSubTotal * newValue) / 100
)
} else {
props.store[props.storeProp].discount_val = Math.round(newValue * 100)
}
props.store[props.storeProp].discount = newValue
},
})
const taxes = computed({
get: () => props.store[props.storeProp].taxes,
set: (value) => {
props.store.$patch((state) => {
state[props.storeProp].taxes = value
})
},
})
const itemWiseTaxes = computed(() => {
let taxes = []
props.store[props.storeProp].items.forEach((item) => {
if (item.taxes) {
item.taxes.forEach((tax) => {
let found = taxes.find((_tax) => {
return _tax.tax_type_id === tax.tax_type_id
})
if (found) {
found.amount += tax.amount
} else if (tax.tax_type_id) {
taxes.push({
tax_type_id: tax.tax_type_id,
amount: tax.amount,
percent: tax.percent,
name: tax.name,
})
}
})
}
})
return taxes
})
const defaultCurrency = computed(() => {
if (props.currency) {
return props.currency
} else {
return companyStore.selectedCompanyCurrency
}
})
function selectFixed() {
if (props.store[props.storeProp].discount_type === 'fixed') {
return
}
props.store[props.storeProp].discount_val = Math.round(
props.store[props.storeProp].discount * 100
)
props.store[props.storeProp].discount_type = 'fixed'
}
function selectPercentage() {
if (props.store[props.storeProp].discount_type === 'percentage') {
return
}
props.store[props.storeProp].discount_val =
(props.store.getSubTotal * props.store[props.storeProp].discount) / 100
props.store[props.storeProp].discount_type = 'percentage'
}
function onSelectTax(selectedTax) {
let amount = 0
if (selectedTax.compound_tax && props.store.getSubtotalWithDiscount) {
amount = Math.round(
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *
selectedTax.percent) /
100
)
} else if (props.store.getSubtotalWithDiscount && selectedTax.percent) {
amount = Math.round(
(props.store.getSubtotalWithDiscount * selectedTax.percent) / 100
)
}
let data = {
...TaxStub,
id: Guid.raw(),
name: selectedTax.name,
percent: selectedTax.percent,
compound_tax: selectedTax.compound_tax,
tax_type_id: selectedTax.id,
amount,
}
props.store.$patch((state) => {
state[props.storeProp].taxes.push({ ...data })
})
}
function updateTax(data) {
const tax = props.store[props.storeProp].taxes.find(
(tax) => tax.id === data.id
)
if (tax) {
Object.assign(tax, { ...data })
}
}
function removeTax(id) {
const index = props.store[props.storeProp].taxes.findIndex(
(tax) => tax.id === id
)
props.store.$patch((state) => {
state[props.storeProp].taxes.splice(index, 1)
})
}
</script>

View File

@ -0,0 +1,83 @@
<template>
<div class="flex items-center justify-between w-full mt-2 text-sm">
<label class="font-semibold leading-5 text-gray-500 uppercase">
{{ tax.name }} ({{ tax.percent }} %)
</label>
<label class="flex items-center justify-center text-lg text-black">
<BaseFormatMoney :amount="tax.amount" :currency="currency" />
<BaseIcon
name="TrashIcon"
class="h-5 ml-2 cursor-pointer"
@click="$emit('remove', tax.id)"
/>
</label>
</div>
</template>
<script setup>
import { computed, watch, inject, watchEffect } from 'vue'
const props = defineProps({
index: {
type: Number,
required: true,
},
tax: {
type: Object,
required: true,
},
taxes: {
type: Array,
required: true,
},
currency: {
type: [Object, String],
required: true,
},
store: {
type: Object,
default: null,
},
data: {
type: String,
default: '',
},
})
const emit = defineEmits(['update', 'remove'])
const utils = inject('$utils')
const taxAmount = computed(() => {
if (props.tax.compound_tax && props.store.getSubtotalWithDiscount) {
return Math.round(
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *
props.tax.percent) /
100
)
}
if (props.store.getSubtotalWithDiscount && props.tax.percent) {
return Math.round(
(props.store.getSubtotalWithDiscount * props.tax.percent) / 100
)
}
return 0
})
watchEffect(() => {
if (props.store.getSubtotalWithDiscount) {
updateTax()
}
if (props.store.getTotalSimpleTax) {
updateTax()
}
})
function updateTax() {
emit('update', {
...props.tax,
amount: taxAmount.value,
})
}
</script>

View File

@ -0,0 +1,173 @@
<template>
<BaseInputGroup
v-if="store.showExchangeRate && selectedCurrency"
:content-loading="isFetching && !isEdit"
:label="$t('settings.exchange_rate.exchange_rate')"
:error="v.exchange_rate.$error && v.exchange_rate.$errors[0].$message"
required
>
<template #labelRight>
<div v-if="hasActiveProvider && isEdit">
<BaseIcon
v-tooltip="{ content: 'Fetch Latest Exchange rate' }"
name="RefreshIcon"
:class="`h-4 w-4 text-primary-500 cursor-pointer outline-none ${
isFetching
? ' animate-spin transform rotate-180 cursor-not-allowed pointer-events-none '
: ''
}`"
@click="getCurrenctExchangeRate(customerCurrency)"
/>
</div>
</template>
<BaseInput
v-model="store[storeProp].exchange_rate"
:content-loading="isFetching && !isEdit"
:addon="`1 ${selectedCurrency.code} =`"
:disabled="isFetching"
@input="v.exchange_rate.$touch()"
>
<template #right>
<span class="text-gray-500 sm:text-sm">
{{ companyCurrency.code }}
</span>
</template>
</BaseInput>
<span class="text-gray-400 text-xs mt-2 font-light">
{{
$t('settings.exchange_rate.exchange_help_text', {
currency: selectedCurrency.code,
baseCurrency: companyCurrency.code,
})
}}
</span>
</BaseInputGroup>
</template>
<script setup>
import { watch, computed, ref, onMounted } from 'vue'
import { useGlobalStore } from '@/scripts/stores/global'
import { useCompanyStore } from '@/scripts/stores/company'
import { useExchangeRateStore } from '@/scripts/stores/exchange-rate'
const props = defineProps({
v: {
type: Object,
default: null,
},
isLoading: {
type: Boolean,
default: false,
},
store: {
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
isEdit: {
type: Boolean,
default: false,
},
customerCurrency: {
type: [String, Number],
default: null,
},
})
const globalStore = useGlobalStore()
const companyStore = useCompanyStore()
const exchangeRateStore = useExchangeRateStore()
const hasActiveProvider = ref(false)
let isFetching = ref(false)
globalStore.fetchCurrencies()
const companyCurrency = computed(() => {
return companyStore.selectedCompanyCurrency
})
const selectedCurrency = computed(() => {
return globalStore.currencies.find(
(c) => c.id === props.store[props.storeProp].currency_id
)
})
const isCurrencyDiffrent = computed(() => {
return companyCurrency.value.id !== props.customerCurrency
})
watch(
() => props.store[props.storeProp].customer,
(v) => {
setCustomerCurrency(v)
},
{ deep: true }
)
watch(
() => props.store[props.storeProp].currency_id,
(v) => {
onChangeCurrency(v)
}
)
watch(
() => props.customerCurrency,
(v) => {
if (v && props.isEdit) {
checkForActiveProvider(v)
}
},
{ immediate: true }
)
function checkForActiveProvider() {
if (isCurrencyDiffrent.value) {
exchangeRateStore
.checkForActiveProvider(props.customerCurrency)
.then((res) => {
if (res.data.success) {
hasActiveProvider.value = true
}
})
}
}
function setCustomerCurrency(v) {
if (v) {
props.store[props.storeProp].currency_id = v.currency.id
} else {
props.store[props.storeProp].currency_id = companyCurrency.value.id
}
}
async function onChangeCurrency(v) {
if (v !== companyCurrency.value.id) {
if (!props.isEdit) {
await getCurrenctExchangeRate(v)
}
props.store.showExchangeRate = true
} else {
props.store.showExchangeRate = false
}
}
function getCurrenctExchangeRate(v) {
isFetching.value = true
exchangeRateStore
.getCurrentExchangeRate(v)
.then((res) => {
if (res.data && !res.data.error) {
props.store[props.storeProp].exchange_rate = res.data.exchangeRate[0]
} else {
props.store[props.storeProp].exchange_rate = ''
}
isFetching.value = false
})
.catch((err) => {
isFetching.value = false
})
}
</script>

View File

@ -0,0 +1,231 @@
<template>
<div class="w-full mt-4 tax-select">
<Popover v-slot="{ isOpen }" class="relative">
<PopoverButton
:class="isOpen ? '' : 'text-opacity-90'"
class="
flex
items-center
text-sm
font-medium
text-primary-400
focus:outline-none focus:border-none
"
>
<BaseIcon
name="PlusIcon"
class="w-4 h-4 font-medium text-primary-400"
/>
{{ $t('settings.tax_types.add_tax') }}
</PopoverButton>
<!-- Tax Select Popup -->
<div class="relative w-full max-w-md px-4">
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-1 opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="translate-y-1 opacity-0"
>
<PopoverPanel
v-slot="{ close }"
style="min-width: 350px; margin-left: 62px; top: -28px"
class="absolute z-10 px-4 py-2 transform -translate-x-full sm:px-0"
>
<div
class="
overflow-hidden
rounded-md
shadow-lg
ring-1 ring-black ring-opacity-5
"
>
<!-- Tax Search Input -->
<div class="relative bg-white">
<div class="relative p-4">
<BaseInput
v-model="textSearch"
:placeholder="$t('general.search')"
type="text"
class="text-black"
>
</BaseInput>
</div>
<!-- List of Taxes -->
<div
v-if="filteredTaxType.length > 0"
class="
relative
flex flex-col
overflow-auto
list
max-h-36
border-t border-gray-200
"
>
<div
v-for="(taxType, index) in filteredTaxType"
:key="index"
:class="{
'bg-gray-100 cursor-not-allowed opacity-50 pointer-events-none':
taxes.find((val) => {
return val.tax_type_id === taxType.id
}),
}"
tabindex="2"
class="
px-6
py-4
border-b border-gray-200 border-solid
cursor-pointer
hover:bg-gray-100 hover:cursor-pointer
last:border-b-0
"
@click="selectTaxType(taxType, close)"
>
<div class="flex justify-between px-2">
<label
class="
m-0
text-base
font-semibold
leading-tight
text-gray-700
cursor-pointer
"
>
{{ taxType.name }}
</label>
<label
class="
m-0
text-base
font-semibold
text-gray-700
cursor-pointer
"
>
{{ taxType.percent }} %
</label>
</div>
</div>
</div>
<div v-else class="flex justify-center p-5 text-gray-400">
<label class="text-base text-gray-500 cursor-pointer">
{{ $t('general.no_tax_found') }}
</label>
</div>
</div>
<!-- Add new Tax action -->
<button
v-if="userStore.hasAbilities(abilities.CREATE_TAX_TYPE)"
type="button"
class="
flex
items-center
justify-center
w-full
h-10
px-2
py-3
bg-gray-200
border-none
outline-none
"
@click="openTaxTypeModal"
>
<BaseIcon name="CheckCircleIcon" class="text-primary-400" />
<label
class="
m-0
ml-3
text-sm
leading-none
cursor-pointer
font-base
text-primary-400
"
>
{{ $t('estimates.add_new_tax') }}
</label>
</button>
</div>
</PopoverPanel>
</transition>
</div>
</Popover>
</div>
</template>
<script setup>
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
import { computed, ref, inject, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useEstimateStore } from '@/scripts/stores/estimate'
import { useInvoiceStore } from '@/scripts/stores/invoice'
import { useModalStore } from '@/scripts/stores/modal'
import { useTaxTypeStore } from '@/scripts/stores/tax-type'
import { useUserStore } from '@/scripts/stores/user'
import abilities from '@/scripts/stub/abilities'
const props = defineProps({
type: {
type: String,
default: null,
},
store: {
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
})
const emit = defineEmits(['select:taxType'])
const modalStore = useModalStore()
const taxTypeStore = useTaxTypeStore()
const userStore = useUserStore()
const { t } = useI18n()
const textSearch = ref(null)
const filteredTaxType = computed(() => {
if (textSearch.value) {
return taxTypeStore.taxTypes.filter(function (el) {
return (
el.name.toLowerCase().indexOf(textSearch.value.toLowerCase()) !== -1
)
})
} else {
return taxTypeStore.taxTypes
}
})
const taxes = computed(() => {
return props.store[props.storeProp].taxes
})
function selectTaxType(data, close) {
emit('select:taxType', { ...data })
close()
}
function openTaxTypeModal() {
modalStore.openModal({
title: t('settings.tax_types.add_tax'),
componentName: 'TaxTypeModal',
size: 'sm',
refreshData: (data) => emit('select:taxType', data),
})
}
</script>

View File

@ -0,0 +1,51 @@
<template>
<div>
<label class="flex text-primary-800 font-medium text-sm mb-2">
{{ $t('general.select_template') }}
<span class="text-sm text-red-500"> *</span>
</label>
<BaseButton
type="button"
class="flex justify-center w-full text-sm lg:w-auto hover:bg-gray-200"
variant="gray"
@click="openTemplateModal"
>
<template #right="slotProps">
<BaseIcon name="PencilIcon" :class="slotProps.class" />
</template>
{{ store[storeProp].template_name }}
</BaseButton>
</div>
</template>
<script setup>
import { useModalStore } from '@/scripts/stores/modal'
import { useI18n } from 'vue-i18n'
const props = defineProps({
store: {
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
})
const modalStore = useModalStore()
const { t } = useI18n()
function openTemplateModal() {
modalStore.openModal({
title: t('general.choose_template'),
componentName: 'SelectTemplate',
data: {
templates: props.store.templates,
store: props.store,
storeProp: props.storeProp,
},
})
}
</script>