mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-27 19:51:09 -04:00
v5.0.0 update
This commit is contained in:
@ -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>
|
||||
Reference in New Issue
Block a user